Move a few things to export subdir

This commit is contained in:
ackwell 2024-01-01 00:18:03 +11:00
parent 6a2b802196
commit 551c25a64c
5 changed files with 169 additions and 87 deletions

View file

@ -6,15 +6,39 @@ using SharpGLTF.Geometry;
using SharpGLTF.Geometry.VertexTypes;
using SharpGLTF.IO;
using SharpGLTF.Materials;
using SharpGLTF.Scenes;
namespace Penumbra.Import.Modules;
namespace Penumbra.Import.Models.Export;
public sealed class MeshConverter
public class MeshExporter
{
public static IMeshBuilder<MaterialBuilder>[] ToGltf(MdlFile mdl, byte lod, ushort meshIndex, Dictionary<string, int>? boneNameMap)
public class Mesh
{
var self = new MeshConverter(mdl, lod, meshIndex, boneNameMap);
return self.BuildMesh();
private IMeshBuilder<MaterialBuilder>[] _meshes;
private NodeBuilder[]? _joints;
public Mesh(IMeshBuilder<MaterialBuilder>[] meshes, NodeBuilder[]? joints)
{
_meshes = meshes;
_joints = joints;
}
public void AddToScene(SceneBuilder scene)
{
// TODO: throw if mesh has skinned vertices but no joints are available?
foreach (var mesh in _meshes)
if (_joints == null)
scene.AddRigidMesh(mesh, Matrix4x4.Identity);
else
scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, _joints);
}
}
// TODO: replace bonenamemap with a gltfskeleton
public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, GltfSkeleton? skeleton)
{
var self = new MeshExporter(mdl, lod, meshIndex, skeleton?.Names);
return new Mesh(self.BuildMeshes(), skeleton?.Joints);
}
private const byte MaximumMeshBufferStreams = 3;
@ -22,14 +46,14 @@ public sealed class MeshConverter
private readonly MdlFile _mdl;
private readonly byte _lod;
private readonly ushort _meshIndex;
private MdlStructs.MeshStruct Mesh => _mdl.Meshes[_meshIndex];
private MdlStructs.MeshStruct XivMesh => _mdl.Meshes[_meshIndex];
private readonly Dictionary<ushort, int>? _boneIndexMap;
private readonly Type _geometryType;
private readonly Type _skinningType;
private MeshConverter(MdlFile mdl, byte lod, ushort meshIndex, Dictionary<string, int>? boneNameMap)
private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, Dictionary<string, int>? boneNameMap)
{
_mdl = mdl;
_lod = lod;
@ -49,7 +73,7 @@ public sealed class MeshConverter
private Dictionary<ushort, int> BuildBoneIndexMap(Dictionary<string, int> boneNameMap)
{
// todo: BoneTableIndex of 255 means null? if so, it should probably feed into the attributes we assign...
var xivBoneTable = _mdl.BoneTables[Mesh.BoneTableIndex];
var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex];
var indexMap = new Dictionary<ushort, int>();
@ -66,16 +90,16 @@ public sealed class MeshConverter
return indexMap;
}
private IMeshBuilder<MaterialBuilder>[] BuildMesh()
private IMeshBuilder<MaterialBuilder>[] BuildMeshes()
{
var indices = BuildIndices();
var vertices = BuildVertices();
// TODO: handle submeshCount = 0
// TODO: handle SubMeshCount = 0
return _mdl.SubMeshes
.Skip(Mesh.SubMeshIndex)
.Take(Mesh.SubMeshCount)
.Skip(XivMesh.SubMeshIndex)
.Take(XivMesh.SubMeshCount)
.Select(submesh => BuildSubMesh(submesh, indices, vertices))
.ToArray();
}
@ -83,7 +107,7 @@ public sealed class MeshConverter
private IMeshBuilder<MaterialBuilder> BuildSubMesh(MdlStructs.SubmeshStruct submesh, IReadOnlyList<ushort> indices, IReadOnlyList<IVertexBuilder> vertices)
{
// Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh.
var startIndex = (int)(submesh.IndexOffset - Mesh.StartIndex);
var startIndex = (int)(submesh.IndexOffset - XivMesh.StartIndex);
var meshBuilderType = typeof(MeshBuilder<,,,>).MakeGenericType(
typeof(MaterialBuilder),
@ -124,7 +148,7 @@ public sealed class MeshConverter
var shapeValues = _mdl.ShapeMeshes
.Skip(shape.ShapeMeshStartIndex[_lod])
.Take(shape.ShapeMeshCount[_lod])
.Where(shapeMesh => shapeMesh.MeshIndexOffset == Mesh.StartIndex)
.Where(shapeMesh => shapeMesh.MeshIndexOffset == XivMesh.StartIndex)
.SelectMany(shapeMesh =>
_mdl.ShapeValues
.Skip((int)shapeMesh.ShapeValueOffset)
@ -158,8 +182,8 @@ public sealed class MeshConverter
private IReadOnlyList<ushort> BuildIndices()
{
var reader = new BinaryReader(new MemoryStream(_mdl.RemainingData));
reader.Seek(_mdl.IndexOffset[_lod] + Mesh.StartIndex * sizeof(ushort));
return reader.ReadStructuresAsArray<ushort>((int)Mesh.IndexCount);
reader.Seek(_mdl.IndexOffset[_lod] + XivMesh.StartIndex * sizeof(ushort));
return reader.ReadStructuresAsArray<ushort>((int)XivMesh.IndexCount);
}
private IReadOnlyList<IVertexBuilder> BuildVertices()
@ -172,7 +196,7 @@ public sealed class MeshConverter
for (var streamIndex = 0; streamIndex < MaximumMeshBufferStreams; streamIndex++)
{
streams[streamIndex] = new BinaryReader(new MemoryStream(_mdl.RemainingData));
streams[streamIndex].Seek(_mdl.VertexOffset[_lod] + Mesh.VertexBufferOffset[streamIndex]);
streams[streamIndex].Seek(_mdl.VertexOffset[_lod] + XivMesh.VertexBufferOffset[streamIndex]);
}
var sortedElements = _mdl.VertexDeclarations[_meshIndex].VertexElements
@ -183,7 +207,7 @@ public sealed class MeshConverter
var vertices = new List<IVertexBuilder>();
var attributes = new Dictionary<MdlFile.VertexUsage, object>();
for (var vertexIndex = 0; vertexIndex < Mesh.VertexCount; vertexIndex++)
for (var vertexIndex = 0; vertexIndex < XivMesh.VertexCount; vertexIndex++)
{
attributes.Clear();

View file

@ -0,0 +1,100 @@
using Penumbra.GameData.Files;
using SharpGLTF.Scenes;
using SharpGLTF.Transforms;
namespace Penumbra.Import.Models.Export;
public class ModelExporter
{
public class Model
{
private List<MeshExporter.Mesh> _meshes;
private GltfSkeleton? _skeleton;
public Model(List<MeshExporter.Mesh> meshes, GltfSkeleton? skeleton)
{
_meshes = meshes;
_skeleton = skeleton;
}
public void AddToScene(SceneBuilder scene)
{
// If there's a skeleton, the root node should be added before we add any potentially skinned meshes.
var skeletonRoot = _skeleton?.Root;
if (skeletonRoot != null)
scene.AddNode(skeletonRoot);
// Add all the meshes to the scene.
foreach (var mesh in _meshes)
mesh.AddToScene(scene);
}
}
public static Model Export(MdlFile mdl, XivSkeleton? xivSkeleton)
{
var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null;
var meshes = ConvertMeshes(mdl, gltfSkeleton);
return new Model(meshes, gltfSkeleton);
}
private static List<MeshExporter.Mesh> ConvertMeshes(MdlFile mdl, GltfSkeleton? skeleton)
{
var meshes = new List<MeshExporter.Mesh>();
for (byte lodIndex = 0; lodIndex < mdl.LodCount; lodIndex++)
{
var lod = mdl.Lods[lodIndex];
// TODO: consider other types of mesh?
for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++)
{
var mesh = MeshExporter.Export(mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), skeleton);
meshes.Add(mesh);
}
}
return meshes;
}
private static GltfSkeleton? ConvertSkeleton(XivSkeleton skeleton)
{
NodeBuilder? root = null;
var names = new Dictionary<string, int>();
var joints = new List<NodeBuilder>();
for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++)
{
var bone = skeleton.Bones[boneIndex];
if (names.ContainsKey(bone.Name)) continue;
var node = new NodeBuilder(bone.Name);
names[bone.Name] = joints.Count;
joints.Add(node);
node.SetLocalTransform(new AffineTransform(
bone.Transform.Scale,
bone.Transform.Rotation,
bone.Transform.Translation
), false);
if (bone.ParentIndex == -1)
{
root = node;
continue;
}
var parent = joints[names[skeleton.Bones[bone.ParentIndex].Name]];
parent.AddNode(node);
}
if (root == null)
return null;
return new()
{
Root = root,
Joints = joints.ToArray(),
Names = names,
};
}
}

View file

@ -1,11 +1,12 @@
namespace Penumbra.Import.Models;
using SharpGLTF.Scenes;
// TODO: this should almost certainly live in gamedata. if not, it should at _least_ be adjacent to the model handling.
public class Skeleton
namespace Penumbra.Import.Models.Export;
public class XivSkeleton
{
public Bone[] Bones;
public Skeleton(Bone[] bones)
public XivSkeleton(Bone[] bones)
{
Bones = bones;
}
@ -23,3 +24,10 @@ public class Skeleton
public Vector3 Translation;
}
}
public struct GltfSkeleton
{
public NodeBuilder Root;
public NodeBuilder[] Joints;
public Dictionary<string, int> Names;
}

View file

@ -2,7 +2,7 @@ using Dalamud.Plugin.Services;
using OtterGui.Tasks;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Files;
using Penumbra.Import.Modules;
using Penumbra.Import.Models.Export;
using SharpGLTF.Scenes;
using SharpGLTF.Transforms;
@ -73,36 +73,18 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
public void Execute(CancellationToken cancel)
{
var xivSkeleton = BuildSkeleton(cancel);
var model = ModelExporter.Export(_mdl, xivSkeleton);
var scene = new SceneBuilder();
model.AddToScene(scene);
var skeleton = BuildSkeleton(cancel);
if (skeleton != null)
scene.AddNode(skeleton.Value.Root);
// TODO: group by LoD in output tree
for (byte lodIndex = 0; lodIndex < _mdl.LodCount; lodIndex++)
{
var lod = _mdl.Lods[lodIndex];
// TODO: consider other types of mesh?
for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++)
{
var meshBuilders = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), skeleton?.Names);
// TODO: use a value from the mesh converter for this check, rather than assuming that it has joints
foreach (var meshBuilder in meshBuilders)
if (skeleton == null)
scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity);
else
scene.AddSkinnedMesh(meshBuilder, Matrix4x4.Identity, skeleton?.Joints);
}
}
var model = scene.ToGltf2();
model.SaveGLTF(_outputPath);
var gltfModel = scene.ToGltf2();
gltfModel.SaveGLTF(_outputPath);
}
// TODO: this should be moved to a seperate model converter or something
private (NodeBuilder Root, NodeBuilder[] Joints, Dictionary<string, int> Names)? BuildSkeleton(CancellationToken cancel)
private XivSkeleton? BuildSkeleton(CancellationToken cancel)
{
if (_sklb == null)
return null;
@ -117,40 +99,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
var skeletonConverter = new SkeletonConverter();
var skeleton = skeletonConverter.FromXml(xml);
// this is (less) atrocious
NodeBuilder? root = null;
var names = new Dictionary<string, int>();
var joints = new List<NodeBuilder>();
for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++)
{
var bone = skeleton.Bones[boneIndex];
if (names.ContainsKey(bone.Name)) continue;
var node = new NodeBuilder(bone.Name);
names[bone.Name] = joints.Count;
joints.Add(node);
node.SetLocalTransform(new AffineTransform(
bone.Transform.Scale,
bone.Transform.Rotation,
bone.Transform.Translation
), false);
if (bone.ParentIndex == -1)
{
root = node;
continue;
}
var parent = joints[names[skeleton.Bones[bone.ParentIndex].Name]];
parent.AddNode(node);
}
if (root == null)
return null;
return (root, joints.ToArray(), names);
return skeleton;
}
public bool Equals(IAction? other)

View file

@ -1,12 +1,13 @@
using System.Xml;
using OtterGui;
using Penumbra.Import.Models.Export;
namespace Penumbra.Import.Models;
// TODO: tempted to say that this living here is more okay? that or next to havok converter, wherever that ends up.
public class SkeletonConverter
{
public Skeleton FromXml(string xml)
public XivSkeleton FromXml(string xml)
{
var document = new XmlDocument();
document.LoadXml(xml);
@ -29,7 +30,7 @@ public class SkeletonConverter
.Select(values =>
{
var (transform, parentIndex, name) = values;
return new Skeleton.Bone()
return new XivSkeleton.Bone()
{
Transform = transform,
ParentIndex = parentIndex,
@ -38,7 +39,7 @@ public class SkeletonConverter
})
.ToArray();
return new Skeleton(bones);
return new XivSkeleton(bones);
}
/// <summary>Get the main skeleton ID for a given skeleton document.</summary>
@ -57,14 +58,14 @@ public class SkeletonConverter
/// <summary>Read the reference pose transforms for a skeleton.</summary>
/// <param name="node">XML node for the skeleton.</param>
private Skeleton.Transform[] ReadReferencePose(XmlNode node)
private XivSkeleton.Transform[] ReadReferencePose(XmlNode node)
{
return ReadArray(
CheckExists(node.SelectSingleNode("array[@name='referencePose']")),
node =>
{
var raw = ReadVec12(node);
return new Skeleton.Transform()
return new XivSkeleton.Transform()
{
Translation = new(raw[0], raw[1], raw[2]),
Rotation = new(raw[4], raw[5], raw[6], raw[7]),