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
|
@@ -37,6 +37,7 @@ class FogPass extends BasePass {
|
|
|
37
37
|
get fogAlpha() { return this.settings?.environment?.fog?.alpha ?? [0.0, 0.3, 0.8] }
|
|
38
38
|
get fogHeightFade() { return this.settings?.environment?.fog?.heightFade ?? [-10, 100] }
|
|
39
39
|
get fogBrightResist() { return this.settings?.environment?.fog?.brightResist ?? 0.8 }
|
|
40
|
+
get fogDebug() { return this.settings?.environment?.fog?.debug ?? 0 } // 0=off, 1=show fogAlpha, 2=show distance, 3=show heightFactor
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Set the input texture (HDR lighting output)
|
|
@@ -134,6 +135,8 @@ class FogPass extends BasePass {
|
|
|
134
135
|
brightResist: f32, // float 47
|
|
135
136
|
heightFade: vec2f, // floats 48-49
|
|
136
137
|
screenSize: vec2f, // floats 50-51
|
|
138
|
+
debug: f32, // float 52: 0=off, 1=fogAlpha, 2=distance, 3=heightFactor
|
|
139
|
+
_pad: vec3f, // floats 53-55
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
@@ -262,6 +265,26 @@ class FogPass extends BasePass {
|
|
|
262
265
|
let brightnessResist = clamp((luminance - 1.0) / 2.0, 0.0, 1.0);
|
|
263
266
|
fogAlpha *= (1.0 - brightnessResist * uniforms.brightResist);
|
|
264
267
|
|
|
268
|
+
// Debug output
|
|
269
|
+
let debugMode = i32(uniforms.debug);
|
|
270
|
+
if (debugMode == 1) {
|
|
271
|
+
// Show fog alpha as grayscale
|
|
272
|
+
return vec4f(vec3f(fogAlpha), 1.0);
|
|
273
|
+
} else if (debugMode == 2) {
|
|
274
|
+
// Show distance (normalized to 0-100m range)
|
|
275
|
+
let normDist = clamp(cameraDistance / 100.0, 0.0, 1.0);
|
|
276
|
+
return vec4f(vec3f(normDist), 1.0);
|
|
277
|
+
} else if (debugMode == 3) {
|
|
278
|
+
// Show height factor
|
|
279
|
+
return vec4f(vec3f(heightFactor), 1.0);
|
|
280
|
+
} else if (debugMode == 4) {
|
|
281
|
+
// Show distance fog (before height fade)
|
|
282
|
+
return vec4f(vec3f(distanceFog), 1.0);
|
|
283
|
+
} else if (debugMode == 5) {
|
|
284
|
+
// Show the actual fog color being used (to verify it matches particles)
|
|
285
|
+
return vec4f(uniforms.fogColor, 1.0);
|
|
286
|
+
}
|
|
287
|
+
|
|
265
288
|
// Apply fog
|
|
266
289
|
let foggedColor = mix(color.rgb, uniforms.fogColor, fogAlpha);
|
|
267
290
|
|
|
@@ -363,6 +386,9 @@ class FogPass extends BasePass {
|
|
|
363
386
|
uniformData[50] = this.width
|
|
364
387
|
uniformData[51] = this.height
|
|
365
388
|
|
|
389
|
+
// debug (float 52) + padding (floats 53-55)
|
|
390
|
+
uniformData[52] = this.fogDebug
|
|
391
|
+
|
|
366
392
|
device.queue.writeBuffer(this.uniformBuffer, 0, uniformData)
|
|
367
393
|
|
|
368
394
|
// Render fog pass
|
|
@@ -293,7 +293,8 @@ class GBufferPass extends BasePass {
|
|
|
293
293
|
const isSkinned = mesh.hasSkin && mesh.skin
|
|
294
294
|
const meshId = mesh.uid || mesh.geometry?.uid || 'default'
|
|
295
295
|
const forceEmissive = mesh.material?.forceEmissive ? '_emissive' : ''
|
|
296
|
-
|
|
296
|
+
const doubleSided = mesh.material?.doubleSided ? '_dbl' : ''
|
|
297
|
+
return `${mesh.material.uid}_${meshId}${isSkinned ? '_skinned' : ''}${forceEmissive}${doubleSided}`
|
|
297
298
|
}
|
|
298
299
|
|
|
299
300
|
/**
|
|
@@ -341,6 +342,7 @@ class GBufferPass extends BasePass {
|
|
|
341
342
|
renderTarget: this.gbuffer,
|
|
342
343
|
skin: isSkinned ? mesh.skin : null,
|
|
343
344
|
noiseTexture: this.noiseTexture,
|
|
345
|
+
doubleSided: mesh.material?.doubleSided ?? false,
|
|
344
346
|
}).then(pipeline => {
|
|
345
347
|
// Move from pending to ready
|
|
346
348
|
this.pendingPipelines.delete(key)
|
|
@@ -386,6 +388,7 @@ class GBufferPass extends BasePass {
|
|
|
386
388
|
renderTarget: this.gbuffer,
|
|
387
389
|
skin: isSkinned ? mesh.skin : null,
|
|
388
390
|
noiseTexture: this.noiseTexture,
|
|
391
|
+
doubleSided: mesh.material?.doubleSided ?? false,
|
|
389
392
|
})
|
|
390
393
|
// Mark as warming up - needs 2 frames to stabilize
|
|
391
394
|
pipeline._warmupFrames = 2
|
|
@@ -418,6 +421,11 @@ class GBufferPass extends BasePass {
|
|
|
418
421
|
const emissionFactor = this.settings?.environment?.emissionFactor ?? [1.0, 1.0, 1.0, 4.0]
|
|
419
422
|
const mipBias = this.settings?.rendering?.mipBias ?? options.mipBias ?? 0
|
|
420
423
|
|
|
424
|
+
// Use absolute time for scene-loaded skins (same as entity animations)
|
|
425
|
+
// This ensures consistent timing regardless of frame rate fluctuations
|
|
426
|
+
const animationSpeed = this.settings?.animation?.speed ?? 1.0
|
|
427
|
+
const globalAnimTime = (performance.now() / 1000) * animationSpeed
|
|
428
|
+
|
|
421
429
|
stats.drawCalls = 0
|
|
422
430
|
stats.triangles = 0
|
|
423
431
|
|
|
@@ -432,15 +440,25 @@ class GBufferPass extends BasePass {
|
|
|
432
440
|
let commandEncoder = null
|
|
433
441
|
let passEncoder = null
|
|
434
442
|
|
|
443
|
+
// Track which skins have been updated this frame (avoids duplicate updates)
|
|
444
|
+
const updatedSkins = new Set()
|
|
445
|
+
|
|
435
446
|
// New system: render batches from InstanceManager
|
|
436
447
|
if (batches && batches.size > 0) {
|
|
437
448
|
for (const [modelId, batch] of batches) {
|
|
438
449
|
const mesh = batch.mesh
|
|
439
450
|
if (!mesh) continue
|
|
440
451
|
|
|
441
|
-
// Update skin animation if skinned (skip if externally managed)
|
|
442
|
-
|
|
443
|
-
|
|
452
|
+
// Update skin animation if skinned (skip if externally managed or already updated)
|
|
453
|
+
// Use absolute time (same as entities) for consistent animation speed
|
|
454
|
+
if (batch.hasSkin && batch.skin && !batch.skin.externallyManaged && !updatedSkins.has(batch.skin)) {
|
|
455
|
+
// Track animation start time per skin for absolute timing
|
|
456
|
+
if (batch.skin._animStartTime === undefined) {
|
|
457
|
+
batch.skin._animStartTime = globalAnimTime
|
|
458
|
+
}
|
|
459
|
+
const skinAnimTime = globalAnimTime - batch.skin._animStartTime
|
|
460
|
+
batch.skin.updateAtTime(skinAnimTime)
|
|
461
|
+
updatedSkins.add(batch.skin)
|
|
444
462
|
}
|
|
445
463
|
|
|
446
464
|
const pipeline = await this._getOrCreatePipeline(mesh)
|
|
@@ -598,9 +616,15 @@ class GBufferPass extends BasePass {
|
|
|
598
616
|
|
|
599
617
|
this.legacyCullingStats.rendered++
|
|
600
618
|
|
|
601
|
-
// Update skin animation (skip if externally managed
|
|
602
|
-
if (mesh.skin && mesh.hasSkin && !mesh.skin.externallyManaged) {
|
|
603
|
-
|
|
619
|
+
// Update skin animation using absolute time (skip if externally managed or already updated)
|
|
620
|
+
if (mesh.skin && mesh.hasSkin && !mesh.skin.externallyManaged && !updatedSkins.has(mesh.skin)) {
|
|
621
|
+
// Track animation start time per skin for absolute timing
|
|
622
|
+
if (mesh.skin._animStartTime === undefined) {
|
|
623
|
+
mesh.skin._animStartTime = globalAnimTime
|
|
624
|
+
}
|
|
625
|
+
const skinAnimTime = globalAnimTime - mesh.skin._animStartTime
|
|
626
|
+
mesh.skin.updateAtTime(skinAnimTime)
|
|
627
|
+
updatedSkins.add(mesh.skin)
|
|
604
628
|
}
|
|
605
629
|
|
|
606
630
|
// Ensure pipeline geometry matches mesh geometry
|
|
@@ -84,6 +84,10 @@ class HiZPass extends BasePass {
|
|
|
84
84
|
|
|
85
85
|
// Flag to prevent operations during destruction
|
|
86
86
|
this._destroyed = false
|
|
87
|
+
|
|
88
|
+
// Warmup frames - occlusion is disabled until scene has rendered for a few frames
|
|
89
|
+
// This prevents false occlusion on engine creation when depth buffer is not yet populated
|
|
90
|
+
this._warmupFramesRemaining = 5
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
/**
|
|
@@ -94,6 +98,20 @@ class HiZPass extends BasePass {
|
|
|
94
98
|
this.depthTexture = depth
|
|
95
99
|
}
|
|
96
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Invalidate occlusion culling data and reset warmup period.
|
|
103
|
+
* Call this after engine creation, scene loading, or major camera changes
|
|
104
|
+
* to prevent incorrect occlusion culling with stale data.
|
|
105
|
+
*/
|
|
106
|
+
invalidate() {
|
|
107
|
+
this.hasValidHistory = false
|
|
108
|
+
this.hizDataReady = false
|
|
109
|
+
this._warmupFramesRemaining = 5 // Wait 5 frames before enabling occlusion
|
|
110
|
+
// Reset camera tracking to avoid false invalidations
|
|
111
|
+
vec3.set(this.lastCameraPosition, 0, 0, 0)
|
|
112
|
+
vec3.set(this.lastCameraDirection, 0, 0, 0)
|
|
113
|
+
}
|
|
114
|
+
|
|
97
115
|
async _init() {
|
|
98
116
|
const { device, canvas } = this.engine
|
|
99
117
|
await this._createResources(canvas.width, canvas.height)
|
|
@@ -207,6 +225,7 @@ class HiZPass extends BasePass {
|
|
|
207
225
|
this._destroyed = false
|
|
208
226
|
this.hizDataReady = false
|
|
209
227
|
this.pendingReadback = null
|
|
228
|
+
this._warmupFramesRemaining = 5 // Wait a few frames after resize before enabling occlusion
|
|
210
229
|
}
|
|
211
230
|
|
|
212
231
|
/**
|
|
@@ -263,6 +282,11 @@ class HiZPass extends BasePass {
|
|
|
263
282
|
// Increment frame counter
|
|
264
283
|
this._frameCounter++
|
|
265
284
|
|
|
285
|
+
// Decrement warmup counter - occlusion is disabled until this reaches 0
|
|
286
|
+
if (this._warmupFramesRemaining > 0) {
|
|
287
|
+
this._warmupFramesRemaining--
|
|
288
|
+
}
|
|
289
|
+
|
|
266
290
|
// Check if occlusion culling is enabled
|
|
267
291
|
if (!this.settings?.occlusionCulling?.enabled) {
|
|
268
292
|
return
|
|
@@ -468,6 +492,12 @@ class HiZPass extends BasePass {
|
|
|
468
492
|
testSphereOcclusion(bsphere, viewProj, near, far, cameraPos) {
|
|
469
493
|
this.debugStats.tested++
|
|
470
494
|
|
|
495
|
+
// Warmup period - disable occlusion for first few frames after creation/reset
|
|
496
|
+
// This ensures the depth buffer has valid geometry before we use it for culling
|
|
497
|
+
if (this._warmupFramesRemaining > 0) {
|
|
498
|
+
return false // Still warming up - assume visible
|
|
499
|
+
}
|
|
500
|
+
|
|
471
501
|
// Safety checks
|
|
472
502
|
if (!this.hizDataReady || !this.hasValidHistory) {
|
|
473
503
|
return false // No valid data - assume visible
|
|
@@ -734,6 +734,20 @@ class LightingPass extends BasePass {
|
|
|
734
734
|
getOutputTexture() {
|
|
735
735
|
return this.outputTexture
|
|
736
736
|
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Get the light buffer for volumetric fog
|
|
740
|
+
*/
|
|
741
|
+
getLightBuffer() {
|
|
742
|
+
return this.lightBuffer
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Get the current light count
|
|
747
|
+
*/
|
|
748
|
+
getLightCount() {
|
|
749
|
+
return this.lights?.length ?? 0
|
|
750
|
+
}
|
|
737
751
|
}
|
|
738
752
|
|
|
739
753
|
export { LightingPass }
|
|
@@ -718,10 +718,10 @@ class ParticlePass extends BasePass {
|
|
|
718
718
|
uniformData[85] = fogAlphas[1]
|
|
719
719
|
uniformData[86] = fogAlphas[2]
|
|
720
720
|
uniformData[87] = 0.0 // padding
|
|
721
|
-
// fogHeightFade (vec2f) + fogPad2 (
|
|
721
|
+
// fogHeightFade (vec2f) + fogDebug (f32) + fogPad2 (f32) - floats 88-91
|
|
722
722
|
uniformData[88] = fogHeightFade[0] // bottomY
|
|
723
723
|
uniformData[89] = fogHeightFade[1] // topY
|
|
724
|
-
uniformData[90] =
|
|
724
|
+
uniformData[90] = fogSettings.debug ?? 0 // fogDebug (0=off, 2=show distance)
|
|
725
725
|
uniformData[91] = 0.0 // padding
|
|
726
726
|
|
|
727
727
|
// Update emitter render settings buffer (lit, emissive, softness, zOffset per emitter)
|
|
@@ -774,11 +774,14 @@ class ParticlePass extends BasePass {
|
|
|
774
774
|
const hasAdditive = emitters.some(e => e.blendMode === 'additive')
|
|
775
775
|
|
|
776
776
|
// Draw alpha-blended particles first (back-to-front would be ideal, but we draw all)
|
|
777
|
+
// NOTE: Must submit each pass separately because writeBuffer is immediate
|
|
778
|
+
// but render passes execute when command buffer is submitted
|
|
777
779
|
if (hasAlpha) {
|
|
778
780
|
uniformData[48] = 0.0 // blendMode = alpha
|
|
779
781
|
device.queue.writeBuffer(this.renderUniformBuffer, 0, uniformData)
|
|
780
782
|
|
|
781
|
-
const
|
|
783
|
+
const alphaEncoder = device.createCommandEncoder({ label: 'Particle Alpha Pass' })
|
|
784
|
+
const renderPass = alphaEncoder.beginRenderPass({
|
|
782
785
|
colorAttachments: [{
|
|
783
786
|
view: this.outputTexture.view,
|
|
784
787
|
loadOp: 'load',
|
|
@@ -795,6 +798,7 @@ class ParticlePass extends BasePass {
|
|
|
795
798
|
renderPass.setBindGroup(0, renderBindGroup)
|
|
796
799
|
renderPass.draw(6, maxParticles, 0, 0)
|
|
797
800
|
renderPass.end()
|
|
801
|
+
device.queue.submit([alphaEncoder.finish()])
|
|
798
802
|
}
|
|
799
803
|
|
|
800
804
|
// Draw additive particles (order doesn't matter for additive)
|
|
@@ -802,7 +806,8 @@ class ParticlePass extends BasePass {
|
|
|
802
806
|
uniformData[48] = 1.0 // blendMode = additive
|
|
803
807
|
device.queue.writeBuffer(this.renderUniformBuffer, 0, uniformData)
|
|
804
808
|
|
|
805
|
-
const
|
|
809
|
+
const additiveEncoder = device.createCommandEncoder({ label: 'Particle Additive Pass' })
|
|
810
|
+
const renderPass = additiveEncoder.beginRenderPass({
|
|
806
811
|
colorAttachments: [{
|
|
807
812
|
view: this.outputTexture.view,
|
|
808
813
|
loadOp: 'load',
|
|
@@ -819,6 +824,7 @@ class ParticlePass extends BasePass {
|
|
|
819
824
|
renderPass.setBindGroup(0, renderBindGroup)
|
|
820
825
|
renderPass.draw(6, maxParticles, 0, 0)
|
|
821
826
|
renderPass.end()
|
|
827
|
+
device.queue.submit([additiveEncoder.finish()])
|
|
822
828
|
}
|
|
823
829
|
}
|
|
824
830
|
|
|
@@ -26,6 +26,16 @@ class PostProcessPass extends BasePass {
|
|
|
26
26
|
this.guiCanvas = null // 2D canvas for GUI overlay
|
|
27
27
|
this.guiTexture = null // GPU texture for GUI
|
|
28
28
|
this.guiSampler = null
|
|
29
|
+
|
|
30
|
+
// CRT support: intermediate texture when CRT is enabled
|
|
31
|
+
this.intermediateTexture = null
|
|
32
|
+
this._outputWidth = 0
|
|
33
|
+
this._outputHeight = 0
|
|
34
|
+
this._lastOutputToTexture = false // Track output mode changes
|
|
35
|
+
|
|
36
|
+
// Store resize dimensions for runtime texture creation
|
|
37
|
+
this._resizeWidth = 0
|
|
38
|
+
this._resizeHeight = 0
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
// Convenience getter for exposure setting
|
|
@@ -38,11 +48,19 @@ class PostProcessPass extends BasePass {
|
|
|
38
48
|
get ditheringEnabled() { return this.settings?.dithering?.enabled ?? true }
|
|
39
49
|
get colorLevels() { return this.settings?.dithering?.colorLevels ?? 32 }
|
|
40
50
|
|
|
51
|
+
// Tonemap mode: 0=ACES, 1=Reinhard, 2=None/Linear
|
|
52
|
+
get tonemapMode() { return this.settings?.rendering?.tonemapMode ?? 0 }
|
|
53
|
+
|
|
41
54
|
// Convenience getters for bloom settings
|
|
42
55
|
get bloomEnabled() { return this.settings?.bloom?.enabled ?? true }
|
|
43
56
|
get bloomIntensity() { return this.settings?.bloom?.intensity ?? 1.0 }
|
|
44
57
|
get bloomRadius() { return this.settings?.bloom?.radius ?? 5 }
|
|
45
58
|
|
|
59
|
+
// CRT settings (determines if we output to intermediate texture)
|
|
60
|
+
get crtEnabled() { return this.settings?.crt?.enabled ?? false }
|
|
61
|
+
get crtUpscaleEnabled() { return this.settings?.crt?.upscaleEnabled ?? false }
|
|
62
|
+
get shouldOutputToTexture() { return this.crtEnabled || this.crtUpscaleEnabled }
|
|
63
|
+
|
|
46
64
|
/**
|
|
47
65
|
* Set the input texture (HDR image from LightingPass)
|
|
48
66
|
* @param {Texture} texture - Input HDR texture
|
|
@@ -86,6 +104,71 @@ class PostProcessPass extends BasePass {
|
|
|
86
104
|
this.guiCanvas = canvas
|
|
87
105
|
}
|
|
88
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Get the output texture (for CRT pass to use)
|
|
109
|
+
* Returns null if outputting directly to canvas
|
|
110
|
+
*/
|
|
111
|
+
getOutputTexture() {
|
|
112
|
+
return this.shouldOutputToTexture ? this.intermediateTexture : null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create or resize the intermediate texture for CRT
|
|
117
|
+
*/
|
|
118
|
+
async _createIntermediateTexture(width, height) {
|
|
119
|
+
if (!this.shouldOutputToTexture) {
|
|
120
|
+
// Destroy if no longer needed
|
|
121
|
+
if (this.intermediateTexture?.texture) {
|
|
122
|
+
this.intermediateTexture.texture.destroy()
|
|
123
|
+
this.intermediateTexture = null
|
|
124
|
+
}
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Skip if size hasn't changed
|
|
129
|
+
if (this._outputWidth === width && this._outputHeight === height && this.intermediateTexture) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { device } = this.engine
|
|
134
|
+
|
|
135
|
+
// Destroy old texture
|
|
136
|
+
if (this.intermediateTexture?.texture) {
|
|
137
|
+
this.intermediateTexture.texture.destroy()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Create intermediate texture (SDR format for CRT input)
|
|
141
|
+
const texture = device.createTexture({
|
|
142
|
+
label: 'PostProcess Intermediate',
|
|
143
|
+
size: [width, height, 1],
|
|
144
|
+
format: 'rgba8unorm',
|
|
145
|
+
usage: GPUTextureUsage.TEXTURE_BINDING |
|
|
146
|
+
GPUTextureUsage.RENDER_ATTACHMENT |
|
|
147
|
+
GPUTextureUsage.COPY_SRC,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const sampler = device.createSampler({
|
|
151
|
+
label: 'PostProcess Intermediate Sampler',
|
|
152
|
+
minFilter: 'nearest',
|
|
153
|
+
magFilter: 'nearest',
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
this.intermediateTexture = {
|
|
157
|
+
texture,
|
|
158
|
+
view: texture.createView(),
|
|
159
|
+
sampler,
|
|
160
|
+
width,
|
|
161
|
+
height,
|
|
162
|
+
format: 'rgba8unorm', // Required by Pipeline.create()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this._outputWidth = width
|
|
166
|
+
this._outputHeight = height
|
|
167
|
+
this._needsRebuild = true
|
|
168
|
+
|
|
169
|
+
console.log(`PostProcessPass: Created intermediate texture ${width}x${height} for CRT`)
|
|
170
|
+
}
|
|
171
|
+
|
|
89
172
|
async _init() {
|
|
90
173
|
// Create dummy 1x1 black bloom texture for when bloom is disabled
|
|
91
174
|
// This ensures shader bindings are always valid
|
|
@@ -169,6 +252,11 @@ class PostProcessPass extends BasePass {
|
|
|
169
252
|
|
|
170
253
|
const hasBloom = this.bloomTexture && this.bloomEnabled
|
|
171
254
|
|
|
255
|
+
// Determine render target: intermediate texture (for CRT) or canvas
|
|
256
|
+
const renderTarget = this.shouldOutputToTexture && this.intermediateTexture
|
|
257
|
+
? this.intermediateTexture
|
|
258
|
+
: null
|
|
259
|
+
|
|
172
260
|
this.pipeline = await Pipeline.create(this.engine, {
|
|
173
261
|
label: 'postProcess',
|
|
174
262
|
wgslSource: postProcessingWGSL,
|
|
@@ -176,17 +264,41 @@ class PostProcessPass extends BasePass {
|
|
|
176
264
|
textures: textures,
|
|
177
265
|
uniforms: () => ({
|
|
178
266
|
noiseParams: [this.noiseSize, this.noiseAnimated ? Math.random() : 0, this.noiseAnimated ? Math.random() : 0, this.fxaa ? 1.0 : 0.0],
|
|
179
|
-
ditherParams: [this.ditheringEnabled ? 1.0 : 0.0, this.colorLevels,
|
|
267
|
+
ditherParams: [this.ditheringEnabled ? 1.0 : 0.0, this.colorLevels, this.tonemapMode, 0],
|
|
180
268
|
bloomParams: [hasBloom ? 1.0 : 0.0, this.bloomIntensity, this.bloomRadius, effectiveBloomTexture?.mipCount ?? 1]
|
|
181
269
|
}),
|
|
182
|
-
|
|
270
|
+
renderTarget: renderTarget,
|
|
183
271
|
})
|
|
184
272
|
|
|
185
273
|
this._needsRebuild = false
|
|
186
274
|
}
|
|
187
275
|
|
|
188
276
|
async _execute(context) {
|
|
189
|
-
const { device } = this.engine
|
|
277
|
+
const { device, canvas } = this.engine
|
|
278
|
+
|
|
279
|
+
// Check if CRT state changed (need to switch between canvas/texture output)
|
|
280
|
+
const needsOutputToTexture = this.shouldOutputToTexture
|
|
281
|
+
const hasIntermediateTexture = !!this.intermediateTexture
|
|
282
|
+
|
|
283
|
+
// Detect output mode change
|
|
284
|
+
if (needsOutputToTexture !== this._lastOutputToTexture) {
|
|
285
|
+
this._lastOutputToTexture = needsOutputToTexture
|
|
286
|
+
this._needsRebuild = true
|
|
287
|
+
|
|
288
|
+
if (needsOutputToTexture && !hasIntermediateTexture) {
|
|
289
|
+
// CRT was just enabled - create intermediate texture at stored resize dimensions
|
|
290
|
+
// Use resize dimensions (render-scaled), not canvas dimensions (full resolution)
|
|
291
|
+
const w = this._resizeWidth || canvas.width
|
|
292
|
+
const h = this._resizeHeight || canvas.height
|
|
293
|
+
await this._createIntermediateTexture(w, h)
|
|
294
|
+
} else if (!needsOutputToTexture && hasIntermediateTexture) {
|
|
295
|
+
// CRT was just disabled - destroy intermediate texture
|
|
296
|
+
if (this.intermediateTexture?.texture) {
|
|
297
|
+
this.intermediateTexture.texture.destroy()
|
|
298
|
+
this.intermediateTexture = null
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
190
302
|
|
|
191
303
|
// Update GUI texture from canvas if available
|
|
192
304
|
if (this.guiCanvas && this.guiCanvas.width > 0 && this.guiCanvas.height > 0) {
|
|
@@ -249,7 +361,7 @@ class PostProcessPass extends BasePass {
|
|
|
249
361
|
// Update uniforms each frame
|
|
250
362
|
this.pipeline.uniformValues.set({
|
|
251
363
|
noiseParams: [this.noiseSize, this.noiseAnimated ? Math.random() : 0, this.noiseAnimated ? Math.random() : 0, this.fxaa ? 1.0 : 0.0],
|
|
252
|
-
ditherParams: [this.ditheringEnabled ? 1.0 : 0.0, this.colorLevels,
|
|
364
|
+
ditherParams: [this.ditheringEnabled ? 1.0 : 0.0, this.colorLevels, this.tonemapMode, 0],
|
|
253
365
|
bloomParams: [hasBloom ? 1.0 : 0.0, this.bloomIntensity, this.bloomRadius, effectiveBloomTexture?.mipCount ?? 1]
|
|
254
366
|
})
|
|
255
367
|
|
|
@@ -258,6 +370,13 @@ class PostProcessPass extends BasePass {
|
|
|
258
370
|
}
|
|
259
371
|
|
|
260
372
|
async _resize(width, height) {
|
|
373
|
+
// Store resize dimensions for runtime texture creation
|
|
374
|
+
this._resizeWidth = width
|
|
375
|
+
this._resizeHeight = height
|
|
376
|
+
|
|
377
|
+
// Create intermediate texture for CRT if enabled
|
|
378
|
+
await this._createIntermediateTexture(width, height)
|
|
379
|
+
|
|
261
380
|
// Pipeline needs rebuild since canvas size changed
|
|
262
381
|
this._needsRebuild = true
|
|
263
382
|
}
|
|
@@ -276,6 +395,10 @@ class PostProcessPass extends BasePass {
|
|
|
276
395
|
this.dummyGuiTexture.texture.destroy()
|
|
277
396
|
this.dummyGuiTexture = null
|
|
278
397
|
}
|
|
398
|
+
if (this.intermediateTexture?.texture) {
|
|
399
|
+
this.intermediateTexture.texture.destroy()
|
|
400
|
+
this.intermediateTexture = null
|
|
401
|
+
}
|
|
279
402
|
}
|
|
280
403
|
}
|
|
281
404
|
|
|
@@ -148,8 +148,9 @@ class SSGIPass extends BasePass {
|
|
|
148
148
|
return
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
// Update uniforms
|
|
152
|
-
|
|
151
|
+
// Update uniforms - use render dimensions (this.width/height are half-res)
|
|
152
|
+
// Don't use canvas dimensions as they may differ from render dimensions
|
|
153
|
+
this._updateUniforms(ssgiSettings, this.width * 2, this.height * 2)
|
|
153
154
|
|
|
154
155
|
// Create bind group
|
|
155
156
|
const bindGroup = device.createBindGroup({
|
|
@@ -35,6 +35,10 @@ class SSGITilePass extends BasePass {
|
|
|
35
35
|
this.tileCountX = 0
|
|
36
36
|
this.tileCountY = 0
|
|
37
37
|
|
|
38
|
+
// Stored render dimensions (from resize)
|
|
39
|
+
this.renderWidth = 0
|
|
40
|
+
this.renderHeight = 0
|
|
41
|
+
|
|
38
42
|
// Input textures
|
|
39
43
|
this.prevHDRTexture = null
|
|
40
44
|
this.emissiveTexture = null
|
|
@@ -75,6 +79,10 @@ class SSGITilePass extends BasePass {
|
|
|
75
79
|
async _createResources(width, height) {
|
|
76
80
|
const { device } = this.engine
|
|
77
81
|
|
|
82
|
+
// Store render dimensions for use in _execute
|
|
83
|
+
this.renderWidth = width
|
|
84
|
+
this.renderHeight = height
|
|
85
|
+
|
|
78
86
|
// Calculate tile grid dimensions
|
|
79
87
|
this.tileCountX = Math.ceil(width / TILE_SIZE)
|
|
80
88
|
this.tileCountY = Math.ceil(height / TILE_SIZE)
|
|
@@ -160,7 +168,7 @@ class SSGITilePass extends BasePass {
|
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
async _execute(context) {
|
|
163
|
-
const { device
|
|
171
|
+
const { device } = this.engine
|
|
164
172
|
|
|
165
173
|
// Check if SSGI is enabled
|
|
166
174
|
const ssgiSettings = this.settings?.ssgi
|
|
@@ -168,9 +176,9 @@ class SSGITilePass extends BasePass {
|
|
|
168
176
|
return
|
|
169
177
|
}
|
|
170
178
|
|
|
171
|
-
// Rebuild if needed
|
|
179
|
+
// Rebuild if needed (use stored render dimensions, not canvas)
|
|
172
180
|
if (this._needsRebuild) {
|
|
173
|
-
await this._createResources(
|
|
181
|
+
await this._createResources(this.renderWidth, this.renderHeight)
|
|
174
182
|
}
|
|
175
183
|
|
|
176
184
|
// Check required textures
|
|
@@ -182,8 +190,9 @@ class SSGITilePass extends BasePass {
|
|
|
182
190
|
return
|
|
183
191
|
}
|
|
184
192
|
|
|
185
|
-
|
|
186
|
-
const
|
|
193
|
+
// Use stored render dimensions, not canvas dimensions
|
|
194
|
+
const width = this.renderWidth
|
|
195
|
+
const height = this.renderHeight
|
|
187
196
|
const emissiveBoost = ssgiSettings.emissiveBoost ?? 2.0
|
|
188
197
|
const maxBrightness = ssgiSettings.maxBrightness ?? 4.0
|
|
189
198
|
|