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,568 @@
1
+ // A game stage drawn by its own shaders: the package ag-rip's stage_native.py
2
+ // writes from a recording of the game's scene — its meshes, textures and
3
+ // materials as the game holds them, every renderer at its place, and the
4
+ // translated shader variants each material's passes use.
5
+ //
6
+ // This is the compare page's renderer (agtools/web/agplay) inside the engine,
7
+ // with one difference that matters: nothing here is recorded per frame. The
8
+ // camera, the clock, the sun and lamps and the cascades are the engine's, turned
9
+ // into the game's globals by globals.ts; the stage brings only what is its own —
10
+ // geometry, materials, and its scene settings (fog, tint, ambient, environment
11
+ // cube), which it hands the engine to set for every native draw.
12
+ //
13
+ // SPACE. Meshes and renderer matrices stay in the game's world, as recorded:
14
+ // the globals carry the engine's camera into it (globals.ts, gameToEngine), so
15
+ // a native draw lands where the game drew it, as the MMD export places it.
16
+ //
17
+ // SHADOWS. Every caster draws its material's own SHADOWCASTER pass into the
18
+ // engine's cascade tiles, as the game drew its own atlas — alpha-tested leaves
19
+ // cast their leaves, not their quads.
20
+
21
+ import { NativeHost, type NativeShader, type NativeStream, type NativeValue, type ValueSource } from "./host"
22
+ import type { PassState } from "./state"
23
+ import { gameToEngine, invert4, mul4, scale4, flipY } from "./globals"
24
+
25
+ export type NativeStageMesh = {
26
+ id: string
27
+ vertexCount: number
28
+ /** Semantic -> [byte offset, components] (float32). */
29
+ streams: Record<string, [number, number]>
30
+ submeshes: { offset: number; count: number; topology?: string }[]
31
+ bounds: { center: [number, number, number]; size: [number, number, number] }
32
+ }
33
+
34
+ export type NativeStageTexture = {
35
+ id: string
36
+ name: string
37
+ width: number
38
+ height: number
39
+ hdr: number
40
+ srgb: number
41
+ cube: number
42
+ wrap?: string
43
+ filter?: string
44
+ mips?: number
45
+ mipsExported?: number
46
+ aniso?: number
47
+ files: string[]
48
+ }
49
+
50
+ export type NativeStageMaterial = {
51
+ id: string
52
+ name: string
53
+ shader: string
54
+ queue: number
55
+ passes: { lightMode: string; shader: string; state: PassState }[]
56
+ values: Record<string, NativeValue>
57
+ textures: Record<string, string>
58
+ /** An empty slot's stand-in, per the shader: white, black, grey, bump. */
59
+ defaults?: Record<string, string>
60
+ }
61
+
62
+ export type NativeStageRenderer = {
63
+ id: string
64
+ path: string
65
+ mesh: string
66
+ materials: (string | null)[]
67
+ /** Object to world, game units, column-major. */
68
+ matrix: number[]
69
+ layers: number
70
+ castShadows: number
71
+ receiveShadows: number
72
+ values: Record<string, NativeValue>
73
+ textures: Record<string, string>
74
+ }
75
+
76
+ export type NativeStageLights = {
77
+ main: {
78
+ direction: [number, number, number]
79
+ color: [number, number, number]
80
+ shadow: number
81
+ }
82
+ additional: {
83
+ position: [number, number, number]
84
+ range: number
85
+ color: [number, number, number]
86
+ type: "point" | "spot"
87
+ aim?: [number, number, number]
88
+ cosOuter?: number
89
+ cosInner?: number
90
+ unity?: {
91
+ spotDir: number[]
92
+ colorW: number
93
+ extra: number[]
94
+ worldToLight: number[] | null
95
+ lightType: number
96
+ simSpotDir?: number[] | null
97
+ simColorW?: number
98
+ }
99
+ }[]
100
+ }
101
+
102
+ /** stage.json, as stage_native.py writes it. */
103
+ export type NativeStagePackage = {
104
+ version: number
105
+ kind: "unity-native-stage"
106
+ name: string
107
+ source?: string
108
+ /** Engine units per game unit. */
109
+ scale: number
110
+ keywords: string[]
111
+ shaders: string[]
112
+ meshes: NativeStageMesh[]
113
+ textures: NativeStageTexture[]
114
+ materials: NativeStageMaterial[]
115
+ renderers: NativeStageRenderer[]
116
+ lights: NativeStageLights
117
+ settings: Record<string, NativeValue>
118
+ globalTextures: Record<string, string>
119
+ }
120
+
121
+ /** Reads one file of the package by its path inside it (stage.json's folder). */
122
+ export type NativeStageReader = (path: string) => Promise<ArrayBuffer>
123
+
124
+ type Tex = { texture: GPUTexture; view: GPUTextureView; sampler: GPUSampler }
125
+
126
+ type Item = {
127
+ renderer: { id: string; castShadows: number }
128
+ material: NativeStageMaterial
129
+ sub: number
130
+ mesh: { info: NativeStageMesh; buffer: GPUBuffer }
131
+ pass: NativeStageMaterial["passes"][number]
132
+ perDraw: ValueSource
133
+ textures: Record<string, GPUTextureView | undefined>
134
+ samplers: Record<string, GPUSampler | undefined>
135
+ /** World-space centre, engine units — for sorting. */
136
+ center: [number, number, number]
137
+ key: string
138
+ }
139
+
140
+ const MAIN_MODES = ["ALWAYS", "FORWARDBASE"]
141
+
142
+ export class NativeStage {
143
+ readonly name: string
144
+ readonly pkg: NativeStagePackage
145
+ /** Engine-unit box around everything that casts, for the cascades. */
146
+ readonly bounds: {
147
+ min: [number, number, number]
148
+ max: [number, number, number]
149
+ }
150
+ private buffers: GPUBuffer[] = []
151
+ private textures: GPUTexture[] = []
152
+ private opaque: Item[] = []
153
+ private transparent: Item[] = []
154
+ private casters: Item[] = []
155
+ private zero: GPUBuffer
156
+ private white: GPUBuffer
157
+
158
+ private constructor(
159
+ private device: GPUDevice,
160
+ pkg: NativeStagePackage,
161
+ ) {
162
+ this.name = pkg.name
163
+ this.pkg = pkg
164
+ this.bounds = {
165
+ min: [Infinity, Infinity, Infinity],
166
+ max: [-Infinity, -Infinity, -Infinity],
167
+ }
168
+ this.zero = device.createBuffer({
169
+ label: "native stage: zero stream",
170
+ size: 64,
171
+ usage: GPUBufferUsage.VERTEX,
172
+ })
173
+ this.white = device.createBuffer({
174
+ label: "native stage: white stream",
175
+ size: 64,
176
+ usage: GPUBufferUsage.VERTEX,
177
+ mappedAtCreation: true,
178
+ })
179
+ new Float32Array(this.white.getMappedRange()).fill(1)
180
+ this.white.unmap()
181
+ this.buffers.push(this.zero, this.white)
182
+ }
183
+
184
+ /**
185
+ * Load a package: every mesh and texture it names through `read`, every
186
+ * shader registered with the hosts (the scene pass's and the shadow atlas's).
187
+ */
188
+ static async load(
189
+ device: GPUDevice,
190
+ pkg: NativeStagePackage,
191
+ read: NativeStageReader,
192
+ hosts: { scene: NativeHost; shadow: NativeHost },
193
+ tools: {
194
+ mipmaps: (t: GPUTexture, levels: number) => void
195
+ fallback: Record<string, GPUTextureView>
196
+ },
197
+ onProgress?: (done: number, total: number) => void,
198
+ ): Promise<NativeStage> {
199
+ if (pkg.kind !== "unity-native-stage") throw new Error("not a native stage package")
200
+ const st = new NativeStage(device, pkg)
201
+ // Progress by weight, not by file: a 2048² picture is most of a second to
202
+ // decode, a shader or a small mesh nothing — so pictures weigh their
203
+ // megapixels, everything else one unit of a quarter of one.
204
+ const texWeight = (t: NativeStageTexture) => Math.max(0.25, ((t.cube ? 6 : 1) * t.width * t.height) / (1 << 20))
205
+ const total = 0.25 * (pkg.shaders.length + pkg.meshes.length) + pkg.textures.reduce((s, t) => s + texWeight(t), 0)
206
+ let done = 0
207
+ const tick = (w = 0.25) => onProgress?.((done += w), total)
208
+ const text = async (p: string) => new TextDecoder().decode(await read(p))
209
+
210
+ const shaders = new Map<string, NativeShader>()
211
+ await Promise.all(
212
+ pkg.shaders.map(async (n) => {
213
+ const [vert, frag, info] = await Promise.all([
214
+ text(`shaders/${n}.vert.wgsl`),
215
+ text(`shaders/${n}.frag.wgsl`),
216
+ text(`shaders/${n}.json`),
217
+ ])
218
+ shaders.set(n, { name: n, vert, frag, info: JSON.parse(info) })
219
+ tick()
220
+ }),
221
+ )
222
+ const casterShaders = new Set<string>()
223
+ for (const m of pkg.materials) for (const p of m.passes) if (p.lightMode === "SHADOWCASTER") casterShaders.add(p.shader)
224
+ for (const [n, s] of shaders) {
225
+ if (casterShaders.has(n)) hosts.shadow.register(s)
226
+ else hosts.scene.register(s)
227
+ }
228
+
229
+ const meshes = new Map<string, { info: NativeStageMesh; buffer: GPUBuffer }>()
230
+ await Promise.all(
231
+ pkg.meshes.map(async (m) => {
232
+ const data = await read(`meshes/${m.id}.bin`)
233
+ const buffer = device.createBuffer({
234
+ label: `native stage mesh ${m.id}`,
235
+ size: Math.max(16, Math.ceil(data.byteLength / 4) * 4),
236
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
237
+ })
238
+ device.queue.writeBuffer(buffer, 0, data, 0, data.byteLength & ~3)
239
+ st.buffers.push(buffer)
240
+ meshes.set(m.id, { info: m, buffer })
241
+ tick()
242
+ }),
243
+ )
244
+
245
+ const textures = new Map<string, Tex>()
246
+ await Promise.all(
247
+ pkg.textures.map(async (t) => {
248
+ try {
249
+ textures.set(t.id, await st.loadTexture(t, read, tools.mipmaps))
250
+ } catch (e) {
251
+ console.warn(`native stage: texture ${t.id} ${t.name}:`, e)
252
+ }
253
+ tick(texWeight(t))
254
+ }),
255
+ )
256
+
257
+ st.build(meshes, textures, tools.fallback)
258
+ return st
259
+ }
260
+
261
+ private async loadTexture(
262
+ t: NativeStageTexture,
263
+ read: NativeStageReader,
264
+ mipmaps: (t: GPUTexture, levels: number) => void,
265
+ ): Promise<Tex> {
266
+ const d = this.device
267
+ const full = Math.max(1, Math.floor(Math.log2(Math.max(t.width, t.height))) + 1)
268
+ const format: GPUTextureFormat = t.hdr ? "rgba16float" : t.srgb ? "rgba8unorm-srgb" : "rgba8unorm"
269
+ if (t.cube) {
270
+ // Every face and mip as recorded: face-major <id>_f<face>_m<mip>.bin, RGBA16F
271
+ // rows along WebGPU's face directions (AGDlcRecord's AGCubeFace).
272
+ const mips = t.mipsExported || 1
273
+ const texture = d.createTexture({
274
+ label: `native stage ${t.name}`,
275
+ size: [t.width, t.height, 6],
276
+ format,
277
+ mipLevelCount: mips,
278
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
279
+ })
280
+ await Promise.all(
281
+ t.files.map(async (file, k) => {
282
+ const face = Math.floor(k / mips)
283
+ const mip = k % mips
284
+ const s = Math.max(1, t.width >> mip)
285
+ const data = await read(`textures/${file}`)
286
+ d.queue.writeTexture({ texture, origin: [0, 0, face], mipLevel: mip }, data, { bytesPerRow: s * 8, rowsPerImage: s }, [s, s, 1])
287
+ }),
288
+ )
289
+ this.textures.push(texture)
290
+ const sampler = d.createSampler({
291
+ magFilter: "linear",
292
+ minFilter: "linear",
293
+ mipmapFilter: "linear",
294
+ })
295
+ return {
296
+ texture,
297
+ view: texture.createView({ dimension: "cube" }),
298
+ sampler,
299
+ }
300
+ }
301
+ const mips = t.hdr ? 1 : full
302
+ const texture = d.createTexture({
303
+ label: `native stage ${t.name}`,
304
+ size: [t.width, t.height],
305
+ format,
306
+ mipLevelCount: mips,
307
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
308
+ })
309
+ const data = await read(`textures/${t.files[0]}`)
310
+ if (t.hdr) {
311
+ // Raw RGBA16F rows as Unity holds them (row 0 is v 0): as they are.
312
+ d.queue.writeTexture({ texture }, data, { bytesPerRow: t.width * 8 }, [t.width, t.height])
313
+ } else {
314
+ // A PNG runs top down; Unity's row 0 is the bottom (v 0): flipped on the way in.
315
+ const bmp = await createImageBitmap(new Blob([data]), {
316
+ imageOrientation: "flipY",
317
+ colorSpaceConversion: "none",
318
+ premultiplyAlpha: "none",
319
+ // A picture swapped into the folder need not be the size it replaced.
320
+ resizeWidth: t.width,
321
+ resizeHeight: t.height,
322
+ resizeQuality: "high",
323
+ })
324
+ d.queue.copyExternalImageToTexture({ source: bmp }, { texture }, [t.width, t.height])
325
+ bmp.close()
326
+ if (mips > 1) mipmaps(texture, mips)
327
+ }
328
+ this.textures.push(texture)
329
+ const wrap: GPUAddressMode =
330
+ (
331
+ {
332
+ Repeat: "repeat",
333
+ Clamp: "clamp-to-edge",
334
+ Mirror: "mirror-repeat",
335
+ MirrorOnce: "mirror-repeat",
336
+ } as Record<string, GPUAddressMode>
337
+ )[t.wrap ?? "Repeat"] ?? "repeat"
338
+ const filter: GPUFilterMode = t.filter === "Point" ? "nearest" : "linear"
339
+ const trilinear = t.filter === "Trilinear"
340
+ const sampler = d.createSampler({
341
+ addressModeU: wrap,
342
+ addressModeV: wrap,
343
+ magFilter: filter,
344
+ minFilter: filter,
345
+ mipmapFilter: trilinear ? "linear" : "nearest",
346
+ maxAnisotropy: filter === "linear" && trilinear ? Math.min(16, Math.max(1, t.aniso || 1)) : 1,
347
+ })
348
+ return { texture, view: texture.createView(), sampler }
349
+ }
350
+
351
+ /** Every renderer's draws, resolved once: the stage does not move. */
352
+ private build(
353
+ meshes: Map<string, { info: NativeStageMesh; buffer: GPUBuffer }>,
354
+ textures: Map<string, Tex>,
355
+ fallback: Record<string, GPUTextureView>,
356
+ ): void {
357
+ const pkg = this.pkg
358
+ const s = pkg.scale
359
+ const mats = new Map(pkg.materials.map((m) => [m.id, m]))
360
+ const globalViews: Record<string, GPUTextureView | undefined> = {}
361
+ const globalSamplers: Record<string, GPUSampler | undefined> = {}
362
+ for (const [slot, id] of Object.entries(pkg.globalTextures)) {
363
+ const t = textures.get(id)
364
+ if (t) {
365
+ globalViews[slot] = t.view
366
+ globalSamplers[slot] = t.sampler
367
+ }
368
+ }
369
+ for (const r of pkg.renderers) {
370
+ const mesh = meshes.get(r.mesh)
371
+ if (!mesh) continue
372
+ const m = r.matrix
373
+ // centre and corners of the mesh box, to engine units
374
+ const b = mesh.info.bounds
375
+ const cx = b.center[0],
376
+ cy = b.center[1],
377
+ cz = b.center[2]
378
+ const center: [number, number, number] = [
379
+ -(m[0] * cx + m[4] * cy + m[8] * cz + m[12]) * s,
380
+ (m[1] * cx + m[5] * cy + m[9] * cz + m[13]) * s,
381
+ -(m[2] * cx + m[6] * cy + m[10] * cz + m[14]) * s,
382
+ ]
383
+ const ex = b.size[0] / 2,
384
+ ey = b.size[1] / 2,
385
+ ez = b.size[2] / 2
386
+ const half = [0, 1, 2].map((i) => (Math.abs(m[i]) * ex + Math.abs(m[4 + i]) * ey + Math.abs(m[8 + i]) * ez) * s)
387
+ const world = Float32Array.from(m)
388
+ const det = m[0] * (m[5] * m[10] - m[9] * m[6]) - m[4] * (m[1] * m[10] - m[9] * m[2]) + m[8] * (m[1] * m[6] - m[5] * m[2])
389
+ const perDraw: ValueSource = {
390
+ unity_ObjectToWorld: world,
391
+ unity_WorldToObject: invert4(world),
392
+ unity_WorldTransformParams: [0, 0, 0, det < 0 ? -1 : 1],
393
+ unity_LightData: [0, 0, 0, 0],
394
+ unity_LightIndices: [
395
+ [0, 0, 0, 0],
396
+ [0, 0, 0, 0],
397
+ ],
398
+ unity_RenderingLayer: new Uint32Array([r.layers >>> 0, 0, 0, 0]),
399
+ unity_LODFade: [1, 1, 0, 0],
400
+ ...r.values,
401
+ }
402
+ this.addItems(r, mesh, perDraw, center, mats, globalViews, globalSamplers, textures, fallback)
403
+ if (r.castShadows) {
404
+ for (let i = 0; i < 3; i++) {
405
+ this.bounds.min[i] = Math.min(this.bounds.min[i], center[i] - half[i])
406
+ this.bounds.max[i] = Math.max(this.bounds.max[i], center[i] + half[i])
407
+ }
408
+ }
409
+ }
410
+ }
411
+
412
+ private addItems(
413
+ r: { id: string; materials: (string | null)[]; castShadows: number; textures: Record<string, string> },
414
+ mesh: { info: NativeStageMesh; buffer: GPUBuffer },
415
+ perDraw: ValueSource,
416
+ center: [number, number, number],
417
+ mats: Map<string, NativeStageMaterial>,
418
+ globalViews: Record<string, GPUTextureView | undefined>,
419
+ globalSamplers: Record<string, GPUSampler | undefined>,
420
+ textures: Map<string, Tex>,
421
+ fallback: Record<string, GPUTextureView>,
422
+ ): void {
423
+ r.materials.forEach((mid, sub) => {
424
+ const mat = mid ? mats.get(mid) : undefined
425
+ if (!mat || sub >= mesh.info.submeshes.length) return
426
+ const views: Record<string, GPUTextureView | undefined> = {
427
+ ...globalViews,
428
+ }
429
+ const samplers: Record<string, GPUSampler | undefined> = {
430
+ ...globalSamplers,
431
+ }
432
+ // an empty slot reads what the shader declared it defaults to
433
+ for (const [slot, def] of Object.entries(mat.defaults ?? {})) views[slot] = fallback[def] ?? fallback.white
434
+ for (const [slot, id] of [...Object.entries(mat.textures), ...Object.entries(r.textures)]) {
435
+ const t = textures.get(id)
436
+ if (!t) continue
437
+ views[slot] = t.view
438
+ samplers[slot] = t.sampler
439
+ }
440
+ for (const pass of mat.passes) {
441
+ const item: Item = {
442
+ renderer: r,
443
+ material: mat,
444
+ sub,
445
+ mesh,
446
+ pass,
447
+ perDraw,
448
+ textures: views,
449
+ samplers,
450
+ center,
451
+ key: `stage|${r.id}|${sub}|${pass.lightMode}|${pass.shader}`,
452
+ }
453
+ if (pass.lightMode === "SHADOWCASTER") {
454
+ if (r.castShadows) this.casters.push(item)
455
+ } else if (MAIN_MODES.includes(pass.lightMode)) {
456
+ ;(mat.queue > 2500 ? this.transparent : this.opaque).push(item)
457
+ }
458
+ }
459
+ })
460
+ }
461
+
462
+ private streams(host: NativeHost, it: Item): NativeStream[] {
463
+ const info = it.mesh.info
464
+ return host.inputs(it.pass.shader).map(({ semantic }) => {
465
+ const st = info.streams[semantic]
466
+ if (!st) {
467
+ // A missing vertex colour reads white, as Unity binds it; anything else zero.
468
+ return {
469
+ buffer: semantic === "COLOR" ? this.white : this.zero,
470
+ offset: 0,
471
+ stride: 0,
472
+ format: "float32x4" as const,
473
+ }
474
+ }
475
+ const n = st[1]
476
+ return {
477
+ buffer: it.mesh.buffer,
478
+ offset: st[0],
479
+ stride: n * 4,
480
+ format: (n === 1 ? "float32" : `float32x${n}`) as GPUVertexFormat,
481
+ }
482
+ })
483
+ }
484
+
485
+ private draw(
486
+ pass: GPURenderPassEncoder,
487
+ host: NativeHost,
488
+ it: Item,
489
+ globals: Record<string, NativeValue>,
490
+ frame: Record<string, GPUTextureView | undefined>,
491
+ keySuffix = "",
492
+ ): void {
493
+ const sm = it.mesh.info.submeshes[it.sub]
494
+ host.draw(pass, {
495
+ shader: it.pass.shader,
496
+ state: it.pass.state,
497
+ streams: this.streams(host, it),
498
+ index: { buffer: it.mesh.buffer, format: "uint32", offset: sm.offset, count: sm.count },
499
+ sources: [it.perDraw, it.material.values, globals],
500
+ textures: { ...it.textures, ...frame },
501
+ samplers: it.samplers,
502
+ key: it.key + keySuffix,
503
+ pass: keySuffix || "main",
504
+ })
505
+ }
506
+
507
+ /** One cascade tile of the engine's atlas: every caster's SHADOWCASTER pass. */
508
+ drawShadow(
509
+ pass: GPURenderPassEncoder,
510
+ host: NativeHost,
511
+ globals: Record<string, NativeValue>,
512
+ cascade: number,
513
+ viewProj: ArrayLike<number>,
514
+ engineScale: number,
515
+ ): void {
516
+ const vp = flipY(mul4(viewProj, gameToEngine(engineScale)))
517
+ // ShadowUtils.GetShadowBias as the game set it: half a texel along the light
518
+ // and half a texel in along the normal, a texel being this tile's width in
519
+ // game units over its resolution (2048).
520
+ const rowLen = Math.hypot(vp[0], vp[4], vp[8])
521
+ const texel = rowLen > 0 ? 2 / rowLen / 2048 : 0
522
+ const g: Record<string, NativeValue> = {
523
+ ...globals,
524
+ unity_MatrixVP: vp,
525
+ unity_MatrixV: scale4(1),
526
+ unity_MatrixP: vp,
527
+ glstate_matrix_projection: vp,
528
+ sim_ShadowBias: [0.5 * texel, -0.5 * texel, 0, 0],
529
+ }
530
+ for (const it of this.casters) this.draw(pass, host, it, g, {}, `|c${cascade}`)
531
+ }
532
+
533
+ /** The opaque queue, front to back within each queue. */
534
+ drawOpaque(
535
+ pass: GPURenderPassEncoder,
536
+ host: NativeHost,
537
+ globals: Record<string, NativeValue>,
538
+ frame: Record<string, GPUTextureView | undefined>,
539
+ eye: [number, number, number],
540
+ ): void {
541
+ const d = (it: Item) => (it.center[0] - eye[0]) ** 2 + (it.center[1] - eye[1]) ** 2 + (it.center[2] - eye[2]) ** 2
542
+ const list = this.opaque.slice().sort((a, b) => a.material.queue - b.material.queue || d(a) - d(b))
543
+ for (const it of list) this.draw(pass, host, it, globals, frame)
544
+ }
545
+
546
+ /** The transparent queue, back to front within each queue. */
547
+ drawTransparent(
548
+ pass: GPURenderPassEncoder,
549
+ host: NativeHost,
550
+ globals: Record<string, NativeValue>,
551
+ frame: Record<string, GPUTextureView | undefined>,
552
+ eye: [number, number, number],
553
+ ): void {
554
+ const d = (it: Item) => (it.center[0] - eye[0]) ** 2 + (it.center[1] - eye[1]) ** 2 + (it.center[2] - eye[2]) ** 2
555
+ const list = this.transparent.slice().sort((a, b) => a.material.queue - b.material.queue || d(b) - d(a))
556
+ for (const it of list) this.draw(pass, host, it, globals, frame)
557
+ }
558
+
559
+ destroy(hosts: NativeHost[]): void {
560
+ for (const h of hosts) h.release("stage|")
561
+ const bufs = this.buffers
562
+ const texs = this.textures
563
+ void this.device.queue.onSubmittedWorkDone().then(() => {
564
+ for (const b of bufs) b.destroy()
565
+ for (const t of texs) t.destroy()
566
+ })
567
+ }
568
+ }