Dynamic MikkTSpace Tangent Generation for glTF Meshes
Generating MikkTSpace tangents dynamically for glTF meshes requires
implementing Morten Mikkelsen’s standardized tangent space algorithm
directly within a runtime pipeline. When a glTF asset supplies normal
maps but omits the four-component TANGENT vertex attribute,
the host application must derive these tangents on the fly to prevent
visual seams and distorted surface shading. This dynamic process
extracts position, normal, and texture coordinate buffers, computes
triangle-level directional derivatives in texture space, performs
angle-weighted averaging across shared vertices, orthogonalizes the
resulting vectors against vertex normals, and computes a handedness
scalar to produce the final four-component vector required by the glTF
specification.
1. Mesh Buffer Extraction and Pre-Conditioning
The glTF format stores mesh data across disparate accessor buffers. Dynamic generation begins by obtaining three core vertex attributes:
- Positions: 3-component float vectors
(
VEC3). - Normals: 3-component float vectors
(
VEC3). - UV Coordinates: 2-component float vectors
(
VEC2fromTEXCOORD_0). - Indices: Triangle list indices defining primitive topology.
MikkTSpace assumes indexed triangle meshes where vertices are
distinct if their geometric attributes diverge. If positions, normals,
or UV coordinates differ, or if an edge represents a sharp seam, those
vertices must not share calculated tangents. The algorithm operates
either by unwrapping the mesh into an unindexed triangle array or by
maintaining an adjacency map based on combined
(Position, Normal, UV) identity.
2. Triangle-Level Tangent and Bitangent Calculation
For every individual triangle defined by vertices \(P_0, P_1, P_2\) with corresponding UV coordinates \(UV_0, UV_1, UV_2\), edge vectors in object space and parametric texture space are established:
\[\Delta P_1 = P_1 - P_0, \quad \Delta P_2 = P_2 - P_0\] \[\Delta U_1 = U_1 - U_0, \quad \Delta V_1 = V_1 - V_0\] \[\Delta U_2 = U_2 - U_0, \quad \Delta V_2 = V_2 - V_0\]
The relationship between spatial coordinates and texture space coordinates forms a linear system:
\[\begin{bmatrix} \Delta P_1 \\ \Delta P_2 \end{bmatrix} = \begin{bmatrix} \Delta U_1 & \Delta V_1 \\ \Delta U_2 & \Delta V_2 \end{bmatrix} \begin{bmatrix} T \\ B \end{bmatrix}\]
Solving for the unnormalized tangent vector \(T\) and bitangent vector \(B\) yields:
\[r = \frac{1}{\Delta U_1 \Delta V_2 - \Delta U_2 \Delta V_1}\] \[T = (\Delta P_1 \Delta V_2 - \Delta P_2 \Delta V_1) \cdot r\] \[B = (\Delta P_2 \Delta U_1 - \Delta P_1 \Delta U_2) \cdot r\]
If the determinant \((\Delta U_1 \Delta V_2 - \Delta U_2 \Delta V_1)\) is zero or near-zero, the triangle exhibits degenerate mapping. In dynamic implementations, these triangles assign zero vectors to \(T\) and \(B\) to avoid division by zero.
3. Angle-Weighted Neighborhood Accumulation
Unlike basic tangent generation that uses uniform or area-weighted averaging, MikkTSpace dictates an angle-weighted accumulation scheme to avoid bias introduced by tessellation density:
- Vertex Angle Weighting: For each vertex of a triangle, the internal angle \(\alpha\) subtended by the incident edges is evaluated using the dot product: \[\alpha = \arccos\left(\frac{E_1 \cdot E_2}{|E_1| |E_2|}\right)\]
- Grouping Criteria: Triangles contribute their weighted tangents (\(\alpha \cdot T\)) and bitangents (\(\alpha \cdot B\)) to an accumulator mapped to the vertex.
- Discontinuity Detection: Neighboring faces only
merge their tangent spaces if:
- They share identical position, normal, and UV coordinates.
- The angle between their unnormalized face normals does not exceed a smoothing threshold.
- The sign of their determinants (texture orientation) matches. If a texture is mirrored across a seam, the vertices must be split to prevent canceling out tangent contributions.
4. Gram-Schmidt Orthogonalization
Because interpolated surface normals (\(N\)) are not guaranteed to be strictly perpendicular to accumulated surface tangents (\(T_{acc}\)), the tangent must be re-projected onto the tangent plane perpendicular to the normal:
\[T_{\text{ortho}} = T_{acc} - N \cdot (N \cdot T_{acc})\] \[T_{\text{final}} = \text{normalize}(T_{\text{ortho}})\]
If \(T_{\text{ortho}}\) has a length near zero (which occurs when the UV coordinates are degenerately mapped parallel to the normal), an arbitrary vector orthogonal to \(N\) is generated programmatically to maintain pipeline stability.
5. Handedness Evaluation for glTF Compliance
glTF requires tangents to be stored as a VEC4 attribute:
\((T_x, T_y, T_z, w)\). The fourth
component, \(w\), is a sign scalar
(\(+1.0\) or \(-1.0\)) indicating the handedness of the
coordinate basis. This value allows the vertex or fragment shader to
reconstruct the bitangent vector (\(B\)) dynamically:
\[B = (N \times T_{\text{final}}) \cdot w\]
To determine \(w\), the algorithm compares the cross product of the normal and the final tangent with the accumulated bitangent \(B_{acc}\):
\[w = \operatorname{sgn}((N \times T_{\text{final}}) \cdot B_{acc})\]
If the dot product is greater than or equal to zero, \(w = 1.0\); otherwise, \(w = -1.0\).
6. Execution Strategies for Dynamic Generation
Executing this algorithm dynamically can become a bottleneck during runtime glTF loading. High-performance dynamic implementations apply several architectural optimizations:
- Compute Shaders: The algorithm maps efficiently to
GPU compute pipelines. Stage one computes triangle differentials; stage
two groups and accumulates tangents atomically or via pre-sorted spatial
bins; stage three orthogonalizes and packs the
VEC4output. - WebAssembly / SIMD Compilation: For CPU-bound loaders (such as WebGL/WebGPU browser runtimes), the official C reference implementation by Morten Mikkelsen is typically compiled to WebAssembly with SIMD vectorization enabled, operating on flat typed arrays directly allocated in shared memory.
- In-Place Cache Optimization: Reusing vertex index arrays and operating on interleaved attribute buffers limits cache misses when traversing large index lists.