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,715 @@
1
+ import { BasePass } from "./BasePass.js"
2
+ import { Texture } from "../../Texture.js"
3
+
4
+ import volumetricRaymarchWGSL from "../shaders/volumetric_raymarch.wgsl"
5
+ import volumetricBlurWGSL from "../shaders/volumetric_blur.wgsl"
6
+ import volumetricCompositeWGSL from "../shaders/volumetric_composite.wgsl"
7
+
8
+ /**
9
+ * VolumetricFogPass - Simple ray marching volumetric fog
10
+ *
11
+ * Pipeline:
12
+ * 1. Ray Marching (Fragment) - March rays, sample shadows directly at each step
13
+ * 2. Blur (Fragment) - Gaussian blur to soften edges
14
+ * 3. Composite (Fragment) - Blend fog into scene
15
+ */
16
+ class VolumetricFogPass extends BasePass {
17
+ constructor(engine = null) {
18
+ super('VolumetricFog', engine)
19
+
20
+ // Pipelines
21
+ this.raymarchPipeline = null
22
+ this.blurHPipeline = null
23
+ this.blurVPipeline = null
24
+ this.compositePipeline = null
25
+
26
+ // Bind group layouts
27
+ this.raymarchBGL = null
28
+ this.blurBGL = null
29
+ this.compositeBGL = null
30
+
31
+ // 2D textures
32
+ this.raymarchTexture = null
33
+ this.blurTempTexture = null
34
+ this.blurredTexture = null
35
+ this.outputTexture = null
36
+
37
+ // Uniform buffers
38
+ this.raymarchUniformBuffer = null
39
+ this.blurHUniformBuffer = null
40
+ this.blurVUniformBuffer = null
41
+ this.compositeUniformBuffer = null
42
+
43
+ // Samplers
44
+ this.linearSampler = null
45
+
46
+ // External dependencies
47
+ this.inputTexture = null
48
+ this.gbuffer = null
49
+ this.shadowPass = null
50
+ this.lightingPass = null
51
+
52
+ // Dimensions
53
+ this.canvasWidth = 0
54
+ this.canvasHeight = 0
55
+ this.renderWidth = 0
56
+ this.renderHeight = 0
57
+
58
+ // Adaptive scatter state
59
+ this._currentMainLightScatter = null // Will be initialized on first execute
60
+ this._lastUpdateTime = 0
61
+ this._cameraInShadowSmooth = 0.0 // 0 = in light, 1 = in shadow
62
+
63
+ // Sky detection state (for adaptive scatter)
64
+ this._skyVisible = true // Assume sky visible until proven otherwise
65
+ this._skyCheckPending = false
66
+ this._lastSkyCheckTime = 0
67
+ }
68
+
69
+ // Settings getters
70
+ get volumetricSettings() { return this.settings?.volumetricFog ?? {} }
71
+ get fogSettings() { return this.settings?.fog ?? {} }
72
+ get isVolumetricEnabled() { return this.volumetricSettings.enabled ?? false }
73
+ get resolution() { return this.volumetricSettings.resolution ?? 0.25 }
74
+ get maxSamples() { return this.volumetricSettings.maxSamples ?? 32 }
75
+ get blurRadius() { return this.volumetricSettings.blurRadius ?? 4.0 }
76
+ get fogDensity() { return this.volumetricSettings.density ?? this.volumetricSettings.densityMultiplier ?? 0.5 }
77
+ get scatterStrength() { return this.volumetricSettings.scatterStrength ?? 1.0 }
78
+ get maxDistance() { return this.volumetricSettings.maxDistance ?? 20.0 }
79
+ get heightRange() { return this.volumetricSettings.heightRange ?? [-5, 20] }
80
+ get shadowsEnabled() { return this.volumetricSettings.shadowsEnabled ?? true }
81
+ get noiseStrength() { return this.volumetricSettings.noiseStrength ?? 1.0 } // 0 = uniform fog, 1 = full noise
82
+ get noiseAnimated() { return this.volumetricSettings.noiseAnimated ?? true }
83
+ get noiseScale() { return this.volumetricSettings.noiseScale ?? 0.25 } // Noise frequency (higher = finer detail)
84
+ get mainLightScatter() { return this.volumetricSettings.mainLightScatter ?? 1.0 } // Scatter when camera in light
85
+ get mainLightScatterDark() { return this.volumetricSettings.mainLightScatterDark ?? 3.0 } // Scatter when camera in shadow
86
+ get mainLightSaturation() { return this.volumetricSettings.mainLightSaturation ?? 1.0 } // Max brightness cap
87
+ // Brightness-based attenuation (fog less visible over bright surfaces)
88
+ get brightnessThreshold() { return this.volumetricSettings.brightnessThreshold ?? 1.0 } // Scene luminance where fog starts fading
89
+ get minVisibility() { return this.volumetricSettings.minVisibility ?? 0.15 } // Minimum fog visibility over very bright surfaces
90
+ get skyBrightness() { return this.volumetricSettings.skyBrightness ?? 5.0 } // Virtual brightness for sky (far depth)
91
+ // Debug mode: 0=normal, 1=depth, 2=ray dir, 3=noise, 4=viewDir.z, 5=worldPos, 6=accum, 7=light dist, 8=light pos
92
+ get debugMode() { return this.volumetricSettings.debug ?? 0 }
93
+
94
+ setInputTexture(texture) { this.inputTexture = texture }
95
+ setGBuffer(gbuffer) { this.gbuffer = gbuffer }
96
+ setShadowPass(shadowPass) { this.shadowPass = shadowPass }
97
+ setLightingPass(lightingPass) { this.lightingPass = lightingPass }
98
+ getOutputTexture() { return this.outputTexture }
99
+
100
+ // Unused setters (kept for API compatibility)
101
+ setHiZPass() {}
102
+
103
+ async _init() {
104
+ const { device } = this.engine
105
+
106
+ this.linearSampler = device.createSampler({
107
+ label: 'Volumetric Linear Sampler',
108
+ minFilter: 'linear',
109
+ magFilter: 'linear',
110
+ addressModeU: 'clamp-to-edge',
111
+ addressModeV: 'clamp-to-edge',
112
+ })
113
+
114
+ // Fallback shadow resources
115
+ this.fallbackShadowSampler = device.createSampler({
116
+ label: 'Volumetric Fallback Shadow Sampler',
117
+ compare: 'less',
118
+ })
119
+
120
+ this.fallbackCascadeShadowMap = device.createTexture({
121
+ label: 'Volumetric Fallback Cascade Shadow',
122
+ size: [1, 1, 3],
123
+ format: 'depth32float',
124
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
125
+ })
126
+
127
+ const identityMatrix = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1])
128
+ const matrixData = new Float32Array(16 * 3)
129
+ for (let i = 0; i < 3; i++) matrixData.set(identityMatrix, i * 16)
130
+
131
+ this.fallbackCascadeMatrices = device.createBuffer({
132
+ label: 'Volumetric Fallback Cascade Matrices',
133
+ size: 16 * 4 * 3,
134
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
135
+ })
136
+ device.queue.writeBuffer(this.fallbackCascadeMatrices, 0, matrixData)
137
+
138
+ // Fallback lights buffer (empty) - 96 bytes per light to match WGSL alignment
139
+ this.fallbackLightsBuffer = device.createBuffer({
140
+ label: 'Volumetric Fallback Lights',
141
+ size: 768 * 96, // MAX_LIGHTS * 96 bytes per light
142
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
143
+ })
144
+
145
+ // Fallback spot shadow atlas (1x1 depth texture)
146
+ this.fallbackSpotShadowAtlas = device.createTexture({
147
+ label: 'Volumetric Fallback Spot Shadow Atlas',
148
+ size: [1, 1, 1],
149
+ format: 'depth32float',
150
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
151
+ })
152
+
153
+ // Fallback spot matrices buffer
154
+ const spotMatrixData = new Float32Array(16 * 16) // 16 spot shadows max
155
+ for (let i = 0; i < 16; i++) spotMatrixData.set(identityMatrix, i * 16)
156
+ this.fallbackSpotMatrices = device.createBuffer({
157
+ label: 'Volumetric Fallback Spot Matrices',
158
+ size: 16 * 4 * 16,
159
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
160
+ })
161
+ device.queue.writeBuffer(this.fallbackSpotMatrices, 0, spotMatrixData)
162
+
163
+ await this._createResources(this.engine.canvas.width, this.engine.canvas.height)
164
+ }
165
+
166
+ async _createResources(width, height) {
167
+ const { device } = this.engine
168
+
169
+ this.canvasWidth = width
170
+ this.canvasHeight = height
171
+ this.renderWidth = Math.max(1, Math.floor(width * this.resolution))
172
+ this.renderHeight = Math.max(1, Math.floor(height * this.resolution))
173
+
174
+ this._destroyTextures()
175
+
176
+ // Create 2D textures at reduced resolution
177
+ this.raymarchTexture = this._create2DTexture('Raymarch Output', this.renderWidth, this.renderHeight)
178
+ this.blurTempTexture = this._create2DTexture('Blur Temp', this.renderWidth, this.renderHeight)
179
+ this.blurredTexture = this._create2DTexture('Blurred Fog', this.renderWidth, this.renderHeight)
180
+ this.outputTexture = await Texture.renderTarget(this.engine, 'rgba16float', width, height)
181
+
182
+ // Uniform buffers
183
+ this.raymarchUniformBuffer = this._createUniformBuffer('Raymarch Uniforms', 256)
184
+ this.blurHUniformBuffer = this._createUniformBuffer('Blur H Uniforms', 32)
185
+ this.blurVUniformBuffer = this._createUniformBuffer('Blur V Uniforms', 32)
186
+ this.compositeUniformBuffer = this._createUniformBuffer('Composite Uniforms', 48)
187
+
188
+ await this._createPipelines()
189
+ }
190
+
191
+ _create2DTexture(label, width, height) {
192
+ const texture = this.engine.device.createTexture({
193
+ label,
194
+ size: [width, height, 1],
195
+ format: 'rgba16float',
196
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
197
+ })
198
+ return { texture, view: texture.createView(), width, height }
199
+ }
200
+
201
+ _createUniformBuffer(label, size) {
202
+ return this.engine.device.createBuffer({
203
+ label,
204
+ size,
205
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
206
+ })
207
+ }
208
+
209
+ async _createPipelines() {
210
+ const { device } = this.engine
211
+
212
+ // === Raymarch Pipeline ===
213
+ const raymarchModule = device.createShaderModule({
214
+ label: 'Volumetric Raymarch Shader',
215
+ code: volumetricRaymarchWGSL,
216
+ })
217
+
218
+ this.raymarchBGL = device.createBindGroupLayout({
219
+ label: 'Volumetric Raymarch BGL',
220
+ entries: [
221
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
222
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
223
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth', viewDimension: '2d-array' } },
224
+ { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
225
+ { binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } }, // cascade matrices
226
+ { binding: 5, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } }, // lights
227
+ { binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } }, // spot shadow atlas
228
+ { binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } }, // spot matrices
229
+ ],
230
+ })
231
+
232
+ this.raymarchPipeline = await device.createRenderPipelineAsync({
233
+ label: 'Volumetric Raymarch Pipeline',
234
+ layout: device.createPipelineLayout({ bindGroupLayouts: [this.raymarchBGL] }),
235
+ vertex: { module: raymarchModule, entryPoint: 'vertexMain' },
236
+ fragment: {
237
+ module: raymarchModule,
238
+ entryPoint: 'fragmentMain',
239
+ targets: [{ format: 'rgba16float' }],
240
+ },
241
+ primitive: { topology: 'triangle-list' },
242
+ })
243
+
244
+ // === Blur Pipeline ===
245
+ const blurModule = device.createShaderModule({
246
+ label: 'Volumetric Blur Shader',
247
+ code: volumetricBlurWGSL,
248
+ })
249
+
250
+ this.blurBGL = device.createBindGroupLayout({
251
+ label: 'Volumetric Blur BGL',
252
+ entries: [
253
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
254
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
255
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
256
+ ],
257
+ })
258
+
259
+ const blurPipelineDesc = {
260
+ layout: device.createPipelineLayout({ bindGroupLayouts: [this.blurBGL] }),
261
+ vertex: { module: blurModule, entryPoint: 'vertexMain' },
262
+ fragment: {
263
+ module: blurModule,
264
+ entryPoint: 'fragmentMain',
265
+ targets: [{ format: 'rgba16float' }],
266
+ },
267
+ primitive: { topology: 'triangle-list' },
268
+ }
269
+
270
+ this.blurHPipeline = await device.createRenderPipelineAsync({ ...blurPipelineDesc, label: 'Volumetric Blur H' })
271
+ this.blurVPipeline = await device.createRenderPipelineAsync({ ...blurPipelineDesc, label: 'Volumetric Blur V' })
272
+
273
+ // === Composite Pipeline ===
274
+ const compositeModule = device.createShaderModule({
275
+ label: 'Volumetric Composite Shader',
276
+ code: volumetricCompositeWGSL,
277
+ })
278
+
279
+ this.compositeBGL = device.createBindGroupLayout({
280
+ label: 'Volumetric Composite BGL',
281
+ entries: [
282
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
283
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
284
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
285
+ { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
286
+ { binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
287
+ ],
288
+ })
289
+
290
+ this.compositePipeline = await device.createRenderPipelineAsync({
291
+ label: 'Volumetric Composite Pipeline',
292
+ layout: device.createPipelineLayout({ bindGroupLayouts: [this.compositeBGL] }),
293
+ vertex: { module: compositeModule, entryPoint: 'vertexMain' },
294
+ fragment: {
295
+ module: compositeModule,
296
+ entryPoint: 'fragmentMain',
297
+ targets: [{ format: 'rgba16float' }],
298
+ },
299
+ primitive: { topology: 'triangle-list' },
300
+ })
301
+ }
302
+
303
+ async _execute(context) {
304
+ if (!this.isVolumetricEnabled) return
305
+ if (!this.inputTexture || !this.gbuffer) return
306
+
307
+ const { device } = this.engine
308
+ const { camera, mainLight, lights } = context
309
+ const time = performance.now() / 1000
310
+
311
+ // Update adaptive main light scatter based on camera shadow state
312
+ this._updateAdaptiveScatter(camera, mainLight, time)
313
+
314
+ // Get light count from context or lighting pass
315
+ const lightCount = lights?.length ?? this.lightingPass?.lightCount ?? 0
316
+
317
+ const commandEncoder = device.createCommandEncoder({ label: 'Volumetric Fog Pass' })
318
+
319
+ // === Stage 1: Ray Marching ===
320
+ this._updateRaymarchUniforms(camera, mainLight, time, lightCount)
321
+ const raymarchBindGroup = this._createRaymarchBindGroup()
322
+ if (raymarchBindGroup) {
323
+ const raymarchPass = commandEncoder.beginRenderPass({
324
+ label: 'Volumetric Raymarch',
325
+ colorAttachments: [{
326
+ view: this.raymarchTexture.view,
327
+ loadOp: 'clear',
328
+ storeOp: 'store',
329
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
330
+ }],
331
+ })
332
+ raymarchPass.setPipeline(this.raymarchPipeline)
333
+ raymarchPass.setBindGroup(0, raymarchBindGroup)
334
+ raymarchPass.draw(3)
335
+ raymarchPass.end()
336
+ }
337
+
338
+ // === Stage 2: Blur ===
339
+ this._updateBlurUniforms(this.blurHUniformBuffer, 1, 0)
340
+ this._updateBlurUniforms(this.blurVUniformBuffer, 0, 1)
341
+
342
+ const blurHBindGroup = this._createBlurBindGroup(this.raymarchTexture, this.blurHUniformBuffer)
343
+ const blurHPass = commandEncoder.beginRenderPass({
344
+ label: 'Volumetric Blur H',
345
+ colorAttachments: [{
346
+ view: this.blurTempTexture.view,
347
+ loadOp: 'clear',
348
+ storeOp: 'store',
349
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
350
+ }],
351
+ })
352
+ blurHPass.setPipeline(this.blurHPipeline)
353
+ blurHPass.setBindGroup(0, blurHBindGroup)
354
+ blurHPass.draw(3)
355
+ blurHPass.end()
356
+
357
+ const blurVBindGroup = this._createBlurBindGroup(this.blurTempTexture, this.blurVUniformBuffer)
358
+ const blurVPass = commandEncoder.beginRenderPass({
359
+ label: 'Volumetric Blur V',
360
+ colorAttachments: [{
361
+ view: this.blurredTexture.view,
362
+ loadOp: 'clear',
363
+ storeOp: 'store',
364
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
365
+ }],
366
+ })
367
+ blurVPass.setPipeline(this.blurVPipeline)
368
+ blurVPass.setBindGroup(0, blurVBindGroup)
369
+ blurVPass.draw(3)
370
+ blurVPass.end()
371
+
372
+ // === Stage 3: Composite ===
373
+ this._updateCompositeUniforms()
374
+ const compositeBindGroup = this._createCompositeBindGroup()
375
+ if (compositeBindGroup) {
376
+ const compositePass = commandEncoder.beginRenderPass({
377
+ label: 'Volumetric Composite',
378
+ colorAttachments: [{
379
+ view: this.outputTexture.view,
380
+ loadOp: 'clear',
381
+ storeOp: 'store',
382
+ clearValue: { r: 0, g: 0, b: 0, a: 1 },
383
+ }],
384
+ })
385
+ compositePass.setPipeline(this.compositePipeline)
386
+ compositePass.setBindGroup(0, compositeBindGroup)
387
+ compositePass.draw(3)
388
+ compositePass.end()
389
+ }
390
+
391
+ device.queue.submit([commandEncoder.finish()])
392
+ }
393
+
394
+ /**
395
+ * Update adaptive main light scatter based on camera shadow state and sky visibility
396
+ * Smoothly transitions between light/dark scatter values
397
+ * Only uses dark scatter if camera is in shadow AND there's something overhead (no sky)
398
+ */
399
+ _updateAdaptiveScatter(camera, mainLight, currentTime) {
400
+ // Initialize on first call
401
+ if (this._currentMainLightScatter === null) {
402
+ this._currentMainLightScatter = this.mainLightScatter
403
+ }
404
+
405
+ const deltaTime = this._lastUpdateTime > 0 ? (currentTime - this._lastUpdateTime) : 0.016
406
+ this._lastUpdateTime = currentTime
407
+
408
+ // Check if camera is in shadow using cascade shadow matrices
409
+ let cameraInShadow = false
410
+
411
+ if (this.shadowPass && this.shadowsEnabled && mainLight?.enabled !== false) {
412
+ const cameraPos = camera.position || [0, 0, 0]
413
+ cameraInShadow = this._isCameraInShadow(cameraPos)
414
+
415
+ // Periodically check sky visibility using raycaster (every 0.5s)
416
+ if (!this._skyCheckPending && currentTime - this._lastSkyCheckTime > 0.5) {
417
+ this._checkSkyVisibility(cameraPos)
418
+ }
419
+ }
420
+
421
+ // Only consider "in dark area" if in shadow AND sky is NOT visible
422
+ // If sky is visible (outdoors), don't boost scatter even in shadow
423
+ const inDarkArea = cameraInShadow && !this._skyVisible
424
+
425
+ // Smooth transition toward target state
426
+ // Going into dark area: 5 seconds (slow), exiting: 1 second (fast)
427
+ const targetShadowState = inDarkArea ? 1.0 : 0.0
428
+ const transitionSpeed = inDarkArea ? (1.0 / 5.0) : (1.0 / 1.0) // per second
429
+
430
+ // Exponential smoothing toward target
431
+ const t = 1.0 - Math.exp(-transitionSpeed * deltaTime * 3.0)
432
+ this._cameraInShadowSmooth += (targetShadowState - this._cameraInShadowSmooth) * t
433
+
434
+ // Clamp to valid range
435
+ this._cameraInShadowSmooth = Math.max(0, Math.min(1, this._cameraInShadowSmooth))
436
+
437
+ // Interpolate scatter value
438
+ const lightScatter = this.mainLightScatter
439
+ const darkScatter = this.mainLightScatterDark
440
+ this._currentMainLightScatter = lightScatter + (darkScatter - lightScatter) * this._cameraInShadowSmooth
441
+ }
442
+
443
+ /**
444
+ * Check if sky is visible above the camera using raycaster
445
+ * This is async and updates _skyVisible when complete
446
+ */
447
+ _checkSkyVisibility(cameraPos) {
448
+ const raycaster = this.engine?.raycaster
449
+ if (!raycaster) return
450
+
451
+ this._skyCheckPending = true
452
+ this._lastSkyCheckTime = this._lastUpdateTime
453
+
454
+ // Cast ray upward from camera position
455
+ const skyCheckDistance = this.volumetricSettings.skyCheckDistance ?? 100
456
+ const debugSkyCheck = this.volumetricSettings.debugSkyCheck ?? false
457
+
458
+ raycaster.cast(
459
+ cameraPos,
460
+ [0, 1, 0], // Straight up
461
+ skyCheckDistance,
462
+ (result) => {
463
+ const wasVisible = this._skyVisible
464
+ this._skyVisible = !result.hit
465
+ this._skyCheckPending = false
466
+
467
+ if (debugSkyCheck) {
468
+ const pos = cameraPos.map(v => v.toFixed(1)).join(', ')
469
+ if (result.hit) {
470
+ console.log(`Sky check from [${pos}]: HIT ${result.meshName || result.candidateId} at dist=${result.distance?.toFixed(1)}`)
471
+ } else {
472
+ console.log(`Sky check from [${pos}]: NO HIT (sky visible)`)
473
+ }
474
+ }
475
+ },
476
+ { backfaces: true, debug: debugSkyCheck } // Need backfaces to hit ceilings from below
477
+ )
478
+ }
479
+
480
+ /**
481
+ * Check if camera position is in shadow
482
+ * Uses the shadow pass's isCameraInShadow method if available,
483
+ * otherwise falls back to checking fog height bounds
484
+ */
485
+ _isCameraInShadow(cameraPos) {
486
+ // Try shadow pass method first (if it has GPU shadow readback)
487
+ if (typeof this.shadowPass?.isCameraInShadow === 'function') {
488
+ return this.shadowPass.isCameraInShadow(cameraPos)
489
+ }
490
+
491
+ // Fallback: check if camera is below the fog height range
492
+ // This is a simple heuristic - if camera is at the bottom of fog volume,
493
+ // it's more likely to be in a shadowed area (cave, corridor, etc.)
494
+ const heightRange = this.heightRange
495
+ const fogBottom = heightRange[0]
496
+ const fogTop = heightRange[1]
497
+ const fogMiddle = (fogBottom + fogTop) / 2
498
+
499
+ // Consider "in shadow" if camera is in the lower third of fog volume
500
+ // This is a rough approximation - works for indoor/cave scenarios
501
+ const lowerThird = fogBottom + (fogTop - fogBottom) * 0.33
502
+ if (cameraPos[1] < lowerThird) {
503
+ return true
504
+ }
505
+
506
+ // Also check if main light direction is mostly blocked (sun very low)
507
+ // This helps with sunset/sunrise scenarios
508
+ const mainLightDir = this.engine?.settings?.mainLight?.direction
509
+ if (mainLightDir) {
510
+ // If sun direction Y component is positive (sun below horizon in our convention)
511
+ // or very low angle, consider shadowed
512
+ const sunAngle = mainLightDir[1] // Y component of direction
513
+ if (sunAngle > 0.7) { // Sun mostly pointing up = below horizon
514
+ return true
515
+ }
516
+ }
517
+
518
+ return false
519
+ }
520
+
521
+ _updateRaymarchUniforms(camera, mainLight, time, lightCount) {
522
+ const { device } = this.engine
523
+
524
+ // Verify camera matrices exist
525
+ if (!camera.iProj || !camera.iView) {
526
+ console.warn('VolumetricFogPass: Camera missing iProj or iView matrices')
527
+ return
528
+ }
529
+
530
+ const invProj = camera.iProj
531
+ const invView = camera.iView
532
+ const cameraPos = camera.position || [0, 0, 0]
533
+
534
+ const mainLightEnabled = mainLight?.enabled !== false
535
+ const mainLightDir = mainLight?.direction ?? [-1.0, 1.0, -0.5]
536
+ const mainLightColor = mainLight?.color ?? [1, 0.95, 0.9]
537
+ const mainLightIntensity = mainLightEnabled ? (mainLight?.intensity ?? 1.0) : 0.0
538
+
539
+ const fogColor = this.fogSettings.color ?? [0.8, 0.85, 0.9]
540
+ const heightFade = this.heightRange
541
+
542
+ const shadowsEnabled = this.shadowsEnabled && this.shadowPass != null
543
+
544
+ const data = new Float32Array(64)
545
+ let offset = 0
546
+
547
+ // mat4 inverseProjection
548
+ data.set(invProj, offset); offset += 16
549
+
550
+ // mat4 inverseView
551
+ data.set(invView, offset); offset += 16
552
+
553
+ // vec3 cameraPosition + nearPlane
554
+ data[offset++] = cameraPos[0]
555
+ data[offset++] = cameraPos[1]
556
+ data[offset++] = cameraPos[2]
557
+ data[offset++] = camera.near ?? 0.1
558
+
559
+ // farPlane + maxSamples + time + fogDensity
560
+ data[offset++] = camera.far ?? 1000
561
+ data[offset++] = this.maxSamples
562
+ data[offset++] = time
563
+ data[offset++] = this.fogDensity
564
+
565
+ // vec3 fogColor + shadowsEnabled
566
+ data[offset++] = fogColor[0]
567
+ data[offset++] = fogColor[1]
568
+ data[offset++] = fogColor[2]
569
+ data[offset++] = shadowsEnabled ? 1.0 : 0.0
570
+
571
+ // vec3 mainLightDir + mainLightIntensity
572
+ data[offset++] = mainLightDir[0]
573
+ data[offset++] = mainLightDir[1]
574
+ data[offset++] = mainLightDir[2]
575
+ data[offset++] = mainLightIntensity
576
+
577
+ // vec3 mainLightColor + scatterStrength
578
+ data[offset++] = mainLightColor[0]
579
+ data[offset++] = mainLightColor[1]
580
+ data[offset++] = mainLightColor[2]
581
+ data[offset++] = this.scatterStrength
582
+
583
+ // vec2 fogHeightFade + maxDistance + lightCount
584
+ data[offset++] = heightFade[0]
585
+ data[offset++] = heightFade[1]
586
+ data[offset++] = this.maxDistance
587
+ data[offset++] = lightCount
588
+
589
+ // debugMode + noiseStrength + noiseAnimated + mainLightScatter (adaptive)
590
+ data[offset++] = this.debugMode
591
+ data[offset++] = this.noiseStrength
592
+ data[offset++] = this.noiseAnimated ? 1.0 : 0.0
593
+ data[offset++] = this._currentMainLightScatter // Uses adaptive value
594
+
595
+ // noiseScale + mainLightSaturation (ends the struct)
596
+ data[offset++] = this.noiseScale
597
+ data[offset++] = this.mainLightSaturation
598
+
599
+ device.queue.writeBuffer(this.raymarchUniformBuffer, 0, data)
600
+ }
601
+
602
+ _updateBlurUniforms(buffer, dirX, dirY) {
603
+ const data = new Float32Array([
604
+ dirX, dirY,
605
+ 1.0 / this.renderWidth, 1.0 / this.renderHeight,
606
+ this.blurRadius, 0, 0, 0,
607
+ ])
608
+ this.engine.device.queue.writeBuffer(buffer, 0, data)
609
+ }
610
+
611
+ _updateCompositeUniforms() {
612
+ const data = new Float32Array([
613
+ this.canvasWidth, this.canvasHeight,
614
+ this.renderWidth, this.renderHeight,
615
+ 1.0 / this.canvasWidth, 1.0 / this.canvasHeight,
616
+ this.brightnessThreshold, this.minVisibility,
617
+ this.skyBrightness, 0, // skyBrightness + padding
618
+ 0, 0, // extra padding to 48 bytes
619
+ ])
620
+ this.engine.device.queue.writeBuffer(this.compositeUniformBuffer, 0, data)
621
+ }
622
+
623
+ _createRaymarchBindGroup() {
624
+ const { device } = this.engine
625
+
626
+ const depthTexture = this.gbuffer?.depth
627
+ if (!depthTexture) return null
628
+
629
+ const cascadeShadows = this.shadowPass?.getShadowMap?.() ?? this.fallbackCascadeShadowMap
630
+ const cascadeMatrices = this.shadowPass?.getCascadeMatricesBuffer?.() ?? this.fallbackCascadeMatrices
631
+ const shadowSampler = this.shadowPass?.getShadowSampler?.() ?? this.fallbackShadowSampler
632
+
633
+ // Light resources
634
+ const lightsBuffer = this.lightingPass?.getLightBuffer?.() ?? this.fallbackLightsBuffer
635
+ const spotShadowAtlas = this.shadowPass?.getSpotShadowAtlasView?.() ?? this.fallbackSpotShadowAtlas.createView()
636
+ const spotMatrices = this.shadowPass?.getSpotMatricesBuffer?.() ?? this.fallbackSpotMatrices
637
+
638
+ return device.createBindGroup({
639
+ label: 'Volumetric Raymarch Bind Group',
640
+ layout: this.raymarchBGL,
641
+ entries: [
642
+ { binding: 0, resource: { buffer: this.raymarchUniformBuffer } },
643
+ { binding: 1, resource: depthTexture.texture.createView({ aspect: 'depth-only' }) },
644
+ { binding: 2, resource: cascadeShadows.createView({ dimension: '2d-array', aspect: 'depth-only' }) },
645
+ { binding: 3, resource: shadowSampler },
646
+ { binding: 4, resource: { buffer: cascadeMatrices } },
647
+ { binding: 5, resource: { buffer: lightsBuffer } },
648
+ { binding: 6, resource: spotShadowAtlas },
649
+ { binding: 7, resource: { buffer: spotMatrices } },
650
+ ],
651
+ })
652
+ }
653
+
654
+ _createBlurBindGroup(inputTexture, uniformBuffer) {
655
+ return this.engine.device.createBindGroup({
656
+ label: 'Volumetric Blur Bind Group',
657
+ layout: this.blurBGL,
658
+ entries: [
659
+ { binding: 0, resource: { buffer: uniformBuffer } },
660
+ { binding: 1, resource: inputTexture.view },
661
+ { binding: 2, resource: this.linearSampler },
662
+ ],
663
+ })
664
+ }
665
+
666
+ _createCompositeBindGroup() {
667
+ if (!this.inputTexture) return null
668
+
669
+ const depthTexture = this.gbuffer?.depth
670
+ if (!depthTexture) return null
671
+
672
+ return this.engine.device.createBindGroup({
673
+ label: 'Volumetric Composite Bind Group',
674
+ layout: this.compositeBGL,
675
+ entries: [
676
+ { binding: 0, resource: { buffer: this.compositeUniformBuffer } },
677
+ { binding: 1, resource: this.inputTexture.view },
678
+ { binding: 2, resource: this.blurredTexture.view },
679
+ { binding: 3, resource: this.linearSampler },
680
+ { binding: 4, resource: depthTexture.texture.createView({ aspect: 'depth-only' }) },
681
+ ],
682
+ })
683
+ }
684
+
685
+ _destroyTextures() {
686
+ const textures = [this.raymarchTexture, this.blurTempTexture, this.blurredTexture, this.outputTexture]
687
+ for (const tex of textures) {
688
+ if (tex?.texture) tex.texture.destroy()
689
+ }
690
+ this.raymarchTexture = null
691
+ this.blurTempTexture = null
692
+ this.blurredTexture = null
693
+ this.outputTexture = null
694
+ }
695
+
696
+ async _resize(width, height) {
697
+ await this._createResources(width, height)
698
+ }
699
+
700
+ _destroy() {
701
+ this._destroyTextures()
702
+
703
+ const buffers = [this.raymarchUniformBuffer, this.blurHUniformBuffer, this.blurVUniformBuffer, this.compositeUniformBuffer]
704
+ for (const buf of buffers) {
705
+ if (buf) buf.destroy()
706
+ }
707
+
708
+ this.raymarchPipeline = null
709
+ this.blurHPipeline = null
710
+ this.blurVPipeline = null
711
+ this.compositePipeline = null
712
+ }
713
+ }
714
+
715
+ export { VolumetricFogPass }