mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 12:14:17 +01:00
Merge remote-tracking branch 'ackwell/mdl-name-io'
This commit is contained in:
commit
63fff56a60
8 changed files with 272 additions and 52 deletions
|
|
@ -13,23 +13,34 @@ namespace Penumbra.Import.Models.Export;
|
||||||
|
|
||||||
public class MeshExporter
|
public class MeshExporter
|
||||||
{
|
{
|
||||||
public class Mesh(IEnumerable<IMeshBuilder<MaterialBuilder>> meshes, NodeBuilder[]? joints)
|
public class Mesh(IEnumerable<MeshData> meshes, NodeBuilder[]? joints)
|
||||||
{
|
{
|
||||||
public void AddToScene(SceneBuilder scene)
|
public void AddToScene(SceneBuilder scene)
|
||||||
{
|
{
|
||||||
foreach (var mesh in meshes)
|
foreach (var data in meshes)
|
||||||
{
|
{
|
||||||
if (joints == null)
|
var instance = joints != null
|
||||||
scene.AddRigidMesh(mesh, Matrix4x4.Identity);
|
? scene.AddSkinnedMesh(data.Mesh, Matrix4x4.Identity, joints)
|
||||||
else
|
: scene.AddRigidMesh(data.Mesh, Matrix4x4.Identity);
|
||||||
scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, joints);
|
|
||||||
|
var extras = new Dictionary<string, object>();
|
||||||
|
foreach (var attribute in data.Attributes)
|
||||||
|
extras.Add(attribute, true);
|
||||||
|
|
||||||
|
instance.WithExtras(JsonContent.CreateFrom(extras));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, GltfSkeleton? skeleton)
|
public struct MeshData
|
||||||
{
|
{
|
||||||
var self = new MeshExporter(mdl, lod, meshIndex, skeleton?.Names);
|
public IMeshBuilder<MaterialBuilder> Mesh;
|
||||||
|
public string[] Attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Mesh Export(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, GltfSkeleton? skeleton)
|
||||||
|
{
|
||||||
|
var self = new MeshExporter(mdl, lod, meshIndex, materials, skeleton?.Names);
|
||||||
return new Mesh(self.BuildMeshes(), skeleton?.Joints);
|
return new Mesh(self.BuildMeshes(), skeleton?.Joints);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,18 +53,22 @@ public class MeshExporter
|
||||||
private MdlStructs.MeshStruct XivMesh
|
private MdlStructs.MeshStruct XivMesh
|
||||||
=> _mdl.Meshes[_meshIndex];
|
=> _mdl.Meshes[_meshIndex];
|
||||||
|
|
||||||
|
private readonly MaterialBuilder _material;
|
||||||
|
|
||||||
private readonly Dictionary<ushort, int>? _boneIndexMap;
|
private readonly Dictionary<ushort, int>? _boneIndexMap;
|
||||||
|
|
||||||
private readonly Type _geometryType;
|
private readonly Type _geometryType;
|
||||||
private readonly Type _materialType;
|
private readonly Type _materialType;
|
||||||
private readonly Type _skinningType;
|
private readonly Type _skinningType;
|
||||||
|
|
||||||
private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, IReadOnlyDictionary<string, int>? boneNameMap)
|
private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, MaterialBuilder[] materials, IReadOnlyDictionary<string, int>? boneNameMap)
|
||||||
{
|
{
|
||||||
_mdl = mdl;
|
_mdl = mdl;
|
||||||
_lod = lod;
|
_lod = lod;
|
||||||
_meshIndex = meshIndex;
|
_meshIndex = meshIndex;
|
||||||
|
|
||||||
|
_material = materials[XivMesh.MaterialIndex];
|
||||||
|
|
||||||
if (boneNameMap != null)
|
if (boneNameMap != null)
|
||||||
_boneIndexMap = BuildBoneIndexMap(boneNameMap);
|
_boneIndexMap = BuildBoneIndexMap(boneNameMap);
|
||||||
|
|
||||||
|
|
@ -99,31 +114,33 @@ public class MeshExporter
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Build glTF meshes for this XIV mesh. </summary>
|
/// <summary> Build glTF meshes for this XIV mesh. </summary>
|
||||||
private IMeshBuilder<MaterialBuilder>[] BuildMeshes()
|
private MeshData[] BuildMeshes()
|
||||||
{
|
{
|
||||||
var indices = BuildIndices();
|
var indices = BuildIndices();
|
||||||
var vertices = BuildVertices();
|
var vertices = BuildVertices();
|
||||||
|
|
||||||
// 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.
|
// 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)
|
if (XivMesh.SubMeshCount == 0)
|
||||||
return [BuildMesh($"mesh {_meshIndex}", indices, vertices, 0, (int)XivMesh.IndexCount)];
|
return [BuildMesh($"mesh {_meshIndex}", indices, vertices, 0, (int)XivMesh.IndexCount, 0)];
|
||||||
|
|
||||||
return _mdl.SubMeshes
|
return _mdl.SubMeshes
|
||||||
.Skip(XivMesh.SubMeshIndex)
|
.Skip(XivMesh.SubMeshIndex)
|
||||||
.Take(XivMesh.SubMeshCount)
|
.Take(XivMesh.SubMeshCount)
|
||||||
.WithIndex()
|
.WithIndex()
|
||||||
.Select(subMesh => BuildMesh($"mesh {_meshIndex}.{subMesh.Index}", indices, vertices,
|
.Select(subMesh => BuildMesh($"mesh {_meshIndex}.{subMesh.Index}", indices, vertices,
|
||||||
(int)(subMesh.Value.IndexOffset - XivMesh.StartIndex), (int)subMesh.Value.IndexCount))
|
(int)(subMesh.Value.IndexOffset - XivMesh.StartIndex), (int)subMesh.Value.IndexCount,
|
||||||
|
subMesh.Value.AttributeIndexMask))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> 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. </summary>
|
/// <summary> 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. </summary>
|
||||||
private IMeshBuilder<MaterialBuilder> BuildMesh(
|
private MeshData BuildMesh(
|
||||||
string name,
|
string name,
|
||||||
IReadOnlyList<ushort> indices,
|
IReadOnlyList<ushort> indices,
|
||||||
IReadOnlyList<IVertexBuilder> vertices,
|
IReadOnlyList<IVertexBuilder> vertices,
|
||||||
int indexBase,
|
int indexBase,
|
||||||
int indexCount
|
int indexCount,
|
||||||
|
uint attributeMask
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var meshBuilderType = typeof(MeshBuilder<,,,>).MakeGenericType(
|
var meshBuilderType = typeof(MeshBuilder<,,,>).MakeGenericType(
|
||||||
|
|
@ -134,13 +151,7 @@ public class MeshExporter
|
||||||
);
|
);
|
||||||
var meshBuilder = (IMeshBuilder<MaterialBuilder>)Activator.CreateInstance(meshBuilderType, name)!;
|
var meshBuilder = (IMeshBuilder<MaterialBuilder>)Activator.CreateInstance(meshBuilderType, name)!;
|
||||||
|
|
||||||
// TODO: share materials &c
|
var primitiveBuilder = meshBuilder.UsePrimitive(_material);
|
||||||
var materialBuilder = new MaterialBuilder()
|
|
||||||
.WithDoubleSide(true)
|
|
||||||
.WithMetallicRoughnessShader()
|
|
||||||
.WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, new Vector4(1, 1, 1, 1));
|
|
||||||
|
|
||||||
var primitiveBuilder = meshBuilder.UsePrimitive(materialBuilder);
|
|
||||||
|
|
||||||
// Store a list of the glTF indices. The list index will be equivalent to the xiv (submesh) index.
|
// Store a list of the glTF indices. The list index will be equivalent to the xiv (submesh) index.
|
||||||
var gltfIndices = new List<int>();
|
var gltfIndices = new List<int>();
|
||||||
|
|
@ -192,12 +203,23 @@ public class MeshExporter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Named morph targets aren't part of the specification, however `MESH.extras.targetNames`
|
||||||
|
// is a commonly-accepted means of providing the data.
|
||||||
meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary<string, object>()
|
meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary<string, object>()
|
||||||
{
|
{
|
||||||
{ "targetNames", shapeNames },
|
{ "targetNames", shapeNames },
|
||||||
});
|
});
|
||||||
|
|
||||||
return meshBuilder;
|
var attributes = Enumerable.Range(0, 32)
|
||||||
|
.Where(index => ((attributeMask >> index) & 1) == 1)
|
||||||
|
.Select(index => _mdl.Attributes[index])
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return new MeshData
|
||||||
|
{
|
||||||
|
Mesh = meshBuilder,
|
||||||
|
Attributes = attributes,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Read in the indices for this mesh. </summary>
|
/// <summary> Read in the indices for this mesh. </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
|
using SharpGLTF.Materials;
|
||||||
using SharpGLTF.Scenes;
|
using SharpGLTF.Scenes;
|
||||||
using SharpGLTF.Transforms;
|
using SharpGLTF.Transforms;
|
||||||
|
|
||||||
|
|
@ -25,12 +26,13 @@ public class ModelExporter
|
||||||
public static Model Export(MdlFile mdl, IEnumerable<XivSkeleton>? xivSkeleton)
|
public static Model Export(MdlFile mdl, IEnumerable<XivSkeleton>? xivSkeleton)
|
||||||
{
|
{
|
||||||
var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null;
|
var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null;
|
||||||
var meshes = ConvertMeshes(mdl, gltfSkeleton);
|
var materials = ConvertMaterials(mdl);
|
||||||
|
var meshes = ConvertMeshes(mdl, materials, gltfSkeleton);
|
||||||
return new Model(meshes, gltfSkeleton);
|
return new Model(meshes, gltfSkeleton);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Convert a .mdl to a mesh (group) per LoD. </summary>
|
/// <summary> Convert a .mdl to a mesh (group) per LoD. </summary>
|
||||||
private static List<MeshExporter.Mesh> ConvertMeshes(MdlFile mdl, GltfSkeleton? skeleton)
|
private static List<MeshExporter.Mesh> ConvertMeshes(MdlFile mdl, MaterialBuilder[] materials, GltfSkeleton? skeleton)
|
||||||
{
|
{
|
||||||
var meshes = new List<MeshExporter.Mesh>();
|
var meshes = new List<MeshExporter.Mesh>();
|
||||||
|
|
||||||
|
|
@ -41,7 +43,7 @@ public class ModelExporter
|
||||||
// TODO: consider other types of mesh?
|
// TODO: consider other types of mesh?
|
||||||
for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++)
|
for (ushort meshOffset = 0; meshOffset < lod.MeshCount; meshOffset++)
|
||||||
{
|
{
|
||||||
var mesh = MeshExporter.Export(mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), skeleton);
|
var mesh = MeshExporter.Export(mdl, lodIndex, (ushort)(lod.MeshIndex + meshOffset), materials, skeleton);
|
||||||
meshes.Add(mesh);
|
meshes.Add(mesh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -49,6 +51,18 @@ public class ModelExporter
|
||||||
return meshes;
|
return meshes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Compose textures for use with these materials
|
||||||
|
/// <summary> Build placeholder materials for each of the material slots in the .mdl. </summary>
|
||||||
|
private static MaterialBuilder[] ConvertMaterials(MdlFile mdl)
|
||||||
|
=> mdl.Materials
|
||||||
|
.Select(name =>
|
||||||
|
new MaterialBuilder(name)
|
||||||
|
.WithMetallicRoughnessShader()
|
||||||
|
.WithDoubleSide(true)
|
||||||
|
.WithChannelParam(KnownChannel.BaseColor, KnownProperty.RGBA, Vector4.One)
|
||||||
|
)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
/// <summary> Convert XIV skeleton data into a glTF-compatible node tree, with mappings. </summary>
|
/// <summary> Convert XIV skeleton data into a glTF-compatible node tree, with mappings. </summary>
|
||||||
private static GltfSkeleton? ConvertSkeleton(IEnumerable<XivSkeleton> skeletons)
|
private static GltfSkeleton? ConvertSkeleton(IEnumerable<XivSkeleton> skeletons)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ public class MeshImporter(IEnumerable<Node> nodes)
|
||||||
public MdlStructs.MeshStruct MeshStruct;
|
public MdlStructs.MeshStruct MeshStruct;
|
||||||
public List<MdlStructs.SubmeshStruct> SubMeshStructs;
|
public List<MdlStructs.SubmeshStruct> SubMeshStructs;
|
||||||
|
|
||||||
|
public string? Material;
|
||||||
|
|
||||||
public MdlStructs.VertexDeclarationStruct VertexDeclaration;
|
public MdlStructs.VertexDeclarationStruct VertexDeclaration;
|
||||||
public IEnumerable<byte> VertexBuffer;
|
public IEnumerable<byte> VertexBuffer;
|
||||||
|
|
||||||
|
|
@ -17,6 +19,8 @@ public class MeshImporter(IEnumerable<Node> nodes)
|
||||||
|
|
||||||
public List<string>? Bones;
|
public List<string>? Bones;
|
||||||
|
|
||||||
|
public List<string> MetaAttributes;
|
||||||
|
|
||||||
public List<MeshShapeKey> ShapeKeys;
|
public List<MeshShapeKey> ShapeKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,6 +39,8 @@ public class MeshImporter(IEnumerable<Node> nodes)
|
||||||
|
|
||||||
private readonly List<MdlStructs.SubmeshStruct> _subMeshes = [];
|
private readonly List<MdlStructs.SubmeshStruct> _subMeshes = [];
|
||||||
|
|
||||||
|
private string? _material;
|
||||||
|
|
||||||
private MdlStructs.VertexDeclarationStruct? _vertexDeclaration;
|
private MdlStructs.VertexDeclarationStruct? _vertexDeclaration;
|
||||||
private byte[]? _strides;
|
private byte[]? _strides;
|
||||||
private ushort _vertexCount;
|
private ushort _vertexCount;
|
||||||
|
|
@ -44,6 +50,8 @@ public class MeshImporter(IEnumerable<Node> nodes)
|
||||||
|
|
||||||
private List<string>? _bones;
|
private List<string>? _bones;
|
||||||
|
|
||||||
|
private readonly List<string> _metaAttributes = [];
|
||||||
|
|
||||||
private readonly Dictionary<string, List<MdlStructs.ShapeValueStruct>> _shapeValues = [];
|
private readonly Dictionary<string, List<MdlStructs.ShapeValueStruct>> _shapeValues = [];
|
||||||
|
|
||||||
private Mesh Create()
|
private Mesh Create()
|
||||||
|
|
@ -74,10 +82,12 @@ public class MeshImporter(IEnumerable<Node> nodes)
|
||||||
BoneTableIndex = 0,
|
BoneTableIndex = 0,
|
||||||
},
|
},
|
||||||
SubMeshStructs = _subMeshes,
|
SubMeshStructs = _subMeshes,
|
||||||
|
Material = _material,
|
||||||
VertexDeclaration = _vertexDeclaration.Value,
|
VertexDeclaration = _vertexDeclaration.Value,
|
||||||
VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]),
|
VertexBuffer = _streams[0].Concat(_streams[1]).Concat(_streams[2]),
|
||||||
Indices = _indices,
|
Indices = _indices,
|
||||||
Bones = _bones,
|
Bones = _bones,
|
||||||
|
MetaAttributes = _metaAttributes,
|
||||||
ShapeKeys = _shapeValues
|
ShapeKeys = _shapeValues
|
||||||
.Select(pair => new MeshShapeKey()
|
.Select(pair => new MeshShapeKey()
|
||||||
{
|
{
|
||||||
|
|
@ -105,6 +115,9 @@ public class MeshImporter(IEnumerable<Node> nodes)
|
||||||
|
|
||||||
var subMeshName = node.Name ?? node.Mesh.Name;
|
var subMeshName = node.Name ?? node.Mesh.Name;
|
||||||
|
|
||||||
|
// TODO: Record a warning if there's a mismatch between current and incoming, as we can't support multiple materials per mesh.
|
||||||
|
_material ??= subMesh.Material;
|
||||||
|
|
||||||
// Check that vertex declarations match - we need to combine the buffers, so a mismatch would take a whole load of resolution.
|
// Check that vertex declarations match - we need to combine the buffers, so a mismatch would take a whole load of resolution.
|
||||||
if (_vertexDeclaration == null)
|
if (_vertexDeclaration == null)
|
||||||
_vertexDeclaration = subMesh.VertexDeclaration;
|
_vertexDeclaration = subMesh.VertexDeclaration;
|
||||||
|
|
@ -145,6 +158,8 @@ public class MeshImporter(IEnumerable<Node> nodes)
|
||||||
_subMeshes.Add(subMesh.SubMeshStruct with
|
_subMeshes.Add(subMesh.SubMeshStruct with
|
||||||
{
|
{
|
||||||
IndexOffset = (ushort)(subMesh.SubMeshStruct.IndexOffset + indexOffset),
|
IndexOffset = (ushort)(subMesh.SubMeshStruct.IndexOffset + indexOffset),
|
||||||
|
AttributeIndexMask = Utility.GetMergedAttributeMask(
|
||||||
|
subMesh.SubMeshStruct.AttributeIndexMask, subMesh.MetaAttributes, _metaAttributes),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ public partial class ModelImporter(ModelRoot model)
|
||||||
private readonly List<MdlStructs.MeshStruct> _meshes = [];
|
private readonly List<MdlStructs.MeshStruct> _meshes = [];
|
||||||
private readonly List<MdlStructs.SubmeshStruct> _subMeshes = [];
|
private readonly List<MdlStructs.SubmeshStruct> _subMeshes = [];
|
||||||
|
|
||||||
|
private readonly List<string> _materials = [];
|
||||||
|
|
||||||
private readonly List<MdlStructs.VertexDeclarationStruct> _vertexDeclarations = [];
|
private readonly List<MdlStructs.VertexDeclarationStruct> _vertexDeclarations = [];
|
||||||
private readonly List<byte> _vertexBuffer = [];
|
private readonly List<byte> _vertexBuffer = [];
|
||||||
|
|
||||||
|
|
@ -27,6 +29,8 @@ public partial class ModelImporter(ModelRoot model)
|
||||||
private readonly List<string> _bones = [];
|
private readonly List<string> _bones = [];
|
||||||
private readonly List<MdlStructs.BoneTableStruct> _boneTables = [];
|
private readonly List<MdlStructs.BoneTableStruct> _boneTables = [];
|
||||||
|
|
||||||
|
private readonly List<string> _metaAttributes = [];
|
||||||
|
|
||||||
private readonly Dictionary<string, List<MdlStructs.ShapeMeshStruct>> _shapeMeshes = [];
|
private readonly Dictionary<string, List<MdlStructs.ShapeMeshStruct>> _shapeMeshes = [];
|
||||||
private readonly List<MdlStructs.ShapeValueStruct> _shapeValues = [];
|
private readonly List<MdlStructs.ShapeValueStruct> _shapeValues = [];
|
||||||
|
|
||||||
|
|
@ -37,6 +41,8 @@ public partial class ModelImporter(ModelRoot model)
|
||||||
BuildMeshForGroup(subMeshNodes);
|
BuildMeshForGroup(subMeshNodes);
|
||||||
|
|
||||||
// Now that all the meshes have been built, we can build some of the model-wide metadata.
|
// Now that all the meshes have been built, we can build some of the model-wide metadata.
|
||||||
|
var materials = _materials.Count > 0 ? _materials : ["/NO_MATERIAL"];
|
||||||
|
|
||||||
var shapes = new List<MdlFile.Shape>();
|
var shapes = new List<MdlFile.Shape>();
|
||||||
var shapeMeshes = new List<MdlStructs.ShapeMeshStruct>();
|
var shapeMeshes = new List<MdlStructs.ShapeMeshStruct>();
|
||||||
foreach (var (keyName, keyMeshes) in _shapeMeshes)
|
foreach (var (keyName, keyMeshes) in _shapeMeshes)
|
||||||
|
|
@ -67,6 +73,7 @@ public partial class ModelImporter(ModelRoot model)
|
||||||
Bones = [.. _bones],
|
Bones = [.. _bones],
|
||||||
// TODO: Game doesn't seem to rely on this, but would be good to populate.
|
// TODO: Game doesn't seem to rely on this, but would be good to populate.
|
||||||
SubMeshBoneMap = [],
|
SubMeshBoneMap = [],
|
||||||
|
Attributes = [.. _metaAttributes],
|
||||||
Shapes = [.. shapes],
|
Shapes = [.. shapes],
|
||||||
ShapeMeshes = [.. shapeMeshes],
|
ShapeMeshes = [.. shapeMeshes],
|
||||||
ShapeValues = [.. _shapeValues],
|
ShapeValues = [.. _shapeValues],
|
||||||
|
|
@ -86,8 +93,7 @@ public partial class ModelImporter(ModelRoot model)
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
// TODO: Would be good to populate from gltf material names.
|
Materials = [.. materials],
|
||||||
Materials = ["/NO_MATERIAL"],
|
|
||||||
|
|
||||||
// TODO: Would be good to calculate all of this up the tree.
|
// TODO: Would be good to calculate all of this up the tree.
|
||||||
Radius = 1,
|
Radius = 1,
|
||||||
|
|
@ -130,15 +136,20 @@ public partial class ModelImporter(ModelRoot model)
|
||||||
var mesh = MeshImporter.Import(subMeshNodes);
|
var mesh = MeshImporter.Import(subMeshNodes);
|
||||||
var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset);
|
var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset);
|
||||||
|
|
||||||
|
var materialIndex = mesh.Material != null
|
||||||
|
? GetMaterialIndex(mesh.Material)
|
||||||
|
: (ushort)0;
|
||||||
|
|
||||||
// If no bone table is used for a mesh, the index is set to 255.
|
// If no bone table is used for a mesh, the index is set to 255.
|
||||||
var boneTableIndex = 255;
|
var boneTableIndex = mesh.Bones != null
|
||||||
if (mesh.Bones != null)
|
? BuildBoneTable(mesh.Bones)
|
||||||
boneTableIndex = BuildBoneTable(mesh.Bones);
|
: (ushort)255;
|
||||||
|
|
||||||
_meshes.Add(mesh.MeshStruct with
|
_meshes.Add(mesh.MeshStruct with
|
||||||
{
|
{
|
||||||
|
MaterialIndex = materialIndex,
|
||||||
SubMeshIndex = (ushort)(mesh.MeshStruct.SubMeshIndex + subMeshOffset),
|
SubMeshIndex = (ushort)(mesh.MeshStruct.SubMeshIndex + subMeshOffset),
|
||||||
BoneTableIndex = (ushort)boneTableIndex,
|
BoneTableIndex = boneTableIndex,
|
||||||
StartIndex = meshStartIndex,
|
StartIndex = meshStartIndex,
|
||||||
VertexBufferOffset = mesh.MeshStruct.VertexBufferOffset
|
VertexBufferOffset = mesh.MeshStruct.VertexBufferOffset
|
||||||
.Select(offset => (uint)(offset + vertexOffset))
|
.Select(offset => (uint)(offset + vertexOffset))
|
||||||
|
|
@ -147,6 +158,8 @@ public partial class ModelImporter(ModelRoot model)
|
||||||
|
|
||||||
_subMeshes.AddRange(mesh.SubMeshStructs.Select(m => m with
|
_subMeshes.AddRange(mesh.SubMeshStructs.Select(m => m with
|
||||||
{
|
{
|
||||||
|
AttributeIndexMask = Utility.GetMergedAttributeMask(
|
||||||
|
m.AttributeIndexMask, mesh.MetaAttributes, _metaAttributes),
|
||||||
IndexOffset = (uint)(m.IndexOffset + indexOffset),
|
IndexOffset = (uint)(m.IndexOffset + indexOffset),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -181,6 +194,23 @@ public partial class ModelImporter(ModelRoot model)
|
||||||
throw new Exception($"Importing this file would require more than the maximum of {ushort.MaxValue} shape values.\nTry removing or applying shape keys that do not need to be changed at runtime in-game.");
|
throw new Exception($"Importing this file would require more than the maximum of {ushort.MaxValue} shape values.\nTry removing or applying shape keys that do not need to be changed at runtime in-game.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ushort GetMaterialIndex(string materialName)
|
||||||
|
{
|
||||||
|
// If we already have this material, grab the current index.
|
||||||
|
var index = _materials.IndexOf(materialName);
|
||||||
|
if (index >= 0)
|
||||||
|
return (ushort)index;
|
||||||
|
|
||||||
|
// If there's already 4 materials, we can't add any more.
|
||||||
|
// TODO: permit, with a warning to reduce, and validation in MdlTab.
|
||||||
|
var count = _materials.Count;
|
||||||
|
if (count >= 4)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
_materials.Add(materialName);
|
||||||
|
return (ushort)count;
|
||||||
|
}
|
||||||
|
|
||||||
private ushort BuildBoneTable(List<string> boneNames)
|
private ushort BuildBoneTable(List<string> boneNames)
|
||||||
{
|
{
|
||||||
var boneIndices = new List<ushort>();
|
var boneIndices = new List<ushort>();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Text.Json;
|
||||||
using Lumina.Data.Parsing;
|
using Lumina.Data.Parsing;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using SharpGLTF.Schema2;
|
using SharpGLTF.Schema2;
|
||||||
|
|
@ -10,6 +11,8 @@ public class SubMeshImporter
|
||||||
{
|
{
|
||||||
public MdlStructs.SubmeshStruct SubMeshStruct;
|
public MdlStructs.SubmeshStruct SubMeshStruct;
|
||||||
|
|
||||||
|
public string? Material;
|
||||||
|
|
||||||
public MdlStructs.VertexDeclarationStruct VertexDeclaration;
|
public MdlStructs.VertexDeclarationStruct VertexDeclaration;
|
||||||
|
|
||||||
public ushort VertexCount;
|
public ushort VertexCount;
|
||||||
|
|
@ -18,6 +21,8 @@ public class SubMeshImporter
|
||||||
|
|
||||||
public ushort[] Indices;
|
public ushort[] Indices;
|
||||||
|
|
||||||
|
public string[] MetaAttributes;
|
||||||
|
|
||||||
public Dictionary<string, List<MdlStructs.ShapeValueStruct>> ShapeValues;
|
public Dictionary<string, List<MdlStructs.ShapeValueStruct>> ShapeValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,10 +32,11 @@ public class SubMeshImporter
|
||||||
return importer.Create();
|
return importer.Create();
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly MeshPrimitive _primitive;
|
private readonly MeshPrimitive _primitive;
|
||||||
private readonly IDictionary<ushort, ushort>? _nodeBoneMap;
|
private readonly IDictionary<ushort, ushort>? _nodeBoneMap;
|
||||||
|
private readonly IDictionary<string, JsonElement>? _nodeExtras;
|
||||||
|
|
||||||
private List<VertexAttribute>? _attributes;
|
private List<VertexAttribute>? _vertexAttributes;
|
||||||
|
|
||||||
private ushort _vertexCount;
|
private ushort _vertexCount;
|
||||||
private byte[] _strides = [0, 0, 0];
|
private byte[] _strides = [0, 0, 0];
|
||||||
|
|
@ -38,6 +44,8 @@ public class SubMeshImporter
|
||||||
|
|
||||||
private ushort[]? _indices;
|
private ushort[]? _indices;
|
||||||
|
|
||||||
|
private string[]? _metaAttributes;
|
||||||
|
|
||||||
private readonly List<string>? _morphNames;
|
private readonly List<string>? _morphNames;
|
||||||
private Dictionary<string, List<MdlStructs.ShapeValueStruct>>? _shapeValues;
|
private Dictionary<string, List<MdlStructs.ShapeValueStruct>>? _shapeValues;
|
||||||
|
|
||||||
|
|
@ -55,6 +63,15 @@ public class SubMeshImporter
|
||||||
_primitive = mesh.Primitives[0];
|
_primitive = mesh.Primitives[0];
|
||||||
_nodeBoneMap = nodeBoneMap;
|
_nodeBoneMap = nodeBoneMap;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_nodeExtras = node.Extras.Deserialize<Dictionary<string, JsonElement>>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_nodeExtras = null;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_morphNames = mesh.Extras.GetNode("targetNames").Deserialize<List<string>>();
|
_morphNames = mesh.Extras.GetNode("targetNames").Deserialize<List<string>>();
|
||||||
|
|
@ -74,12 +91,26 @@ public class SubMeshImporter
|
||||||
{
|
{
|
||||||
// Build all the data we'll need.
|
// Build all the data we'll need.
|
||||||
BuildIndices();
|
BuildIndices();
|
||||||
BuildAttributes();
|
BuildVertexAttributes();
|
||||||
BuildVertices();
|
BuildVertices();
|
||||||
|
BuildMetaAttributes();
|
||||||
|
|
||||||
ArgumentNullException.ThrowIfNull(_indices);
|
ArgumentNullException.ThrowIfNull(_indices);
|
||||||
ArgumentNullException.ThrowIfNull(_attributes);
|
ArgumentNullException.ThrowIfNull(_vertexAttributes);
|
||||||
ArgumentNullException.ThrowIfNull(_shapeValues);
|
ArgumentNullException.ThrowIfNull(_shapeValues);
|
||||||
|
ArgumentNullException.ThrowIfNull(_metaAttributes);
|
||||||
|
|
||||||
|
var material = _primitive.Material.Name;
|
||||||
|
if (material == "")
|
||||||
|
material = null;
|
||||||
|
|
||||||
|
// At this level, we assume that attributes are wholly controlled by this sub-mesh.
|
||||||
|
var attributeMask = _metaAttributes.Length switch
|
||||||
|
{
|
||||||
|
< 32 => (1u << _metaAttributes.Length) - 1,
|
||||||
|
32 => uint.MaxValue,
|
||||||
|
> 32 => throw new Exception("Models may utilise a maximum of 32 attributes."),
|
||||||
|
};
|
||||||
|
|
||||||
return new SubMesh()
|
return new SubMesh()
|
||||||
{
|
{
|
||||||
|
|
@ -87,20 +118,22 @@ public class SubMeshImporter
|
||||||
{
|
{
|
||||||
IndexOffset = 0,
|
IndexOffset = 0,
|
||||||
IndexCount = (uint)_indices.Length,
|
IndexCount = (uint)_indices.Length,
|
||||||
AttributeIndexMask = 0,
|
AttributeIndexMask = attributeMask,
|
||||||
|
|
||||||
// TODO: Flesh these out. Game doesn't seem to rely on them existing, though.
|
// TODO: Flesh these out. Game doesn't seem to rely on them existing, though.
|
||||||
BoneStartIndex = 0,
|
BoneStartIndex = 0,
|
||||||
BoneCount = 0,
|
BoneCount = 0,
|
||||||
},
|
},
|
||||||
|
Material = material,
|
||||||
VertexDeclaration = new MdlStructs.VertexDeclarationStruct()
|
VertexDeclaration = new MdlStructs.VertexDeclarationStruct()
|
||||||
{
|
{
|
||||||
VertexElements = _attributes.Select(attribute => attribute.Element).ToArray(),
|
VertexElements = _vertexAttributes.Select(attribute => attribute.Element).ToArray(),
|
||||||
},
|
},
|
||||||
VertexCount = _vertexCount,
|
VertexCount = _vertexCount,
|
||||||
Strides = _strides,
|
Strides = _strides,
|
||||||
Streams = _streams,
|
Streams = _streams,
|
||||||
Indices = _indices,
|
Indices = _indices,
|
||||||
|
MetaAttributes = _metaAttributes,
|
||||||
ShapeValues = _shapeValues,
|
ShapeValues = _shapeValues,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -110,7 +143,7 @@ public class SubMeshImporter
|
||||||
_indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray();
|
_indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildAttributes()
|
private void BuildVertexAttributes()
|
||||||
{
|
{
|
||||||
var accessors = _primitive.VertexAccessors;
|
var accessors = _primitive.VertexAccessors;
|
||||||
|
|
||||||
|
|
@ -146,14 +179,14 @@ public class SubMeshImporter
|
||||||
offsets[attribute.Stream] += attribute.Size;
|
offsets[attribute.Stream] += attribute.Size;
|
||||||
}
|
}
|
||||||
|
|
||||||
_attributes = attributes;
|
_vertexAttributes = attributes;
|
||||||
// After building the attributes, the resulting next offsets are our stream strides.
|
// After building the attributes, the resulting next offsets are our stream strides.
|
||||||
_strides = offsets;
|
_strides = offsets;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildVertices()
|
private void BuildVertices()
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(_attributes);
|
ArgumentNullException.ThrowIfNull(_vertexAttributes);
|
||||||
|
|
||||||
// Lists of vertex indices that are effected by each morph target for this primitive.
|
// Lists of vertex indices that are effected by each morph target for this primitive.
|
||||||
var morphModifiedVertices = Enumerable.Range(0, _primitive.MorphTargetsCount)
|
var morphModifiedVertices = Enumerable.Range(0, _primitive.MorphTargetsCount)
|
||||||
|
|
@ -166,13 +199,13 @@ public class SubMeshImporter
|
||||||
for (var vertexIndex = 0; vertexIndex < _vertexCount; vertexIndex++)
|
for (var vertexIndex = 0; vertexIndex < _vertexCount; vertexIndex++)
|
||||||
{
|
{
|
||||||
// Write out vertex data to streams for each attribute.
|
// Write out vertex data to streams for each attribute.
|
||||||
foreach (var attribute in _attributes)
|
foreach (var attribute in _vertexAttributes)
|
||||||
_streams[attribute.Stream].AddRange(attribute.Build(vertexIndex));
|
_streams[attribute.Stream].AddRange(attribute.Build(vertexIndex));
|
||||||
|
|
||||||
// Record which morph targets have values for this vertex, if any.
|
// Record which morph targets have values for this vertex, if any.
|
||||||
var changedMorphs = morphModifiedVertices
|
var changedMorphs = morphModifiedVertices
|
||||||
.WithIndex()
|
.WithIndex()
|
||||||
.Where(pair => _attributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex)))
|
.Where(pair => _vertexAttributes.Any(attribute => attribute.HasMorph(pair.Index, vertexIndex)))
|
||||||
.Select(pair => pair.Value);
|
.Select(pair => pair.Value);
|
||||||
foreach (var modifiedVertices in changedMorphs)
|
foreach (var modifiedVertices in changedMorphs)
|
||||||
modifiedVertices.Add(vertexIndex);
|
modifiedVertices.Add(vertexIndex);
|
||||||
|
|
@ -184,7 +217,7 @@ public class SubMeshImporter
|
||||||
private void BuildShapeValues(IEnumerable<List<int>> morphModifiedVertices)
|
private void BuildShapeValues(IEnumerable<List<int>> morphModifiedVertices)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(_indices);
|
ArgumentNullException.ThrowIfNull(_indices);
|
||||||
ArgumentNullException.ThrowIfNull(_attributes);
|
ArgumentNullException.ThrowIfNull(_vertexAttributes);
|
||||||
|
|
||||||
var morphShapeValues = new Dictionary<string, List<MdlStructs.ShapeValueStruct>>();
|
var morphShapeValues = new Dictionary<string, List<MdlStructs.ShapeValueStruct>>();
|
||||||
|
|
||||||
|
|
@ -196,7 +229,7 @@ public class SubMeshImporter
|
||||||
foreach (var vertexIndex in modifiedVertices)
|
foreach (var vertexIndex in modifiedVertices)
|
||||||
{
|
{
|
||||||
// Write out the morphed vertex to the vertex streams.
|
// Write out the morphed vertex to the vertex streams.
|
||||||
foreach (var attribute in _attributes)
|
foreach (var attribute in _vertexAttributes)
|
||||||
_streams[attribute.Stream].AddRange(attribute.BuildMorph(morphIndex, vertexIndex));
|
_streams[attribute.Stream].AddRange(attribute.BuildMorph(morphIndex, vertexIndex));
|
||||||
|
|
||||||
// Find any indices that target this vertex index and create a mapping.
|
// Find any indices that target this vertex index and create a mapping.
|
||||||
|
|
@ -217,4 +250,13 @@ public class SubMeshImporter
|
||||||
|
|
||||||
_shapeValues = morphShapeValues;
|
_shapeValues = morphShapeValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BuildMetaAttributes()
|
||||||
|
{
|
||||||
|
// We consider any "extras" key with a boolean value set to `true` to be an attribute.
|
||||||
|
_metaAttributes = _nodeExtras?
|
||||||
|
.Where(pair => pair.Value.ValueKind == JsonValueKind.True)
|
||||||
|
.Select(pair => pair.Key)
|
||||||
|
.ToArray() ?? [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
34
Penumbra/Import/Models/Import/Utility.cs
Normal file
34
Penumbra/Import/Models/Import/Utility.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
namespace Penumbra.Import.Models.Import;
|
||||||
|
|
||||||
|
public static class Utility
|
||||||
|
{
|
||||||
|
/// <summary> Merge attributes into an existing attribute array, providing an updated submesh mask. </summary>
|
||||||
|
/// <param name="oldMask"> Old submesh attribute mask. </param>
|
||||||
|
/// <param name="oldAttributes"> Old attribute array that should be merged. </param>
|
||||||
|
/// <param name="newAttributes"> New attribute array. Will be mutated. </param>
|
||||||
|
/// <returns> New submesh attribute mask, updated to match the merged attribute array. </returns>
|
||||||
|
public static uint GetMergedAttributeMask(uint oldMask, IList<string> oldAttributes, List<string> newAttributes)
|
||||||
|
{
|
||||||
|
var metaAttributes = Enumerable.Range(0, 32)
|
||||||
|
.Where(index => ((oldMask >> index) & 1) == 1)
|
||||||
|
.Select(index => oldAttributes[index]);
|
||||||
|
|
||||||
|
var newMask = 0u;
|
||||||
|
|
||||||
|
foreach (var metaAttribute in metaAttributes)
|
||||||
|
{
|
||||||
|
var attributeIndex = newAttributes.IndexOf(metaAttribute);
|
||||||
|
if (attributeIndex == -1)
|
||||||
|
{
|
||||||
|
if (newAttributes.Count >= 32)
|
||||||
|
throw new Exception("Models may utilise a maximum of 32 attributes.");
|
||||||
|
|
||||||
|
newAttributes.Add(metaAttribute);
|
||||||
|
attributeIndex = newAttributes.Count - 1;
|
||||||
|
}
|
||||||
|
newMask |= 1u << attributeIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newMask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,9 @@ public partial class ModEditWindow
|
||||||
public MdlFile Mdl { get; private set; }
|
public MdlFile Mdl { get; private set; }
|
||||||
private List<string>[] _attributes;
|
private List<string>[] _attributes;
|
||||||
|
|
||||||
|
public bool ImportKeepMaterials;
|
||||||
|
public bool ImportKeepAttributes;
|
||||||
|
|
||||||
public List<Utf8GamePath>? GamePaths { get; private set; }
|
public List<Utf8GamePath>? GamePaths { get; private set; }
|
||||||
public int GamePathIndex;
|
public int GamePathIndex;
|
||||||
|
|
||||||
|
|
@ -110,6 +113,7 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
/// <summary> Export model to an interchange format. </summary>
|
/// <summary> Export model to an interchange format. </summary>
|
||||||
/// <param name="outputPath"> Disk path to save the resulting file to. </param>
|
/// <param name="outputPath"> Disk path to save the resulting file to. </param>
|
||||||
|
/// <param name="mdlPath"> .mdl game path to resolve satellite files such as skeletons relative to. </param>
|
||||||
public void Export(string outputPath, Utf8GamePath mdlPath)
|
public void Export(string outputPath, Utf8GamePath mdlPath)
|
||||||
{
|
{
|
||||||
IEnumerable<SklbFile> skeletons;
|
IEnumerable<SklbFile> skeletons;
|
||||||
|
|
@ -143,14 +147,70 @@ public partial class ModEditWindow
|
||||||
{
|
{
|
||||||
RecordIoExceptions(task.Exception);
|
RecordIoExceptions(task.Exception);
|
||||||
if (task is { IsCompletedSuccessfully: true, Result: not null })
|
if (task is { IsCompletedSuccessfully: true, Result: not null })
|
||||||
{
|
FinalizeImport(task.Result);
|
||||||
Initialize(task.Result);
|
|
||||||
_dirty = true;
|
|
||||||
}
|
|
||||||
PendingIo = false;
|
PendingIo = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary> Finalise the import of a .mdl, applying any post-import transformations and state updates. </summary>
|
||||||
|
/// <param name="newMdl"> Model data to finalize. </param>
|
||||||
|
private void FinalizeImport(MdlFile newMdl)
|
||||||
|
{
|
||||||
|
if (ImportKeepMaterials)
|
||||||
|
MergeMaterials(newMdl, Mdl);
|
||||||
|
|
||||||
|
if (ImportKeepAttributes)
|
||||||
|
MergeAttributes(newMdl, Mdl);
|
||||||
|
|
||||||
|
Initialize(newMdl);
|
||||||
|
_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Merge material configuration from the source onto the target. </summary>
|
||||||
|
/// <param name="target"> Model that will be updated. </param>
|
||||||
|
/// <param name="source"> Model to copy material configuration from. </param>
|
||||||
|
public void MergeMaterials(MdlFile target, MdlFile source)
|
||||||
|
{
|
||||||
|
target.Materials = source.Materials;
|
||||||
|
|
||||||
|
for (var meshIndex = 0; meshIndex < target.Meshes.Length; meshIndex++)
|
||||||
|
{
|
||||||
|
target.Meshes[meshIndex].MaterialIndex = meshIndex < source.Meshes.Length
|
||||||
|
? source.Meshes[meshIndex].MaterialIndex
|
||||||
|
: (ushort)0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Merge attribute configuration from the source onto the target. </summary>
|
||||||
|
/// <param name="target" Model that will be update. ></param>
|
||||||
|
/// <param name="source"> Model to copy attribute configuration from. </param>
|
||||||
|
public void MergeAttributes(MdlFile target, MdlFile source)
|
||||||
|
{
|
||||||
|
target.Attributes = source.Attributes;
|
||||||
|
|
||||||
|
var indexEnumerator = Enumerable.Range(0, target.Meshes.Length)
|
||||||
|
.SelectMany(mi => Enumerable.Range(0, target.Meshes[mi].SubMeshCount).Select(so => (mi, so)));
|
||||||
|
foreach (var (meshIndex, subMeshOffset) in indexEnumerator)
|
||||||
|
{
|
||||||
|
var subMeshIndex = target.Meshes[meshIndex].SubMeshIndex + subMeshOffset;
|
||||||
|
|
||||||
|
// Preemptively reset the mask in case we need to shortcut out.
|
||||||
|
target.SubMeshes[subMeshIndex].AttributeIndexMask = 0u;
|
||||||
|
|
||||||
|
// Rather than comparing sub-meshes directly, we're grouping by parent mesh in an attempt
|
||||||
|
// to maintain semantic connection betwen mesh index and submesh attributes.
|
||||||
|
if (meshIndex >= source.Meshes.Length)
|
||||||
|
continue;
|
||||||
|
var sourceMesh = source.Meshes[meshIndex];
|
||||||
|
|
||||||
|
if (subMeshOffset >= sourceMesh.SubMeshCount)
|
||||||
|
continue;
|
||||||
|
var sourceSubMesh = source.SubMeshes[sourceMesh.SubMeshIndex + subMeshOffset];
|
||||||
|
|
||||||
|
target.SubMeshes[subMeshIndex].AttributeIndexMask = sourceSubMesh.AttributeIndexMask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void RecordIoExceptions(Exception? exception)
|
private void RecordIoExceptions(Exception? exception)
|
||||||
{
|
{
|
||||||
IoExceptions = exception switch {
|
IoExceptions = exception switch {
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,9 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
using (var frame = ImRaii.FramedGroup("Import", size, headerPreIcon: FontAwesomeIcon.FileImport))
|
using (var frame = ImRaii.FramedGroup("Import", size, headerPreIcon: FontAwesomeIcon.FileImport))
|
||||||
{
|
{
|
||||||
|
ImGui.Checkbox("Keep current materials", ref tab.ImportKeepMaterials);
|
||||||
|
ImGui.Checkbox("Keep current attributes", ref tab.ImportKeepAttributes);
|
||||||
|
|
||||||
if (ImGuiUtil.DrawDisabledButton("Import from glTF", Vector2.Zero, "Imports a glTF file, overriding the content of this mdl.",
|
if (ImGuiUtil.DrawDisabledButton("Import from glTF", Vector2.Zero, "Imports a glTF file, overriding the content of this mdl.",
|
||||||
tab.PendingIo))
|
tab.PendingIo))
|
||||||
_fileDialog.OpenFilePicker("Load model from glTF.", "glTF{.gltf,.glb}", (success, paths) =>
|
_fileDialog.OpenFilePicker("Load model from glTF.", "glTF{.gltf,.glb}", (success, paths) =>
|
||||||
|
|
@ -86,7 +89,6 @@ public partial class ModEditWindow
|
||||||
if (success && paths.Count > 0)
|
if (success && paths.Count > 0)
|
||||||
tab.Import(paths[0]);
|
tab.Import(paths[0]);
|
||||||
}, 1, _mod!.ModPath.FullName, false);
|
}, 1, _mod!.ModPath.FullName, false);
|
||||||
ImGui.Dummy(new Vector2(ImGui.GetFrameHeight()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dragDropManager.CreateImGuiTarget("ModelDragDrop", out var files, out _) && GetFirstModel(files, out var importFile))
|
if (_dragDropManager.CreateImGuiTarget("ModelDragDrop", out var files, out _) && GetFirstModel(files, out var importFile))
|
||||||
|
|
@ -477,5 +479,6 @@ public partial class ModEditWindow
|
||||||
private static readonly string[] ValidModelExtensions =
|
private static readonly string[] ValidModelExtensions =
|
||||||
[
|
[
|
||||||
".gltf",
|
".gltf",
|
||||||
|
".glb",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue