topazcube 0.1.31 → 0.1.35

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 (103) hide show
  1. package/LICENSE.txt +0 -0
  2. package/README.md +0 -0
  3. package/dist/Renderer.cjs +20844 -0
  4. package/dist/Renderer.cjs.map +1 -0
  5. package/dist/Renderer.js +20827 -0
  6. package/dist/Renderer.js.map +1 -0
  7. package/dist/client.cjs +91 -260
  8. package/dist/client.cjs.map +1 -1
  9. package/dist/client.js +68 -215
  10. package/dist/client.js.map +1 -1
  11. package/dist/server.cjs +165 -432
  12. package/dist/server.cjs.map +1 -1
  13. package/dist/server.js +117 -370
  14. package/dist/server.js.map +1 -1
  15. package/dist/terminal.cjs +113 -200
  16. package/dist/terminal.cjs.map +1 -1
  17. package/dist/terminal.js +50 -51
  18. package/dist/terminal.js.map +1 -1
  19. package/dist/utils-CRhi1BDa.cjs +259 -0
  20. package/dist/utils-CRhi1BDa.cjs.map +1 -0
  21. package/dist/utils-D7tXt6-2.js +260 -0
  22. package/dist/utils-D7tXt6-2.js.map +1 -0
  23. package/package.json +19 -15
  24. package/src/{client.ts → network/client.js} +170 -403
  25. package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
  26. package/src/{compress-node.ts → network/compress-node.js} +8 -14
  27. package/src/{server.ts → network/server.js} +229 -317
  28. package/src/{terminal.js → network/terminal.js} +0 -0
  29. package/src/{topazcube.ts → network/topazcube.js} +2 -2
  30. package/src/network/utils.js +375 -0
  31. package/src/renderer/Camera.js +191 -0
  32. package/src/renderer/DebugUI.js +703 -0
  33. package/src/renderer/Geometry.js +1049 -0
  34. package/src/renderer/Material.js +64 -0
  35. package/src/renderer/Mesh.js +211 -0
  36. package/src/renderer/Node.js +112 -0
  37. package/src/renderer/Pipeline.js +645 -0
  38. package/src/renderer/Renderer.js +1496 -0
  39. package/src/renderer/Skin.js +792 -0
  40. package/src/renderer/Texture.js +584 -0
  41. package/src/renderer/core/AssetManager.js +394 -0
  42. package/src/renderer/core/CullingSystem.js +308 -0
  43. package/src/renderer/core/EntityManager.js +541 -0
  44. package/src/renderer/core/InstanceManager.js +343 -0
  45. package/src/renderer/core/ParticleEmitter.js +358 -0
  46. package/src/renderer/core/ParticleSystem.js +564 -0
  47. package/src/renderer/core/SpriteSystem.js +349 -0
  48. package/src/renderer/gltf.js +563 -0
  49. package/src/renderer/math.js +161 -0
  50. package/src/renderer/rendering/HistoryBufferManager.js +333 -0
  51. package/src/renderer/rendering/ProbeCapture.js +1495 -0
  52. package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
  53. package/src/renderer/rendering/RenderGraph.js +2258 -0
  54. package/src/renderer/rendering/passes/AOPass.js +308 -0
  55. package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
  56. package/src/renderer/rendering/passes/BasePass.js +101 -0
  57. package/src/renderer/rendering/passes/BloomPass.js +420 -0
  58. package/src/renderer/rendering/passes/CRTPass.js +724 -0
  59. package/src/renderer/rendering/passes/FogPass.js +445 -0
  60. package/src/renderer/rendering/passes/GBufferPass.js +730 -0
  61. package/src/renderer/rendering/passes/HiZPass.js +744 -0
  62. package/src/renderer/rendering/passes/LightingPass.js +753 -0
  63. package/src/renderer/rendering/passes/ParticlePass.js +841 -0
  64. package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
  65. package/src/renderer/rendering/passes/PostProcessPass.js +405 -0
  66. package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
  67. package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
  68. package/src/renderer/rendering/passes/SSGIPass.js +266 -0
  69. package/src/renderer/rendering/passes/SSGITilePass.js +305 -0
  70. package/src/renderer/rendering/passes/ShadowPass.js +2072 -0
  71. package/src/renderer/rendering/passes/TransparentPass.js +831 -0
  72. package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
  73. package/src/renderer/rendering/shaders/ao.wgsl +182 -0
  74. package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
  75. package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
  76. package/src/renderer/rendering/shaders/crt.wgsl +455 -0
  77. package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
  78. package/src/renderer/rendering/shaders/geometry.wgsl +580 -0
  79. package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
  80. package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
  81. package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
  82. package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
  83. package/src/renderer/rendering/shaders/particle_render.wgsl +672 -0
  84. package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
  85. package/src/renderer/rendering/shaders/postproc.wgsl +293 -0
  86. package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
  87. package/src/renderer/rendering/shaders/shadow.wgsl +117 -0
  88. package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
  89. package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
  90. package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
  91. package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
  92. package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
  93. package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
  94. package/src/renderer/utils/BoundingSphere.js +439 -0
  95. package/src/renderer/utils/Frustum.js +281 -0
  96. package/src/renderer/utils/Raycaster.js +761 -0
  97. package/dist/client.d.cts +0 -211
  98. package/dist/client.d.ts +0 -211
  99. package/dist/server.d.cts +0 -120
  100. package/dist/server.d.ts +0 -120
  101. package/dist/terminal.d.cts +0 -64
  102. package/dist/terminal.d.ts +0 -64
  103. package/src/utils.ts +0 -403
@@ -0,0 +1,593 @@
1
+ import { BasePass } from "./BasePass.js"
2
+ import { Texture } from "../../Texture.js"
3
+ import { GBufferPass } from "./GBufferPass.js"
4
+ import { LightingPass } from "./LightingPass.js"
5
+ import { mat4, vec3 } from "../../math.js"
6
+
7
+ /**
8
+ * AmbientCapturePass - Captures 6 directional ambient light samples for sky-aware GI
9
+ *
10
+ * Renders simplified views in 6 directions (up, down, left, right, front, back)
11
+ * to capture sky visibility and distant lighting. This provides ambient lighting
12
+ * that responds to sky visibility (blue tint from sky when outdoors, darker when
13
+ * under a roof).
14
+ *
15
+ * Features:
16
+ * - 64x64 resolution per direction (configurable)
17
+ * - Staggered updates: 2 faces per frame (full cycle in 3 frames)
18
+ * - Direct lights + emissive + skybox only (no IBL on geometry)
19
+ * - Aggressive distance culling (25m default)
20
+ * - Output: 6 average colors applied in RenderPost based on surface normal
21
+ *
22
+ * Settings (from engine.settings.ambientCapture):
23
+ * - enabled: boolean - Enable/disable ambient capture
24
+ * - maxDistance: number - Distance culling (default 25m)
25
+ * - intensity: number - Output intensity multiplier
26
+ * - resolution: number - Capture resolution (default 64)
27
+ */
28
+ class AmbientCapturePass extends BasePass {
29
+ constructor(engine = null) {
30
+ super('AmbientCapture', engine)
31
+
32
+ // Capture settings
33
+ this.captureSize = 64
34
+ this.maxDistance = 25
35
+ this.currentFaceIndex = 0 // cycles 0-5, update 2 per frame
36
+
37
+ // 6 face directions (calculated each frame from camera)
38
+ // Order: up, down, left, right, front, back
39
+ this.directions = [
40
+ [0, 1, 0],
41
+ [0, -1, 0],
42
+ [-1, 0, 0],
43
+ [1, 0, 0],
44
+ [0, 0, 1],
45
+ [0, 0, -1]
46
+ ]
47
+
48
+ // Output: 6 average colors (vec4 each)
49
+ this.faceColors = new Float32Array(6 * 4)
50
+ this.faceColorsBuffer = null // GPU storage buffer
51
+
52
+ // Internal passes
53
+ this.gbufferPass = null
54
+ this.lightingPass = null
55
+
56
+ // Render targets (reused for each face)
57
+ this.captureTexture = null
58
+ this.dummyAO = null
59
+
60
+ // Compute pipeline for reduction
61
+ this.reducePipeline = null
62
+ this.reduceBindGroupLayout = null
63
+ this.reduceBindGroups = [] // One per face
64
+
65
+ // Camera matrices (reused)
66
+ this.faceView = mat4.create()
67
+ this.faceProj = mat4.create()
68
+
69
+ // Textures pending destruction
70
+ this._pendingDestroyRing = [[], [], []]
71
+ this._pendingDestroyIndex = 0
72
+ }
73
+
74
+ async _init() {
75
+ const { device } = this.engine
76
+
77
+ // Get settings
78
+ this.captureSize = this.settings?.ambientCapture?.resolution ?? 64
79
+ this.maxDistance = this.settings?.ambientCapture?.maxDistance ?? 25
80
+
81
+ await this._createResources()
82
+ await this._createComputePipeline()
83
+ }
84
+
85
+ async _createResources() {
86
+ const { device } = this.engine
87
+
88
+ // Create capture render target
89
+ this.captureTexture = await Texture.renderTarget(this.engine, 'rgba16float', this.captureSize, this.captureSize)
90
+ this.captureTexture.label = 'ambientCaptureRT'
91
+
92
+ // Create internal GBuffer pass
93
+ this.gbufferPass = new GBufferPass(this.engine)
94
+ await this.gbufferPass.initialize()
95
+ await this.gbufferPass.resize(this.captureSize, this.captureSize)
96
+
97
+ // Create internal Lighting pass
98
+ this.lightingPass = new LightingPass(this.engine)
99
+ await this.lightingPass.initialize()
100
+ await this.lightingPass.resize(this.captureSize, this.captureSize)
101
+ this.lightingPass.exposureOverride = 1.0
102
+
103
+ // Disable IBL on geometry for ambient capture (skybox still renders as background)
104
+ this.lightingPass.ambientCaptureMode = true
105
+
106
+ // Create dummy AO texture (white = no occlusion)
107
+ const dummyAOTexture = device.createTexture({
108
+ label: 'ambientCaptureAO',
109
+ size: [this.captureSize, this.captureSize],
110
+ format: 'r8unorm',
111
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST
112
+ })
113
+ const whiteData = new Uint8Array(this.captureSize * this.captureSize).fill(255)
114
+ device.queue.writeTexture(
115
+ { texture: dummyAOTexture },
116
+ whiteData,
117
+ { bytesPerRow: this.captureSize },
118
+ { width: this.captureSize, height: this.captureSize }
119
+ )
120
+ this.dummyAO = {
121
+ texture: dummyAOTexture,
122
+ view: dummyAOTexture.createView()
123
+ }
124
+
125
+ // Wire up passes
126
+ await this.lightingPass.setGBuffer(this.gbufferPass.getGBuffer())
127
+ this.lightingPass.setAOTexture(this.dummyAO)
128
+
129
+ // Create output buffer for 6 face colors
130
+ this.faceColorsBuffer = device.createBuffer({
131
+ label: 'ambientCaptureFaceColors',
132
+ size: 6 * 4 * 4, // 6 vec4f
133
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
134
+ })
135
+
136
+ // Initialize with distinct colors per direction for debugging
137
+ // up=cyan, down=brown, left=red, right=green, front=blue, back=yellow
138
+ const initColors = new Float32Array(6 * 4)
139
+ // Up: sky blue
140
+ initColors[0] = 0.4; initColors[1] = 0.6; initColors[2] = 1.0; initColors[3] = 1.0
141
+ // Down: brown/ground
142
+ initColors[4] = 0.3; initColors[5] = 0.2; initColors[6] = 0.1; initColors[7] = 1.0
143
+ // Left (-X): dark
144
+ initColors[8] = 0.2; initColors[9] = 0.2; initColors[10] = 0.2; initColors[11] = 1.0
145
+ // Right (+X): dark
146
+ initColors[12] = 0.2; initColors[13] = 0.2; initColors[14] = 0.2; initColors[15] = 1.0
147
+ // Front (+Z): dark
148
+ initColors[16] = 0.2; initColors[17] = 0.2; initColors[18] = 0.2; initColors[19] = 1.0
149
+ // Back (-Z): dark
150
+ initColors[20] = 0.2; initColors[21] = 0.2; initColors[22] = 0.2; initColors[23] = 1.0
151
+ device.queue.writeBuffer(this.faceColorsBuffer, 0, initColors)
152
+ }
153
+
154
+ async _createComputePipeline() {
155
+ const { device } = this.engine
156
+
157
+ // Bind group layout for reduction compute
158
+ this.reduceBindGroupLayout = device.createBindGroupLayout({
159
+ label: 'ambientReduceBindGroupLayout',
160
+ entries: [
161
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: 'float' } },
162
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
163
+ { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }
164
+ ]
165
+ })
166
+
167
+ // Create compute shader for reduction with temporal smoothing
168
+ const shaderCode = `
169
+ // Reduce a texture to a single average color using parallel reduction
170
+ // Includes temporal smoothing to blend with previous frame's value
171
+ // Workgroup: 8x8 threads, each samples region of the texture
172
+ // Uses shared memory for efficient reduction
173
+
174
+ struct ReduceParams {
175
+ faceIndex: u32,
176
+ textureSize: u32,
177
+ blendFactor: f32, // 0 = keep old, 1 = use new
178
+ emissiveBoost: f32,
179
+ }
180
+
181
+ @group(0) @binding(0) var inputTexture: texture_2d<f32>;
182
+ @group(0) @binding(1) var<storage, read_write> outputColors: array<vec4f>;
183
+ @group(0) @binding(2) var<uniform> params: ReduceParams;
184
+
185
+ var<workgroup> sharedColors: array<vec4f, 64>;
186
+
187
+ @compute @workgroup_size(8, 8, 1)
188
+ fn main(
189
+ @builtin(local_invocation_id) localId: vec3u,
190
+ @builtin(local_invocation_index) localIndex: u32
191
+ ) {
192
+ let faceIndex = params.faceIndex;
193
+ let textureSize = params.textureSize;
194
+ let tilesPerThread = textureSize / 8u;
195
+
196
+ // Each thread samples a region and accumulates
197
+ var sum = vec4f(0.0);
198
+ let baseX = localId.x * tilesPerThread;
199
+ let baseY = localId.y * tilesPerThread;
200
+
201
+ for (var dy = 0u; dy < tilesPerThread; dy++) {
202
+ for (var dx = 0u; dx < tilesPerThread; dx++) {
203
+ let coord = vec2i(i32(baseX + dx), i32(baseY + dy));
204
+ var sample = textureLoad(inputTexture, coord, 0);
205
+
206
+ // Clamp max luminance to prevent sun/bright HDR from dominating
207
+ // This preserves color ratios while limiting brightness
208
+ var lum = max(sample.r, max(sample.g, sample.b));
209
+ let maxLum = 2.0; // Cap brightness (sun can be 100+)
210
+ if (lum > maxLum) {
211
+ sample = sample * (maxLum / lum);
212
+ lum = maxLum;
213
+ }
214
+
215
+ // Gentle boost for emissive after clamping (logarithmic)
216
+ if (lum > 0.5 && params.emissiveBoost > 0.0) {
217
+ let boost = 1.0 + log2(lum + 1.0) * params.emissiveBoost;
218
+ sample = sample * boost;
219
+ }
220
+
221
+ sum += sample;
222
+ }
223
+ }
224
+
225
+ // Average this thread's samples
226
+ let pixelCount = f32(tilesPerThread * tilesPerThread);
227
+ sharedColors[localIndex] = sum / pixelCount;
228
+
229
+ workgroupBarrier();
230
+
231
+ // Parallel reduction: 64 -> 32 -> 16 -> 8 -> 4 -> 2 -> 1
232
+ if (localIndex < 32u) {
233
+ sharedColors[localIndex] += sharedColors[localIndex + 32u];
234
+ }
235
+ workgroupBarrier();
236
+
237
+ if (localIndex < 16u) {
238
+ sharedColors[localIndex] += sharedColors[localIndex + 16u];
239
+ }
240
+ workgroupBarrier();
241
+
242
+ if (localIndex < 8u) {
243
+ sharedColors[localIndex] += sharedColors[localIndex + 8u];
244
+ }
245
+ workgroupBarrier();
246
+
247
+ if (localIndex < 4u) {
248
+ sharedColors[localIndex] += sharedColors[localIndex + 4u];
249
+ }
250
+ workgroupBarrier();
251
+
252
+ if (localIndex < 2u) {
253
+ sharedColors[localIndex] += sharedColors[localIndex + 2u];
254
+ }
255
+ workgroupBarrier();
256
+
257
+ // Final reduction with temporal smoothing
258
+ if (localIndex == 0u) {
259
+ let finalSum = sharedColors[0] + sharedColors[1];
260
+ let newColor = finalSum / 64.0;
261
+
262
+ // Blend with previous value for temporal smoothing
263
+ let oldColor = outputColors[faceIndex];
264
+ let blendFactor = params.blendFactor;
265
+ outputColors[faceIndex] = mix(oldColor, newColor, blendFactor);
266
+ }
267
+ }
268
+ `
269
+
270
+ const shaderModule = device.createShaderModule({
271
+ label: 'ambientReduceShader',
272
+ code: shaderCode
273
+ })
274
+
275
+ this.reducePipeline = await device.createComputePipelineAsync({
276
+ label: 'ambientReducePipeline',
277
+ layout: device.createPipelineLayout({
278
+ bindGroupLayouts: [this.reduceBindGroupLayout]
279
+ }),
280
+ compute: {
281
+ module: shaderModule,
282
+ entryPoint: 'main'
283
+ }
284
+ })
285
+
286
+ // Create uniform buffers for each face (stores faceIndex, textureSize, blendFactor, emissiveBoost)
287
+ this.reduceUniformBuffers = []
288
+ this.reduceUniformData = []
289
+ for (let i = 0; i < 6; i++) {
290
+ const buffer = device.createBuffer({
291
+ label: `ambientReduceParams_${i}`,
292
+ size: 16, // u32 + u32 + f32 + f32
293
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
294
+ })
295
+ // Initialize with default values
296
+ const data = new ArrayBuffer(16)
297
+ const view = new DataView(data)
298
+ view.setUint32(0, i, true) // faceIndex
299
+ view.setUint32(4, this.captureSize, true) // textureSize
300
+ view.setFloat32(8, 1.0, true) // blendFactor (1.0 = instant, for first frame)
301
+ view.setFloat32(12, 2.0, true) // emissiveBoost
302
+ device.queue.writeBuffer(buffer, 0, data)
303
+ this.reduceUniformBuffers.push(buffer)
304
+ this.reduceUniformData.push(data)
305
+ }
306
+
307
+ // Smoothing time in seconds
308
+ this.smoothingTime = 1.0
309
+ this.lastUpdateTime = performance.now() / 1000
310
+
311
+ // Create bind groups for each face
312
+ this._createReduceBindGroups()
313
+ }
314
+
315
+ _createReduceBindGroups() {
316
+ const { device } = this.engine
317
+
318
+ this.reduceBindGroups = []
319
+ const lightingOutput = this.lightingPass.getOutputTexture()
320
+
321
+ for (let i = 0; i < 6; i++) {
322
+ const bindGroup = device.createBindGroup({
323
+ label: `ambientReduceBindGroup_${i}`,
324
+ layout: this.reduceBindGroupLayout,
325
+ entries: [
326
+ { binding: 0, resource: lightingOutput.view },
327
+ { binding: 1, resource: { buffer: this.faceColorsBuffer } },
328
+ { binding: 2, resource: { buffer: this.reduceUniformBuffers[i] } }
329
+ ]
330
+ })
331
+ this.reduceBindGroups.push(bindGroup)
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Set dependencies from main render pipeline
337
+ */
338
+ setDependencies(options) {
339
+ const { environmentMap, encoding, shadowPass, noise, noiseSize } = options
340
+
341
+ if (environmentMap) {
342
+ this.lightingPass.setEnvironmentMap(environmentMap, encoding ?? 0)
343
+ }
344
+ if (shadowPass) {
345
+ this.lightingPass.setShadowPass(shadowPass)
346
+ }
347
+ if (noise) {
348
+ this.gbufferPass.setNoise(noise, noiseSize, false)
349
+ this.lightingPass.setNoise(noise, noiseSize, false)
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Calculate 6 world-space directions for ambient capture
355
+ * Uses fixed world axes so shader can apply using world-space normals
356
+ */
357
+ _calculateDirections(camera) {
358
+ // 6 directions in world space: up, down, left (-X), right (+X), front (+Z), back (-Z)
359
+ // These match the shader's normal-direction mapping
360
+ this.directions = [
361
+ [0, 1, 0], // up (+Y)
362
+ [0, -1, 0], // down (-Y)
363
+ [-1, 0, 0], // left (-X)
364
+ [1, 0, 0], // right (+X)
365
+ [0, 0, 1], // front (+Z)
366
+ [0, 0, -1] // back (-Z)
367
+ ]
368
+ }
369
+
370
+ /**
371
+ * Create a camera looking in a specific direction
372
+ */
373
+ _createFaceCamera(camera, direction, faceIndex) {
374
+ const position = camera.position
375
+
376
+ // Create look-at view matrix
377
+ // Target = position + direction
378
+ const target = [
379
+ position[0] + direction[0],
380
+ position[1] + direction[1],
381
+ position[2] + direction[2]
382
+ ]
383
+
384
+ // Up vector: for up/down faces, use world +Z; otherwise use world up
385
+ let up
386
+ if (faceIndex === 0) {
387
+ // Looking up (+Y): use +Z as up
388
+ up = [0, 0, 1]
389
+ } else if (faceIndex === 1) {
390
+ // Looking down (-Y): use +Z as up
391
+ up = [0, 0, 1]
392
+ } else {
393
+ // Horizontal directions: use world up (+Y)
394
+ up = [0, 1, 0]
395
+ }
396
+
397
+ mat4.lookAt(this.faceView, position, target, up)
398
+
399
+ // 90 degree FOV perspective projection
400
+ const fov = Math.PI / 2 // 90 degrees
401
+ const aspect = 1.0
402
+ const near = 0.1
403
+ const far = this.maxDistance
404
+ mat4.perspective(this.faceProj, fov, aspect, near, far)
405
+
406
+ // Create inverse matrices
407
+ const iView = mat4.create()
408
+ const iProj = mat4.create()
409
+ const viewProj = mat4.create()
410
+ const iViewProj = mat4.create()
411
+ mat4.invert(iView, this.faceView)
412
+ mat4.invert(iProj, this.faceProj)
413
+ mat4.multiply(viewProj, this.faceProj, this.faceView)
414
+ mat4.invert(iViewProj, viewProj)
415
+
416
+ return {
417
+ view: this.faceView,
418
+ proj: this.faceProj,
419
+ iView,
420
+ iProj,
421
+ iViewProj,
422
+ viewProj,
423
+ position,
424
+ near,
425
+ far,
426
+ aspect,
427
+ jitterEnabled: false,
428
+ jitterOffset: [0, 0],
429
+ screenSize: [this.captureSize, this.captureSize],
430
+ forward: direction,
431
+ updateMatrix: () => {},
432
+ updateView: () => {}
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Execute ambient capture pass
438
+ */
439
+ async _execute(context) {
440
+ const { device, stats } = this.engine
441
+ const { camera, meshes, lights, mainLight, dt = 0.016 } = context
442
+
443
+ // Process deferred texture destruction
444
+ this._pendingDestroyIndex = (this._pendingDestroyIndex + 1) % 3
445
+ const toDestroy = this._pendingDestroyRing[this._pendingDestroyIndex]
446
+ for (const tex of toDestroy) {
447
+ tex.destroy()
448
+ }
449
+ this._pendingDestroyRing[this._pendingDestroyIndex] = []
450
+
451
+ // Check if enabled
452
+ if (!this.settings?.ambientCapture?.enabled) {
453
+ return
454
+ }
455
+
456
+ // Update settings
457
+ this.maxDistance = this.settings?.ambientCapture?.maxDistance ?? 25
458
+ this.smoothingTime = this.settings?.ambientCapture?.smoothingTime ?? 1.0
459
+
460
+ // Calculate directions based on camera orientation
461
+ this._calculateDirections(camera)
462
+
463
+ // Copy lights to internal lighting pass
464
+ this.lightingPass.lights = lights
465
+
466
+ // Determine which 2 faces to update this frame
467
+ const facesToUpdate = [
468
+ this.currentFaceIndex,
469
+ (this.currentFaceIndex + 1) % 6
470
+ ]
471
+
472
+ // Render and reduce each face with temporal smoothing
473
+ for (const faceIndex of facesToUpdate) {
474
+ await this._renderFace(context, faceIndex)
475
+ this._reduceFace(faceIndex, dt)
476
+ }
477
+
478
+ // Advance to next pair of faces
479
+ this.currentFaceIndex = (this.currentFaceIndex + 2) % 6
480
+ }
481
+
482
+ async _renderFace(context, faceIndex) {
483
+ const { camera, meshes, lights, mainLight, dt } = context
484
+
485
+ const direction = this.directions[faceIndex]
486
+ const faceCamera = this._createFaceCamera(camera, direction, faceIndex)
487
+
488
+ // Execute GBuffer pass
489
+ await this.gbufferPass.execute({
490
+ camera: faceCamera,
491
+ meshes,
492
+ dt,
493
+ // Custom culling for ambient capture
494
+ cullingOverride: {
495
+ maxDistance: this.maxDistance,
496
+ frustum: true,
497
+ hiZ: false
498
+ }
499
+ })
500
+
501
+ // Execute Lighting pass
502
+ await this.lightingPass.execute({
503
+ camera: faceCamera,
504
+ meshes,
505
+ dt,
506
+ lights,
507
+ mainLight
508
+ })
509
+ }
510
+
511
+ _reduceFace(faceIndex, dt) {
512
+ const { device } = this.engine
513
+
514
+ // Calculate blend factor for temporal smoothing
515
+ // blend = 1 - exp(-dt / smoothingTime) gives exponential smoothing
516
+ // With smoothingTime=1s, after 1s we've blended ~63% of the way to target
517
+ const blendFactor = 1.0 - Math.exp(-dt / this.smoothingTime)
518
+
519
+ // Get emissive boost from settings
520
+ const emissiveBoost = this.settings?.ambientCapture?.emissiveBoost ?? 2.0
521
+
522
+ // Update uniform buffer with blend factor and emissive boost
523
+ const data = this.reduceUniformData[faceIndex]
524
+ const view = new DataView(data)
525
+ view.setFloat32(8, blendFactor, true) // blendFactor
526
+ view.setFloat32(12, emissiveBoost, true) // emissiveBoost
527
+ device.queue.writeBuffer(this.reduceUniformBuffers[faceIndex], 0, data)
528
+
529
+ const commandEncoder = device.createCommandEncoder({
530
+ label: `ambientReduce_face${faceIndex}`
531
+ })
532
+
533
+ const computePass = commandEncoder.beginComputePass({
534
+ label: `ambientReducePass_face${faceIndex}`
535
+ })
536
+
537
+ computePass.setPipeline(this.reducePipeline)
538
+ computePass.setBindGroup(0, this.reduceBindGroups[faceIndex])
539
+ computePass.dispatchWorkgroups(1) // Single workgroup handles entire 64x64 texture
540
+ computePass.end()
541
+
542
+ device.queue.submit([commandEncoder.finish()])
543
+ }
544
+
545
+ /**
546
+ * Get the face colors buffer for RenderPost
547
+ */
548
+ getFaceColorsBuffer() {
549
+ return this.faceColorsBuffer
550
+ }
551
+
552
+ /**
553
+ * Get the 6 directions (for RenderPost to know orientation)
554
+ */
555
+ getDirections() {
556
+ return this.directions
557
+ }
558
+
559
+ async _resize(width, height) {
560
+ // Ambient capture doesn't resize with screen - fixed resolution
561
+ }
562
+
563
+ _destroy() {
564
+ if (this.gbufferPass) {
565
+ this.gbufferPass.destroy()
566
+ this.gbufferPass = null
567
+ }
568
+ if (this.lightingPass) {
569
+ this.lightingPass.destroy()
570
+ this.lightingPass = null
571
+ }
572
+ if (this.dummyAO?.texture) {
573
+ this.dummyAO.texture.destroy()
574
+ this.dummyAO = null
575
+ }
576
+ if (this.faceColorsBuffer) {
577
+ this.faceColorsBuffer.destroy()
578
+ this.faceColorsBuffer = null
579
+ }
580
+ for (const buffer of this.reduceUniformBuffers || []) {
581
+ buffer.destroy()
582
+ }
583
+ this.reduceUniformBuffers = []
584
+ for (const slot of this._pendingDestroyRing) {
585
+ for (const tex of slot) {
586
+ tex.destroy()
587
+ }
588
+ }
589
+ this._pendingDestroyRing = [[], [], []]
590
+ }
591
+ }
592
+
593
+ export { AmbientCapturePass }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * BasePass - Abstract base class for render passes
3
+ *
4
+ * All render passes in the 7-pass pipeline inherit from this.
5
+ */
6
+ class BasePass {
7
+ constructor(name, engine = null) {
8
+ this.name = name
9
+ this.engine = engine
10
+ this.enabled = true
11
+ this._initialized = false
12
+ }
13
+
14
+ // Convenience getter for settings (with fallback for passes without engine)
15
+ get settings() {
16
+ return this.engine?.settings
17
+ }
18
+
19
+ /**
20
+ * Initialize the pass (create pipelines, textures, etc.)
21
+ * Called once before first use.
22
+ * @returns {Promise<void>}
23
+ */
24
+ async initialize() {
25
+ if (this._initialized) return
26
+ await this._init()
27
+ this._initialized = true
28
+ }
29
+
30
+ /**
31
+ * Override in subclass to perform initialization
32
+ * @protected
33
+ */
34
+ async _init() {
35
+ // Override in subclass
36
+ }
37
+
38
+ /**
39
+ * Execute the render pass
40
+ * @param {Object} context - Render context with camera, entities, etc.
41
+ * @returns {Promise<void>}
42
+ */
43
+ async execute(context) {
44
+ if (!this.enabled) return
45
+ if (!this._initialized) {
46
+ await this.initialize()
47
+ }
48
+ await this._execute(context)
49
+ }
50
+
51
+ /**
52
+ * Override in subclass to perform rendering
53
+ * @param {Object} context - Render context
54
+ * @protected
55
+ */
56
+ async _execute(context) {
57
+ // Override in subclass
58
+ }
59
+
60
+ /**
61
+ * Resize pass resources (called on window resize)
62
+ * @param {number} width - New width
63
+ * @param {number} height - New height
64
+ */
65
+ async resize(width, height) {
66
+ await this._resize(width, height)
67
+ }
68
+
69
+ /**
70
+ * Override in subclass to handle resize
71
+ * @protected
72
+ */
73
+ async _resize(width, height) {
74
+ // Override in subclass
75
+ }
76
+
77
+ /**
78
+ * Destroy pass resources
79
+ */
80
+ destroy() {
81
+ this._destroy()
82
+ this._initialized = false
83
+ }
84
+
85
+ /**
86
+ * Override in subclass to clean up resources
87
+ * @protected
88
+ */
89
+ _destroy() {
90
+ // Override in subclass
91
+ }
92
+
93
+ /**
94
+ * Get debug name for profiling
95
+ */
96
+ getDebugName() {
97
+ return `Pass: ${this.name}`
98
+ }
99
+ }
100
+
101
+ export { BasePass }