mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Clean up models
This commit is contained in:
parent
1a1c662364
commit
13d594ca87
2 changed files with 226 additions and 250 deletions
224
Penumbra/Import/Models/Import/ModelImporter.cs
Normal file
224
Penumbra/Import/Models/Import/ModelImporter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue