using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text; using TRANSFORM = System.Numerics.Matrix4x4; using V3 = System.Numerics.Vector3; using V4 = System.Numerics.Vector4; namespace SharpGLTF.Transforms { /// /// Interface for a mesh transform object /// public interface IGeometryTransform { /// /// Gets a value indicating whether the current will render visible geometry. /// /// /// When this value is false, a runtime should skip rendering any geometry using /// this instance, since it will not be visible anyway. /// bool Visible { get; } /// /// Gets a value indicating whether the triangles need to be flipped to render correctly. /// /// /// When this value is true, a runtime rendering triangles should inverse the face culling. /// bool FlipFaces { get; } V3 TransformPosition(V3 position, V3[] morphTargets, in SparseWeight8 skinWeights); V3 TransformNormal(V3 normal, V3[] morphTargets, in SparseWeight8 skinWeights); V4 TransformTangent(V4 tangent, V3[] morphTargets, in SparseWeight8 skinWeights); V4 MorphColors(V4 color, V4[] morphTargets); } public abstract class MorphTransform { #region constructor protected MorphTransform() { Update(default, false); } protected MorphTransform(SparseWeight8 morphWeights, bool useAbsoluteMorphTargets) { Update(morphWeights, useAbsoluteMorphTargets); } #endregion #region data /// /// Represents a sparse collection of weights where: /// - Index of value points to the Mesh master positions. /// - All other indices point to Mesh MorphTarget[index] positions. /// private SparseWeight8 _Weights; public const int COMPLEMENT_INDEX = 65536; /// /// True if morph targets represent absolute values. /// False if morph targets represent values relative to master value. /// private bool _AbsoluteMorphTargets; #endregion #region properties /// /// Gets the current morph weights to use for morph target blending. represents the index for the base geometry. /// public SparseWeight8 MorphWeights => _Weights; /// /// Gets a value indicating whether morph target values are absolute, and not relative to the master value. /// public bool AbsoluteMorphTargets => _AbsoluteMorphTargets; #endregion #region API public void Update(SparseWeight8 morphWeights, bool useAbsoluteMorphTargets = false) { _AbsoluteMorphTargets = useAbsoluteMorphTargets; if (morphWeights.IsWeightless) { _Weights = SparseWeight8.Create((COMPLEMENT_INDEX, 1)); return; } _Weights = morphWeights.GetNormalizedWithComplement(COMPLEMENT_INDEX); } protected V3 MorphVectors(V3 value, V3[] morphTargets) { if (_Weights.Index0 == COMPLEMENT_INDEX && _Weights.Weight0 == 1) return value; if (morphTargets == null) return value; var p = V3.Zero; if (_AbsoluteMorphTargets) { foreach (var pair in _Weights.GetNonZeroWeights()) { var val = pair.Item1 == COMPLEMENT_INDEX ? value : morphTargets[pair.Item1]; p += val * pair.Item2; } } else { foreach (var pair in _Weights.GetNonZeroWeights()) { var val = pair.Item1 == COMPLEMENT_INDEX ? value : value + morphTargets[pair.Item1]; p += val * pair.Item2; } } return p; } protected V4 MorphVectors(V4 value, V4[] morphTargets) { if (_Weights.Index0 == COMPLEMENT_INDEX && _Weights.Weight0 == 1) return value; if (morphTargets == null) return value; var p = V4.Zero; if (_AbsoluteMorphTargets) { foreach (var pair in _Weights.GetNonZeroWeights()) { var val = pair.Item1 == COMPLEMENT_INDEX ? value : morphTargets[pair.Item1]; p += val * pair.Item2; } } else { foreach (var pair in _Weights.GetNonZeroWeights()) { var val = pair.Item1 == COMPLEMENT_INDEX ? value : value + morphTargets[pair.Item1]; p += val * pair.Item2; } } return p; } public V4 MorphColors(V4 color, V4[] morphTargets) { return MorphVectors(color, morphTargets); } #endregion } public class StaticTransform : MorphTransform, IGeometryTransform { #region constructor public StaticTransform() { Update(TRANSFORM.Identity); } public StaticTransform(TRANSFORM worldMatrix) { Update(default, false); Update(worldMatrix); } public StaticTransform(TRANSFORM worldMatrix, SparseWeight8 morphWeights, bool useAbsoluteMorphs) { Update(morphWeights, useAbsoluteMorphs); Update(worldMatrix); } #endregion #region data private TRANSFORM _WorldMatrix; private Boolean _Visible; private Boolean _FlipFaces; #endregion #region properties public Boolean Visible => _Visible; public Boolean FlipFaces => _FlipFaces; public Matrix4x4 WorldMatrix => _WorldMatrix; #endregion #region API public void Update(TRANSFORM worldMatrix) { _WorldMatrix = worldMatrix; // http://m-hikari.com/ija/ija-password-2009/ija-password5-8-2009/hajrizajIJA5-8-2009.pdf float determinant3x3 = +(worldMatrix.M13 * worldMatrix.M21 * worldMatrix.M32) + (worldMatrix.M11 * worldMatrix.M22 * worldMatrix.M33) + (worldMatrix.M12 * worldMatrix.M23 * worldMatrix.M31) - (worldMatrix.M12 * worldMatrix.M21 * worldMatrix.M33) - (worldMatrix.M13 * worldMatrix.M22 * worldMatrix.M31) - (worldMatrix.M11 * worldMatrix.M23 * worldMatrix.M32); _Visible = Math.Abs(determinant3x3) > float.Epsilon; _FlipFaces = determinant3x3 < 0; } public V3 TransformPosition(V3 position, V3[] morphTargets, in SparseWeight8 skinWeights) { position = MorphVectors(position, morphTargets); return V3.Transform(position, _WorldMatrix); } public V3 TransformNormal(V3 normal, V3[] morphTargets, in SparseWeight8 skinWeights) { normal = MorphVectors(normal, morphTargets); return V3.Normalize(V3.Transform(normal, _WorldMatrix)); } public V4 TransformTangent(V4 tangent, V3[] morphTargets, in SparseWeight8 skinWeights) { var n = MorphVectors(new V3(tangent.X, tangent.Y, tangent.Z), morphTargets); n = V3.Normalize(V3.Transform(n, _WorldMatrix)); return new V4(n, tangent.W); } #endregion } public class SkinTransform : MorphTransform, IGeometryTransform { #region constructor public SkinTransform() { } public SkinTransform(TRANSFORM[] invBindMatrix, TRANSFORM[] currWorldMatrix, SparseWeight8 morphWeights, bool useAbsoluteMorphTargets) { Update(morphWeights, useAbsoluteMorphTargets); Update(invBindMatrix, currWorldMatrix); } public SkinTransform(int count, Func invBindMatrix, Func currWorldMatrix, SparseWeight8 morphWeights, bool useAbsoluteMorphTargets) { Update(morphWeights, useAbsoluteMorphTargets); Update(count, invBindMatrix, currWorldMatrix); } #endregion #region data private TRANSFORM[] _SkinTransforms; #endregion #region properties /// /// Gets the collection of the current, final matrices to use for skinning /// public IReadOnlyList SkinMatrices => _SkinTransforms; #endregion #region API public void Update(TRANSFORM[] invBindMatrix, TRANSFORM[] currWorldMatrix) { Guard.NotNull(invBindMatrix, nameof(invBindMatrix)); Guard.NotNull(currWorldMatrix, nameof(currWorldMatrix)); Guard.IsTrue(invBindMatrix.Length == currWorldMatrix.Length, nameof(currWorldMatrix), $"{invBindMatrix} and {currWorldMatrix} length mismatch."); if (_SkinTransforms == null || _SkinTransforms.Length != invBindMatrix.Length) _SkinTransforms = new TRANSFORM[invBindMatrix.Length]; for (int i = 0; i < _SkinTransforms.Length; ++i) { _SkinTransforms[i] = invBindMatrix[i] * currWorldMatrix[i]; } } public void Update(int count, Func invBindMatrix, Func currWorldMatrix) { Guard.NotNull(invBindMatrix, nameof(invBindMatrix)); Guard.NotNull(currWorldMatrix, nameof(currWorldMatrix)); if (_SkinTransforms == null || _SkinTransforms.Length != count) _SkinTransforms = new TRANSFORM[count]; for (int i = 0; i < _SkinTransforms.Length; ++i) { _SkinTransforms[i] = invBindMatrix(i) * currWorldMatrix(i); } } public bool Visible => true; public bool FlipFaces => false; public V3 TransformPosition(V3 localPosition, V3[] morphTargets, in SparseWeight8 skinWeights) { Guard.NotNull(skinWeights, nameof(skinWeights)); localPosition = MorphVectors(localPosition, morphTargets); var worldPosition = V3.Zero; var wnrm = 1.0f / skinWeights.WeightSum; foreach (var jw in skinWeights.GetIndexedWeights()) { worldPosition += V3.Transform(localPosition, _SkinTransforms[jw.Item1]) * jw.Item2 * wnrm; } return worldPosition; } public V3 TransformNormal(V3 localNormal, V3[] morphTargets, in SparseWeight8 skinWeights) { Guard.NotNull(skinWeights, nameof(skinWeights)); localNormal = MorphVectors(localNormal, morphTargets); var worldNormal = V3.Zero; foreach (var jw in skinWeights.GetIndexedWeights()) { worldNormal += V3.TransformNormal(localNormal, _SkinTransforms[jw.Item1]) * jw.Item2; } return V3.Normalize(localNormal); } public V4 TransformTangent(V4 localTangent, V3[] morphTargets, in SparseWeight8 skinWeights) { Guard.NotNull(skinWeights, nameof(skinWeights)); var localTangentV = MorphVectors(new V3(localTangent.X, localTangent.Y, localTangent.Z), morphTargets); var worldTangent = V3.Zero; foreach (var jw in skinWeights.GetIndexedWeights()) { worldTangent += V3.TransformNormal(localTangentV, _SkinTransforms[jw.Item1]) * jw.Item2; } worldTangent = V3.Normalize(worldTangent); return new V4(worldTangent, localTangent.W); } #endregion #region helper utilities /// /// Calculates the inverse bind matrix to use for runtime skinning. /// /// The world space of the mesh at the time of binding (POSE). /// The world space of the given bone joint at the time of binding (POSE). /// A representing the inverse bind transform. public static TRANSFORM CalculateInverseBinding(TRANSFORM meshWorldTransform, TRANSFORM jointWorldTransform) { // var xform = meshWorldTransform.Inverse(); // xform = jointWorldTransform * xform; // return xform.Inverse(); var invJoint = jointWorldTransform.Inverse(); return meshWorldTransform * invJoint; } #endregion } }