@carbonenginejs/core-math 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tangent.js ADDED
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Packed tangent-frame helpers for CarbonEngineJS/GR2-style mesh data.
3
+ *
4
+ * The packed-frame constants and decode/encode behavior are based on observed
5
+ * Fenris Creations (CCP Games) shader behavior for EVE/Carbon packed tangent
6
+ * frames. No shader source is included here.
7
+ */
8
+
9
+ import {
10
+ generateBiNormals,
11
+ generateNormals,
12
+ generateTangents
13
+ } from "./mesh.js";
14
+ import { num } from "./num.js";
15
+ import { vec3 } from "./vec3.js";
16
+
17
+ /** Full-turn float32 constant used by the CCP tangent-frame shader. */
18
+ export const TANGENT_TAU = 6.28318548;
19
+
20
+ /** Half-turn float32 constant used by the CCP tangent-frame shader. */
21
+ export const TANGENT_PI = 3.14159274;
22
+
23
+ const
24
+ TAU = TANGENT_TAU,
25
+ PI = TANGENT_PI;
26
+
27
+ /**
28
+ * Packed UNorm sentinel used for vertices with no authored tangent frame.
29
+ *
30
+ * @type {number[]}
31
+ */
32
+ export const NULL_TANGENT_UNORM = Object.freeze([ 0, 1, 0, 1 ]);
33
+
34
+ /**
35
+ * Test whether a packed tangent payload is the null-frame sentinel.
36
+ *
37
+ * @param {ArrayLike<number>} u Four UNorm values.
38
+ * @returns {boolean} Whether the payload marks a missing authored frame.
39
+ */
40
+ export function isNullTangent(u)
41
+ {
42
+ const
43
+ e1 = u[1],
44
+ e3 = u[3];
45
+
46
+ return (e1 <= 1e-3 || e1 >= 1 - 1e-3) && (e3 <= 1e-3 || e3 >= 1 - 1e-3);
47
+ }
48
+
49
+ const
50
+ scratchT = new Float64Array(3),
51
+ scratchB = new Float64Array(3),
52
+ scratchN = new Float64Array(3);
53
+
54
+ function decodeTangentFrameInto(u0, u1, u2, u3, outT, outB, outN)
55
+ {
56
+ const
57
+ a0 = u0 * TAU - PI,
58
+ a1 = u1 * TAU - PI,
59
+ a2 = u2 * TAU - PI,
60
+ a3 = u3 * TAU - PI,
61
+ s1 = Math.abs(Math.sin(a1)),
62
+ s3 = Math.abs(Math.sin(a3));
63
+
64
+ outT[0] = s1 * Math.cos(a0);
65
+ outT[1] = s1 * Math.sin(a0);
66
+ outT[2] = Math.cos(a1);
67
+
68
+ outB[0] = s3 * Math.cos(a2);
69
+ outB[1] = s3 * Math.sin(a2);
70
+ outB[2] = Math.cos(a3);
71
+
72
+ const sign = (a1 > 0 && a3 > 0) ? 1 : -1;
73
+ outN[0] = (outT[1] * outB[2] - outT[2] * outB[1]) * sign;
74
+ outN[1] = (outT[2] * outB[0] - outT[0] * outB[2]) * sign;
75
+ outN[2] = (outT[0] * outB[1] - outT[1] * outB[0]) * sign;
76
+
77
+ return s1 < 1e-6 && s3 < 1e-6;
78
+ }
79
+
80
+ /**
81
+ * Decode a packed tangent frame.
82
+ *
83
+ * @param {ArrayLike<number>} u Four UNorm values in `[0, 1]`.
84
+ * @returns {{T: number[], B: number[], N: number[], null: boolean}} Decoded basis.
85
+ */
86
+ export function decodeTangentFrame(u)
87
+ {
88
+ const isNull = decodeTangentFrameInto(u[0], u[1], u[2], u[3], scratchT, scratchB, scratchN);
89
+ return { T: Array.from(scratchT), B: Array.from(scratchB), N: Array.from(scratchN), null: isNull };
90
+ }
91
+
92
+ /**
93
+ * Encode a tangent frame back to four UNorm angles.
94
+ *
95
+ * @param {ArrayLike<number>} T Unit tangent.
96
+ * @param {ArrayLike<number>} B Unit binormal.
97
+ * @param {ArrayLike<number>} [N] Unit normal; only handedness is used.
98
+ * @returns {number[]} Four UNorm values in `[0, 1]`.
99
+ */
100
+ export function encodeTangentFrame(T, B, N)
101
+ {
102
+ let a0 = Math.atan2(T[1], T[0]),
103
+ a1 = Math.acos(num.clamp(T[2], -1, 1));
104
+
105
+ const
106
+ a2 = Math.atan2(B[1], B[0]),
107
+ a3 = Math.acos(num.clamp(B[2], -1, 1));
108
+
109
+ if (N && vec3.dot(N, vec3.cross([ 0, 0, 0 ], T, B)) < 0)
110
+ {
111
+ a1 = -a1;
112
+ }
113
+
114
+ const enc = angle => num.clamp((angle + PI) / TAU, 0, 1);
115
+ return [ enc(a0), enc(a1), enc(a2), enc(a3) ];
116
+ }
117
+
118
+ function vertexCount(mesh)
119
+ {
120
+ const p = mesh.vertex && mesh.vertex.position;
121
+ return p ? (p.length / 3) | 0 : 0;
122
+ }
123
+
124
+ /**
125
+ * Is this shared JSON mesh's tangent frame packed?
126
+ *
127
+ * @param {object} mesh Shared JSON mesh.
128
+ * @returns {boolean} Whether the mesh has packed tangent frames.
129
+ */
130
+ export function isPacked(mesh)
131
+ {
132
+ const v = mesh.vertex;
133
+ if (!v || !v.tangent || !v.tangent.length) return false;
134
+
135
+ const n = vertexCount(mesh);
136
+ if (!n) return false;
137
+
138
+ const
139
+ comps = v.tangent.length / n,
140
+ empty = value => !value || value.length === 0;
141
+
142
+ return comps === 4 && empty(v.normal) && empty(v.binormal);
143
+ }
144
+
145
+ /**
146
+ * Unpack a packed shared JSON mesh in place.
147
+ *
148
+ * @param {object} mesh Shared JSON mesh to mutate.
149
+ * @returns {boolean} Whether unpacking happened.
150
+ */
151
+ export function unpackMeshTangents(mesh)
152
+ {
153
+ if (!isPacked(mesh)) return false;
154
+
155
+ const
156
+ v = mesh.vertex,
157
+ n = vertexCount(mesh),
158
+ src = v.tangent,
159
+ normal = new Array(n * 3),
160
+ tangent = new Array(n * 3),
161
+ binormal = new Array(n * 3);
162
+
163
+ for (let i = 0; i < n; i++)
164
+ {
165
+ const
166
+ s = i * 4,
167
+ o = i * 3,
168
+ isNull = decodeTangentFrameInto(src[s], src[s + 1], src[s + 2], src[s + 3], scratchT, scratchB, scratchN);
169
+
170
+ if (isNull)
171
+ {
172
+ normal[o] = normal[o + 1] = normal[o + 2] = 0;
173
+ tangent[o] = tangent[o + 1] = tangent[o + 2] = 0;
174
+ binormal[o] = binormal[o + 1] = binormal[o + 2] = 0;
175
+ }
176
+ else
177
+ {
178
+ normal[o] = scratchN[0];
179
+ normal[o + 1] = scratchN[1];
180
+ normal[o + 2] = scratchN[2];
181
+ tangent[o] = scratchT[0];
182
+ tangent[o + 1] = scratchT[1];
183
+ tangent[o + 2] = scratchT[2];
184
+ binormal[o] = scratchB[0];
185
+ binormal[o + 1] = scratchB[1];
186
+ binormal[o + 2] = scratchB[2];
187
+ }
188
+ }
189
+
190
+ v.normal = normal;
191
+ v.tangent = tangent;
192
+ v.binormal = binormal;
193
+ return true;
194
+ }
195
+
196
+ /**
197
+ * Pack explicit tangent frames into GR2-style four-component tangent data.
198
+ *
199
+ * @param {ArrayLike<number>} normals Flat xyz normals.
200
+ * @param {ArrayLike<number>} tangents Flat xyz tangents.
201
+ * @param {ArrayLike<number>} binormals Flat xyz binormals.
202
+ * @returns {number[]} Flat xyzw packed tangent-frame values.
203
+ */
204
+ export function packTangentFrames(normals, tangents, binormals)
205
+ {
206
+ if (normals.length !== tangents.length || normals.length !== binormals.length)
207
+ {
208
+ throw new Error("packTangentFrames requires normal, tangent, and binormal channels with matching lengths");
209
+ }
210
+
211
+ const packed = new Array((normals.length / 3) * 4);
212
+ for (let i = 0, o = 0; i < normals.length; i += 3, o += 4)
213
+ {
214
+ const
215
+ normal = vec3.normalize([ 0, 0, 0 ], [ normals[i], normals[i + 1], normals[i + 2] ]),
216
+ tangent = vec3.normalize([ 0, 0, 0 ], [ tangents[i], tangents[i + 1], tangents[i + 2] ]),
217
+ binormal = vec3.normalize([ 0, 0, 0 ], [ binormals[i], binormals[i + 1], binormals[i + 2] ]);
218
+
219
+ if (vec3.length(normal) <= num.EPSILON || vec3.length(tangent) <= num.EPSILON || vec3.length(binormal) <= num.EPSILON)
220
+ {
221
+ packed[o] = NULL_TANGENT_UNORM[0];
222
+ packed[o + 1] = NULL_TANGENT_UNORM[1];
223
+ packed[o + 2] = NULL_TANGENT_UNORM[2];
224
+ packed[o + 3] = NULL_TANGENT_UNORM[3];
225
+ continue;
226
+ }
227
+
228
+ const encoded = encodeTangentFrame(tangent, binormal, normal);
229
+ packed[o] = encoded[0];
230
+ packed[o + 1] = encoded[1];
231
+ packed[o + 2] = encoded[2];
232
+ packed[o + 3] = encoded[3];
233
+ }
234
+
235
+ return packed;
236
+ }
237
+
238
+ export const tangent = Object.freeze({
239
+ TAU: TANGENT_TAU,
240
+ PI: TANGENT_PI,
241
+ NULL_TANGENT_UNORM,
242
+ isNull: isNullTangent,
243
+ isNullTangent,
244
+ decode: decodeTangentFrame,
245
+ decodeTangentFrame,
246
+ pack: encodeTangentFrame,
247
+ encode: encodeTangentFrame,
248
+ encodeTangentFrame,
249
+ packTangentFrames,
250
+ unpack: unpackMeshTangents,
251
+ unpackMeshTangents,
252
+ isPacked,
253
+ generateNormals,
254
+ generateTangents,
255
+ generateBiNormals
256
+ });