Compare commits

...

2 Commits

Author SHA1 Message Date
83bb7498eb parse: add support for animations 2026-01-23 18:16:36 -06:00
bccb3632ae collada: incomplete collada file parser 2026-01-23 18:16:36 -06:00
4 changed files with 1538 additions and 0 deletions

3
.gitignore vendored
View File

@ -7,3 +7,6 @@
*.obj *.obj
*.res *.res
*.aps *.aps
__pycache__
*.pyc
.#*

View File

@ -24,6 +24,7 @@ CFLAGS += -Wno-unknown-pragmas
CFLAGS += -I./include CFLAGS += -I./include
CFLAGS += -municode CFLAGS += -municode
LDFLAGS += -municode LDFLAGS += -municode
LDFLAGS += -static-libgcc
CXXFLAGS += -fno-exceptions CXXFLAGS += -fno-exceptions

995
collada/parse.py Normal file
View File

@ -0,0 +1,995 @@
from lxml import etree
from collada import types
from functools import partial
xml_namespace = "http://www.collada.org/2005/11/COLLADASchema"
def tag(s):
return f"{{{xml_namespace}}}{s}"
def lookup_add(lookup, name, value):
if name is None:
return
assert name not in lookup
lookup[name] = value
def parse_init_from(lookup, root):
return types.InitFrom(root.text.strip())
def parse_surface_type(s):
if s == "UNTYPED":
return types.FxSurfaceType.UNTYPED
if s == "1D":
return types.FxSurfaceType._1D
if s == "2D":
return types.FxSurfaceType._2D
if s == "3D":
return types.FxSurfaceType._3D
if s == "CUBE":
return types.FxSurfaceType.CUBE
if s == "DEPTH":
return types.FxSurfaceType.DEPTH
if s == "RECT":
return types.FxSurfaceType.RECT
assert False, s
def parse_surface(lookup, root):
type = parse_surface_type(root.attrib["type"])
init_from = None
for child in root.getchildren():
if child.tag == tag("init_from"):
assert init_from is None
init_from = parse_init_from(lookup, child)
surface = types.Surface(type, init_from)
return surface
def parse_source_fx(lookup, root):
return types.SourceFX(root.text.strip())
def parse_sampler2d(lookup, root):
source_fx = None
for child in root.getchildren():
if child.tag == tag("source"):
assert source_fx is None
source_fx = parse_source_fx(lookup, child)
assert source_fx is not None
sampler2d = types.Sampler2D(source_fx)
return sampler2d
def parse_newparam(lookup, sid_lookup, root):
sid = root.attrib["sid"]
parameter_type = None
for child in root.getchildren():
if child.tag == tag("surface"):
assert parameter_type is None
parameter_type = parse_surface(lookup, child)
if child.tag == tag("sampler2D"):
assert parameter_type is None
parameter_type = parse_sampler2d(lookup, child)
assert parameter_type is not None
newparam = types.Newparam(sid, parameter_type)
lookup_add(sid_lookup, sid, newparam)
return newparam
def parse_fields(lookup, root, *, cls, fields):
field_kv = {}
for child in root.getchildren():
for tag_name, parser in fields:
if child.tag == tag(tag_name):
assert tag_name not in field_kv
grandchildren = child.getchildren()
assert len(grandchildren) == 1
field_kv[tag_name] = parser(lookup, grandchildren[0])
for tag_name, _ in fields:
if tag_name not in field_kv:
field_kv[tag_name] = None
return cls(**field_kv)
def parse_color_or_texture(lookup, root):
if root.tag == tag("color"):
return types.Color(tuple(map(float, root.text.strip().split())))
if root.tag == tag("texture"):
return types.Texture(root.attrib["texture"], root.attrib["texcoord"])
assert False, (root, root.tag)
def parse_float(lookup, root):
assert root.tag == tag("float")
return types.Float(float(root.text.strip()))
blinn_phong_fields = [
("emission", parse_color_or_texture),
("ambient", parse_color_or_texture),
("diffuse", parse_color_or_texture),
("specular", parse_color_or_texture),
("shininess", parse_float),
("reflective", parse_color_or_texture),
("reflectivity", parse_float),
("transparent", parse_color_or_texture),
("transparency", parse_float),
("index_of_refraction", parse_float),
]
lambert_fields = [
("emission", parse_color_or_texture),
("ambient", parse_color_or_texture),
("diffuse", parse_color_or_texture),
("reflective", parse_color_or_texture),
("reflectivity", parse_float),
("transparent", parse_color_or_texture),
("transparency", parse_float),
("index_of_refraction", parse_float),
]
constant_fields = [
("emission", parse_color_or_texture),
("reflective", parse_color_or_texture),
("reflectivity", parse_float),
("transparent", parse_color_or_texture),
("transparency", parse_float),
("index_of_refraction", parse_float),
]
parse_blinn = partial(parse_fields, cls=types.Blinn, fields=blinn_phong_fields)
parse_phong = partial(parse_fields, cls=types.Phong, fields=blinn_phong_fields)
parse_lambert = partial(parse_fields, cls=types.Lambert, fields=lambert_fields)
parse_constant = partial(parse_fields, cls=types.Constant, fields=constant_fields)
def parse_technique_fx(lookup, sid_lookup, root):
id = root.attrib.get("id")
sid = root.attrib["sid"]
shader = None
for child in root.getchildren():
if child.tag == tag("blinn"):
assert shader is None
shader = parse_blinn(lookup, child)
if child.tag == tag("constant"):
assert shader is None
shader = parse_constant(lookup, child)
if child.tag == tag("lambert"):
assert shader is None
shader = parse_lambert(lookup, child)
if child.tag == tag("phong"):
assert shader is None
shader = parse_phong(lookup, child)
assert shader is not None
technique = types.TechniqueFX(id, sid, shader)
lookup_add(lookup, id, technique)
lookup_add(sid_lookup, sid, technique)
return technique
def parse_profile_common(lookup, root):
newparam = []
technique_fx = None
sid_lookup = {}
for child in root.getchildren():
if child.tag == tag("newparam"):
newparam.append(parse_newparam(lookup, sid_lookup, child))
if child.tag == tag("technique"):
assert technique_fx is None
technique_fx = parse_technique_fx(lookup, sid_lookup, child)
assert technique_fx is not None
return types.ProfileCommon(newparam, technique_fx, sid_lookup)
def parse_effect(lookup, root):
id = root.attrib["id"]
name = root.attrib.get("name")
profile_common = []
for child in root.getchildren():
if child.tag == tag("profile_COMMON"):
profile_common.append(parse_profile_common(lookup, child))
effect = types.Effect(id, name, profile_common)
lookup_add(lookup, id, effect)
return effect
def parse_library_effects(lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
effects = []
for child in root.getchildren():
if child.tag == tag("effect"):
effects.append(parse_effect(lookup, child))
library_effects = types.LibraryEffects(id, name, effects)
lookup_add(lookup, id, library_effects)
return library_effects
def parse_instance_effect(lookup, sid_lookup, root):
sid = root.attrib.get("sid")
name = root.attrib.get("name")
url = root.attrib["url"]
instance_effect = types.InstanceEffect(sid, name, url)
lookup_add(sid_lookup, sid, instance_effect)
return instance_effect
def parse_material(lookup, root):
sid_lookup = {}
id = root.attrib.get("id")
name = root.attrib.get("name")
instance_effect = None
for child in root.getchildren():
if child.tag == tag("instance_effect"):
assert instance_effect is None
instance_effect = parse_instance_effect(lookup, sid_lookup, child)
assert instance_effect is not None
material = types.Material(id, name, instance_effect, sid_lookup)
lookup_add(lookup, id, material)
return material
def parse_library_materials(lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
materials = []
for child in root.getchildren():
if child.tag == tag("material"):
materials.append(parse_material(lookup, child))
library_materials = types.LibraryMaterials(id, name, materials)
lookup_add(lookup, id, library_materials)
return library_materials
def parse_input_unshared(lookup, root):
semantic = root.attrib["semantic"]
source = root.attrib["source"]
return types.InputUnshared(semantic, source)
def parse_vertices(lookup, root):
id = root.attrib["id"]
name = root.attrib.get("name")
input = []
for child in root.getchildren():
if child.tag == tag("input"):
input.append(parse_input_unshared(lookup, child))
assert len(input) >= 1
vertices = types.Vertices(id, name, input)
lookup_add(lookup, id, vertices)
return vertices
def parse_name_array(lookup, root):
count = int(root.attrib["count"])
id = root.attrib.get("id")
name = root.attrib.get("name")
assert len(root.getchildren()) == 0
names = root.text.strip().split()
name_array = types.NameArray(count, id, name, names)
lookup_add(lookup, id, name_array)
return name_array
def parse_bool(s):
if s == "false":
return False
if s == "true":
return True
assert False, s
def parse_bool_array(lookup, root):
count = int(root.attrib["count"])
id = root.attrib.get("id")
name = root.attrib.get("name")
assert len(root.getchildren()) == 0
bools = [parse_bool(s) for s in root.text.strip().split()]
bool_array = types.BoolArray(count, id, name, bools)
lookup_add(lookup, id, bool_array)
return bool_array
def parse_float_array(lookup, root):
count = int(root.attrib["count"])
id = root.attrib.get("id")
name = root.attrib.get("name")
digits = int(root.attrib.get("digits", 6))
magnitude = int(root.attrib.get("digits", 38))
assert len(root.getchildren()) == 0
floats = [float(s) for s in root.text.strip().split()]
float_array = types.FloatArray(count, id, name, digits, magnitude, floats)
lookup_add(lookup, id, float_array)
return float_array
def parse_int_array(lookup, root):
count = int(root.attrib["count"])
id = root.attrib.get("id")
name = root.attrib.get("name")
min_inclusive = int(root.attrib.get("minInclusive", -2147483648))
max_inclusive = int(root.attrib.get("maxInclusive", 2147483647))
assert len(root.getchildren()) == 0
ints = [int(s) for s in root.text.strip().split()]
int_array = types.IntArray(count, id, name, minInclusive, maxInclusive, ints)
lookup_add(lookup, id, int_array)
return int_array
def parse_param(lookup, sid_lookup, root):
name = root.attrib.get("name")
sid = root.attrib.get("sid")
type = root.attrib["type"]
semantic = root.attrib.get("semantic")
param = types.Param(name, sid, type, semantic)
lookup_add(sid_lookup, sid, param)
return param
def parse_accessor(lookup, root):
count = int(root.attrib["count"])
offset = int(root.attrib.get("offset", 0))
source = root.attrib["source"]
stride = int(root.attrib.get("stride", 1))
sid_lookup = {}
params = []
for child in root.getchildren():
if child.tag == tag("param"):
params.append(parse_param(lookup, sid_lookup, child))
return types.Accessor(count, offset, source, stride, params, sid_lookup)
def parse_technique_common_source_core(lookup, root):
accessor = None
for child in root.getchildren():
if child.tag == tag("accessor"):
assert accessor is None
accessor = parse_accessor(lookup, child)
assert accessor is not None
return types.TechniqueCommon_SourceCore(accessor)
def parse_source_core(lookup, root):
id = root.attrib["id"]
name = root.attrib.get("name")
array_element = None # 0 or 1
technique_common = None # 0 or 1
for child in root.getchildren():
if child.tag == tag("IDREF_array"):
assert array_element is None
assert False, child.tag # not implemented
if child.tag == tag("Name_array"):
assert array_element is None
array_element = parse_name_array(lookup, child)
if child.tag == tag("bool_array"):
assert array_element is None
array_element = parse_bool_array(lookup, child)
if child.tag == tag("float_array"):
assert array_element is None
array_element = parse_float_array(lookup, child)
if child.tag == tag("int_array"):
assert array_element is None
array_element = parse_int_array(lookup, child)
if child.tag == tag("technique_common"):
assert technique_common is None
technique_common = parse_technique_common_source_core(lookup, child)
if child.tag == tag("technique"):
assert False, child.tag # not implemented
source_core = types.SourceCore(id, name, array_element, technique_common)
lookup_add(lookup, id, source_core)
return source_core
def parse_input_shared(lookup, root):
offset = int(root.attrib["offset"])
semantic = root.attrib["semantic"]
source = root.attrib["source"]
set = int(root.attrib["set"]) if "set" in root.attrib else None
return types.InputShared(offset, semantic, source, set)
def parse_p(lookup, root):
assert len(root.getchildren()) == 0
return [int(i) for i in root.text.strip().split()]
def parse_triangles(lookup, root):
name = root.attrib.get("name")
count = int(root.attrib["count"])
material = root.attrib.get("material")
inputs = []
p = None
for child in root.getchildren():
if child.tag == tag("input"):
inputs.append(parse_input_shared(lookup, child))
if child.tag == tag("p"):
assert p is None
p = parse_p(lookup, child)
return types.Triangles(name, count, material, inputs, p)
def parse_mesh(lookup, root):
sources = []
vertices = None
primitive_elements = []
for child in root.getchildren():
if child.tag == tag("source"):
sources.append(parse_source_core(lookup, child))
if child.tag == tag("vertices"):
assert vertices is None
vertices = parse_vertices(lookup, child)
if child.tag == tag("triangles"):
primitive_elements.append(parse_triangles(lookup, child))
assert vertices is not None
assert len(sources) >= 1
mesh = types.Mesh(sources, vertices, primitive_elements)
return mesh
def parse_geometry(lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
geometric_element = None
for child in root.getchildren():
if child.tag == tag("mesh"):
assert geometric_element is None
geometric_element = parse_mesh(lookup, child)
assert geometric_element is not None
geometry = types.Geometry(id, name, geometric_element)
lookup_add(lookup, id, geometry)
return geometry
def parse_library_geometries(lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
geometries = []
for child in root.getchildren():
if child.tag == tag("geometry"):
geometries.append(parse_geometry(lookup, child))
assert len(geometries) >= 1
library_geometries = types.LibraryGeometries(id, name, geometries)
lookup_add(lookup, id, library_geometries)
return library_geometries
def parse_light_color(lookup, root, *, cls):
color = None
for child in root.getchildren():
if child.tag == tag("color"):
assert color is None
color = tuple([float(i) for i in child.text.strip().split()])
assert color is not None
return cls(color)
parse_ambient = partial(parse_light_color, cls=types.Ambient)
parse_directional = partial(parse_light_color, cls=types.Directional)
parse_point = partial(parse_light_color, cls=types.Point)
parse_spot = partial(parse_light_color, cls=types.Spot)
def parse_technique_common_light(lookup, root):
light = None
for child in root.getchildren():
if child.tag == tag("ambient"):
assert light is None
light = parse_ambient(lookup, child)
if child.tag == tag("directional"):
assert light is None
light = parse_directional(lookup, child)
if child.tag == tag("point"):
assert light is None
light = parse_point(lookup, child)
if child.tag == tag("spot"):
assert light is None
light = parse_spot(lookup, child)
assert light is not None
return types.TechniqueCommon_Light(light)
def parse_light(lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
technique_common = None
for child in root.getchildren():
if child.tag == tag("technique_common"):
assert technique_common is None
technique_common = parse_technique_common_light(lookup, child)
assert technique_common is not None
light = types.Light(id, name, technique_common)
lookup_add(lookup, id, light)
return light
def parse_library_lights(lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
lights = []
for child in root.getchildren():
if child.tag == tag("light"):
lights.append(parse_light(lookup, child))
assert len(lights) >= 1
library_lights = types.LibraryLights(id, name, lights)
lookup_add(lookup, id, library_lights)
return library_lights
def parse_init_from(lookup, root):
assert len(root.getchildren()) == 0
return types.InitFrom(root.text.strip())
def parse_image(lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
format = root.attrib.get("format")
height = root.attrib.get("height")
width = root.attrib.get("width")
depth = root.attrib.get("depth")
image_source = None
for child in root.getchildren():
if child.tag == tag("init_from"):
assert image_source is None
image_source = parse_init_from(lookup, child)
assert image_source is not None
image = types.Image(id, name, format, height, width, depth, image_source)
lookup_add(lookup, id, image)
return image
def parse_library_images(lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
images = []
for child in root.getchildren():
if child.tag == tag("image"):
images.append(parse_image(lookup, child))
assert len(images) >= 1
library_images = types.LibraryImages(id, name, images)
lookup_add(lookup, id, library_images)
return library_images
def parse_node_type(s):
if s == "JOINT":
return types.NodeType.JOINT
if s == "NODE":
return types.NodeType.NODE
assert False, s
def parse_lookat(lookup, sid_lookup, root):
sid = root.attrib.get("sid")
assert len(root.getchildren()) == 0
values = [float(i) for i in root.text.strip().split()]
assert len(values) == 9
eye = tuple(values[0:3])
at = tuple(values[3:6])
up = tuple(values[6:9])
lookat = types.Lookat(sid, eye, at, up)
lookup_add(sid_lookup, sid, lookat)
return lookat
def parse_matrix(lookup, sid_lookup, root):
sid = root.attrib.get("sid")
assert len(root.getchildren()) == 0
values = [float(i) for i in root.text.strip().split()]
assert len(values) == 16
r0 = tuple(values[0:4])
r1 = tuple(values[4:8])
r2 = tuple(values[8:12])
r3 = tuple(values[12:16])
matrix = types.Matrix(sid, tuple([r0, r1, r2, r3]))
lookup_add(sid_lookup, sid, matrix)
return matrix
def parse_simple_transform(lookup, sid_lookup, root, *, cls, count):
sid = root.attrib.get("sid")
assert len(root.getchildren()) == 0
values = [float(i) for i in root.text.strip().split()]
assert len(values) == count
inst = cls(sid, values)
lookup_add(sid_lookup, sid, inst)
return inst
parse_rotate = partial(parse_simple_transform, cls=types.Rotate, count=4)
parse_scale = partial(parse_simple_transform, cls=types.Scale, count=3)
parse_skew = partial(parse_simple_transform, cls=types.Skew, count=7)
parse_translate = partial(parse_simple_transform, cls=types.Translate, count=3)
def parse_bind_vertex_input(lookup, root):
semantic = root.attrib["semantic"]
input_semantic = root.attrib["input_semantic"]
input_set = root.attrib.get("input_set")
return types.BindVertexInput(semantic, input_semantic, input_set)
def parse_instance_material(lookup, sid_lookup, root):
sid = root.attrib.get("sid")
name = root.attrib.get("name")
target = root.attrib["target"]
symbol = root.attrib["symbol"]
bind_vertex_inputs = []
for child in root.getchildren():
if child.tag == tag("bind_vertex_input"):
bind_vertex_inputs.append(parse_bind_vertex_input(lookup, child))
inst = types.InstanceMaterial(sid, name, target, symbol, bind_vertex_inputs)
lookup_add(sid_lookup, sid, inst)
return inst
def parse_technique_common_bind_material(lookup, root):
materials = []
sid_lookup = {}
for child in root.getchildren():
if child.tag == tag("instance_material"):
materials.append(parse_instance_material(lookup, sid_lookup, child))
assert len(materials) >= 1
return types.TechniqueCommon_BindMaterial(materials, sid_lookup)
def parse_bind_material(lookup, root):
technique_common = None
for child in root.getchildren():
if child.tag == tag("technique_common"):
assert technique_common is None
technique_common = parse_technique_common_bind_material(lookup, child)
assert technique_common is not None
return types.BindMaterial(technique_common)
def parse_instance_geometry(lookup, sid_lookup, root):
sid = root.attrib.get("sid")
name = root.attrib.get("name")
url = root.attrib["url"]
bind_material = None # optional
for child in root.getchildren():
if child.tag == tag("bind_material"):
assert bind_material is None
bind_material = parse_bind_material(lookup, child)
# bind_material may be none
instance_geometry = types.InstanceGeometry(sid, name, url, bind_material)
lookup_add(sid_lookup, sid, instance_geometry)
return instance_geometry
def parse_instance_light(lookup, sid_lookup, root):
sid = root.attrib.get("sid")
name = root.attrib.get("name")
url = root.attrib["url"]
instance_light = types.InstanceLight(sid, name, url)
lookup_add(sid_lookup, sid, instance_light)
return instance_light
def parse_node(lookup, sid_lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
sid = root.attrib.get("sid")
type = root.attrib["type"] if "type" in root.attrib else types.NodeType.NODE
layer = root.attrib.get("layer", "").strip().split()
transformation_elements = []
instance_geometries = []
instance_lights = []
nodes = []
child_sid_lookup = {}
for child in root.getchildren():
if child.tag == tag("lookat"):
transformation_elements.append(parse_lookat(lookup, child_sid_lookup, child))
if child.tag == tag("matrix"):
transformation_elements.append(parse_matrix(lookup, child_sid_lookup, child))
if child.tag == tag("rotate"):
transformation_elements.append(parse_rotate(lookup, child_sid_lookup, child))
if child.tag == tag("scale"):
transformation_elements.append(parse_scale(lookup, child_sid_lookup, child))
if child.tag == tag("skew"):
transformation_elements.append(parse_skew(lookup, child_sid_lookup, child))
if child.tag == tag("translate"):
transformation_elements.append(parse_translate(lookup, child_sid_lookup, child))
if child.tag == tag("instance_geometry"):
instance_geometries.append(parse_instance_geometry(lookup, child_sid_lookup, child))
if child.tag == tag("instance_light"):
instance_lights.append(parse_instance_light(lookup, child_sid_lookup, child))
if child.tag == tag("node"):
nodes.append(parse_node(lookup, child_sid_lookup, child))
node = types.Node(id, name, sid, type, layer,
transformation_elements,
instance_geometries,
instance_lights,
nodes,
child_sid_lookup)
lookup_add(lookup, id, node)
lookup_add(sid_lookup, sid, node)
return node
def parse_visual_scene(lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
sid_lookup = {}
nodes = []
for child in root.getchildren():
if child.tag == tag("node"):
nodes.append(parse_node(lookup, sid_lookup, child))
assert len(nodes) >= 1
visual_scenes = types.VisualScene(id, name, nodes, sid_lookup)
lookup_add(lookup, id, visual_scenes)
return visual_scenes
def parse_library_visual_scenes(lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
visual_scenes = []
for child in root.getchildren():
if child.tag == tag("visual_scene"):
visual_scenes.append(parse_visual_scene(lookup, child))
assert len(visual_scenes) >= 1
library_visual_scenes = types.LibraryVisualScenes(id, name, visual_scenes)
lookup_add(lookup, id, library_visual_scenes)
return library_visual_scenes
def parse_instance_visual_scene(lookup, sid_lookup, root):
sid = root.attrib.get("sid")
name = root.attrib.get("name")
url = root.attrib["url"]
instance_visual_scene = types.InstanceVisualScene(sid, name, url)
lookup_add(sid_lookup, sid, instance_visual_scene)
return instance_visual_scene
def parse_scene(lookup, root):
instance_visual_scene = None
sid_lookup = {}
for child in root.getchildren():
if child.tag == tag("instance_visual_scene"):
instance_visual_scene = parse_instance_visual_scene(lookup, sid_lookup, child)
# instance_visual_scene may be none
return types.Scene(instance_visual_scene, sid_lookup)
def parse_sampler(lookup, root):
id = root.attrib.get("id")
inputs = []
for child in root.getchildren():
if child.tag == tag("input"):
inputs.append(parse_input_unshared(lookup, child))
sampler = types.Sampler(id, inputs)
lookup_add(lookup, id, sampler)
return sampler
def parse_channel(lookup, root):
source = root.attrib["source"]
target = root.attrib["target"]
return types.Channel(source, target)
def parse_animation(lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
animations = [] # nested animation
sources = []
samplers = []
channels = []
for child in root.getchildren():
if child.tag == tag("animation"):
animations.append(parse_animation(lookup, child))
if child.tag == tag("source"):
sources.append(parse_source_core(lookup, child))
if child.tag == tag("sampler"):
samplers.append(parse_sampler(lookup, child))
if child.tag == tag("channels"):
channels.append(parse_channel(lookup, child))
animation = types.Animation(id, name, animations, sources, samplers, channels)
lookup_add(lookup, id, animation)
return animation
def parse_library_animations(lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
animations = []
for child in root.getchildren():
if child.tag == tag("animation"):
animations.append(parse_animation(lookup, child))
assert len(animations) >= 1
library_animations = types.LibraryAnimations(id, name, animations)
lookup_add(lookup, id, library_animations)
return library_animations
def parse_bind_shape_matrix(lookup, root):
assert len(root.getchildren()) == 0
values = [float(i) for i in root.text.strip().split()]
assert len(values) == 16
r0 = tuple(values[0:4])
r1 = tuple(values[4:8])
r2 = tuple(values[8:12])
r3 = tuple(values[12:16])
values = tuple([r0, r1, r2, r3])
return types.BindShapeMatrix(values)
def parse_joints(lookup, root):
inputs = []
for child in root.getchildren():
if child.tag == tag("input"):
inputs.append(parse_input_unshared(lookup, child))
assert len(inputs) >= 2
return types.Joints(inputs)
def parse_vertex_weights(lookup, root):
inputs = []
vcount = None
v = None
for child in root.getchildren():
if child.tag == tag("input"):
inputs.append(parse_input_shared(lookup, child))
if child.tag == tag("vcount"):
assert vcount is None
vcount = parse_p(lookup, child)
if child.tag == tag("v"):
assert v is None
v = parse_p(lookup, child)
assert len(inputs) >= 2
assert vcount is not None
assert v is not None
return types.VertexWeights(inputs, vcount, v)
def parse_skin(lookup, root):
source = root.attrib["source"]
bind_shape_matrix = None
sources = []
joints = None
vertex_weights = None
for child in root.getchildren():
if child.tag == tag("bind_shape_matrix"):
bind_shape_matrix = parse_bind_shape_matrix(lookup, child)
if child.tag == tag("source"):
sources.append(parse_source_core(lookup, child))
if child.tag == tag("joints"):
assert joints is None
joints = parse_joints(lookup, child)
if child.tag == tag("vertex_weights"):
assert vertex_weights is None
vertex_weights = parse_vertex_weights(lookup, child)
assert joints is not None
assert vertex_weights is not None
return types.Skin(source, bind_shape_matrix, sources, joints, vertex_weights)
def parse_controller(lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
control_element = None
for child in root.getchildren():
if child.tag == tag("skin"):
assert control_element is None
control_element = parse_skin(lookup, child)
if child.tag == tag("morph"):
assert control_element is None
assert False, child.tag # not implemented
assert control_element is not None
controller = types.Controller(id, name, control_element)
lookup_add(lookup, id, controller)
return controller
def parse_library_controllers(lookup, root):
id = root.attrib.get("id")
name = root.attrib.get("name")
controllers = []
for child in root.getchildren():
if child.tag == tag("controller"):
controllers.append(parse_controller(lookup, child))
assert len(controllers) >= 1
library_controllers = types.LibraryControllers(id, name, controllers)
lookup_add(lookup, id, library_controllers)
return library_controllers
def parse_collada(tree):
root = tree.getroot()
assert root.tag == tag("COLLADA")
collada = types.Collada()
lookup = {}
for child in root.getchildren():
if child.tag == tag("library_animations"):
collada.library_animations.append(parse_library_animations(lookup, child))
if child.tag == tag("library_controllers"):
collada.library_controllers.append(parse_library_controllers(lookup, child))
if child.tag == tag("library_effects"):
collada.library_effects.append(parse_library_effects(lookup, child))
if child.tag == tag("library_materials"):
collada.library_materials.append(parse_library_materials(lookup, child))
if child.tag == tag("library_geometries"):
collada.library_geometries.append(parse_library_geometries(lookup, child))
if child.tag == tag("library_lights"):
collada.library_lights.append(parse_library_lights(lookup, child))
if child.tag == tag("library_images"):
collada.library_images.append(parse_library_images(lookup, child))
if child.tag == tag("library_visual_scenes"):
collada.library_visual_scenes.append(parse_library_visual_scenes(lookup, child))
if child.tag == tag("scene"):
collada.scenes.append(parse_scene(lookup, child))
collada._lookup = lookup
return collada
def parse_collada_file(filename):
with open(filename) as f:
tree = etree.parse(f)
collada = parse_collada(tree)
return collada
if __name__ == "__main__":
import sys
from prettyprinter import pprint, install_extras
install_extras(include=["dataclasses"])
collada = parse_collada_file(sys.argv[1])
pprint(collada, width=120)

539
collada/types.py Normal file
View File

@ -0,0 +1,539 @@
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Optional, Union, List, Tuple
URI = str
ID = str
Float3 = Tuple[float, float, float]
Float4 = Tuple[float, float, float, float]
Float7 = Tuple[float, float, float, float, float, float, float]
@dataclass
class Lookat:
sid: Optional[str]
eye: Float3
at: Float3
up: Float3
@dataclass
class Matrix:
# collada is the transpose of a directx matrix
# 1.0 0.0 0.0 0.0
# 0.0 1.0 0.0 0.0
# 0.0 0.0 1.0 0.0
# 2.0 3.0 4.0 1.0
sid: Optional[str]
values: Tuple[Float4, Float4, Float4, Float4]
@dataclass
class Rotate:
# x y z w ; quaternion
sid: Optional[str]
rotate: Float4
@dataclass
class Scale:
sid: Optional[str]
scale: Float3
@dataclass
class Skew:
sid: Optional[str]
skew: Float7
@dataclass
class Translate:
sid: Optional[str]
translate: Float3
@dataclass
class Param:
name: Optional[str]
sid: Optional[str]
type: str
semantic: Optional[str]
@dataclass
class Accessor:
count: int
offset: Optional[int]
source: URI
stride: Optional[int]
params: List[Param]
sid_lookup: dict = field(repr=False)
@dataclass
class TechniqueCommon_SourceCore:
accessor: Accessor
@dataclass
class Ambient:
color: Float3
@dataclass
class Directional:
color: Float3
@dataclass
class Point:
color: Float3
@dataclass
class Spot:
color: Float3
@dataclass
class BindVertexInput:
semantic: str
input_semantic: str
input_set: int
@dataclass
class InstanceMaterial:
sid: Optional[str]
name: Optional[str]
target: URI
symbol: str
bind_vertex_inputs: List[BindVertexInput]
@dataclass
class TechniqueCommon_BindMaterial:
materials: List[InstanceMaterial] # one or more
sid_lookup: dict = field(repr=False)
@dataclass
class BindMaterial:
technique_common: TechniqueCommon_BindMaterial
@dataclass
class InstanceGeometry:
sid: Optional[str]
name: Optional[str]
url: URI
# child element
bind_material: Optional[BindMaterial]
@dataclass
class InstanceLight:
sid: Optional[str]
name: Optional[str]
url: URI
@dataclass
class InstanceVisualScene:
sid: Optional[str]
name: Optional[str]
url: URI
@dataclass
class Scene:
instance_visual_scene: InstanceVisualScene
sid_lookup: dict = field(repr=False)
class FxSurfaceType(Enum):
UNTYPED = auto()
_1D = auto()
_2D = auto()
_3D = auto()
CUBE = auto()
DEPTH = auto()
RECT = auto()
@dataclass
class InitFrom:
uri: str
@dataclass
class Surface:
type: FxSurfaceType
init_from: InitFrom
@dataclass
class SourceFX:
sid: ID
@dataclass
class Sampler2D:
source: SourceFX
@dataclass
class Newparam:
sid: str # required
parameter_type: Union[Surface, Sampler2D]
@dataclass
class Color:
value: Float4
@dataclass
class Float:
value: float
@dataclass
class Texture:
texture: str
texcoord: str
@dataclass
class Blinn:
emission: Optional[Union[Color, Texture]]
ambient: Optional[Union[Color, Texture]]
diffuse: Optional[Union[Color, Texture]]
specular: Optional[Union[Color, Texture]]
shininess: Optional[Float]
reflective: Optional[Union[Color, Texture]]
reflectivity: Optional[Float]
transparent: Optional[Union[Color, Texture]]
transparency: Optional[Float]
index_of_refraction: Optional[Float]
@dataclass
class Lambert:
emission: Optional[Union[Color, Texture]]
ambient: Optional[Union[Color, Texture]]
diffuse: Optional[Union[Color, Texture]]
reflective: Optional[Union[Color, Texture]]
reflectivity: Optional[Float]
transparent: Optional[Union[Color, Texture]]
transparency: Optional[Float]
index_of_refraction: Optional[Float]
@dataclass
class Phong:
emission: Optional[Union[Color, Texture]]
ambient: Optional[Union[Color, Texture]]
diffuse: Optional[Union[Color, Texture]]
specular: Optional[Union[Color, Texture]]
shininess: Optional[Float]
reflective: Optional[Union[Color, Texture]]
reflectivity: Optional[Float]
transparent: Optional[Union[Color, Texture]]
transparency: Optional[Float]
index_of_refraction: Optional[Float]
@dataclass
class Constant:
emission: Optional[Color]
reflective: Optional[Union[Color, Texture]]
reflectivity: Optional[Float]
transparent: Optional[Union[Color, Texture]]
transparency: Optional[Float]
index_of_refraction: Optional[Float]
@dataclass
class TechniqueFX:
id: Optional[ID]
sid: str # required
shader: Union[Blinn, Lambert, Phong, Constant]
@dataclass
class ProfileCommon:
newparam: List[Newparam]
technique: TechniqueFX
sid_lookup: dict = field(repr=False)
@dataclass
class Effect:
id: str
name: Optional[str]
profile_common: List[ProfileCommon]
@dataclass
class LibraryEffects:
id: Optional[ID]
name: Optional[str]
effects: List[Effect]
@dataclass
class InstanceEffect:
sid: Optional[str]
name: Optional[str]
url: URI
@dataclass
class Material:
id: Optional[ID]
name: Optional[str]
instance_effect: InstanceEffect
sid_lookup: dict = field(repr=False)
@dataclass
class LibraryMaterials:
id: Optional[ID]
name: Optional[str]
materials: List[Material]
@dataclass
class NameArray:
count: int
id: Optional[ID]
name: Optional[str]
names: List[str]
@dataclass
class BoolArray:
count: int
id: Optional[ID]
name: Optional[str]
bools: List[bool]
@dataclass
class FloatArray:
count: int
id: Optional[ID]
name: Optional[str]
digits: Optional[int]
magnitude: Optional[int]
floats: List[float]
@dataclass
class IntArray:
count: int
id: Optional[ID]
name: Optional[str]
minInclusive: Optional[int]
maxInclusive: Optional[int]
ints: List[int]
@dataclass
class SourceCore:
id: ID
name: Optional[str]
array_element: Union[FloatArray]
technique_common: TechniqueCommon_SourceCore
@dataclass
class InputShared:
offset: int
semantic: str
source: URI
set: Optional[int]
@dataclass
class Triangles:
name: Optional[str]
count: int
material: Optional[str]
inputs: List[InputShared]
p: List[int]
@dataclass
class InputUnshared:
semantic: str
source: URI
@dataclass
class Vertices:
id: ID
name: Optional[str]
input: List[InputUnshared] # 1 or more
@dataclass
class Mesh:
sources: List[SourceCore]
vertices: Vertices
primitive_elements: List[Union[Triangles]]
@dataclass
class Geometry:
id: Optional[ID]
name: Optional[str]
geometric_element: Union[Mesh]
@dataclass
class LibraryGeometries:
id: Optional[ID]
name: Optional[str]
geometries: List[Geometry]
@dataclass
class TechniqueCommon_Light:
light: Union[Ambient, Directional, Point, Spot]
@dataclass
class Light:
id: Optional[ID]
name: Optional[str]
technique_common: TechniqueCommon_Light
@dataclass
class LibraryLights:
id: Optional[ID]
name: Optional[str]
lights: List[Light]
@dataclass
class Image:
id: Optional[ID]
name: Optional[str]
format: Optional[str]
height: Optional[int]
width: Optional[int]
depth: Optional[int]
image_source: Union[InitFrom]
@dataclass
class LibraryImages:
id: Optional[ID]
name: Optional[str]
images: List[Image]
class NodeType(Enum):
JOINT = auto()
NODE = auto()
TransformationElements = Union[Lookat, Matrix, Rotate, Scale, Skew, Translate]
@dataclass
class Node:
id: Optional[ID]
name: Optional[str]
sid: Optional[str]
type: NodeType
layer: List[str]
transformation_elements: List[TransformationElements]
instance_geometries: List[InstanceGeometry]
instance_lights: List[InstanceLight]
nodes: List['Node']
sid_lookup: dict = field(repr=False)
@dataclass
class VisualScene:
id: Optional[ID]
name: Optional[str]
nodes: List[Node]
sid_lookup: dict = field(repr=False)
@dataclass
class LibraryVisualScenes:
id: Optional[ID]
name: Optional[str]
visual_scenes: List[VisualScene]
@dataclass
class BindShapeMatrix:
# it is written in row-major order in the COLLADA document for
# human readability.
values: Tuple[Float4, Float4, Float4, Float4]
@dataclass
class Joints:
inputs: List[InputUnshared] # 2 or more
@dataclass
class VertexWeights:
inputs: List[InputShared] # 2 or more
vcount: List[int]
v: List[int]
@dataclass
class Skin:
source: URI # required
bind_shape_matrix: Optional[BindShapeMatrix]
sources: List[SourceCore] # 3 or more
joints: Joints # 1
vertex_weights: VertexWeights # 1
@dataclass
class Controller:
id: Optional[ID]
name: Optional[str]
control_element: Union[Skin]
@dataclass
class LibraryControllers:
id: Optional[ID]
name: Optional[str]
controllers: List[Controller]
@dataclass
class Sampler:
id: Optional[ID]
inputs: List[InputUnshared] # 1 or more
@dataclass
class Channel:
source: URI
target: URI
@dataclass
class Animation:
id: Optional[ID]
name: Optional[str]
animations: List['Animation']
sources: List[SourceCore]
samplers: List[Sampler]
channels: List[Channel]
@dataclass
class LibraryAnimations:
id: Optional[ID]
name: Optional[str]
animations: List[Animation] # 1 or more
@dataclass
class Collada:
library_animations: List[LibraryAnimations]
library_controllers: List[LibraryControllers]
library_effects: List[LibraryEffects]
library_materials: List[LibraryMaterials]
library_geometries: List[LibraryGeometries]
library_lights: List[LibraryLights]
library_images: List[LibraryImages]
library_visual_scenes: List[LibraryVisualScenes]
scenes: List[Scene]
_lookup: dict = field(repr=False)
def __init__(self):
self.library_animations = []
self.library_controllers = []
self.library_effects = []
self.library_materials = []
self.library_geometries = []
self.library_lights = []
self.library_images = []
self.library_visual_scenes = []
self.scenes = []
self._lookup = None
def lookup(self, s):
assert '/' not in s
assert s.startswith("#")
id = s[1:]
return self._lookup[id]