diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 5e931b36..af285cbb 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,3 +1,5 @@ +using System.Collections.Immutable; +using Lumina.Data.Parsing; using Lumina.Extensions; using OtterGui.Tasks; using Penumbra.GameData.Files; @@ -62,17 +64,26 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable public void Execute(CancellationToken token) { - var meshBuilder = new MeshBuilder("mesh"); + // 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)Activator.CreateInstance(meshBuilderType, "mesh2")!; var material = new MaterialBuilder() .WithDoubleSide(true) .WithMetallicRoughnessShader() .WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 1, 1, 1)); - // lol, lmao even - var meshIndex = 2; - var lod = 0; - var mesh = _mdl.Meshes[meshIndex]; var submesh = _mdl.SubMeshes[mesh.SubMeshIndex]; // just first for now @@ -86,18 +97,7 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable var indices = dataReader.ReadStructuresAsArray((int)_mdl.IndexBufferSize[lod] / sizeof(ushort)); // read in verts for this mesh - var baseOffset = _mdl.VertexOffset[lod] + mesh.VertexBufferOffset[positionVertexElement.Stream] + positionVertexElement.Offset; - var vertices = new List(); - for (var vertexIndex = 0; vertexIndex < mesh.VertexCount; vertexIndex++) - { - dataReader.Seek(baseOffset + vertexIndex * mesh.VertexBufferStride[positionVertexElement.Stream]); - // todo handle type - vertices.Add(new VertexPosition( - dataReader.ReadSingle(), - dataReader.ReadSingle(), - dataReader.ReadSingle() - )); - } + var vertices = BuildVertices(lod, mesh, _mdl.VertexDeclarations[meshIndex].VertexElements, geometryType); // build a primitive for the submesh var primitiveBuilder = meshBuilder.UsePrimitive(material); @@ -120,12 +120,125 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable model.SaveGLTF(_path); } + // todo all of this is mesh specific so probably should be a class per mesh? with the lod, too? + private IReadOnlyList BuildVertices(int lod, MdlStructs.MeshStruct mesh, IEnumerable 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(); + + // note this is being reused + var attributes = new Dictionary(); + 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 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 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) return false; - // TODO: compare configuration + // TODO: compare configuration and such return true; } }