Skip to content

Commit f404b73

Browse files
authored
Merge pull request #1047 from zeux/nicet
tangentspace: Improved tangent space generation
2 parents 97d4992 + 5b138d8 commit f404b73

5 files changed

Lines changed: 206 additions & 52 deletions

File tree

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,52 @@ Additionally, note that while the code above works with 32-bit OMM indices, afte
796796

797797
When using 4-state OMMs, rasterization code produces both unknown-transparent and unknown-opaque states based on microtriangle coverage; this enables the use of forced 2-state flag during traversal for specific effects where micromap data is sufficient for reasonable quality; this is recommended for performance as this results in no any-hit invocations.
798798

799+
### Tangent spaces
800+
801+
Meshes that use tangent space normal maps often require per-vertex tangent vectors in addition to normals. These could be exported alongside mesh data, but this library also provides an algorithm similar to MikkTSpace that can generate them from positions, normals and texture coordinates:
802+
803+
```c++
804+
std::vector<vec4> tangents(indices.size());
805+
meshopt_generateTangents(&tangents[0].x, &indices[0], indices.size(),
806+
&vertices[0].px, vertices.size(), sizeof(Vertex), &vertices[0].nx, sizeof(Vertex), &vertices[0].tx, sizeof(Vertex));
807+
```
808+
809+
For each triangle *corner* this writes a normalized tangent vector (xyz) and an orientation sign (+-1); the bitangent can be reconstructed in the shader as `cross(normal, tangent.xyz) * tangent.w`. Note that some coordinate space conventions that flip V direction in the texture space require negating orientation sign. The input can be indexed, as in the example above, or not (`indices=NULL`); this does not affect the output tangents.
810+
811+
Because tangents are computed per corner, applying them to mesh vertices requires de-indexing the mesh and generating a new index/vertex buffer afterwards (see `Indexing` section earlier), or using indexed data and copying tangents to existing vertex data while duplicating vertices with different tangents (see `tangents` example in `demo/main.cpp`). With the indexed input, if it contains UV mirroring, vertices along the mirror edge may have different tangent spaces on different sides of the edge and need to be split - copying tangents to existing vertex data without splitting will not produce correct results.
812+
813+
If splitting is required, it can be efficiently done with an extra pass and an index list per vertex:
814+
815+
```c++
816+
// seed each vertex with one of its corner tangents; the loop below fixes any mismatches
817+
for (size_t i = 0; i < indices.size(); ++i)
818+
vertices[indices[i]].tangent = tangents[i];
819+
820+
std::vector<unsigned int> splits(vertices.size(), ~0u);
821+
822+
for (size_t i = 0; i < indices.size(); ++i)
823+
{
824+
// walk the chain of split copies looking for a vertex whose tangent matches
825+
unsigned int v = indices[i];
826+
while (v != ~0u && vertices[v].tangent != tangents[i])
827+
v = splits[v];
828+
829+
// no match in chain: append a new split copy with the target tangent and chain it
830+
if (v == ~0u)
831+
{
832+
v = unsigned(vertices.size());
833+
vertices.push_back(vertices[indices[i]]);
834+
vertices[v].tangent = tangents[i];
835+
splits.push_back(splits[indices[i]]);
836+
splits[indices[i]] = v;
837+
}
838+
839+
indices[i] = v;
840+
}
841+
```
842+
843+
The algorithm uses a MikkTSpace-like construction but by default, uses a modified weighting scheme that significantly improves tangent quality around beveled regions in the mesh. If the normal maps are baked from higher resolution geometry using MikkTSpace weighting, it's possible to produce MikkTSpace-compatible tangents by passing `meshopt_TangentCompatible` option as an extra argument to the function.
844+
799845
## Memory management
800846

801847
Many algorithms allocate temporary memory to store intermediate results or accelerate processing. The amount of memory allocated is a function of various input parameters such as vertex count and index count. By default memory is allocated using `operator new` and `operator delete`; if these operators are overloaded by the application, the overloads will be used instead. Alternatively it's possible to specify custom allocation/deallocation functions using `meshopt_setAllocator`, e.g.
@@ -828,6 +874,7 @@ Currently, the following APIs are experimental:
828874
- `meshopt_encode/decodeMeshlet*` functions (`meshopt_encodeMeshlet`, `meshopt_encodeMeshletBound`, `meshopt_decodeMeshlet`, `meshopt_decodeMeshletRaw`)
829875
- `meshopt_extractMeshletIndices` and `meshopt_optimizeMeshletLevel` functions
830876
- `meshopt_opacityMap*` functions (`meshopt_opacityMapMeasure`, `meshopt_opacityMapRasterize`, `meshopt_opacityMapCompact`, `meshopt_opacityMapEntrySize`)
877+
- `meshopt_generateTangents` function and `meshopt_Tangent*` flags
831878
832879
## License
833880

demo/main.cpp

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,8 +1410,8 @@ void tangents(const Mesh& mesh)
14101410

14111411
std::vector<VertexTangent> newvertices(mesh.vertices.size());
14121412
std::vector<unsigned int> newindices = mesh.indices;
1413-
std::vector<unsigned int> splits(mesh.vertices.size(), ~0u);
14141413

1414+
// copy positions/normals/UVs (Vertex has no tangent field, so we use VertexTangent)
14151415
for (size_t i = 0; i < mesh.vertices.size(); ++i)
14161416
{
14171417
VertexTangent& vt = newvertices[i];
@@ -1420,37 +1420,38 @@ void tangents(const Mesh& mesh)
14201420
vt.px = v.px, vt.py = v.py, vt.pz = v.pz;
14211421
vt.nx = v.nx, vt.ny = v.ny, vt.nz = v.nz;
14221422
vt.tu = v.tx, vt.tv = v.ty;
1423-
// leave vt.txyzw as 0 as we'll fill them below
14241423
}
14251424

1425+
// seed each vertex with one of its corner tangents; the loop below fixes any mismatches
14261426
for (size_t i = 0; i < mesh.indices.size(); ++i)
14271427
{
1428+
VertexTangent& vt = newvertices[mesh.indices[i]];
14281429
const float* t = &tangents[i * 4];
1429-
unsigned int v = mesh.indices[i];
1430+
vt.tx = t[0], vt.ty = t[1], vt.tz = t[2], vt.tw = t[3];
1431+
}
14301432

1431-
// initial tangent value
1432-
if (newvertices[v].tw == 0.f)
1433-
newvertices[v].tx = t[0], newvertices[v].ty = t[1], newvertices[v].tz = t[2], newvertices[v].tw = t[3];
1434-
else if (newvertices[v].tx != t[0] || newvertices[v].ty != t[1] || newvertices[v].tz != t[2] || newvertices[v].tw != t[3])
1435-
{
1436-
// tangent split; we might be able to reuse previously split vertices, or might need to add a new one
1437-
unsigned int sv = splits[v];
1438-
while (sv != ~0u && (newvertices[sv].tx != t[0] || newvertices[sv].ty != t[1] || newvertices[sv].tz != t[2] || newvertices[sv].tw != t[3]))
1439-
sv = splits[sv];
1433+
std::vector<unsigned int> splits(mesh.vertices.size(), ~0u);
14401434

1441-
// need to add a new split and chain it to the existing list
1442-
if (sv == ~0u)
1443-
{
1444-
sv = unsigned(newvertices.size());
1445-
newvertices.push_back(newvertices[v]);
1446-
newvertices[sv].tx = t[0], newvertices[sv].ty = t[1], newvertices[sv].tz = t[2], newvertices[sv].tw = t[3];
1435+
for (size_t i = 0; i < mesh.indices.size(); ++i)
1436+
{
1437+
const float* t = &tangents[i * 4];
1438+
unsigned int v = mesh.indices[i];
14471439

1448-
splits.push_back(splits[v]);
1449-
splits[v] = sv;
1450-
}
1440+
// walk the chain of split copies looking for a vertex whose tangent matches
1441+
while (v != ~0u && (newvertices[v].tx != t[0] || newvertices[v].ty != t[1] || newvertices[v].tz != t[2] || newvertices[v].tw != t[3]))
1442+
v = splits[v];
14511443

1452-
newindices[i] = sv;
1444+
// no match in chain: append a new split copy with the target tangent and chain it
1445+
if (v == ~0u)
1446+
{
1447+
v = unsigned(newvertices.size());
1448+
newvertices.push_back(newvertices[mesh.indices[i]]);
1449+
newvertices[v].tx = t[0], newvertices[v].ty = t[1], newvertices[v].tz = t[2], newvertices[v].tw = t[3];
1450+
splits.push_back(splits[mesh.indices[i]]);
1451+
splits[mesh.indices[i]] = v;
14531452
}
1453+
1454+
newindices[i] = v;
14541455
}
14551456

14561457
double end = timestamp();

demo/tests.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3123,6 +3123,60 @@ static void opacityMapSpecial()
31233123
}
31243124
}
31253125

3126+
static void tangentDegenerate()
3127+
{
3128+
struct Vertex
3129+
{
3130+
float px, py, pz;
3131+
float nx, ny, nz;
3132+
float tx, ty;
3133+
};
3134+
3135+
const Vertex vertices[] = {
3136+
{0, 0, 0, 0, 0, 1, 0, 0},
3137+
{1, 1, 0, 0, 0, 1, 1, 1},
3138+
{2, 2, 0, 0, 0, 1, 2, 2},
3139+
{-1, -2, 1, 0, 0, 1, 0, -2},
3140+
{0, 1, 1, 0, 0, 1, 1, 0},
3141+
{-1, 0, 0, 0, 0, 1, 1, 0},
3142+
};
3143+
3144+
const unsigned int indices[] = {
3145+
0, 3, 1, // outer, positive
3146+
0, 1, 2, // inner, degenerate UVs
3147+
1, 4, 2, // outer, positive
3148+
2, 5, 0, // outer, negative
3149+
};
3150+
3151+
float tangents[12 * 4];
3152+
meshopt_generateTangents(tangents, indices, 12, &vertices[0].px, 6, sizeof(Vertex), &vertices[0].nx, sizeof(Vertex), &vertices[0].tx, sizeof(Vertex), meshopt_TangentCompatible);
3153+
3154+
const float vx = 0.0836f;
3155+
const float vy = 0.9965f;
3156+
const float expected[12][4] = {
3157+
// outer, positive
3158+
{1.f, 0.f, 0.f, 1.f}, // 0
3159+
{1.f, 0.f, 0.f, 1.f}, // 3
3160+
{vx, vy, 0.f, 1.f}, // 1
3161+
// inner, degenerate UVs
3162+
{1.f, 0.f, 0.f, 1.f}, // 0
3163+
{vx, vy, 0.f, 1.f}, // 1
3164+
{0.f, 1.f, 0.f, 1.f}, // 2
3165+
// outer, positive
3166+
{vx, vy, 0.f, 1.f}, // 1
3167+
{0.f, 1.f, 0.f, 1.f}, // 4
3168+
{0.f, 1.f, 0.f, 1.f}, // 2
3169+
// outer, negative
3170+
{-1.f, 0.f, 0.f, -1.f}, // 2 (split)
3171+
{-1.f, 0.f, 0.f, -1.f}, // 5
3172+
{-1.f, 0.f, 0.f, -1.f}, // 0 (split)
3173+
};
3174+
3175+
for (size_t i = 0; i < 12; ++i)
3176+
for (size_t k = 0; k < 4; ++k)
3177+
assert(fabsf(tangents[i * 4 + k] - expected[i][k]) < 1e-3f);
3178+
}
3179+
31263180
void runTests()
31273181
{
31283182
decodeIndexV0();
@@ -3254,4 +3308,6 @@ void runTests()
32543308
opacityMap();
32553309
opacityMapRasterize0();
32563310
opacityMapSpecial();
3311+
3312+
tangentDegenerate();
32573313
}

src/meshoptimizer.h

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -898,9 +898,20 @@ MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_opacityMapEntrySize(int level, int sta
898898
*/
899899
MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_opacityMapCompact(unsigned char* data, size_t data_size, unsigned char* levels, unsigned int* offsets, size_t omm_count, int* omm_indices, size_t triangle_count, int states);
900900

901+
/**
902+
* Tangent generation options
903+
*/
904+
enum
905+
{
906+
/* Produce tangents compatible with MikkTSpace (same weighting and fallbacks) at the cost of reduced quality. Not recommended unless normal maps are baked. */
907+
meshopt_TangentCompatible = 1 << 0,
908+
/* Experimental: For vertices only connected to degenerate triangles, output zero tangents instead of an arbitrary fallback. */
909+
meshopt_TangentZeroFallback = 1 << 1,
910+
};
911+
901912
/**
902913
* Experimental: Tangent space generator
903-
* Computes per-corner tangent vectors following the MikkTSpace algorithm; for each corner, computes normalized tangent vector (xyz) and orientation (w, +/-1).
914+
* Computes per-corner tangent vectors; for each corner, computes normalized tangent vector (xyz) and orientation (w, +/-1).
904915
* Bitangent can be reconstructed via cross(normal, tangent.xyz) * tangent.w.
905916
* To apply tangents to the mesh, either deindex and reindex it with the tangent stream, or copy tangents to existing vertex data while duplicating
906917
* vertices with different tangent vectors (e.g. on UV mirror seams).
@@ -912,7 +923,7 @@ MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_opacityMapCompact(unsigned char* data,
912923
* vertex_normals should have unit float3 normal in the first 12 bytes of each vertex
913924
* vertex_uvs should have float2 texture coordinate in the first 8 bytes of each vertex
914925
*/
915-
MESHOPTIMIZER_EXPERIMENTAL void meshopt_generateTangents(float* result, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_normals, size_t vertex_normals_stride, const float* vertex_uvs, size_t vertex_uvs_stride);
926+
MESHOPTIMIZER_EXPERIMENTAL void meshopt_generateTangents(float* result, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_normals, size_t vertex_normals_stride, const float* vertex_uvs, size_t vertex_uvs_stride, unsigned int options);
916927

917928
/**
918929
* Quantize a float into half-precision (as defined by IEEE-754 fp16) floating point value
@@ -1055,7 +1066,7 @@ inline size_t meshopt_partitionClusters(unsigned int* destination, const T* clus
10551066
template <typename T>
10561067
inline void meshopt_spatialSortTriangles(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
10571068
template <typename T>
1058-
inline void meshopt_generateTangents(float* result, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_normals, size_t vertex_normals_stride, const float* vertex_uvs, size_t vertex_uvs_stride);
1069+
inline void meshopt_generateTangents(float* result, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_normals, size_t vertex_normals_stride, const float* vertex_uvs, size_t vertex_uvs_stride, unsigned int options = 0);
10591070
#endif
10601071

10611072
/* Inline implementation */
@@ -1548,11 +1559,11 @@ inline void meshopt_spatialSortTriangles(T* destination, const T* indices, size_
15481559
}
15491560

15501561
template <typename T>
1551-
inline void meshopt_generateTangents(float* result, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_normals, size_t vertex_normals_stride, const float* vertex_uvs, size_t vertex_uvs_stride)
1562+
inline void meshopt_generateTangents(float* result, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_normals, size_t vertex_normals_stride, const float* vertex_uvs, size_t vertex_uvs_stride, unsigned int options)
15521563
{
15531564
meshopt_IndexAdapter<T> in(NULL, indices, indices ? index_count : 0);
15541565

1555-
meshopt_generateTangents(result, indices ? in.data : NULL, index_count, vertex_positions, vertex_count, vertex_positions_stride, vertex_normals, vertex_normals_stride, vertex_uvs, vertex_uvs_stride);
1566+
meshopt_generateTangents(result, indices ? in.data : NULL, index_count, vertex_positions, vertex_count, vertex_positions_stride, vertex_normals, vertex_normals_stride, vertex_uvs, vertex_uvs_stride, options);
15561567
}
15571568
#endif
15581569

0 commit comments

Comments
 (0)