dsma-library/tools/md5_to_dsma.py
2024-06-02 12:08:21 +01:00

863 lines
31 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2022 Antonio Niño Díaz <antonio_nd@outlook.com>
import os
from collections import namedtuple
from math import sqrt
from display_list import DisplayList, float_to_f32
class MD5FormatError(Exception):
pass
VALID_TEXTURE_SIZES = [8, 16, 32, 64, 128, 256, 512, 1024]
def is_valid_texture_size(size):
return size in VALID_TEXTURE_SIZES
def assert_num_args(cmd, real, expected, tokens):
if real != expected:
raise MD5FormatError(f"Unexpected nargs for '{cmd}' ({real} != {expected}): {tokens}")
class Quaternion():
def __init__(self, w, x, y, z):
self.w = w
self.x = x
self.y = y
self.z = z
def to_v3(self):
return Vector(self.x, self.y, self.z)
def complement(self):
return Quaternion(self.w, -self.x, -self.y, -self.z)
def normalize(self):
mag = sqrt((self.w ** 2) + (self.x ** 2) + (self.y ** 2) + (self.z ** 2))
return Quaternion(self.w / mag, self.x /mag, self.y / mag, self.z / mag)
def mul(self, other):
w = (self.w * other.w) - (self.x * other.x) - (self.y * other.y) - (self.z * other.z)
x = (self.x * other.w) + (self.w * other.x) + (self.y * other.z) - (self.z * other.y)
y = (self.y * other.w) + (self.w * other.y) + (self.z * other.x) - (self.x * other.z)
z = (self.z * other.w) + (self.w * other.z) + (self.x * other.y) - (self.y * other.x)
return Quaternion(w, x, y, z)
def quaternion_fill_incomplete_w(v):
"""
This expands an incomplete quaternion, not a regular vector. This is
needed if a quaternion is stored as the components x, y and z and it is
expected that the code will fill the value of w.
"""
t = 1.0 - (v[0] * v[0]) - (v[1] * v[1]) - (v[2] * v[2])
if t < 0:
w = 0
else:
w = -sqrt(t)
return Quaternion(w, v[0], v[1], v[2])
class Vector():
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def to_q(self):
return Quaternion(0, self.x, self.y, self.z)
def length(self):
return sqrt((self.x ** 2) + (self.y ** 2) + (self.z ** 2))
def normalize(self):
mag = self.length()
return Vector(self.x / mag, self.y / mag, self.z / mag)
def add(self, other):
return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
def sub(self, other):
return Vector(self.x - other.x, self.y - other.y, self.z - other.z)
def cross(self, other):
x = (self.y * other.z) - (other.y * self.z)
y = (self.z * other.x) - (other.z * self.x)
z = (self.x * other.y) - (other.x * self.y)
return Vector(x, y, z)
def mul_m4x3(self, m):
x = (self.x * m[0][0]) + (self.y * m[0][1]) + (self.z * m[0][2]) + (m[0][3] * 1)
y = (self.x * m[1][0]) + (self.y * m[1][1]) + (self.z * m[1][2]) + (m[1][3] * 1)
z = (self.x * m[2][0]) + (self.y * m[2][1]) + (self.z * m[2][2]) + (m[2][3] * 1)
return Vector(x, y, z)
def joint_info_to_m4x3(q, trans):
"""
This generates a 4x3 matrix that represents a rotation and a translation.
q is a Quaternion with a orientation, trans is a Vector with a translation.
"""
wx = 2 * q.w * q.x
wy = 2 * q.w * q.y
wz = 2 * q.w * q.z
x2 = 2 * q.x * q.x
xy = 2 * q.x * q.y
xz = 2 * q.x * q.z
y2 = 2 * q.y * q.y
yz = 2 * q.y * q.z
z2 = 2 * q.z * q.z
return [[1 - y2 - z2, xy - wz, xz + wy, trans.x],
[ xy + wz, 1 - x2 - z2, yz - wx, trans.y],
[ xz - wy, yz + wx, 1 - x2 - y2, trans.z]]
def parse_md5mesh(input_file):
Joint = namedtuple("Joint", "name parent pos orient")
Vert = namedtuple("Vert", "st startWeight countWeight")
Weight = namedtuple("Weight", "joint bias pos")
Mesh = namedtuple("Mesh", "numverts verts numtris tris numweights weights")
joints = []
meshes = []
with open(input_file, 'r') as md5mesh_file:
numJoints = None
numMeshes = None
# This can have three values:
# - "root": Parsing commands in the md5mesh outside of any group
# - "joints": Inside a "joints" node.
# - "mesh": Inside a "mesh" node.
mode = "root"
# Temporary variables used to store mesh information before packing it
numverts = None
verts = None
numtris = None
tris = None
numweights = None
weights = None
for line in md5mesh_file:
# Remove comments
line = line.split('//')[0]
# Parse line
tokens = line.split()
if len(tokens) == 0: # Empty line
continue
cmd = tokens[0]
tokens = tokens[1:]
nargs = len(tokens)
if mode == "root":
if cmd == 'MD5Version':
assert_num_args('MD5Version', nargs, 1, tokens)
version = int(tokens[0])
if version != 10:
raise MD5FormatError(f"Invalid 'MD5Version': {version} != 10")
elif cmd == 'commandline':
# Ignore this
pass
elif cmd == 'numJoints':
assert_num_args('numJoints', nargs, 1, tokens)
numJoints = int(tokens[0])
if numJoints == 0:
raise MD5FormatError(f"'numJoints' is 0")
elif cmd == 'numMeshes':
assert_num_args('numMeshes', nargs, 1, tokens)
numMeshes = int(tokens[0])
if numMeshes == 0:
raise MD5FormatError(f"'numMeshes' is 0")
elif cmd == 'joints':
assert_num_args('joints', nargs, 1, tokens)
if tokens[0] != '{':
raise MD5FormatError(f"Unexpected token for 'joints': {tokens}")
if numJoints is None:
raise MD5FormatError("'joints' command before 'numJoints'")
mode = "joints"
elif cmd == 'mesh':
assert_num_args('mesh', nargs, 1, tokens)
if tokens[0] != '{':
raise MD5FormatError(f"Unexpected token for 'mesh': {tokens}")
if numMeshes is None:
raise MD5FormatError("'mesh' command before 'numMeshes'")
mode = "mesh"
else:
print(f"Ignored unsupported command: {cmd} {tokens}")
elif mode == "joints":
if cmd == '}':
if nargs > 0:
raise MD5FormatError(f"Unexpected tokens after 'joints {{}}': {tokens}")
mode = "root"
else:
_, name, line = line.split('"')
tokens = line.strip().split(" ")
nargs = len(tokens)
assert_num_args('joint entry', nargs, 11, tokens)
parent = int(tokens[0])
if tokens[1] != '(':
raise MD5FormatError(f"Unexpected token 1 for joint': {tokens}")
pos = Vector(float(tokens[2]), float(tokens[3]), float(tokens[4]))
if tokens[5] != ')':
raise MD5FormatError(f"Unexpected token 5 for joint': {tokens}")
if tokens[6] != '(':
raise MD5FormatError(f"Unexpected token 6 for joint': {tokens}")
orient = (float(tokens[7]), float(tokens[8]), float(tokens[9]))
q_orient = quaternion_fill_incomplete_w(orient)
if tokens[10] != ')':
raise MD5FormatError(f"Unexpected token 10 for joint': {tokens}")
joints.append(Joint(name, parent, pos, q_orient))
elif mode == "mesh":
if cmd == '}':
if nargs != 0:
raise MD5FormatError(f"Unexpected tokens after 'mesh {{}}': {tokens}")
mode = "root"
meshes.append(Mesh(numverts, verts, numtris, tris, numweights, weights))
numverts = None
verts = None
numtris = None
tris = None
numweights = None
weights = None
elif cmd == 'shader':
# Ignore this
pass
elif cmd == 'numverts':
assert_num_args('numverts', nargs, 1, tokens)
numverts = int(tokens[0])
verts = [None] * numverts
elif cmd == 'vert':
assert_num_args('vert', nargs, 7, tokens)
if numverts is None:
raise MD5FormatError("'vert' command before 'numverts'")
index = int(tokens[0])
if tokens[1] != '(':
raise MD5FormatError(f"Unexpected token 1 for vert': {tokens}")
st = (float(tokens[2]), float(tokens[3]))
if tokens[4] != ')':
raise MD5FormatError(f"Unexpected token 4 for vert': {tokens}")
startWeight = int(tokens[5])
countWeight = int(tokens[6])
if countWeight != 1:
raise MD5FormatError(
f"Vertex with {countWeight} weights detected, but this tool "
"only supports vertices with one weight. Ensure that all your "
"vertices are assigned exactly one weight with a bias of 1.0."
)
verts[index] = Vert(st, startWeight, countWeight)
elif cmd == 'numtris':
assert_num_args('numtris', nargs, 1, tokens)
numtris = int(tokens[0])
tris = [None] * numtris
elif cmd == 'tri':
assert_num_args('tri', nargs, 4, tokens)
if numtris is None:
raise MD5FormatError("'tri' command before 'numtris'")
index = int(tokens[0])
# Reverse order so that they face the right direction
vertIndices = (int(tokens[3]), int(tokens[2]), int(tokens[1]))
tris[index] = vertIndices
elif cmd == 'numweights':
assert_num_args('numweights', nargs, 1, tokens)
numweights = int(tokens[0])
weights = [None] * numweights
elif cmd == 'weight':
assert_num_args('weight', nargs, 8, tokens)
if numverts is None:
raise MD5FormatError("'weight' command before 'numweights'")
index = int(tokens[0])
jointIndex = int(tokens[1])
bias = float(tokens[2])
if bias != 1.0:
raise MD5FormatError(
f"Weight with bias {bias} detected, but this tool only"
"supports weights with bias equal to 1.0. Ensure that all"
"your vertices are assigned exactly one weight with a"
"bias of 1.0."
)
if tokens[3] != '(':
raise MD5FormatError(f"Unexpected token 3 for weight': {tokens}")
pos = Vector(float(tokens[4]), float(tokens[5]), float(tokens[6]))
if tokens[7] != ')':
raise MD5FormatError(f"Unexpected token 7 for weight': {tokens}")
weights[index] = Weight(jointIndex, bias, pos)
else:
print(f"Ignored unsupported command: {cmd} {tokens}")
if mode != "root":
raise MD5FormatError("Unexpected end of file (expected '}')")
realJoints = len(joints)
if numJoints != realJoints:
raise MD5FormatError(f"Incorrect number of joints: {numJoints} != {realJoints}")
realMeshes = len(meshes)
if numJoints != realJoints:
raise MD5FormatError(f"Incorrect number of joints: {numJoints} != {realJoints}")
return (joints, meshes)
def parse_md5anim(input_file):
Joint = namedtuple("Joint", "name parent pos orient")
joints = []
frames = []
with open(input_file, 'r') as md5anim_file:
numFrames = None
numJoints = None
baseframe = []
hierarchy = []
# This can have three values:
# - "root": Parsing commands in the md5mesh outside of any group
# - "hierarchy": Inside a "hierarchy" node.
# - "bounds": Inside a "bounds" node.
# - "baseframe": Inside a "baseframe" node.
# - "frame": Inside a "frame" node.
mode = "root"
frame_index = None
for line in md5anim_file:
# Remove comments
line = line.split('//')[0]
# Parse line
tokens = line.split()
if len(tokens) == 0: # Empty line
continue
cmd = tokens[0]
tokens = tokens[1:]
nargs = len(tokens)
if mode == "root":
if cmd == 'MD5Version':
assert_num_args('MD5Version', nargs, 1, tokens)
version = int(tokens[0])
if version != 10:
raise MD5FormatError(f"Invalid 'MD5Version': {version} != 10")
elif cmd == 'commandline':
# Ignore this
pass
elif cmd == 'numFrames':
assert_num_args('numFrames', nargs, 1, tokens)
numFrames = int(tokens[0])
if numFrames == 0:
raise MD5FormatError(f"'numFrames' is 0")
frames = [None] * numFrames
elif cmd == 'numJoints':
assert_num_args('numJoints', nargs, 1, tokens)
numJoints = int(tokens[0])
if numJoints == 0:
raise MD5FormatError(f"'numJoints' is 0")
elif cmd == 'frameRate':
# Ignore this
pass
elif cmd == 'numAnimatedComponents':
# Ignore this
pass
elif cmd == 'hierarchy':
assert_num_args('hierarchy', nargs, 1, tokens)
if tokens[0] != '{':
raise MD5FormatError(f"Unexpected token for 'hierarchy': {tokens}")
mode = "hierarchy"
elif cmd == 'bounds':
assert_num_args('bounds', nargs, 1, tokens)
if tokens[0] != '{':
raise MD5FormatError(f"Unexpected token for 'bounds': {tokens}")
mode = "bounds"
elif cmd == 'baseframe':
assert_num_args('baseframe', nargs, 1, tokens)
if tokens[0] != '{':
raise MD5FormatError(f"Unexpected token for 'baseframe': {tokens}")
mode = "baseframe"
elif cmd == 'frame':
assert_num_args('frame', nargs, 2, tokens)
frame_index = int(tokens[0])
if tokens[1] != '{':
raise MD5FormatError(f"Unexpected token for 'frame': {tokens}")
if numFrames is None:
raise MD5FormatError("'frame' command before 'numFrames'")
mode = "frame"
joints = []
else:
print(f"Ignored unsupported command: {cmd} {tokens}")
elif mode == "hierarchy":
if cmd == '}':
if nargs > 0:
raise MD5FormatError(f"Unexpected tokens after 'hierarchy {{}}': {tokens}")
mode = "root"
else:
_, name, line = line.split('"')
tokens = line.strip().split(" ")
nargs = len(tokens)
assert_num_args('hierarchy entry', nargs, 3, tokens)
parent_index = int(tokens[0])
flags = int(tokens[1])
if flags != 63:
raise MD5FormatError(f"Unexpected flags in hierarchy: {flags}")
frame_data_index = int(tokens[2])
hierarchy.append(parent_index)
elif mode == "bounds":
if cmd == '}':
if nargs > 0:
raise MD5FormatError(f"Unexpected tokens after 'bounds {{}}': {tokens}")
mode = "root"
else:
# Ignore everything else
pass
elif mode == "baseframe":
if cmd == '}':
if nargs > 0:
raise MD5FormatError(f"Unexpected tokens after 'baseframe {{}}': {tokens}")
mode = "root"
else:
values = line.strip().split()
assert_num_args('baseframe joint', len(values), 10, values)
if values[0] != '(':
raise MD5FormatError(f"Unexpected token 0 for baseframe': {values}")
pos = Vector(float(values[1]), float(values[2]), float(values[3]))
if values[4] != ')':
raise MD5FormatError(f"Unexpected token 4 for baseframe': {values}")
if values[5] != '(':
raise MD5FormatError(f"Unexpected token 5 for baseframe': {values}")
orient = (float(values[6]), float(values[7]), float(values[8]))
q_orient = quaternion_fill_incomplete_w(orient)
if values[9] != ')':
raise MD5FormatError(f"Unexpected token 9 for baseframe': {values}")
baseframe.append(Joint("", -1, pos, q_orient))
elif mode == "frame":
if cmd == '}':
if nargs > 0:
raise MD5FormatError(f"Unexpected tokens after 'frame {{}}': {tokens}")
mode = "root"
# Now that the frame has been read, process the real
# positions and orientations of the bones before storing
# them.
transformed_joints = []
for joint, parent_index in zip(joints, hierarchy):
if parent_index == -1:
# Root bone
transformed_joints.append(joint)
else:
parent_pos = transformed_joints[parent_index].pos
parent_orient = transformed_joints[parent_index].orient
this_pos = joint.pos
this_orient = joint.orient
q = parent_orient
qt = q.complement()
q_pos_delta = q.mul(this_pos.to_q()).mul(qt)
pos_delta = q_pos_delta.to_v3()
pos = parent_pos.add(pos_delta)
orient = parent_orient.mul(this_orient).normalize()
transformed_joints.append(Joint("", -1, pos, orient))
frames[frame_index] = transformed_joints
else:
values = line.strip().split()
assert_num_args('frame joint', len(values), 6, values)
pos = Vector(float(values[0]), float(values[1]), float(values[2]))
orient = (float(values[3]), float(values[4]), float(values[5]))
q_orient = quaternion_fill_incomplete_w(orient)
joints.append(Joint("", -1, pos, q_orient))
if mode != "root":
raise MD5FormatError("Unexpected end of file (expected '}')")
realJoints = len(joints)
if numJoints != realJoints:
raise MD5FormatError(f"Incorrect number of joints: {numJoints} != {realJoints}")
realFrames = len(frames)
if numFrames != realFrames:
raise MD5FormatError(f"Incorrect number of frames: {numFrames} != {realFrames}")
return frames
def save_animation(frames, output_file, blender_fix):
version = 1
num_frames = len(frames)
num_bones = len(frames[0])
u32_array = [version, num_frames, num_bones]
for joints in frames:
if num_bones != len(joints):
raise MD5FormatError("Different number of bones across frames")
for joint in joints:
this_pos = joint.pos
this_orient = joint.orient
if blender_fix:
# It is needed to rotate all bones because all bones have
# absolute transformations. Rotate orientation and position by
# -90 degrees on the X axis.
q_rot = Quaternion(0.7071068, -0.7071068, 0, 0)
this_orient = q_rot.mul(this_orient)
this_pos = Vector(this_pos.x, this_pos.z, -this_pos.y)
pos = [float_to_f32(this_pos.x), float_to_f32(this_pos.y),
float_to_f32(this_pos.z)]
orient = [float_to_f32(this_orient.w), float_to_f32(this_orient.x),
float_to_f32(this_orient.y), float_to_f32(this_orient.z)]
u32_array.extend(pos)
u32_array.extend(orient)
with open(output_file, "wb") as f:
for u32 in u32_array:
b = [u32 & 0xFF, \
(u32 >> 8) & 0xFF, \
(u32 >> 16) & 0xFF, \
(u32 >> 24) & 0xFF]
f.write(bytearray(b))
def convert_md5mesh(model_file, name, output_folder, texture_size,
draw_normal_polygons, extension_mesh, extension_anim,
blender_fix, export_base_pose):
print(f"Converting model: {model_file}")
# Parse md5mesh file
joints, meshes = parse_md5mesh(model_file)
print(f"Loaded {len(joints)} joint(s) and {len(meshes)} mesh(es).")
if len(meshes) > 1:
print("WARNING: More than one mesh found. All meshes will share the same "
"texture. If you want them to have different textures, you must use "
"multiple .md5mesh files.")
if export_base_pose:
print("Converting base pose...")
save_animation([joints],
os.path.join(output_folder, f"{name}{extension_anim}"),
blender_fix)
print("Converting meshes...")
# Display list shared between all meshes
dl = DisplayList()
dl.switch_vtxs("triangles")
base_matrix = 30 - len(joints) + 1
last_joint_index = None
for mesh in meshes:
print(f" Vertices: {mesh.numverts}")
print(f" Tris: {mesh.numtris}")
print(f" Weights: {mesh.numweights}")
print(" Generating per-triangle normals...")
tri_normal = []
for tri in mesh.tris:
verts = [mesh.verts[i] for i in tri]
weights = [mesh.weights[v.startWeight] for v in verts]
vtx = []
for vert, weight in zip(verts, weights):
joint = joints[weight.joint]
m = joint_info_to_m4x3(joint.orient, joint.pos)
final = weight.pos.mul_m4x3(m)
vtx.append(final)
a = vtx[0].sub(vtx[1])
b = vtx[1].sub(vtx[2])
n = a.cross(b)
if n.length() > 0:
n = n.normalize()
tri_normal.append(n)
else:
tri_normal.append(Vector(0, 0, 0))
print(" Generating display list...")
for tri, norm in zip(mesh.tris, tri_normal):
verts = [mesh.verts[i] for i in tri]
weights = [mesh.weights[v.startWeight] for v in verts]
finals = []
for vert, weight in zip(verts, weights):
# Texture
# -------
st = vert.st
# In the MD5 format (0, 0) is the top-left corner, same as what
# the GPU of the DS expects.
u = st[0] * texture_size[0]
v = st[1] * texture_size[1]
dl.texcoord(u, v)
# Vertex and normal
# -----------------
# Load joint matrix. When drawing normal polygons it has to be
# loaded every time, because drawing the normal restores the
# original matrix.
joint_index = weight.joint
if draw_normal_polygons or joint_index != last_joint_index:
dl.mtx_restore(base_matrix + joint_index)
last_joint_index = joint_index
# Calculate normal in joint space
joint = joints[joint_index]
q = joint.orient
qt = q.complement()
n = norm.to_q()
# Transform by the inverted quaternion
n = qt.mul(n).mul(q).to_v3()
if n.length() > 0:
n = n.normalize()
dl.normal(n.x, n.y, n.z)
# The vertex is already in joint space
dl.vtx(weight.pos.x, weight.pos.y, weight.pos.z)
if draw_normal_polygons:
# Calculate actual location of the vertex so that the
# vertices of the triangle can be averaged as origin of the
# normal polygon.
q = joint.orient
qt = q.complement()
v = weight.pos.to_q()
delta = q.mul(v).mul(qt).to_v3()
final = joint.pos.add(delta)
finals.append(final)
if draw_normal_polygons:
# Don't use any of the joint transformation matrices
dl.mtx_restore(1)
vert_avg = Vector(
(finals[0].x + finals[1].x + finals[2].x) / 3,
(finals[0].y + finals[1].y + finals[2].y) / 3,
(finals[0].z + finals[1].z + finals[2].z) / 3
)
vert_avg_end = vert_avg.add(norm)
dl.texcoord(0, 0)
dl.color(1, 0, 0)
dl.vtx(vert_avg.x + 0.1, vert_avg.y, vert_avg.z)
dl.vtx(vert_avg.x, vert_avg.y, vert_avg.z)
dl.color(0, 1, 0)
dl.vtx(vert_avg_end.x, vert_avg_end.y, vert_avg_end.z)
dl.color(1, 0, 0)
dl.vtx(vert_avg.x, vert_avg.y, vert_avg.z)
dl.vtx(vert_avg.x, vert_avg.y + 0.1, vert_avg.z)
dl.color(0, 1, 0)
dl.vtx(vert_avg_end.x, vert_avg_end.y, vert_avg_end.z)
dl.color(1, 0, 0)
dl.vtx(vert_avg.x, vert_avg.y, vert_avg.z)
dl.vtx(vert_avg.x, vert_avg.y, vert_avg.z + 0.1)
dl.color(0, 1, 0)
dl.vtx(vert_avg_end.x, vert_avg_end.y, vert_avg_end.z)
dl.end_vtxs()
dl.finalize()
dl.save_to_file(os.path.join(output_folder, f"{name}{extension_mesh}"))
def convert_md5anim(name, output_folder, anim_file, skip_frames, extension_anim,
blender_fix):
print(f"Converting animation: {anim_file}")
frames = parse_md5anim(anim_file)
# Create name of animation based on file name
file_basename = os.path.basename(anim_file).replace(".md5anim", "")
anim_name = file_basename.replace(".", "_").lower()
frames = frames[::skip_frames+1]
save_animation(frames, os.path.join(output_folder,
f"{name}_{anim_name}{extension_anim}"), blender_fix)
if __name__ == "__main__":
import argparse
import sys
import traceback
print("md5_to_dsma v0.1.1")
print("Copyright (c) 2022-2024 Antonio Niño Díaz <antonio_nd@outlook.com>")
print("All rights reserved")
print("")
parser = argparse.ArgumentParser(
description='Converts md5mesh and md5anim files into DSM and DSA files.')
# Required arguments
parser.add_argument("--name", required=True,
help="model name to be used in output files")
parser.add_argument("--output", required=True,
help="output folder")
# Optional arguments
parser.add_argument("--model", required=False, type=str, default=None,
help="input md5mesh file")
parser.add_argument("--texture", required=False, type=int, default=[],
nargs="+", action="extend",
help="texture width and height (e.g. '--texture 32 64')")
parser.add_argument("--anims", required=False, type=str, default=[],
nargs="+", action="extend",
help="list of md5anim files to convert")
parser.add_argument("--bin", required=False,
action='store_true',
help="add '.bin' to the name of the output files")
parser.add_argument("--blender-fix", required=False,
action='store_true',
help="rotate model -90 degrees on X axis to match Blender's orientation")
parser.add_argument("--export-base-pose", required=False,
action='store_true',
help="export base pose of a md5mesh as a DSA file")
parser.add_argument("--skip-frames", required=False,
default=0, type=int,
help="number of frames to skip in an animation (0 = export all, 1 = export half, 2 = export 33%, etc)")
parser.add_argument("--draw-normal-polygons", required=False,
action='store_true',
help="draw polygons with the shape of normals for debugging")
args = parser.parse_args()
if args.model is not None:
if len(args.texture) != 2:
print("Please, provide exactly 2 values to the --texture argument")
sys.exit(1)
if not is_valid_texture_size(args.texture[0]):
print(f"Invalid texture width. Valid values: {VALID_TEXTURE_SIZES}")
sys.exit(1)
if not is_valid_texture_size(args.texture[1]):
print(f"Invalid texture height. Valid values: {VALID_TEXTURE_SIZES}")
sys.exit(1)
# Create output directory if it doesn't exist
os.makedirs(args.output, exist_ok=True)
# Add '.bin' to the name of the files if requested
extension_mesh = "_dsm.bin" if args.bin else ".dsm"
extension_anim = "_dsa.bin" if args.bin else ".dsa"
try:
if args.model is not None:
convert_md5mesh(args.model, args.name, args.output, args.texture,
args.draw_normal_polygons, extension_mesh,
extension_anim, args.blender_fix,
args.export_base_pose)
for anim_file in args.anims:
convert_md5anim(args.name, args.output, anim_file, args.skip_frames,
extension_anim, args.blender_fix)
except BaseException as e:
print("ERROR: " + str(e))
traceback.print_exc()
sys.exit(1)
except MD5FormatError as e:
print("ERROR: Invalid MD5 file: " + str(e))
traceback.print_exc()
sys.exit(1)
print("Done!")
sys.exit(0)