Loading...
Searching...
No Matches
TangentUtils.hpp
1#pragma once
2#include <mikktspace.h>
3
4#include <cmath>
5#include <cstdint>
6#include <memory>
7#include <vector>
8
9namespace Threedim
10{
11
12// Generate glTF-compatible float4 tangents (xyz = unit tangent, w =
13// handedness ±1) using mikktspace from a mesh's position / normal /
14// texcoord_0 streams and an optional uint32 index buffer. Returns a
15// shared buffer of `vertex_count * 4` floats, or nullptr on failure
16// (missing streams, degenerate mesh, etc).
17//
18// For indexed meshes: mikktspace's contract is unindexed ("DO NOT use
19// an already existing index list"), but we're constrained to keep
20// indexed data. We call the mikktspace callbacks against the EXPANDED
21// (unindexed) triangle list via the index buffer, and write the
22// generated tangent back through the same index lookup. When two
23// triangles share a vertex with the same tangent (smooth surface),
24// successive writes produce the same value. At UV seams they disagree
25// and the last write wins — a known small artifact compared to
26// un-indexing the whole mesh.
27inline std::shared_ptr<std::vector<float>> generate_tangents_mikktspace(
28 const std::shared_ptr<std::vector<float>>& positions,
29 const std::shared_ptr<std::vector<float>>& normals,
30 const std::shared_ptr<std::vector<float>>& texcoords,
31 const std::shared_ptr<std::vector<uint32_t>>& indices,
32 uint32_t vertex_count)
33{
34 if(!positions || !normals || !texcoords || vertex_count == 0)
35 return {};
36 if(positions->size() < vertex_count * 3
37 || normals->size() < vertex_count * 3
38 || texcoords->size() < vertex_count * 2)
39 return {};
40
41 // Triangle count: indexed → indices/3, non-indexed → vertex_count/3.
42 const uint32_t num_faces
43 = indices ? uint32_t(indices->size() / 3)
44 : uint32_t(vertex_count / 3);
45 if(num_faces == 0)
46 return {};
47
48 // Index values come straight from the asset file. Reject any that
49 // exceed the vertex streams: the accessor callbacks below read them
50 // unchecked and m_setTSpaceBasic writes through them into the
51 // vertex_count*4 tangent buffer.
52 if(indices)
53 for(const uint32_t v : *indices)
54 if(v >= vertex_count)
55 return {};
56
57 auto tangents = std::make_shared<std::vector<float>>(vertex_count * 4, 0.f);
58
59 struct UserData
60 {
61 const float* positions;
62 const float* normals;
63 const float* texcoords;
64 const uint32_t* indices; // null when un-indexed
65 uint32_t num_faces;
66 std::vector<float>* tangents;
67 };
68 UserData ud{positions->data(),
69 normals->data(),
70 texcoords->data(),
71 indices ? indices->data() : nullptr,
72 num_faces,
73 tangents.get()};
74
75 auto vertexIndex
76 = [](const UserData& u, int iFace, int iVert) -> uint32_t {
77 const uint32_t flat = uint32_t(iFace) * 3u + uint32_t(iVert);
78 return u.indices ? u.indices[flat] : flat;
79 };
80
81 SMikkTSpaceInterface iface{};
82 iface.m_getNumFaces = [](const SMikkTSpaceContext* ctx) {
83 return int(static_cast<const UserData*>(ctx->m_pUserData)->num_faces);
84 };
85 iface.m_getNumVerticesOfFace = [](const SMikkTSpaceContext*, int) {
86 return 3;
87 };
88 iface.m_getPosition = [](const SMikkTSpaceContext* ctx, float out[],
89 int iFace, int iVert) {
90 auto& u = *static_cast<const UserData*>(ctx->m_pUserData);
91 auto vi = uint32_t(iFace) * 3u + uint32_t(iVert);
92 auto v = u.indices ? u.indices[vi] : vi;
93 out[0] = u.positions[v * 3 + 0];
94 out[1] = u.positions[v * 3 + 1];
95 out[2] = u.positions[v * 3 + 2];
96 };
97 iface.m_getNormal = [](const SMikkTSpaceContext* ctx, float out[],
98 int iFace, int iVert) {
99 auto& u = *static_cast<const UserData*>(ctx->m_pUserData);
100 auto vi = uint32_t(iFace) * 3u + uint32_t(iVert);
101 auto v = u.indices ? u.indices[vi] : vi;
102 out[0] = u.normals[v * 3 + 0];
103 out[1] = u.normals[v * 3 + 1];
104 out[2] = u.normals[v * 3 + 2];
105 };
106 iface.m_getTexCoord = [](const SMikkTSpaceContext* ctx, float out[],
107 int iFace, int iVert) {
108 auto& u = *static_cast<const UserData*>(ctx->m_pUserData);
109 auto vi = uint32_t(iFace) * 3u + uint32_t(iVert);
110 auto v = u.indices ? u.indices[vi] : vi;
111 out[0] = u.texcoords[v * 2 + 0];
112 out[1] = u.texcoords[v * 2 + 1];
113 };
114 iface.m_setTSpaceBasic = [](const SMikkTSpaceContext* ctx,
115 const float tangent[], float sign,
116 int iFace, int iVert) {
117 auto& u = *static_cast<const UserData*>(ctx->m_pUserData);
118 auto vi = uint32_t(iFace) * 3u + uint32_t(iVert);
119 auto v = u.indices ? u.indices[vi] : vi;
120 auto& t = *u.tangents;
121 t[v * 4 + 0] = tangent[0];
122 t[v * 4 + 1] = tangent[1];
123 t[v * 4 + 2] = tangent[2];
124 t[v * 4 + 3] = sign;
125 };
126 (void)vertexIndex;
127
128 SMikkTSpaceContext ctx{&iface, &ud};
129 if(!genTangSpaceDefault(&ctx))
130 return {};
131
132 // Fallback for vertices never touched (rare; mostly for non-manifold
133 // meshes): orient any zero tangent along X so shader doesn't divide
134 // by zero when reconstructing the TBN.
135 for(uint32_t v = 0; v < vertex_count; ++v)
136 {
137 float* t = tangents->data() + v * 4;
138 const float len2 = t[0] * t[0] + t[1] * t[1] + t[2] * t[2];
139 if(len2 < 1e-10f)
140 {
141 t[0] = 1.f; t[1] = 0.f; t[2] = 0.f; t[3] = 1.f;
142 }
143 }
144 return tangents;
145}
146
147}