486 lines
20 KiB
Python
486 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
"""Convert Ogre3D .mesh.xml + .skeleton.xml to glTF 2.0 (.glb) with all animations."""
|
|
|
|
import xml.etree.ElementTree as ET
|
|
import json
|
|
import struct
|
|
import math
|
|
import os
|
|
|
|
# ── Math ────────────────────────────────────────────────────────────────────
|
|
|
|
def axis_angle_to_quat(ax, ay, az, angle):
|
|
s = math.sin(angle / 2)
|
|
return (ax * s, ay * s, az * s, math.cos(angle / 2))
|
|
|
|
def mat4_from_trs(tx, ty, tz, qx, qy, qz, qw, sx, sy, sz):
|
|
x, y, z, w = qx, qy, qz, qw
|
|
xx, yy, zz = x*x, y*y, z*z
|
|
xy, xz, yz = x*y, x*z, y*z
|
|
wx, wy, wz = w*x, w*y, w*z
|
|
return [
|
|
(1 - 2*(yy + zz))*sx, (2*(xy + wz))*sx, (2*(xz - wy))*sx, 0,
|
|
(2*(xy - wz))*sy, (1 - 2*(xx + zz))*sy, (2*(yz + wx))*sy, 0,
|
|
(2*(xz + wy))*sz, (2*(yz - wx))*sz, (1 - 2*(xx + yy))*sz, 0,
|
|
tx, ty, tz, 1,
|
|
]
|
|
|
|
def mat4_mult(a, b):
|
|
r = [0]*16
|
|
for i in range(4):
|
|
for j in range(4):
|
|
for k in range(4):
|
|
r[j*4 + i] += a[k*4 + i] * b[j*4 + k]
|
|
return r
|
|
|
|
def lerp_keyframes(kf_times, kf_vals, query_times):
|
|
"""Linearly interpolate keyframe values to query times."""
|
|
if len(kf_times) == 1:
|
|
v = kf_vals[0]
|
|
result = []
|
|
for _ in query_times:
|
|
result.extend(v)
|
|
return result
|
|
result = []
|
|
for qt in query_times:
|
|
if qt <= kf_times[0]:
|
|
result.extend(kf_vals[0])
|
|
elif qt >= kf_times[-1]:
|
|
result.extend(kf_vals[-1])
|
|
else:
|
|
for i in range(len(kf_times) - 1):
|
|
if kf_times[i] <= qt <= kf_times[i+1]:
|
|
a = (qt - kf_times[i]) / (kf_times[i+1] - kf_times[i])
|
|
v = [kf_vals[i][j] + a*(kf_vals[i+1][j] - kf_vals[i][j])
|
|
for j in range(len(kf_vals[0]))]
|
|
result.extend(v)
|
|
break
|
|
else:
|
|
result.extend(kf_vals[-1])
|
|
return result
|
|
|
|
def pad(data, alignment=4):
|
|
"""Pad binary data with null bytes to alignment (for BIN chunk)."""
|
|
while len(data) % alignment:
|
|
data += b'\x00'
|
|
return data
|
|
|
|
# ── Parsing ─────────────────────────────────────────────────────────────────
|
|
|
|
def parse_skeleton(filepath):
|
|
tree = ET.parse(filepath)
|
|
root = tree.getroot()
|
|
bones = []
|
|
for b in root.find('bones').findall('bone'):
|
|
bid = int(b.get('id', '0'))
|
|
name = b.get('name', '')
|
|
pos = b.find('position')
|
|
px, py, pz = float(pos.get('x','0')), float(pos.get('y','0')), float(pos.get('z','0'))
|
|
rot = b.find('rotation')
|
|
angle = float(rot.get('angle','0'))
|
|
axis = rot.find('axis')
|
|
ax, ay, az = float(axis.get('x','1')), float(axis.get('y','0')), float(axis.get('z','0'))
|
|
bones.append({'id': bid, 'name': name, 'pos': (px, py, pz),
|
|
'rot_axis': (ax, ay, az), 'rot_angle': angle, 'scale': (1,1,1)})
|
|
hierarchy = {}
|
|
hier = root.find('bonehierarchy')
|
|
if hier is not None:
|
|
for bp in hier.findall('boneparent'):
|
|
hierarchy[bp.get('bone')] = bp.get('parent')
|
|
animations = {}
|
|
anims = root.find('animations')
|
|
if anims is not None:
|
|
for anim in anims.findall('animation'):
|
|
name = anim.get('name')
|
|
tracks = {}
|
|
for track in anim.find('tracks').findall('track'):
|
|
bn = track.get('bone')
|
|
kfs = []
|
|
for kf in track.find('keyframes').findall('keyframe'):
|
|
t = float(kf.get('time'))
|
|
tr = kf.find('translate')
|
|
tx, ty, tz = float(tr.get('x','0')), float(tr.get('y','0')), float(tr.get('z','0'))
|
|
ro = kf.find('rotate')
|
|
ang = float(ro.get('angle','0'))
|
|
ax_e = ro.find('axis')
|
|
ax, ay, az = float(ax_e.get('x','1')), float(ax_e.get('y','0')), float(ax_e.get('z','0'))
|
|
q = axis_angle_to_quat(ax, ay, az, ang)
|
|
sc = kf.find('scale')
|
|
sx = float(sc.get('x','1')) if sc is not None else 1.0
|
|
sy = float(sc.get('y','1')) if sc is not None else 1.0
|
|
sz = float(sc.get('z','1')) if sc is not None else 1.0
|
|
kfs.append((t, tx, ty, tz, q[0], q[1], q[2], q[3], sx, sy, sz))
|
|
tracks[bn] = kfs
|
|
animations[name] = tracks
|
|
return bones, hierarchy, animations
|
|
|
|
def parse_mesh(filepath):
|
|
tree = ET.parse(filepath)
|
|
root = tree.getroot()
|
|
verts = []
|
|
sg = root.find('sharedgeometry')
|
|
if sg is not None:
|
|
for vb in sg.findall('vertexbuffer'):
|
|
for v in vb.findall('vertex'):
|
|
pos = v.find('position')
|
|
norm = v.find('normal')
|
|
vt = {'x': float(pos.get('x','0')), 'y': float(pos.get('y','0')), 'z': float(pos.get('z','0')),
|
|
'nx': 0, 'ny': 0, 'nz': 1}
|
|
if norm is not None:
|
|
vt['nx'] = float(norm.get('x','0')); vt['ny'] = float(norm.get('y','0')); vt['nz'] = float(norm.get('z','0'))
|
|
verts.append(vt)
|
|
faces = []
|
|
for sm in root.findall('.//submesh'):
|
|
for fe in sm.findall('faces'):
|
|
for f in fe.findall('face'):
|
|
faces.append((int(f.get('v1','0')), int(f.get('v2','0')), int(f.get('v3','0'))))
|
|
bw = [{} for _ in range(len(verts))]
|
|
ba = root.find('boneassignments')
|
|
if ba is not None:
|
|
for a in ba.findall('vertexboneassignment'):
|
|
vi = int(a.get('vertexindex','0'))
|
|
bi = int(a.get('boneindex','0'))
|
|
w = float(a.get('weight','1.0'))
|
|
if vi < len(bw):
|
|
bw[vi][bi] = w
|
|
return verts, faces, bw
|
|
|
|
# ── glTF builder ────────────────────────────────────────────────────────────
|
|
|
|
def build_gltf(mesh_path, skeleton_path, output_path):
|
|
verts, faces, bw = parse_mesh(mesh_path)
|
|
bones, hierarchy, animations = parse_skeleton(skeleton_path)
|
|
|
|
# Topological sort of bones
|
|
bone_by_name = {b['name']: b for b in bones}
|
|
bone_by_id = {b['id']: b for b in bones}
|
|
|
|
roots = [b for b in bones if b['name'] not in hierarchy]
|
|
order = []
|
|
visited = set()
|
|
def dfs(name):
|
|
if name in visited: return
|
|
visited.add(name)
|
|
order.append(name)
|
|
for child, parent in hierarchy.items():
|
|
if parent == name:
|
|
dfs(child)
|
|
for root in roots:
|
|
dfs(root['name'])
|
|
|
|
name_to_idx = {name: i for i, name in enumerate(order)}
|
|
|
|
# glTF nodes (bones only for now)
|
|
nodes = []
|
|
for name in order:
|
|
b = bone_by_name[name]
|
|
q = axis_angle_to_quat(*b['rot_axis'], b['rot_angle'])
|
|
nodes.append({
|
|
'name': name,
|
|
'translation': list(b['pos']),
|
|
'rotation': [q[0], q[1], q[2], q[3]],
|
|
'scale': list(b['scale']),
|
|
})
|
|
for child, parent in hierarchy.items():
|
|
if child in name_to_idx and parent in name_to_idx:
|
|
ci, pi = name_to_idx[child], name_to_idx[parent]
|
|
nodes[pi].setdefault('children', []).append(ci)
|
|
|
|
# Add a skeleton root node (identity matrix, all bones as children)
|
|
skeleton_root_idx = len(nodes)
|
|
nodes.append({'name': '__skeleton_root__', 'children': []})
|
|
# Only add bones that are NOT children of other bones in hierarchy
|
|
for i, name in enumerate(order):
|
|
is_child = any(i in nodes[idx].get('children', []) for idx in range(len(nodes)))
|
|
if not is_child:
|
|
nodes[skeleton_root_idx]['children'].append(i)
|
|
|
|
# Inverse bind matrices
|
|
def world_transform(name):
|
|
b = bone_by_name[name]
|
|
q = axis_angle_to_quat(*b['rot_axis'], b['rot_angle'])
|
|
local = mat4_from_trs(b['pos'][0], b['pos'][1], b['pos'][2],
|
|
q[0], q[1], q[2], q[3], 1, 1, 1)
|
|
parent = hierarchy.get(name)
|
|
if parent and parent in bone_by_name:
|
|
return mat4_mult(world_transform(parent), local)
|
|
return local
|
|
|
|
ibm_flat = []
|
|
for name in order:
|
|
w = world_transform(name)
|
|
# Invert 4x4 matrix
|
|
m = [w[i + j*4] for j in range(4) for i in range(4)] # transpose to row-major for inversion
|
|
inv = invert_4x4(m)
|
|
ibm_flat.extend(inv) # back in row-major? No - glTF uses column-major
|
|
|
|
# Actually glTF stores matrices column-major in the linear buffer
|
|
# The inverse bind matrix in column-major form: each 4 floats = 1 column
|
|
ibm_col_major = []
|
|
for name in order:
|
|
w = world_transform(name)
|
|
# w is column-major: [c0x, c0y, c0z, c0w, c1x, ..., c3w]
|
|
# Invert it
|
|
inv = invert_4x4_colmajor(w)
|
|
ibm_col_major.extend(inv)
|
|
|
|
# Joint indices for skin
|
|
skin_joints = [name_to_idx[name] for name in order]
|
|
|
|
# Vertex data
|
|
positions = []
|
|
nrm = []
|
|
joints0 = []
|
|
weights0 = []
|
|
for i, v in enumerate(verts):
|
|
positions.extend([v['x'], v['y'], v['z']])
|
|
nrm.extend([v['nx'], v['ny'], v['nz']])
|
|
wmap = bw[i] if i < len(bw) else {}
|
|
sw = sorted(wmap.items(), key=lambda x: x[1], reverse=True)[:4]
|
|
j = [0, 0, 0, 0]
|
|
wgt = [0.0, 0.0, 0.0, 0.0]
|
|
for k, (bid, weight) in enumerate(sw):
|
|
if bid in bone_by_id:
|
|
jname = bone_by_id[bid]['name']
|
|
j[k] = name_to_idx.get(jname, 0)
|
|
wgt[k] = weight
|
|
total = sum(wgt)
|
|
if total > 0:
|
|
wgt = [x / total for x in wgt]
|
|
joints0.extend(j)
|
|
weights0.extend(wgt)
|
|
|
|
indices = []
|
|
for f in faces:
|
|
indices.extend(list(f))
|
|
|
|
# Bounding box
|
|
all_x = [v['x'] for v in verts]
|
|
all_y = [v['y'] for v in verts]
|
|
all_z = [v['z'] for v in verts]
|
|
bbox_min = [min(all_x), min(all_y), min(all_z)]
|
|
bbox_max = [max(all_x), max(all_y), max(all_z)]
|
|
|
|
# Model node (has mesh and skin)
|
|
model_node_idx = len(nodes)
|
|
nodes.append({'name': os.path.basename(mesh_path).replace('.mesh.xml', '') + '_mesh',
|
|
'mesh': 0, 'skin': 0})
|
|
|
|
# Scene root
|
|
scene_node_idx = len(nodes)
|
|
nodes.append({'name': 'scene_root', 'children': [skeleton_root_idx, model_node_idx]})
|
|
|
|
# ── Build buffer ────────────────────────────────────────────────────────
|
|
pos_bytes = pad(struct.pack(f'<{len(positions)}f', *positions))
|
|
nrm_bytes = pad(struct.pack(f'<{len(nrm)}f', *nrm))
|
|
idx_bytes = pad(struct.pack(f'<{len(indices)}H', *indices))
|
|
jnt_bytes = pad(struct.pack(f'<{len(joints0)}H', *joints0))
|
|
wgt_bytes = pad(struct.pack(f'<{len(weights0)}f', *weights0))
|
|
ibm_bytes = pad(struct.pack(f'<{len(ibm_col_major)}f', *ibm_col_major))
|
|
|
|
# Base data end
|
|
base_end = len(pos_bytes) + len(nrm_bytes) + len(idx_bytes) + len(jnt_bytes) + len(wgt_bytes) + len(ibm_bytes)
|
|
|
|
# ── Animations ──────────────────────────────────────────────────────────
|
|
anim_data = bytearray()
|
|
gltf_animations = []
|
|
buf_views = []
|
|
accessors_list = []
|
|
|
|
# Fill static buffer views and accessors first
|
|
off = 0
|
|
bv_pos = {'buffer': 0, 'byteOffset': off, 'byteLength': len(pos_bytes), 'target': 34962}; off += len(pos_bytes)
|
|
bv_nrm = {'buffer': 0, 'byteOffset': off, 'byteLength': len(nrm_bytes), 'target': 34962}; off += len(nrm_bytes)
|
|
bv_idx = {'buffer': 0, 'byteOffset': off, 'byteLength': len(idx_bytes), 'target': 34963}; off += len(idx_bytes)
|
|
bv_jnt = {'buffer': 0, 'byteOffset': off, 'byteLength': len(jnt_bytes), 'target': 34962}; off += len(jnt_bytes)
|
|
bv_wgt = {'buffer': 0, 'byteOffset': off, 'byteLength': len(wgt_bytes), 'target': 34962}; off += len(wgt_bytes)
|
|
bv_ibm = {'buffer': 0, 'byteOffset': off, 'byteLength': len(ibm_bytes)}; off += len(ibm_bytes)
|
|
|
|
buffer_views = [bv_pos, bv_nrm, bv_idx, bv_jnt, bv_wgt, bv_ibm]
|
|
|
|
accessors = [
|
|
{'bufferView': 0, 'componentType': 5126, 'count': len(verts), 'type': 'VEC3',
|
|
'min': bbox_min, 'max': bbox_max},
|
|
{'bufferView': 1, 'componentType': 5126, 'count': len(verts), 'type': 'VEC3'},
|
|
{'bufferView': 2, 'componentType': 5123, 'count': len(indices), 'type': 'SCALAR'},
|
|
{'bufferView': 3, 'componentType': 5123, 'count': len(verts), 'type': 'VEC4'},
|
|
{'bufferView': 4, 'componentType': 5126, 'count': len(verts), 'type': 'VEC4'},
|
|
{'bufferView': 5, 'componentType': 5126, 'count': len(order), 'type': 'MAT4'},
|
|
]
|
|
|
|
anim_base = off # animations start here
|
|
|
|
for anim_name, tracks in animations.items():
|
|
all_times = set()
|
|
for bn, kfs in tracks.items():
|
|
for t, *_ in kfs:
|
|
all_times.add(t)
|
|
sorted_times = sorted(all_times)
|
|
if len(sorted_times) < 2:
|
|
continue
|
|
|
|
time_data = pad(struct.pack(f'<{len(sorted_times)}f', *sorted_times))
|
|
time_off = len(anim_data)
|
|
anim_data.extend(time_data)
|
|
|
|
# Buffer view for time
|
|
tv_idx = len(buffer_views)
|
|
buffer_views.append({'buffer': 0, 'byteOffset': anim_base + time_off, 'byteLength': len(time_data)})
|
|
|
|
# Accessor for time (shared per animation)
|
|
t_acc_idx = len(accessors)
|
|
accessors.append({'bufferView': tv_idx, 'componentType': 5126, 'count': len(sorted_times),
|
|
'type': 'SCALAR', 'min': [sorted_times[0]], 'max': [sorted_times[-1]]})
|
|
|
|
samplers = []
|
|
channels = []
|
|
|
|
for bone_name, keyframes in tracks.items():
|
|
if bone_name not in name_to_idx:
|
|
continue
|
|
node_idx = name_to_idx[bone_name]
|
|
|
|
kf_times = [t for t, *_ in keyframes]
|
|
kf_trans = [list(kf[1:4]) for kf in keyframes]
|
|
kf_rot = [list(kf[4:8]) for kf in keyframes]
|
|
kf_scale = [list(kf[8:11]) for kf in keyframes]
|
|
|
|
for data, acc_type, path in [
|
|
(lerp_keyframes(kf_times, kf_trans, sorted_times), 'VEC3', 'translation'),
|
|
(lerp_keyframes(kf_times, kf_rot, sorted_times), 'VEC4', 'rotation'),
|
|
(lerp_keyframes(kf_times, kf_scale, sorted_times), 'VEC3', 'scale'),
|
|
]:
|
|
raw = pad(struct.pack(f'<{len(data)}f', *data))
|
|
off2 = len(anim_data)
|
|
anim_data.extend(raw)
|
|
|
|
dv_idx = len(buffer_views)
|
|
buffer_views.append({'buffer': 0, 'byteOffset': anim_base + off2, 'byteLength': len(raw)})
|
|
|
|
acc_idx = len(accessors)
|
|
accessors.append({'bufferView': dv_idx, 'componentType': 5126,
|
|
'count': len(sorted_times), 'type': acc_type})
|
|
|
|
samp_idx = len(samplers)
|
|
samplers.append({'input': t_acc_idx, 'interpolation': 'LINEAR', 'output': acc_idx})
|
|
|
|
channels.append({'sampler': samp_idx, 'target': {'node': node_idx, 'path': path}})
|
|
|
|
if samplers and channels:
|
|
gltf_animations.append({'name': anim_name, 'samplers': samplers, 'channels': channels})
|
|
|
|
anim_bin = bytes(anim_data)
|
|
|
|
# Combine
|
|
all_bin = pos_bytes + nrm_bytes + idx_bytes + jnt_bytes + wgt_bytes + ibm_bytes + anim_bin
|
|
|
|
# ── Build JSON ──────────────────────────────────────────────────────────
|
|
meshes = [{'name': 'mesh', 'primitives': [{
|
|
'attributes': {'POSITION': 0, 'NORMAL': 1, 'JOINTS_0': 3, 'WEIGHTS_0': 4},
|
|
'indices': 2,
|
|
'material': 0,
|
|
}]}]
|
|
|
|
materials = [{
|
|
'name': 'default',
|
|
'pbrMetallicRoughness': {
|
|
'baseColorFactor': [1.0, 1.0, 1.0, 1.0],
|
|
'metallicFactor': 0.0,
|
|
'roughnessFactor': 0.5,
|
|
},
|
|
}]
|
|
|
|
skin = [{'inverseBindMatrices': 5, 'joints': skin_joints, 'name': 'skin'}]
|
|
|
|
gltf = {
|
|
'asset': {'version': '2.0', 'generator': 'ogre2gltf.py'},
|
|
'scene': 0,
|
|
'scenes': [{'nodes': [scene_node_idx]}],
|
|
'nodes': nodes,
|
|
'meshes': meshes,
|
|
'materials': materials,
|
|
'skins': skin,
|
|
'animations': gltf_animations,
|
|
'accessors': accessors,
|
|
'bufferViews': buffer_views,
|
|
'buffers': [{'byteLength': len(all_bin)}],
|
|
}
|
|
|
|
# ── Write GLB ───────────────────────────────────────────────────────────
|
|
gltf_json = json.dumps(gltf, separators=(',', ':'), allow_nan=False).encode('utf-8')
|
|
# GLB spec: JSON chunk padded with spaces (0x20), BIN chunk padded with nulls (0x00)
|
|
while len(gltf_json) % 4:
|
|
gltf_json += b' '
|
|
while len(all_bin) % 4:
|
|
all_bin += b'\x00'
|
|
|
|
header = struct.pack('<III', 0x46546C67, 2, 12 + 8 + len(gltf_json) + 8 + len(all_bin))
|
|
json_chunk = struct.pack('<II', len(gltf_json), 0x4E4F534A)
|
|
bin_chunk = struct.pack('<II', len(all_bin), 0x004E4942)
|
|
|
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
with open(output_path, 'wb') as f:
|
|
f.write(header + json_chunk + gltf_json + bin_chunk + all_bin)
|
|
|
|
print(f' -> {output_path} ({len(verts)}v {len(faces)}f {len(bones)}b {len(gltf_animations)}a)')
|
|
|
|
# ── Matrix inversion ───────────────────────────────────────────────────────
|
|
|
|
def invert_4x4(m):
|
|
"""Invert 4x4 row-major matrix, return row-major."""
|
|
a, b, c, d, e, f, g, h, i, j, k, l, m_, n, o, p = m
|
|
det = (a * (f*k*p + g*l*n + h*j*o - h*k*n - f*l*o - g*j*p) -
|
|
b * (e*k*p + g*l*m_ + h*i*o - h*k*m_ - e*l*o - g*i*p) +
|
|
c * (e*j*p + f*l*m_ + h*i*n - h*j*m_ - e*l*n - f*i*p) -
|
|
d * (e*j*o + f*k*m_ + g*i*n - g*j*m_ - e*k*n - f*i*o))
|
|
if abs(det) < 1e-12:
|
|
return [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]
|
|
inv_det = 1.0 / det
|
|
return [
|
|
(f*k*p + g*l*n + h*j*o - h*k*n - f*l*o - g*j*p) * inv_det,
|
|
(b*k*p + c*l*n + d*j*o - d*k*n - b*l*o - c*j*p) * inv_det,
|
|
(b*g*p + c*h*n + d*f*o - d*g*n - b*h*o - c*f*p) * inv_det,
|
|
(b*g*l + c*h*j + d*f*k - d*g*j - b*h*k - c*f*l) * inv_det,
|
|
(e*k*p + g*l*m_ + h*i*o - h*k*m_ - e*l*o - g*i*p) * inv_det,
|
|
(a*k*p + c*l*m_ + d*i*o - d*k*m_ - a*l*o - c*i*p) * inv_det,
|
|
(a*g*p + c*h*m_ + d*e*o - d*g*m_ - a*h*o - c*e*p) * inv_det,
|
|
(a*g*l + c*h*i + d*e*k - d*g*i - a*h*k - c*e*l) * inv_det,
|
|
(e*j*p + f*l*m_ + h*i*n - h*j*m_ - e*l*n - f*i*p) * inv_det,
|
|
(a*j*p + b*l*m_ + d*i*n - d*j*m_ - a*l*n - b*i*p) * inv_det,
|
|
(a*f*p + b*h*m_ + d*e*n - d*f*m_ - a*h*n - b*e*p) * inv_det,
|
|
(a*f*l + b*h*i + d*e*j - d*f*i - a*h*j - b*e*l) * inv_det,
|
|
(e*j*o + f*k*m_ + g*i*n - g*j*m_ - e*k*n - f*i*o) * inv_det,
|
|
(a*j*o + b*k*m_ + c*i*n - c*j*m_ - a*k*n - b*i*o) * inv_det,
|
|
(a*f*o + b*g*m_ + c*e*n - c*f*m_ - a*g*n - b*e*o) * inv_det,
|
|
(a*f*k + b*g*i + c*e*j - c*f*i - a*g*j - b*e*k) * inv_det,
|
|
]
|
|
|
|
def invert_4x4_colmajor(m):
|
|
"""Invert 4x4 column-major matrix, return column-major."""
|
|
# Convert col-major to row-major, invert, convert back
|
|
rowm = [m[i + j*4] for j in range(4) for i in range(4)]
|
|
inv_rowm = invert_4x4(rowm)
|
|
return [inv_rowm[j + i*4] for i in range(4) for j in range(4)]
|
|
|
|
# ── Main ────────────────────────────────────────────────────────────────────
|
|
|
|
def main():
|
|
base = '/var/home/nico/Gamedev/Wackelpeter/extracted/Models'
|
|
out = '/var/home/nico/Gamedev/Wackelpeter/web/public/models'
|
|
|
|
for name, mesh, skel in [
|
|
('blob', 'blob/Blob', 'blob/Blob'),
|
|
('sword', 'sword/sword', 'sword/sword'),
|
|
('shield', 'shield/shield', 'shield/shield'),
|
|
]:
|
|
mf = os.path.join(base, f'{mesh}.mesh.xml')
|
|
sf = os.path.join(base, f'{skel}.skeleton.xml')
|
|
if os.path.exists(mf) and os.path.exists(sf):
|
|
print(f'Converting {name}...')
|
|
build_gltf(mf, sf, os.path.join(out, f'{name}.glb'))
|
|
else:
|
|
print(f'Skipping {name} (missing XML)')
|
|
|
|
if __name__ == '__main__':
|
|
main()
|