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,703 @@
1
+ import GUI from 'lil-gui'
2
+
3
+ /**
4
+ * DebugUI - Stats display and settings GUI for the renderer
5
+ *
6
+ * Created lazily on first debug mode activation.
7
+ * Shows stats panel and collapsible settings folders.
8
+ */
9
+ class DebugUI {
10
+ constructor(engine) {
11
+ this.engine = engine
12
+ this.gui = null
13
+ this.statsFolder = null
14
+ this.initialized = false
15
+
16
+ // Stats display elements
17
+ this._statsController = null
18
+ this._statsText = ''
19
+
20
+ // Stats update throttling (5 updates per second)
21
+ this._lastStatsUpdate = 0
22
+ this._statsUpdateInterval = 200 // ms
23
+
24
+ // Folder references for updating
25
+ this.folders = {}
26
+ }
27
+
28
+ /**
29
+ * Initialize the debug UI (called lazily on first debug mode)
30
+ */
31
+ init() {
32
+ if (this.initialized) return
33
+
34
+ this.gui = new GUI({ title: 'Debug Panel' })
35
+ this.gui.close() // Start collapsed
36
+
37
+ // Add CSS for stats styling
38
+ this._addStyles()
39
+
40
+ // Create stats folder at the top
41
+ this._createStatsFolder()
42
+
43
+ // Create settings folders
44
+ this._createRenderingFolder()
45
+ this._createCameraFolder()
46
+ this._createEnvironmentFolder()
47
+ this._createLightingFolder()
48
+ this._createMainLightFolder()
49
+ this._createShadowFolder()
50
+ this._createPlanarReflectionFolder()
51
+ this._createAOFolder()
52
+ this._createAmbientCaptureFolder()
53
+ this._createSSGIFolder()
54
+ this._createVolumetricFogFolder()
55
+ this._createBloomFolder()
56
+ this._createTonemapFolder()
57
+ this._createDitheringFolder()
58
+ this._createCRTFolder()
59
+ this._createDebugFolder()
60
+
61
+ // Close all folders by default
62
+ for (const folder of Object.values(this.folders)) {
63
+ folder.close()
64
+ }
65
+
66
+ this.initialized = true
67
+ }
68
+
69
+ /**
70
+ * Add custom CSS for the debug UI
71
+ */
72
+ _addStyles() {
73
+ if (document.getElementById('debug-ui-styles')) return
74
+
75
+ const style = document.createElement('style')
76
+ style.id = 'debug-ui-styles'
77
+ style.textContent = `
78
+ .lil-gui .stats-display {
79
+ font-family: monospace;
80
+ font-size: 11px;
81
+ line-height: 1.4;
82
+ white-space: pre;
83
+ padding: 4px 8px;
84
+ background: rgba(0, 0, 0, 0.3);
85
+ border-radius: 3px;
86
+ color: #8f8;
87
+ }
88
+ .lil-gui .stats-title {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 8px;
92
+ }
93
+ .lil-gui .stats-title::before {
94
+ content: '📊';
95
+ }
96
+ `
97
+ document.head.appendChild(style)
98
+ }
99
+
100
+ /**
101
+ * Create stats folder with live updating display
102
+ */
103
+ _createStatsFolder() {
104
+ this.statsFolder = this.gui.addFolder('Stats')
105
+ this.statsFolder.$title.classList.add('stats-title')
106
+
107
+ // Create a custom stats display element
108
+ const statsDiv = document.createElement('div')
109
+ statsDiv.className = 'stats-display'
110
+ statsDiv.textContent = 'Loading...'
111
+ this._statsElement = statsDiv
112
+
113
+ // Add to folder
114
+ this.statsFolder.$children.appendChild(statsDiv)
115
+
116
+ // Keep stats folder open by default
117
+ this.statsFolder.open()
118
+ }
119
+
120
+ /**
121
+ * Update stats display (throttled to 5 updates/sec, only when panel is open)
122
+ */
123
+ updateStats() {
124
+ if (!this._statsElement || !this.engine.stats) return
125
+
126
+ // Only update if GUI panel is open
127
+ if (this.gui?._closed) return
128
+
129
+ // Throttle updates to 5 per second
130
+ const now = performance.now()
131
+ if (now - this._lastStatsUpdate < this._statsUpdateInterval) return
132
+ this._lastStatsUpdate = now
133
+
134
+ const s = this.engine.stats
135
+ const fmt = this._fmt.bind(this)
136
+
137
+ // Basic timing stats
138
+ const fps = s.avg_fps?.toFixed(0) || '0'
139
+ const ms = s.avg_dt?.toFixed(1) || '0'
140
+ const renderMs = s.avg_dt_render?.toFixed(2) || '0'
141
+
142
+ // Draw calls breakdown
143
+ const dc = s.totalDC || s.drawCalls || 0
144
+ const dcParts = []
145
+ if (s.shadowDC) dcParts.push(`sh:${fmt(s.shadowDC)}`)
146
+ if (s.planarDC) dcParts.push(`pl:${fmt(s.planarDC)}`)
147
+ if (s.transparentDC) dcParts.push(`tr:${fmt(s.transparentDC)}`)
148
+ const dcBreakdown = dcParts.length > 0 ? ` (${dcParts.join(' ')})` : ''
149
+
150
+ // Triangles breakdown
151
+ const tri = s.totalTri || s.triangles || 0
152
+ const triStr = this._formatNumber(tri)
153
+ const triParts = []
154
+ if (s.shadowTri) triParts.push(`sh:${this._formatNumber(s.shadowTri)}`)
155
+ if (s.planarTri) triParts.push(`pl:${this._formatNumber(s.planarTri)}`)
156
+ if (s.transparentTri) triParts.push(`tr:${this._formatNumber(s.transparentTri)}`)
157
+ const triBreakdown = triParts.length > 0 ? ` (${triParts.join(' ')})` : ''
158
+
159
+ // Get renderer stats
160
+ const renderer = this.engine.renderer
161
+ const lightingStats = renderer?.getPass?.('lighting')?.stats || {}
162
+ const gbufferStats = renderer?.getPass?.('gbuffer')?.legacyCullingStats || {}
163
+ const occStats = renderer?.cullingSystem?.getOcclusionStats?.() || {}
164
+ const rendererStats = renderer?.stats || {}
165
+
166
+ // Entity culling stats
167
+ const visibleEntities = rendererStats.visibleEntities || 0
168
+ const occCulled = occStats.culled || 0
169
+
170
+ // Mesh culling stats
171
+ const meshRendered = gbufferStats.rendered || 0
172
+ const meshTotal = gbufferStats.total || 0
173
+ const meshOccCulled = gbufferStats.culledByOcclusion || 0
174
+ const meshSkipped = gbufferStats.skippedNoBsphere || 0
175
+
176
+ // Shadow entity culling
177
+ const shEntCull = []
178
+ if (s.shadowFrustumCulled) shEntCull.push(`fr:${fmt(s.shadowFrustumCulled)}`)
179
+ if (s.shadowDistanceCulled) shEntCull.push(`dist:${fmt(s.shadowDistanceCulled)}`)
180
+ if (s.shadowHiZCulled) shEntCull.push(`hiz:${fmt(s.shadowHiZCulled)}`)
181
+ if (s.shadowPixelCulled) shEntCull.push(`px:${fmt(s.shadowPixelCulled)}`)
182
+ const shEntStr = shEntCull.length > 0 ? ` ${shEntCull.join(' ')}` : ''
183
+
184
+ // Shadow mesh culling
185
+ const shMeshCull = []
186
+ if (s.shadowMeshFrustumCulled) shMeshCull.push(`fr:${fmt(s.shadowMeshFrustumCulled)}`)
187
+ if (s.shadowMeshDistanceCulled) shMeshCull.push(`dist:${fmt(s.shadowMeshDistanceCulled)}`)
188
+ if (s.shadowMeshOcclusionCulled) shMeshCull.push(`occ:${fmt(s.shadowMeshOcclusionCulled)}`)
189
+ const shMeshStr = shMeshCull.length > 0 ? ` ${shMeshCull.join(' ')}` : ''
190
+
191
+ // Lights stats
192
+ const visibleLights = lightingStats.visibleLights || 0
193
+ const lightOccCulled = lightingStats.culledByOcclusion || 0
194
+
195
+ this._statsElement.textContent =
196
+ `FPS: ${fps} (${ms}ms) render: ${renderMs}ms\n` +
197
+ `DC: ${fmt(dc)}${dcBreakdown}\n` +
198
+ `Tri: ${triStr}${triBreakdown}\n` +
199
+ `Entities: ${fmt(visibleEntities)} (occ:${fmt(occCulled)})\n` +
200
+ `Meshes: ${fmt(meshRendered)}/${fmt(meshTotal)} (occ:${fmt(meshOccCulled)} skip:${fmt(meshSkipped)})\n` +
201
+ `Shadow ent:${shEntStr || ' -'} mesh:${shMeshStr || ' -'}\n` +
202
+ `Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`
203
+ }
204
+
205
+ /**
206
+ * Format number with thin space as thousand separator
207
+ */
208
+ _fmt(n) {
209
+ if (n === undefined || n === null) return '0'
210
+ return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '\u2009')
211
+ }
212
+
213
+ /**
214
+ * Format large numbers with K/M suffix
215
+ */
216
+ _formatNumber(n) {
217
+ if (n > 1000000) return (n / 1000000).toFixed(1) + 'M'
218
+ if (n > 1000) return (n / 1000).toFixed(0) + 'K'
219
+ return n.toString()
220
+ }
221
+
222
+ _createRenderingFolder() {
223
+ const s = this.engine.settings
224
+ const folder = this.gui.addFolder('Rendering')
225
+ this.folders.rendering = folder
226
+
227
+ folder.add(s.rendering, 'renderScale', 0.25, 2.0, 0.25).name('Render Scale')
228
+ .onChange(() => this.engine.needsResize = true)
229
+ folder.add(s.rendering.autoScale, 'enabled').name('Auto-Scale (4K)')
230
+ .onChange(() => this.engine.needsResize = true)
231
+ folder.add(s.rendering.autoScale, 'maxHeight', 720, 2160, 1).name('Max Height')
232
+ folder.add(s.rendering.autoScale, 'scaleFactor', 0.25, 1.0, 0.05).name('Scale Factor')
233
+ folder.add(s.rendering, 'fxaa').name('FXAA')
234
+ folder.add(s.rendering, 'jitter').name('TAA Jitter')
235
+ if (s.rendering.jitterAmount !== undefined) {
236
+ folder.add(s.rendering, 'jitterAmount', 0, 1, 0.01).name('Jitter Amount')
237
+ }
238
+ if (s.rendering.jitterFadeDistance !== undefined) {
239
+ folder.add(s.rendering, 'jitterFadeDistance', 5, 100, 1).name('Jitter Fade Dist')
240
+ }
241
+ folder.add(s.rendering, 'debug').name('Debug Mode')
242
+ .onChange((value) => {
243
+ // Immediately hide panel when debug mode is turned off
244
+ if (!value && this.gui) {
245
+ this.gui.domElement.style.display = 'none'
246
+ }
247
+ })
248
+
249
+ // Culling subfolder
250
+ if (s.culling) {
251
+ const cullFolder = folder.addFolder('Culling')
252
+ cullFolder.add(s.culling, 'frustumEnabled').name('Frustum Culling')
253
+
254
+ if (s.occlusionCulling) {
255
+ cullFolder.add(s.occlusionCulling, 'enabled').name('Occlusion Culling')
256
+ cullFolder.add(s.occlusionCulling, 'threshold', 0.1, 2.0, 0.1).name('Occlusion Threshold')
257
+ }
258
+
259
+ // Planar reflection culling sub-folder
260
+ if (s.culling.planarReflection) {
261
+ const prFolder = cullFolder.addFolder('Planar Reflection')
262
+ prFolder.add(s.culling.planarReflection, 'frustum').name('Frustum Culling')
263
+ prFolder.add(s.culling.planarReflection, 'maxDistance', 10, 200, 10).name('Max Distance')
264
+ prFolder.add(s.culling.planarReflection, 'maxSkinned', 0, 100, 1).name('Max Skinned')
265
+ prFolder.add(s.culling.planarReflection, 'minPixelSize', 0, 16, 1).name('Min Pixel Size')
266
+ prFolder.close()
267
+ }
268
+ cullFolder.close()
269
+ }
270
+
271
+ // Noise subfolder
272
+ if (s.noise) {
273
+ const noiseFolder = folder.addFolder('Noise')
274
+ noiseFolder.add(s.noise, 'type', ['bluenoise', 'bayer8']).name('Type')
275
+ .onChange(() => {
276
+ // Reload noise texture when type changes
277
+ if (this.engine.renderer?.renderGraph) {
278
+ this.engine.renderer.renderGraph.reloadNoiseTexture()
279
+ }
280
+ })
281
+ noiseFolder.add(s.noise, 'animated').name('Animated')
282
+ noiseFolder.close()
283
+ }
284
+ }
285
+
286
+ _createAOFolder() {
287
+ const s = this.engine.settings
288
+ if (!s.ao) return
289
+
290
+ const folder = this.gui.addFolder('Ambient Occlusion')
291
+ this.folders.ao = folder
292
+
293
+ folder.add(s.ao, 'enabled').name('Enabled')
294
+ folder.add(s.ao, 'intensity', 0, 2, 0.1).name('Intensity')
295
+ folder.add(s.ao, 'radius', 8, 128, 1).name('Radius')
296
+ folder.add(s.ao, 'fadeDistance', 10, 100, 1).name('Fade Distance')
297
+ folder.add(s.ao, 'bias', 0, 0.05, 0.001).name('Bias')
298
+ folder.add(s.ao, 'level', 0, 1, 0.05).name('Level')
299
+ }
300
+
301
+ _createShadowFolder() {
302
+ const s = this.engine.settings
303
+ if (!s.shadow) return
304
+
305
+ const folder = this.gui.addFolder('Shadows')
306
+ this.folders.shadow = folder
307
+
308
+ folder.add(s.shadow, 'strength', 0, 1, 0.05).name('Strength')
309
+ if (s.shadow.spotMaxDistance !== undefined) {
310
+ folder.add(s.shadow, 'spotMaxDistance', 10, 200, 5).name('Spot Max Distance')
311
+ }
312
+ if (s.shadow.spotFadeStart !== undefined) {
313
+ folder.add(s.shadow, 'spotFadeStart', 10, 150, 5).name('Spot Fade Start')
314
+ }
315
+ folder.add(s.shadow, 'bias', 0, 0.01, 0.0001).name('Bias')
316
+ folder.add(s.shadow, 'normalBias', 0, 0.05, 0.001).name('Normal Bias')
317
+ folder.add(s.shadow, 'surfaceBias', 0, 0.1, 0.001).name('Surface Bias')
318
+ }
319
+
320
+ _createMainLightFolder() {
321
+ const s = this.engine.settings
322
+ if (!s.mainLight) return
323
+
324
+ const folder = this.gui.addFolder('Main Light')
325
+ this.folders.mainLight = folder
326
+
327
+ folder.add(s.mainLight, 'enabled').name('Enabled')
328
+ folder.add(s.mainLight, 'intensity', 0, 2, 0.05).name('Intensity')
329
+
330
+ // Color picker
331
+ folder.addColor({ color: this._rgbToHex(s.mainLight.color) }, 'color')
332
+ .name('Color')
333
+ .onChange((hex) => {
334
+ const rgb = this._hexToRgb(hex)
335
+ s.mainLight.color = [rgb.r, rgb.g, rgb.b]
336
+ })
337
+
338
+ // Direction controls
339
+ const dirProxy = {
340
+ x: s.mainLight.direction[0],
341
+ y: s.mainLight.direction[1],
342
+ z: s.mainLight.direction[2]
343
+ }
344
+ const updateDir = () => {
345
+ s.mainLight.direction = [dirProxy.x, dirProxy.y, dirProxy.z]
346
+ }
347
+ folder.add(dirProxy, 'x', -1, 1, 0.05).name('Direction X').onChange(updateDir)
348
+ folder.add(dirProxy, 'y', -1, 1, 0.05).name('Direction Y').onChange(updateDir)
349
+ folder.add(dirProxy, 'z', -1, 1, 0.05).name('Direction Z').onChange(updateDir)
350
+ }
351
+
352
+ _createEnvironmentFolder() {
353
+ const s = this.engine.settings
354
+ if (!s.environment) return
355
+
356
+ const folder = this.gui.addFolder('Environment')
357
+ this.folders.environment = folder
358
+
359
+ folder.add(s.environment, 'exposure', 0.1, 4, 0.1).name('Exposure')
360
+ folder.add(s.environment, 'diffuse', 0, 10, 0.1).name('Diffuse IBL')
361
+ folder.add(s.environment, 'specular', 0, 10, 0.1).name('Specular IBL')
362
+
363
+ // Fog subfolder
364
+ if (s.environment.fog) {
365
+ const fogFolder = folder.addFolder('Fog')
366
+ fogFolder.add(s.environment.fog, 'enabled').name('Enabled')
367
+ fogFolder.addColor({ color: this._rgbToHex(s.environment.fog.color) }, 'color')
368
+ .name('Color')
369
+ .onChange((hex) => {
370
+ const rgb = this._hexToRgb(hex)
371
+ s.environment.fog.color[0] = rgb.r
372
+ s.environment.fog.color[1] = rgb.g
373
+ s.environment.fog.color[2] = rgb.b
374
+ })
375
+ fogFolder.add(s.environment.fog.distances, '0', 0, 50, 1).name('Near Distance')
376
+ fogFolder.add(s.environment.fog.distances, '1', 0, 200, 5).name('Mid Distance')
377
+ fogFolder.add(s.environment.fog.distances, '2', 50, 500, 10).name('Far Distance')
378
+ fogFolder.add(s.environment.fog.alpha, '0', 0, 1, 0.01).name('Near Alpha')
379
+ fogFolder.add(s.environment.fog.alpha, '1', 0, 1, 0.01).name('Mid Alpha')
380
+ fogFolder.add(s.environment.fog.alpha, '2', 0, 1, 0.01).name('Far Alpha')
381
+ fogFolder.add(s.environment.fog.heightFade, '0', -100, 100, 1).name('Bottom Y')
382
+ fogFolder.add(s.environment.fog.heightFade, '1', -50, 200, 5).name('Top Y')
383
+ fogFolder.add(s.environment.fog, 'brightResist', 0, 1, 0.05).name('Bright Resist')
384
+ // Initialize debug if not present
385
+ if (s.environment.fog.debug === undefined) s.environment.fog.debug = 0
386
+ fogFolder.add(s.environment.fog, 'debug', 0, 10, 1).name('Debug Mode')
387
+ }
388
+ }
389
+
390
+ _createLightingFolder() {
391
+ const s = this.engine.settings
392
+ if (!s.lighting) return
393
+
394
+ const folder = this.gui.addFolder('Lighting System')
395
+ this.folders.lighting = folder
396
+
397
+ folder.add(s.lighting, 'cullingEnabled').name('Light Culling')
398
+ folder.add(s.lighting, 'maxDistance', 50, 500, 10).name('Max Distance')
399
+ if (s.lighting.specularBoost !== undefined) {
400
+ folder.add(s.lighting, 'specularBoost', 0, 2, 0.05).name('Specular Boost')
401
+ }
402
+ if (s.lighting.specularBoostRoughnessCutoff !== undefined) {
403
+ folder.add(s.lighting, 'specularBoostRoughnessCutoff', 0.1, 1.0, 0.05).name('Boost Roughness Cutoff')
404
+ }
405
+ }
406
+
407
+ _createSSGIFolder() {
408
+ const s = this.engine.settings
409
+ if (!s.ssgi) return
410
+
411
+ const folder = this.gui.addFolder('SSGI (Indirect Light)')
412
+ this.folders.ssgi = folder
413
+
414
+ folder.add(s.ssgi, 'enabled').name('Enabled')
415
+ folder.add(s.ssgi, 'intensity', 0.0, 5.0, 0.1).name('Intensity')
416
+ folder.add(s.ssgi, 'emissiveBoost', 0.0, 50.0, 1.0).name('Emissive Boost')
417
+ folder.add(s.ssgi, 'maxBrightness', 1.0, 50.0, 1.0).name('Max Brightness')
418
+ folder.add(s.ssgi, 'sampleRadius', 0.5, 4.0, 0.5).name('Sample Radius')
419
+ folder.add(s.ssgi, 'saturateLevel', 0.1, 2.0, 0.1).name('Saturate Level')
420
+ }
421
+
422
+ _createVolumetricFogFolder() {
423
+ const s = this.engine.settings
424
+ if (!s.volumetricFog) return
425
+
426
+ // Initialize defaults for missing properties
427
+ const vf = s.volumetricFog
428
+ if (vf.density === undefined && vf.densityMultiplier === undefined) vf.density = 0.5
429
+ if (vf.scatterStrength === undefined) vf.scatterStrength = 1.0
430
+ if (!vf.heightRange) vf.heightRange = [-5, 20]
431
+ if (vf.resolution === undefined) vf.resolution = 0.25
432
+ if (vf.maxSamples === undefined) vf.maxSamples = 32
433
+ if (vf.blurRadius === undefined) vf.blurRadius = 4
434
+ if (vf.noiseStrength === undefined) vf.noiseStrength = 1.0
435
+ if (vf.noiseScale === undefined) vf.noiseScale = 0.25
436
+ if (vf.noiseAnimated === undefined) vf.noiseAnimated = true
437
+ if (vf.shadowsEnabled === undefined) vf.shadowsEnabled = true
438
+ if (vf.mainLightScatter === undefined) vf.mainLightScatter = 1.0
439
+ if (vf.mainLightScatterDark === undefined) vf.mainLightScatterDark = 3.0
440
+ if (vf.mainLightSaturation === undefined) vf.mainLightSaturation = 1.0
441
+ if (vf.brightnessThreshold === undefined) vf.brightnessThreshold = 1.0
442
+ if (vf.minVisibility === undefined) vf.minVisibility = 0.15
443
+ if (vf.skyBrightness === undefined) vf.skyBrightness = 5.0
444
+ if (vf.debug === undefined) vf.debug = 0
445
+
446
+ const folder = this.gui.addFolder('Volumetric Fog')
447
+ this.folders.volumetricFog = folder
448
+
449
+ folder.add(vf, 'enabled').name('Enabled')
450
+
451
+ // Density - use whichever property exists
452
+ if (vf.density !== undefined) {
453
+ folder.add(vf, 'density', 0.0, 2.0, 0.05).name('Density')
454
+ } else if (vf.densityMultiplier !== undefined) {
455
+ folder.add(vf, 'densityMultiplier', 0.0, 2.0, 0.05).name('Density')
456
+ }
457
+
458
+ folder.add(vf, 'scatterStrength', 0.0, 10.0, 0.1).name('Scatter (Lights)')
459
+ folder.add(vf, 'mainLightScatter', 0.0, 5.0, 0.1).name('Sun Scatter (Light)')
460
+ folder.add(vf, 'mainLightScatterDark', 0.0, 10.0, 0.1).name('Sun Scatter (Dark)')
461
+ folder.add(vf, 'mainLightSaturation', 0.0, 1.0, 0.01).name('Sun Saturation')
462
+ folder.add(vf, 'brightnessThreshold', 0.1, 5.0, 0.1).name('Bright Threshold')
463
+ folder.add(vf, 'minVisibility', 0.0, 1.0, 0.05).name('Min Visibility')
464
+ folder.add(vf, 'skyBrightness', 0.0, 10.0, 0.5).name('Sky Brightness')
465
+ folder.add(vf.heightRange, '0', -50, 50, 1).name('Height Bottom')
466
+ folder.add(vf.heightRange, '1', -10, 100, 1).name('Height Top')
467
+
468
+ // Quality sub-folder
469
+ const qualityFolder = folder.addFolder('Quality')
470
+ qualityFolder.add(vf, 'resolution', 0.125, 0.5, 0.125).name('Resolution')
471
+ qualityFolder.add(vf, 'maxSamples', 16, 128, 8).name('Max Samples')
472
+ qualityFolder.add(vf, 'blurRadius', 0, 8, 1).name('Blur Radius')
473
+ qualityFolder.close()
474
+
475
+ // Noise sub-folder
476
+ const noiseFolder = folder.addFolder('Noise')
477
+ noiseFolder.add(vf, 'noiseStrength', 0.0, 1.0, 0.1).name('Strength')
478
+ noiseFolder.add(vf, 'noiseScale', 0.05, 1.0, 0.05).name('Scale (Detail)')
479
+ noiseFolder.add(vf, 'noiseAnimated').name('Animated')
480
+ noiseFolder.close()
481
+
482
+ // Shadows & Debug
483
+ folder.add(vf, 'shadowsEnabled').name('Shadows')
484
+ folder.add(vf, 'debug', 0, 12, 1).name('Debug Mode')
485
+ }
486
+
487
+ _createBloomFolder() {
488
+ const s = this.engine.settings
489
+ if (!s.bloom) return
490
+
491
+ const folder = this.gui.addFolder('Bloom')
492
+ this.folders.bloom = folder
493
+
494
+ folder.add(s.bloom, 'enabled').name('Enabled')
495
+ folder.add(s.bloom, 'intensity', 0.0, 1.0, 0.01).name('Intensity')
496
+ folder.add(s.bloom, 'threshold', 0.0, 2.0, 0.01).name('Threshold')
497
+ folder.add(s.bloom, 'softThreshold', 0.0, 1.0, 0.1).name('Soft Threshold')
498
+ folder.add(s.bloom, 'radius', 8, 128, 4).name('Blur Radius')
499
+ folder.add(s.bloom, 'emissiveBoost', 0.0, 20.0, 0.1).name('Emissive Boost')
500
+ folder.add(s.bloom, 'maxBrightness', 1.0, 10.0, 0.5).name('Max Brightness')
501
+ if (s.bloom.scale !== undefined) {
502
+ folder.add(s.bloom, 'scale', 0.25, 1.0, 0.25).name('Resolution Scale')
503
+ }
504
+ }
505
+
506
+ _createTonemapFolder() {
507
+ const s = this.engine.settings
508
+ if (!s.rendering) return
509
+
510
+ // Ensure tonemapMode exists
511
+ if (s.rendering.tonemapMode === undefined) s.rendering.tonemapMode = 0
512
+
513
+ const folder = this.gui.addFolder('Tone Mapping')
514
+ this.folders.tonemap = folder
515
+
516
+ folder.add(s.rendering, 'tonemapMode', { 'ACES': 0, 'Reinhard': 1, 'None (Linear)': 2 }).name('Mode')
517
+ }
518
+
519
+ _createPlanarReflectionFolder() {
520
+ const s = this.engine.settings
521
+ if (!s.planarReflection) return
522
+
523
+ const folder = this.gui.addFolder('Planar Reflection')
524
+ this.folders.planarReflection = folder
525
+
526
+ folder.add(s.planarReflection, 'enabled').name('Enabled')
527
+ folder.add(s.planarReflection, 'groundLevel', -10, 10, 0.1).name('Ground Level')
528
+ folder.add(s.planarReflection, 'resolution', 0.25, 1.0, 0.25).name('Resolution')
529
+ folder.add(s.planarReflection, 'roughnessCutoff', 0.0, 1.0, 0.05).name('Roughness Cutoff')
530
+ folder.add(s.planarReflection, 'normalPerturbation', 0.0, 2.0, 0.1).name('Normal Perturbation')
531
+ folder.add(s.planarReflection, 'distanceFade', 0.1, 10.0, 0.1).name('Distance Fade')
532
+ }
533
+
534
+ _createAmbientCaptureFolder() {
535
+ const s = this.engine.settings
536
+ if (!s.ambientCapture) return
537
+
538
+ const folder = this.gui.addFolder('Probe GI (Ambient Capture)')
539
+ this.folders.ambientCapture = folder
540
+
541
+ folder.add(s.ambientCapture, 'enabled').name('Enabled')
542
+ folder.add(s.ambientCapture, 'intensity', 0.0, 2.0, 0.05).name('Intensity')
543
+ folder.add(s.ambientCapture, 'maxDistance', 5, 100, 5).name('Max Distance')
544
+ folder.add(s.ambientCapture, 'emissiveBoost', 0.0, 10.0, 0.1).name('Emissive Boost')
545
+ folder.add(s.ambientCapture, 'saturateLevel', 0.0, 2.0, 0.05).name('Saturate Level')
546
+ }
547
+
548
+ _createDitheringFolder() {
549
+ const s = this.engine.settings
550
+ if (!s.dithering) return
551
+
552
+ const folder = this.gui.addFolder('Dithering')
553
+ this.folders.dithering = folder
554
+
555
+ folder.add(s.dithering, 'enabled').name('Enabled')
556
+ folder.add(s.dithering, 'colorLevels', 4, 256, 1).name('Color Levels')
557
+ }
558
+
559
+ _createCRTFolder() {
560
+ const s = this.engine.settings
561
+ if (!s.crt) return
562
+
563
+ const folder = this.gui.addFolder('CRT Effect')
564
+ this.folders.crt = folder
565
+
566
+ folder.add(s.crt, 'enabled').name('CRT Enabled')
567
+ folder.add(s.crt, 'upscaleEnabled').name('Upscale Only')
568
+ folder.add(s.crt, 'upscaleTarget', 1, 8, 1).name('Upscale Target')
569
+
570
+ // Geometry sub-folder
571
+ const geomFolder = folder.addFolder('Geometry')
572
+ geomFolder.add(s.crt, 'curvature', 0, 0.25, 0.005).name('Curvature')
573
+ geomFolder.add(s.crt, 'cornerRadius', 0, 0.2, 0.005).name('Corner Radius')
574
+ geomFolder.add(s.crt, 'zoom', 1.0, 1.25, 0.005).name('Zoom')
575
+ geomFolder.close()
576
+
577
+ // Scanlines sub-folder
578
+ const scanFolder = folder.addFolder('Scanlines')
579
+ scanFolder.add(s.crt, 'scanlineIntensity', 0, 1, 0.05).name('Intensity')
580
+ scanFolder.add(s.crt, 'scanlineWidth', 0, 1, 0.05).name('Width')
581
+ scanFolder.add(s.crt, 'scanlineBrightBoost', 0, 2, 0.05).name('Bright Boost')
582
+ scanFolder.add(s.crt, 'scanlineHeight', 1, 10, 1).name('Height (px)')
583
+ scanFolder.close()
584
+
585
+ // Convergence sub-folder
586
+ const convFolder = folder.addFolder('RGB Convergence')
587
+ convFolder.add(s.crt.convergence, '0', -3, 3, 0.1).name('Red X Offset')
588
+ convFolder.add(s.crt.convergence, '1', -3, 3, 0.1).name('Green X Offset')
589
+ convFolder.add(s.crt.convergence, '2', -3, 3, 0.1).name('Blue X Offset')
590
+ convFolder.close()
591
+
592
+ // Phosphor mask sub-folder
593
+ const maskFolder = folder.addFolder('Phosphor Mask')
594
+ maskFolder.add(s.crt, 'maskType', ['none', 'aperture', 'slot', 'shadow']).name('Type')
595
+ maskFolder.add(s.crt, 'maskIntensity', 0, 1, 0.05).name('Intensity')
596
+ maskFolder.close()
597
+
598
+ // Vignette sub-folder
599
+ const vigFolder = folder.addFolder('Vignette')
600
+ vigFolder.add(s.crt, 'vignetteIntensity', 0, 1, 0.05).name('Intensity')
601
+ vigFolder.add(s.crt, 'vignetteSize', 0.1, 1, 0.05).name('Size')
602
+ vigFolder.close()
603
+
604
+ // Blur (top level, not in subfolder)
605
+ folder.add(s.crt, 'blurSize', 0, 8, 0.1).name('H-Blur (px)')
606
+ }
607
+
608
+ _createCameraFolder() {
609
+ const s = this.engine.settings
610
+ if (!s.camera) return
611
+
612
+ const folder = this.gui.addFolder('Camera')
613
+ this.folders.camera = folder
614
+
615
+ folder.add(s.camera, 'fov', 30, 120, 1).name('FOV').onChange(() => {
616
+ if (this.engine.camera) this.engine.camera.fov = s.camera.fov
617
+ })
618
+ folder.add(s.camera, 'near', 0.01, 1, 0.01).name('Near Plane').onChange(() => {
619
+ if (this.engine.camera) this.engine.camera.near = s.camera.near
620
+ })
621
+ folder.add(s.camera, 'far', 100, 10000, 100).name('Far Plane').onChange(() => {
622
+ if (this.engine.camera) this.engine.camera.far = s.camera.far
623
+ })
624
+ }
625
+
626
+ _createDebugFolder() {
627
+ const folder = this.gui.addFolder('Debug Options')
628
+ this.folders.debug = folder
629
+
630
+ // Add engine-level debug options
631
+ if (!this.engine._debugSettings) {
632
+ this.engine._debugSettings = {
633
+ showLights: false,
634
+ lightCrossSize: 10
635
+ }
636
+ }
637
+
638
+ folder.add(this.engine._debugSettings, 'showLights').name('Show Lights')
639
+ folder.add(this.engine._debugSettings, 'lightCrossSize', 5, 30, 1).name('Cross Size')
640
+ }
641
+
642
+ /**
643
+ * Show or hide the debug UI
644
+ */
645
+ setVisible(visible) {
646
+ if (visible && !this.initialized) {
647
+ this.init()
648
+ }
649
+
650
+ if (this.gui) {
651
+ this.gui.domElement.style.display = visible ? '' : 'none'
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Check if debug mode is on and update visibility
657
+ */
658
+ update() {
659
+ const debugMode = this.engine.settings?.rendering?.debug ?? false
660
+
661
+ if (debugMode && !this.initialized) {
662
+ this.init()
663
+ }
664
+
665
+ if (this.gui) {
666
+ this.gui.domElement.style.display = debugMode ? '' : 'none'
667
+ }
668
+
669
+ if (debugMode && this.initialized) {
670
+ this.updateStats()
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Destroy the debug UI
676
+ */
677
+ destroy() {
678
+ if (this.gui) {
679
+ this.gui.destroy()
680
+ this.gui = null
681
+ }
682
+ this.initialized = false
683
+ }
684
+
685
+ // Utility functions
686
+ _rgbToHex(rgb) {
687
+ const r = Math.round((rgb[0] || 0) * 255)
688
+ const g = Math.round((rgb[1] || 0) * 255)
689
+ const b = Math.round((rgb[2] || 0) * 255)
690
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
691
+ }
692
+
693
+ _hexToRgb(hex) {
694
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
695
+ return result ? {
696
+ r: parseInt(result[1], 16) / 255,
697
+ g: parseInt(result[2], 16) / 255,
698
+ b: parseInt(result[3], 16) / 255
699
+ } : { r: 1, g: 1, b: 1 }
700
+ }
701
+ }
702
+
703
+ export { DebugUI }