mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Wire up notifier through import
This commit is contained in:
parent
6f3be39cb9
commit
6da725350a
6 changed files with 63 additions and 51 deletions
|
|
@ -3,7 +3,7 @@ using SharpGLTF.Schema2;
|
|||
|
||||
namespace Penumbra.Import.Models.Import;
|
||||
|
||||
public class MeshImporter(IEnumerable<Node> nodes)
|
||||
public class MeshImporter(IEnumerable<Node> nodes, IoNotifier notifier)
|
||||
{
|
||||
public struct Mesh
|
||||
{
|
||||
|
|
@ -33,9 +33,9 @@ public class MeshImporter(IEnumerable<Node> nodes)
|
|||
public List<MdlStructs.ShapeValueStruct> ShapeValues;
|
||||
}
|
||||
|
||||
public static Mesh Import(IEnumerable<Node> nodes)
|
||||
public static Mesh Import(IEnumerable<Node> nodes, IoNotifier notifier)
|
||||
{
|
||||
var importer = new MeshImporter(nodes);
|
||||
var importer = new MeshImporter(nodes, notifier);
|
||||
return importer.Create();
|
||||
}
|
||||
|
||||
|
|
@ -115,11 +115,11 @@ public class MeshImporter(IEnumerable<Node> nodes)
|
|||
var vertexOffset = _vertexCount;
|
||||
var indexOffset = _indices.Count;
|
||||
|
||||
var nodeBoneMap = CreateNodeBoneMap(node);
|
||||
var subMesh = SubMeshImporter.Import(node, nodeBoneMap);
|
||||
|
||||
var subMeshName = node.Name ?? node.Mesh.Name;
|
||||
|
||||
var nodeBoneMap = CreateNodeBoneMap(node);
|
||||
var subMesh = SubMeshImporter.Import(node, nodeBoneMap, notifier.WithContext($"Sub-mesh {subMeshName}"));
|
||||
|
||||
// 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;
|
||||
|
||||
|
|
@ -127,8 +127,11 @@ public class MeshImporter(IEnumerable<Node> nodes)
|
|||
if (_vertexDeclaration == null)
|
||||
_vertexDeclaration = subMesh.VertexDeclaration;
|
||||
else if (VertexDeclarationMismatch(subMesh.VertexDeclaration, _vertexDeclaration.Value))
|
||||
throw new Exception(
|
||||
$"Sub-mesh \"{subMeshName}\" vertex declaration mismatch. All sub-meshes of a mesh must have equivalent vertex declarations.");
|
||||
throw notifier.Exception(
|
||||
$@"All sub-meshes of a mesh must have equivalent vertex declarations.
|
||||
Current: {FormatVertexDeclaration(_vertexDeclaration.Value)}
|
||||
Sub-mesh ""{subMeshName}"": {FormatVertexDeclaration(subMesh.VertexDeclaration)}"
|
||||
);
|
||||
|
||||
// Given that strides are derived from declarations, a lack of mismatch in declarations means the strides are fine.
|
||||
// TODO: I mean, given that strides are derivable, might be worth dropping strides from the sub mesh return structure and computing when needed.
|
||||
|
|
@ -170,6 +173,9 @@ public class MeshImporter(IEnumerable<Node> nodes)
|
|||
});
|
||||
}
|
||||
|
||||
private static string FormatVertexDeclaration(MdlStructs.VertexDeclarationStruct vertexDeclaration)
|
||||
=> string.Join(", ", vertexDeclaration.VertexElements.Select(element => $"{element.Usage} ({element.Type}@{element.Stream}:{element.Offset})"));
|
||||
|
||||
private static bool VertexDeclarationMismatch(MdlStructs.VertexDeclarationStruct a, MdlStructs.VertexDeclarationStruct b)
|
||||
{
|
||||
var elA = a.VertexElements;
|
||||
|
|
@ -204,13 +210,13 @@ public class MeshImporter(IEnumerable<Node> nodes)
|
|||
var meshName = node.Name ?? mesh.Name ?? "(no name)";
|
||||
var primitiveCount = mesh.Primitives.Count;
|
||||
if (primitiveCount != 1)
|
||||
throw new Exception($"Mesh \"{meshName}\" has {primitiveCount} primitives, expected 1.");
|
||||
throw notifier.Exception($"Mesh \"{meshName}\" has {primitiveCount} primitives, expected 1.");
|
||||
|
||||
var primitive = mesh.Primitives[0];
|
||||
|
||||
// Per glTF specification, an asset with a skin MUST contain skinning attributes on its mesh.
|
||||
var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0")
|
||||
?? throw new Exception($"Skinned mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes.");
|
||||
?? throw notifier.Exception($"Skinned mesh \"{meshName}\" is skinned but does not contain skinning vertex attributes.");
|
||||
|
||||
// Build a set of joints that are referenced by this mesh.
|
||||
// TODO: Would be neat to omit 0-weighted joints here, but doing so will require some further work on bone mapping behavior to ensure the unweighted joints can still be resolved to valid bone indices during vertex data construction.
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
using Lumina.Data.Parsing;
|
||||
using OtterGui;
|
||||
using Penumbra.GameData.Files;
|
||||
using SharpGLTF.Schema2;
|
||||
|
||||
namespace Penumbra.Import.Models.Import;
|
||||
|
||||
public partial class ModelImporter(ModelRoot model)
|
||||
public partial class ModelImporter(ModelRoot model, IoNotifier notifier)
|
||||
{
|
||||
public static MdlFile Import(ModelRoot model)
|
||||
public static MdlFile Import(ModelRoot model, IoNotifier notifier)
|
||||
{
|
||||
var importer = new ModelImporter(model);
|
||||
var importer = new ModelImporter(model, notifier);
|
||||
return importer.Create();
|
||||
}
|
||||
|
||||
|
|
@ -39,8 +40,8 @@ public partial class ModelImporter(ModelRoot model)
|
|||
private MdlFile Create()
|
||||
{
|
||||
// Group and build out meshes in this model.
|
||||
foreach (var subMeshNodes in GroupedMeshNodes())
|
||||
BuildMeshForGroup(subMeshNodes);
|
||||
foreach (var (subMeshNodes, index) in GroupedMeshNodes().WithIndex())
|
||||
BuildMeshForGroup(subMeshNodes, index);
|
||||
|
||||
// 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"];
|
||||
|
|
@ -128,7 +129,7 @@ public partial class ModelImporter(ModelRoot model)
|
|||
)
|
||||
.OrderBy(group => group.Key);
|
||||
|
||||
private void BuildMeshForGroup(IEnumerable<Node> subMeshNodes)
|
||||
private void BuildMeshForGroup(IEnumerable<Node> subMeshNodes, int index)
|
||||
{
|
||||
// Record some offsets we'll be using later, before they get mutated with mesh values.
|
||||
var subMeshOffset = _subMeshes.Count;
|
||||
|
|
@ -136,7 +137,7 @@ public partial class ModelImporter(ModelRoot model)
|
|||
var indexOffset = _indices.Count;
|
||||
var shapeValueOffset = _shapeValues.Count;
|
||||
|
||||
var mesh = MeshImporter.Import(subMeshNodes);
|
||||
var mesh = MeshImporter.Import(subMeshNodes, notifier.WithContext($"Mesh {index}"));
|
||||
var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset);
|
||||
|
||||
var materialIndex = mesh.Material != null
|
||||
|
|
@ -196,7 +197,7 @@ public partial class ModelImporter(ModelRoot model)
|
|||
// arrays, values is practically guaranteed to be the highest of the
|
||||
// group, so a failure on any of them will be a failure on it.
|
||||
if (_shapeValues.Count > ushort.MaxValue)
|
||||
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 notifier.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)
|
||||
|
|
@ -232,7 +233,7 @@ public partial class ModelImporter(ModelRoot model)
|
|||
}
|
||||
|
||||
if (boneIndices.Count > 64)
|
||||
throw new Exception("XIV does not support meshes weighted to more than 64 bones.");
|
||||
throw notifier.Exception("XIV does not support meshes weighted to a total of more than 64 bones.");
|
||||
|
||||
var boneIndicesArray = new ushort[64];
|
||||
Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count);
|
||||
|
|
|
|||
|
|
@ -28,12 +28,14 @@ public class SubMeshImporter
|
|||
public Dictionary<string, List<MdlStructs.ShapeValueStruct>> ShapeValues;
|
||||
}
|
||||
|
||||
public static SubMesh Import(Node node, IDictionary<ushort, ushort>? nodeBoneMap)
|
||||
public static SubMesh Import(Node node, IDictionary<ushort, ushort>? nodeBoneMap, IoNotifier notifier)
|
||||
{
|
||||
var importer = new SubMeshImporter(node, nodeBoneMap);
|
||||
var importer = new SubMeshImporter(node, nodeBoneMap, notifier);
|
||||
return importer.Create();
|
||||
}
|
||||
|
||||
private readonly IoNotifier _notifier;
|
||||
|
||||
private readonly MeshPrimitive _primitive;
|
||||
private readonly IDictionary<ushort, ushort>? _nodeBoneMap;
|
||||
private readonly IDictionary<string, JsonElement>? _nodeExtras;
|
||||
|
|
@ -53,16 +55,15 @@ public class SubMeshImporter
|
|||
private readonly List<string>? _morphNames;
|
||||
private Dictionary<string, List<MdlStructs.ShapeValueStruct>>? _shapeValues;
|
||||
|
||||
private SubMeshImporter(Node node, IDictionary<ushort, ushort>? nodeBoneMap)
|
||||
private SubMeshImporter(Node node, IDictionary<ushort, ushort>? nodeBoneMap, IoNotifier notifier)
|
||||
{
|
||||
_notifier = notifier;
|
||||
|
||||
var mesh = node.Mesh;
|
||||
|
||||
var primitiveCount = mesh.Primitives.Count;
|
||||
if (primitiveCount != 1)
|
||||
{
|
||||
var name = node.Name ?? mesh.Name ?? "(no name)";
|
||||
throw new Exception($"Mesh \"{name}\" has {primitiveCount} primitives, expected 1.");
|
||||
}
|
||||
throw _notifier.Exception($"Mesh has {primitiveCount} primitives, expected 1.");
|
||||
|
||||
_primitive = mesh.Primitives[0];
|
||||
_nodeBoneMap = nodeBoneMap;
|
||||
|
|
@ -115,7 +116,7 @@ public class SubMeshImporter
|
|||
{
|
||||
< 32 => (1u << _metaAttributes.Length) - 1,
|
||||
32 => uint.MaxValue,
|
||||
> 32 => throw new Exception("Models may utilise a maximum of 32 attributes."),
|
||||
> 32 => throw _notifier.Exception("Models may utilise a maximum of 32 attributes."),
|
||||
};
|
||||
|
||||
return new SubMesh()
|
||||
|
|
@ -165,11 +166,11 @@ public class SubMeshImporter
|
|||
// The order here is chosen to match a typical model's element order.
|
||||
var rawAttributes = new[]
|
||||
{
|
||||
VertexAttribute.Position(accessors, morphAccessors),
|
||||
VertexAttribute.BlendWeight(accessors),
|
||||
VertexAttribute.BlendIndex(accessors, _nodeBoneMap),
|
||||
VertexAttribute.Position(accessors, morphAccessors, _notifier),
|
||||
VertexAttribute.BlendWeight(accessors, _notifier),
|
||||
VertexAttribute.BlendIndex(accessors, _nodeBoneMap, _notifier),
|
||||
VertexAttribute.Normal(accessors, morphAccessors),
|
||||
VertexAttribute.Tangent1(accessors, morphAccessors, _indices),
|
||||
VertexAttribute.Tangent1(accessors, morphAccessors, _indices, _notifier),
|
||||
VertexAttribute.Color(accessors),
|
||||
VertexAttribute.Uv(accessors),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,10 +72,10 @@ public class VertexAttribute
|
|||
private byte[] DefaultBuildMorph(int morphIndex, int vertexIndex)
|
||||
=> Build(vertexIndex);
|
||||
|
||||
public static VertexAttribute Position(Accessors accessors, IEnumerable<Accessors> morphAccessors)
|
||||
public static VertexAttribute Position(Accessors accessors, IEnumerable<Accessors> morphAccessors, IoNotifier notifier)
|
||||
{
|
||||
if (!accessors.TryGetValue("POSITION", out var accessor))
|
||||
throw new Exception("Meshes must contain a POSITION attribute.");
|
||||
throw notifier.Exception("Meshes must contain a POSITION attribute.");
|
||||
|
||||
var element = new MdlStructs.VertexElement()
|
||||
{
|
||||
|
|
@ -115,13 +115,13 @@ public class VertexAttribute
|
|||
);
|
||||
}
|
||||
|
||||
public static VertexAttribute? BlendWeight(Accessors accessors)
|
||||
public static VertexAttribute? BlendWeight(Accessors accessors, IoNotifier notifier)
|
||||
{
|
||||
if (!accessors.TryGetValue("WEIGHTS_0", out var accessor))
|
||||
return null;
|
||||
|
||||
if (!accessors.ContainsKey("JOINTS_0"))
|
||||
throw new Exception("Mesh contained WEIGHTS_0 attribute but no corresponding JOINTS_0 attribute.");
|
||||
throw notifier.Exception("Mesh contained WEIGHTS_0 attribute but no corresponding JOINTS_0 attribute.");
|
||||
|
||||
var element = new MdlStructs.VertexElement()
|
||||
{
|
||||
|
|
@ -138,16 +138,16 @@ public class VertexAttribute
|
|||
);
|
||||
}
|
||||
|
||||
public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary<ushort, ushort>? boneMap)
|
||||
public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary<ushort, ushort>? boneMap, IoNotifier notifier)
|
||||
{
|
||||
if (!accessors.TryGetValue("JOINTS_0", out var accessor))
|
||||
return null;
|
||||
|
||||
if (!accessors.ContainsKey("WEIGHTS_0"))
|
||||
throw new Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute.");
|
||||
throw notifier.Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute.");
|
||||
|
||||
if (boneMap == null)
|
||||
throw new Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created.");
|
||||
throw notifier.Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created.");
|
||||
|
||||
var element = new MdlStructs.VertexElement()
|
||||
{
|
||||
|
|
@ -242,22 +242,22 @@ public class VertexAttribute
|
|||
);
|
||||
}
|
||||
|
||||
public static VertexAttribute? Tangent1(Accessors accessors, IEnumerable<Accessors> morphAccessors, ushort[] indices)
|
||||
public static VertexAttribute? Tangent1(Accessors accessors, IEnumerable<Accessors> morphAccessors, ushort[] indices, IoNotifier notifier)
|
||||
{
|
||||
if (!accessors.TryGetValue("NORMAL", out var normalAccessor))
|
||||
{
|
||||
Penumbra.Log.Warning("Normals are required to facilitate import or calculation of tangents.");
|
||||
notifier.Warning("Normals are required to facilitate import or calculation of tangents.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var normals = normalAccessor.AsVector3Array();
|
||||
var tangents = accessors.TryGetValue("TANGENT", out var accessor)
|
||||
? accessor.AsVector4Array()
|
||||
: CalculateTangents(accessors, indices, normals);
|
||||
: CalculateTangents(accessors, indices, normals, notifier);
|
||||
|
||||
if (tangents == null)
|
||||
{
|
||||
Penumbra.Log.Warning("No tangents available for sub-mesh. This could lead to incorrect lighting, or mismatched vertex attributes.");
|
||||
notifier.Warning("No tangents available for sub-mesh. This could lead to incorrect lighting, or mismatched vertex attributes.");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -309,7 +309,7 @@ public class VertexAttribute
|
|||
}
|
||||
|
||||
/// <summary> Attempt to calculate tangent values based on other pre-existing data. </summary>
|
||||
private static Vector4[]? CalculateTangents(Accessors accessors, ushort[] indices, IList<Vector3> normals)
|
||||
private static Vector4[]? CalculateTangents(Accessors accessors, ushort[] indices, IList<Vector3> normals, IoNotifier notifier)
|
||||
{
|
||||
// To calculate tangents, we will also need access to uv data.
|
||||
if (!accessors.TryGetValue("TEXCOORD_0", out var uvAccessor))
|
||||
|
|
@ -318,8 +318,7 @@ public class VertexAttribute
|
|||
var positions = accessors["POSITION"].AsVector3Array();
|
||||
var uvs = uvAccessor.AsVector2Array();
|
||||
|
||||
// TODO: Surface this in the UI.
|
||||
Penumbra.Log.Warning(
|
||||
notifier.Warning(
|
||||
"Calculating tangents, this may result in degraded light interaction. For best results, ensure tangents are caculated or retained during export from 3D modelling tools.");
|
||||
|
||||
var vertexCount = positions.Count;
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
|
|||
action => action.Notifier
|
||||
);
|
||||
|
||||
public Task<MdlFile?> ImportGltf(string inputPath)
|
||||
public Task<(MdlFile?, IoNotifier)> ImportGltf(string inputPath)
|
||||
=> EnqueueWithResult(
|
||||
new ImportGltfAction(inputPath),
|
||||
action => action.Out
|
||||
action => (action.Out, action.Notifier)
|
||||
);
|
||||
|
||||
/// <summary> Try to find the .sklb paths for a .mdl file. </summary>
|
||||
|
|
@ -273,12 +273,13 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect
|
|||
private partial class ImportGltfAction(string inputPath) : IAction
|
||||
{
|
||||
public MdlFile? Out;
|
||||
public IoNotifier Notifier = new IoNotifier();
|
||||
|
||||
public void Execute(CancellationToken cancel)
|
||||
{
|
||||
var model = Schema2.ModelRoot.Load(inputPath);
|
||||
|
||||
Out = ModelImporter.Import(model);
|
||||
Out = ModelImporter.Import(model, Notifier);
|
||||
}
|
||||
|
||||
public bool Equals(IAction? other)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using Lumina.Data.Parsing;
|
|||
using OtterGui;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Import.Models;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
|
|
@ -146,11 +147,14 @@ public partial class ModEditWindow
|
|||
{
|
||||
PendingIo = true;
|
||||
_edit._models.ImportGltf(inputPath)
|
||||
.ContinueWith(task =>
|
||||
.ContinueWith((Task<(MdlFile?, IoNotifier)> task) =>
|
||||
{
|
||||
RecordIoExceptions(task.Exception);
|
||||
if (task is { IsCompletedSuccessfully: true, Result: not null })
|
||||
FinalizeImport(task.Result);
|
||||
if (task is { IsCompletedSuccessfully: true, Result: (not null, _) })
|
||||
{
|
||||
IoWarnings = task.Result.Item2.GetWarnings().ToList();
|
||||
FinalizeImport(task.Result.Item1);
|
||||
}
|
||||
PendingIo = false;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue