diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 3a57ab55..20158776 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -311,15 +311,28 @@ public class MeshExporter 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.UByte4 => reader.ReadBytes(4), - MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, - reader.ReadByte() / 255f), + MdlFile.VertexType.NByte4 => 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()), - + MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()), + MdlFile.VertexType.UShort4 => ReadUShort4(reader), var other => throw _notifier.Exception($"Unhandled vertex type {other}"), }; } + + private byte[] ReadUShort4(BinaryReader reader) + { + var buffer = reader.ReadBytes(8); + var byteValues = new byte[8]; + byteValues[0] = buffer[0]; + byteValues[4] = buffer[1]; + byteValues[1] = buffer[2]; + byteValues[5] = buffer[3]; + byteValues[2] = buffer[4]; + byteValues[6] = buffer[5]; + byteValues[3] = buffer[6]; + byteValues[7] = buffer[7]; + return byteValues; + } /// Get the vertex geometry type for this mesh's vertex usages. private Type GetGeometryType(IReadOnlyDictionary usages) @@ -444,7 +457,16 @@ public class MeshExporter private static Type GetSkinningType(IReadOnlyDictionary usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) - return typeof(VertexJoints4); + { + if (usages[MdlFile.VertexUsage.BlendWeights] == MdlFile.VertexType.UShort4) + { + return typeof(VertexJoints8); + } + else + { + return typeof(VertexJoints4); + } + } return typeof(VertexEmpty); } @@ -455,15 +477,17 @@ public class MeshExporter if (_skinningType == typeof(VertexEmpty)) return new VertexEmpty(); - if (_skinningType == typeof(VertexJoints4)) + if (_skinningType == typeof(VertexJoints4) || _skinningType == typeof(VertexJoints8)) { if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indices = ToByteArray(attributes[MdlFile.VertexUsage.BlendIndices]); - var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]); - - var bindings = Enumerable.Range(0, 4) + var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices]; + var weightsData = attributes[MdlFile.VertexUsage.BlendWeights]; + var indices = ToByteArray(indiciesData); + var weights = ToFloatArray(weightsData); + + var bindings = Enumerable.Range(0, indices.Length) .Select(bindingIndex => { // NOTE: I've not seen any files that throw this error that aren't completely broken. @@ -474,7 +498,13 @@ public class MeshExporter return (jointIndex, weights[bindingIndex]); }) .ToArray(); - return new VertexJoints4(bindings); + + return bindings.Length switch + { + 4 => new VertexJoints4(bindings), + 8 => new VertexJoints8(bindings), + _ => throw _notifier.Exception($"Invalid number of bone bindings {bindings.Length}.") + }; } throw _notifier.Exception($"Unknown skinning type {_skinningType}"); @@ -517,4 +547,12 @@ public class MeshExporter byte[] value => value, _ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}"), }; + + private static float[] ToFloatArray(object data) + => data switch + { + byte[] value => value.Select(x => x / 255f).ToArray(), + _ => throw new ArgumentOutOfRangeException($"Invalid float[] input {data}"), + }; } + diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 1df97907..e3567780 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -194,17 +194,37 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) foreach (var (primitive, primitiveIndex) in node.Mesh.Primitives.WithIndex()) { // Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes. - var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array(); - var weightsAccessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array(); + var joints0Accessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array(); + var weights0Accessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array(); - if (jointsAccessor == null || weightsAccessor == null) + if (joints0Accessor == null || weights0Accessor == null) throw notifier.Exception($"Primitive {primitiveIndex} is skinned but does not contain skinning vertex attributes."); // Build a set of joints that are referenced by this mesh. - for (var i = 0; i < jointsAccessor.Count; i++) + for (var i = 0; i < joints0Accessor.Count; i++) { - var joints = jointsAccessor[i]; - var weights = weightsAccessor[i]; + var joints = joints0Accessor[i]; + var weights = weights0Accessor[i]; + for (var index = 0; index < 4; index++) + { + // If a joint has absolutely no weight, we omit the bone entirely. + if (weights[index] == 0) + continue; + + usedJoints.Add((ushort)joints[index]); + } + } + + var joints1Accessor = primitive.GetVertexAccessor("JOINTS_1")?.AsVector4Array(); + var weights1Accessor = primitive.GetVertexAccessor("WEIGHTS_1")?.AsVector4Array(); + + if (joints1Accessor == null || weights1Accessor == null) + continue; + + for (var i = 0; i < joints1Accessor.Count; i++) + { + var joints = joints1Accessor[i]; + var weights = weights1Accessor[i]; for (var index = 0; index < 4; index++) { // If a joint has absolutely no weight, we omit the bone entirely. diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index af401ec1..b71ad429 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -40,6 +40,7 @@ public class VertexAttribute MdlFile.VertexType.NByte4 => 4, MdlFile.VertexType.Half2 => 4, MdlFile.VertexType.Half4 => 8, + MdlFile.VertexType.UShort4 => 8, _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), }; @@ -121,89 +122,219 @@ public class VertexAttribute public static VertexAttribute? BlendWeight(Accessors accessors, IoNotifier notifier) { - if (!accessors.TryGetValue("WEIGHTS_0", out var accessor)) + if (!accessors.TryGetValue("WEIGHTS_0", out var weights0Accessor)) return null; if (!accessors.ContainsKey("JOINTS_0")) throw notifier.Exception("Mesh contained WEIGHTS_0 attribute but no corresponding JOINTS_0 attribute."); - var element = new MdlStructs.VertexElement() + if (accessors.TryGetValue("WEIGHTS_1", out var weights1Accessor)) { - Stream = 0, - Type = (byte)MdlFile.VertexType.NByte4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, - }; + if (!accessors.ContainsKey("JOINTS_1")) + throw notifier.Exception("Mesh contained WEIGHTS_1 attribute but no corresponding JOINTS_1 attribute."); + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; - var values = accessor.AsVector4Array(); + var weights0 = weights0Accessor.AsVector4Array(); + var weights1 = weights1Accessor.AsVector4Array(); - return new VertexAttribute( - element, - index => { - // Blend weights are _very_ sensitive to float imprecision - a vertex sum being off - // by one, such as 256, is enough to cause a visible defect. To avoid this, we tweak - // the converted values to have the expected sum, preferencing values with minimal differences. - var originalValues = values[index]; - var byteValues = BuildNByte4(originalValues); - - var adjustment = 255 - byteValues.Select(value => (int)value).Sum(); - while (adjustment != 0) - { - var convertedValues = byteValues.Select(value => value * (1f / 255f)).ToArray(); - var closestIndex = Enumerable.Range(0, 4) - .Where(index => { - var byteValue = byteValues[index]; - if (adjustment < 0) return byteValue > 0; - if (adjustment > 0) return byteValue < 255; - return true; - }) - .Select(index => (index, delta: Math.Abs(originalValues[index] - convertedValues[index]))) - .MinBy(x => x.delta) - .index; - byteValues[closestIndex] = (byte)(byteValues[closestIndex] + Math.CopySign(1, adjustment)); - adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + return new VertexAttribute( + element, + index => { + var weight0 = weights0[index]; + var weight1 = weights1[index]; + var originalData = BuildUshort4(weight0, weight1); + var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); + return AdjustByteArray(byteValues, originalData); } - - return byteValues; - } - ); + ); + } + else + { + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; + + var weights0 = weights0Accessor.AsVector4Array(); + + return new VertexAttribute( + element, + index => { + var weight0 = weights0[index]; + var weight1 = Vector4.Zero; + var originalData = BuildUshort4(weight0, weight1); + var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); + return AdjustByteArray(byteValues, originalData); + } + ); + + /*var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.NByte4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; + + var weights0 = weights0Accessor.AsVector4Array(); + + return new VertexAttribute( + element, + index => + { + var weight0 = weights0[index]; + var originalData = new[] { weight0.X, weight0.Y, weight0.Z, weight0.W }; + var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); + var newByteValues = AdjustByteArray(byteValues, originalData); + if (!newByteValues.SequenceEqual(byteValues)) + notifier.Warning("Adjusted blend weights to maintain precision."); + return newByteValues; + });*/ + } + } + + private static byte[] AdjustByteArray(byte[] byteValues, float[] originalValues) + { + // Blend weights are _very_ sensitive to float imprecision - a vertex sum being off + // by one, such as 256, is enough to cause a visible defect. To avoid this, we tweak + // the converted values to have the expected sum, preferencing values with minimal differences. + var adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + while (adjustment != 0) + { + var convertedValues = byteValues.Select(value => value * (1f / 255f)).ToArray(); + var closestIndex = Enumerable.Range(0, byteValues.Length) + .Where(index => + { + var byteValue = byteValues[index]; + if (adjustment < 0) + return byteValue > 0; + if (adjustment > 0) + return byteValue < 255; + + return true; + }) + .Select(index => (index, delta: Math.Abs(originalValues[index] - convertedValues[index]))) + .MinBy(x => x.delta) + .index; + byteValues[closestIndex] = (byte)(byteValues[closestIndex] + Math.CopySign(1, adjustment)); + adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + } + + return byteValues; } public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap, IoNotifier notifier) { - if (!accessors.TryGetValue("JOINTS_0", out var jointsAccessor)) + if (!accessors.TryGetValue("JOINTS_0", out var joints0Accessor)) return null; - if (!accessors.TryGetValue("WEIGHTS_0", out var weightsAccessor)) + if (!accessors.TryGetValue("WEIGHTS_0", out var weights0Accessor)) throw notifier.Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); if (boneMap == null) throw notifier.Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created."); - var element = new MdlStructs.VertexElement() + var joints0 = joints0Accessor.AsVector4Array(); + var weights0 = weights0Accessor.AsVector4Array(); + + if (accessors.TryGetValue("JOINTS_1", out var joints1Accessor)) { - Stream = 0, - Type = (byte)MdlFile.VertexType.UByte4, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, - }; + if (!accessors.TryGetValue("WEIGHTS_1", out var weights1Accessor)) + throw notifier.Exception("Mesh contained JOINTS_1 attribute but no corresponding WEIGHTS_1 attribute."); - var joints = jointsAccessor.AsVector4Array(); - var weights = weightsAccessor.AsVector4Array(); - - return new VertexAttribute( - element, - index => + var element = new MdlStructs.VertexElement { - var gltfIndices = joints[index]; - var gltfWeights = weights[index]; + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; - return BuildUByte4(new Vector4( - gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], - gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], - gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], - gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] - )); - } - ); + var joints1 = joints1Accessor.AsVector4Array(); + var weights1 = weights1Accessor.AsVector4Array(); + + return new VertexAttribute( + element, + index => + { + var gltfIndices0 = joints0[index]; + var gltfWeights0 = weights0[index]; + var gltfIndices1 = joints1[index]; + var gltfWeights1 = weights1[index]; + var v0 = new Vector4( + gltfWeights0.X == 0 ? 0 : boneMap[(ushort)gltfIndices0.X], + gltfWeights0.Y == 0 ? 0 : boneMap[(ushort)gltfIndices0.Y], + gltfWeights0.Z == 0 ? 0 : boneMap[(ushort)gltfIndices0.Z], + gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] + ); + var v1 = new Vector4( + gltfWeights1.X == 0 ? 0 : boneMap[(ushort)gltfIndices1.X], + gltfWeights1.Y == 0 ? 0 : boneMap[(ushort)gltfIndices1.Y], + gltfWeights1.Z == 0 ? 0 : boneMap[(ushort)gltfIndices1.Z], + gltfWeights1.W == 0 ? 0 : boneMap[(ushort)gltfIndices1.W] + ); + + var byteValues = BuildUshort4(v0, v1); + + return byteValues.Select(x => (byte)x).ToArray(); + } + ); + } + else + { + var element = new MdlStructs.VertexElement + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; + + return new VertexAttribute( + element, + index => + { + var gltfIndices0 = joints0[index]; + var gltfWeights0 = weights0[index]; + var v0 = new Vector4( + gltfWeights0.X == 0 ? 0 : boneMap[(ushort)gltfIndices0.X], + gltfWeights0.Y == 0 ? 0 : boneMap[(ushort)gltfIndices0.Y], + gltfWeights0.Z == 0 ? 0 : boneMap[(ushort)gltfIndices0.Z], + gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] + ); + var v1 = Vector4.Zero; + var byteValues = BuildUshort4(v0, v1); + + return byteValues.Select(x => (byte)x).ToArray(); + } + ); + /*var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UByte4, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; + + return new VertexAttribute( + element, + index => + { + var gltfIndices = joints0[index]; + var gltfWeights = weights0[index]; + return BuildUByte4(new Vector4( + gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], + gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], + gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], + gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] + )); + } + );*/ + } } public static VertexAttribute? Normal(Accessors accessors, IEnumerable morphAccessors) @@ -232,7 +363,7 @@ public class VertexAttribute var value = values[vertexIndex]; var delta = morphValues[morphIndex]?[vertexIndex]; - if (delta != null) + if (delta != null) value += delta.Value; return BuildSingle3(value); @@ -489,4 +620,18 @@ public class VertexAttribute (byte)Math.Round(input.Z * 255f), (byte)Math.Round(input.W * 255f), ]; + + private static float[] BuildUshort4(Vector4 v0, Vector4 v1) + { + var buf = new float[8]; + buf[0] = v0.X; + buf[4] = v1.X; + buf[1] = v0.Y; + buf[5] = v1.Y; + buf[2] = v0.Z; + buf[6] = v1.Z; + buf[3] = v0.W; + buf[7] = v1.W; + return buf; + } }