Compare commits

...

2 Commits

Author SHA1 Message Date
70b46b0dd8 header: bug: input element indices are global, not per-<triangles>
This was never triggered in previous commits because all prior tests
were with Collada files that only used a single vertex format for the
entire file (therefore index=0 is always correct).

3ds Max CAT bones are (very appropriately) exported with no texture
coordinates, and therefore require a different vertex attribute
declaration with a different stride.

Coincidentally, this meant that CAT meshes were rendered with the
correct vertex attribute, while all (e.g: non-CAT, containing texture
coordinates) meshes incorrectly used the CAT vertex attributes, which
resulted in garbled nonsensical renderings with meaningless garbage
vertex data.

This bug alone consumed at least 4 hours of focused debugging
time. Because the Collada file that triggered this also contained a
complex skeleton, I was originally convinced that the "vertex
transformation result is garbage" bug was caused by something more
directly related to the animation/joint transform calculation.
2026-03-31 19:10:13 -05:00
58b9c254f2 collada: explicit namespace name from argv
This also adds a hack for replacing .png with .dds (maybe the run-time
should be responsible for this? perhaps image extension replacement
should be a command-line argument?).

Improved get_node_name_id behavior for <node> elements with no id and
no name.
2026-03-31 19:08:13 -05:00
3 changed files with 61 additions and 44 deletions

View File

@ -1,4 +1,5 @@
from itertools import islice
import os.path
from collada import types
@ -243,9 +244,9 @@ def render_descriptor(namespace):
yield ".images = images,"
yield ".images_count = (sizeof (images)) / (sizeof (images[0])),"
yield ""
yield f'.position_normal_texture_buffer = "data/scenes/{namespace}/{namespace}.vtx",'
yield f'.joint_weight_buffer = "data/scenes/{namespace}/{namespace}.vjw",'
yield f'.index_buffer = "data/scenes/{namespace}/{namespace}.idx",'
yield f'.position_normal_texture_buffer = "data/scenes/{namespace}/scene.vtx",'
yield f'.joint_weight_buffer = "data/scenes/{namespace}/scene.vjw",'
yield f'.index_buffer = "data/scenes/{namespace}/scene.idx",'
yield "};"
def render_prelude(namespace):
@ -305,10 +306,19 @@ def render_light(light_name, light_type, color):
yield f".color = {render_float_tuple(color)},"
yield "};"
def render_image(image_id, image_name, resource_name, uri):
def render_image(namespace, image_id, image_name, resource_name, uri):
yield f"// {image_id}"
yield f"image const image_{image_name} = {{"
yield f'.uri = "{uri}",'
# hacky
if uri.startswith("./"):
uri = uri.removeprefix("./")
if uri.endswith(".png"):
uri = uri.removesuffix(".png")
uri = uri + ".dds"
else:
assert False, uri
uri = os.path.join('data', 'scenes', namespace, uri)
yield f'.uri = "{uri}"'
yield "};"
def render_library_images(image_names):

View File

@ -9,7 +9,7 @@ from io import BytesIO
import os.path
import struct
from collada.util import matrix_transpose, find_semantics
from collada.util import find_semantics
from collada import parse
from collada import types
from collada.generate import renderer
@ -106,7 +106,7 @@ def sanitize_name(state, name, value, *, allow_slash=False):
if not allow_slash:
assert '/' not in name, name
c_id = _sanitize(name).lower()
assert c_id not in state.symbol_names or state.symbol_names[c_id] is value
assert c_id not in state.symbol_names or state.symbol_names[c_id] is value, c_id
state.symbol_names[c_id] = value
return c_id
@ -132,13 +132,14 @@ def input_elements_key_name(key):
return "_".join(map(str, chain.from_iterable(key))).lower()
def render_input_elements(state, collada, geometry_name, offset_tables):
for i, offset_table in enumerate(offset_tables):
for offset_table in offset_tables:
key = tuple(offset_table_key(offset_table))
key_name = input_elements_key_name(key)
if key_name in state.emitted_input_elements_arrays:
assert state.emitted_input_elements_arrays[key_name][1] == key
continue
state.emitted_input_elements_arrays[key_name] = (i, key)
index = len(state.emitted_input_elements_arrays)
state.emitted_input_elements_arrays[key_name] = (index, key)
yield from lang_header.render_input_elements(key_name, key)
@ -208,7 +209,7 @@ def render_node_transforms(state, collada, node_name, transformation_elements):
if type(transform) is types.Lookat:
yield from lang_header.render_transform_lookat(transform.eye, transform.at, transform.up)
elif type(transform) is types.Matrix:
yield from lang_header.render_transform_matrix(matrix_transpose(transform.values))
yield from lang_header.render_transform_matrix(transform.values)
elif type(transform) is types.Rotate:
yield from lang_header.render_transform_rotate(transform.rotate)
elif type(transform) is types.Scale:
@ -306,10 +307,16 @@ def render_node_instance_geometries(state, collada, node_name, instance_geometri
yield from lang_header.render_node_instance_geometries(node_name, items())
def get_node_name_id(node):
name = node.id if node.id is not None else f"node-{node.name}"
assert name is not None, node
return name
def get_node_name_id(state, node, node_index, use_id=True, name_prefix="node-"):
if use_id and node.id is not None:
return node.id
elif node.name is not None:
return f"{name_prefix}{node.name}"
else:
parent_index = state.node_parents[node_index]
parent = state.linearized_nodes[parent_index]
parent_name = get_node_name_id(state, parent, parent_index)
return f"{parent_name}-{node_index}"
#def render_node_children(state, collada, node_name, nodes):
# yield f"node const * const node_children_{node_name}[] = {{"
@ -371,7 +378,7 @@ def render_joint_node_indices(state, collada, skin, node_name, controller_name,
assert joint_node is not None, (node_sid, skeleton_node.sid_lookup)
joint_node_index = find_node_index(state, joint_node)
joint_node_name_id = get_node_name_id(joint_node)
joint_node_name_id = get_node_name_id(state, joint_node, joint_node_index)
joint_node_name = sanitize_name(state, joint_node_name_id, joint_node)
yield joint_node_index, node_sid, joint_node_name
@ -410,7 +417,7 @@ def render_node_instance_controllers(state, collada, node_name, instance_control
yield from lang_header.render_node_instance_controllers(node_name, items())
def render_node(state, collada, node, node_index):
node_name_id = get_node_name_id(node)
node_name_id = get_node_name_id(state, node, node_index)
node_name = sanitize_name(state, node_name_id, node)
#yield from render_node_children(state, collada, node_name, node.nodes)
yield from render_node_transforms(state, collada, node_name, node.transformation_elements)
@ -431,8 +438,10 @@ def render_node(state, collada, node, node_index):
instance_lights_count = len(node.instance_lights)
channels_count = len(state.node_animation_channels[node.id])
name = get_node_name_id(state, node, node_index, use_id=False, name_prefix="")
yield from lang_header.render_node(node_name,
node.name,
name,
parent_index, type,
transforms_count,
instance_geometries_count,
@ -463,7 +472,7 @@ def render_library_visual_scenes(state, collada):
def items():
for node_index, node in enumerate(state.linearized_nodes):
node_name_id = get_node_name_id(node)
node_name_id = get_node_name_id(state, node, node_index)
node_name = sanitize_name(state, node_name_id, node)
yield node_name, node_index
@ -587,7 +596,7 @@ def render_library_materials(state, collada):
def render_input_elements_list(state):
def items():
for key_name, (index, key) in state.emitted_input_elements_arrays.items():
for key_name, (index, key) in sorted(state.emitted_input_elements_arrays.items(), key=lambda k_v: k_v[1][0]):
elements_count = len(key)
yield key_name, elements_count
yield from lang_header.render_input_elements_list(items())
@ -707,7 +716,8 @@ def render_channel(state, collada, channel):
assert target_attribute in target_attributes
node = collada.lookup(f"#{node_id}", types.Node)
node_name_id = get_node_name_id(node)
node_index = find_node_index(state, node)
node_name_id = get_node_name_id(state, node, node_index)
node_name = sanitize_name(state, node_name_id, node)
transform = transform_sid_lookup(node, node_transform_sid)
@ -790,7 +800,7 @@ def image_resource_name(state, uri):
state.image_paths[filename] = path
return sanitize_resource_name(state, filename, path)
def render_image(state, collada, image, image_index):
def render_image(namespace, state, collada, image, image_index):
assert image.id is not None
assert image.id not in state.image_indices
state.image_indices[image.id] = image_index
@ -799,13 +809,13 @@ def render_image(state, collada, image, image_index):
resource_name = image_resource_name(state, image.image_source.uri)
image_name = sanitize_name(state, image.id, image)
yield from lang_header.render_image(image.id, image_name, resource_name, image.image_source.uri)
yield from lang_header.render_image(namespace, image.id, image_name, resource_name, image.image_source.uri)
def render_library_images(state, collada):
def render_library_images(namespace, state, collada):
image_index = 0
for library_images in collada.library_images:
for image in library_images.images:
yield from render_image(state, collada, image, image_index)
yield from render_image(namespace, state, collada, image, image_index)
image_index += 1
def image_names():
@ -829,7 +839,7 @@ def render_inverse_bind_matrices(collada, skin, controller_name):
def matrices():
for i in range(count):
offset = stride * i
matrix = matrix_transpose(array.floats[offset:offset+stride])
matrix = array.floats[offset:offset+stride]
yield matrix
yield from lang_header.render_inverse_bind_matrices(controller_name, matrices())
@ -860,7 +870,7 @@ def render_controller(state, collada, controller):
vertex_buffer_size = state.joint_weight_vertex_buffer.tell() - vertex_buffer_offset
yield from render_inverse_bind_matrices(collada, skin, controller_name)
bind_shape_matrix = matrix_transpose(skin.bind_shape_matrix.values)
bind_shape_matrix = skin.bind_shape_matrix.values
yield from lang_header.render_controller(controller_name, geometry_name, bind_shape_matrix, vertex_buffer_offset, vertex_buffer_size)
def render_library_controllers(state, collada):
@ -897,7 +907,7 @@ def render_all(collada, namespace, input_filename):
render(render_library_cameras(state, collada))
render(render_library_lights(state, collada))
render(render_library_animations(state, collada))
render(render_library_images(state, collada))
render(render_library_images(namespace, state, collada))
render(render_library_effects(state, collada))
render(render_library_materials(state, collada))
render(render_library_geometries(state, collada))

View File

@ -9,15 +9,12 @@ from collada import lua_header
def usage():
name = sys.argv[0]
print("usage (source):")
print(f" {name} [input_collada.dae] [output_source.cpp] [output_position_normal_texture.vtx] [output_joint_weight.vjw] [output_index.idx]") # [output_resource.rc] [output_makefile.mk]
print(f" {name} [namespace] [input_collada.dae] [output_source.cpp] [output_position_normal_texture.vtx] [output_joint_weight.vjw] [output_index.idx]") # [output_resource.rc] [output_makefile.mk]
print()
print("usage (header):")
print(f" {name} [output_header.h]")
print(f" {name} [namespace] [output_header.h]")
sys.exit(1)
def parse_namespace(filename):
namespace = os.path.splitext(os.path.split(filename)[1])[0]
return namespace.replace("-", "_")
def render_resource_file(state, namespace, output_vtx, output_vjw, output_idx, f):
f.write(f'RES_SCENES_{namespace.upper()}_VTX RCDATA "{output_vtx}"\n'.encode('ascii'))
f.write(f'RES_SCENES_{namespace.upper()}_VJW RCDATA "{output_vjw}"\n'.encode('ascii'))
@ -54,13 +51,14 @@ def render_makefile(state, f):
def main():
try:
input_collada = sys.argv[1]
output_source = sys.argv[2]
output_position_normal_texture = sys.argv[3]
output_joint_weight = sys.argv[4]
output_index = sys.argv[5]
#output_resource = sys.argv[6]
#output_makefile = sys.argv[7]
namespace = sys.argv[1]
input_collada = sys.argv[2]
output_source = sys.argv[3]
output_position_normal_texture = sys.argv[4]
output_joint_weight = sys.argv[5]
output_index = sys.argv[6]
#output_resource = sys.argv[7]
#output_makefile = sys.argv[8]
assert input_collada.lower().endswith(".dae")
assert output_source.lower().endswith(".cpp") or output_source.lower().endswith(".lua")
assert output_position_normal_texture.lower().endswith(".vtx")
@ -72,7 +70,6 @@ def main():
usage()
collada = parse.parse_collada_file(input_collada)
namespace = parse_namespace(input_collada)
if output_source.lower().endswith(".cpp"):
header.lang_header = cpp_header
@ -104,14 +101,14 @@ def main():
def main_header():
try:
output_header = sys.argv[1]
namespace = sys.argv[1]
output_header = sys.argv[2]
assert output_header.lower().endswith(".h")
except Exception as e:
usage()
header.lang_header = cpp_header
namespace = parse_namespace(output_header)
out_header = header.render_all_hpp(namespace)
with open(output_header, 'wb') as f:
@ -120,7 +117,7 @@ def main_header():
f.write(header_buf.encode('utf-8'))
if __name__ == "__main__":
if len(sys.argv) == 2:
if len(sys.argv) == 3:
main_header()
else:
main()