collada/header: generate animation channels

This commit is contained in:
Zack Buhman 2026-01-26 20:43:19 -06:00
parent 802136c03c
commit f939e70bf9
5 changed files with 462 additions and 44 deletions

View File

@ -17,7 +17,7 @@ def _render(out, lines):
if l and (l[0] == "}" or l[0] == ")"):
level -= 2
if level < 0:
assert namespace >= 0
assert namespace >= 0, l
namespace -= 1
level = 0

View File

@ -1,5 +1,6 @@
from typing import Dict, List, Any
from collections import defaultdict
from dataclasses import dataclass
from itertools import islice, chain
from io import BytesIO
@ -34,12 +35,12 @@ class State:
# symbol_names: C++ symbols/names already emitted
symbol_names: Dict[str, Any]
# joint_sids
joint_sids: Dict[str, types.Node]
# 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]
def __init__(self):
self.vertex_buffer = BytesIO()
self.index_buffer = BytesIO()
@ -47,14 +48,15 @@ class State:
self.geometry__vertex_index_tables = {}
self.node_names = {}
self.symbol_names = {}
self.joint_sids = {}
self.emitted_input_elements_arrays = {}
self.node_animation_channels = defaultdict(set)
def sanitize_name(state, name, value):
assert name is not None
def sanitize_name(state, name, value, *, allow_slash=False):
assert name is not None, value
assert type(name) is str
assert '/' not in name, name
c_id = name.lower().replace('-', '_').replace('.', '_')
if not allow_slash:
assert '/' not in name, name
c_id = name.lower().replace('-', '_').replace('.', '_').replace('/', '_')
assert c_id not in state.node_names or state.node_names[c_id] is value
state.symbol_names[c_id] = value
@ -282,20 +284,37 @@ def get_node_name_id(node):
assert name is not None, node
return name
def render_node(state, collada, node):
if node.type is types.NodeType.JOINT:
assert node.sid is not None, node.sid
assert node.sid not in state.joint_sids, node.sid
state.joint_sids[node.sid] = node
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 f"channel const * const node_channels_{node_name}[] = {{}};"
return
target_names = state.node_animation_channels[node.id]
yield f"channel const * const node_channels_{node_name}[] = {{"
for target_name in target_names:
yield f"&node_channel_{target_name},"
yield "};"
def render_node(state, collada, node):
# render children first
for node in node.nodes:
yield from render_node(state, collada, node)
for child_node in node.nodes:
yield from render_node(state, collada, child_node)
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_channels(state, collada, node, node_name)
type = {
types.NodeType.JOINT: "JOINT",
@ -310,6 +329,12 @@ def render_node(state, collada, node):
yield ""
yield f".instance_geometries = instance_geometries_{node_name},"
yield f".instance_geometries_count = {len(node.instance_geometries)},"
yield ""
yield f".channels = node_channels_{node_name},"
yield f".channels_count = {len(state.node_animation_channels[node.id])},"
yield ""
yield f".nodes = node_children_{node_name},"
yield f".nodes_count = {len(node.nodes)},"
yield "};"
def linear_nodes(collada):
@ -446,13 +471,166 @@ def render_descriptor():
def render_end_of_namespace():
yield "}"
def render_animation_children(state, collada, animation_name, animations):
yield f"node const * const animation_children_{animation_name} = {{"
for animation in animations:
animation_name = sanitize_name(state, animation.id, animation)
yield "&animation_{animation_name},"
yield "};"
def array_c_type(accessor):
if accessor.params[0].name == "INTERPOLATION":
return "interpolation"
assert all(param.type == "float" for param in accessor.params)
if accessor.stride == 1:
return "float"
elif accessor.stride in {2, 3, 4}:
assert list(param.name for param in accessor.params) == ["X", "Y", "Z", "W"][:accessor.stride], accessor.params
return f"float{accessor.stride}"
else:
assert False, accessor.stride
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
yield f"enum interpolation const array_{array_name}[] = {{"
for name in array.names:
assert name in {"BEZIER", "LINEAR"}, name
yield f"interpolation::{name},"
yield "};"
return "interpolation"
elif type(array) is types.FloatArray:
c_type = array_c_type(accessor)
yield f"{c_type} const array_{array_name}[] = {{"
it = iter(array.floats)
for i in range(accessor.count):
vector = ", ".join(f"{float(f)}f" for f in islice(it, accessor.stride))
yield f"{{ {vector} }},"
yield "};"
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 = array_c_type(source.technique_common.accessor)
source_name = sanitize_name(state, source.id, source)
#yield f"source const source_{source_name} = {{"
yield f"// {source_name}"
yield f".{field_name} = {{"
yield f".{c_type}_array = array_{array_name},"
yield f".count = {source.technique_common.accessor.count},"
yield "},"
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])
# 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)
sampler_name = sanitize_name(state, sampler.id, sampler)
yield f"sampler const sampler_{sampler_name} = {{"
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 "};"
target_attributes = {
"A", "ANGLE", "B", "G", "P", "Q", "R", "S", "T", "TIME", "U", "V", "W", "X", "Y", "Z"
}
def render_transform_type(transformation_element):
return {
types.Lookat: "LOOKAT",
types.Matrix: "MATRIX",
types.Rotate: "ROTATE",
types.Scale: "SCALE",
types.Skew: "SKEW",
types.Translate: "TRANSLATE",
}[type(transformation_element)]
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 '.' in channel.target, channel.target
assert "(" not in channel.target, channel.target
node_id, rest = channel.target.split("/")
node_transform_sid, target_attribute = rest.split(".")
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)
transformation_element = node.sid_lookup[node_transform_sid]
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 f"channel const node_channel_{target_name} = {{"
yield f".source_sampler = &sampler_{sampler_name},"
yield f".target_transform_type = transform_type::{render_transform_type(transformation_element)},"
yield f".target_attribute = target_attribute::{target_attribute},"
yield "};"
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_all(collada, namespace):
state = State()
render, out = renderer()
render(render_header(namespace))
render(render_library_animations(state, collada))
render(render_library_effects(state, collada))
render(render_library_materials(state, collada))
render(render_library_geometries(state, collada))
# root elements
render(render_library_visual_scenes(state, collada))
render(render_input_elements_list(state))
render(render_descriptor())

View File

@ -874,7 +874,7 @@ def parse_animation(lookup, root):
sources.append(parse_source_core(lookup, child))
if child.tag == tag("sampler"):
samplers.append(parse_sampler(lookup, child))
if child.tag == tag("channels"):
if child.tag == tag("channel"):
channels.append(parse_channel(lookup, child))
animation = types.Animation(id, name, animations, sources, samplers, channels)

View File

@ -2,6 +2,11 @@
namespace collada {
struct float2 {
float const x;
float const y;
};
struct float3 {
float const x;
float const y;
@ -25,32 +30,6 @@ namespace collada {
float const g;
};
//////////////////////////////////////////////////////////////////////
// animation
//////////////////////////////////////////////////////////////////////
enum class interpolation {
LINEAR,
BEZIER,
};
struct source {
union {
float const * const float_array;
enum interpolation const name_array;
};
int const count;
int const stride;
};
struct sampler {
source const input;
source const output;
source const intangent;
source const outangent;
source const interpolation;
};
//////////////////////////////////////////////////////////////////////
// geometry
//////////////////////////////////////////////////////////////////////
@ -228,6 +207,74 @@ namespace collada {
int const instance_materials_count;
};
//////////////////////////////////////////////////////////////////////
// animation
//////////////////////////////////////////////////////////////////////
enum class interpolation {
LINEAR,
BEZIER,
};
struct source {
union {
float const * const float_array;
float2 const * const float2_array;
float3 const * const float3_array;
float4 const * const float4_array;
enum interpolation const * const interpolation_array;
};
int const count;
};
struct sampler {
source const input;
source const output;
source const in_tangent;
source const out_tangent;
source const interpolation;
};
enum class target_attribute {
A, // alpha color component
ANGLE, // euler angle
B, // blue color component
G, // green color component
P, // third texture component
Q, // fourth texture component
R, // red color component
S, // first texture coordinate
T, // second texture coordinate
TIME, // time in seconds
U, // first generic parameter
V, // second generic parameter
W, // fourth cartesian coordinate
X, // first cartesian coordinate
Y, // second cartesian coordinate
Z, // third cartesian coordinate
};
struct channel {
sampler const * const source_sampler;
int const target_node_index; // an index into the nodes array
transform_type const target_transform_type;
target_attribute const target_attribute;
};
/*
struct animation {
animation const * const animations; // nested animations
int const animations_count;
channels const * const channels;
int const channels_count;
};
*/
//////////////////////////////////////////////////////////////////////
// scene
//////////////////////////////////////////////////////////////////////
struct node {
node_type const type;
@ -237,6 +284,9 @@ namespace collada {
instance_geometry const * const instance_geometries;
int const instance_geometries_count;
channel const * const * const channels;
int const channels_count;
node const * const nodes;
int const nodes_count;
};
@ -245,7 +295,10 @@ namespace collada {
node const * const * const nodes;
int const nodes_count;
inputs const * inputs_list;
inputs const * const inputs_list;
int const inputs_list_count;
//animation const * const animations;
//int const animations_count;
};
}

View File

@ -4,6 +4,144 @@ namespace curve_interpolation {
using namespace collada;
float const array_node_cube_translation_x_input_array[] = {
{ 0.0f },
{ 1.666667f },
{ 3.333333f },
{ 5.0f },
};
float const array_node_cube_translation_x_output_array[] = {
{ 10.0f },
{ -10.0f },
{ 10.0f },
{ -10.0f },
};
float2 const array_node_cube_translation_x_intangent_array[] = {
{ -0.3332306f, 10.0f },
{ 1.111167f, -10.0f },
{ 2.778333f, 10.0f },
{ 4.4445f, -9.219337f },
};
float2 const array_node_cube_translation_x_outtangent_array[] = {
{ 0.5555f, 10.0f },
{ 2.222167f, -10.0f },
{ 3.888333f, 10.0f },
{ 4.000208f, -8.594958f },
};
enum interpolation const array_node_cube_translation_x_interpolation_array[] = {
interpolation::BEZIER,
interpolation::BEZIER,
interpolation::BEZIER,
interpolation::BEZIER,
};
sampler const sampler_node_cube_translation_x_sampler = {
// node_cube_translation_x_input
.input = {
.float_array = array_node_cube_translation_x_input_array,
.count = 4,
},
// node_cube_translation_x_output
.output = {
.float_array = array_node_cube_translation_x_output_array,
.count = 4,
},
// node_cube_translation_x_intangent
.in_tangent = {
.float2_array = array_node_cube_translation_x_intangent_array,
.count = 4,
},
// node_cube_translation_x_outtangent
.out_tangent = {
.float2_array = array_node_cube_translation_x_outtangent_array,
.count = 4,
},
// node_cube_translation_x_interpolation
.interpolation = {
.interpolation_array = array_node_cube_translation_x_interpolation_array,
.count = 4,
},
};
float const array_node_cube_translation_y_input_array[] = {
{ -0.8333334f },
{ 0.8333334f },
{ 2.5f },
{ 4.166667f },
};
float const array_node_cube_translation_y_output_array[] = {
{ -10.05776f },
{ 10.05852f },
{ -9.941484f },
{ 10.05852f },
};
float2 const array_node_cube_translation_y_intangent_array[] = {
{ -1.166264f, -10.05776f },
{ 0.2778334f, 10.05852f },
{ 1.9445f, -9.941484f },
{ 3.611667f, 10.05852f },
};
float2 const array_node_cube_translation_y_outtangent_array[] = {
{ -0.2783333f, -10.05776f },
{ 1.388833f, 10.05852f },
{ 3.0555f, -9.941484f },
{ 4.499598f, 10.05852f },
};
enum interpolation const array_node_cube_translation_y_interpolation_array[] = {
interpolation::BEZIER,
interpolation::BEZIER,
interpolation::BEZIER,
interpolation::BEZIER,
};
sampler const sampler_node_cube_translation_y_sampler = {
// node_cube_translation_y_input
.input = {
.float_array = array_node_cube_translation_y_input_array,
.count = 4,
},
// node_cube_translation_y_output
.output = {
.float_array = array_node_cube_translation_y_output_array,
.count = 4,
},
// node_cube_translation_y_intangent
.in_tangent = {
.float2_array = array_node_cube_translation_y_intangent_array,
.count = 4,
},
// node_cube_translation_y_outtangent
.out_tangent = {
.float2_array = array_node_cube_translation_y_outtangent_array,
.count = 4,
},
// node_cube_translation_y_interpolation
.interpolation = {
.interpolation_array = array_node_cube_translation_y_interpolation_array,
.count = 4,
},
};
channel const node_channel_node_cube_translation_x = {
.source_sampler = &sampler_node_cube_translation_x_sampler,
.target_transform_type = transform_type::TRANSLATE,
.target_attribute = target_attribute::X,
};
channel const node_channel_node_cube_translation_y = {
.source_sampler = &sampler_node_cube_translation_y_sampler,
.target_transform_type = transform_type::TRANSLATE,
.target_attribute = target_attribute::Y,
};
effect const effect_material__15 = {
.type = effect_type::BLINN,
.blinn = {
@ -372,12 +510,17 @@ geometry const * const geometries[] = {
&geometry_geom_plane001,
};
node const * const node_children_node_environmentambientlight = {
};
transform const transforms_node_environmentambientlight[] = {
};
instance_geometry const instance_geometries_node_environmentambientlight[] = {
};
channel const * const node_channels_node_environmentambientlight[] = {};
node const node_node_environmentambientlight = {
.type = node_type::NODE,
@ -386,6 +529,15 @@ node const node_node_environmentambientlight = {
.instance_geometries = instance_geometries_node_environmentambientlight,
.instance_geometries_count = 0,
.channels = node_channels_node_environmentambientlight,
.channels_count = 0,
.nodes = node_children_node_environmentambientlight,
.nodes_count = 0,
};
node const * const node_children_node_cube = {
};
transform const transforms_node_cube[] = {
@ -430,6 +582,11 @@ instance_geometry const instance_geometries_node_cube[] = {
},
};
channel const * const node_channels_node_cube[] = {
&node_channel_node_cube_translation_x,
&node_channel_node_cube_translation_y,
};
node const node_node_cube = {
.type = node_type::NODE,
@ -438,6 +595,15 @@ node const node_node_cube = {
.instance_geometries = instance_geometries_node_cube,
.instance_geometries_count = 1,
.channels = node_channels_node_cube,
.channels_count = 2,
.nodes = node_children_node_cube,
.nodes_count = 0,
};
node const * const node_children_node_cylinder001 = {
};
transform const transforms_node_cylinder001[] = {
@ -458,6 +624,9 @@ instance_geometry const instance_geometries_node_cylinder001[] = {
},
};
channel const * const node_channels_node_cylinder001[] = {
};
node const node_node_cylinder001 = {
.type = node_type::NODE,
@ -466,6 +635,15 @@ node const node_node_cylinder001 = {
.instance_geometries = instance_geometries_node_cylinder001,
.instance_geometries_count = 1,
.channels = node_channels_node_cylinder001,
.channels_count = 0,
.nodes = node_children_node_cylinder001,
.nodes_count = 0,
};
node const * const node_children_node_plane001 = {
};
transform const transforms_node_plane001[] = {
@ -494,6 +672,9 @@ instance_geometry const instance_geometries_node_plane001[] = {
},
};
channel const * const node_channels_node_plane001[] = {
};
node const node_node_plane001 = {
.type = node_type::NODE,
@ -502,6 +683,12 @@ node const node_node_plane001 = {
.instance_geometries = instance_geometries_node_plane001,
.instance_geometries_count = 1,
.channels = node_channels_node_plane001,
.channels_count = 0,
.nodes = node_children_node_plane001,
.nodes_count = 0,
};
node const * const nodes[] = {