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
44 changes: 44 additions & 0 deletions examples/src/examples/gaussian-splatting/shader-rings.controls.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
BindingTwoWay,
LabelGroup,
Panel,
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='Rings'>
<LabelGroup text='Width (px)'>
<SliderInput
binding={new BindingTwoWay()}
link={{ observer, path: 'ringWidth' }}
min={1}
max={10}
precision={1}
step={0.5}
/>
</LabelGroup>
<LabelGroup text='Alpha'>
<SliderInput
binding={new BindingTwoWay()}
link={{ observer, path: 'ringAlpha' }}
min={0.05}
max={1}
precision={2}
step={0.05}
/>
</LabelGroup>
</Panel>
</>
);
}
154 changes: 154 additions & 0 deletions examples/src/examples/gaussian-splatting/shader-rings.example.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// @config
//
// This example demonstrates per-pixel customization of gaussian splat rendering using the
// gsplatModifyPS shader chunk. Each splat is rendered as a ring of its own color, with an
// adjustable ring width and a time based highlight pulse.

import * as pc from 'playcanvas';

import { data, deviceType } from 'examples/context';

import shaderGlslFrag from './shader.glsl.frag';
import shaderWgslFrag from './shader.wgsl.frag';

const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();

const gfxOptions = {
deviceTypes: [deviceType],

// disable antialiasing as gaussian splats do not benefit from it and it's expensive
antialias: false
};

const device = await pc.createGraphicsDevice(canvas, gfxOptions);
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);

const createOptions = new pc.AppOptions();
createOptions.graphicsDevice = device;
createOptions.mouse = new pc.Mouse(document.body);
createOptions.touch = new pc.TouchDevice(document.body);

createOptions.componentSystems = [
pc.RenderComponentSystem,
pc.CameraComponentSystem,
pc.LightComponentSystem,
pc.ScriptComponentSystem,
pc.GSplatComponentSystem
];
createOptions.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler, pc.GSplatHandler];

const app = new pc.AppBase(canvas);
app.init(createOptions);

// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);

// Ensure canvas is resized when window changes size
const resize = () => app.resizeCanvas();
window.addEventListener('resize', resize);
app.on('destroy', () => {
window.removeEventListener('resize', resize);
});

const assets = {
skull: new pc.Asset('gsplat', 'gsplat', { url: './assets/splats/skull.compressed.ply' }),
orbit: new pc.Asset('script', 'script', { url: './scripts/camera/orbit-camera.js' })
};

const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
assetListLoader.load(() => {
app.start();

data.set('ringWidth', 1);
data.set('ringAlpha', 0.25);

// update spherical harmonics colors every degree of camera movement
app.scene.gsplat.colorUpdateAngle = 1;

// apply the custom fragment chunk to the scene-wide gsplat material
const material = app.scene.gsplat.material;
material.getShaderChunks('glsl').set('gsplatModifyPS', shaderGlslFrag);
material.getShaderChunks('wgsl').set('gsplatModifyPS', shaderWgslFrag);
material.update();

// Create skull gsplat
const skull = new pc.Entity('skull');
skull.addComponent('gsplat', {
asset: assets.skull
});
skull.setLocalEulerAngles(180, 90, 0);
skull.setLocalScale(0.7, 0.7, 0.7);
app.root.addChild(skull);

// Create an Entity with a camera component
const camera = new pc.Entity();
camera.addComponent('camera', {
clearColor: pc.Color.BLACK,
fov: 80
});
app.root.addChild(camera);

// add orbit camera script with a mouse and a touch support
camera.addComponent('script');
const orbitCam = /** @type {any} */ (camera.script?.create('orbitCamera', {
attributes: {
inertiaFactor: 0.2,
distanceMax: 6,
frameOnStart: false
}
}));
if (orbitCam) {
orbitCam.pivotPoint.copy(new pc.Vec3(0, 0.9, -0.28));
orbitCam.reset(88, -28, 0.9);
orbitCam._updatePosition();
}
camera.script?.create('orbitCameraInputMouse');
camera.script?.create('orbitCameraInputTouch');

// Auto-rotate camera when idle
let autoRotateEnabled = true;
let lastInteractionTime = 0;
const autoRotateDelay = 2; // seconds of inactivity before auto-rotate resumes
const autoRotateSpeed = 10; // degrees per second

// Detect user interaction (click/touch only, not mouse movement)
const onUserInteraction = () => {
autoRotateEnabled = false;
lastInteractionTime = Date.now();
};

// Listen for click and touch events only
if (app.mouse) {
app.mouse.on('mousedown', onUserInteraction);
app.mouse.on('mousewheel', onUserInteraction);
}
if (app.touch) {
app.touch.on('touchstart', onUserInteraction);
}

let time = 0;
app.on('update', (dt) => {
time += dt;

// drive the shader uniforms
material.setParameter('uRingWidth', data.get('ringWidth'));
material.setParameter('uRingAlpha', data.get('ringAlpha'));
material.setParameter('uTime', time);
material.update();

// Re-enable auto-rotate after delay
if (!autoRotateEnabled && (Date.now() - lastInteractionTime) / 1000 > autoRotateDelay) {
autoRotateEnabled = true;
}

// Apply auto-rotation
if (autoRotateEnabled) {
const orbitCamera = camera.script?.get('orbitCamera');
if (orbitCamera) {
orbitCamera.yaw += autoRotateSpeed * dt;
}
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
uniform float uRingWidth;
uniform float uRingAlpha;
uniform float uTime;
uniform vec4 uScreenSize;

void modifySplatColor(vec2 gaussianUV, inout vec4 color) {
// distance from the splat center: 0 at center, 1 at the clipping edge
float radius = length(gaussianUV);

// sharp ring of constant screen-space width at the splat edge: nothing inside, fixed
// transparency on the ring, replacing the gaussian falloff so the ring is clearly visible.
// fwidth gives the change of radius per screen pixel, converting pixels to radius units.
float radiusPerPixel = fwidth(radius);
float innerEdge = 1.0 - uRingWidth * radiusPerPixel;
float ring = smoothstep(innerEdge - radiusPerPixel, innerEdge, radius);
color.a = ring * uRingAlpha;

// time based highlight pulse, with a screen position based phase so the rings
// light up in a wave instead of all at the same time
float phase = (gl_FragCoord.x + gl_FragCoord.y) * uScreenSize.z * 6.0;
float highlight = pow(0.5 + 0.5 * sin(uTime * 2.0 - phase), 4.0);
color.rgb *= 1.0 + highlight * 1.5;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
uniform uRingWidth: f32;
uniform uRingAlpha: f32;
uniform uTime: f32;
uniform uScreenSize: vec4f;

fn modifySplatColor(gaussianUV: vec2f, color: ptr<function, vec4f>) {
// distance from the splat center: 0 at center, 1 at the clipping edge
let radius = length(gaussianUV);

// sharp ring of constant screen-space width at the splat edge: nothing inside, fixed
// transparency on the ring, replacing the gaussian falloff so the ring is clearly visible.
// fwidth gives the change of radius per screen pixel, converting pixels to radius units.
let radiusPerPixel = fwidth(radius);
let innerEdge = 1.0 - uniform.uRingWidth * radiusPerPixel;
let ring = smoothstep(innerEdge - radiusPerPixel, innerEdge, radius);
let alpha = ring * uniform.uRingAlpha;

// time based highlight pulse, with a screen position based phase so the rings
// light up in a wave instead of all at the same time
let phase = (pcPosition.x + pcPosition.y) * uniform.uScreenSize.z * 6.0;
let highlight = pow(0.5 + 0.5 * sin(uniform.uTime * 2.0 - phase), 4.0);
*color = vec4f((*color).rgb * (1.0 + highlight * 1.5), alpha);
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions src/scene/shader-lib/wgsl/chunks/gsplat/frag/gsplat.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
var output: FragmentOutput;

let A: half = dot(gaussianUV, gaussianUV);

// note: no early return after the discard - it would make the control flow non-uniform,
// preventing user gsplatModifyPS chunks from using derivatives (fwidth etc.)
if (A > half(1.0)) {
discard;
return output;
}

// evaluate alpha
Expand Down Expand Up @@ -84,7 +86,6 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {

if (alpha < half(uniform.alphaClipForward)) {
discard;
return output;
}

#ifndef DITHER_NONE
Expand Down