Clean up models

This commit is contained in:
ackwell 2024-01-06 23:13:34 +11:00
parent 1a1c662364
commit 13d594ca87
2 changed files with 226 additions and 250 deletions

View file

@ -0,0 +1,224 @@
using Lumina.Data.Parsing;
using Penumbra.GameData.Files;
using SharpGLTF.Schema2;
namespace Penumbra.Import.Models.Import;
public partial class ModelImporter
{
public static MdlFile Import(ModelRoot model)
{
var importer = new ModelImporter(model);
return importer.Create();
}
// NOTE: This is intended to match TexTool's grouping regex, ".*[_ ^]([0-9]+)[\\.\\-]?([0-9]+)?$"
[GeneratedRegex(@"[_ ^](?'Mesh'[0-9]+)[.-]?(?'SubMesh'[0-9]+)?$", RegexOptions.Compiled)]
private static partial Regex MeshNameGroupingRegex();
private readonly ModelRoot _model;
private List<MdlStructs.MeshStruct> _meshes = new();
private List<MdlStructs.SubmeshStruct> _subMeshes = new();
private List<MdlStructs.VertexDeclarationStruct> _vertexDeclarations = new();
private List<byte> _vertexBuffer = new();
private List<ushort> _indices = new();
private List<string> _bones = new();
private List<MdlStructs.BoneTableStruct> _boneTables = new();
private Dictionary<string, List<MdlStructs.ShapeMeshStruct>> _shapeMeshes = new();
private List<MdlStructs.ShapeValueStruct> _shapeValues = new();
private ModelImporter(ModelRoot model)
{
_model = model;
}
private MdlFile Create()
{
// Group and build out meshes in this model.
foreach (var subMeshNodes in GroupedMeshNodes())
BuildMeshForGroup(subMeshNodes);
// Now that all of the meshes have been built, we can build some of the model-wide metadata.
var shapes = new List<MdlFile.Shape>();
var shapeMeshes = new List<MdlStructs.ShapeMeshStruct>();
foreach (var (keyName, keyMeshes) in _shapeMeshes)
{
shapes.Add(new MdlFile.Shape()
{
ShapeName = keyName,
// NOTE: these values are per-LoD.
ShapeMeshStartIndex = [(ushort)shapeMeshes.Count, 0, 0],
ShapeMeshCount = [(ushort)keyMeshes.Count, 0, 0],
});
shapeMeshes.AddRange(keyMeshes);
}
var indexBuffer = _indices.SelectMany(BitConverter.GetBytes).ToArray();
var emptyBoundingBox = new MdlStructs.BoundingBoxStruct()
{
Min = [0, 0, 0, 0],
Max = [0, 0, 0, 0],
};
// And finally, the MdlFile itself.
return new MdlFile()
{
VertexOffset = [0, 0, 0],
VertexBufferSize = [(uint)_vertexBuffer.Count, 0, 0],
IndexOffset = [(uint)_vertexBuffer.Count, 0, 0],
IndexBufferSize = [(uint)indexBuffer.Length, 0, 0],
VertexDeclarations = _vertexDeclarations.ToArray(),
Meshes = _meshes.ToArray(),
SubMeshes = _subMeshes.ToArray(),
BoneTables = _boneTables.ToArray(),
Bones = _bones.ToArray(),
// TODO: Game doesn't seem to rely on this, but would be good to populate.
SubMeshBoneMap = [],
Shapes = shapes.ToArray(),
ShapeMeshes = shapeMeshes.ToArray(),
ShapeValues = _shapeValues.ToArray(),
LodCount = 1,
Lods = [new MdlStructs.LodStruct()
{
MeshIndex = 0,
MeshCount = (ushort)_meshes.Count,
ModelLodRange = 0,
TextureLodRange = 0,
VertexDataOffset = 0,
VertexBufferSize = (uint)_vertexBuffer.Count,
IndexDataOffset = (uint)_vertexBuffer.Count,
IndexBufferSize = (uint)indexBuffer.Length,
}],
// TODO: Would be good to populate from gltf material names.
Materials = ["/NO_MATERIAL"],
// TODO: Would be good to calculate all of this up the tree.
Radius = 1,
BoundingBoxes = emptyBoundingBox,
BoneBoundingBoxes = Enumerable.Repeat(emptyBoundingBox, _bones.Count).ToArray(),
RemainingData = [.._vertexBuffer, ..indexBuffer],
};
}
/// <summary> Returns an iterator over sorted, grouped mesh nodes. </summary>
private IEnumerable<IEnumerable<Node>> GroupedMeshNodes() =>
_model.LogicalNodes
.Where(node => node.Mesh != null)
.Select(node =>
{
var name = node.Name ?? node.Mesh.Name ?? "NOMATCH";
var match = MeshNameGroupingRegex().Match(name);
return (node, match);
})
.Where(pair => pair.match.Success)
.OrderBy(pair =>
{
var subMeshGroup = pair.match.Groups["SubMesh"];
return subMeshGroup.Success ? int.Parse(subMeshGroup.Value) : 0;
})
.GroupBy(
pair => int.Parse(pair.match.Groups["Mesh"].Value),
pair => pair.node
)
.OrderBy(group => group.Key);
private void BuildMeshForGroup(IEnumerable<Node> subMeshNodes)
{
// Record some offsets we'll be using later, before they get mutated with mesh values.
var subMeshOffset = _subMeshes.Count;
var vertexOffset = _vertexBuffer.Count;
var indexOffset = _indices.Count;
var shapeValueOffset = _shapeValues.Count;
var mesh = MeshImporter.Import(subMeshNodes);
var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset);
// If no bone table is used for a mesh, the index is set to 255.
var boneTableIndex = 255;
if (mesh.Bones != null)
boneTableIndex = BuildBoneTable(mesh.Bones);
_meshes.Add(mesh.MeshStruct with
{
SubMeshIndex = (ushort)(mesh.MeshStruct.SubMeshIndex + subMeshOffset),
BoneTableIndex = (ushort)boneTableIndex,
StartIndex = meshStartIndex,
VertexBufferOffset = mesh.MeshStruct.VertexBufferOffset
.Select(offset => (uint)(offset + vertexOffset))
.ToArray(),
});
foreach (var subMesh in mesh.SubMeshStructs)
_subMeshes.Add(subMesh with
{
IndexOffset = (uint)(subMesh.IndexOffset + indexOffset),
});
_vertexDeclarations.Add(mesh.VertexDeclaration);
_vertexBuffer.AddRange(mesh.VertexBuffer);
_indices.AddRange(mesh.Indicies);
foreach (var meshShapeKey in mesh.ShapeKeys)
{
if (!_shapeMeshes.TryGetValue(meshShapeKey.Name, out var shapeMeshes))
{
shapeMeshes = new();
_shapeMeshes.Add(meshShapeKey.Name, shapeMeshes);
}
shapeMeshes.Add(meshShapeKey.ShapeMesh with
{
MeshIndexOffset = meshStartIndex,
ShapeValueOffset = (uint)shapeValueOffset,
});
_shapeValues.AddRange(meshShapeKey.ShapeValues);
}
}
private ushort BuildBoneTable(List<string> boneNames)
{
var boneIndices = new List<ushort>();
foreach (var boneName in boneNames)
{
var boneIndex = _bones.IndexOf(boneName);
if (boneIndex == -1)
{
boneIndex = _bones.Count;
_bones.Add(boneName);
}
boneIndices.Add((ushort)boneIndex);
}
if (boneIndices.Count > 64)
throw new Exception("XIV does not support meshes weighted to more than 64 bones.");
var boneIndicesArray = new ushort[64];
Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count);
var boneTableIndex = _boneTables.Count;
_boneTables.Add(new MdlStructs.BoneTableStruct()
{
BoneIndex = boneIndicesArray,
BoneCount = (byte)boneIndices.Count,
});
return (ushort)boneTableIndex;
}
}

View file

@ -1,20 +1,15 @@
using Dalamud.Plugin.Services;
using Lumina.Data.Parsing;
using OtterGui;
using OtterGui.Tasks;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Files;
using Penumbra.Import.Models.Export;
using Penumbra.Import.Models.Import;
using SharpGLTF.Geometry;
using SharpGLTF.Geometry.VertexTypes;
using SharpGLTF.Materials;
using SharpGLTF.Scenes;
using SharpGLTF.Schema2;
namespace Penumbra.Import.Models;
public sealed partial class ModelManager : SingleTaskQueue, IDisposable
public sealed class ModelManager : SingleTaskQueue, IDisposable
{
private readonly IFramework _framework;
private readonly IDataManager _gameData;
@ -125,10 +120,6 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
private partial class ImportGltfAction : IAction
{
// TODO: clean this up a bit, i don't actually need all of it.
[GeneratedRegex(@".*[_ ^](?'Mesh'[0-9]+)[\\.\\-]?([0-9]+)?$", RegexOptions.Compiled)]
private static partial Regex MeshNameGroupingRegex();
public MdlFile? Out;
public ImportGltfAction()
@ -136,250 +127,11 @@ public sealed partial class ModelManager : SingleTaskQueue, IDisposable
//
}
private ModelRoot Build()
{
// Build a super simple plane as a fake gltf input.
var material = new MaterialBuilder();
var mesh = new MeshBuilder<VertexPositionNormalTangent, VertexColor1Texture2, VertexJoints4>("mesh 0.0");
var prim = mesh.UsePrimitive(material);
var tangent = new Vector4(.5f, .5f, 0, 1);
var vert1 = new VertexBuilder<VertexPositionNormalTangent, VertexColor1Texture2, VertexJoints4>(
new VertexPositionNormalTangent(new Vector3(-1, 0, 1), Vector3.UnitY, tangent),
new VertexColor1Texture2(Vector4.One, Vector2.UnitY, Vector2.Zero),
new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)])
);
var vert2 = new VertexBuilder<VertexPositionNormalTangent, VertexColor1Texture2, VertexJoints4>(
new VertexPositionNormalTangent(new Vector3(1, 0, 1), Vector3.UnitY, tangent),
new VertexColor1Texture2(Vector4.One, Vector2.One, Vector2.Zero),
new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)])
);
var vert3 = new VertexBuilder<VertexPositionNormalTangent, VertexColor1Texture2, VertexJoints4>(
new VertexPositionNormalTangent(new Vector3(-1, 0, -1), Vector3.UnitY, tangent),
new VertexColor1Texture2(Vector4.One, Vector2.Zero, Vector2.Zero),
new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)])
);
var vert4 = new VertexBuilder<VertexPositionNormalTangent, VertexColor1Texture2, VertexJoints4>(
new VertexPositionNormalTangent(new Vector3(1, 0, -1), Vector3.UnitY, tangent),
new VertexColor1Texture2(Vector4.One, Vector2.UnitX, Vector2.Zero),
new VertexJoints4([(0, 1), (0, 0), (0, 0), (0, 0)])
);
prim.AddTriangle(vert2, vert3, vert1);
prim.AddTriangle(vert2, vert4, vert3);
var jKosi = new NodeBuilder("j_kosi");
var scene = new SceneBuilder();
scene.AddNode(jKosi);
scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, [jKosi]);
var model = scene.ToGltf2();
return model;
}
public void Execute(CancellationToken cancel)
{
var model = ModelRoot.Load("C:\\Users\\ackwell\\blender\\gltf-tests\\c0201e6180_top.gltf");
// TODO: for grouping, should probably use `node.name ?? mesh.name`, as which are set seems to depend on the exporter.
// var nodes = model.LogicalNodes
// .Where(node => node.Mesh != null)
// // TODO: I'm just grabbing the first 3, as that will contain 0.0, 0.1, and 1.0. testing, and all that.
// .Take(3);
// tt uses this
// ".*[_ ^]([0-9]+)[\\.\\-]?([0-9]+)?$"
var nodes = model.LogicalNodes
.Where(node => node.Mesh != null)
.Take(6) // this model has all 3 lods in it - the first 6 are the real lod0
.SelectWhere(node =>
{
var name = node.Name ?? node.Mesh.Name;
var match = MeshNameGroupingRegex().Match(name);
return match.Success
? (true, (node, int.Parse(match.Groups["Mesh"].Value)))
: (false, (node, -1));
})
.GroupBy(pair => pair.Item2, pair => pair.node)
.OrderBy(group => group.Key);
// this is a representation of a single LoD
var vertexDeclarations = new List<MdlStructs.VertexDeclarationStruct>();
var bones = new List<string>();
var boneTables = new List<MdlStructs.BoneTableStruct>();
var meshes = new List<MdlStructs.MeshStruct>();
var submeshes = new List<MdlStructs.SubmeshStruct>();
var vertexBuffer = new List<byte>();
var indices = new List<byte>();
var shapeData = new Dictionary<string, List<MdlStructs.ShapeMeshStruct>>();
var shapeValues = new List<MdlStructs.ShapeValueStruct>();
foreach (var submeshnodes in nodes)
{
var boneTableOffset = boneTables.Count;
var meshOffset = meshes.Count;
var subOffset = submeshes.Count;
var vertOffset = vertexBuffer.Count;
var idxOffset = indices.Count;
var shapeValueOffset = shapeValues.Count;
var meshthing = MeshImporter.Import(submeshnodes);
var boneTableIndex = 255;
if (meshthing.Bones != null)
{
var boneIndices = new List<ushort>();
foreach (var mb in meshthing.Bones)
{
var boneIndex = bones.IndexOf(mb);
if (boneIndex == -1)
{
boneIndex = bones.Count;
bones.Add(mb);
}
boneIndices.Add((ushort)boneIndex);
}
if (boneIndices.Count > 64)
throw new Exception("One mesh cannot be weighted to more than 64 bones.");
var boneIndicesArray = new ushort[64];
Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count);
boneTableIndex = boneTableOffset;
boneTables.Add(new MdlStructs.BoneTableStruct()
{
BoneCount = (byte)boneIndices.Count,
BoneIndex = boneIndicesArray,
});
}
vertexDeclarations.Add(meshthing.VertexDeclaration);
var meshStartIndex = (uint)(meshthing.MeshStruct.StartIndex + idxOffset / sizeof(ushort));
meshes.Add(meshthing.MeshStruct with
{
SubMeshIndex = (ushort)(meshthing.MeshStruct.SubMeshIndex + subOffset),
// TODO: should probably define a type for index type hey.
BoneTableIndex = (ushort)boneTableIndex,
StartIndex = meshStartIndex,
VertexBufferOffset = meshthing.MeshStruct.VertexBufferOffset
.Select(offset => (uint)(offset + vertOffset))
.ToArray(),
});
// TODO: could probably do this with linq cleaner
foreach (var xivSubmesh in meshthing.SubMeshStructs)
submeshes.Add(xivSubmesh with
{
// TODO: this will need to keep ticking up for each submesh in the same mesh
IndexOffset = (uint)(xivSubmesh.IndexOffset + idxOffset / sizeof(ushort))
});
vertexBuffer.AddRange(meshthing.VertexBuffer);
indices.AddRange(meshthing.Indicies.SelectMany(index => BitConverter.GetBytes((ushort)index)));
foreach (var shapeKey in meshthing.ShapeKeys)
{
List<MdlStructs.ShapeMeshStruct> keyshapedata;
if (!shapeData.TryGetValue(shapeKey.Name, out keyshapedata))
{
keyshapedata = new();
shapeData.Add(shapeKey.Name, keyshapedata);
}
keyshapedata.Add(shapeKey.ShapeMesh with
{
MeshIndexOffset = meshStartIndex,
ShapeValueOffset = (uint)shapeValueOffset,
});
shapeValues.AddRange(shapeKey.ShapeValues);
}
}
var shapes = new List<MdlFile.Shape>();
var shapeMeshes = new List<MdlStructs.ShapeMeshStruct>();
foreach (var (name, sms) in shapeData)
{
var smOff = shapeMeshes.Count;
shapeMeshes.AddRange(sms);
shapes.Add(new MdlFile.Shape()
{
ShapeName = name,
// TODO: THESE IS PER LOD
ShapeMeshStartIndex = [(ushort)smOff, 0, 0],
ShapeMeshCount = [(ushort)sms.Count, 0, 0],
});
}
var mdl = new MdlFile()
{
Radius = 1,
// todo: lod calcs... probably handled in penum? we probably only need to think about lod0 for actual import workflow.
VertexOffset = [0, 0, 0],
IndexOffset = [(uint)vertexBuffer.Count, 0, 0],
VertexBufferSize = [(uint)vertexBuffer.Count, 0, 0],
IndexBufferSize = [(uint)indices.Count, 0, 0],
LodCount = 1,
BoundingBoxes = new MdlStructs.BoundingBoxStruct()
{
Min = [-1, 0, -1, 1],
Max = [1, 0, 1, 1],
},
VertexDeclarations = vertexDeclarations.ToArray(),
Meshes = meshes.ToArray(),
BoneTables = boneTables.ToArray(),
BoneBoundingBoxes = [
// new MdlStructs.BoundingBoxStruct()
// {
// Min = [
// -0.081672676f,
// -0.113717034f,
// -0.11905348f,
// 1.0f,
// ],
// Max = [
// 0.03941727f,
// 0.09845419f,
// 0.107391916f,
// 1.0f,
// ],
// },
// _would_ be nice if i didn't need to fill out this
new MdlStructs.BoundingBoxStruct()
{
Min = [0, 0, 0, 0],
Max = [0, 0, 0, 0],
}
],
SubMeshes = submeshes.ToArray(),
// TODO pretty sure this is garbage data as far as textools functions
// game clearly doesn't rely on this, but the "correct" values are a listing of the bones used by each submesh
SubMeshBoneMap = [0],
Shapes = shapes.ToArray(),
ShapeMeshes = shapeMeshes.ToArray(),
ShapeValues = shapeValues.ToArray(),
Lods = [new MdlStructs.LodStruct()
{
MeshIndex = 0,
MeshCount = (ushort)meshes.Count,
ModelLodRange = 0,
TextureLodRange = 0,
VertexBufferSize = (uint)vertexBuffer.Count,
VertexDataOffset = 0,
IndexBufferSize = (uint)indices.Count,
IndexDataOffset = (uint)vertexBuffer.Count,
},
],
Bones = bones.ToArray(),
Materials = [
"/mt_c0201e6180_top_a.mtrl",
],
RemainingData = vertexBuffer.Concat(indices).ToArray(),
};
Out = mdl;
Out = ModelImporter.Import(model);
}
public bool Equals(IAction? other)