mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-15 13:14:17 +01:00
Move mesh logic to new file, export all meshes
This commit is contained in:
parent
ca46e7482f
commit
bc24110c9f
2 changed files with 205 additions and 169 deletions
191
Penumbra/Import/Models/MeshConverter.cs
Normal file
191
Penumbra/Import/Models/MeshConverter.cs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
using System.Collections.Immutable;
|
||||
using Lumina.Data.Parsing;
|
||||
using Lumina.Extensions;
|
||||
using Penumbra.GameData.Files;
|
||||
using SharpGLTF.Geometry;
|
||||
using SharpGLTF.Geometry.VertexTypes;
|
||||
using SharpGLTF.Materials;
|
||||
|
||||
namespace Penumbra.Import.Modules;
|
||||
|
||||
public sealed class MeshConverter
|
||||
{
|
||||
public static IMeshBuilder<MaterialBuilder> ToGltf(MdlFile mdl, byte lod, ushort meshIndex)
|
||||
{
|
||||
var self = new MeshConverter(mdl, lod, meshIndex);
|
||||
return self.BuildMesh();
|
||||
}
|
||||
|
||||
private const byte MaximumMeshBufferStreams = 3;
|
||||
|
||||
private readonly MdlFile _mdl;
|
||||
private readonly byte _lod;
|
||||
private readonly ushort _meshIndex;
|
||||
private MdlStructs.MeshStruct Mesh => _mdl.Meshes[_meshIndex];
|
||||
|
||||
private readonly Type _geometryType;
|
||||
|
||||
private MeshConverter(MdlFile mdl, byte lod, ushort meshIndex)
|
||||
{
|
||||
_mdl = mdl;
|
||||
_lod = lod;
|
||||
_meshIndex = meshIndex;
|
||||
|
||||
var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements
|
||||
.Select(element => (MdlFile.VertexUsage)element.Usage)
|
||||
.ToImmutableHashSet();
|
||||
|
||||
_geometryType = GetGeometryType(usages);
|
||||
}
|
||||
|
||||
private IMeshBuilder<MaterialBuilder> BuildMesh()
|
||||
{
|
||||
var indices = BuildIndices();
|
||||
var vertices = BuildVertices();
|
||||
|
||||
var meshBuilderType = typeof(MeshBuilder<,,,>).MakeGenericType(
|
||||
typeof(MaterialBuilder),
|
||||
_geometryType,
|
||||
typeof(VertexEmpty),
|
||||
typeof(VertexEmpty)
|
||||
);
|
||||
var meshBuilder = (IMeshBuilder<MaterialBuilder>)Activator.CreateInstance(meshBuilderType, $"mesh{_meshIndex}")!;
|
||||
|
||||
// TODO: share materials &c
|
||||
var materialBuilder = new MaterialBuilder()
|
||||
.WithDoubleSide(true)
|
||||
.WithMetallicRoughnessShader()
|
||||
.WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 1, 1, 1));
|
||||
|
||||
var primitiveBuilder = meshBuilder.UsePrimitive(materialBuilder);
|
||||
|
||||
// All XIV meshes use triangle lists.
|
||||
// TODO: split by submeshes
|
||||
for (var indexOffset = 0; indexOffset < Mesh.IndexCount; indexOffset += 3)
|
||||
primitiveBuilder.AddTriangle(
|
||||
vertices[indices[indexOffset + 0]],
|
||||
vertices[indices[indexOffset + 1]],
|
||||
vertices[indices[indexOffset + 2]]
|
||||
);
|
||||
|
||||
return meshBuilder;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private IReadOnlyList<IVertexBuilder> BuildVertices()
|
||||
{
|
||||
var vertexBuilderType = typeof(VertexBuilder<,,>)
|
||||
.MakeGenericType(_geometryType, typeof(VertexEmpty), typeof(VertexEmpty));
|
||||
|
||||
// NOTE: This assumes that buffer streams are tightly packed, which has proven safe across tested files. If this assumption is broken, seeks will need to be moved into the vertex element loop.
|
||||
var streams = new BinaryReader[MaximumMeshBufferStreams];
|
||||
for (var streamIndex = 0; streamIndex < MaximumMeshBufferStreams; streamIndex++)
|
||||
{
|
||||
streams[streamIndex] = new BinaryReader(new MemoryStream(_mdl.RemainingData));
|
||||
streams[streamIndex].Seek(_mdl.VertexOffset[_lod] + Mesh.VertexBufferOffset[streamIndex]);
|
||||
}
|
||||
|
||||
var sortedElements = _mdl.VertexDeclarations[_meshIndex].VertexElements
|
||||
.OrderBy(element => element.Offset)
|
||||
.Select(element => ((MdlFile.VertexUsage)element.Usage, element))
|
||||
.ToList();
|
||||
|
||||
var vertices = new List<IVertexBuilder>();
|
||||
|
||||
var attributes = new Dictionary<MdlFile.VertexUsage, object>();
|
||||
for (var vertexIndex = 0; vertexIndex < Mesh.VertexCount; vertexIndex++)
|
||||
{
|
||||
attributes.Clear();
|
||||
|
||||
foreach (var (usage, element) in sortedElements)
|
||||
attributes[usage] = ReadVertexAttribute(streams[element.Stream], element);
|
||||
|
||||
var vertexGeometry = BuildVertexGeometry(attributes);
|
||||
|
||||
var vertexBuilder = (IVertexBuilder)Activator.CreateInstance(vertexBuilderType, vertexGeometry, new VertexEmpty(), new VertexEmpty())!;
|
||||
vertices.Add(vertexBuilder);
|
||||
}
|
||||
|
||||
return vertices;
|
||||
}
|
||||
|
||||
private object ReadVertexAttribute(BinaryReader reader, MdlStructs.VertexElement element)
|
||||
{
|
||||
return (MdlFile.VertexType)element.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()),
|
||||
MdlFile.VertexType.UInt => reader.ReadBytes(4),
|
||||
MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f),
|
||||
MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()),
|
||||
MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()),
|
||||
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
private Type GetGeometryType(IReadOnlySet<MdlFile.VertexUsage> usages)
|
||||
{
|
||||
if (!usages.Contains(MdlFile.VertexUsage.Position))
|
||||
throw new Exception("Mesh does not contain position vertex elements.");
|
||||
|
||||
if (!usages.Contains(MdlFile.VertexUsage.Normal))
|
||||
return typeof(VertexPosition);
|
||||
|
||||
if (!usages.Contains(MdlFile.VertexUsage.Tangent1))
|
||||
return typeof(VertexPositionNormal);
|
||||
|
||||
return typeof(VertexPositionNormalTangent);
|
||||
}
|
||||
|
||||
private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary<MdlFile.VertexUsage, object> attributes)
|
||||
{
|
||||
if (_geometryType == typeof(VertexPosition))
|
||||
return new VertexPosition(
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Position])
|
||||
);
|
||||
|
||||
if (_geometryType == typeof(VertexPositionNormal))
|
||||
return new VertexPositionNormal(
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Position]),
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Normal])
|
||||
);
|
||||
|
||||
if (_geometryType == typeof(VertexPositionNormalTangent))
|
||||
return new VertexPositionNormalTangent(
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Position]),
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Normal]),
|
||||
FixTangentVector(ToVector4(attributes[MdlFile.VertexUsage.Tangent1]))
|
||||
);
|
||||
|
||||
throw new Exception($"Unknown geometry type {_geometryType}.");
|
||||
}
|
||||
|
||||
// Some tangent W values that should be -1 are stored as 0.
|
||||
private Vector4 FixTangentVector(Vector4 tangent)
|
||||
=> tangent with { W = tangent.W == 1 ? 1 : -1 };
|
||||
|
||||
private Vector3 ToVector3(object data)
|
||||
=> data switch
|
||||
{
|
||||
Vector2 v2 => new Vector3(v2.X, v2.Y, 0),
|
||||
Vector3 v3 => v3,
|
||||
Vector4 v4 => new Vector3(v4.X, v4.Y, v4.Z),
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}")
|
||||
};
|
||||
|
||||
private Vector4 ToVector4(object data)
|
||||
=> data switch
|
||||
{
|
||||
Vector2 v2 => new Vector4(v2.X, v2.Y, 0, 0),
|
||||
Vector3 v3 => new Vector4(v3.X, v3.Y, v3.Z, 1),
|
||||
Vector4 v4 => v4,
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}")
|
||||
};
|
||||
}
|
||||
|
|
@ -1,11 +1,6 @@
|
|||
using System.Collections.Immutable;
|
||||
using Lumina.Data.Parsing;
|
||||
using Lumina.Extensions;
|
||||
using OtterGui.Tasks;
|
||||
using Penumbra.GameData.Files;
|
||||
using SharpGLTF.Geometry;
|
||||
using SharpGLTF.Geometry.VertexTypes;
|
||||
using SharpGLTF.Materials;
|
||||
using Penumbra.Import.Modules;
|
||||
using SharpGLTF.Scenes;
|
||||
|
||||
namespace Penumbra.Import.Models;
|
||||
|
|
@ -64,175 +59,25 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
|||
|
||||
public void Execute(CancellationToken token)
|
||||
{
|
||||
// lol, lmao even
|
||||
var meshIndex = 2;
|
||||
var lod = 0;
|
||||
|
||||
var elements = _mdl.VertexDeclarations[meshIndex].VertexElements;
|
||||
|
||||
var usages = elements
|
||||
.Select(element => (MdlFile.VertexUsage)element.Usage)
|
||||
.ToImmutableHashSet();
|
||||
var geometryType = GetGeometryType(usages);
|
||||
|
||||
// TODO: probablly can do this a bit later but w/e
|
||||
var meshBuilderType = typeof(MeshBuilder<,,>).MakeGenericType(geometryType, typeof(VertexEmpty), typeof(VertexEmpty));
|
||||
var meshBuilder = (IMeshBuilder<MaterialBuilder>)Activator.CreateInstance(meshBuilderType, "mesh2")!;
|
||||
|
||||
var material = new MaterialBuilder()
|
||||
.WithDoubleSide(true)
|
||||
.WithMetallicRoughnessShader()
|
||||
.WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 1, 1, 1));
|
||||
|
||||
var mesh = _mdl.Meshes[meshIndex];
|
||||
var submesh = _mdl.SubMeshes[mesh.SubMeshIndex]; // just first for now
|
||||
|
||||
var positionVertexElement = _mdl.VertexDeclarations[meshIndex].VertexElements
|
||||
.Where(decl => (MdlFile.VertexUsage)decl.Usage == MdlFile.VertexUsage.Position)
|
||||
.First();
|
||||
|
||||
// reading in the entire indices list
|
||||
var dataReader = new BinaryReader(new MemoryStream(_mdl.RemainingData));
|
||||
dataReader.Seek(_mdl.IndexOffset[lod]);
|
||||
var indices = dataReader.ReadStructuresAsArray<ushort>((int)_mdl.IndexBufferSize[lod] / sizeof(ushort));
|
||||
|
||||
// read in verts for this mesh
|
||||
var vertices = BuildVertices(lod, mesh, _mdl.VertexDeclarations[meshIndex].VertexElements, geometryType);
|
||||
|
||||
// build a primitive for the submesh
|
||||
var primitiveBuilder = meshBuilder.UsePrimitive(material);
|
||||
// they're all tri list
|
||||
for (var indexOffset = 0; indexOffset < submesh.IndexCount; indexOffset += 3)
|
||||
{
|
||||
var index = indexOffset + submesh.IndexOffset;
|
||||
|
||||
primitiveBuilder.AddTriangle(
|
||||
vertices[indices[index + 0]],
|
||||
vertices[indices[index + 1]],
|
||||
vertices[indices[index + 2]]
|
||||
);
|
||||
}
|
||||
|
||||
var scene = new SceneBuilder();
|
||||
|
||||
// TODO: group by LoD in output tree
|
||||
for (byte lodIndex = 0; lodIndex < _mdl.LodCount; lodIndex++)
|
||||
{
|
||||
var lod = _mdl.Lods[lodIndex];
|
||||
|
||||
// TODO: consider other types?
|
||||
for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++)
|
||||
{
|
||||
var meshBuilder = MeshConverter.ToGltf(_mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset));
|
||||
scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity);
|
||||
}
|
||||
}
|
||||
|
||||
var model = scene.ToGltf2();
|
||||
model.SaveGLTF(_path);
|
||||
}
|
||||
|
||||
// todo all of this is mesh specific so probably should be a class per mesh? with the lod, too?
|
||||
private IReadOnlyList<IVertexBuilder> BuildVertices(int lod, MdlStructs.MeshStruct mesh, IEnumerable<MdlStructs.VertexElement> elements, Type geometryType)
|
||||
{
|
||||
var vertexBuilderType = typeof(VertexBuilder<,,>).MakeGenericType(geometryType, typeof(VertexEmpty), typeof(VertexEmpty));
|
||||
|
||||
// todo: demagic the 3
|
||||
// todo note this assumes that the buffer streams are tightly packed. that's a safe assumption - right? lumina assumes as much
|
||||
var streams = new BinaryReader[3];
|
||||
for (var streamIndex = 0; streamIndex < 3; streamIndex++)
|
||||
{
|
||||
streams[streamIndex] = new BinaryReader(new MemoryStream(_mdl.RemainingData));
|
||||
streams[streamIndex].Seek(_mdl.VertexOffset[lod] + mesh.VertexBufferOffset[streamIndex]);
|
||||
}
|
||||
|
||||
var sortedElements = elements
|
||||
.OrderBy(element => element.Offset)
|
||||
.ToList();
|
||||
|
||||
var vertices = new List<IVertexBuilder>();
|
||||
|
||||
// note this is being reused
|
||||
var attributes = new Dictionary<MdlFile.VertexUsage, object>();
|
||||
for (var vertexIndex = 0; vertexIndex < mesh.VertexCount; vertexIndex++)
|
||||
{
|
||||
attributes.Clear();
|
||||
|
||||
foreach (var element in sortedElements)
|
||||
attributes[(MdlFile.VertexUsage)element.Usage] = ReadVertexAttribute(streams[element.Stream], element);
|
||||
|
||||
var vertexGeometry = BuildVertexGeometry(geometryType, attributes);
|
||||
|
||||
var vertexBuilder = (IVertexBuilder)Activator.CreateInstance(vertexBuilderType, vertexGeometry, new VertexEmpty(), new VertexEmpty())!;
|
||||
vertices.Add(vertexBuilder);
|
||||
}
|
||||
|
||||
return vertices;
|
||||
}
|
||||
|
||||
// todo i fucking hate this `object` type god i hate c# gimme sum types pls
|
||||
private object ReadVertexAttribute(BinaryReader reader, MdlStructs.VertexElement element)
|
||||
{
|
||||
return (MdlFile.VertexType)element.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()),
|
||||
MdlFile.VertexType.UInt => reader.ReadBytes(4),
|
||||
MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f),
|
||||
MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()),
|
||||
MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()),
|
||||
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
private Type GetGeometryType(IReadOnlySet<MdlFile.VertexUsage> usages)
|
||||
{
|
||||
if (!usages.Contains(MdlFile.VertexUsage.Position))
|
||||
throw new Exception("Mesh does not contain position vertex elements.");
|
||||
|
||||
if (!usages.Contains(MdlFile.VertexUsage.Normal))
|
||||
return typeof(VertexPosition);
|
||||
|
||||
if (!usages.Contains(MdlFile.VertexUsage.Tangent1))
|
||||
return typeof(VertexPositionNormal);
|
||||
|
||||
return typeof(VertexPositionNormalTangent);
|
||||
}
|
||||
|
||||
private IVertexGeometry BuildVertexGeometry(Type geometryType, IReadOnlyDictionary<MdlFile.VertexUsage, object> attributes)
|
||||
{
|
||||
if (geometryType == typeof(VertexPosition))
|
||||
return new VertexPosition(
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Position])
|
||||
);
|
||||
|
||||
if (geometryType == typeof(VertexPositionNormal))
|
||||
return new VertexPositionNormal(
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Position]),
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Normal])
|
||||
);
|
||||
|
||||
if (geometryType == typeof(VertexPositionNormalTangent))
|
||||
return new VertexPositionNormalTangent(
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Position]),
|
||||
ToVector3(attributes[MdlFile.VertexUsage.Normal]),
|
||||
ToVector4(attributes[MdlFile.VertexUsage.Tangent1])
|
||||
);
|
||||
|
||||
throw new Exception($"Unknown geometry type {geometryType}.");
|
||||
}
|
||||
|
||||
private Vector3 ToVector3(object data)
|
||||
{
|
||||
return data switch
|
||||
{
|
||||
Vector2 v2 => new Vector3(v2.X, v2.Y, 0),
|
||||
Vector3 v3 => v3,
|
||||
Vector4 v4 => new Vector3(v4.X, v4.Y, v4.Z),
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}")
|
||||
};
|
||||
}
|
||||
|
||||
private Vector4 ToVector4(object data)
|
||||
{
|
||||
return data switch
|
||||
{
|
||||
Vector2 v2 => new Vector4(v2.X, v2.Y, 0, 0),
|
||||
Vector3 v3 => new Vector4(v3.X, v3.Y, v3.Z, 1),
|
||||
Vector4 v4 => v4,
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}")
|
||||
};
|
||||
}
|
||||
|
||||
public bool Equals(IAction? other)
|
||||
{
|
||||
if (other is not ExportToGltfAction rhs)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue