from typing import Dict, List, Any, Tuple from operator import attrgetter from collections import defaultdict from dataclasses import dataclass from itertools import islice, chain from urllib.parse import unquote from io import BytesIO import os.path import struct from collada.util import matrix_transpose, find_semantics from collada import parse from collada import types from collada.generate import renderer from collada import buffer from collada import cpp_header @dataclass class State: # arbitrary binary data, including vertex buffers and index # buffers vertex_buffer: BytesIO index_buffer: BytesIO joint_weight_vertex_buffer: BytesIO # geometry__indices: # keys: collada id # values: the index the geometry was placed at in the C++ geometries[] array geometry__indices: Dict[str, int] # geometry__vertex_index_tables: # keys: collada id # values: vertex_index_table (see buffer.py) geometry__vertex_index_tables: Dict[str, List[int]] # symbol_names: C++ symbols/names already emitted symbol_names: Dict[str, Any] # emitted_input_elements_arrays emitted_input_elements_arrays: Dict[str, tuple] # channel nodes: node_id to list of sanitized target names node_animation_channels: Dict[str, set] # linearized_nodes linearized_nodes: List[types.Node] node_parents: Dict[int, int] # image_paths: filename: path image_paths: Dict[str, str] # image_indices: image_id: image_index image_indices: Dict[str, str] # resource_names: resource compiler names already emitted resource_names: Dict[str, str] # effect_textures_by_texcoord: (effect_id, channel): [sampler_sid] effect_textures_by_texcoord: Dict[Tuple[str, str], list] # input_filename input_filename: str relative_path: bool def __init__(self, input_filename): self.vertex_buffer = BytesIO() self.index_buffer = BytesIO() self.joint_weight_vertex_buffer = BytesIO() self.geometry__indices = {} self.geometry__vertex_index_tables = {} self.symbol_names = {} self.emitted_input_elements_arrays = {} self.node_animation_channels = defaultdict(set) self.linearized_nodes = [] self.node_parents = {} self.image_paths = {} self.image_indices = {} self.resource_names = {} self.effect_textures_by_texcoord = defaultdict(list) self.input_filename = input_filename self.relative_path = False def _sanitize(name): return name.replace(' ', '_').replace('-', '_').replace('.', '_').replace('/', '_') def _validate_name_value(name, value): assert name is not None, name assert value is not None, value assert type(name) is str, name assert '\\' not in name, name def sanitize_resource_name(state, name, value): _validate_name_value(name, value) assert '/' not in name, name resource_name = f'_{_sanitize(name).upper()}' assert resource_name not in state.resource_names or state.resource_names[resource_name] is value state.resource_names[resource_name] = value return resource_name def sanitize_name(state, name, value, *, allow_slash=False): _validate_name_value(name, value) assert ' ' not in name, name 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 state.symbol_names[c_id] = value return c_id def renderbin(f, elems, t): fmt = { float: ' with `symbol` to with `target` # symbol is not an XML id # symbol is the `.material` attribute of in geometry.mesh element_index = find_material_symbol(geometry, instance_material.symbol) # target is an XML id material = collada.lookup(instance_material.target, types.Material) material_name = sanitize_name(state, material.id, material) channel_to_input_set = {} for bind_vertex_input in instance_material.bind_vertex_inputs: assert bind_vertex_input.input_semantic == "TEXCOORD", bind_vertex_input if bind_vertex_input.semantic in channel_to_input_set: assert channel_to_input_set[bind_vertex_input.semantic] == bind_vertex_input.input_set else: channel_to_input_set[bind_vertex_input.semantic] = bind_vertex_input.input_set effect = collada.lookup(material.instance_effect.url, types.Effect) profile_common, = effect.profile_common shader = profile_common.technique.shader emission_input_set = shader_to_input_set(channel_to_input_set, shader, attrgetter("emission")) ambient_input_set = shader_to_input_set(channel_to_input_set, shader, attrgetter("ambient")) diffuse_input_set = shader_to_input_set(channel_to_input_set, shader, attrgetter("diffuse")) specular_input_set = shader_to_input_set(channel_to_input_set, shader, attrgetter("specular")) yield element_index, material_name, emission_input_set, ambient_input_set, diffuse_input_set, specular_input_set yield from cpp_header.render_node_geometry_instance_materials(prefix, node_name, i, items()) def get_instance_materials(instance_geometry): return instance_geometry.bind_material.technique_common.materials if instance_geometry.bind_material is not None else [] def render_node_instance_geometries(state, collada, node_name, instance_geometries): for i, instance_geometry in enumerate(instance_geometries): geometry = collada.lookup(instance_geometry.url, types.Geometry) instance_materials = get_instance_materials(instance_geometry) yield from render_node_geometry_instance_materials(state, collada, "instance_geometry", node_name, i, geometry, instance_materials) def items(): for i, instance_geometry in enumerate(instance_geometries): geometry = collada.lookup(instance_geometry.url, types.Geometry) geometry_name = sanitize_name(state, geometry.id, geometry) instance_materials = get_instance_materials(instance_geometry) instance_materials_count = len(instance_materials) yield geometry_name, i, instance_materials_count yield from cpp_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 render_node_children(state, collada, node_name, nodes): # yield f"node const * const node_children_{node_name}[] = {{" # for node in nodes: # node_name_id = get_node_name_id(node) # node_name = sanitize_name(state, node_name_id, node) # yield "&node_{node_name}," # yield "};" def render_node_channels(state, collada, node, node_name): if node.id is None: # nodes with no ID can't have channels yield from cpp_header.render_node_channels(node_name, []) else: target_names = state.node_animation_channels[node.id] yield from cpp_header.render_node_channels(node_name, target_names) def render_node_instance_lights(state, collada, node_name, instance_lights): def light_names(): for instance_light in instance_lights: light = collada.lookup(instance_light.url, types.Light) light_name = sanitize_name(state, light.id, light) yield light_name yield from cpp_header.render_node_instance_lights(node_name, light_names()) def find_node_by_sid(root_node, sid): if sid == root_node.sid: return root_node for child_node in root_node.nodes: node = find_node_by_sid(child_node, sid) if node is not None: return node return None def find_node_index(state, node): for other_index, other in enumerate(state.linearized_nodes): if other is node: return other_index def render_joint_node_indices(state, collada, skin, node_name, controller_name, skeleton_node): joint_input, = find_semantics(skin.joints.inputs, "JOINT") joint_source = collada.lookup(joint_input.source, types.SourceCore) stride = joint_source.technique_common.accessor.stride count = joint_source.technique_common.accessor.count array = joint_source.array_element assert type(joint_source.array_element) == types.NameArray, array assert stride == 1 assert array.count == count * stride def items(): for node_sid in array.names: joint_node = find_node_by_sid(skeleton_node, node_sid); 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 = sanitize_name(state, joint_node_name_id, joint_node) yield joint_node_index, node_sid, joint_node_name yield from cpp_header.render_joint_node_indices(node_name, controller_name, items()) def render_node_instance_controller_joint_node_indices(state, collada, node_name, instance_controller): controller = collada.lookup(instance_controller.url, types.Controller) controller_name = sanitize_name(state, controller.id, controller) assert type(controller.control_element) is types.Skin skin = controller.control_element skeleton_node = collada.lookup(instance_controller.skeleton, types.Node) yield from render_joint_node_indices(state, collada, skin, node_name, controller_name, skeleton_node) def render_node_instance_controllers(state, collada, node_name, instance_controllers): for i, instance_controller in enumerate(instance_controllers): yield from render_node_instance_controller_joint_node_indices(state, collada, node_name, instance_controller) controller = collada.lookup(instance_controller.url, types.Controller) assert type(controller.control_element) is types.Skin geometry = collada.lookup(controller.control_element.source, types.Geometry) instance_materials = get_instance_materials(instance_controller) yield from render_node_geometry_instance_materials(state, collada, "instance_controller", node_name, i, geometry, instance_materials) def items(): for i, instance_controller in enumerate(instance_controllers): controller = collada.lookup(instance_controller.url, types.Controller) controller_name = sanitize_name(state, controller.id, controller) instance_materials = get_instance_materials(instance_controller) instance_materials_count = len(instance_materials) yield controller_name, i, instance_materials_count yield from cpp_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 = 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) yield from render_node_instance_geometries(state, collada, node_name, node.instance_geometries) yield from render_node_instance_controllers(state, collada, node_name, node.instance_controllers) yield from render_node_instance_lights(state, collada, node_name, node.instance_lights) yield from render_node_channels(state, collada, node, node_name) type = { types.NodeType.JOINT: "JOINT", types.NodeType.NODE: "NODE", }[node.type] parent_index = state.node_parents[node_index] transforms_count = len(node.transformation_elements) instance_geometries_count = len(node.instance_geometries) instance_controllers_count = len(node.instance_controllers) instance_lights_count = len(node.instance_lights) channels_count = len(state.node_animation_channels[node.id]) yield from cpp_header.render_node(node_name, parent_index, type, transforms_count, instance_geometries_count, instance_controllers_count, instance_lights_count, channels_count) def traverse_node(state, parent_node_index, node): assert parent_node_index < len(state.linearized_nodes) node_index = len(state.linearized_nodes) state.linearized_nodes.append(node) assert node_index not in state.node_parents state.node_parents[node_index] = parent_node_index for child_node in node.nodes: traverse_node(state, node_index, child_node) def linearize_nodes(state, collada): for library_visual_scenes in collada.library_visual_scenes: for visual_scene in library_visual_scenes.visual_scenes: for node in visual_scene.nodes: traverse_node(state, -1, node) def render_library_visual_scenes(state, collada): linearize_nodes(state, collada) for node_index, node in enumerate(state.linearized_nodes): yield from render_node(state, collada, node, node_index) def items(): for node_index, node in enumerate(state.linearized_nodes): node_name_id = get_node_name_id(node) node_name = sanitize_name(state, node_name_id, node) yield node_name, node_index yield from cpp_header.render_library_visual_scenes(items()) def render_opt_texture(state, profile_common, field_name, texture): sampler_sid = texture.texture sampler = profile_common.sid_lookup[sampler_sid] assert type(sampler) is types.Newparam, sampler assert type(sampler.parameter_type) is types.Sampler2D, sampler surface_sid = sampler.parameter_type.source.sid surface = profile_common.sid_lookup[surface_sid] assert type(surface) is types.Newparam, surface assert type(surface.parameter_type) is types.Surface, surface assert surface.parameter_type.type is types.FxSurfaceType._2D image_id = surface.parameter_type.init_from.uri image_index = state.image_indices[image_id] yield from cpp_header.render_opt_texture(field_name, image_index, image_id) def render_opt_color(field_name, color): yield from cpp_header.render_opt_color(field_name, color) def render_opt_color_or_texture(state, profile_common, field_name, color_or_texture): if color_or_texture is None: color_or_texture = types.Color(value=(0.0, 0.0, 0.0, 0.0)) opt_type = { types.Color: "COLOR", types.Texture: "TEXTURE", }[type(color_or_texture)] def render_body(): if type(color_or_texture) is types.Color: yield from render_opt_color("color", color_or_texture) elif type(color_or_texture) is types.Texture: yield from render_opt_texture(state, profile_common, "texture", color_or_texture) else: assert False, color_or_texture yield from cpp_header.render_opt_color_or_texture(field_name, opt_type, render_body) def render_opt_float(field_name, f): if f is None: f = types.Float(value=0.0) yield from cpp_header.render_opt_float(field_name, float(f.value)) def render_effect(state, collada, effect): profile_common, = effect.profile_common shader = profile_common.technique.shader effect_name = sanitize_name(state, effect.id, effect) if type(shader) is types.Blinn: type_name = "BLINN" field_name = "blinn" def render_body(): yield from render_opt_color_or_texture(state, profile_common, "emission", shader.emission) yield from render_opt_color_or_texture(state, profile_common, "ambient", shader.ambient) yield from render_opt_color_or_texture(state, profile_common, "diffuse", shader.diffuse) yield from render_opt_color_or_texture(state, profile_common, "specular", shader.specular) yield from render_opt_float("shininess", shader.shininess) yield from render_opt_color_or_texture(state, profile_common, "reflective", shader.reflective) yield from render_opt_float("reflectivity", shader.reflectivity) yield from render_opt_color_or_texture(state, profile_common, "transparent", shader.transparent) yield from render_opt_float("transparency", shader.transparency) yield from render_opt_float("index_of_refraction", shader.index_of_refraction) elif type(shader) is types.Lambert: type_name = "LAMBERT" field_name = "lambert" def render_body(): yield from render_opt_color_or_texture(state, profile_common, "emission", shader.emission) yield from render_opt_color_or_texture(state, profile_common, "ambient", shader.ambient) yield from render_opt_color_or_texture(state, profile_common, "diffuse", shader.diffuse) yield from render_opt_color_or_texture(state, profile_common, "reflective", shader.reflective) yield from render_opt_float("reflectivity", shader.reflectivity) yield from render_opt_color_or_texture(state, profile_common, "transparent", shader.transparent) yield from render_opt_float("transparency", shader.transparency) yield from render_opt_float("index_of_refraction", shader.index_of_refraction) elif type(shader) is types.Phong: type_name = "PHONG" field_name = "phong" def render_body(): yield from render_opt_color_or_texture(state, profile_common, "emission", shader.emission) yield from render_opt_color_or_texture(state, profile_common, "ambient", shader.ambient) yield from render_opt_color_or_texture(state, profile_common, "diffuse", shader.diffuse) yield from render_opt_color_or_texture(state, profile_common, "specular", shader.specular) yield from render_opt_float("shininess", shader.shininess) yield from render_opt_color_or_texture(state, profile_common, "reflective", shader.reflective) yield from render_opt_float("reflectivity", shader.reflectivity) yield from render_opt_color_or_texture(state, profile_common, "transparent", shader.transparent) yield from render_opt_float("transparency", shader.transparency) yield from render_opt_float("index_of_refraction", shader.index_of_refraction) elif type(shader) is types.Constant: type_name = "CONSTANT" field_name = "constant" def render_body(): yield from render_opt_color("color", shader.color) yield from render_opt_color_or_texture(state, profile_common, "reflective", shader.reflective) yield from render_opt_float("reflectivity", shader.reflectivity) yield from render_opt_color_or_texture(state, profile_common, "transparent", shader.transparent) yield from render_opt_float("transparency", shader.transparency) yield from render_opt_float("index_of_refraction", shader.index_of_refraction) else: assert False, type(shader) yield from cpp_header.render_effect(effect_name, type_name, field_name, render_body) def render_library_effects(state, collada): for library_effects in collada.library_effects: for effect in library_effects.effects: yield from render_effect(state, collada, effect) def render_library_materials(state, collada): for library_materials in collada.library_materials: for material in library_materials.materials: effect = collada.lookup(material.instance_effect.url, types.Effect) material_name = sanitize_name(state, material.id, material) effect_name = sanitize_name(state, effect.id, effect) yield from cpp_header.render_library_material(material_name, effect_name) def render_input_elements_list(state): def items(): for key_name, (index, key) in state.emitted_input_elements_arrays.items(): elements_count = len(key) yield key_name, elements_count yield from cpp_header.render_input_elements_list(items()) def render_animation_children(state, collada, animation_name, animations): def items(): for animation in animations: animation_name = sanitize_name(state, animation.id, animation) yield animation_name yield from cpp_header.render_animation_children(animation_name, items()) def render_array(state, collada, accessor, array): array_name = sanitize_name(state, array.id, array) # render the array if type(array) is types.NameArray: assert accessor.stride == 1 assert accessor.params[0].name == "INTERPOLATION" assert len(array.names) == accessor.count def names(): for name in array.names: assert name in {"BEZIER", "LINEAR"}, name yield name yield from cpp_header.render_interpolation_array(array_name, names()) elif type(array) is types.FloatArray: def vectors(): it = iter(array.floats) for i in range(accessor.count): vector = ", ".join(f"{float(f)}f" for f in islice(it, accessor.stride)) yield vector yield from cpp_header.render_float_array(array_name, vectors()) else: assert False, type(array) def render_source(state, collada, field_name, source): array_name = sanitize_name(state, source.array_element.id, source.array_element) c_type = "interpolation" if type(source.array_element) is types.NameArray else "float" source_name = sanitize_name(state, source.id, source) yield from cpp_header.render_source(source_name, field_name, c_type, array_name, source.technique_common.accessor.count, source.technique_common.accessor.stride) def render_sampler(state, collada, sampler): order = dict((s, i) for i, s in enumerate(["INPUT", "OUTPUT", "IN_TANGENT", "OUT_TANGENT", "INTERPOLATION"])) inputs = sorted((input for input in sampler.inputs if input.semantic in order), key=lambda input: order[input.semantic]) inputs_semantics = [input.semantic for input in inputs] assert len(inputs_semantics) == len(set(inputs_semantics)) assert "INPUT" in inputs_semantics assert "OUTPUT" in inputs_semantics assert "INTERPOLATION" in inputs_semantics # sampler validation counts = set() for input in inputs: source = collada.lookup(input.source, types.SourceCore) if input.semantic == "INTERPOLATION": nonlinear_interpolation_params = [e for e in source.array_element.names if e != "LINEAR"] if nonlinear_interpolation_params: assert "IN_TANGENT" in inputs_semantics assert "OUT_TANGENT" in inputs_semantics counts.add(source.technique_common.accessor.count) assert len(counts) == 1 # render the source arrays first for input in inputs: assert type(input) is types.InputUnshared source = collada.lookup(input.source, types.SourceCore) yield from render_array(state, collada, source.technique_common.accessor, source.array_element) # render the sampler sampler_name = sanitize_name(state, sampler.id, sampler) def render_body(): for input in inputs: source = collada.lookup(input.source, types.SourceCore) field_name = input.semantic.lower() yield from render_source(state, collada, field_name, source) yield from cpp_header.render_sampler(sampler_name, render_body) target_attributes = { "A", "ANGLE", "B", "G", "P", "Q", "R", "S", "T", "TIME", "U", "V", "W", "X", "Y", "Z", "ALL" } def find_transform_index_in_node(node, transform): for i, node_transform in enumerate(node.transformation_elements): if node_transform is transform: return i assert False, (node, transform) def transform_sid_lookup(node, sid): transform_types = { types.Lookat, types.Matrix, types.Rotate, types.Scale, types.Skew, types.Translate } transform = node.sid_lookup[sid] assert type(transform) in transform_types, transform return transform def render_channel(state, collada, channel): sampler = collada.lookup(channel.source, types.Sampler) sampler_name = sanitize_name(state, sampler.id, sampler) assert '/' in channel.target, channel.target assert "(" not in channel.target, channel.target node_id, rest = channel.target.split("/") if '.' in rest: node_transform_sid, target_attribute = rest.split(".") else: node_transform_sid = rest target_attribute = 'ALL' assert target_attribute in target_attributes node = collada.lookup(f"#{node_id}", types.Node) node_name_id = get_node_name_id(node) node_name = sanitize_name(state, node_name_id, node) transform = transform_sid_lookup(node, node_transform_sid) transform_index = find_transform_index_in_node(node, transform) target_name = sanitize_name(state, channel.target, channel, allow_slash=True) assert target_name not in state.node_animation_channels[node.id] state.node_animation_channels[node.id].add(target_name) yield from cpp_header.render_channel(target_name, sampler_name, transform_index, target_attribute) def render_animation(state, collada, animation_name, animation): # render children first for i, child_animation in enumerate(animation.animations): child_animation_name = f"{animation_name}_{i}" yield from render_animation(state, collada, child_animation_name, child_animation) # samplers (includes sources) for sampler in animation.samplers: yield from render_sampler(state, collada, sampler) for channel in animation.channels: yield from render_channel(state, collada, channel) # all animations channels are referenced from the node (inverse # the relationship in collada) # # I haven't considered how nested or layered animations are # affected by this inversion. #yield f"animation const animation_{animation_name} = {{" #yield f".animations = animation_children_{animation_name}," #yield f".animations_count = {len(animation.animations)}," #yield "" #yield f".channels = animation_channels_{animation_name}" #yield f".channels_count = {len(animation.channels)}" #yield "};" def render_library_animations(state, collada): animation_ix = 0 for library_animations in collada.library_animations: for animation in library_animations.animations: animation_name = f"{animation_ix}" yield from render_animation(state, collada, animation_name, animation) animation_ix += 1 def render_light_type(technique_common: types.TechniqueCommon_Light): return { types.Ambient: "AMBIENT", types.Directional: "DIRECTIONAL", types.Point: "POINT", types.Spot: "SPOT", }[type(technique_common.light)] def render_light(state, collada, light): technique_common = light.technique_common light_name = sanitize_name(state, light.id, light) color = ", ".join(f"{float(f)}f" for f in technique_common.light.color) light_type = render_light_type(technique_common) yield from cpp_header.render_light(light_name, light_type, color) def render_library_lights(state, collada): for library_lights in collada.library_lights: for light in library_lights.lights: yield from render_light(state, collada, light) def image_resource_name(state, uri): uri = unquote(uri) file_prefix = "file:///" path = uri[len(file_prefix):] if uri.startswith(file_prefix) else uri if not os.path.isabs(path) and not os.path.exists(path): path = os.path.join(os.path.dirname(state.input_filename), path) state.relative_path = True assert os.path.exists(path), path image_extensions = {".png", ".jpg", ".bmp", ".jpeg", ".tiff"} assert os.path.splitext(path)[1].lower() in image_extensions, path filename = os.path.split(path)[1] assert filename not in state.image_paths, filename state.image_paths[filename] = path return sanitize_resource_name(state, filename, path) def render_image(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 assert type(image.image_source) is types.InitFrom resource_name = image_resource_name(state, image.image_source.uri) image_name = sanitize_name(state, image.id, image) yield from cpp_header.render_image(image.id, image_name, resource_name) def render_library_images(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) image_index += 1 def image_names(): for library_images in collada.library_images: for image in library_images.images: image_name = sanitize_name(state, image.id, image) yield image_name yield from cpp_header.render_library_images(image_names()) def render_inverse_bind_matrices(collada, skin, controller_name): inverse_bind_matrix_input, = find_semantics(skin.joints.inputs, "INV_BIND_MATRIX") inverse_bind_matrix_source = collada.lookup(inverse_bind_matrix_input.source, types.SourceCore) stride = inverse_bind_matrix_source.technique_common.accessor.stride count = inverse_bind_matrix_source.technique_common.accessor.count array = inverse_bind_matrix_source.array_element assert type(inverse_bind_matrix_source.array_element) == types.FloatArray assert stride == 16 assert array.count == count * stride # emitted in joint order def matrices(): for i in range(count): offset = stride * i matrix = matrix_transpose(array.floats[offset:offset+stride]) yield matrix yield from cpp_header.render_inverse_bind_matrices(controller_name, matrices()) def renderbin_any(f, elems): fmt = { float: '