HumanoidRetargeter/Formats/Gltf/GltfModelDmxWriter.cs
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Text;
using System.Text.Json;
using HumanoidRetargeter.Formats.Dmx;
using HumanoidRetargeter.Formats.Fbx;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;
namespace HumanoidRetargeter.Formats.Gltf;
using Matrix4x4 = System.Numerics.Matrix4x4;
using Quaternion = System.Numerics.Quaternion;
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;
/// <summary>
/// Converts the skinned meshes in a glTF/GLB model to Source 2 model-DMX. ModelDoc does
/// not accept glTF as a RenderMeshFile, while DMX preserves the same skeleton, vertices,
/// materials and four-weight skinning without an external converter.
/// </summary>
public static class GltfModelDmxWriter
{
private const float MetersToCentimeters = 100f;
/// <summary>Writes a Y-up, centimeter model-DMX for the already imported target rig.</summary>
public static string Write(
byte[] data, SkeletonModel skeleton, string name,
Func<string, byte[]>? externalBufferResolver = null)
{
ArgumentNullException.ThrowIfNull(data);
ArgumentNullException.ThrowIfNull(skeleton);
ArgumentNullException.ThrowIfNull(name);
var document = GltfDocument.Parse(data, externalBufferResolver);
var parts = ReadMeshParts(document, skeleton);
if (parts.Count == 0)
throw new FormatException("glTF contains no supported mesh primitives.");
return Emit(skeleton, name, parts);
}
private sealed class MeshPart
{
public required string Name;
public required string Material;
public required Vector3[] Positions;
public required Vector3[] Normals;
public required Vector2[] TexCoords;
public required int[] Triangles;
public required float[] Weights;
public required int[] Joints;
}
private static List<MeshPart> ReadMeshParts(GltfDocument document, SkeletonModel skeleton)
{
var root = document.Root;
if (!root.TryGetProperty("nodes", out var nodeArray)
|| !root.TryGetProperty("meshes", out var meshArray))
return new List<MeshPart>();
root.TryGetProperty("skins", out var skinArray);
root.TryGetProperty("materials", out var materialArray);
var worlds = NodeWorlds(document);
var bonesByName = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < skeleton.Count; i++)
bonesByName[skeleton[i].Name] = i;
var parts = new List<MeshPart>();
var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var nodeIndex = 0; nodeIndex < nodeArray.GetArrayLength(); nodeIndex++)
{
var node = nodeArray[nodeIndex];
if (!node.TryGetProperty("mesh", out var meshProperty))
continue;
var meshIndex = meshProperty.GetInt32();
if (meshIndex < 0 || meshIndex >= meshArray.GetArrayLength())
throw new FormatException($"glTF node {nodeIndex} references invalid mesh {meshIndex}.");
var mesh = meshArray[meshIndex];
if (!mesh.TryGetProperty("primitives", out var primitives))
continue;
var skinIndex = node.TryGetProperty("skin", out var skinProperty)
? skinProperty.GetInt32() : -1;
var skinJoints = MapSkinJoints(
document, skeleton, bonesByName, skinArray, skinIndex);
var skinTransforms = SkinTransforms(document, skinArray, skinIndex, worlds, skinJoints, skeleton);
var normalMatrix = NormalMatrix(worlds[nodeIndex]);
var primitiveIndex = 0;
foreach (var primitive in primitives.EnumerateArray())
{
if (!primitive.TryGetProperty("attributes", out var attributes)
|| !attributes.TryGetProperty("POSITION", out var positionProperty))
{
primitiveIndex++;
continue;
}
var positions = new Accessor(document, positionProperty.GetInt32(), 3);
var vertexCount = positions.Count;
if (vertexCount == 0)
{
primitiveIndex++;
continue;
}
var transformedPositions = new Vector3[vertexCount];
ReadSkinning(document, attributes, vertexCount, skinJoints,
out var weights, out var joints);
var vertexTransforms = new Matrix4x4[vertexCount];
for (var i = 0; i < vertexCount; i++)
{
var transform = worlds[nodeIndex];
if (skinTransforms is not null)
{
transform = default;
for (var influence = 0; influence < 4; influence++)
{
var at = i * 4 + influence;
if (weights[at] > 0f)
transform += skinTransforms[joints[at]] * weights[at];
}
}
vertexTransforms[i] = transform;
var value = new Vector3(
positions.Float(i, 0), positions.Float(i, 1), positions.Float(i, 2));
transformedPositions[i] = Vector3.Transform(value, transform)
* MetersToCentimeters;
}
var rawIndices = ReadIndices(document, primitive, vertexCount);
var mode = primitive.TryGetProperty("mode", out var modeProperty)
? modeProperty.GetInt32() : 4;
var triangles = Triangulate(rawIndices, mode);
var normals = new Vector3[vertexCount];
if (attributes.TryGetProperty("NORMAL", out var normalProperty))
{
var source = new Accessor(document, normalProperty.GetInt32(), 3);
RequireCount(source, vertexCount, "NORMAL");
for (var i = 0; i < vertexCount; i++)
{
var value = new Vector3(
source.Float(i, 0), source.Float(i, 1), source.Float(i, 2));
var transform = skinTransforms is null ? normalMatrix : NormalMatrix(vertexTransforms[i]);
normals[i] = NormalizeOr(Vector3.TransformNormal(value, transform), Vector3.UnitY);
}
}
else
{
GenerateNormals(transformedPositions, triangles, normals);
}
var texCoords = new Vector2[vertexCount];
if (attributes.TryGetProperty("TEXCOORD_0", out var texCoordProperty))
{
var source = new Accessor(document, texCoordProperty.GetInt32(), 2);
RequireCount(source, vertexCount, "TEXCOORD_0");
for (var i = 0; i < vertexCount; i++)
texCoords[i] = new Vector2(source.Float(i, 0), source.Float(i, 1));
}
var baseName = node.TryGetProperty("name", out var nodeName)
? nodeName.GetString()
: mesh.TryGetProperty("name", out var meshName) ? meshName.GetString() : null;
var partName = UniqueName(
Sanitize(baseName ?? $"mesh_{meshIndex}") + $"_{primitiveIndex}", usedNames);
parts.Add(new MeshPart
{
Name = partName,
Material = MaterialName(materialArray, primitive),
Positions = transformedPositions,
Normals = normals,
TexCoords = texCoords,
Triangles = triangles,
Weights = weights,
Joints = joints,
});
primitiveIndex++;
}
}
return parts;
}
private static Matrix4x4[] NodeWorlds(GltfDocument document)
{
var result = new Matrix4x4[document.Nodes.Count];
var state = new byte[document.Nodes.Count];
Matrix4x4 Visit(int index)
{
if (state[index] == 2)
return result[index];
if (state[index] == 1)
throw new FormatException("glTF node graph contains a cycle.");
state[index] = 1;
var node = document.Nodes[index];
var local = Matrix4x4.CreateScale(node.Scale)
* Matrix4x4.CreateFromQuaternion(node.Rotation)
* Matrix4x4.CreateTranslation(node.Translation);
result[index] = node.Parent < 0 ? local : local * Visit(node.Parent);
state[index] = 2;
return result[index];
}
for (var i = 0; i < result.Length; i++)
Visit(i);
return result;
}
private static Matrix4x4 NormalMatrix(Matrix4x4 world)
{
if (!Matrix4x4.Invert(world, out var inverse))
return Matrix4x4.Identity;
return Matrix4x4.Transpose(inverse);
}
// Bake the authored skin into the node rest pose before DMX generates new inverse
// binds. glTF skinned vertices use inverseBind * jointWorld, NOT meshNodeWorld.
// Keeping the full matrices here also bakes inherited scale into the rigid DMX rig.
private static Dictionary<int, Matrix4x4>? SkinTransforms(
GltfDocument document, JsonElement skins, int skinIndex,
Matrix4x4[] worlds, int[] mappedJoints, SkeletonModel skeleton)
{
if (mappedJoints.Length == 0)
return null;
var skin = skins[skinIndex];
var nodes = skin.GetProperty("joints");
var inverseBinds = skin.TryGetProperty("inverseBindMatrices", out var property)
? new Accessor(document, property.GetInt32(), 16) : null;
if (inverseBinds is not null)
RequireCount(inverseBinds, mappedJoints.Length, "inverseBindMatrices");
var result = new Dictionary<int, Matrix4x4>();
for (var i = 0; i < mappedJoints.Length; i++)
{
var inverse = Matrix4x4.Identity;
if (inverseBinds is not null)
inverse = ReadMatrix(inverseBinds, i);
// The target can use the authored skin bind instead of the posed scene TRS.
// Rebind the mesh to that exact skeleton; retain scale omitted by XForm.
Matrix4x4.Decompose(worlds[nodes[i].GetInt32()], out var scale, out _, out _);
var rest = skeleton.RestWorld[mappedJoints[i]];
var jointWorld = Matrix4x4.CreateScale(scale)
* Matrix4x4.CreateFromQuaternion(rest.Rot)
* Matrix4x4.CreateTranslation(rest.Pos / MetersToCentimeters);
result[mappedJoints[i]] = inverse * jointWorld;
}
return result;
}
internal static SkeletonModel WithSkinBindPose(GltfDocument document, SkeletonModel skeleton)
{
if (!document.Root.TryGetProperty("skins", out var skins))
return skeleton;
var byName = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < skeleton.Count; i++)
byName[Sanitize(skeleton[i].Name)] = i;
var binds = new Dictionary<int, XForm>();
var nodeWorlds = NodeWorlds(document);
for (var s = 0; s < skins.GetArrayLength(); s++)
{
if (!skins[s].TryGetProperty("inverseBindMatrices", out var property))
continue;
var joints = MapSkinJoints(document, skeleton, byName, skins, s);
var accessor = new Accessor(document, property.GetInt32(), 16);
RequireCount(accessor, joints.Length, "inverseBindMatrices");
for (var j = 0; j < joints.Length; j++)
{
if (!Matrix4x4.Invert(ReadMatrix(accessor, j), out var matrix))
throw new FormatException("glTF skin has a singular inverse bind matrix.");
// Some exporters fold a bind-shape scale into these matrices. Those
// are valid for skinning but cannot replace the scene's rigid rest.
// Zero-offset scaffold joints do not describe the character's scale.
if (skeleton[joints[j]].RestLocal.Pos.LengthSquared() > 1e-6f)
{
Matrix4x4.Decompose(matrix, out var bindScale, out _, out _);
var node = skins[s].GetProperty("joints")[j].GetInt32();
Matrix4x4.Decompose(nodeWorlds[node], out var sceneScale, out _, out _);
if (Vector3.Distance(bindScale, sceneScale) > .001f * sceneScale.Length())
return skeleton;
}
var bind = FbxTransform.ToRigid(matrix);
bind.Pos *= MetersToCentimeters;
if (binds.TryGetValue(joints[j], out var previous)
&& (Vector3.Distance(previous.Pos, bind.Pos) > .01f
|| MathQ.AngleBetween(previous.Rot, bind.Rot) > .001f))
return skeleton; // Different per-mesh bind spaces cannot define one rig rest.
binds[joints[j]] = bind;
}
}
if (binds.Count == 0)
return skeleton;
// Bind matrices may use an origin below the displayed scene. Preserve the
// scene's floor placement (glTF is Y-up), rather than burying the new rest.
var sceneFloor = float.PositiveInfinity;
var bindFloor = float.PositiveInfinity;
foreach (var pair in binds)
{
sceneFloor = MathF.Min(sceneFloor, skeleton.RestWorld[pair.Key].Pos.Y);
bindFloor = MathF.Min(bindFloor, pair.Value.Pos.Y);
}
var placement = new Vector3(0f, sceneFloor - bindFloor, 0f);
var world = new XForm[skeleton.Count];
var definitions = new List<BoneDefinition>();
for (var i = 0; i < skeleton.Count; i++)
{
var bone = skeleton[i];
var parent = bone.ParentIndex;
world[i] = binds.TryGetValue(i, out var bind) ? bind
: parent < 0 ? bone.RestLocal : XForm.Compose(world[parent], bone.RestLocal);
if (binds.ContainsKey(i))
world[i].Pos += placement;
var local = parent < 0 ? world[i] : XForm.ToLocal(world[parent], world[i]);
definitions.Add(new BoneDefinition(bone.Name, parent < 0 ? null : skeleton[parent].Name, local));
}
return SkeletonModel.Create(definitions);
}
private static Matrix4x4 ReadMatrix(Accessor source, int i) => new(
source.Float(i, 0), source.Float(i, 1), source.Float(i, 2), source.Float(i, 3),
source.Float(i, 4), source.Float(i, 5), source.Float(i, 6), source.Float(i, 7),
source.Float(i, 8), source.Float(i, 9), source.Float(i, 10), source.Float(i, 11),
source.Float(i, 12), source.Float(i, 13), source.Float(i, 14), source.Float(i, 15));
private static int[] MapSkinJoints(
GltfDocument document, SkeletonModel skeleton, Dictionary<string, int> bonesByName,
JsonElement skinArray, int skinIndex)
{
if (skinIndex < 0)
return Array.Empty<int>();
if (skinArray.ValueKind != JsonValueKind.Array || skinIndex >= skinArray.GetArrayLength())
throw new FormatException($"glTF mesh references invalid skin {skinIndex}.");
var skin = skinArray[skinIndex];
if (!skin.TryGetProperty("joints", out var joints))
return Array.Empty<int>();
var result = new int[joints.GetArrayLength()];
for (var i = 0; i < result.Length; i++)
{
var nodeIndex = joints[i].GetInt32();
if (nodeIndex < 0 || nodeIndex >= document.Nodes.Count)
throw new FormatException($"glTF skin references invalid joint node {nodeIndex}.");
var raw = document.Nodes[nodeIndex].Name ?? $"node_{nodeIndex}";
var safe = Sanitize(raw);
if (!bonesByName.TryGetValue(safe, out var bone)
&& !bonesByName.TryGetValue(Sanitize(raw + "#" + nodeIndex), out bone))
{
throw new FormatException(
$"glTF skin joint '{raw}' is absent from the imported target skeleton.");
}
if (bone < 0 || bone >= skeleton.Count)
throw new FormatException($"glTF skin joint '{raw}' mapped outside the target skeleton.");
result[i] = bone;
}
return result;
}
private static void ReadSkinning(
GltfDocument document, JsonElement attributes, int vertexCount, int[] skinJoints,
out float[] weights, out int[] joints)
{
weights = new float[checked(vertexCount * 4)];
joints = new int[checked(vertexCount * 4)];
if (skinJoints.Length == 0
|| !attributes.TryGetProperty("JOINTS_0", out var jointProperty)
|| !attributes.TryGetProperty("WEIGHTS_0", out var weightProperty))
{
for (var i = 0; i < vertexCount; i++)
{
weights[i * 4] = 1f;
joints[i * 4] = skinJoints.Length > 0 ? skinJoints[0] : 0;
}
return;
}
var jointSource = new Accessor(document, jointProperty.GetInt32(), 4);
var weightSource = new Accessor(document, weightProperty.GetInt32(), 4);
RequireCount(jointSource, vertexCount, "JOINTS_0");
RequireCount(weightSource, vertexCount, "WEIGHTS_0");
for (var vertex = 0; vertex < vertexCount; vertex++)
{
var total = 0f;
for (var influence = 0; influence < 4; influence++)
{
var skinJoint = jointSource.Unsigned(vertex, influence);
if (skinJoint < 0 || skinJoint >= skinJoints.Length)
throw new FormatException($"glTF JOINTS_0 references invalid skin joint {skinJoint}.");
var at = vertex * 4 + influence;
joints[at] = skinJoints[skinJoint];
var weight = weightSource.Float(vertex, influence);
weights[at] = float.IsFinite(weight) && weight > 0f ? weight : 0f;
total += weights[at];
}
if (total <= 1e-8f)
{
weights[vertex * 4] = 1f;
joints[vertex * 4] = skinJoints[0];
continue;
}
for (var influence = 0; influence < 4; influence++)
weights[vertex * 4 + influence] /= total;
}
}
private static int[] ReadIndices(
GltfDocument document, JsonElement primitive, int vertexCount)
{
if (!primitive.TryGetProperty("indices", out var indexProperty))
{
var sequential = new int[vertexCount];
for (var i = 0; i < sequential.Length; i++)
sequential[i] = i;
return sequential;
}
var source = new Accessor(document, indexProperty.GetInt32(), 1);
var result = new int[source.Count];
for (var i = 0; i < result.Length; i++)
{
result[i] = source.Unsigned(i, 0);
if (result[i] < 0 || result[i] >= vertexCount)
throw new FormatException($"glTF index {result[i]} exceeds vertex count {vertexCount}.");
}
return result;
}
private static int[] Triangulate(int[] indices, int mode)
{
var triangles = new List<int>();
if (mode == 4) // TRIANGLES
{
if (indices.Length % 3 != 0)
throw new FormatException("glTF triangle index count is not divisible by three.");
triangles.AddRange(indices);
}
else if (mode == 5) // TRIANGLE_STRIP
{
for (var i = 2; i < indices.Length; i++)
{
var a = indices[i - 2];
var b = indices[i - 1];
var c = indices[i];
if ((i & 1) != 0)
(a, b) = (b, a);
if (a != b && b != c && a != c)
{
triangles.Add(a);
triangles.Add(b);
triangles.Add(c);
}
}
}
else if (mode == 6) // TRIANGLE_FAN
{
for (var i = 2; i < indices.Length; i++)
{
if (indices[0] == indices[i - 1] || indices[i - 1] == indices[i]
|| indices[0] == indices[i])
continue;
triangles.Add(indices[0]);
triangles.Add(indices[i - 1]);
triangles.Add(indices[i]);
}
}
else
{
throw new FormatException($"glTF primitive mode {mode} is not a triangle mesh.");
}
return triangles.ToArray();
}
private static void GenerateNormals(Vector3[] positions, int[] triangles, Vector3[] normals)
{
for (var i = 0; i + 2 < triangles.Length; i += 3)
{
var a = triangles[i];
var b = triangles[i + 1];
var c = triangles[i + 2];
var normal = Vector3.Cross(positions[b] - positions[a], positions[c] - positions[a]);
normals[a] += normal;
normals[b] += normal;
normals[c] += normal;
}
for (var i = 0; i < normals.Length; i++)
normals[i] = NormalizeOr(normals[i], Vector3.UnitY);
}
private static Vector3 NormalizeOr(Vector3 value, Vector3 fallback)
=> value.LengthSquared() > 1e-12f ? Vector3.Normalize(value) : fallback;
private static void RequireCount(Accessor accessor, int expected, string semantic)
{
if (accessor.Count != expected)
throw new FormatException(
$"glTF {semantic} has {accessor.Count} entries; expected {expected}.");
}
private static string MaterialName(JsonElement materials, JsonElement primitive)
{
if (!primitive.TryGetProperty("material", out var materialProperty))
return "default";
var index = materialProperty.GetInt32();
if (materials.ValueKind != JsonValueKind.Array || index < 0 || index >= materials.GetArrayLength())
throw new FormatException($"glTF primitive references invalid material {index}.");
var material = materials[index];
return material.TryGetProperty("name", out var name) && !string.IsNullOrEmpty(name.GetString())
? name.GetString()!
: $"material_{index}";
}
private static string Sanitize(string value)
{
var result = new StringBuilder(value.Length);
foreach (var c in value)
result.Append(char.IsLetterOrDigit(c) || c == '_' ? c : '_');
return result.Length > 0 ? result.ToString() : "unnamed";
}
private static string UniqueName(string value, HashSet<string> used)
{
var candidate = value;
var suffix = 2;
while (!used.Add(candidate))
candidate = value + "_" + suffix++;
return candidate;
}
private sealed class Accessor
{
private readonly byte[] _buffer;
private readonly int _start;
private readonly int _stride;
private readonly int _componentSize;
private readonly int _componentType;
private readonly bool _normalized;
public int Count { get; }
public int Components { get; }
public Accessor(GltfDocument document, int index, int expectedComponents)
{
var root = document.Root;
if (!root.TryGetProperty("accessors", out var accessors)
|| index < 0 || index >= accessors.GetArrayLength())
throw new FormatException($"glTF accessor {index} does not exist.");
var accessor = accessors[index];
if (accessor.TryGetProperty("sparse", out _))
throw new FormatException("Sparse glTF mesh accessors are not supported.");
Components = accessor.GetProperty("type").GetString() switch
{
"SCALAR" => 1,
"VEC2" => 2,
"VEC3" => 3,
"VEC4" => 4,
"MAT4" => 16,
var type => throw new FormatException($"Unsupported glTF accessor type '{type}'."),
};
if (Components != expectedComponents)
throw new FormatException(
$"glTF accessor {index} has {Components} components; expected {expectedComponents}.");
Count = accessor.GetProperty("count").GetInt32();
if (Count < 0)
throw new FormatException($"glTF accessor {index} has a negative count.");
_componentType = accessor.GetProperty("componentType").GetInt32();
_componentSize = _componentType switch
{
5120 or 5121 => 1,
5122 or 5123 => 2,
5125 or 5126 => 4,
_ => throw new FormatException(
$"Unsupported glTF accessor component type {_componentType}."),
};
_normalized = accessor.TryGetProperty("normalized", out var normalized)
&& normalized.GetBoolean();
if (!accessor.TryGetProperty("bufferView", out var viewProperty))
{
_buffer = Array.Empty<byte>();
_start = 0;
_stride = checked(Components * _componentSize);
return;
}
var views = root.GetProperty("bufferViews");
var viewIndex = viewProperty.GetInt32();
if (viewIndex < 0 || viewIndex >= views.GetArrayLength())
throw new FormatException($"glTF bufferView {viewIndex} does not exist.");
var view = views[viewIndex];
var bufferIndex = view.GetProperty("buffer").GetInt32();
if (bufferIndex < 0 || bufferIndex >= document.Buffers.Count)
throw new FormatException($"glTF buffer {bufferIndex} does not exist.");
_buffer = document.Buffers[bufferIndex];
var viewOffset = view.TryGetProperty("byteOffset", out var vo) ? vo.GetInt32() : 0;
var accessorOffset = accessor.TryGetProperty("byteOffset", out var ao) ? ao.GetInt32() : 0;
_start = checked(viewOffset + accessorOffset);
var elementSize = checked(Components * _componentSize);
_stride = view.TryGetProperty("byteStride", out var stride)
? stride.GetInt32() : elementSize;
if (_stride < elementSize)
throw new FormatException("glTF accessor stride is smaller than its element.");
var end = Count == 0 ? _start : (long)_start + (long)(Count - 1) * _stride + elementSize;
if (_start < 0 || end > _buffer.Length)
throw new FormatException($"glTF accessor {index} reads beyond its buffer.");
}
public float Float(int element, int component)
{
if (_buffer.Length == 0)
return 0f;
var offset = Offset(element, component);
return _componentType switch
{
5120 => _normalized
? MathF.Max(unchecked((sbyte)_buffer[offset]) / 127f, -1f)
: unchecked((sbyte)_buffer[offset]),
5121 => _normalized ? _buffer[offset] / 255f : _buffer[offset],
5122 => _normalized
? MathF.Max(BitConverter.ToInt16(_buffer, offset) / 32767f, -1f)
: BitConverter.ToInt16(_buffer, offset),
5123 => _normalized
? BitConverter.ToUInt16(_buffer, offset) / 65535f
: BitConverter.ToUInt16(_buffer, offset),
5125 => BitConverter.ToUInt32(_buffer, offset),
_ => BitConverter.ToSingle(_buffer, offset),
};
}
public int Unsigned(int element, int component)
{
if (_buffer.Length == 0)
return 0;
var offset = Offset(element, component);
return _componentType switch
{
5121 => _buffer[offset],
5123 => BitConverter.ToUInt16(_buffer, offset),
5125 => checked((int)BitConverter.ToUInt32(_buffer, offset)),
_ => throw new FormatException(
$"glTF indices require an unsigned integer accessor, got {_componentType}."),
};
}
private int Offset(int element, int component)
{
if (element < 0 || element >= Count || component < 0 || component >= Components)
throw new FormatException("glTF accessor index is out of range.");
return checked(_start + element * _stride + component * _componentSize);
}
}
private static string Emit(SkeletonModel skeleton, string name, IReadOnlyList<MeshPart> parts)
{
var writer = new Kv2Writer();
var modelId = Id(name, "model");
var jointIds = new string[skeleton.Count];
for (var i = 0; i < skeleton.Count; i++)
jointIds[i] = Id(name, "joint:" + skeleton[i].Name);
var dagIds = new string[parts.Count];
var meshIds = new string[parts.Count];
var vertexIds = new string[parts.Count];
for (var i = 0; i < parts.Count; i++)
{
dagIds[i] = Id(name, $"dag:{i}:{parts[i].Name}");
meshIds[i] = Id(name, $"mesh:{i}:{parts[i].Name}");
vertexIds[i] = Id(name, $"vertices:{i}:{parts[i].Name}");
}
writer.Raw("<!-- dmx encoding keyvalues2_noids 4 format model 22 -->");
writer.BeginTop("DmElement");
writer.Attr("name", "string", "root");
writer.Attr("model", "element", modelId);
writer.Attr("skeleton", "element", modelId);
writer.EndTop();
writer.BeginTop("DmeModel");
writer.Attr("id", "elementid", modelId);
writer.Attr("name", "string", name);
WriteTransform(writer, "transform", Vector3.Zero, Quaternion.Identity);
writer.Attr("visible", "bool", "1");
var children = new List<string>();
for (var i = 0; i < skeleton.Count; i++)
if (skeleton[i].ParentIndex < 0)
children.Add(jointIds[i]);
children.AddRange(dagIds);
WriteRefs(writer, "children", children);
WriteRefs(writer, "jointList", jointIds);
writer.Attr("upAxis", "string", "Y");
writer.BeginInline("axisSystem", "DmeAxisSystem");
writer.Attr("upAxis", "int", "2");
writer.Attr("forwardParity", "int", "2");
writer.Attr("coordSys", "int", "0");
writer.EndInline();
writer.EndTop();
for (var i = 0; i < skeleton.Count; i++)
{
var bone = skeleton[i];
writer.BeginTop("DmeJoint");
writer.Attr("id", "elementid", jointIds[i]);
writer.Attr("name", "string", bone.Name);
WriteTransform(writer, "transform", bone.RestLocal.Pos, bone.RestLocal.Rot);
writer.Attr("visible", "bool", "1");
var boneChildren = new List<string>();
for (var child = 0; child < skeleton.Count; child++)
if (skeleton[child].ParentIndex == i)
boneChildren.Add(jointIds[child]);
if (boneChildren.Count > 0)
WriteRefs(writer, "children", boneChildren);
writer.EndTop();
}
for (var i = 0; i < parts.Count; i++)
{
var part = parts[i];
writer.BeginTop("DmeDag");
writer.Attr("id", "elementid", dagIds[i]);
writer.Attr("name", "string", part.Name);
WriteTransform(writer, "transform", Vector3.Zero, Quaternion.Identity);
writer.Attr("shape", "element", meshIds[i]);
writer.Attr("visible", "bool", "1");
writer.EndTop();
writer.BeginTop("DmeMesh");
writer.Attr("id", "elementid", meshIds[i]);
writer.Attr("name", "string", part.Name);
writer.Attr("visible", "bool", "1");
writer.Attr("currentState", "element", vertexIds[i]);
WriteRefs(writer, "baseStates", new[] { vertexIds[i] });
writer.BeginArray("faceSets");
writer.BeginArrayElement("DmeFaceSet");
writer.Attr("name", "string", part.Material);
writer.BeginArray("faces", "int_array");
for (var index = 0; index < part.Triangles.Length; index++)
{
writer.Value(part.Triangles[index].ToString(CultureInfo.InvariantCulture), false);
if (index % 3 == 2)
writer.Value("-1", index == part.Triangles.Length - 1);
}
writer.EndArray();
writer.BeginInline("material", "DmeMaterial");
writer.Attr("name", "string", part.Material);
writer.Attr("mtlName", "string", part.Material);
writer.EndInline();
writer.EndArrayElement(true);
writer.EndArray();
writer.EndTop();
writer.BeginTop("DmeVertexData");
writer.Attr("id", "elementid", vertexIds[i]);
writer.Attr("name", "string", "bind");
writer.BeginArray("vertexFormat", "string_array");
var formats = new[]
{ "position$0", "normal$0", "texcoord$0", "blendweights$0", "blendindices$0" };
for (var format = 0; format < formats.Length; format++)
writer.Value(formats[format], format == formats.Length - 1);
writer.EndArray();
writer.Attr("jointCount", "int", "4");
writer.Attr("flipVCoordinates", "bool", "0");
WriteVectors(writer, "position$0", "vector3_array", part.Positions,
value => Vec(value));
WriteIdentityIndices(writer, "position$0Indices", part.Positions.Length);
WriteVectors(writer, "normal$0", "vector3_array", part.Normals,
value => Vec(value));
WriteIdentityIndices(writer, "normal$0Indices", part.Normals.Length);
WriteVectors(writer, "texcoord$0", "vector2_array", part.TexCoords,
value => $"{F(value.X)} {F(value.Y)}");
WriteIdentityIndices(writer, "texcoord$0Indices", part.TexCoords.Length);
WriteScalars(writer, "blendweights$0", "float_array", part.Weights,
value => F(value));
WriteScalars(writer, "blendindices$0", "int_array", part.Joints,
value => value.ToString(CultureInfo.InvariantCulture));
writer.EndTop();
}
return writer.ToString();
}
private static void WriteTransform(
Kv2Writer writer, string name, Vector3 position, Quaternion orientation)
{
writer.BeginInline(name, "DmeTransform");
writer.Attr("name", "string", name);
writer.Attr("position", "vector3", Vec(position));
writer.Attr("orientation", "quaternion",
$"{F(orientation.X)} {F(orientation.Y)} {F(orientation.Z)} {F(orientation.W)}");
writer.Attr("scale", "float", "1");
writer.EndInline();
}
private static void WriteRefs(Kv2Writer writer, string name, IReadOnlyList<string> ids)
{
writer.BeginArray(name);
for (var i = 0; i < ids.Count; i++)
writer.ElementRef(ids[i], i == ids.Count - 1);
writer.EndArray();
}
private static void WriteVectors<T>(
Kv2Writer writer, string name, string type, T[] values, Func<T, string> format)
{
writer.BeginArray(name, type);
for (var i = 0; i < values.Length; i++)
writer.Value(format(values[i]), i == values.Length - 1);
writer.EndArray();
}
private static void WriteScalars<T>(
Kv2Writer writer, string name, string type, T[] values, Func<T, string> format)
=> WriteVectors(writer, name, type, values, format);
private static void WriteIdentityIndices(Kv2Writer writer, string name, int count)
{
writer.BeginArray(name, "int_array");
for (var i = 0; i < count; i++)
writer.Value(i.ToString(CultureInfo.InvariantCulture), i == count - 1);
writer.EndArray();
}
private static string Id(string name, string path)
=> DmxWriter.ElementGuid(name, "gltf-model:" + path)
.ToString("D", CultureInfo.InvariantCulture);
private static string F(float value)
=> value == 0f ? "0" : ((double)value).ToString("0.##########", CultureInfo.InvariantCulture);
private static string Vec(Vector3 value) => $"{F(value.X)} {F(value.Y)} {F(value.Z)}";
private sealed class Kv2Writer
{
private readonly StringBuilder _text = new();
private int _indent;
public void Raw(string value) => _text.Append(value).Append("\r\n");
private void Line(string value)
=> _text.Append('\t', _indent).Append(value).Append("\r\n");
public void Attr(string name, string type, string value)
=> Line($"\"{Escape(name)}\" \"{type}\" \"{Escape(value)}\"");
public void BeginTop(string type)
{
Line($"\"{type}\"");
Line("{");
_indent++;
}
public void EndTop()
{
_indent--;
Line("}");
_text.Append("\r\n");
}
public void BeginInline(string name, string type)
{
Line($"\"{Escape(name)}\" \"{type}\"");
Line("{");
_indent++;
}
public void EndInline()
{
_indent--;
Line("}");
}
public void BeginArray(string name, string type = "element_array")
{
Line($"\"{Escape(name)}\" \"{type}\"");
Line("[");
_indent++;
}
public void EndArray()
{
_indent--;
Line("]");
}
public void BeginArrayElement(string type)
{
Line($"\"{type}\"");
Line("{");
_indent++;
}
public void EndArrayElement(bool last)
{
_indent--;
Line(last ? "}" : "},");
}
public void ElementRef(string id, bool last)
=> Line($"\"element\" \"{id}\"" + (last ? "" : ","));
public void Value(string value, bool last)
=> Line($"\"{Escape(value)}\"" + (last ? "" : ","));
private static string Escape(string value)
=> value.Replace("\\", "\\\\").Replace("\"", "\\\"")
.Replace("\r", " ").Replace("\n", " ");
public override string ToString() => _text.ToString();
}
}