#!/usr/bin/env python3 """Convert JME3-extracted JSON (from J3OConverter.java) to glTF 2.0 GLB with animations.""" import json import struct import math import os import sys def quat_mult(qa, qb): ax, ay, az, aw = qa bx, by, bz, bw = qb return ( aw*bx + ax*bw + ay*bz - az*by, aw*by - ax*bz + ay*bw + az*bx, aw*bz + ax*by - ay*bx + az*bw, aw*bw - ax*bx - ay*by - az*bz, ) def pad4(data): while len(data) % 4: data += b'\x00' return data 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 mat4_from_trs(t, r, s): x, y, z, w = r 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))*s[0], (2*(xy+wz))*s[0], (2*(xz-wy))*s[0], 0, (2*(xy-wz))*s[1], (1-2*(xx+zz))*s[1], (2*(yz+wx))*s[1], 0, (2*(xz+wy))*s[2], (2*(yz-wx))*s[2], (1-2*(xx+yy))*s[2], 0, t[0], t[1], t[2], 1, ] def invert_4x4_row(m): 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] idet = 1.0/det return [ (f*k*p+g*l*n+h*j*o-h*k*n-f*l*o-g*j*p)*idet, -(b*k*p+c*l*n+d*j*o-d*k*n-b*l*o-c*j*p)*idet, (b*g*p+c*h*n+d*f*o-d*g*n-b*h*o-c*f*p)*idet, -(b*g*l+c*h*j+d*f*k-d*g*j-b*h*k-c*f*l)*idet, -(e*k*p+g*l*m_+h*i*o-h*k*m_-e*l*o-g*i*p)*idet, (a*k*p+c*l*m_+d*i*o-d*k*m_-a*l*o-c*i*p)*idet, -(a*g*p+c*h*m_+d*e*o-d*g*m_-a*h*o-c*e*p)*idet, (a*g*l+c*h*i+d*e*k-d*g*i-a*h*k-c*e*l)*idet, (e*j*p+f*l*m_+h*i*n-h*j*m_-e*l*n-f*i*p)*idet, -(a*j*p+b*l*m_+d*i*n-d*j*m_-a*l*n-b*i*p)*idet, (a*f*p+b*h*m_+d*e*n-d*f*m_-a*h*n-b*e*p)*idet, -(a*f*l+b*h*i+d*e*j-d*f*i-a*h*j-b*e*l)*idet, -(e*j*o+f*k*m_+g*i*n-g*j*m_-e*k*n-f*i*o)*idet, (a*j*o+b*k*m_+c*i*n-c*j*m_-a*k*n-b*i*o)*idet, -(a*f*o+b*g*m_+c*e*n-c*f*m_-a*g*n-b*e*o)*idet, (a*f*k+b*g*i+c*e*j-c*f*i-a*g*j-b*e*k)*idet, ] def convert(json_path, output_path): with open(json_path) as f: data = json.load(f) geos = data.get('geometries', []) skels = data.get('skeletons', []) anims = data.get('animations', {}) if not geos: print(f' No geometry in {json_path}') return geo = geos[0] positions = geo['positions'] normals = geo.get('normals', []) indices = geo.get('indices', []) texcoords = geo.get('texcoords', []) vertex_count = geo.get('vertexCount', len(positions) // 3) bone_weights = geo.get('boneWeights', []) bone_indices = geo.get('boneIndices', []) skel = skels[0] if skels else None nodes = [] skin = None animations_out = [] # ── Skeleton → glTF nodes ────────────────────────────────────────────── if skel: bones = skel['bones'] roots = skel['roots'] name_to_bone = {b['name']: b for b in bones} name_to_idx = {} # IMPORTANT: Keep the ORIGINAL bone list order (from JME3 skeleton.getBone(i)). # BoneTrack.getTargetBoneIndex() and the mesh JOINTS_0 data reference # this exact order. order = [b['name'] for b in bones] for i, name in enumerate(order): name_to_idx[name] = i b = name_to_bone[name] nodes.append({ 'name': name, 'translation': b['position'], 'rotation': b['rotation'], # x,y,z,w 'scale': b['scale'], }) # Hierarchy (by name) for name in order: b = name_to_bone[name] for child_name in b['children']: if child_name in name_to_idx and name in name_to_idx: pi = name_to_idx[name] ci = name_to_idx[child_name] nodes[pi].setdefault('children', []).append(ci) # Inverse bind matrices def world_transform(name, cache={}): if name in cache: return cache[name] b = name_to_bone[name] local = mat4_from_trs(b['position'], b['rotation'], b['scale']) # Find parent parent = None for pn, pb in name_to_bone.items(): if name in pb['children']: parent = pn break if parent and parent in name_to_bone: w = mat4_mult(world_transform(parent, cache), local) else: w = local cache[name] = w return w ibms = [] for name in order: w = world_transform(name) # Convert col-major to row-major, invert, back to col-major row = [w[i + j*4] for j in range(4) for i in range(4)] inv_row = invert_4x4_row(row) inv_col = [inv_row[j + i*4] for i in range(4) for j in range(4)] ibms.extend(inv_col) # Joint indices (identity: bone list order == glTF node order) joints = list(range(len(order))) skin = {'joints': joints} # Mesh node with skin mesh_node_idx = len(nodes) nodes.append({'name': 'mesh', 'mesh': 0, 'skin': 0}) # Skeleton root node root_node_idx = len(nodes) root_children = [name_to_idx[r] for r in roots if r in name_to_idx] root_children.append(mesh_node_idx) nodes.append({'name': 'skeleton_root', 'children': root_children}) # Scene root scene_node_idx = len(nodes) nodes.append({'name': 'scene_root', 'children': [root_node_idx]}) # ── Animations ────────────────────────────────────────────────────── anim_bin = bytearray() anim_buf_views = [] anim_accessors = [] for anim_name, anim in anims.items(): tracks = anim.get('tracks', []) if not tracks: continue # Collect all keyframe times all_times = set() for t in tracks: for tm in t.get('times', []): all_times.add(round(tm, 6)) sorted_times = sorted(all_times) if len(sorted_times) < 2: continue # Per-animation sampler/channel lists anim_samplers_this = [] anim_channels_this = [] # Time buffer time_bytes = pad4(struct.pack(f'<{len(sorted_times)}f', *sorted_times)) time_off = len(anim_bin) anim_bin.extend(time_bytes) tv_idx = len(anim_buf_views) anim_buf_views.append({'byteOffset': time_off, 'byteLength': len(time_bytes)}) t_acc = len(anim_accessors) anim_accessors.append({'type': 'SCALAR', 'count': len(sorted_times), 'min': [sorted_times[0]], 'max': [sorted_times[-1]]}) for track in tracks: bone_idx = track.get('boneIndex', 0) if bone_idx >= len(order): continue node_idx = name_to_idx.get(order[bone_idx], None) if node_idx is None: node_idx = bone_idx # Get bind pose for this bone (JME3 tracks are relative to bind) bone_name = order[bone_idx] bind = name_to_bone[bone_name] bind_pos = bind['position'] bind_rot = bind['rotation'] bind_scale = bind['scale'] kf_times = list(track.get('times', [])) kf_trans = list(track.get('translations', [])) kf_rot = list(track.get('rotations', [])) kf_scale = list(track.get('scales', [])) # Combine with bind pose: final = bind * relative # rotation: bindRot * trackRot combined_rot = [] for i in range(0, len(kf_rot), 4): q = quat_mult(bind_rot, kf_rot[i:i+4]) combined_rot.extend(q) kf_rot = combined_rot # translation: bindPos + trackTrans combined_trans = [] for i in range(0, len(kf_trans), 3): combined_trans.extend([ bind_pos[0] + kf_trans[i], bind_pos[1] + kf_trans[i+1], bind_pos[2] + kf_trans[i+2], ]) kf_trans = combined_trans # scale: bindScale * trackScale combined_scale = [] for i in range(0, len(kf_scale), 3): combined_scale.extend([ bind_scale[0] * kf_scale[i], bind_scale[1] * kf_scale[i+1], bind_scale[2] * kf_scale[i+2], ]) kf_scale = combined_scale def lerp_to(times, values, val_size, targets): if not values: return [] result = [] n_kf = len(times) for qt in targets: if qt <= times[0]: result.extend(values[0:val_size]) elif qt >= times[-1]: result.extend(values[(n_kf-1)*val_size:(n_kf-1)*val_size+val_size]) else: for i in range(n_kf - 1): if times[i] <= qt <= times[i+1]: a = (qt - times[i]) / (times[i+1] - times[i]) if times[i+1] != times[i] else 0 base_i = i * val_size base_j = (i+1) * val_size result.extend([ values[base_i + j] + a * (values[base_j + j] - values[base_i + j]) for j in range(val_size) ]) break else: result.extend(values[(n_kf-1)*val_size:(n_kf-1)*val_size+val_size]) return result trans_interp = lerp_to(kf_times, kf_trans, 3, sorted_times) if kf_trans else [] rot_interp = lerp_to(kf_times, kf_rot, 4, sorted_times) if kf_rot else [] scale_interp = lerp_to(kf_times, kf_scale, 3, sorted_times) if kf_scale else [] for vals, count, acc_type, path in [ (trans_interp, 3, 'VEC3', 'translation'), (rot_interp, 4, 'VEC4', 'rotation'), (scale_interp, 3, 'VEC3', 'scale'), ]: if not vals: continue raw = pad4(struct.pack(f'<{len(vals)}f', *vals)) off2 = len(anim_bin) anim_bin.extend(raw) dv_idx = len(anim_buf_views) anim_buf_views.append({'byteOffset': off2, 'byteLength': len(raw)}) acc_idx = len(anim_accessors) anim_accessors.append({'type': acc_type, 'count': len(sorted_times)}) samp_idx = len(anim_samplers_this) anim_samplers_this.append({'input': t_acc, 'output': acc_idx}) anim_channels_this.append({'sampler': samp_idx, 'target': {'node': node_idx, 'path': path}}) if anim_samplers_this: animations_out.append({ 'name': anim_name, 'samplers': anim_samplers_this, 'channels': anim_channels_this, }) else: # No skeleton - simple mesh node mesh_node_idx = len(nodes) nodes.append({'name': 'mesh', 'mesh': 0}) scene_node_idx = len(nodes) nodes.append({'name': 'scene_root', 'children': [mesh_node_idx]}) anim_bin = bytearray() anim_buf_views = [] anim_accessors = [] anim_samplers = [] anim_channels = [] # ── Build binary buffer ───────────────────────────────────────────────── pos_bytes = pad4(struct.pack(f'<{len(positions)}f', *positions)) norm_bytes = pad4(struct.pack(f'<{len(normals)}f', *normals)) idx_bytes = pad4(struct.pack(f'<{len(indices)}H', *indices)) uv_bytes = pad4(struct.pack(f'<{len(texcoords)}f', *texcoords)) if texcoords else b'' joint_bytes = b'' weight_bytes = b'' if skel and bone_weights: # Joints: use existing bone_indices if available joint_vals = [i & 0xFFFF for i in bone_indices] if bone_indices else [0] * (vertex_count * 4) joint_bytes = pad4(struct.pack(f'<{len(joint_vals)}H', *joint_vals)) weight_bytes = pad4(struct.pack(f'<{len(bone_weights)}f', *bone_weights)) ibm_bytes = pad4(struct.pack(f'<{len(ibms)}f', *ibms)) if skel else b'' all_bin = pos_bytes + norm_bytes + idx_bytes + uv_bytes + joint_bytes + weight_bytes + ibm_bytes + bytes(anim_bin) # ── Buffer views and accessors ────────────────────────────────────────── off = 0 buffer_views = [] bv_pos = {'buffer': 0, 'byteOffset': off, 'byteLength': len(pos_bytes), 'target': 34962}; off += len(pos_bytes) bv_nrm = {'buffer': 0, 'byteOffset': off, 'byteLength': len(norm_bytes), 'target': 34962}; off += len(norm_bytes) bv_idx = {'buffer': 0, 'byteOffset': off, 'byteLength': len(idx_bytes), 'target': 34963}; off += len(idx_bytes) buffer_views = [bv_pos, bv_nrm, bv_idx] accessors = [ {'bufferView': 0, 'componentType': 5126, 'count': vertex_count, 'type': 'VEC3'}, {'bufferView': 1, 'componentType': 5126, 'count': vertex_count, 'type': 'VEC3'}, {'bufferView': 2, 'componentType': 5123, 'count': len(indices), 'type': 'SCALAR'}, ] attributes = {'POSITION': 0, 'NORMAL': 1} if texcoords: bv_uv = {'buffer': 0, 'byteOffset': off, 'byteLength': len(uv_bytes), 'target': 34962}; off += len(uv_bytes) buffer_views.append(bv_uv) accessors.append({'bufferView': 3, 'componentType': 5126, 'count': vertex_count, 'type': 'VEC2'}) attributes['TEXCOORD_0'] = 3 # Joint/weight accessor indices shift if UV present jnt_acc = 4 if texcoords else 3 wgt_acc = 5 if texcoords else 4 if skel and bone_weights: bv_jnt = {'buffer': 0, 'byteOffset': off, 'byteLength': len(joint_bytes), 'target': 34962}; off += len(joint_bytes) bv_wgt = {'buffer': 0, 'byteOffset': off, 'byteLength': len(weight_bytes), 'target': 34962}; off += len(weight_bytes) buffer_views.extend([bv_jnt, bv_wgt]) accessors.append({'bufferView': 3 if not texcoords else 4, 'componentType': 5123, 'count': vertex_count, 'type': 'VEC4'}) accessors.append({'bufferView': 4 if not texcoords else 5, 'componentType': 5126, 'count': vertex_count, 'type': 'VEC4'}) attributes['JOINTS_0'] = jnt_acc attributes['WEIGHTS_0'] = wgt_acc ibm_accessor_idx = None if skel: bv_ibm = {'buffer': 0, 'byteOffset': off, 'byteLength': len(ibm_bytes)}; off += len(ibm_bytes) buffer_views.append(bv_ibm) ibm_accessor_idx = len(accessors) accessors.append({'bufferView': len(buffer_views)-1, 'componentType': 5126, 'count': len(skin['joints']), 'type': 'MAT4'}) skin['inverseBindMatrices'] = ibm_accessor_idx anim_base = off for bv in anim_buf_views: buffer_views.append({'buffer': 0, 'byteOffset': anim_base + bv['byteOffset'], 'byteLength': bv['byteLength']}) acc_offset = len(accessors) for acc in anim_accessors: a = {'componentType': 5126, 'type': acc['type'], 'count': acc['count'], 'bufferView': len(buffer_views) - len(anim_accessors) + 0} accessors.append(a) # Fix animation accessor bufferView indices for i, acc in enumerate(anim_accessors): accessors[acc_offset + i]['bufferView'] = len(buffer_views) - len(anim_accessors) + i # Fix sampler input/output indices (they reference anim_accessors relative) for anim in animations_out: for s in anim['samplers']: s['input'] += acc_offset s['output'] += acc_offset # ── Assemble glTF JSON ────────────────────────────────────────────────── meshes = [{'name': geo.get('name', 'mesh'), 'primitives': [{ 'attributes': attributes, 'indices': 2, 'material': 0, }]}] gltf = { 'asset': {'version': '2.0', 'generator': 'jme3-json2gltf.py'}, 'scene': 0, 'scenes': [{'nodes': [scene_node_idx]}], 'nodes': nodes, 'meshes': meshes, 'materials': [{'name': 'default', 'pbrMetallicRoughness': { 'baseColorFactor': [1, 1, 1, 1], 'metallicFactor': 0, 'roughnessFactor': 0.5}}], 'animations': animations_out, 'accessors': accessors, 'bufferViews': buffer_views, 'buffers': [{'byteLength': len(all_bin)}], } if skin: gltf['skins'] = [skin] gltf_json = json.dumps(gltf, separators=(',', ':')).encode('utf-8') while len(gltf_json) % 4: gltf_json += b' ' while len(all_bin) % 4: all_bin += b'\x00' header = struct.pack(' {output_path} (verts={vertex_count}, bones={len(skin["joints"]) if skin else 0}, ' f'anims={len(animations_out)})') def main(): in_dir = '/var/home/nico/Gamedev/Wackelpeter/web/public/models' models = ['spider', 'staff', 'cross', 'helmet', 'wall1', 'tile1', 'tile2', 'blobknochen'] for m in models: jp = os.path.join(in_dir, f'{m}.json') gp = os.path.join(in_dir, f'{m}.glb') if os.path.exists(jp): print(f'Converting {m}...') try: convert(jp, gp) except Exception as e: print(f' ERROR: {e}') import traceback traceback.print_exc() if __name__ == '__main__': main()