Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
12 changes: 12 additions & 0 deletions examples/src/examples/gaussian-splatting/lod-streaming.example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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';
Expand Down
35 changes: 34 additions & 1 deletion examples/src/examples/gaussian-splatting/weather.controls.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -16,10 +16,43 @@ 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(
LabelGroup,
{ text: 'Splat Fog' },
jsx(BooleanInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'useFog' }
})
)
),
jsx(
Expand Down
59 changes: 55 additions & 4 deletions examples/src/examples/gaussian-splatting/weather.example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -149,9 +176,24 @@ 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('useFog', true);
data.set('preset', 'snow');
applyPreset('snow');
data.set('extents', [weather.extents.x, weather.extents.y, weather.extents.z]);
Expand Down Expand Up @@ -188,6 +230,15 @@ 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');
});
data.on('useFog:set', () => {
app.scene.gsplat.useFog = data.get('useFog');
});

// Grid config — requires rebuild
const rebuild = () => {
Expand Down
24 changes: 21 additions & 3 deletions src/scene/gsplat-unified/gsplat-compute-local-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -241,7 +244,7 @@ class GSplatComputeLocalRenderer extends GSplatRenderer {
super.setRenderMode(renderMode);
}

frameUpdate(gsplat, exposure) {
frameUpdate(gsplat, exposure, fogParams) {
if (this._needsFramePassRegister) {
this._registerFramePass();
}
Expand All @@ -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) {
Expand Down Expand Up @@ -583,14 +587,28 @@ 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);
rasterizeCompute.setParameter('numTilesX', numTilesX);
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);
Expand Down
35 changes: 28 additions & 7 deletions src/scene/gsplat-unified/gsplat-local-dispatch-set.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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),
Expand All @@ -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
cdefines.set('FOG', hasFog ? fogType.toUpperCase() : 'NONE');

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';
Expand Down
3 changes: 2 additions & 1 deletion src/scene/gsplat-unified/gsplat-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.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
if (sortedState?.sortedBefore) {
Expand Down
8 changes: 8 additions & 0 deletions src/scene/gsplat-unified/gsplat-params.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/scene/gsplat-unified/gsplat-quad-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading