topazcube 0.1.33 → 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.
- package/dist/Renderer.cjs +2925 -281
- package/dist/Renderer.cjs.map +1 -1
- package/dist/Renderer.js +2925 -281
- package/dist/Renderer.js.map +1 -1
- package/package.json +1 -1
- package/src/renderer/DebugUI.js +176 -45
- package/src/renderer/Material.js +3 -0
- package/src/renderer/Pipeline.js +3 -1
- package/src/renderer/Renderer.js +185 -13
- package/src/renderer/core/AssetManager.js +40 -5
- package/src/renderer/core/CullingSystem.js +1 -0
- package/src/renderer/gltf.js +17 -0
- package/src/renderer/rendering/RenderGraph.js +224 -30
- package/src/renderer/rendering/passes/BloomPass.js +5 -2
- package/src/renderer/rendering/passes/CRTPass.js +724 -0
- package/src/renderer/rendering/passes/FogPass.js +26 -0
- package/src/renderer/rendering/passes/GBufferPass.js +31 -7
- package/src/renderer/rendering/passes/HiZPass.js +30 -0
- package/src/renderer/rendering/passes/LightingPass.js +14 -0
- package/src/renderer/rendering/passes/ParticlePass.js +10 -4
- package/src/renderer/rendering/passes/PostProcessPass.js +127 -4
- package/src/renderer/rendering/passes/SSGIPass.js +3 -2
- package/src/renderer/rendering/passes/SSGITilePass.js +14 -5
- package/src/renderer/rendering/passes/ShadowPass.js +265 -15
- package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
- package/src/renderer/rendering/shaders/crt.wgsl +455 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +36 -6
- package/src/renderer/rendering/shaders/particle_render.wgsl +153 -6
- package/src/renderer/rendering/shaders/postproc.wgsl +23 -2
- package/src/renderer/rendering/shaders/shadow.wgsl +42 -1
- package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
- package/src/renderer/utils/Raycaster.js +761 -0
package/package.json
CHANGED
package/src/renderer/DebugUI.js
CHANGED
|
@@ -42,19 +42,20 @@ class DebugUI {
|
|
|
42
42
|
|
|
43
43
|
// Create settings folders
|
|
44
44
|
this._createRenderingFolder()
|
|
45
|
-
this.
|
|
46
|
-
this._createShadowFolder()
|
|
47
|
-
this._createMainLightFolder()
|
|
45
|
+
this._createCameraFolder()
|
|
48
46
|
this._createEnvironmentFolder()
|
|
49
47
|
this._createLightingFolder()
|
|
50
|
-
this.
|
|
51
|
-
this.
|
|
52
|
-
this._createBloomFolder()
|
|
48
|
+
this._createMainLightFolder()
|
|
49
|
+
this._createShadowFolder()
|
|
53
50
|
this._createPlanarReflectionFolder()
|
|
51
|
+
this._createAOFolder()
|
|
54
52
|
this._createAmbientCaptureFolder()
|
|
55
|
-
this.
|
|
53
|
+
this._createSSGIFolder()
|
|
54
|
+
this._createVolumetricFogFolder()
|
|
55
|
+
this._createBloomFolder()
|
|
56
|
+
this._createTonemapFolder()
|
|
56
57
|
this._createDitheringFolder()
|
|
57
|
-
this.
|
|
58
|
+
this._createCRTFolder()
|
|
58
59
|
this._createDebugFolder()
|
|
59
60
|
|
|
60
61
|
// Close all folders by default
|
|
@@ -244,6 +245,42 @@ class DebugUI {
|
|
|
244
245
|
this.gui.domElement.style.display = 'none'
|
|
245
246
|
}
|
|
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
|
+
}
|
|
247
284
|
}
|
|
248
285
|
|
|
249
286
|
_createAOFolder() {
|
|
@@ -344,6 +381,9 @@ class DebugUI {
|
|
|
344
381
|
fogFolder.add(s.environment.fog.heightFade, '0', -100, 100, 1).name('Bottom Y')
|
|
345
382
|
fogFolder.add(s.environment.fog.heightFade, '1', -50, 200, 5).name('Top Y')
|
|
346
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')
|
|
347
387
|
}
|
|
348
388
|
}
|
|
349
389
|
|
|
@@ -364,31 +404,6 @@ class DebugUI {
|
|
|
364
404
|
}
|
|
365
405
|
}
|
|
366
406
|
|
|
367
|
-
_createCullingFolder() {
|
|
368
|
-
const s = this.engine.settings
|
|
369
|
-
if (!s.culling) return
|
|
370
|
-
|
|
371
|
-
const folder = this.gui.addFolder('Culling')
|
|
372
|
-
this.folders.culling = folder
|
|
373
|
-
|
|
374
|
-
folder.add(s.culling, 'frustumEnabled').name('Frustum Culling')
|
|
375
|
-
|
|
376
|
-
if (s.occlusionCulling) {
|
|
377
|
-
folder.add(s.occlusionCulling, 'enabled').name('Occlusion Culling')
|
|
378
|
-
folder.add(s.occlusionCulling, 'threshold', 0.1, 2.0, 0.1).name('Occlusion Threshold')
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Planar reflection culling sub-folder
|
|
382
|
-
if (s.culling.planarReflection) {
|
|
383
|
-
const prFolder = folder.addFolder('Planar Reflection')
|
|
384
|
-
prFolder.add(s.culling.planarReflection, 'frustum').name('Frustum Culling')
|
|
385
|
-
prFolder.add(s.culling.planarReflection, 'maxDistance', 10, 200, 10).name('Max Distance')
|
|
386
|
-
prFolder.add(s.culling.planarReflection, 'maxSkinned', 0, 100, 1).name('Max Skinned')
|
|
387
|
-
prFolder.add(s.culling.planarReflection, 'minPixelSize', 0, 16, 1).name('Min Pixel Size')
|
|
388
|
-
prFolder.close()
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
407
|
_createSSGIFolder() {
|
|
393
408
|
const s = this.engine.settings
|
|
394
409
|
if (!s.ssgi) return
|
|
@@ -404,6 +419,71 @@ class DebugUI {
|
|
|
404
419
|
folder.add(s.ssgi, 'saturateLevel', 0.1, 2.0, 0.1).name('Saturate Level')
|
|
405
420
|
}
|
|
406
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
|
+
|
|
407
487
|
_createBloomFolder() {
|
|
408
488
|
const s = this.engine.settings
|
|
409
489
|
if (!s.bloom) return
|
|
@@ -423,6 +503,19 @@ class DebugUI {
|
|
|
423
503
|
}
|
|
424
504
|
}
|
|
425
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
|
+
|
|
426
519
|
_createPlanarReflectionFolder() {
|
|
427
520
|
const s = this.engine.settings
|
|
428
521
|
if (!s.planarReflection) return
|
|
@@ -442,7 +535,7 @@ class DebugUI {
|
|
|
442
535
|
const s = this.engine.settings
|
|
443
536
|
if (!s.ambientCapture) return
|
|
444
537
|
|
|
445
|
-
const folder = this.gui.addFolder('Ambient Capture')
|
|
538
|
+
const folder = this.gui.addFolder('Probe GI (Ambient Capture)')
|
|
446
539
|
this.folders.ambientCapture = folder
|
|
447
540
|
|
|
448
541
|
folder.add(s.ambientCapture, 'enabled').name('Enabled')
|
|
@@ -452,17 +545,6 @@ class DebugUI {
|
|
|
452
545
|
folder.add(s.ambientCapture, 'saturateLevel', 0.0, 2.0, 0.05).name('Saturate Level')
|
|
453
546
|
}
|
|
454
547
|
|
|
455
|
-
_createNoiseFolder() {
|
|
456
|
-
const s = this.engine.settings
|
|
457
|
-
if (!s.noise) return
|
|
458
|
-
|
|
459
|
-
const folder = this.gui.addFolder('Noise')
|
|
460
|
-
this.folders.noise = folder
|
|
461
|
-
|
|
462
|
-
folder.add(s.noise, 'type', ['bluenoise', 'bayer8']).name('Type')
|
|
463
|
-
folder.add(s.noise, 'animated').name('Animated')
|
|
464
|
-
}
|
|
465
|
-
|
|
466
548
|
_createDitheringFolder() {
|
|
467
549
|
const s = this.engine.settings
|
|
468
550
|
if (!s.dithering) return
|
|
@@ -474,6 +556,55 @@ class DebugUI {
|
|
|
474
556
|
folder.add(s.dithering, 'colorLevels', 4, 256, 1).name('Color Levels')
|
|
475
557
|
}
|
|
476
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
|
+
|
|
477
608
|
_createCameraFolder() {
|
|
478
609
|
const s = this.engine.settings
|
|
479
610
|
if (!s.camera) return
|
package/src/renderer/Material.js
CHANGED
package/src/renderer/Pipeline.js
CHANGED
|
@@ -88,6 +88,7 @@ class Pipeline {
|
|
|
88
88
|
tileLightBuffer = null, // Optional tile light indices buffer for tiled lighting
|
|
89
89
|
lightBuffer = null, // Optional light storage buffer for tiled lighting
|
|
90
90
|
noiseTexture = null, // Optional noise texture for alpha hashing
|
|
91
|
+
doubleSided = false, // Optional: disable backface culling for double-sided materials
|
|
91
92
|
}) {
|
|
92
93
|
let texture = textures[0]
|
|
93
94
|
const { canvas, device, canvasFormat, options } = engine
|
|
@@ -154,7 +155,7 @@ class Pipeline {
|
|
|
154
155
|
geometry.vertexBufferLayout,
|
|
155
156
|
geometry.instanceBufferLayout
|
|
156
157
|
];
|
|
157
|
-
pipelineDescriptor.primitive.cullMode = "back";
|
|
158
|
+
pipelineDescriptor.primitive.cullMode = doubleSided ? "none" : "back";
|
|
158
159
|
pipelineDescriptor.depthStencil = {
|
|
159
160
|
depthWriteEnabled: true,
|
|
160
161
|
depthCompare: "less",
|
|
@@ -420,6 +421,7 @@ class Pipeline {
|
|
|
420
421
|
p.tileLightBuffer = tileLightBuffer
|
|
421
422
|
p.lightBuffer = lightBuffer
|
|
422
423
|
p.noiseTexture = noiseTexture
|
|
424
|
+
p.doubleSided = doubleSided
|
|
423
425
|
return p
|
|
424
426
|
}
|
|
425
427
|
|
package/src/renderer/Renderer.js
CHANGED
|
@@ -13,6 +13,7 @@ import { InstanceManager } from "./core/InstanceManager.js"
|
|
|
13
13
|
import { ParticleSystem } from "./core/ParticleSystem.js"
|
|
14
14
|
import { ParticleEmitter } from "./core/ParticleEmitter.js"
|
|
15
15
|
import { DebugUI } from "./DebugUI.js"
|
|
16
|
+
import { Raycaster } from "./utils/Raycaster.js"
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
// Display a failure message and stop rendering
|
|
@@ -77,7 +78,7 @@ const DEFAULT_SETTINGS = {
|
|
|
77
78
|
fxaa: false, // Fast approximate anti-aliasing
|
|
78
79
|
renderScale: 1, // Render resolution multiplier (1.5-2.0 for supersampling AA)
|
|
79
80
|
autoScale: {
|
|
80
|
-
enabled:
|
|
81
|
+
enabled: true, // Auto-reduce renderScale for high resolutions
|
|
81
82
|
enabledForEffects: true,// Auto scale effects at high resolutions (when main autoScale disabled)
|
|
82
83
|
maxHeight: 1536, // Height threshold (above this, scale is reduced)
|
|
83
84
|
scaleFactor: 0.5, // Factor to apply when above threshold
|
|
@@ -91,6 +92,7 @@ const DEFAULT_SETTINGS = {
|
|
|
91
92
|
alphaHash: false, // Enable alpha hashing/dithering for cutout transparency (global default)
|
|
92
93
|
alphaHashScale: 1.0, // Scale factor for alpha hash threshold (higher = more opaque)
|
|
93
94
|
luminanceToAlpha: false, // Derive alpha from color luminance (for old game assets where black=transparent)
|
|
95
|
+
tonemapMode: 0, // 0=ACES, 1=Reinhard, 2=None (linear clamp)
|
|
94
96
|
},
|
|
95
97
|
|
|
96
98
|
// Noise settings for dithering, jittering, etc.
|
|
@@ -128,11 +130,12 @@ const DEFAULT_SETTINGS = {
|
|
|
128
130
|
exposure: 1.6,
|
|
129
131
|
fog: {
|
|
130
132
|
enabled: true,
|
|
131
|
-
color: [0
|
|
132
|
-
distances: [
|
|
133
|
+
color: [100/255.0, 135/255.0, 170/255.0],
|
|
134
|
+
distances: [0, 15, 50],
|
|
133
135
|
alpha: [0.0, 0.5, 0.9],
|
|
134
136
|
heightFade: [-2, 185], // [bottomY, topY] - full fog at bottomY, zero at topY
|
|
135
|
-
brightResist: 0.
|
|
137
|
+
brightResist: 0.0, // How much bright/emissive colors resist fog (0-1)
|
|
138
|
+
debug: 0,
|
|
136
139
|
}
|
|
137
140
|
},
|
|
138
141
|
|
|
@@ -158,6 +161,8 @@ const DEFAULT_SETTINGS = {
|
|
|
158
161
|
normalBias: 0.015, // ~2-3 texels for shadow acne
|
|
159
162
|
surfaceBias: 0, // Scale shadow projection larger (0.01 = 1% larger)
|
|
160
163
|
strength: 1.0,
|
|
164
|
+
//frustum: false,
|
|
165
|
+
//hiZ: false,
|
|
161
166
|
},
|
|
162
167
|
|
|
163
168
|
// Ambient Occlusion settings
|
|
@@ -242,6 +247,33 @@ const DEFAULT_SETTINGS = {
|
|
|
242
247
|
saturateLevel: 0.5, // Logarithmic saturation level for indirect light
|
|
243
248
|
},
|
|
244
249
|
|
|
250
|
+
// Volumetric Fog (light scattering through particles)
|
|
251
|
+
volumetricFog: {
|
|
252
|
+
enabled: false, // Disabled by default (performance impact)
|
|
253
|
+
resolution: 0.125, // 1/4 render resolution for ray marching
|
|
254
|
+
maxSamples: 32, // Ray march samples (8-32)
|
|
255
|
+
blurRadius: 8.0, // Gaussian blur radius
|
|
256
|
+
densityMultiplier: 1.0, // Multiplies base fog density
|
|
257
|
+
scatterStrength: 0.35, // Light scattering intensity
|
|
258
|
+
mainLightScatter: 1.4, // Main directional light scattering boost
|
|
259
|
+
mainLightScatterDark: 5.0, // Main directional light scattering boost
|
|
260
|
+
mainLightSaturation: 0.15, // Main light color saturation in fog
|
|
261
|
+
maxFogOpacity: 0.3, // Maximum fog opacity (0-1)
|
|
262
|
+
heightRange: [-2, 8], // [bottom, top] Y bounds for fog (low ground fog)
|
|
263
|
+
windDirection: [1, 0, 0.2], // Wind direction for fog animation
|
|
264
|
+
windSpeed: 0.5, // Wind speed multiplier
|
|
265
|
+
noiseScale: 0.9, // 3D noise frequency (higher = finer detail)
|
|
266
|
+
noiseStrength: 0.8, // Noise intensity (0 = uniform, 1 = full variation)
|
|
267
|
+
noiseOctaves: 6, // Noise detail layers
|
|
268
|
+
noiseEnabled: true, // Enable 3D noise (disable for debug)
|
|
269
|
+
lightingEnabled: true, // Light fog from scene lights
|
|
270
|
+
shadowsEnabled: true, // Apply shadows to fog
|
|
271
|
+
brightnessThreshold: 0.8, // Scene luminance where fog starts fading (like bloom)
|
|
272
|
+
minVisibility: 0.15, // Minimum fog visibility over bright surfaces (0-1)
|
|
273
|
+
skyBrightness: 1.2, // Virtual brightness for sky pixels (depth at far plane)
|
|
274
|
+
//debugSkyCheck: true
|
|
275
|
+
},
|
|
276
|
+
|
|
245
277
|
// Planar Reflections (alternative to SSR for water/floor)
|
|
246
278
|
planarReflection: {
|
|
247
279
|
enabled: true, // Disabled by default (use SSR instead)
|
|
@@ -279,6 +311,40 @@ const DEFAULT_SETTINGS = {
|
|
|
279
311
|
fpsThreshold: 60, // FPS threshold for auto-disable
|
|
280
312
|
disableDelay: 3.0, // Seconds below threshold before disabling
|
|
281
313
|
},
|
|
314
|
+
|
|
315
|
+
// CRT effect (retro monitor simulation)
|
|
316
|
+
crt: {
|
|
317
|
+
enabled: false, // Enable CRT effect (geometry, scanlines, etc.)
|
|
318
|
+
upscaleEnabled: false, // Enable upscaling (pixelated look) even when CRT disabled
|
|
319
|
+
upscaleTarget: 4, // Target upscale multiplier (4x render resolution)
|
|
320
|
+
maxTextureSize: 4096, // Max upscaled texture dimension
|
|
321
|
+
|
|
322
|
+
// Geometry distortion
|
|
323
|
+
curvature: 0.14, // Screen curvature amount (0-0.15)
|
|
324
|
+
cornerRadius: 0.055, // Rounded corner radius (0-0.1)
|
|
325
|
+
zoom: 1.06, // Zoom to compensate for curvature shrinkage
|
|
326
|
+
|
|
327
|
+
// Scanlines (electron beam simulation - Gaussian profile)
|
|
328
|
+
scanlineIntensity: 0.4, // Scanline effect strength (0-1)
|
|
329
|
+
scanlineWidth: 0.0, // Beam width (0=thin/center only, 1=no gap)
|
|
330
|
+
scanlineBrightBoost: 0.8, // Bright pixels widen beam to fill gaps (0-1)
|
|
331
|
+
scanlineHeight: 5, // Scanline height in canvas pixels
|
|
332
|
+
|
|
333
|
+
// RGB convergence error (color channel misalignment)
|
|
334
|
+
convergence: [0.79, 0.0, -0.77], // RGB X offset in source pixels
|
|
335
|
+
|
|
336
|
+
// Phosphor mask
|
|
337
|
+
maskType: 'aperture', // 'aperture', 'slot', 'shadow', 'none'
|
|
338
|
+
maskIntensity: 0.25, // Mask strength (0-1)
|
|
339
|
+
maskScale: 1.0, // Mask size multiplier
|
|
340
|
+
|
|
341
|
+
// Vignette (edge darkening)
|
|
342
|
+
vignetteIntensity: 0.54, // Edge darkening strength (0-1)
|
|
343
|
+
vignetteSize: 0.85, // Vignette size (larger = more visible)
|
|
344
|
+
|
|
345
|
+
// Horizontal blur (beam softness)
|
|
346
|
+
blurSize: 0.79, // Horizontal blur in pixels (0-2)
|
|
347
|
+
},
|
|
282
348
|
}
|
|
283
349
|
|
|
284
350
|
// Function to create WebGPU context
|
|
@@ -377,10 +443,27 @@ async function createWebGPUContext(engine, canvasId) {
|
|
|
377
443
|
engine.rendering = true
|
|
378
444
|
|
|
379
445
|
function configureContext() {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
446
|
+
// Use exact device pixel size if available (from ResizeObserver)
|
|
447
|
+
// This ensures pixel-perfect rendering for CRT effects
|
|
448
|
+
let pixelWidth, pixelHeight
|
|
449
|
+
if (engine._devicePixelSize) {
|
|
450
|
+
pixelWidth = engine._devicePixelSize.width
|
|
451
|
+
pixelHeight = engine._devicePixelSize.height
|
|
452
|
+
} else {
|
|
453
|
+
// Fallback to clientWidth * devicePixelRatio
|
|
454
|
+
const devicePixelRatio = window.devicePixelRatio || 1
|
|
455
|
+
pixelWidth = Math.round(canvas.clientWidth * devicePixelRatio)
|
|
456
|
+
pixelHeight = Math.round(canvas.clientHeight * devicePixelRatio)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Canvas is ALWAYS at full device pixel resolution for pixel-perfect CRT
|
|
460
|
+
// Render scale only affects internal render passes, not the final canvas
|
|
461
|
+
canvas.width = pixelWidth
|
|
462
|
+
canvas.height = pixelHeight
|
|
463
|
+
|
|
464
|
+
// Store device pixel size for CRT pass
|
|
465
|
+
engine._canvasPixelSize = { width: pixelWidth, height: pixelHeight }
|
|
466
|
+
|
|
384
467
|
context.configure({
|
|
385
468
|
device: device,
|
|
386
469
|
format: canvasFormat,
|
|
@@ -507,9 +590,40 @@ class Engine {
|
|
|
507
590
|
this.stats.avg_dt_render = 0.1
|
|
508
591
|
|
|
509
592
|
requestAnimationFrame(() => this._frame())
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
593
|
+
|
|
594
|
+
// Use ResizeObserver with devicePixelContentBoxSize for pixel-perfect sizing
|
|
595
|
+
this._devicePixelSize = null
|
|
596
|
+
try {
|
|
597
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
598
|
+
for (const entry of entries) {
|
|
599
|
+
// Prefer devicePixelContentBoxSize for exact device pixels
|
|
600
|
+
if (entry.devicePixelContentBoxSize) {
|
|
601
|
+
const size = entry.devicePixelContentBoxSize[0]
|
|
602
|
+
this._devicePixelSize = {
|
|
603
|
+
width: size.inlineSize,
|
|
604
|
+
height: size.blockSize
|
|
605
|
+
}
|
|
606
|
+
} else if (entry.contentBoxSize) {
|
|
607
|
+
// Fallback to contentBoxSize * devicePixelRatio
|
|
608
|
+
const size = entry.contentBoxSize[0]
|
|
609
|
+
const dpr = window.devicePixelRatio || 1
|
|
610
|
+
this._devicePixelSize = {
|
|
611
|
+
width: Math.round(size.inlineSize * dpr),
|
|
612
|
+
height: Math.round(size.blockSize * dpr)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
this.needsResize = true
|
|
616
|
+
}
|
|
617
|
+
})
|
|
618
|
+
resizeObserver.observe(this.canvas, { box: 'device-pixel-content-box' })
|
|
619
|
+
} catch (e) {
|
|
620
|
+
// Fallback if device-pixel-content-box not supported
|
|
621
|
+
console.log('ResizeObserver device-pixel-content-box not supported, falling back to window resize')
|
|
622
|
+
window.addEventListener("resize", () => {
|
|
623
|
+
this.needsResize = true
|
|
624
|
+
})
|
|
625
|
+
}
|
|
626
|
+
|
|
513
627
|
setInterval(() => {
|
|
514
628
|
if (this.needsResize && !this._resizing) {
|
|
515
629
|
this.needsResize = false
|
|
@@ -652,12 +766,22 @@ class Engine {
|
|
|
652
766
|
* @param {Array} options.position - Optional position offset [x, y, z]
|
|
653
767
|
* @param {Array} options.rotation - Optional rotation offset [x, y, z] in radians
|
|
654
768
|
* @param {number} options.scale - Optional uniform scale multiplier
|
|
769
|
+
* @param {boolean} options.doubleSided - Optional: force all materials to be double-sided
|
|
655
770
|
* @returns {Promise<Object>} Object containing { meshes, nodes, skins, animations }
|
|
656
771
|
*/
|
|
657
772
|
async loadScene(url, options = {}) {
|
|
658
773
|
const result = await loadGltf(this, url, options)
|
|
659
774
|
const { meshes, nodes } = result
|
|
660
775
|
|
|
776
|
+
// Apply scene-wide doubleSided option if specified
|
|
777
|
+
if (options.doubleSided) {
|
|
778
|
+
for (const mesh of Object.values(meshes)) {
|
|
779
|
+
if (mesh.material) {
|
|
780
|
+
mesh.material.doubleSided = true
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
661
785
|
// Update node world matrices from their hierarchy
|
|
662
786
|
// This handles Blender's Z-up to Y-up rotation in parent nodes
|
|
663
787
|
for (const node of nodes) {
|
|
@@ -685,6 +809,30 @@ class Engine {
|
|
|
685
809
|
)
|
|
686
810
|
}
|
|
687
811
|
|
|
812
|
+
// For skinned models with multiple submeshes, compute a combined bounding sphere
|
|
813
|
+
// This ensures all submeshes are culled together as a unit (especially for shadows)
|
|
814
|
+
let combinedBsphere = null
|
|
815
|
+
const hasAnySkin = Object.values(meshes).some(m => m.hasSkin)
|
|
816
|
+
|
|
817
|
+
if (hasAnySkin) {
|
|
818
|
+
// Collect all vertex positions from ALL meshes
|
|
819
|
+
const allPositions = []
|
|
820
|
+
for (const mesh of Object.values(meshes)) {
|
|
821
|
+
const positions = mesh.geometry?.attributes?.position
|
|
822
|
+
if (positions) {
|
|
823
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
824
|
+
allPositions.push(positions[i], positions[i + 1], positions[i + 2])
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (allPositions.length > 0) {
|
|
830
|
+
// Calculate combined bounding sphere
|
|
831
|
+
const { calculateBoundingSphere } = await import('./utils/BoundingSphere.js')
|
|
832
|
+
combinedBsphere = calculateBoundingSphere(new Float32Array(allPositions))
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
688
836
|
// For each mesh, find its node and compute world transform
|
|
689
837
|
for (const [name, mesh] of Object.entries(meshes)) {
|
|
690
838
|
// Find the node that references this mesh by nodeIndex
|
|
@@ -705,7 +853,8 @@ class Engine {
|
|
|
705
853
|
}
|
|
706
854
|
|
|
707
855
|
// Compute world bounding sphere from geometry bsphere + world transform
|
|
708
|
-
|
|
856
|
+
// For skinned models, use the combined bsphere so all submeshes are culled together
|
|
857
|
+
const localBsphere = (hasAnySkin && combinedBsphere) ? combinedBsphere : mesh.geometry.getBoundingSphere?.()
|
|
709
858
|
let worldCenter = [0, 0, 0]
|
|
710
859
|
let worldRadius = 1
|
|
711
860
|
|
|
@@ -724,6 +873,11 @@ class Engine {
|
|
|
724
873
|
worldRadius = localBsphere.radius * Math.max(scaleX, scaleY, scaleZ)
|
|
725
874
|
}
|
|
726
875
|
|
|
876
|
+
// Store combined bsphere on mesh for shadow pass culling
|
|
877
|
+
if (hasAnySkin && combinedBsphere) {
|
|
878
|
+
mesh.combinedBsphere = combinedBsphere
|
|
879
|
+
}
|
|
880
|
+
|
|
727
881
|
// Add instance with world bounding sphere
|
|
728
882
|
mesh.addInstance(worldCenter, worldRadius)
|
|
729
883
|
mesh.updateInstance(0, worldMatrix)
|
|
@@ -816,6 +970,17 @@ class Engine {
|
|
|
816
970
|
return this.entityManager.get(id)
|
|
817
971
|
}
|
|
818
972
|
|
|
973
|
+
/**
|
|
974
|
+
* Invalidate occlusion culling data and reset warmup period.
|
|
975
|
+
* Call this after scene loading or major camera teleportation to prevent
|
|
976
|
+
* incorrect occlusion culling with stale depth buffer data.
|
|
977
|
+
*/
|
|
978
|
+
invalidateOcclusionCulling() {
|
|
979
|
+
if (this.renderer) {
|
|
980
|
+
this.renderer.invalidateOcclusionCulling()
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
819
984
|
async _create() {
|
|
820
985
|
let camera = new Camera(this) // Pass engine reference
|
|
821
986
|
camera.updateMatrix()
|
|
@@ -848,6 +1013,10 @@ class Engine {
|
|
|
848
1013
|
|
|
849
1014
|
async _after_create() {
|
|
850
1015
|
this.renderer = await RenderGraph.create(this, this.environment, this.environmentEncoding)
|
|
1016
|
+
|
|
1017
|
+
// Initialize raycaster for async ray intersection tests
|
|
1018
|
+
this.raycaster = new Raycaster(this)
|
|
1019
|
+
await this.raycaster.initialize()
|
|
851
1020
|
}
|
|
852
1021
|
|
|
853
1022
|
_update(dt) {
|
|
@@ -971,7 +1140,9 @@ class Engine {
|
|
|
971
1140
|
this.guiCtx.clearRect(0, 0, canvas.width, canvas.height)
|
|
972
1141
|
}
|
|
973
1142
|
|
|
974
|
-
|
|
1143
|
+
// Pass render scale to RenderGraph - internal passes use scaled dimensions
|
|
1144
|
+
// CRT pass will still output at full canvas resolution
|
|
1145
|
+
await this.renderer.resize(canvas.width, canvas.height, this.renderScale)
|
|
975
1146
|
this.resize()
|
|
976
1147
|
|
|
977
1148
|
// Small delay before allowing renders to ensure all GPU resources are ready
|
|
@@ -1321,4 +1492,5 @@ export {
|
|
|
1321
1492
|
ParticleSystem,
|
|
1322
1493
|
ParticleEmitter,
|
|
1323
1494
|
DebugUI,
|
|
1495
|
+
Raycaster,
|
|
1324
1496
|
}
|