Editor/WeaponImporter/Core/Generation/AnimationDmxWriter.cs
#nullable enable annotations
using System.Numerics;
using WeaponImporter.Core.Formats.Dmx;
using WeaponImporter.Core.Maths;
using WeaponImporter.Core.Rig;
namespace WeaponImporter.Core.Generation;
using Vector3 = System.Numerics.Vector3;
/// <summary>
/// Writes one clip as an animation DMX (<c>keyvalues2_noids</c>) with the element layout of
/// fbx2dmx output: a root DmElement holding an inline DmeModel (joint refs + bind base state),
/// a DmeAnimationList with one DmeChannelsClip carrying a position and an orientation channel
/// per bone, and top-level DmeTransform / DmeJoint elements the channels reference. Joint names
/// come from <see cref="DmxNames.Bones"/>, identical to <see cref="ModelDmxWriter"/>. Output is
/// deterministic (ids hashed from the name, fixed export tags).
/// </summary>
public static class AnimationDmxWriter
{
/// <param name="excludedBones">Bone indices that get no channels (the engine drives them).</param>
public static string Write(Skeleton skeleton, Clip clip, DmxSpace? space, string name, IReadOnlySet<int>? excludedBones = null)
{
ArgumentNullException.ThrowIfNull(skeleton);
ArgumentNullException.ThrowIfNull(clip);
space ??= DmxSpace.SourceZUpInches;
if (clip.FrameCount == 0)
throw new ArgumentException("Clip has no frames.", nameof(clip));
if (skeleton.Count == 0)
throw new ArgumentException("Skeleton has no bones.", nameof(skeleton));
name = string.IsNullOrWhiteSpace(name) ? clip.Name : name;
var boneNames = DmxNames.Bones(skeleton);
var rest = new XForm[skeleton.Count];
for (var i = 0; i < skeleton.Count; i++)
rest[i] = space.Local(skeleton[i].RestLocal, skeleton[i].ParentIndex < 0);
// Frames in file space; missing / zero entries fall back to the rest local.
var frames = new XForm[clip.FrameCount][];
for (var f = 0; f < clip.FrameCount; f++)
{
var src = clip.Frames[f];
var dst = new XForm[skeleton.Count];
for (var i = 0; i < skeleton.Count; i++)
dst[i] = i < src.Length && src[i].Rot.LengthSquared() > 0.5f
? space.Local(new XForm(src[i].Pos, MathQ.Normalize(src[i].Rot)), skeleton[i].ParentIndex < 0)
: rest[i];
frames[f] = dst;
}
var scope = "weapon-anim:" + name;
var animListId = DmxIds.Guid(scope, "animationList");
var jointIds = boneNames.Select(b => DmxIds.Guid(scope, "joint:" + b)).ToArray();
var transformIds = boneNames.Select(b => DmxIds.Guid(scope, "transform:" + b)).ToArray();
var w = new Kv2Writer(blankAfterInline: true);
w.Raw(Kv2Writer.ModelHeader);
w.BeginTop("DmElement");
w.Attr("name", "string", "root");
w.BeginInline("skeleton", "DmeModel");
w.Attr("name", "string", name);
w.BeginInline("transform", "DmeTransform");
w.Attr("position", "vector3", "0 0 0");
w.Attr("orientation", "quaternion", "0 0 0 1");
w.EndInline();
w.Attr("shape", "element", "");
w.Attr("visible", "bool", "1");
var roots = Enumerable.Range(0, skeleton.Count).Where(i => skeleton[i].ParentIndex < 0).Select(i => jointIds[i]).ToList();
w.Refs("children", roots);
w.Refs("jointList", jointIds);
w.BeginArray("baseStates");
w.BeginArrayElement("DmeTransformList");
w.Attr("name", "string", "bind");
w.BeginArray("transforms");
for (var i = 0; i < skeleton.Count; i++)
{
w.BeginArrayElement("DmeTransform");
w.Attr("name", "string", boneNames[i]);
w.Attr("position", "vector3", Kv2Writer.Vec(rest[i].Pos));
w.Attr("orientation", "quaternion", Kv2Writer.Quat(rest[i].Rot));
w.EndArrayElement(i == skeleton.Count - 1);
}
w.EndArray();
w.EndArrayElement(true);
w.EndArray();
space.WriteAxisSystem(w);
w.Attr("animationList", "element", animListId);
w.EndInline(); // skeleton
w.BeginInline("makefile", "DmeDCCMakefile");
w.Attr("name", "string", "makefile");
w.BeginArray("sources");
w.BeginArrayElement("DmeSource");
w.Attr("name", "string", clip.Name);
w.EndArrayElement(true);
w.EndArray();
w.EndInline();
w.BeginInline("exportTags", "DmeExportTags");
w.Attr("name", "string", "exportTags");
w.Attr("date", "string", "2026/01/01");
w.Attr("time", "string", "12:00:00 am");
w.Attr("user", "string", "weaponimporter");
w.Attr("machine", "string", "weaponimporter");
w.Attr("app", "string", "weapon-importer");
w.Attr("appVersion", "string", "1.0");
w.Attr("cmdLine", "string", "weapon-importer");
w.Attr("pwd", "string", "");
w.EndInline();
w.Attr("animationList", "element", animListId);
w.EndTop();
w.BeginTop("DmeAnimationList");
w.Attr("id", "elementid", animListId);
w.Attr("name", "string", "anim");
w.BeginArray("animations");
w.BeginArrayElement("DmeChannelsClip");
w.Attr("name", "string", "anim");
w.BeginInline("timeFrame", "DmeTimeFrame");
w.Attr("start", "time", Kv2Writer.Time(0));
w.Attr("duration", "time", Kv2Writer.Time((clip.FrameCount - 1) / (double)clip.Fps));
w.Attr("offset", "time", Kv2Writer.Time(0));
w.Attr("scale", "float", "1");
w.EndInline();
w.Attr("color", "color", "0 0 0 0");
w.Attr("text", "string", "");
w.Attr("mute", "bool", "0");
w.BeginArray("trackGroups");
w.EndArray();
w.Attr("displayScale", "float", "1");
var channelBones = Enumerable.Range(0, skeleton.Count).Where(i => excludedBones is null || !excludedBones.Contains(i)).ToList();
w.BeginArray("channels");
for (var n = 0; n < channelBones.Count; n++)
{
var i = channelBones[n];
Channel(w, boneNames[i], transformIds[i], frames, i, clip.Fps, position: true, last: false);
Channel(w, boneNames[i], transformIds[i], frames, i, clip.Fps, position: false, last: n == channelBones.Count - 1);
}
w.EndArray();
w.Attr("frameRate", "int", Kv2Writer.I((int)MathF.Round(clip.Fps)));
w.EndArrayElement(true);
w.EndArray();
w.EndTop();
for (var i = 0; i < skeleton.Count; i++)
{
w.BeginTop("DmeTransform");
w.Attr("id", "elementid", transformIds[i]);
w.Attr("name", "string", boneNames[i]);
w.Attr("position", "vector3", Kv2Writer.Vec(rest[i].Pos));
w.Attr("orientation", "quaternion", Kv2Writer.Quat(rest[i].Rot));
w.EndTop();
}
for (var i = 0; i < skeleton.Count; i++)
{
w.BeginTop("DmeJoint");
w.Attr("id", "elementid", jointIds[i]);
w.Attr("name", "string", boneNames[i]);
w.Attr("transform", "element", transformIds[i]);
w.Attr("shape", "element", "");
w.Attr("visible", "bool", "1");
var children = Enumerable.Range(0, skeleton.Count).Where(c => skeleton[c].ParentIndex == i).Select(c => jointIds[c]).ToList();
w.Refs("children", children);
w.EndTop();
}
return w.ToString();
}
private static void Channel(Kv2Writer w, string bone, string transformId, XForm[][] frames, int index, float fps, bool position, bool last)
{
var logClass = position ? "DmeVector3Log" : "DmeQuaternionLog";
var layerClass = position ? "DmeVector3LogLayer" : "DmeQuaternionLogLayer";
var logName = position ? "vector3 log" : "quaternion log";
w.BeginArrayElement("DmeChannel");
w.Attr("name", "string", bone + (position ? "_p" : "_o"));
w.Attr("fromElement", "element", "");
w.Attr("fromAttribute", "string", "");
w.Attr("fromIndex", "int", "0");
w.Attr("toElement", "element", transformId);
w.Attr("toAttribute", "string", position ? "position" : "orientation");
w.Attr("toIndex", "int", "0");
w.Attr("mode", "int", "3");
w.BeginInline("log", logClass);
w.Attr("name", "string", logName);
w.BeginArray("layers");
w.BeginArrayElement(layerClass);
w.Attr("name", "string", logName);
w.BeginArray("times", "time_array");
for (var f = 0; f < frames.Length; f++)
w.Value(Kv2Writer.Time(f / (double)fps), f == frames.Length - 1);
w.EndArray();
w.BeginArray("curvetypes", "int_array");
w.EndArray();
w.BeginArray("values", position ? "vector3_array" : "quaternion_array");
// Hemisphere-align orientations: the engine interpolates samples numerically.
var prev = Quaternion.Identity;
for (var f = 0; f < frames.Length; f++)
{
var x = frames[f][index];
string value;
if (position)
value = Kv2Writer.Vec(x.Pos);
else
{
var q = x.Rot;
if (f > 0 && Quaternion.Dot(prev, q) < 0f)
q = Quaternion.Negate(q);
prev = q;
value = Kv2Writer.Quat(q);
}
w.Value(value, f == frames.Length - 1);
}
w.EndArray();
w.EmptyBinary("compressed");
w.EndArrayElement(true);
w.EndArray();
w.Attr("curveinfo", "element", "");
w.Attr("usedefaultvalue", "bool", "0");
w.Attr("defaultvalue", position ? "vector3" : "quaternion", position ? "0 0 0" : "0 0 0 1");
w.BeginArray("bookmarksX", "time_array");
w.EndArray();
w.BeginArray("bookmarksY", "time_array");
w.EndArray();
w.BeginArray("bookmarksZ", "time_array");
w.EndArray();
w.EndInline();
w.EndArrayElement(last);
}
}