reze-engine 0.63.0 → 0.65.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 (106) hide show
  1. package/dist/engine.d.ts +226 -96
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +1187 -732
  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 +15 -18
  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/model.d.ts +10 -1
  17. package/dist/model.d.ts.map +1 -1
  18. package/dist/model.js +15 -1
  19. package/dist/outline-normals.d.ts +12 -0
  20. package/dist/outline-normals.d.ts.map +1 -0
  21. package/dist/outline-normals.js +100 -0
  22. package/dist/pmx-loader.d.ts +3 -0
  23. package/dist/pmx-loader.d.ts.map +1 -1
  24. package/dist/pmx-loader.js +7 -2
  25. package/dist/shaders/lights.d.ts +3 -2
  26. package/dist/shaders/lights.d.ts.map +1 -1
  27. package/dist/shaders/lights.js +16 -3
  28. package/dist/shaders/materials/common.d.ts.map +1 -1
  29. package/dist/shaders/materials/common.js +65 -56
  30. package/dist/shaders/materials/nodes.d.ts +1 -1
  31. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  32. package/dist/shaders/materials/nodes.js +210 -0
  33. package/dist/shaders/passes/bloom.d.ts +4 -3
  34. package/dist/shaders/passes/bloom.d.ts.map +1 -1
  35. package/dist/shaders/passes/bloom.js +58 -87
  36. package/dist/shaders/passes/composite.d.ts.map +1 -1
  37. package/dist/shaders/passes/composite.js +122 -64
  38. package/dist/shaders/passes/cull.d.ts +1 -1
  39. package/dist/shaders/passes/cull.d.ts.map +1 -1
  40. package/dist/shaders/passes/cull.js +7 -3
  41. package/dist/shaders/passes/ground.d.ts.map +1 -1
  42. package/dist/shaders/passes/ground.js +32 -55
  43. package/dist/shaders/passes/mirror.d.ts +18 -12
  44. package/dist/shaders/passes/mirror.d.ts.map +1 -1
  45. package/dist/shaders/passes/mirror.js +59 -13
  46. package/dist/shaders/passes/outline.d.ts.map +1 -1
  47. package/dist/shaders/passes/outline.js +71 -14
  48. package/dist/shaders/passes/particles.d.ts +1 -1
  49. package/dist/shaders/passes/particles.js +1 -1
  50. package/dist/shaders/scene-light-api.d.ts +18 -10
  51. package/dist/shaders/scene-light-api.d.ts.map +1 -1
  52. package/dist/shaders/scene-light-api.js +58 -46
  53. package/dist/shadow-cascades.d.ts +47 -20
  54. package/dist/shadow-cascades.d.ts.map +1 -1
  55. package/dist/shadow-cascades.js +36 -13
  56. package/dist/unity/character.d.ts +59 -0
  57. package/dist/unity/character.d.ts.map +1 -0
  58. package/dist/unity/character.js +120 -0
  59. package/dist/unity/globals.d.ts +93 -0
  60. package/dist/unity/globals.d.ts.map +1 -0
  61. package/dist/unity/globals.js +340 -0
  62. package/dist/unity/host.d.ts +152 -0
  63. package/dist/unity/host.d.ts.map +1 -0
  64. package/dist/unity/host.js +529 -0
  65. package/dist/unity/looks.d.ts +93 -0
  66. package/dist/unity/looks.d.ts.map +1 -0
  67. package/dist/unity/looks.js +312 -0
  68. package/dist/unity/skin.d.ts +9 -0
  69. package/dist/unity/skin.d.ts.map +1 -0
  70. package/dist/unity/skin.js +120 -0
  71. package/dist/unity/stage.d.ts +155 -0
  72. package/dist/unity/stage.d.ts.map +1 -0
  73. package/dist/unity/stage.js +410 -0
  74. package/dist/unity/state.d.ts +22 -0
  75. package/dist/unity/state.d.ts.map +1 -0
  76. package/dist/unity/state.js +170 -0
  77. package/package.json +1 -1
  78. package/src/engine.ts +1535 -964
  79. package/src/graph/compile.ts +3 -1
  80. package/src/graph/registry.ts +142 -0
  81. package/src/graph/schema.ts +9 -0
  82. package/src/graph/slots.ts +15 -18
  83. package/src/index.ts +12 -0
  84. package/src/model.ts +18 -1
  85. package/src/outline-normals.ts +110 -0
  86. package/src/pmx-loader.ts +8 -2
  87. package/src/shaders/lights.ts +16 -3
  88. package/src/shaders/materials/common.ts +65 -57
  89. package/src/shaders/materials/nodes.ts +210 -0
  90. package/src/shaders/passes/bloom.ts +59 -87
  91. package/src/shaders/passes/composite.ts +122 -64
  92. package/src/shaders/passes/cull.ts +7 -3
  93. package/src/shaders/passes/ground.ts +32 -55
  94. package/src/shaders/passes/mirror.ts +60 -13
  95. package/src/shaders/passes/outline.ts +71 -14
  96. package/src/shaders/passes/particles.ts +1 -1
  97. package/src/shaders/scene-light-api.ts +59 -46
  98. package/src/shadow-cascades.ts +61 -32
  99. package/src/unity/character.ts +169 -0
  100. package/src/unity/globals.ts +443 -0
  101. package/src/unity/host.ts +630 -0
  102. package/src/unity/looks.ts +391 -0
  103. package/src/unity/skin.ts +131 -0
  104. package/src/unity/stage.ts +604 -0
  105. package/src/unity/state.ts +172 -0
  106. package/src/shaders/agx-lut.ts +0 -30
@@ -0,0 +1,391 @@
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
+ /** Per model: its passes' pipelines compiling (see install). */
101
+ private warming = new Map<string, Promise<void>>()
102
+ private skinPipeline: GPUComputePipeline
103
+ private zero: GPUBuffer
104
+ private white: GPUBuffer
105
+ private globals: Record<string, NativeValue> = {}
106
+ private frameTextures: Record<string, GPUTextureView | undefined> = {}
107
+ private scale = 8
108
+
109
+ constructor(
110
+ private device: GPUDevice,
111
+ host: NativeHost,
112
+ ) {
113
+ this.host = host
114
+ this.skinPipeline = device.createComputePipeline({
115
+ label: "native skinning",
116
+ layout: "auto",
117
+ compute: {
118
+ module: device.createShaderModule({
119
+ label: "native skinning",
120
+ code: UNITY_SKIN_WGSL,
121
+ }),
122
+ entryPoint: "main",
123
+ },
124
+ })
125
+ this.zero = device.createBuffer({
126
+ label: "native zero stream",
127
+ size: 64,
128
+ usage: GPUBufferUsage.VERTEX,
129
+ })
130
+ this.white = device.createBuffer({
131
+ label: "native white stream",
132
+ size: 64,
133
+ usage: GPUBufferUsage.VERTEX,
134
+ mappedAtCreation: true,
135
+ })
136
+ new Float32Array(this.white.getMappedRange()).fill(1)
137
+ this.white.unmap()
138
+ }
139
+
140
+ get size(): number {
141
+ return this.installs.size
142
+ }
143
+
144
+ has(model: string): boolean {
145
+ return this.installs.has(model)
146
+ }
147
+
148
+ /** Resolves once every pass of the model's look has its pipeline — until
149
+ * then those draws sit out (NativeHost never compiles at a draw). */
150
+ ready(model: string): Promise<void> {
151
+ return this.warming.get(model) ?? Promise.resolve()
152
+ }
153
+
154
+ /** Is this model's material drawn by its native look? */
155
+ dresses(model: string, material: string): boolean {
156
+ const i = this.installs.get(model)
157
+ return !!i && i.dressed.some((d) => d.draw.materialName === material)
158
+ }
159
+
160
+ install(model: NativeModel, look: NativeLook): void {
161
+ this.remove(model.name)
162
+ const d = this.device
163
+ for (const s of look.shaders) this.host.register(s)
164
+ const views = new Map<string, GPUTextureView>()
165
+ const textures: GPUTexture[] = []
166
+ for (const [key, t] of Object.entries(look.textures)) {
167
+ const tex = d.createTexture({
168
+ label: `${model.name}: ${key}`,
169
+ size: [t.width, t.height],
170
+ format: t.srgb ? "rgba8unorm-srgb" : "rgba8unorm",
171
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
172
+ })
173
+ d.queue.writeTexture({ texture: tex }, t.data as Uint8Array<ArrayBuffer>, { bytesPerRow: t.width * 4 }, [t.width, t.height])
174
+ textures.push(tex)
175
+ views.set(key, tex.createView())
176
+ }
177
+ const n = model.vertices.length / 8
178
+ const buf = (label: string, floats: number, usage: number) =>
179
+ d.createBuffer({
180
+ label: `${model.name}: ${label}`,
181
+ size: Math.max(16, floats * 4),
182
+ usage,
183
+ })
184
+ const out = GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX
185
+ const pos = buf("native position", n * 3, out)
186
+ const nrm = buf("native normal", n * 3, out)
187
+ const tan = buf("native tangent", n * 4, out)
188
+ const restTan = buf("rest tangent", n * 4, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST)
189
+ d.queue.writeBuffer(restTan, 0, tangentsFor(model.vertices, model.indices) as Float32Array<ArrayBuffer>)
190
+ const params = d.createBuffer({
191
+ label: `${model.name}: skin params`,
192
+ size: 16,
193
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
194
+ })
195
+ d.queue.writeBuffer(params, 0, new Uint32Array([n, 0, 0, 0]))
196
+ const skinBind = d.createBindGroup({
197
+ label: `${model.name}: native skinning`,
198
+ layout: this.skinPipeline.getBindGroupLayout(0),
199
+ entries: [
200
+ { binding: 0, resource: { buffer: params } },
201
+ { binding: 1, resource: { buffer: model.vertexBuffer } },
202
+ { binding: 2, resource: { buffer: model.jointsBuffer } },
203
+ { binding: 3, resource: { buffer: model.weightsBuffer } },
204
+ { binding: 4, resource: { buffer: model.skinMatrixBuffer } },
205
+ { binding: 5, resource: { buffer: restTan } },
206
+ { binding: 6, resource: { buffer: pos } },
207
+ { binding: 7, resource: { buffer: nrm } },
208
+ { binding: 8, resource: { buffer: tan } },
209
+ ],
210
+ })
211
+ const dressed: Install["dressed"] = []
212
+ for (const draw of model.draws) {
213
+ const spec = look.materials.find((m) => m.materials.includes(draw.materialName))
214
+ if (spec) dressed.push({ draw, spec })
215
+ }
216
+ const install: Install = {
217
+ model,
218
+ look,
219
+ views,
220
+ textures,
221
+ pos,
222
+ nrm,
223
+ tan,
224
+ restTan,
225
+ params,
226
+ skinBind,
227
+ dressed,
228
+ block: {},
229
+ }
230
+ this.installs.set(model.name, install)
231
+ // Every pass this look will draw, compiled now and side by side — the
232
+ // shader, its state and the streams are all known here.
233
+ const drawn = [...OPAQUE_MODES, ...CHARACTER_MODES, ...OVERRIDE_MODES]
234
+ const jobs: Promise<void>[] = []
235
+ for (const d of dressed)
236
+ for (const p of d.spec.passes) if (drawn.includes(p.lightMode)) jobs.push(this.host.warm(p.shader, p.state, this.streams(install, p.shader)))
237
+ this.warming.set(
238
+ model.name,
239
+ Promise.all(jobs).then(() => {}),
240
+ )
241
+ }
242
+
243
+ remove(name: string): void {
244
+ const i = this.installs.get(name)
245
+ if (!i) return
246
+ this.installs.delete(name)
247
+ this.host.release(`${name}|`)
248
+ const bufs = [i.pos, i.nrm, i.tan, i.restTan, i.params]
249
+ const texs = i.textures
250
+ // Freed once the GPU is done with the frames that may still name them.
251
+ void this.device.queue.onSubmittedWorkDone().then(() => {
252
+ for (const b of bufs) b.destroy()
253
+ for (const t of texs) t.destroy()
254
+ })
255
+ }
256
+
257
+ /**
258
+ * Before the scene pass: skin every dressed model and compute each
259
+ * character's block; the frame's Unity globals come computed (engine.ts).
260
+ */
261
+ prepare(
262
+ encoder: GPUCommandEncoder,
263
+ globals: Record<string, NativeValue>,
264
+ scale: number,
265
+ textures: Record<string, GPUTextureView | undefined>,
266
+ ): void {
267
+ if (!this.installs.size) return
268
+ this.scale = scale
269
+ this.globals = globals
270
+ this.frameTextures = textures
271
+ const pass = encoder.beginComputePass({ label: "native skinning" })
272
+ pass.setPipeline(this.skinPipeline)
273
+ for (const i of this.installs.values()) {
274
+ pass.setBindGroup(0, i.skinBind)
275
+ pass.dispatchWorkgroups(Math.ceil(i.model.vertices.length / 8 / 64))
276
+ i.block = i.look.rig
277
+ ? characterBlock(i.look.rig, i.model.headSkin(), i.model.headRest, scale)
278
+ : {
279
+ unity_RenderingLayer: new Uint32Array([i.model.layers >>> 0, 0, 0, 0]),
280
+ }
281
+ }
282
+ pass.end()
283
+ }
284
+
285
+ private streams(i: Install, shader: string): NativeStream[] {
286
+ return this.host.inputs(shader).map(({ semantic }) => {
287
+ if (semantic === "POSITION")
288
+ return {
289
+ buffer: i.pos,
290
+ offset: 0,
291
+ stride: 12,
292
+ format: "float32x3" as const,
293
+ }
294
+ if (semantic === "NORMAL")
295
+ return {
296
+ buffer: i.nrm,
297
+ offset: 0,
298
+ stride: 12,
299
+ format: "float32x3" as const,
300
+ }
301
+ if (semantic === "TANGENT")
302
+ return {
303
+ buffer: i.tan,
304
+ offset: 0,
305
+ stride: 16,
306
+ format: "float32x4" as const,
307
+ }
308
+ if (semantic === "TEXCOORD0")
309
+ return {
310
+ buffer: i.model.vertexBuffer,
311
+ offset: 24,
312
+ stride: 32,
313
+ format: "float32x2" as const,
314
+ }
315
+ // A missing vertex colour reads white, as Unity binds it; any other
316
+ // missing stream reads zero.
317
+ if (semantic === "COLOR")
318
+ return {
319
+ buffer: this.white,
320
+ offset: 0,
321
+ stride: 0,
322
+ format: "float32x4" as const,
323
+ }
324
+ return {
325
+ buffer: this.zero,
326
+ offset: 0,
327
+ stride: 0,
328
+ format: "float32x4" as const,
329
+ }
330
+ })
331
+ }
332
+
333
+ private drawPass(pass: GPURenderPassEncoder, i: Install, d: Install["dressed"][number], p: NativeMaterialSpec["passes"][number]): void {
334
+ if (i.model.hidden(d.draw.materialName)) return
335
+ const s = this.scale
336
+ const perDraw: ValueSource = {
337
+ // The skinned streams are in engine units; the game's world is 1/scale of them.
338
+ unity_ObjectToWorld: engineToGame(s),
339
+ unity_WorldToObject: gameToEngine(s),
340
+ unity_WorldTransformParams: [0, 0, 0, 1],
341
+ unity_LightData: [0, 0, 0, 0],
342
+ unity_LightIndices: [
343
+ [0, 0, 0, 0],
344
+ [0, 0, 0, 0],
345
+ ],
346
+ unity_LODFade: [1, 1, 0, 0],
347
+ }
348
+ const textures: Record<string, GPUTextureView | undefined> = {
349
+ ...this.frameTextures,
350
+ }
351
+ for (const [slot, key] of Object.entries(d.spec.textures)) textures[slot] = key === "@diffuse" ? d.draw.diffuse : i.views.get(key)
352
+ this.host.draw(pass, {
353
+ shader: p.shader,
354
+ state: p.state,
355
+ streams: this.streams(i, p.shader),
356
+ index: {
357
+ buffer: i.model.indexBuffer,
358
+ format: "uint32",
359
+ offset: d.draw.firstIndex * 4,
360
+ count: d.draw.count,
361
+ },
362
+ sources: [perDraw, i.block, d.spec.values, this.globals],
363
+ textures,
364
+ key: `${i.model.name}|${d.draw.materialName}|${p.lightMode}|${p.shader}`,
365
+ pass: "main",
366
+ })
367
+ }
368
+
369
+ /** Every dressed material whose queue is in range, in queue order. */
370
+ private each(opaque: boolean): { i: Install; d: Install["dressed"][number] }[] {
371
+ const out: { i: Install; d: Install["dressed"][number] }[] = []
372
+ for (const i of this.installs.values()) for (const d of i.dressed) if (d.spec.queue <= 2500 === opaque) out.push({ i, d })
373
+ return out.sort((a, b) => a.d.spec.queue - b.d.spec.queue)
374
+ }
375
+
376
+ /** After the engine's opaque phase: the opaques, then the character passes. */
377
+ drawOpaque(pass: GPURenderPassEncoder): void {
378
+ if (!this.installs.size) return
379
+ const list = this.each(true)
380
+ 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)
381
+ for (const mode of [...CHARACTER_MODES, ...OVERRIDE_MODES])
382
+ for (const { i, d } of list) for (const p of d.spec.passes) if (p.lightMode === mode) this.drawPass(pass, i, d, p)
383
+ }
384
+
385
+ /** In the engine's transparent phase. */
386
+ drawTransparent(pass: GPURenderPassEncoder): void {
387
+ if (!this.installs.size) return
388
+ for (const { i, d } of this.each(false))
389
+ for (const p of d.spec.passes) if (OPAQUE_MODES.includes(p.lightMode)) this.drawPass(pass, i, d, p)
390
+ }
391
+ }
@@ -0,0 +1,131 @@
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
+ // v measured upward, as Unity builds tangent space: a PMX uv runs v down,
82
+ // and taken as it is the bitangent — the handedness in w — comes out
83
+ // reversed, which a game normal map reads as its green channel inverted.
84
+ const du1 = p(b, 6) - p(a, 6),
85
+ dv1 = p(a, 7) - p(b, 7)
86
+ const du2 = p(c, 6) - p(a, 6),
87
+ dv2 = p(a, 7) - p(c, 7)
88
+ const det = du1 * dv2 - du2 * dv1
89
+ if (Math.abs(det) < 1e-12) continue
90
+ const r = 1 / det
91
+ for (const v of [a, b, c]) {
92
+ for (let k = 0; k < 3; k++) {
93
+ tan[v * 3 + k] += (e1[k] * dv2 - e2[k] * dv1) * r
94
+ bit[v * 3 + k] += (e2[k] * du1 - e1[k] * du2) * r
95
+ }
96
+ }
97
+ }
98
+ const out = new Float32Array(n * 4)
99
+ for (let v = 0; v < n; v++) {
100
+ const nx = verts[v * 8 + 3],
101
+ ny = verts[v * 8 + 4],
102
+ nz = verts[v * 8 + 5]
103
+ let tx = tan[v * 3],
104
+ ty = tan[v * 3 + 1],
105
+ tz = tan[v * 3 + 2]
106
+ const d = tx * nx + ty * ny + tz * nz
107
+ tx -= nx * d
108
+ ty -= ny * d
109
+ tz -= nz * d
110
+ let len = Math.hypot(tx, ty, tz)
111
+ if (len < 1e-12) {
112
+ // Any direction perpendicular to the normal: a surface with no uv
113
+ // gradient has no tangent to speak of, and a zero one is NaN downstream.
114
+ const ax = Math.abs(nx) < 0.9 ? 1 : 0
115
+ tx = ax ? 0 : -nz
116
+ ty = ax ? nz : 0
117
+ tz = ax ? -ny : nx
118
+ len = Math.hypot(tx, ty, tz) || 1
119
+ }
120
+ tx /= len
121
+ ty /= len
122
+ tz /= len
123
+ // Handedness: whether the bitangent agrees with n × t.
124
+ const cx = ny * tz - nz * ty,
125
+ cy = nz * tx - nx * tz,
126
+ cz = nx * ty - ny * tx
127
+ const w = cx * bit[v * 3] + cy * bit[v * 3 + 1] + cz * bit[v * 3 + 2] < 0 ? -1 : 1
128
+ out.set([tx, ty, tz, w], v * 4)
129
+ }
130
+ return out
131
+ }