From 9fae88934d2ee827021d01f6bbaf98c7ed3a9bc6 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 17 Jan 2024 00:00:58 +1100 Subject: [PATCH 1/3] Calculate missing tangents on import, convert all to bitangents for use --- .../Import/Models/Import/SubMeshImporter.cs | 6 +- .../Import/Models/Import/VertexAttribute.cs | 123 +++++++++++++++++- 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index 51443f64..e5b5bc8e 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -140,11 +140,15 @@ public class SubMeshImporter private void BuildIndices() { + // TODO: glTF supports a bunch of primitive types, ref. Schema2.PrimitiveType. All this code is currently assuming that it's using plain triangles (4). It should probably be generalised to other formats - I _suspect_ we should be able to get away with evaulating the indices to triangles with GetTriangleIndices, but will need investigation. _indices = _primitive.GetIndices().Select(idx => (ushort)idx).ToArray(); } private void BuildVertexAttributes() { + // Tangent calculation requires indices if missing. + ArgumentNullException.ThrowIfNull(_indices); + var accessors = _primitive.VertexAccessors; var morphAccessors = Enumerable.Range(0, _primitive.MorphTargetsCount) @@ -158,7 +162,7 @@ public class SubMeshImporter VertexAttribute.BlendWeight(accessors), VertexAttribute.BlendIndex(accessors, _nodeBoneMap), VertexAttribute.Normal(accessors, morphAccessors), - VertexAttribute.Tangent1(accessors, morphAccessors), + VertexAttribute.Tangent1(accessors, morphAccessors, _indices), VertexAttribute.Color(accessors), VertexAttribute.Uv(accessors), }; diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 0b0e90ba..5008b58e 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -242,10 +242,24 @@ public class VertexAttribute ); } - public static VertexAttribute? Tangent1(Accessors accessors, IEnumerable morphAccessors) + public static VertexAttribute? Tangent1(Accessors accessors, IEnumerable morphAccessors, ushort[] indices) { - if (!accessors.TryGetValue("TANGENT", out var accessor)) + if (!accessors.TryGetValue("NORMAL", out var normalAccessor)) + { + Penumbra.Log.Warning("Normals are required to facilitate import or calculation of tangents."); return null; + } + + var normals = normalAccessor.AsVector3Array(); + var values = accessors.TryGetValue("TANGENT", out var accessor) + ? accessor.AsVector4Array() + : CalculateTangents(accessors, indices, normals); + + if (values == null) + { + Penumbra.Log.Warning("No tangents available for sub-mesh. This could lead to incorrect lighting, or mismatched vertex attributes."); + return null; + } var element = new MdlStructs.VertexElement() { @@ -254,8 +268,6 @@ public class VertexAttribute Usage = (byte)MdlFile.VertexUsage.Tangent1, }; - var values = accessor.AsVector4Array(); - // Per glTF specification, TANGENT morph values are stored as vec3, with the W component always considered to be 0. var morphValues = morphAccessors .Select(a => a.GetValueOrDefault("TANGENT")?.AsVector3Array()) @@ -263,7 +275,7 @@ public class VertexAttribute return new VertexAttribute( element, - index => BuildByteFloat4(values[index]), + index => BuildBitangent(values[index], normals[index]), buildMorph: (morphIndex, vertexIndex) => { var value = values[vertexIndex]; @@ -272,11 +284,110 @@ public class VertexAttribute if (delta != null) value += new Vector4(delta.Value, 0); - return BuildByteFloat4(value); + return BuildBitangent(value, normals[vertexIndex]); } ); } + /// Build a byte array representing bitagent data computed from the provided tangent and normal. + /// XIV primarily stores bitangents, rather than tangents as with most other software, so we calculate on import. + private static byte[] BuildBitangent(Vector4 tangent, Vector3 normal) + { + var handedness = tangent.W; + var tangent3 = new Vector3(tangent.X, tangent.Y, tangent.Z); + var bitangent = Vector3.Normalize(Vector3.Cross(normal, tangent3)); + bitangent *= handedness; + + // Byte floats encode 0..1, and bitangents are stored as -1..1. Convert. + bitangent = (bitangent + Vector3.One) / 2; + return BuildByteFloat4(new Vector4(bitangent, handedness)); + } + + /// Attempt to calculate tangent values based on other pre-existing data. + private static Vector4[]? CalculateTangents(Accessors accessors, ushort[] indices, IList normals) + { + // To calculate tangents, we will also need access to uv data. + if (!accessors.TryGetValue("TEXCOORD_0", out var uvAccessor)) + return null; + + var positions = accessors["POSITION"].AsVector3Array(); + var uvs = uvAccessor.AsVector2Array(); + + // TODO: Surface this in the UI. + Penumbra.Log.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; + + // https://github.com/TexTools/xivModdingFramework/blob/master/xivModdingFramework/Models/Helpers/ModelModifiers.cs#L1569 + // https://gamedev.stackexchange.com/a/68617 + // https://marti.works/posts/post-calculating-tangents-for-your-mesh/post/ + var tangents = new Vector3[vertexCount]; + var bitangents = new Vector3[vertexCount]; + + // Iterate over triangles, calculating tangents relative to the UVs. + for (var index = 0; index < indices.Length; index += 3) + { + // Collect information for this triangle. + var vertexIndex1 = indices[index]; + var vertexIndex2 = indices[index + 1]; + var vertexIndex3 = indices[index + 2]; + + var position1 = positions[vertexIndex1]; + var position2 = positions[vertexIndex2]; + var position3 = positions[vertexIndex3]; + + var texcoord1 = uvs[vertexIndex1]; + var texcoord2 = uvs[vertexIndex2]; + var texcoord3 = uvs[vertexIndex3]; + + // Calculate deltas for the position XYZ, and texcoord UV. + var edge1 = position2 - position1; + var edge2 = position3 - position1; + + var uv1 = texcoord2 - texcoord1; + var uv2 = texcoord3 - texcoord1; + + // Solve. + var r = 1.0f / (uv1.X * uv2.Y - uv1.Y * uv2.X); + var tangent = new Vector3( + (edge1.X * uv2.Y - edge2.X * uv1.Y) * r, + (edge1.Y * uv2.Y - edge2.Y * uv1.Y) * r, + (edge1.Z * uv2.Y - edge2.Z * uv1.Y) * r + ); + var bitangent = new Vector3( + (edge1.X * uv2.X - edge2.X * uv1.X) * r, + (edge1.Y * uv2.X - edge2.Y * uv1.X) * r, + (edge1.Z * uv2.X - edge2.Z * uv1.X) * r + ); + + // Update vertex values. + tangents[vertexIndex1] += tangent; + tangents[vertexIndex2] += tangent; + tangents[vertexIndex3] += tangent; + + bitangents[vertexIndex1] += bitangent; + bitangents[vertexIndex2] += bitangent; + bitangents[vertexIndex3] += bitangent; + } + + // All the triangles have been calcualted, normalise the results for each vertex. + var result = new Vector4[vertexCount]; + for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) + { + var n = normals[vertexIndex]; + var t = tangents[vertexIndex]; + var b = bitangents[vertexIndex]; + + // Gram-Schmidt orthogonalize and calculate handedness. + var tangent = Vector3.Normalize(t - n * Vector3.Dot(n, t)); + var handedness = Vector3.Dot(Vector3.Cross(t, b), n) > 0 ? 1 : -1; + + result[vertexIndex] = new Vector4(tangent, handedness); + } + + return result; + } + public static VertexAttribute? Color(Accessors accessors) { if (!accessors.TryGetValue("COLOR_0", out var accessor)) From ea04cc554f9e1b6be72ba538b9ea737e6dbfec45 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 17 Jan 2024 00:22:13 +1100 Subject: [PATCH 2/3] Fix up export a little --- Penumbra/Import/Models/Export/MeshExporter.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 8127f348..b00ca49e 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -318,11 +318,17 @@ public class MeshExporter ); if (_geometryType == typeof(VertexPositionNormalTangent)) + { + // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. + // TODO: While this assumption is safe, it would be sensible to actually check. + var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1]) * 2 - Vector4.One; + return new VertexPositionNormalTangent( ToVector3(attributes[MdlFile.VertexUsage.Position]), ToVector3(attributes[MdlFile.VertexUsage.Normal]), - FixTangentVector(ToVector4(attributes[MdlFile.VertexUsage.Tangent1])) + bitangent ); + } throw new Exception($"Unknown geometry type {_geometryType}."); } @@ -440,11 +446,6 @@ public class MeshExporter throw new Exception($"Unknown skinning type {_skinningType}"); } - /// Clamps any tangent W value other than 1 to -1. - /// Some XIV models seemingly store -1 as 0, this patches over that. - private static Vector4 FixTangentVector(Vector4 tangent) - => tangent with { W = tangent.W == 1 ? 1 : -1 }; - /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. private static Vector2 ToVector2(object data) => data switch From 5e794b73baca68c557093ebcd0b4a1cecdd41659 Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 17 Jan 2024 01:40:19 +1100 Subject: [PATCH 3/3] Consider normal morphs for morphed bitangent calculations --- .../Import/Models/Import/VertexAttribute.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 5008b58e..bbf49bcf 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -251,11 +251,11 @@ public class VertexAttribute } var normals = normalAccessor.AsVector3Array(); - var values = accessors.TryGetValue("TANGENT", out var accessor) + var tangents = accessors.TryGetValue("TANGENT", out var accessor) ? accessor.AsVector4Array() : CalculateTangents(accessors, indices, normals); - if (values == null) + if (tangents == null) { Penumbra.Log.Warning("No tangents available for sub-mesh. This could lead to incorrect lighting, or mismatched vertex attributes."); return null; @@ -269,22 +269,30 @@ public class VertexAttribute }; // Per glTF specification, TANGENT morph values are stored as vec3, with the W component always considered to be 0. - var morphValues = morphAccessors + var tangentMorphValues = morphAccessors + .Select(a => a.GetValueOrDefault("TANGENT")?.AsVector3Array()) + .ToArray(); + + var normalMorphValues = morphAccessors .Select(a => a.GetValueOrDefault("TANGENT")?.AsVector3Array()) .ToArray(); return new VertexAttribute( element, - index => BuildBitangent(values[index], normals[index]), + index => BuildBitangent(tangents[index], normals[index]), buildMorph: (morphIndex, vertexIndex) => { - var value = values[vertexIndex]; + var tangent = tangents[vertexIndex]; + var tangentDelta = tangentMorphValues[morphIndex]?[vertexIndex]; + if (tangentDelta != null) + tangent += new Vector4(tangentDelta.Value, 0); - var delta = morphValues[morphIndex]?[vertexIndex]; - if (delta != null) - value += new Vector4(delta.Value, 0); + var normal = normals[vertexIndex]; + var normalDelta = normalMorphValues[morphIndex]?[vertexIndex]; + if (normalDelta != null) + normal += normalDelta.Value; - return BuildBitangent(value, normals[vertexIndex]); + return BuildBitangent(tangent, normal); } ); }