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
51 changes: 50 additions & 1 deletion examples/src/examples/gaussian-splatting/downtown.example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ applyResolution();
const resize = () => {
applyResolution();
app.resizeCanvas();
// With on-demand rendering (autoRender is set to false once the reveal completes), a resize is
// a viewport change the app makes itself — it does not raise 'frame:request' — so request a
// render to draw the scene at the new canvas size.
app.renderNextFrame = true;
};
window.addEventListener('resize', resize);
app.on('destroy', () => {
Expand Down Expand Up @@ -321,12 +325,57 @@ assetListLoader.load(() => {
updateQualityButtons();
data.on('splatBudget:set', updateQualityButtons);

// --- Stats ---
// --- On-demand rendering ----------------------------------------------------------------
// Gaussian-splat streaming (LOD evaluation + file loading) runs every frame regardless of
// rendering. We render continuously while the scene loads and the reveal animation plays, then
// switch to rendering only on demand: when streaming has new data to show (the 'frame:request'
// event), when the camera moves, or on a resize / UI change (handled where those occur). This
// keeps the GPU idle while the huge city sits still, yet still streams in the background.

// render whenever streaming produced new data (or a CPU sort result became ready to apply)
app.systems.gsplat.on('frame:request', () => {
app.renderNextFrame = true;
});

let onDemand = false;
const lastCamPos = new pc.Vec3();
const lastCamRot = new pc.Quat();

// --- Stats + on-demand driver ---
app.on('update', () => {
// keep the reveal frozen (all splats hidden) until it is released on frame:ready
if (!revealStarted) reveal.effectTime = -1e6;

// update HUD stats
data.set('data.stats.gsplats', toM(app.stats.frame.gsplats));
const bb = app.graphicsDevice.backBufferSize;
data.set('data.stats.resolution', `${bb.x} x ${bb.y}`);

if (!onDemand) {
// The reveal starts once the first frame is ready and disables itself when its animation
// completes. Until then autoRender stays true so the scene streams in and the reveal
// animates every frame; once it finishes, switch to on-demand rendering.
if (revealStarted && reveal && !reveal.enabled) {
onDemand = true;
app.autoRender = false;

// The reveal finishing is a draw-state change (it stops masking the splats), not a
// streaming change, so it does not raise 'frame:request'. Render one final frame so
// the fully-revealed scene is shown before we go idle.
app.renderNextFrame = true;

lastCamPos.copy(camera.getPosition());
lastCamRot.copy(camera.getRotation());
}
} else {
// keep the fly camera interactive: render when it has moved or rotated this frame
const pos = camera.getPosition();
const rot = camera.getRotation();
if (!pos.equals(lastCamPos) || !rot.equals(lastCamRot)) {
app.renderNextFrame = true;
lastCamPos.copy(pos);
lastCamRot.copy(rot);
}
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@ app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);

// Ensure canvas is updated when window changes size
const onResize = () => app.resizeCanvas();
const onResize = () => {
app.resizeCanvas();
// With on-demand rendering (autoRender is set to false once the reveal completes), a resize is
// a viewport change the app makes itself — it does not raise 'frame:request' — so request a
// render to draw the scene at the new canvas size.
app.renderNextFrame = true;
};
window.addEventListener('resize', onResize);
app.on('destroy', () => {
window.removeEventListener('resize', onResize);
Expand Down Expand Up @@ -219,8 +225,51 @@ assetListLoader.load(() => {
focusPoint: focusPoint
});

// update HUD stats every frame
// --- On-demand rendering demo -----------------------------------------------------------
// Gaussian-splat streaming (LOD evaluation + file loading) runs every frame regardless of
// rendering. We render continuously while the reveal animation plays, then switch to rendering
// only on demand: when streaming has new data to show (the 'frame:request' event) or when the
// camera moves. This lets an otherwise-idle app keep loading splats in the background while
// staying GPU-idle until there's something new to draw.

// render whenever streaming produced new data (or a CPU sort result became ready to apply)
app.systems.gsplat.on('frame:request', () => {
app.renderNextFrame = true;
});

let revealPlaying = true;
const lastCamPos = new pc.Vec3();
const lastCamRot = new pc.Quat();

app.on('update', () => {
// update HUD stats
data.set('data.stats.gsplats', app.stats.frame.gsplats.toLocaleString());

if (revealPlaying) {
// the reveal script disables itself when its animation completes; once that happens,
// stop rendering every frame and switch to on-demand rendering.
if (revealScript && !revealScript.enabled) {
revealPlaying = false;
app.autoRender = false;

// The reveal finishing is a draw-state change (it ends the per-frame splat masking),
// not a streaming change, so it does not raise 'frame:request'. Render one final
// frame so the fully-revealed scene — including the far environment/sky splats, which
// the eruption reveals last — is shown before we go idle.
app.renderNextFrame = true;

lastCamPos.copy(camera.getPosition());
lastCamRot.copy(camera.getRotation());
}
} else {
// keep the fly camera interactive: render when it has moved or rotated this frame
const pos = camera.getPosition();
const rot = camera.getRotation();
if (!pos.equals(lastCamPos) || !rot.equals(lastCamRot)) {
app.renderNextFrame = true;
lastCamPos.copy(pos);
lastCamRot.copy(rot);
}
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { BindingTwoWay, LabelGroup, Panel, SelectInput, SliderInput } from '@playcanvas/pcui/react';

/**
* @import { Observer } from '@playcanvas/observer'
* @import { ReactElement } from 'react'
*/

/**
* @param {{ observer: Observer }} props - The control panel props.
* @returns {ReactElement} The control panel.
*/
export function Controls({ observer }) {
return (
<>
<Panel headerText='Scene'>
<LabelGroup text='Shadow Clip'>
<SliderInput
binding={new BindingTwoWay()}
link={{ observer, path: 'alphaClip' }}
min={0}
max={1}
precision={3}
/>
</LabelGroup>
<LabelGroup text='Alpha Clip'>
<SliderInput
binding={new BindingTwoWay()}
link={{ observer, path: 'alphaClipForward' }}
min={0}
max={1}
precision={4}
/>
</LabelGroup>
<LabelGroup text='Renderer'>
<SelectInput
type='number'
binding={new BindingTwoWay()}
link={{ observer, path: 'renderer' }}
value={observer.get('renderer') ?? 0}
options={[
{ v: 0, t: 'Auto' },
{ v: 1, t: 'Raster (CPU Sort)' },
{ v: 2, t: 'Raster (GPU Sort)' },
{ v: 3, t: 'Compute' }
]}
/>
</LabelGroup>
</Panel>
</>
);
}
Loading