From 559abed39534999558f357fc4bb187475ce21688 Mon Sep 17 00:00:00 2001 From: Martin Valigursky Date: Wed, 8 Apr 2026 17:24:18 +0100 Subject: [PATCH 1/2] Add fog support for Gaussian splat rendering Adds scene fog to both GSplat rendering paths (quad renderer and compute rasterizer), applied per-splat in linear color space. Creates shared fogMathPS WGSL chunk for reuse across fragment/vertex and compute shaders. Updates weather and LOD streaming examples with fog density controls. --- .../lod-streaming.controls.mjs | 12 ++++ .../lod-streaming.example.mjs | 12 ++++ .../gaussian-splatting/weather.controls.mjs | 25 +++++++++ .../gaussian-splatting/weather.example.mjs | 55 +++++++++++++++++-- .../gsplat-compute-local-renderer.js | 24 +++++++- .../gsplat-local-dispatch-set.js | 35 +++++++++--- src/scene/gsplat-unified/gsplat-manager.js | 3 +- src/scene/gsplat-unified/gsplat-renderer.js | 4 +- .../shader-lib/glsl/chunks/common/frag/fog.js | 31 +++++++---- .../glsl/chunks/gsplat/vert/gsplat.js | 2 +- .../glsl/chunks/gsplat/vert/gsplatOutput.js | 37 ++++++++----- .../shader-lib/wgsl/chunks/common/frag/fog.js | 47 ++++++++++------ .../wgsl/chunks/common/shared/fogMath.js | 14 +++++ .../gsplat/compute-gsplat-local-rasterize.js | 29 +++++++++- .../wgsl/chunks/gsplat/vert/gsplat.js | 2 +- .../wgsl/chunks/gsplat/vert/gsplatOutput.js | 37 ++++++++----- .../wgsl/collections/shader-chunks-wgsl.js | 2 + 17 files changed, 298 insertions(+), 73 deletions(-) create mode 100644 src/scene/shader-lib/wgsl/chunks/common/shared/fogMath.js diff --git a/examples/src/examples/gaussian-splatting/lod-streaming.controls.mjs b/examples/src/examples/gaussian-splatting/lod-streaming.controls.mjs index 69c799ac72e..9dcb5b848c1 100644 --- a/examples/src/examples/gaussian-splatting/lod-streaming.controls.mjs +++ b/examples/src/examples/gaussian-splatting/lod-streaming.controls.mjs @@ -63,6 +63,18 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { { v: 'night', t: 'Night Sky' } ] }) + ), + jsx( + LabelGroup, + { text: 'Fog Density' }, + jsx(SliderInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'fogDensity' }, + min: 0, + max: 0.5, + precision: 3, + step: 0.001 + }) ) ), jsx( diff --git a/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs b/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs index ca7d7db7c2c..2d6878b1370 100644 --- a/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs +++ b/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs @@ -205,6 +205,7 @@ assetListLoader.load(async () => { data.set('lodPreset', pc.platform.mobile ? 'mobile' : 'desktop'); data.set('splatBudget', pc.platform.mobile ? 1 : 4); data.set('environment', 'none'); + data.set('fogDensity', 0); data.set('url', ''); data.set('orientation', 270); @@ -309,6 +310,17 @@ assetListLoader.load(async () => { app.scene.exposure = data.get('exposure'); }); + data.on('fogDensity:set', () => { + const density = data.get('fogDensity'); + if (density > 0) { + app.scene.fog.type = pc.FOG_EXP; + app.scene.fog.density = density; + app.scene.fog.color.copy(camera.camera.clearColor); + } else { + app.scene.fog.type = pc.FOG_NONE; + } + }); + // Poly Haven credit overlay (shown when an HDRI is active) const phCredit = document.createElement('a'); phCredit.href = 'https://polyhaven.com'; diff --git a/examples/src/examples/gaussian-splatting/weather.controls.mjs b/examples/src/examples/gaussian-splatting/weather.controls.mjs index 47ca851e7d7..dd19d3725d5 100644 --- a/examples/src/examples/gaussian-splatting/weather.controls.mjs +++ b/examples/src/examples/gaussian-splatting/weather.controls.mjs @@ -16,10 +16,35 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { link: { observer, path: 'preset' }, type: 'string', options: [ + { v: 'none', t: 'None' }, { v: 'snow', t: 'Snow' }, { v: 'rain', t: 'Rain' } ] }) + ), + jsx( + LabelGroup, + { text: 'Fog Density' }, + jsx(SliderInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'fogDensity' }, + min: 0, + max: 0.5, + precision: 3, + step: 0.001 + }) + ), + jsx( + LabelGroup, + { text: 'Exposure' }, + jsx(SliderInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'exposure' }, + min: 0, + max: 5, + precision: 2, + step: 0.05 + }) ) ), jsx( diff --git a/examples/src/examples/gaussian-splatting/weather.example.mjs b/examples/src/examples/gaussian-splatting/weather.example.mjs index 0e153d067f6..c838de3e55c 100644 --- a/examples/src/examples/gaussian-splatting/weather.example.mjs +++ b/examples/src/examples/gaussian-splatting/weather.example.mjs @@ -116,31 +116,58 @@ assetListLoader.load(() => { const sliderToSize = v => PSIZE_LO + v * PSIZE_RANGE; const presets = { + none: { + speed: 0, + drift: 0, + angle: 0, + opacity: 0, + color: [1, 1, 1], + elongate: 1, + particleMinSize: 0, + particleMaxSize: 0, + fogDensity: 0, + exposure: 1 + }, snow: { speed: 2.0, - drift: 0.15, + drift: 0.4, angle: 0, opacity: 0.8, color: [1, 1, 1], elongate: 1, particleMinSize: 0.006, - particleMaxSize: 0.012 + particleMaxSize: 0.012, + fogDensity: 0.03, + exposure: 0.8 }, rain: { speed: 10, drift: 0, angle: 0, opacity: 0.57, - color: [0.62, 0.62, 0.62], + color: [0.812, 0.812, 0.812], elongate: 20, particleMinSize: 0.003, - particleMaxSize: 0.003 + particleMaxSize: 0.003, + fogDensity: 0.02, + exposure: 0.5 + } + }; + + const applyFog = (density) => { + if (density > 0) { + app.scene.fog.type = pc.FOG_EXP; + app.scene.fog.density = density; + app.scene.fog.color.set(1, 1, 1); + } else { + app.scene.fog.type = pc.FOG_NONE; } }; const applyPreset = (name) => { const p = presets[name]; if (!p) return; + weatherEntity.enabled = name !== 'none'; data.set('speed', p.speed); data.set('drift', p.drift); data.set('angle', p.angle); @@ -149,9 +176,23 @@ assetListLoader.load(() => { data.set('elongate', p.elongate); data.set('particleMinSize', sizeToSlider(p.particleMinSize)); data.set('particleMaxSize', sizeToSlider(p.particleMaxSize)); + data.set('fogDensity', p.fogDensity); + data.set('exposure', p.exposure); + + weather.speed = p.speed; + weather.drift = p.drift; + weather.opacity = p.opacity; + weather.color = p.color; + weather.elongate = p.elongate; + weather.particleMinSize = p.particleMinSize; + weather.particleMaxSize = p.particleMaxSize; + weatherEntity.setLocalEulerAngles(p.angle, 0, 0); + applyFog(p.fogDensity); + app.scene.exposure = p.exposure; }; // Initialize UI data + data.set('exposure', 1); data.set('preset', 'snow'); applyPreset('snow'); data.set('extents', [weather.extents.x, weather.extents.y, weather.extents.z]); @@ -188,6 +229,12 @@ assetListLoader.load(() => { data.on('particleMaxSize:set', () => { weather.particleMaxSize = sliderToSize(data.get('particleMaxSize')); }); + data.on('fogDensity:set', () => { + applyFog(data.get('fogDensity')); + }); + data.on('exposure:set', () => { + app.scene.exposure = data.get('exposure'); + }); // Grid config — requires rebuild const rebuild = () => { diff --git a/src/scene/gsplat-unified/gsplat-compute-local-renderer.js b/src/scene/gsplat-unified/gsplat-compute-local-renderer.js index a13508cab26..40408a7c0e3 100644 --- a/src/scene/gsplat-unified/gsplat-compute-local-renderer.js +++ b/src/scene/gsplat-unified/gsplat-compute-local-renderer.js @@ -13,7 +13,8 @@ import { UNIFORMTYPE_MAT4, UNIFORMTYPE_UINT } from '../../platform/graphics/constants.js'; -import { GSPLAT_FORWARD, PROJECTION_ORTHOGRAPHIC } from '../constants.js'; +import { GSPLAT_FORWARD, PROJECTION_ORTHOGRAPHIC, FOG_NONE } from '../constants.js'; +import { Color } from '../../core/math/color.js'; import { Mat4 } from '../../core/math/mat4.js'; import { GSplatRenderer } from './gsplat-renderer.js'; import { FramePassGSplatComputeLocal } from './frame-pass-gsplat-compute-local.js'; @@ -50,6 +51,8 @@ const _viewProjMat = new Mat4(); const _viewProjData = new Float32Array(16); const _viewData = new Float32Array(16); const _dispatchSize = new Vec2(); +const _fogColorLinear = new Color(); +const _fogColorArray = new Float32Array(3); /** * Renders splats using a tiled compute pipeline with per-tile binning and local sorting. @@ -241,7 +244,7 @@ class GSplatComputeLocalRenderer extends GSplatRenderer { super.setRenderMode(renderMode); } - frameUpdate(gsplat, exposure) { + frameUpdate(gsplat, exposure, fogParams) { if (this._needsFramePassRegister) { this._registerFramePass(); } @@ -250,6 +253,7 @@ class GSplatComputeLocalRenderer extends GSplatRenderer { this._alphaClip = gsplat.alphaClip; this._exposure = exposure ?? 1.0; this._fisheye = gsplat.fisheye; + this._fogParams = fogParams ?? null; const formatHash = this.workBuffer.format.hash; if (formatHash !== this._formatHash) { @@ -583,7 +587,10 @@ class GSplatComputeLocalRenderer extends GSplatRenderer { const hasLinearDepth = cam.shaderParams.sceneDepthMapLinear; const sceneDepthMap = hasLinearDepth ? device.scope.resolve('uSceneDepthMap').value : null; const useDepth = !pickMode && sceneDepthMap; - const rasterizeCompute = set.getRasterizeCompute(pickMode, useDepth); + + const fogParams = this._fogParams; + const fogType = (fogParams && fogParams.type !== FOG_NONE) ? fogParams.type : 'none'; + const rasterizeCompute = set.getRasterizeCompute(pickMode, useDepth, fogType); rasterizeCompute.setParameter('screenWidth', width); rasterizeCompute.setParameter('screenHeight', height); @@ -591,6 +598,17 @@ class GSplatComputeLocalRenderer extends GSplatRenderer { rasterizeCompute.setParameter('nearClip', cam.nearClip); rasterizeCompute.setParameter('farClip', cam.farClip); rasterizeCompute.setParameter('alphaClip', alphaClip); + + if (fogType !== 'none') { + _fogColorLinear.linear(fogParams.color); + _fogColorArray[0] = _fogColorLinear.r; + _fogColorArray[1] = _fogColorLinear.g; + _fogColorArray[2] = _fogColorLinear.b; + rasterizeCompute.setParameter('fog_color', _fogColorArray); + rasterizeCompute.setParameter('fog_start', fogParams.start); + rasterizeCompute.setParameter('fog_end', fogParams.end); + rasterizeCompute.setParameter('fog_density', fogParams.density); + } rasterizeCompute.setParameter('tileEntries', this._tileEntriesBuffer); rasterizeCompute.setParameter('tileSplatCounts', set._tileSplatCountsBuffer); rasterizeCompute.setParameter('projCache', this._projCacheBuffer); diff --git a/src/scene/gsplat-unified/gsplat-local-dispatch-set.js b/src/scene/gsplat-unified/gsplat-local-dispatch-set.js index be89e3d827d..4c1bca291e0 100644 --- a/src/scene/gsplat-unified/gsplat-local-dispatch-set.js +++ b/src/scene/gsplat-unified/gsplat-local-dispatch-set.js @@ -13,7 +13,8 @@ import { SHADERSTAGE_COMPUTE, TEXTUREDIMENSION_2D, UNIFORMTYPE_FLOAT, - UNIFORMTYPE_UINT + UNIFORMTYPE_UINT, + UNIFORMTYPE_VEC3 } from '../../platform/graphics/constants.js'; import { PrefixSumKernel } from '../graphics/prefix-sum-kernel.js'; import { shaderChunksWGSL } from '../shader-lib/wgsl/collections/shader-chunks-wgsl.js'; @@ -278,13 +279,16 @@ class GSplatLocalDispatchSet { * * @param {boolean} pickMode - Whether to use the pick variant. * @param {boolean} depthTest - Whether to enable depth testing against scene geometry. + * @param {string} [fogType] - Fog type string: 'none', 'linear', 'exp', or 'exp2'. * @returns {Compute} The cached Compute instance. */ - getRasterizeCompute(pickMode, depthTest) { - const key = pickMode ? 'pick' : (depthTest ? 'color-depth' : 'color'); + getRasterizeCompute(pickMode, depthTest, fogType = 'none') { + let key = pickMode ? 'pick' : 'color'; + if (depthTest) key += '-depth'; + if (fogType !== 'none') key += `-fog-${fogType}`; let variant = this._rasterizeVariants.get(key); if (!variant) { - const { shader, bindGroupFormat } = this._createRasterizeShaderAndFormat(pickMode, depthTest); + const { shader, bindGroupFormat } = this._createRasterizeShaderAndFormat(pickMode, depthTest, fogType); const compute = new Compute(this.device, shader, `GSplatRasterize-${key}`); variant = { shader, bindGroupFormat, compute }; this._rasterizeVariants.set(key, variant); @@ -297,20 +301,31 @@ class GSplatLocalDispatchSet { * * @param {boolean} pickMode - Whether to create the pick variant. * @param {boolean} depthTest - Whether to enable depth testing against scene geometry. + * @param {string} [fogType] - Fog type string: 'none', 'linear', 'exp', or 'exp2'. * @returns {{ shader: Shader, bindGroupFormat: BindGroupFormat }} The shader and format. * @private */ - _createRasterizeShaderAndFormat(pickMode, depthTest = false) { + _createRasterizeShaderAndFormat(pickMode, depthTest = false, fogType = 'none') { const device = this.device; + const hasFog = fogType !== 'none'; - const ubf = new UniformBufferFormat(device, [ + const uniforms = [ new UniformFormat('screenWidth', UNIFORMTYPE_UINT), new UniformFormat('screenHeight', UNIFORMTYPE_UINT), new UniformFormat('numTilesX', UNIFORMTYPE_UINT), new UniformFormat('nearClip', UNIFORMTYPE_FLOAT), new UniformFormat('farClip', UNIFORMTYPE_FLOAT), new UniformFormat('alphaClip', UNIFORMTYPE_FLOAT) - ]); + ]; + if (hasFog) { + uniforms.push( + new UniformFormat('fog_color', UNIFORMTYPE_VEC3), + new UniformFormat('fog_start', UNIFORMTYPE_FLOAT), + new UniformFormat('fog_end', UNIFORMTYPE_FLOAT), + new UniformFormat('fog_density', UNIFORMTYPE_FLOAT) + ); + } + const ubf = new UniformBufferFormat(device, uniforms); const sharedBindings = [ new BindUniformBufferFormat('uniforms', SHADERSTAGE_COMPUTE), @@ -337,8 +352,14 @@ class GSplatLocalDispatchSet { const cdefines = new Map(); if (pickMode) cdefines.set('PICK_MODE', ''); if (depthTest) cdefines.set('DEPTH_TEST', ''); + cdefines.set('GAMMA', 'SRGB'); // assumes splat colors are in gamma space, will need to change when we get linear splats + if (hasFog) cdefines.set('FOG', fogType.toUpperCase()); const cincludes = pickMode ? undefined : new Map([['decodePS', shaderChunksWGSL.decodePS]]); + if (hasFog && cincludes) { + cincludes.set('fogMathPS', shaderChunksWGSL.fogMathPS); + cincludes.set('gammaPS', shaderChunksWGSL.gammaPS); + } let name = 'GSplatLocalRasterize'; if (pickMode) name = 'GSplatLocalRasterizePick'; diff --git a/src/scene/gsplat-unified/gsplat-manager.js b/src/scene/gsplat-unified/gsplat-manager.js index 5b0cb8e4738..57bd2845e6e 100644 --- a/src/scene/gsplat-unified/gsplat-manager.js +++ b/src/scene/gsplat-unified/gsplat-manager.js @@ -1506,7 +1506,8 @@ class GSplatManager { } // renderer per-frame update (material syncing, deferred setup) - this.renderer.frameUpdate(this.scene.gsplat, this.scene.exposure); + const fogParams = this.cameraNode.camera.fogParams ?? this.scene.fog; + this.renderer.frameUpdate(this.scene.gsplat, this.scene.exposure, fogParams); // camera tracking only after first sort if (sortedState?.sortedBefore) { diff --git a/src/scene/gsplat-unified/gsplat-renderer.js b/src/scene/gsplat-unified/gsplat-renderer.js index 2e5242081e9..1d9e5d12512 100644 --- a/src/scene/gsplat-unified/gsplat-renderer.js +++ b/src/scene/gsplat-unified/gsplat-renderer.js @@ -9,6 +9,7 @@ import { FisheyeProjection } from './fisheye-projection.js'; * @import { GraphNode } from '../graph-node.js' * @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js' * @import { GSplatWorkBuffer } from './gsplat-work-buffer.js' + * @import { FogParams } from '../fog-params.js' */ /** @@ -136,8 +137,9 @@ class GSplatRenderer { * * @param {object} params - The gsplat parameters. * @param {number} [exposure] - Scene exposure value. + * @param {FogParams} [fogParams] - Fog parameters. */ - frameUpdate(params, exposure) { + frameUpdate(params, exposure, fogParams) { } /** diff --git a/src/scene/shader-lib/glsl/chunks/common/frag/fog.js b/src/scene/shader-lib/glsl/chunks/common/frag/fog.js index 4e69024bb60..2ac15413d66 100644 --- a/src/scene/shader-lib/glsl/chunks/common/frag/fog.js +++ b/src/scene/shader-lib/glsl/chunks/common/frag/fog.js @@ -13,9 +13,13 @@ float dBlendModeFogFactor = 1.0; #endif #endif -float getFogFactor() { +#ifdef VERTEXSHADER + float getFogFactor(float depth) { +#else + float getFogFactor() { + float depth = gl_FragCoord.z / gl_FragCoord.w; +#endif - float depth = gl_FragCoord.z / gl_FragCoord.w; float fogFactor = 0.0; #if (FOG == LINEAR) @@ -29,12 +33,19 @@ float getFogFactor() { return clamp(fogFactor, 0.0, 1.0); } -vec3 addFog(vec3 color) { - - #if (FOG != NONE) - return mix(fog_color * dBlendModeFogFactor, color, getFogFactor()); - #endif - - return color; -} +#ifdef VERTEXSHADER + vec3 addFog(vec3 color, float depth) { + #if (FOG != NONE) + return mix(fog_color * dBlendModeFogFactor, color, getFogFactor(depth)); + #endif + return color; + } +#else + vec3 addFog(vec3 color) { + #if (FOG != NONE) + return mix(fog_color * dBlendModeFogFactor, color, getFogFactor()); + #endif + return color; + } +#endif `; diff --git a/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplat.js b/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplat.js index 00d90321347..63fcb9de78d 100644 --- a/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplat.js +++ b/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplat.js @@ -105,7 +105,7 @@ void main(void) { clr.a *= (1.0 / 32.0) * colorRampIntensity; gaussianColor = vec4(rampColor, clr.a); #else - gaussianColor = vec4(prepareOutputFromGamma(max(clr.xyz, 0.0)), clr.w); + gaussianColor = vec4(prepareOutputFromGamma(max(clr.xyz, 0.0), -center.view.z), clr.w); #endif #ifndef DITHER_NONE diff --git a/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatOutput.js b/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatOutput.js index a11410b3345..62baeb6208b 100644 --- a/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatOutput.js +++ b/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatOutput.js @@ -3,21 +3,32 @@ export default /* glsl */` #include "tonemappingPS" #include "decodePS" #include "gammaPS" +#include "fogPS" // prepare the output color for the given gamma-space color -vec3 prepareOutputFromGamma(vec3 gammaColor) { - #if TONEMAP == NONE - #if GAMMA == NONE - // convert to linear space - return decodeGamma(gammaColor); - #else - // output gamma space color directly - return gammaColor; - #endif - #else - // apply tonemapping in linear space and output to linear or - // gamma (which is handled by gammaCorrectOutput) - return gammaCorrectOutput(toneMap(decodeGamma(gammaColor))); +vec3 prepareOutputFromGamma(vec3 gammaColor, float depth) { + vec3 color = gammaColor; + + // decode to linear when we need linear-space processing + #if TONEMAP != NONE || GAMMA == NONE || FOG != NONE + color = decodeGamma(color); + #endif + + // apply fog in linear space + #if FOG != NONE + color = addFog(color, depth); + #endif + + // apply tonemapping + #if TONEMAP != NONE + color = toneMap(color); + #endif + + // encode to gamma when needed + #if TONEMAP != NONE || (GAMMA != NONE && FOG != NONE) + color = gammaCorrectOutput(color); #endif + + return color; } `; diff --git a/src/scene/shader-lib/wgsl/chunks/common/frag/fog.js b/src/scene/shader-lib/wgsl/chunks/common/frag/fog.js index 7b1afc27467..0d506012645 100644 --- a/src/scene/shader-lib/wgsl/chunks/common/frag/fog.js +++ b/src/scene/shader-lib/wgsl/chunks/common/frag/fog.js @@ -1,5 +1,7 @@ export default /* wgsl */` +#include "fogMathPS" + var dBlendModeFogFactor : f32 = 1.0; #if (FOG != NONE) @@ -13,28 +15,39 @@ var dBlendModeFogFactor : f32 = 1.0; #endif #endif -fn getFogFactor() -> f32 { - - let depth = pcPosition.z / pcPosition.w; - - var fogFactor : f32 = 0.0; +#ifdef VERTEXSHADER + fn getFogFactor(depth: f32) -> f32 { +#else + fn getFogFactor() -> f32 { + let depth = pcPosition.z / pcPosition.w; +#endif #if (FOG == LINEAR) - fogFactor = (uniform.fog_end - depth) / (uniform.fog_end - uniform.fog_start); + return evaluateFogFactorLinear(depth, uniform.fog_start, uniform.fog_end); #elif (FOG == EXP) - fogFactor = exp(-depth * uniform.fog_density); + return evaluateFogFactorExp(depth, uniform.fog_density); #elif (FOG == EXP2) - fogFactor = exp(-depth * depth * uniform.fog_density * uniform.fog_density); - #endif - - return clamp(fogFactor, 0.0, 1.0); -} - -fn addFog(color : vec3f) -> vec3f { - #if (FOG != NONE) - return mix(uniform.fog_color * dBlendModeFogFactor, color, getFogFactor()); + return evaluateFogFactorExp2(depth, uniform.fog_density); #else - return color; + return 1.0; #endif } + +#ifdef VERTEXSHADER + fn addFog(color: vec3f, depth: f32) -> vec3f { + #if (FOG != NONE) + return mix(uniform.fog_color * dBlendModeFogFactor, color, getFogFactor(depth)); + #else + return color; + #endif + } +#else + fn addFog(color: vec3f) -> vec3f { + #if (FOG != NONE) + return mix(uniform.fog_color * dBlendModeFogFactor, color, getFogFactor()); + #else + return color; + #endif + } +#endif `; diff --git a/src/scene/shader-lib/wgsl/chunks/common/shared/fogMath.js b/src/scene/shader-lib/wgsl/chunks/common/shared/fogMath.js new file mode 100644 index 00000000000..fde893d27aa --- /dev/null +++ b/src/scene/shader-lib/wgsl/chunks/common/shared/fogMath.js @@ -0,0 +1,14 @@ +export default /* wgsl */` + +fn evaluateFogFactorLinear(depth: f32, fogStart: f32, fogEnd: f32) -> f32 { + return clamp((fogEnd - depth) / (fogEnd - fogStart), 0.0, 1.0); +} + +fn evaluateFogFactorExp(depth: f32, fogDensity: f32) -> f32 { + return clamp(exp(-depth * fogDensity), 0.0, 1.0); +} + +fn evaluateFogFactorExp2(depth: f32, fogDensity: f32) -> f32 { + return clamp(exp(-depth * depth * fogDensity * fogDensity), 0.0, 1.0); +} +`; diff --git a/src/scene/shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-rasterize.js b/src/scene/shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-rasterize.js index e95613bf3a8..1aebb2d919c 100644 --- a/src/scene/shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-rasterize.js +++ b/src/scene/shader-lib/wgsl/chunks/gsplat/compute-gsplat-local-rasterize.js @@ -5,7 +5,11 @@ export const computeGsplatLocalRasterizeSource = /* wgsl */` #include "halfTypesCS" #ifndef PICK_MODE -#include "decodePS" + #include "decodePS" + #if FOG != NONE + #include "fogMathPS" + #include "gammaPS" + #endif #endif const CACHE_STRIDE: u32 = 8u; @@ -24,6 +28,12 @@ struct Uniforms { nearClip: f32, farClip: f32, alphaClip: f32, + #if FOG != NONE + fog_color: vec3f, + fog_start: f32, + fog_end: f32, + fog_density: f32, + #endif } @group(0) @binding(0) var uniforms: Uniforms; @group(0) @binding(1) var tileEntries: array; @@ -203,7 +213,22 @@ fn main( #else let rg = unpack2x16float(projCache[base + 5u]); let ba = unpack2x16float(projCache[base + 6u]); - sharedColor[localIdx] = half4(half(rg.x), half(rg.y), half(ba.x), half(ba.y)); + + #if FOG != NONE + let viewDepth = bitcast(projCache[base + 7u]); + #if (FOG == LINEAR) + let fogFactor = evaluateFogFactorLinear(viewDepth, uniforms.fog_start, uniforms.fog_end); + #elif (FOG == EXP) + let fogFactor = evaluateFogFactorExp(viewDepth, uniforms.fog_density); + #elif (FOG == EXP2) + let fogFactor = evaluateFogFactorExp2(viewDepth, uniforms.fog_density); + #endif + var foggedColor = decodeGamma3(vec3f(rg.x, rg.y, ba.x)); + foggedColor = mix(uniforms.fog_color, foggedColor, fogFactor); + sharedColor[localIdx] = half4(half3(gammaCorrectOutput(foggedColor)), half(ba.y)); + #else + sharedColor[localIdx] = half4(half(rg.x), half(rg.y), half(ba.x), half(ba.y)); + #endif #ifdef DEPTH_TEST sharedViewDepth[localIdx] = bitcast(projCache[base + 7u]); diff --git a/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplat.js b/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplat.js index b2be35c5b37..ff5b71bb972 100644 --- a/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplat.js +++ b/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplat.js @@ -113,7 +113,7 @@ fn vertexMain(input: VertexInput) -> VertexOutput { clr.a = clr.a * half(1.0 / 32.0) * half(uniform.colorRampIntensity); output.gaussianColor = half4(half3(rampColor), clr.a); #else - output.gaussianColor = half4(half3(prepareOutputFromGamma(max(vec3f(clr.xyz), vec3f(0.0)))), clr.w); + output.gaussianColor = half4(half3(prepareOutputFromGamma(max(vec3f(clr.xyz), vec3f(0.0)), -center.view.z)), clr.w); #endif #ifndef DITHER_NONE diff --git a/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatOutput.js b/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatOutput.js index 28e95c477a7..47efafe3251 100644 --- a/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatOutput.js +++ b/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatOutput.js @@ -3,21 +3,32 @@ export default /* wgsl */` #include "tonemappingPS" #include "decodePS" #include "gammaPS" +#include "fogPS" // prepare the output color for the given gamma-space color -fn prepareOutputFromGamma(gammaColor: vec3f) -> vec3f { - #if TONEMAP == NONE - #if GAMMA == NONE - // convert to linear space - return decodeGamma3(gammaColor); - #else - // output gamma space color directly - return gammaColor; - #endif - #else - // apply tonemapping in linear space and output to linear or - // gamma (which is handled by gammaCorrectOutput) - return gammaCorrectOutput(toneMap(decodeGamma3(gammaColor))); +fn prepareOutputFromGamma(gammaColor: vec3f, depth: f32) -> vec3f { + var color = gammaColor; + + // decode to linear when we need linear-space processing + #if TONEMAP != NONE || GAMMA == NONE || FOG != NONE + color = decodeGamma3(color); + #endif + + // apply fog in linear space + #if FOG != NONE + color = addFog(color, depth); + #endif + + // apply tonemapping + #if TONEMAP != NONE + color = toneMap(color); + #endif + + // encode to gamma when needed + #if TONEMAP != NONE || (GAMMA != NONE && FOG != NONE) + color = gammaCorrectOutput(color); #endif + + return color; } `; diff --git a/src/scene/shader-lib/wgsl/collections/shader-chunks-wgsl.js b/src/scene/shader-lib/wgsl/collections/shader-chunks-wgsl.js index fc76a5c0f82..4c5ae5997ae 100644 --- a/src/scene/shader-lib/wgsl/collections/shader-chunks-wgsl.js +++ b/src/scene/shader-lib/wgsl/collections/shader-chunks-wgsl.js @@ -39,6 +39,7 @@ import falloffInvSquaredPS from '../chunks/lit/frag/falloffInvSquared.js'; import falloffLinearPS from '../chunks/lit/frag/falloffLinear.js'; import floatAsUintPS from '../chunks/common/frag/float-as-uint.js'; import fogPS from '../chunks/common/frag/fog.js'; +import fogMathPS from '../chunks/common/shared/fogMath.js'; import fresnelSchlickPS from '../chunks/lit/frag/fresnelSchlick.js'; import fullscreenQuadVS from '../chunks/common/vert/fullscreenQuad.js'; import gammaPS from '../chunks/common/frag/gamma.js'; @@ -199,6 +200,7 @@ const shaderChunksWGSL = { falloffLinearPS, floatAsUintPS, fogPS, + fogMathPS, fresnelSchlickPS, frontendCodePS: '', // empty chunk, supplied by the shader generator frontendDeclPS: '', // empty chunk, supplied by the shader generator From 35fe8069201aa4b3bb77c32649cc37ac578103db Mon Sep 17 00:00:00 2001 From: Martin Valigursky Date: Thu, 9 Apr 2026 09:36:14 +0100 Subject: [PATCH 2/2] useFog toggle --- .../examples/gaussian-splatting/weather.controls.mjs | 10 +++++++++- .../examples/gaussian-splatting/weather.example.mjs | 4 ++++ src/scene/gsplat-unified/gsplat-local-dispatch-set.js | 2 +- src/scene/gsplat-unified/gsplat-manager.js | 2 +- src/scene/gsplat-unified/gsplat-params.js | 8 ++++++++ src/scene/gsplat-unified/gsplat-quad-renderer.js | 7 +++++++ .../shader-lib/glsl/chunks/gsplat/vert/gsplatOutput.js | 10 +++++++--- .../shader-lib/wgsl/chunks/gsplat/vert/gsplatOutput.js | 10 +++++++--- 8 files changed, 44 insertions(+), 9 deletions(-) diff --git a/examples/src/examples/gaussian-splatting/weather.controls.mjs b/examples/src/examples/gaussian-splatting/weather.controls.mjs index dd19d3725d5..7e0823b8a71 100644 --- a/examples/src/examples/gaussian-splatting/weather.controls.mjs +++ b/examples/src/examples/gaussian-splatting/weather.controls.mjs @@ -3,7 +3,7 @@ * @returns {JSX.Element} The returned JSX Element. */ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { - const { BindingTwoWay, Label, LabelGroup, Panel, SliderInput, ColorPicker, SelectInput, VectorInput } = ReactPCUI; + const { BindingTwoWay, BooleanInput, Label, LabelGroup, Panel, SliderInput, ColorPicker, SelectInput, VectorInput } = ReactPCUI; return fragment( jsx( Panel, @@ -45,6 +45,14 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { precision: 2, step: 0.05 }) + ), + jsx( + LabelGroup, + { text: 'Splat Fog' }, + jsx(BooleanInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'useFog' } + }) ) ), jsx( diff --git a/examples/src/examples/gaussian-splatting/weather.example.mjs b/examples/src/examples/gaussian-splatting/weather.example.mjs index c838de3e55c..71f8be2b7d2 100644 --- a/examples/src/examples/gaussian-splatting/weather.example.mjs +++ b/examples/src/examples/gaussian-splatting/weather.example.mjs @@ -193,6 +193,7 @@ assetListLoader.load(() => { // Initialize UI data data.set('exposure', 1); + data.set('useFog', true); data.set('preset', 'snow'); applyPreset('snow'); data.set('extents', [weather.extents.x, weather.extents.y, weather.extents.z]); @@ -235,6 +236,9 @@ assetListLoader.load(() => { data.on('exposure:set', () => { app.scene.exposure = data.get('exposure'); }); + data.on('useFog:set', () => { + app.scene.gsplat.useFog = data.get('useFog'); + }); // Grid config — requires rebuild const rebuild = () => { diff --git a/src/scene/gsplat-unified/gsplat-local-dispatch-set.js b/src/scene/gsplat-unified/gsplat-local-dispatch-set.js index 4c1bca291e0..4eb13273ac4 100644 --- a/src/scene/gsplat-unified/gsplat-local-dispatch-set.js +++ b/src/scene/gsplat-unified/gsplat-local-dispatch-set.js @@ -353,7 +353,7 @@ class GSplatLocalDispatchSet { if (pickMode) cdefines.set('PICK_MODE', ''); if (depthTest) cdefines.set('DEPTH_TEST', ''); cdefines.set('GAMMA', 'SRGB'); // assumes splat colors are in gamma space, will need to change when we get linear splats - if (hasFog) cdefines.set('FOG', fogType.toUpperCase()); + cdefines.set('FOG', hasFog ? fogType.toUpperCase() : 'NONE'); const cincludes = pickMode ? undefined : new Map([['decodePS', shaderChunksWGSL.decodePS]]); if (hasFog && cincludes) { diff --git a/src/scene/gsplat-unified/gsplat-manager.js b/src/scene/gsplat-unified/gsplat-manager.js index 57bd2845e6e..a4e6201ff29 100644 --- a/src/scene/gsplat-unified/gsplat-manager.js +++ b/src/scene/gsplat-unified/gsplat-manager.js @@ -1506,7 +1506,7 @@ class GSplatManager { } // renderer per-frame update (material syncing, deferred setup) - const fogParams = this.cameraNode.camera.fogParams ?? this.scene.fog; + const fogParams = this.scene.gsplat.useFog ? (this.cameraNode.camera.fogParams ?? this.scene.fog) : null; this.renderer.frameUpdate(this.scene.gsplat, this.scene.exposure, fogParams); // camera tracking only after first sort diff --git a/src/scene/gsplat-unified/gsplat-params.js b/src/scene/gsplat-unified/gsplat-params.js index f574a6ce023..b05ee8a77a7 100644 --- a/src/scene/gsplat-unified/gsplat-params.js +++ b/src/scene/gsplat-unified/gsplat-params.js @@ -471,6 +471,14 @@ class GSplatParams { */ colorRampIntensity = 1; + /** + * Whether to apply scene fog to Gaussian splats. When false, splats ignore fog settings + * even if the scene or camera has fog configured. Defaults to true. + * + * @type {boolean} + */ + useFog = true; + /** * Enables debug colorization to visualize when spherical harmonics are evaluated. * When true, each update pass renders with a random color to visualize the behavior diff --git a/src/scene/gsplat-unified/gsplat-quad-renderer.js b/src/scene/gsplat-unified/gsplat-quad-renderer.js index ab497bd33cc..adaea605e45 100644 --- a/src/scene/gsplat-unified/gsplat-quad-renderer.js +++ b/src/scene/gsplat-unified/gsplat-quad-renderer.js @@ -306,6 +306,13 @@ class GSplatQuadRenderer extends GSplatRenderer { this._material.setParameter('fisheye_projMat11', fp.projMat11); } + const noFog = !params.useFog; + if (noFog !== this._lastNoFog) { + this._lastNoFog = noFog; + this._material.setDefine('GSPLAT_NO_FOG', noFog); + this._material.update(); + } + // Check if work buffer format has changed (extra streams added) this._syncWithWorkBufferFormat(); diff --git a/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatOutput.js b/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatOutput.js index 62baeb6208b..09257ac8d24 100644 --- a/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatOutput.js +++ b/src/scene/shader-lib/glsl/chunks/gsplat/vert/gsplatOutput.js @@ -5,17 +5,21 @@ export default /* glsl */` #include "gammaPS" #include "fogPS" +#if FOG != NONE && !defined(GSPLAT_NO_FOG) + #define GSPLAT_FOG +#endif + // prepare the output color for the given gamma-space color vec3 prepareOutputFromGamma(vec3 gammaColor, float depth) { vec3 color = gammaColor; // decode to linear when we need linear-space processing - #if TONEMAP != NONE || GAMMA == NONE || FOG != NONE + #if TONEMAP != NONE || GAMMA == NONE || defined(GSPLAT_FOG) color = decodeGamma(color); #endif // apply fog in linear space - #if FOG != NONE + #ifdef GSPLAT_FOG color = addFog(color, depth); #endif @@ -25,7 +29,7 @@ vec3 prepareOutputFromGamma(vec3 gammaColor, float depth) { #endif // encode to gamma when needed - #if TONEMAP != NONE || (GAMMA != NONE && FOG != NONE) + #if TONEMAP != NONE || (GAMMA != NONE && defined(GSPLAT_FOG)) color = gammaCorrectOutput(color); #endif diff --git a/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatOutput.js b/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatOutput.js index 47efafe3251..0c88f95b4a9 100644 --- a/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatOutput.js +++ b/src/scene/shader-lib/wgsl/chunks/gsplat/vert/gsplatOutput.js @@ -5,17 +5,21 @@ export default /* wgsl */` #include "gammaPS" #include "fogPS" +#if FOG != NONE && !defined(GSPLAT_NO_FOG) + #define GSPLAT_FOG +#endif + // prepare the output color for the given gamma-space color fn prepareOutputFromGamma(gammaColor: vec3f, depth: f32) -> vec3f { var color = gammaColor; // decode to linear when we need linear-space processing - #if TONEMAP != NONE || GAMMA == NONE || FOG != NONE + #if TONEMAP != NONE || GAMMA == NONE || defined(GSPLAT_FOG) color = decodeGamma3(color); #endif // apply fog in linear space - #if FOG != NONE + #ifdef GSPLAT_FOG color = addFog(color, depth); #endif @@ -25,7 +29,7 @@ fn prepareOutputFromGamma(gammaColor: vec3f, depth: f32) -> vec3f { #endif // encode to gamma when needed - #if TONEMAP != NONE || (GAMMA != NONE && FOG != NONE) + #if TONEMAP != NONE || (GAMMA != NONE && defined(GSPLAT_FOG)) color = gammaCorrectOutput(color); #endif