diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs
index e835fe62..cf7cc975 100644
--- a/Penumbra/Import/Models/Export/MeshExporter.cs
+++ b/Penumbra/Import/Models/Export/MeshExporter.cs
@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using Lumina.Data.Parsing;
using Lumina.Extensions;
+using OtterGui;
using Penumbra.GameData.Files;
using SharpGLTF.Geometry;
using SharpGLTF.Geometry.VertexTypes;
@@ -25,7 +26,6 @@ public class MeshExporter
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);
@@ -73,8 +73,11 @@ public class MeshExporter
// If there's skinning usages but no bone mapping, there's probably something wrong with the data.
if (_skinningType != typeof(VertexEmpty) && _boneIndexMap == null)
Penumbra.Log.Warning($"Mesh {meshIndex} has skinned vertex usages but no bone information was provided.");
+
+ Penumbra.Log.Debug($"Mesh {meshIndex} using vertex types geometry: {_geometryType.Name}, material: {_materialType.Name}, skinning: {_skinningType.Name}");
}
+ /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provdied.
private Dictionary? BuildBoneIndexMap(Dictionary boneNameMap)
{
// A BoneTableIndex of 255 means that this mesh is not skinned.
@@ -97,6 +100,7 @@ public class MeshExporter
return indexMap;
}
+ /// Build glTF meshes for this XIV mesh.
private IMeshBuilder[] BuildMeshes()
{
var indices = BuildIndices();
@@ -105,16 +109,19 @@ public class MeshExporter
// NOTE: Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh, so we're specifying the index base relative to the mesh's base.
if (XivMesh.SubMeshCount == 0)
- return [BuildMesh(indices, vertices, 0, (int)XivMesh.IndexCount)];
+ return [BuildMesh($"mesh {_meshIndex}", indices, vertices, 0, (int)XivMesh.IndexCount)];
return _mdl.SubMeshes
.Skip(XivMesh.SubMeshIndex)
.Take(XivMesh.SubMeshCount)
- .Select(submesh => BuildMesh(indices, vertices, (int)(submesh.IndexOffset - XivMesh.StartIndex), (int)submesh.IndexCount))
+ .WithIndex()
+ .Select(submesh => BuildMesh($"mesh {_meshIndex}.{submesh.Index}", indices, vertices, (int)(submesh.Value.IndexOffset - XivMesh.StartIndex), (int)submesh.Value.IndexCount))
.ToArray();
}
+ /// Build a mesh from the provided indices and vertices. A subset of the full indices may be built by providing an index base and count.
private IMeshBuilder BuildMesh(
+ string name,
IReadOnlyList indices,
IReadOnlyList vertices,
int indexBase,
@@ -127,7 +134,7 @@ public class MeshExporter
_materialType,
_skinningType
);
- var meshBuilder = (IMeshBuilder)Activator.CreateInstance(meshBuilderType, $"mesh{_meshIndex}")!;
+ var meshBuilder = (IMeshBuilder)Activator.CreateInstance(meshBuilderType, name)!;
// TODO: share materials &c
var materialBuilder = new MaterialBuilder()
@@ -191,6 +198,7 @@ public class MeshExporter
return meshBuilder;
}
+ /// Read in the indices for this mesh.
private IReadOnlyList BuildIndices()
{
var reader = new BinaryReader(new MemoryStream(_mdl.RemainingData));
@@ -198,6 +206,7 @@ public class MeshExporter
return reader.ReadStructuresAsArray((int)XivMesh.IndexCount);
}
+ /// Build glTF-compatible vertex data for all vertices in this mesh.
private IReadOnlyList BuildVertices()
{
var vertexBuilderType = typeof(VertexBuilder<,,>)
@@ -224,7 +233,7 @@ public class MeshExporter
attributes.Clear();
foreach (var (usage, element) in sortedElements)
- attributes[usage] = ReadVertexAttribute(streams[element.Stream], element);
+ attributes[usage] = ReadVertexAttribute((MdlFile.VertexType)element.Type, streams[element.Stream]);
var vertexGeometry = BuildVertexGeometry(attributes);
var vertexMaterial = BuildVertexMaterial(attributes);
@@ -237,9 +246,10 @@ public class MeshExporter
return vertices;
}
- private object ReadVertexAttribute(BinaryReader reader, MdlStructs.VertexElement element)
+ /// Read a vertex attribute of the specified type from a vertex buffer stream.
+ private object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader)
{
- return (MdlFile.VertexType)element.Type switch
+ return type switch
{
MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
@@ -252,6 +262,7 @@ public class MeshExporter
};
}
+ /// Get the vertex geometry type for this mesh's vertex usages.
private Type GetGeometryType(IReadOnlySet usages)
{
if (!usages.Contains(MdlFile.VertexUsage.Position))
@@ -266,6 +277,7 @@ public class MeshExporter
return typeof(VertexPositionNormalTangent);
}
+ /// Build a geometry vertex from a vertex's attributes.
private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary attributes)
{
if (_geometryType == typeof(VertexPosition))
@@ -289,6 +301,7 @@ public class MeshExporter
throw new Exception($"Unknown geometry type {_geometryType}.");
}
+ /// Get the vertex material type for this mesh's vertex usages.
private Type GetMaterialType(IReadOnlySet usages)
{
// TODO: IIUC, xiv's uv2 is usually represented as the second two components of a vec4 uv attribute - add support.
@@ -306,6 +319,7 @@ public class MeshExporter
};
}
+ /// Build a material vertex from a vertex's attributes.
private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary attributes)
{
if (_materialType == typeof(VertexEmpty))
@@ -326,15 +340,16 @@ public class MeshExporter
throw new Exception($"Unknown material type {_skinningType}");
}
+ /// Get the vertex skinning type for this mesh's vertex usages.
private Type GetSkinningType(IReadOnlySet usages)
{
- // TODO: possibly need to check only index - weight might be missing?
if (usages.Contains(MdlFile.VertexUsage.BlendWeights) && usages.Contains(MdlFile.VertexUsage.BlendIndices))
return typeof(VertexJoints4);
return typeof(VertexEmpty);
}
+ /// Build a skinning vertex from a vertex's attributes.
private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary attributes)
{
if (_skinningType == typeof(VertexEmpty))
@@ -342,17 +357,21 @@ public class MeshExporter
if (_skinningType == typeof(VertexJoints4))
{
- // todo: this shouldn't happen... right? better approach?
if (_boneIndexMap == null)
- throw new Exception("cannot build skinned vertex without index mapping");
+ throw new Exception("Tried to build skinned vertex but no bone mappings are available.");
var indices = ToByteArray(attributes[MdlFile.VertexUsage.BlendIndices]);
var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]);
- // todo: if this throws on the bone index map, the mod is broken, as it contains weights for bones that do not exist.
- // i've not seen any of these that even tt can understand
var bindings = Enumerable.Range(0, 4)
- .Select(index => (_boneIndexMap[indices[index]], weights[index]))
+ .Select(bindingIndex => {
+ // NOTE: I've not seen any files that throw this error that aren't completely broken.
+ var xivBoneIndex = indices[bindingIndex];
+ if (!_boneIndexMap.TryGetValue(xivBoneIndex, out var jointIndex))
+ throw new Exception($"Vertex contains weight for unknown bone index {xivBoneIndex}.");
+
+ return (jointIndex, weights[bindingIndex]);
+ })
.ToArray();
return new VertexJoints4(bindings);
}
@@ -360,10 +379,12 @@ public class MeshExporter
throw new Exception($"Unknown skinning type {_skinningType}");
}
- // Some tangent W values that should be -1 are stored as 0.
+ /// Clamps any tangent W value other than 1 to -1.
+ /// Some XIV models seemingly store -1 as 0, this patches over that.
private Vector4 FixTangentVector(Vector4 tangent)
=> tangent with { W = tangent.W == 1 ? 1 : -1 };
+ /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4.
private Vector2 ToVector2(object data)
=> data switch
{
@@ -373,6 +394,7 @@ public class MeshExporter
_ => throw new ArgumentOutOfRangeException($"Invalid Vector2 input {data}")
};
+ /// Convert a vertex attribute value to a Vector3. Supported inputs are Vector2, Vector3, and Vector4.
private Vector3 ToVector3(object data)
=> data switch
{
@@ -382,6 +404,7 @@ public class MeshExporter
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}")
};
+ /// Convert a vertex attribute value to a Vector4. Supported inputs are Vector2, Vector3, and Vector4.
private Vector4 ToVector4(object data)
=> data switch
{
@@ -391,6 +414,7 @@ public class MeshExporter
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}")
};
+ /// Convert a vertex attribute value to a byte array.
private byte[] ToByteArray(object data)
=> data switch
{
diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs
index c8716cf3..35819e7a 100644
--- a/Penumbra/Import/Models/Export/ModelExporter.cs
+++ b/Penumbra/Import/Models/Export/ModelExporter.cs
@@ -30,6 +30,7 @@ public class ModelExporter
}
}
+ /// Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate.
public static Model Export(MdlFile mdl, XivSkeleton? xivSkeleton)
{
var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null;
@@ -37,6 +38,7 @@ public class ModelExporter
return new Model(meshes, gltfSkeleton);
}
+ /// Convert a .mdl to a mesh (group) per LoD.
private static List ConvertMeshes(MdlFile mdl, GltfSkeleton? skeleton)
{
var meshes = new List();
@@ -56,6 +58,7 @@ public class ModelExporter
return meshes;
}
+ /// Convert XIV skeleton data into a glTF-compatible node tree, with mappings.
private static GltfSkeleton? ConvertSkeleton(XivSkeleton skeleton)
{
NodeBuilder? root = null;
diff --git a/Penumbra/Import/Models/Export/Skeleton.cs b/Penumbra/Import/Models/Export/Skeleton.cs
index 13379dc4..09cdcc32 100644
--- a/Penumbra/Import/Models/Export/Skeleton.cs
+++ b/Penumbra/Import/Models/Export/Skeleton.cs
@@ -2,6 +2,7 @@ using SharpGLTF.Scenes;
namespace Penumbra.Import.Models.Export;
+/// Representation of a skeleton within XIV.
public class XivSkeleton
{
public Bone[] Bones;
@@ -25,9 +26,15 @@ public class XivSkeleton
}
}
+/// Representation of a glTF-compatible skeleton.
public struct GltfSkeleton
{
+ /// Root node of the skeleton.
public NodeBuilder Root;
+
+ /// Flattened list of skeleton nodes.
public NodeBuilder[] Joints;
+
+ /// Mapping of bone names to their index within the joints array.
public Dictionary Names;
}
diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs
index 4f761549..e71b8baf 100644
--- a/Penumbra/Import/Models/ModelManager.cs
+++ b/Penumbra/Import/Models/ModelManager.cs
@@ -88,6 +88,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
gltfModel.SaveGLTF(_outputPath);
}
+ /// Attempt to read out the pertinent information from a .sklb.
private XivSkeleton? BuildSkeleton(CancellationToken cancel)
{
if (_sklb == null)