reze-engine 0.62.1 → 0.64.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.
Files changed (83) hide show
  1. package/dist/engine.d.ts +95 -57
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +466 -162
  4. package/dist/graph/compile.d.ts.map +1 -1
  5. package/dist/graph/compile.js +4 -1
  6. package/dist/graph/registry.d.ts +3 -0
  7. package/dist/graph/registry.d.ts.map +1 -1
  8. package/dist/graph/registry.js +133 -0
  9. package/dist/graph/schema.d.ts +12 -0
  10. package/dist/graph/schema.d.ts.map +1 -1
  11. package/dist/graph/slots.d.ts.map +1 -1
  12. package/dist/graph/slots.js +11 -20
  13. package/dist/index.d.ts +9 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/shaders/lights.d.ts +3 -2
  17. package/dist/shaders/lights.d.ts.map +1 -1
  18. package/dist/shaders/lights.js +16 -3
  19. package/dist/shaders/materials/common.d.ts.map +1 -1
  20. package/dist/shaders/materials/common.js +63 -56
  21. package/dist/shaders/materials/nodes.d.ts +1 -1
  22. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  23. package/dist/shaders/materials/nodes.js +210 -0
  24. package/dist/shaders/passes/composite.d.ts.map +1 -1
  25. package/dist/shaders/passes/composite.js +13 -1
  26. package/dist/shaders/passes/cull.d.ts +1 -1
  27. package/dist/shaders/passes/cull.d.ts.map +1 -1
  28. package/dist/shaders/passes/cull.js +7 -3
  29. package/dist/shaders/passes/ground.d.ts.map +1 -1
  30. package/dist/shaders/passes/ground.js +32 -55
  31. package/dist/shaders/passes/particles.d.ts +11 -1
  32. package/dist/shaders/passes/particles.d.ts.map +1 -1
  33. package/dist/shaders/passes/particles.js +9 -4
  34. package/dist/shaders/scene-light-api.d.ts +18 -10
  35. package/dist/shaders/scene-light-api.d.ts.map +1 -1
  36. package/dist/shaders/scene-light-api.js +58 -46
  37. package/dist/shadow-cascades.d.ts +47 -20
  38. package/dist/shadow-cascades.d.ts.map +1 -1
  39. package/dist/shadow-cascades.js +36 -13
  40. package/dist/unity/character.d.ts +59 -0
  41. package/dist/unity/character.d.ts.map +1 -0
  42. package/dist/unity/character.js +120 -0
  43. package/dist/unity/globals.d.ts +93 -0
  44. package/dist/unity/globals.d.ts.map +1 -0
  45. package/dist/unity/globals.js +340 -0
  46. package/dist/unity/host.d.ts +137 -0
  47. package/dist/unity/host.d.ts.map +1 -0
  48. package/dist/unity/host.js +489 -0
  49. package/dist/unity/looks.d.ts +88 -0
  50. package/dist/unity/looks.d.ts.map +1 -0
  51. package/dist/unity/looks.js +295 -0
  52. package/dist/unity/skin.d.ts +9 -0
  53. package/dist/unity/skin.d.ts.map +1 -0
  54. package/dist/unity/skin.js +117 -0
  55. package/dist/unity/stage.d.ts +148 -0
  56. package/dist/unity/stage.d.ts.map +1 -0
  57. package/dist/unity/stage.js +375 -0
  58. package/dist/unity/state.d.ts +22 -0
  59. package/dist/unity/state.d.ts.map +1 -0
  60. package/dist/unity/state.js +170 -0
  61. package/package.json +1 -1
  62. package/src/engine.ts +504 -176
  63. package/src/graph/compile.ts +3 -1
  64. package/src/graph/registry.ts +142 -0
  65. package/src/graph/schema.ts +9 -0
  66. package/src/graph/slots.ts +11 -20
  67. package/src/index.ts +11 -0
  68. package/src/shaders/lights.ts +16 -3
  69. package/src/shaders/materials/common.ts +63 -57
  70. package/src/shaders/materials/nodes.ts +210 -0
  71. package/src/shaders/passes/composite.ts +13 -1
  72. package/src/shaders/passes/cull.ts +7 -3
  73. package/src/shaders/passes/ground.ts +32 -55
  74. package/src/shaders/passes/particles.ts +19 -5
  75. package/src/shaders/scene-light-api.ts +59 -46
  76. package/src/shadow-cascades.ts +61 -32
  77. package/src/unity/character.ts +169 -0
  78. package/src/unity/globals.ts +443 -0
  79. package/src/unity/host.ts +587 -0
  80. package/src/unity/looks.ts +372 -0
  81. package/src/unity/skin.ts +128 -0
  82. package/src/unity/stage.ts +568 -0
  83. package/src/unity/state.ts +172 -0
@@ -0,0 +1,372 @@
1
+ // Native looks: a model dressed in the game's own materials.
2
+ //
3
+ // A look names translated shader variants (host.ts), the shared images they
4
+ // sample (ramps, matcaps), and per material the passes it draws and the values
5
+ // it carries — what a game character's material held, with the model's own
6
+ // texture standing in for its _MainTex. Optionally a CharacterRig, which makes
7
+ // the model a character in the game's sense (character.ts).
8
+ //
9
+ // The frame, as the game's CharacterFeature lays it out after the opaques:
10
+ // 1. every native opaque material's passes (Outline, ForwardBase), in queue
11
+ // order — after the engine's own opaque bundle, so both share one depth;
12
+ // 2. CharHairShadow, CharFaceShadow, CharHairTransE, each over every
13
+ // material that has one (the hair's shadow on the face, by stencil);
14
+ // 3. the eyes' see-through-hair passes, Override1 for all, then 2, then 3;
15
+ // and the transparent queue in the engine's transparent phase. Shadows are the
16
+ // engine's: a native material still casts through its pass.
17
+
18
+ import { NativeHost, type NativeShader, type NativeStream, type NativeValue, type ValueSource } from "./host"
19
+ import type { PassState } from "./state"
20
+ import { UNITY_SKIN_WGSL, tangentsFor } from "./skin"
21
+ import { characterBlock, type CharacterRig } from "./character"
22
+ import { engineToGame, gameToEngine } from "./globals"
23
+
24
+ export type NativeTexture = {
25
+ width: number
26
+ height: number
27
+ /** RGBA8, rows top first. */
28
+ data: Uint8Array
29
+ /** Colour (sRGB) or data (linear) — how the shader expects to read it. */
30
+ srgb: boolean
31
+ }
32
+
33
+ export type NativeMaterialSpec = {
34
+ /** The model's materials this dresses, by name. */
35
+ materials: string[]
36
+ /** Unity render queue: above 2500 draws in the transparent phase. */
37
+ queue: number
38
+ passes: { lightMode: string; shader: string; state: PassState }[]
39
+ /** Material properties, by name (colours linear, as Unity uploads them). */
40
+ values: Record<string, NativeValue>
41
+ /** Texture slots: a key into the look's images, or "@diffuse" for the
42
+ * material's own texture. */
43
+ textures: Record<string, string>
44
+ }
45
+
46
+ export type NativeLook = {
47
+ shaders: NativeShader[]
48
+ textures: Record<string, NativeTexture>
49
+ materials: NativeMaterialSpec[]
50
+ rig?: CharacterRig
51
+ }
52
+
53
+ /** What the engine lends for one model. */
54
+ export type NativeModel = {
55
+ name: string
56
+ vertices: Float32Array
57
+ indices: Uint32Array
58
+ vertexBuffer: GPUBuffer
59
+ jointsBuffer: GPUBuffer
60
+ weightsBuffer: GPUBuffer
61
+ skinMatrixBuffer: GPUBuffer
62
+ indexBuffer: GPUBuffer
63
+ draws: {
64
+ materialName: string
65
+ firstIndex: number
66
+ count: number
67
+ diffuse: GPUTextureView
68
+ }[]
69
+ hidden: (material: string) => boolean
70
+ /** The head bone's current skinning matrix, or null. */
71
+ headSkin: () => ArrayLike<number> | null
72
+ headRest: [number, number, number]
73
+ /** Its rendering-layer bits, for a look without a rig. */
74
+ layers: number
75
+ }
76
+
77
+ type Install = {
78
+ model: NativeModel
79
+ look: NativeLook
80
+ views: Map<string, GPUTextureView>
81
+ textures: GPUTexture[]
82
+ pos: GPUBuffer
83
+ nrm: GPUBuffer
84
+ tan: GPUBuffer
85
+ restTan: GPUBuffer
86
+ params: GPUBuffer
87
+ skinBind: GPUBindGroup
88
+ /** Per draw of the model: the spec that dresses it, if any. */
89
+ dressed: { draw: NativeModel["draws"][number]; spec: NativeMaterialSpec }[]
90
+ block: Record<string, NativeValue>
91
+ }
92
+
93
+ const OPAQUE_MODES = ["ALWAYS", "FORWARDBASE"]
94
+ const CHARACTER_MODES = ["CHARHAIRSHADOW", "CHARFACESHADOW", "CHARHAIRTRANSE"]
95
+ const OVERRIDE_MODES = ["OVERRIDE1", "OVERRIDE2", "OVERRIDE3"]
96
+
97
+ export class NativeLooks {
98
+ readonly host: NativeHost
99
+ private installs = new Map<string, Install>()
100
+ private skinPipeline: GPUComputePipeline
101
+ private zero: GPUBuffer
102
+ private white: GPUBuffer
103
+ private globals: Record<string, NativeValue> = {}
104
+ private frameTextures: Record<string, GPUTextureView | undefined> = {}
105
+ private scale = 8
106
+
107
+ constructor(
108
+ private device: GPUDevice,
109
+ host: NativeHost,
110
+ ) {
111
+ this.host = host
112
+ this.skinPipeline = device.createComputePipeline({
113
+ label: "native skinning",
114
+ layout: "auto",
115
+ compute: {
116
+ module: device.createShaderModule({
117
+ label: "native skinning",
118
+ code: UNITY_SKIN_WGSL,
119
+ }),
120
+ entryPoint: "main",
121
+ },
122
+ })
123
+ this.zero = device.createBuffer({
124
+ label: "native zero stream",
125
+ size: 64,
126
+ usage: GPUBufferUsage.VERTEX,
127
+ })
128
+ this.white = device.createBuffer({
129
+ label: "native white stream",
130
+ size: 64,
131
+ usage: GPUBufferUsage.VERTEX,
132
+ mappedAtCreation: true,
133
+ })
134
+ new Float32Array(this.white.getMappedRange()).fill(1)
135
+ this.white.unmap()
136
+ }
137
+
138
+ get size(): number {
139
+ return this.installs.size
140
+ }
141
+
142
+ has(model: string): boolean {
143
+ return this.installs.has(model)
144
+ }
145
+
146
+ /** Is this model's material drawn by its native look? */
147
+ dresses(model: string, material: string): boolean {
148
+ const i = this.installs.get(model)
149
+ return !!i && i.dressed.some((d) => d.draw.materialName === material)
150
+ }
151
+
152
+ install(model: NativeModel, look: NativeLook): void {
153
+ this.remove(model.name)
154
+ const d = this.device
155
+ for (const s of look.shaders) this.host.register(s)
156
+ const views = new Map<string, GPUTextureView>()
157
+ const textures: GPUTexture[] = []
158
+ for (const [key, t] of Object.entries(look.textures)) {
159
+ const tex = d.createTexture({
160
+ label: `${model.name}: ${key}`,
161
+ size: [t.width, t.height],
162
+ format: t.srgb ? "rgba8unorm-srgb" : "rgba8unorm",
163
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
164
+ })
165
+ d.queue.writeTexture({ texture: tex }, t.data as Uint8Array<ArrayBuffer>, { bytesPerRow: t.width * 4 }, [t.width, t.height])
166
+ textures.push(tex)
167
+ views.set(key, tex.createView())
168
+ }
169
+ const n = model.vertices.length / 8
170
+ const buf = (label: string, floats: number, usage: number) =>
171
+ d.createBuffer({
172
+ label: `${model.name}: ${label}`,
173
+ size: Math.max(16, floats * 4),
174
+ usage,
175
+ })
176
+ const out = GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX
177
+ const pos = buf("native position", n * 3, out)
178
+ const nrm = buf("native normal", n * 3, out)
179
+ const tan = buf("native tangent", n * 4, out)
180
+ const restTan = buf("rest tangent", n * 4, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST)
181
+ d.queue.writeBuffer(restTan, 0, tangentsFor(model.vertices, model.indices) as Float32Array<ArrayBuffer>)
182
+ const params = d.createBuffer({
183
+ label: `${model.name}: skin params`,
184
+ size: 16,
185
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
186
+ })
187
+ d.queue.writeBuffer(params, 0, new Uint32Array([n, 0, 0, 0]))
188
+ const skinBind = d.createBindGroup({
189
+ label: `${model.name}: native skinning`,
190
+ layout: this.skinPipeline.getBindGroupLayout(0),
191
+ entries: [
192
+ { binding: 0, resource: { buffer: params } },
193
+ { binding: 1, resource: { buffer: model.vertexBuffer } },
194
+ { binding: 2, resource: { buffer: model.jointsBuffer } },
195
+ { binding: 3, resource: { buffer: model.weightsBuffer } },
196
+ { binding: 4, resource: { buffer: model.skinMatrixBuffer } },
197
+ { binding: 5, resource: { buffer: restTan } },
198
+ { binding: 6, resource: { buffer: pos } },
199
+ { binding: 7, resource: { buffer: nrm } },
200
+ { binding: 8, resource: { buffer: tan } },
201
+ ],
202
+ })
203
+ const dressed: Install["dressed"] = []
204
+ for (const draw of model.draws) {
205
+ const spec = look.materials.find((m) => m.materials.includes(draw.materialName))
206
+ if (spec) dressed.push({ draw, spec })
207
+ }
208
+ this.installs.set(model.name, {
209
+ model,
210
+ look,
211
+ views,
212
+ textures,
213
+ pos,
214
+ nrm,
215
+ tan,
216
+ restTan,
217
+ params,
218
+ skinBind,
219
+ dressed,
220
+ block: {},
221
+ })
222
+ }
223
+
224
+ remove(name: string): void {
225
+ const i = this.installs.get(name)
226
+ if (!i) return
227
+ this.installs.delete(name)
228
+ this.host.release(`${name}|`)
229
+ const bufs = [i.pos, i.nrm, i.tan, i.restTan, i.params]
230
+ const texs = i.textures
231
+ // Freed once the GPU is done with the frames that may still name them.
232
+ void this.device.queue.onSubmittedWorkDone().then(() => {
233
+ for (const b of bufs) b.destroy()
234
+ for (const t of texs) t.destroy()
235
+ })
236
+ }
237
+
238
+ /**
239
+ * Before the scene pass: skin every dressed model and compute each
240
+ * character's block; the frame's Unity globals come computed (engine.ts).
241
+ */
242
+ prepare(
243
+ encoder: GPUCommandEncoder,
244
+ globals: Record<string, NativeValue>,
245
+ scale: number,
246
+ textures: Record<string, GPUTextureView | undefined>,
247
+ ): void {
248
+ if (!this.installs.size) return
249
+ this.scale = scale
250
+ this.globals = globals
251
+ this.frameTextures = textures
252
+ const pass = encoder.beginComputePass({ label: "native skinning" })
253
+ pass.setPipeline(this.skinPipeline)
254
+ for (const i of this.installs.values()) {
255
+ pass.setBindGroup(0, i.skinBind)
256
+ pass.dispatchWorkgroups(Math.ceil(i.model.vertices.length / 8 / 64))
257
+ i.block = i.look.rig
258
+ ? characterBlock(i.look.rig, i.model.headSkin(), i.model.headRest, scale)
259
+ : {
260
+ unity_RenderingLayer: new Uint32Array([i.model.layers >>> 0, 0, 0, 0]),
261
+ }
262
+ }
263
+ pass.end()
264
+ }
265
+
266
+ private streams(i: Install, shader: string): NativeStream[] {
267
+ return this.host.inputs(shader).map(({ semantic }) => {
268
+ if (semantic === "POSITION")
269
+ return {
270
+ buffer: i.pos,
271
+ offset: 0,
272
+ stride: 12,
273
+ format: "float32x3" as const,
274
+ }
275
+ if (semantic === "NORMAL")
276
+ return {
277
+ buffer: i.nrm,
278
+ offset: 0,
279
+ stride: 12,
280
+ format: "float32x3" as const,
281
+ }
282
+ if (semantic === "TANGENT")
283
+ return {
284
+ buffer: i.tan,
285
+ offset: 0,
286
+ stride: 16,
287
+ format: "float32x4" as const,
288
+ }
289
+ if (semantic === "TEXCOORD0")
290
+ return {
291
+ buffer: i.model.vertexBuffer,
292
+ offset: 24,
293
+ stride: 32,
294
+ format: "float32x2" as const,
295
+ }
296
+ // A missing vertex colour reads white, as Unity binds it; any other
297
+ // missing stream reads zero.
298
+ if (semantic === "COLOR")
299
+ return {
300
+ buffer: this.white,
301
+ offset: 0,
302
+ stride: 0,
303
+ format: "float32x4" as const,
304
+ }
305
+ return {
306
+ buffer: this.zero,
307
+ offset: 0,
308
+ stride: 0,
309
+ format: "float32x4" as const,
310
+ }
311
+ })
312
+ }
313
+
314
+ private drawPass(pass: GPURenderPassEncoder, i: Install, d: Install["dressed"][number], p: NativeMaterialSpec["passes"][number]): void {
315
+ if (i.model.hidden(d.draw.materialName)) return
316
+ const s = this.scale
317
+ const perDraw: ValueSource = {
318
+ // The skinned streams are in engine units; the game's world is 1/scale of them.
319
+ unity_ObjectToWorld: engineToGame(s),
320
+ unity_WorldToObject: gameToEngine(s),
321
+ unity_WorldTransformParams: [0, 0, 0, 1],
322
+ unity_LightData: [0, 0, 0, 0],
323
+ unity_LightIndices: [
324
+ [0, 0, 0, 0],
325
+ [0, 0, 0, 0],
326
+ ],
327
+ unity_LODFade: [1, 1, 0, 0],
328
+ }
329
+ const textures: Record<string, GPUTextureView | undefined> = {
330
+ ...this.frameTextures,
331
+ }
332
+ for (const [slot, key] of Object.entries(d.spec.textures)) textures[slot] = key === "@diffuse" ? d.draw.diffuse : i.views.get(key)
333
+ this.host.draw(pass, {
334
+ shader: p.shader,
335
+ state: p.state,
336
+ streams: this.streams(i, p.shader),
337
+ index: {
338
+ buffer: i.model.indexBuffer,
339
+ format: "uint32",
340
+ offset: d.draw.firstIndex * 4,
341
+ count: d.draw.count,
342
+ },
343
+ sources: [perDraw, i.block, d.spec.values, this.globals],
344
+ textures,
345
+ key: `${i.model.name}|${d.draw.materialName}|${p.lightMode}|${p.shader}`,
346
+ pass: "main",
347
+ })
348
+ }
349
+
350
+ /** Every dressed material whose queue is in range, in queue order. */
351
+ private each(opaque: boolean): { i: Install; d: Install["dressed"][number] }[] {
352
+ const out: { i: Install; d: Install["dressed"][number] }[] = []
353
+ for (const i of this.installs.values()) for (const d of i.dressed) if (d.spec.queue <= 2500 === opaque) out.push({ i, d })
354
+ return out.sort((a, b) => a.d.spec.queue - b.d.spec.queue)
355
+ }
356
+
357
+ /** After the engine's opaque phase: the opaques, then the character passes. */
358
+ drawOpaque(pass: GPURenderPassEncoder): void {
359
+ if (!this.installs.size) return
360
+ const list = this.each(true)
361
+ for (const { i, d } of list) for (const p of d.spec.passes) if (OPAQUE_MODES.includes(p.lightMode)) this.drawPass(pass, i, d, p)
362
+ for (const mode of [...CHARACTER_MODES, ...OVERRIDE_MODES])
363
+ for (const { i, d } of list) for (const p of d.spec.passes) if (p.lightMode === mode) this.drawPass(pass, i, d, p)
364
+ }
365
+
366
+ /** In the engine's transparent phase. */
367
+ drawTransparent(pass: GPURenderPassEncoder): void {
368
+ if (!this.installs.size) return
369
+ for (const { i, d } of this.each(false))
370
+ for (const p of d.spec.passes) if (OPAQUE_MODES.includes(p.lightMode)) this.drawPass(pass, i, d, p)
371
+ }
372
+ }
@@ -0,0 +1,128 @@
1
+ // Skinned vertex streams for native (game-shader) materials.
2
+ //
3
+ // The engine skins in the vertex stage of its own material shaders, from the
4
+ // bone palette (skinMats) and the post-morph vertex buffer. A game shader skins
5
+ // nothing: Unity hands it world-space-ready POSITION / NORMAL / TANGENT streams
6
+ // from its SkinnedMeshRenderer. So a model that wears native materials is
7
+ // skinned once per frame here, by compute, into three plain vertex buffers the
8
+ // game shaders read as Unity's — the same streams the compare page feeds them.
9
+ //
10
+ // Inputs are the engine's own buffers, untouched: the interleaved vertex
11
+ // buffer (position, normal, uv — 8 floats, morphs already applied), the joints
12
+ // (uint16x4, two words a vertex), the weights (unorm8x4, one word) and the
13
+ // skin matrices. Tangents are not in the engine's vertex format; they are
14
+ // derived once at install (tangentsFor) and skinned here with the normal.
15
+
16
+ export const UNITY_SKIN_WGSL = /* wgsl */ `
17
+ struct Params { count: u32, _a: u32, _b: u32, _c: u32 }
18
+ @group(0) @binding(0) var<uniform> P: Params;
19
+ @group(0) @binding(1) var<storage, read> verts: array<f32>; // pos(3) nrm(3) uv(2)
20
+ @group(0) @binding(2) var<storage, read> joints: array<u32>; // uint16x4: two words
21
+ @group(0) @binding(3) var<storage, read> weights: array<u32>; // unorm8x4: one word
22
+ @group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
23
+ @group(0) @binding(5) var<storage, read> tangents: array<f32>; // rest tangent(4)
24
+ @group(0) @binding(6) var<storage, read_write> outPos: array<f32>;
25
+ @group(0) @binding(7) var<storage, read_write> outNrm: array<f32>;
26
+ @group(0) @binding(8) var<storage, read_write> outTan: array<f32>;
27
+
28
+ @compute @workgroup_size(64)
29
+ fn main(@builtin(global_invocation_id) id: vec3u) {
30
+ let v = id.x;
31
+ if (v >= P.count) { return; }
32
+ let p = vec4f(verts[v * 8u], verts[v * 8u + 1u], verts[v * 8u + 2u], 1.0);
33
+ let n = vec3f(verts[v * 8u + 3u], verts[v * 8u + 4u], verts[v * 8u + 5u]);
34
+ let t = vec4f(tangents[v * 4u], tangents[v * 4u + 1u], tangents[v * 4u + 2u], tangents[v * 4u + 3u]);
35
+ let j01 = joints[v * 2u];
36
+ let j23 = joints[v * 2u + 1u];
37
+ let j = vec4u(j01 & 0xffffu, j01 >> 16u, j23 & 0xffffu, j23 >> 16u);
38
+ var w = unpack4x8unorm(weights[v]);
39
+ // Renormalised as the material vertex stage does: PMX ships unnormalised
40
+ // weights on extras, and a sum of zero is the bind pose.
41
+ let s = w.x + w.y + w.z + w.w;
42
+ w = select(vec4f(1.0, 0.0, 0.0, 0.0), w / s, s > 0.0001);
43
+ var sp = vec3f(0.0);
44
+ var sn = vec3f(0.0);
45
+ var st = vec3f(0.0);
46
+ for (var k = 0u; k < 4u; k++) {
47
+ let m = skinMats[j[k]];
48
+ let r = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
49
+ sp += (m * p).xyz * w[k];
50
+ sn += (r * n) * w[k];
51
+ st += (r * t.xyz) * w[k];
52
+ }
53
+ // Degenerate blends (opposing bones at 50/50) cancel to zero; up, not NaN.
54
+ let ln = dot(sn, sn);
55
+ let lt = dot(st, st);
56
+ sn = select(vec3f(0.0, 1.0, 0.0), sn * inverseSqrt(ln), ln > 1e-12);
57
+ st = select(vec3f(1.0, 0.0, 0.0), st * inverseSqrt(lt), lt > 1e-12);
58
+ outPos[v * 3u] = sp.x; outPos[v * 3u + 1u] = sp.y; outPos[v * 3u + 2u] = sp.z;
59
+ outNrm[v * 3u] = sn.x; outNrm[v * 3u + 1u] = sn.y; outNrm[v * 3u + 2u] = sn.z;
60
+ outTan[v * 4u] = st.x; outTan[v * 4u + 1u] = st.y; outTan[v * 4u + 2u] = st.z; outTan[v * 4u + 3u] = t.w;
61
+ }
62
+ `
63
+
64
+ /**
65
+ * Per-vertex tangents (xyz, w = handedness) from positions, normals and uvs,
66
+ * accumulated per triangle and orthogonalised against the normal — the
67
+ * standard construction, which is what a mesh imported without tangents gets
68
+ * in Unity too. `verts` is the engine's interleaved layout (8 floats).
69
+ */
70
+ export function tangentsFor(verts: Float32Array, indices: Uint32Array | Uint16Array): Float32Array {
71
+ const n = verts.length / 8
72
+ const tan = new Float64Array(n * 3)
73
+ const bit = new Float64Array(n * 3)
74
+ for (let i = 0; i + 2 < indices.length; i += 3) {
75
+ const a = indices[i],
76
+ b = indices[i + 1],
77
+ c = indices[i + 2]
78
+ const p = (v: number, k: number) => verts[v * 8 + k]
79
+ const e1 = [p(b, 0) - p(a, 0), p(b, 1) - p(a, 1), p(b, 2) - p(a, 2)]
80
+ const e2 = [p(c, 0) - p(a, 0), p(c, 1) - p(a, 1), p(c, 2) - p(a, 2)]
81
+ const du1 = p(b, 6) - p(a, 6),
82
+ dv1 = p(b, 7) - p(a, 7)
83
+ const du2 = p(c, 6) - p(a, 6),
84
+ dv2 = p(c, 7) - p(a, 7)
85
+ const det = du1 * dv2 - du2 * dv1
86
+ if (Math.abs(det) < 1e-12) continue
87
+ const r = 1 / det
88
+ for (const v of [a, b, c]) {
89
+ for (let k = 0; k < 3; k++) {
90
+ tan[v * 3 + k] += (e1[k] * dv2 - e2[k] * dv1) * r
91
+ bit[v * 3 + k] += (e2[k] * du1 - e1[k] * du2) * r
92
+ }
93
+ }
94
+ }
95
+ const out = new Float32Array(n * 4)
96
+ for (let v = 0; v < n; v++) {
97
+ const nx = verts[v * 8 + 3],
98
+ ny = verts[v * 8 + 4],
99
+ nz = verts[v * 8 + 5]
100
+ let tx = tan[v * 3],
101
+ ty = tan[v * 3 + 1],
102
+ tz = tan[v * 3 + 2]
103
+ const d = tx * nx + ty * ny + tz * nz
104
+ tx -= nx * d
105
+ ty -= ny * d
106
+ tz -= nz * d
107
+ let len = Math.hypot(tx, ty, tz)
108
+ if (len < 1e-12) {
109
+ // Any direction perpendicular to the normal: a surface with no uv
110
+ // gradient has no tangent to speak of, and a zero one is NaN downstream.
111
+ const ax = Math.abs(nx) < 0.9 ? 1 : 0
112
+ tx = ax ? 0 : -nz
113
+ ty = ax ? nz : 0
114
+ tz = ax ? -ny : nx
115
+ len = Math.hypot(tx, ty, tz) || 1
116
+ }
117
+ tx /= len
118
+ ty /= len
119
+ tz /= len
120
+ // Handedness: whether the bitangent agrees with n × t.
121
+ const cx = ny * tz - nz * ty,
122
+ cy = nz * tx - nx * tz,
123
+ cz = nx * ty - ny * tx
124
+ const w = cx * bit[v * 3] + cy * bit[v * 3 + 1] + cz * bit[v * 3 + 2] < 0 ? -1 : 1
125
+ out.set([tx, ty, tz, w], v * 4)
126
+ }
127
+ return out
128
+ }