From ee4deb53fb05e0dfcfd5cd4d770f1bdc7dd061b6 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Fri, 2 Jan 2026 19:58:15 -0600 Subject: [PATCH] initial --- .gitignore | 2 + generate.py | 42 +++++++++++++ gltf.hpp | 52 ++++++++++++++++ gltf.py | 156 +++++++++++++++++++++++++++++++++++++++++++++++ render_cpp.py | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 416 insertions(+) create mode 100644 .gitignore create mode 100644 generate.py create mode 100644 gltf.hpp create mode 100644 gltf.py create mode 100644 render_cpp.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a295864 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +__pycache__ diff --git a/generate.py b/generate.py new file mode 100644 index 0000000..ad924f9 --- /dev/null +++ b/generate.py @@ -0,0 +1,42 @@ +import io + +def should_autonewline(line): + return ( + "static_assert" not in line + and "extern" not in line + and (len(line.split()) < 2 or line.split()[1] != '=') # hacky; meh + ) + +def _render(out, lines, indent_length): + indent = " " + level = 0 + namespace = 0 + for l in lines: + if l and (l[0] == "}" or l[0] == ")"): + level -= indent_length + if level < 0: + assert namespace >= 0 + namespace -= 1 + level = 0 + + if len(l) == 0: + out.write("\n") + else: + out.write(indent * level + l + "\n") + + if l and (l[-1] == "{" or l[-1] == "("): + if l.startswith("namespace"): + namespace += 1 + else: + level += indent_length + + if level == 0 and l and l[-1] == ";": + if should_autonewline(l): + out.write("\n") + return out + +def renderer(indent_length=2): + out = io.StringIO() + def render(lines): + return _render(out, lines, indent_length) + return render, out diff --git a/gltf.hpp b/gltf.hpp new file mode 100644 index 0000000..646b879 --- /dev/null +++ b/gltf.hpp @@ -0,0 +1,52 @@ +template +struct Array { + T * e; + int length; +}; + +struct Mesh { + D3DXVECTOR3 * position; + D3DXVECTOR3 * normal; + D3DXVECTOR2 * texcoord_0; + DWORD * indices; +}; + +struct Skin; + +struct Node { + Skin * skin; // skin index (global) + Mesh * mesh; // mesh index (global) + D3DXVECTOR3 scale; + D3DXVECTOR3 translation; + D3DXQUATERNION rotation; +}; + +struct Skin { + D3DXMATRIX * inverse_bind_matrices; // accessor + Array joints; +}; + +enum AnimationChannelPath { + ACP__WEIGHTS, + ACP__ROTATION, + ACP__TRANSLATION, + ACP__SCALE, +}; + +struct AnimationSampler { + float * input; // accessor index, containing keyframe timestamps + void * output; // accessor index, containing keyframe values (type depends on channel target path) +}; + +struct AnimationChannel { + AnimationSampler * sampler; // sampler index, this animation + struct { + Node * node; // node index + AnimationChannelPath path; // property to animate + } target; +}; + +struct Animation { + Array channels; + Array samplers; +}; diff --git a/gltf.py b/gltf.py new file mode 100644 index 0000000..7b86f58 --- /dev/null +++ b/gltf.py @@ -0,0 +1,156 @@ +import struct +import json +import base64 + +class GLTF: + def __init__(self, json, buffers): + # json: dict + # buffers: list[memoryview] + self.json = json + self.buffers = buffers + +class Mesh: + def __init__(self, attributes, indices): + # attributes: list + # indices: list + self.attributes = attributes + self.indices = indices + +def parse_header(mem, offset): + magic, version, length = struct.unpack("= byte_stride + byte_stride = buffer_view["byteStride"] + + components = [] + for _ in range(accessor_count): + for i in range(components_per_element): + start = offset + i * size + end = offset + i * size + size + assert end <= buffer_end + c, = struct.unpack(format, buffer[start:end]) + components.append(c) + offset += byte_stride + return components + +def decode_accessor(gltf, accessor): + components = decode_components(gltf, accessor) + components_per_element = element_type_count(accessor["type"]) + for i in range(accessor["count"]): + if accessor["type"] == "SCALAR": + yield components[i] + else: + yield tuple( + components[i*components_per_element+j] for j in range(components_per_element) + ) + +def validate_mesh(gltf, mesh): + assert len(mesh["primitives"]) == 1 + primitive, = mesh["primitives"] + assert "mode" not in primitive or primitive["mode"] == 4 # triangles + +def decode_glb(mem): + offset = 0 + offset, length = parse_header(mem, offset) + offset, json_chunk = parse_json_chunk(mem, offset) + offset, bin_chunk = parse_bin_chunk(mem, offset) + assert offset == length + gltf = GLTF(json_chunk, [bin_chunk]) + return gltf + +def remove_uri_prefix(uri): + prefixes = [ + "data:application/octet-stream;base64,", + "data:application/gltf-buffer;base64,", + ] + for prefix in prefixes: + if uri.startswith(prefix): + return uri[len(prefix):] + assert False, uri + +def decode_gltf(mem): + gltf_json = json.loads(bytes(mem)) + + buffers = [] + for buffer in gltf_json["buffers"]: + uri = buffer["uri"] + uri = remove_uri_prefix(uri) + data = base64.b64decode(uri) + assert len(data) == buffer["byteLength"] + buffers.append(memoryview(data)) + + gltf = GLTF(gltf_json, buffers) + return gltf + +def decode_file(filename): + with open(filename, "rb") as f: + buf = f.read() + mem = memoryview(buf) + if filename.lower().endswith(".glb"): + return decode_glb(mem) + elif filename.lower().endswith(".gltf"): + return decode_gltf(mem) + else: + assert False, filename + +if __name__ == "__main__": + import sys + gltf = decode_file(sys.argv[1]) + import json + print(json.dumps(gltf.json, indent=4)) diff --git a/render_cpp.py b/render_cpp.py new file mode 100644 index 0000000..2780889 --- /dev/null +++ b/render_cpp.py @@ -0,0 +1,164 @@ +import sys +from pprint import pprint +from generate import renderer + +from gltf import decode_file +from gltf import validate_mesh +from gltf import decode_accessor + +filename = sys.argv[1] +gltf = decode_file(filename) + +def type_name(type): + return { + "SCALAR": "DWORD", + "VEC2": "D3DXVECTOR2", + "VEC3": "D3DXVECTOR3", + "VEC4": "D3DXVECTOR4", + # MAT2 + # MAT3 + "MAT4": "D3DXMATRIX", + }[type] + +def float_s(f): + return f"{f:10.7f}f" + +def sv(v, c_type): + args = ", ".join( + float_s(c) for c in v + ) + return f"{c_type}({args})" + +def mv(v, c_type): + assert len(v) == 16 + assert c_type == "D3DXMATRIX" + + v = [float_s(c) for c in v] + + return f""" +D3DXMATRIX({v[ 0]}, {v[ 1]}, {v[ 2]}, {v[ 3]}, + {v[ 4]}, {v[ 5]}, {v[ 6]}, {v[ 7]}, + {v[ 8]}, {v[ 9]}, {v[10]}, {v[11]}, + {v[12]}, {v[13]}, {v[14]}, {v[15]}) +""".strip() + +def render_value(value, c_type): + if "MAT" in c_type: + return mv(value, c_type) + elif "VEC" in c_type: + return sv(value, c_type) + elif type(value) in {int, float}: + return f"{value}" + else: + assert False + +def render_accessors(gltf): + for accessor_ix, accessor in enumerate(gltf.json["accessors"]): + components = list(decode_accessor(gltf, accessor)) + accessor_name = f"accessor_{accessor_ix}" + accessor_type = accessor['type'] + if type(components[0]) in {int, float}: + c_type = "DWORD" if type(components[0]) is int else "float" + else: + c_type = type_name(accessor_type) + yield f"const {c_type} {accessor_name}[] = {{" + for v in components: + yield f"{render_value(v, c_type)}," + yield "};" + yield f"const int {accessor_name}_length = (sizeof ({accessor_name})) / (sizeof ({accessor_name}[0]));" + +def render_meshes(gltf): + for mesh_ix, mesh in enumerate(gltf.json["meshes"]): + validate_mesh(gltf, mesh) + primitive, = mesh["primitives"] + attributes = primitive["attributes"] + position = attributes["POSITION"] + normal = attributes.get("NORMAL", None) + texcoord_0 = attributes.get("TEXCOORD_0", None) + indices = primitive["indices"] + yield f"const Mesh mesh_{mesh_ix} = {{" + yield f"accessor_{position}, // position" + yield f"accessor_{normal}, // normal" if normal is not None else "NULL," + yield f"accessor_{texcoord_0}, // texcoord_0" if texcoord_0 is not None else "NULL," + yield f"accessor_{indices}, // indices" + yield "};" + +def render_nodes(gltf): + for node in gltf.json["nodes"]: + if "skin" not in node: + continue + skin = node["skin"] + yield f"const Skin skin_{skin};" + + for node_ix, node in enumerate(gltf.json["nodes"]): + skin = f"&skin_{node['skin']}" if "skin" in node else "NULL" + mesh = f"&mesh_{node['skin']}" if "mesh" in node else "NULL" + + scale = (1, 1, 1) + translation = (0, 0, 0) + rotation = (0, 0, 0, 1) + if "scale" in node: + scale = node["scale"] + if "translation" in node: + translation = node["translation"] + if "rotation" in node: + rotation = node["rotation"] + + yield f"const Node node_{node_ix} = {{" + yield f"{skin}, // skin" + yield f"{mesh}, // mesh" + yield f"{render_value(scale, 'D3DXVECTOR3')}, // scale" + yield f"{render_value(translation, 'D3DXVECTOR3')}, // translation" + yield f"{render_value(rotation, 'D3DXVECTOR4')}, // rotation" + yield "};" + +def render_skins(gltf): + for skin_ix, skin in enumerate(gltf.json["skins"]): + yield f"const Node * skin_{skin_ix}__joints[] = {{" + for joint in skin["joints"]: + yield f"&node_{joint}," + yield "};" + + for skin_ix, skin in enumerate(gltf.json["skins"]): + inverse_bind_matrices = skin["inverseBindMatrices"] + yield f"const Skin skin_{skin_ix} = {{" + yield f"accessor_{inverse_bind_matrices}, // inverse bind matrices" + yield f"{{ skin_{skin_ix}__joints, {len(skin['joints'])} }}," + yield "};" + +def render_animation_samplers(animation_ix, samplers): + for sampler_ix, sampler in enumerate(samplers): + yield f"const AnimationSampler animation_{animation_ix}__sampler_{sampler_ix} = {{" + yield f"accessor_{sampler['input']}, // input, keyframe timestamps" + yield f"accessor_{sampler['output']}, // output, keyframe values (void *)" + yield "};" + + +def render_animation_channels(animation_ix, channels): + yield f"const AnimationChannel animation_{animation_ix}__channels[] = {{" + for channel in channels: + sampler = channel["sampler"] + target_node = channel["target"]["node"] + target_path = channel["target"]["path"] + yield f"&animation_{animation_ix}__sampler_{sampler}, // animation sampler" + yield "{" + yield f"&node_{target_node}, // target node" + yield f"ACP__{target_path.upper()}, // target path" + yield "}," + yield "};" + +def render_animations(gltf): + for animation_ix, animation in enumerate(gltf.json["animations"]): + yield from render_animation_samplers(animation_ix, animation["samplers"]) + yield from render_animation_channels(animation_ix, animation["channels"]) + +def render_gltf(gltf): + yield from render_accessors(gltf) + yield from render_meshes(gltf) + yield from render_nodes(gltf) + yield from render_skins(gltf) + yield from render_animations(gltf) + +render, out = renderer() +render(render_gltf(gltf)) +print(out.getvalue())