Reads morph targets for a fitted hair mesh from a native resource atlas. It opens a companion resource file, parses a table to find a morph block, decodes a texture atlas and reconstructs per-vertex position and normal deltas into Target records.
using System;
using System.IO;
using System.Text;
namespace CharacterEditor;
/// <summary>Retains native expression targets when extending an authored hair mesh.
/// SaveToVmdl omits MRPH, so read the resource's own morph atlas once per fitted model.</summary>
internal static class SerializedModelMorphs
{
internal sealed record Target(string Name, Vector3[] Positions, Vector3[] Normals);
static Dictionary<string, object> Object(object value) => (Dictionary<string, object>)value;
static List<object> Array(object value) => (List<object>)value;
static int Int(object value) => Convert.ToInt32(value);
static float Float(object value) => Convert.ToSingle(value);
static Vector3 Vector(object value)
{
var a = Array(value);
return new(Float(a[0]), Float(a[1]), Float(a[2]));
}
internal static Target[] Read(Model source, int mesh, int vertexCount, bool identityOnly = false)
{
if (source.MorphCount == 0) return System.Array.Empty<Target>();
var bytes = FileSystem.Mounted.ReadAllBytes(source.ResourcePath + "_c").ToArray();
int table = 8 + BitConverter.ToInt32(bytes, 8), count = BitConverter.ToInt32(bytes, 12);
byte[] Block(int i)
{
int entry = table + 12 * i;
int start = entry + 4 + BitConverter.ToInt32(bytes, entry + 4), size = BitConverter.ToInt32(bytes, entry + 8);
return bytes.AsSpan(start, size).ToArray();
}
int control = Enumerable.Range(0, count).Single(i => Encoding.ASCII.GetString(bytes, table + 12 * i, 4) == "CTRL");
var root = SerializedMeshBones.ReadObject(Block(control));
var embedded = Array(root["embedded_meshes"]).Select(Object).Single(x => Int(x["mesh_index"]) == mesh);
int index = Int(embedded["morph_block"]);
if (index < 0) return System.Array.Empty<Target>();
var data = SerializedMeshBones.ReadObject(Block(index));
if ((string)data["m_nLookupType"] != "LOOKUP_TYPE_VERTEX_ID" || (string)data["m_nEncodingType"] != "ENCODING_TYPE_OBJECT_SPACE")
throw new InvalidDataException("Unsupported hair morph encoding: " + source.Name);
var texture = Texture.Load((string)data["m_pTextureAtlas"]);
if (texture is null || texture.IsError) throw new InvalidDataException("Missing native hair expression atlas: " + source.Name);
texture.MarkUsed(Math.Max(texture.Width, texture.Height));
var pixels = texture.GetPixels();
int width = Int(data["m_nWidth"]), height = Int(data["m_nHeight"]);
if (vertexCount > width * height) throw new InvalidDataException("Hair morph vertex grid is smaller than its mesh.");
var bundles = Array(data["m_bundleTypes"]).Cast<string>().ToArray();
var result = new List<Target>();
foreach (var morph in Array(data["m_morphDatas"]).Select(Object))
{
var name = (string)morph["m_name"];
if (string.IsNullOrWhiteSpace(name)) continue;
if (identityOnly && !name.StartsWith("ce_", StringComparison.Ordinal)) continue;
var positions = new Vector3[vertexCount];
var normals = new Vector3[vertexCount];
foreach (var rect in Array(morph["m_morphRectDatas"]).Select(Object))
{
int dx = Int(rect["m_nXLeftDst"]), dy = Int(rect["m_nYTopDst"]);
int w = (int)MathF.Round(Float(rect["m_flUWidthSrc"]) * texture.Width), h = (int)MathF.Round(Float(rect["m_flVHeightSrc"]) * texture.Height);
var parts = Array(rect["m_bundleDatas"]);
for (int bundle = 0; bundle < parts.Count; bundle++)
{
bool normal = bundles[bundle] == "MORPH_BUNDLE_TYPE_NORMAL_WRINKLE";
if (!normal && bundles[bundle] != "MORPH_BUNDLE_TYPE_POSITION_SPEED") continue;
var part = Object(parts[bundle]);
int sx = (int)MathF.Round(Float(part["m_flULeftSrc"]) * texture.Width), sy = (int)MathF.Round(Float(part["m_flVTopSrc"]) * texture.Height);
var range = Vector(part["m_ranges"]); var offset = Vector(part["m_offsets"]);
float Snap(float v, float r) => MathF.Abs(v) < MathF.Abs(r) / 510 ? 0 : v;
for (int y = 0; y < h; y++) for (int x = 0; x < w; x++)
{
int id = (dy + y) * width + dx + x;
if (dx + x >= width || id < 0 || id >= vertexCount || sx + x >= texture.Width || sy + y >= texture.Height) continue;
var pixel = pixels[(sy + y) * texture.Width + sx + x];
var value = new Vector3(pixel.r, pixel.g, pixel.b) / 255 * range + offset;
value = new(Snap(value.x, range.x), Snap(value.y, range.y), Snap(value.z, range.z));
(normal ? normals : positions)[id] = value;
}
}
}
result.Add(new(name, positions, normals));
}
return result.ToArray();
}
}