import itertools
import xml.etree.ElementTree as etree
from xml.etree.ElementTree import Element, SubElement
import numpy as np
from scfile.core import FileEncoder, ModelContent
from scfile.enums import ByteOrder, FileFormat
from scfile.structures import models as S
from scfile.structures.models import Flag
from scfile.structures.models import transforms as T
from . import utils
VERSION = "1.5.0"
DECLARATION = b'<?xml version="1.0" encoding="utf-8"?>\n'
XMLNS = "http://www.collada.org/2008/03/COLLADASchema"
UP_AXIS = "Y_UP"
DEFAULT_COLOR = "1 1 1 1"
[docs]
class DaeEncoder(FileEncoder[ModelContent]):
format = FileFormat.DAE
order = ByteOrder.LITTLE
transforms = [T.unique_names, T.invert_uv, T.skeleton_to_local, T.build_hierarchy]
[docs]
def serialize(self):
self.ctx["ROOT"] = Element("COLLADA", xmlns=XMLNS, version=VERSION)
self.write(DECLARATION)
self._add_asset()
self._add_effects()
self._add_materials()
self._add_geometries()
if self._skeleton_presented:
self._add_controllers()
self._add_scenes()
# Render XML
etree.indent(self.ctx["ROOT"])
self.write(etree.tostring(self.ctx["ROOT"]))
def _add_asset(self):
asset = SubElement(self.ctx["ROOT"], "asset")
SubElement(asset, "unit", name="meter", meter="1")
SubElement(asset, "up_axis").text = UP_AXIS
def _add_effects(self):
library = SubElement(self.ctx["ROOT"], "library_effects")
for mesh in self.data.scene.meshes:
effect = SubElement(library, "effect", id=f"{mesh.material}-effect")
profile = SubElement(effect, "profile_COMMON")
technique = SubElement(profile, "technique", sid="common")
phong = SubElement(technique, "phong")
diffuse = SubElement(phong, "diffuse")
color = SubElement(diffuse, "color")
color.text = DEFAULT_COLOR
def _add_materials(self):
library = SubElement(self.ctx["ROOT"], "library_materials")
for mesh in self.data.scene.meshes:
material = SubElement(library, "material", id=f"{mesh.material}-material", name=mesh.material)
SubElement(material, "instance_effect", url=f"#{mesh.material}-effect")
def _add_mesh_sources(self, mesh: S.ModelMesh, node: Element):
# XYZ Positions
pos_source = utils.create_source(node, mesh.name, "positions", mesh.vertices)
utils.add_accessor(pos_source, mesh.name, "positions", len(mesh.vertices), ["X", "Y", "Z"], "float")
# UV Texture
if self.data.flags[Flag.UV]:
tex_source = utils.create_source(node, mesh.name, "texture", mesh.uv1)
utils.add_accessor(tex_source, mesh.name, "texture", len(mesh.uv1), ["S", "T"], "float")
# XYZ Normals
if self.data.flags[Flag.NORMALS]:
norm_source = utils.create_source(node, mesh.name, "normals", mesh.normals)
utils.add_accessor(norm_source, mesh.name, "normals", len(mesh.normals), ["X", "Y", "Z"], "float")
def _add_triangles(self, mesh: S.ModelMesh, node: Element):
vertices = SubElement(node, "vertices", id=f"{mesh.name}-vertices")
SubElement(vertices, "input", semantic="POSITION", source=f"#{mesh.name}-positions")
triangles = SubElement(node, "triangles", count=str(len(mesh.polygons)), material=f"{mesh.material}-material")
# Inputs
SubElement(triangles, "input", semantic="VERTEX", source=f"#{mesh.name}-vertices", offset="0")
if self.data.flags[Flag.UV]:
SubElement(triangles, "input", semantic="TEXCOORD", source=f"#{mesh.name}-texture", offset="0")
if self.data.flags[Flag.NORMALS]:
SubElement(triangles, "input", semantic="NORMAL", source=f"#{mesh.name}-normals", offset="0")
# ABC Polygons
p = SubElement(triangles, "p")
p.text = " ".join(map(str, mesh.polygons.flatten()))
def _add_geometries(self):
library = SubElement(self.ctx["ROOT"], "library_geometries")
for mesh in self.data.scene.meshes:
geometry = SubElement(library, "geometry", id=mesh.name, name=mesh.name)
node = SubElement(geometry, "mesh")
self._add_mesh_sources(mesh, node)
self._add_triangles(mesh, node)
def _add_controllers(self):
library = SubElement(self.ctx["ROOT"], "library_controllers")
for mesh in self.data.scene.meshes:
if mesh.max_influences > 0:
controller = SubElement(library, "controller", id=f"{mesh.name}-skin", name="Armature")
skin = SubElement(controller, "skin", source=f"#{mesh.name}")
self._add_controller_sources(mesh, skin)
self._add_joints_and_weights(mesh, skin)
def _add_controller_sources(self, mesh: S.ModelMesh, skin: Element):
# Add joint names
joint_data = np.array([bone.name for bone in self.data.scene.skeleton.bones])
joint_source = utils.create_source(skin, mesh.name, "joints", joint_data, "Name_array")
utils.add_accessor(joint_source, mesh.name, "joints", len(joint_data), ["JOINT"], "name")
# Add bind poses
bind_data = self.data.scene.skeleton.inverse_bind_matrices(transpose=False)
bind_source = utils.create_source(skin, mesh.name, "bindposes", bind_data, count=len(bind_data) * 16)
utils.add_accessor(bind_source, mesh.name, "bindposes", len(bind_data), ["TRANSFORM"], "float4x4", 16)
# Add weights
weight_source = utils.create_source(skin, mesh.name, "weights", mesh.links_weights.flatten())
utils.add_accessor(weight_source, mesh.name, "weights", len(mesh.links_weights), ["WEIGHT"], "float")
def _add_joints_and_weights(self, mesh: S.ModelMesh, skin: Element):
# Add joints
joints = SubElement(skin, "joints")
SubElement(joints, "input", semantic="JOINT", source=f"#{mesh.name}-joints")
SubElement(joints, "input", semantic="INV_BIND_MATRIX", source=f"#{mesh.name}-bindposes")
# Add vertex weights
weights = SubElement(skin, "vertex_weights", count=str(len(mesh.vertices)))
SubElement(weights, "input", semantic="JOINT", source=f"#{mesh.name}-joints", offset="0")
SubElement(weights, "input", semantic="WEIGHT", source=f"#{mesh.name}-weights", offset="1")
# Add indices
weight_index = itertools.count()
bone_indices = [f"{bone_id} {next(weight_index)}" for bone_id in mesh.links_ids.flatten()]
SubElement(weights, "vcount").text = " ".join(["4"] * len(mesh.vertices))
SubElement(weights, "v").text = " ".join(bone_indices)
def _add_scenes(self):
library = SubElement(self.ctx["ROOT"], "library_visual_scenes")
visual_scene = SubElement(library, "visual_scene", id="scene", name="Scene")
if self._skeleton_presented:
visual_scene = self._add_armature(visual_scene)
self._add_mesh_instances(visual_scene)
scene = SubElement(self.ctx["ROOT"], "scene")
SubElement(scene, "instance_visual_scene", url="#scene")
def _add_armature(self, scene: Element) -> Element:
node = SubElement(scene, "node", id="armature", name="Armature", type="NODE")
SubElement(node, "matrix", sid="transform").text = "1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1"
for root in self.data.scene.skeleton.roots:
self._add_bone(node, root)
return node
def _add_bone(self, parent: Element, bone: S.SkeletonBone):
joint = SubElement(parent, "node", id=f"armature-{bone.name}", sid=bone.name, name=bone.name, type="JOINT")
matrix = S.create_transform_matrix(bone.position, bone.rotation)
SubElement(joint, "matrix", sid="transform").text = " ".join(map(str, matrix.flatten()))
for child in bone.children:
self._add_bone(joint, child)
def _add_mesh_instances(self, parent: Element):
for mesh in self.data.scene.meshes:
node = SubElement(parent, "node", id=mesh.name, name=mesh.name, type="NODE")
skeleton_presented = self._skeleton_presented and mesh.max_influences > 0
if skeleton_presented:
bone = self.data.scene.skeleton.roots[0]
instance = SubElement(node, "instance_controller", url=f"#{mesh.name}-skin")
SubElement(instance, "skeleton").text = f"#armature-{bone.name}"
else:
instance = SubElement(node, "instance_geometry", url=f"#{mesh.name}", name=mesh.name)
self._add_bind_material(mesh, instance)
def _add_bind_material(self, mesh: S.ModelMesh, instance: Element):
bind = SubElement(instance, "bind_material")
technique_common = SubElement(bind, "technique_common")
SubElement(
technique_common,
"instance_material",
symbol=f"{mesh.material}-material",
target=f"#{mesh.material}-material",
)