From 44f316213d3ab9ac62e87bde0a2bb3dcf27ba323 Mon Sep 17 00:00:00 2001 From: Martin Valigursky Date: Fri, 1 May 2026 14:05:42 +0100 Subject: [PATCH] examples: unified GSplat renderer dropdown and orbit pivots Multi-splat, simple, and spherical-harmonics: renderer observer, controls where missing, unified splats, scene alphaClip, pivot-based orbit (no focusEntity for unified). --- .../multi-splat.controls.mjs | 19 ++++++++- .../multi-splat.example.mjs | 9 +++++ .../gaussian-splatting/simple.controls.mjs | 26 +++++++++++++ .../gaussian-splatting/simple.example.mjs | 38 +++++++++++++----- .../spherical-harmonics.controls.mjs | 26 +++++++++++++ .../spherical-harmonics.example.mjs | 39 ++++++++++++++----- 6 files changed, 137 insertions(+), 20 deletions(-) create mode 100644 examples/src/examples/gaussian-splatting/simple.controls.mjs create mode 100644 examples/src/examples/gaussian-splatting/spherical-harmonics.controls.mjs diff --git a/examples/src/examples/gaussian-splatting/multi-splat.controls.mjs b/examples/src/examples/gaussian-splatting/multi-splat.controls.mjs index 3bc89356a50..0368d7c5132 100644 --- a/examples/src/examples/gaussian-splatting/multi-splat.controls.mjs +++ b/examples/src/examples/gaussian-splatting/multi-splat.controls.mjs @@ -6,8 +6,25 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => { if (observer.get('shader') === undefined) { observer.set('shader', false); } - const { Button } = ReactPCUI; + const { BindingTwoWay, Button, LabelGroup, SelectInput } = ReactPCUI; return fragment( + jsx( + LabelGroup, + { text: 'Renderer' }, + jsx(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' }, + { v: 4, t: 'Raster (Hybrid)' } + ] + }) + ), jsx(Button, { text: 'Custom Shader', onClick: () => { diff --git a/examples/src/examples/gaussian-splatting/multi-splat.example.mjs b/examples/src/examples/gaussian-splatting/multi-splat.example.mjs index 0186002447f..3d635992b02 100644 --- a/examples/src/examples/gaussian-splatting/multi-splat.example.mjs +++ b/examples/src/examples/gaussian-splatting/multi-splat.example.mjs @@ -56,6 +56,15 @@ const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets assetListLoader.load(() => { app.start(); + data.on('renderer:set', () => { + app.scene.gsplat.renderer = data.get('renderer'); + const current = app.scene.gsplat.currentRenderer; + if (current !== data.get('renderer')) { + setTimeout(() => data.set('renderer', current), 0); + } + }); + data.set('renderer', pc.GSPLAT_RENDERER_AUTO); + // camera placement const ORBIT_PIVOT = new pc.Vec3(0, 0.8, 0); const ORBIT_DISTANCE = 5; diff --git a/examples/src/examples/gaussian-splatting/simple.controls.mjs b/examples/src/examples/gaussian-splatting/simple.controls.mjs new file mode 100644 index 00000000000..31c25d1cba2 --- /dev/null +++ b/examples/src/examples/gaussian-splatting/simple.controls.mjs @@ -0,0 +1,26 @@ +/** + * @param {import('../../app/components/Example.mjs').ControlOptions} options - The options. + * @returns {JSX.Element} The returned JSX Element. + */ +export const controls = ({ observer, ReactPCUI, jsx, fragment }) => { + const { BindingTwoWay, LabelGroup, SelectInput } = ReactPCUI; + return fragment( + jsx( + LabelGroup, + { text: 'Renderer' }, + jsx(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' }, + { v: 4, t: 'Raster (Hybrid)' } + ] + }) + ) + ); +}; diff --git a/examples/src/examples/gaussian-splatting/simple.example.mjs b/examples/src/examples/gaussian-splatting/simple.example.mjs index 5376dcdad05..3078fdbf28e 100644 --- a/examples/src/examples/gaussian-splatting/simple.example.mjs +++ b/examples/src/examples/gaussian-splatting/simple.example.mjs @@ -1,4 +1,5 @@ // @config DESCRIPTION Basic example showing a simple Gaussian Splat with orbit camera controls. +import { data } from 'examples/observer'; import { deviceType, rootPath } from 'examples/utils'; import * as pc from 'playcanvas'; @@ -52,19 +53,36 @@ const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets assetListLoader.load(() => { app.start(); + data.on('renderer:set', () => { + app.scene.gsplat.renderer = data.get('renderer'); + const current = app.scene.gsplat.currentRenderer; + if (current !== data.get('renderer')) { + setTimeout(() => data.set('renderer', current), 0); + } + }); + data.set('renderer', pc.GSPLAT_RENDERER_AUTO); + // create a splat entity and place it in the world const biker = new pc.Entity(); biker.addComponent('gsplat', { asset: assets.biker, - castShadows: true + castShadows: true, + unified: true }); biker.setLocalPosition(-1.5, 0.05, 0); biker.setLocalEulerAngles(180, 90, 0); biker.setLocalScale(0.7, 0.7, 0.7); app.root.addChild(biker); - // set alpha clip value, used by shadows and picking - biker.gsplat.material.setParameter('alphaClip', 0.4); + // Orbit pivot at splat (unified gsplats have no mesh AABB for focusEntity framing). + const ORBIT_PIVOT = new pc.Vec3().copy(biker.getPosition()); + ORBIT_PIVOT.y += 1; + const ORBIT_DISTANCE = 4; + const ORBIT_INITIAL_YAW = 32; + const ORBIT_INITIAL_PITCH = -10; + + // alpha clip for unified splats (shadows); scene-level for unified path + app.scene.gsplat.alphaClip = 0.4; // Create an Entity with a camera component const camera = new pc.Entity(); @@ -72,21 +90,23 @@ assetListLoader.load(() => { clearColor: new pc.Color(0.2, 0.2, 0.2), toneMapping: pc.TONEMAP_ACES }); - camera.setLocalPosition(-0.8, 2, 3); + app.root.addChild(camera); - // add orbit camera script with a mouse and a touch support camera.addComponent('script'); - camera.script.create('orbitCamera', { + const orbitCam = /** @type {any} */ (camera.script.create('orbitCamera', { attributes: { inertiaFactor: 0.2, - focusEntity: biker, distanceMax: 60, frameOnStart: false } - }); + })); + if (orbitCam) { + orbitCam.pivotPoint.copy(ORBIT_PIVOT); + orbitCam.reset(ORBIT_INITIAL_YAW, ORBIT_INITIAL_PITCH, ORBIT_DISTANCE); + orbitCam._updatePosition(); + } camera.script.create('orbitCameraInputMouse'); camera.script.create('orbitCameraInputTouch'); - app.root.addChild(camera); // create ground to receive shadows const material = new pc.StandardMaterial(); diff --git a/examples/src/examples/gaussian-splatting/spherical-harmonics.controls.mjs b/examples/src/examples/gaussian-splatting/spherical-harmonics.controls.mjs new file mode 100644 index 00000000000..31c25d1cba2 --- /dev/null +++ b/examples/src/examples/gaussian-splatting/spherical-harmonics.controls.mjs @@ -0,0 +1,26 @@ +/** + * @param {import('../../app/components/Example.mjs').ControlOptions} options - The options. + * @returns {JSX.Element} The returned JSX Element. + */ +export const controls = ({ observer, ReactPCUI, jsx, fragment }) => { + const { BindingTwoWay, LabelGroup, SelectInput } = ReactPCUI; + return fragment( + jsx( + LabelGroup, + { text: 'Renderer' }, + jsx(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' }, + { v: 4, t: 'Raster (Hybrid)' } + ] + }) + ) + ); +}; diff --git a/examples/src/examples/gaussian-splatting/spherical-harmonics.example.mjs b/examples/src/examples/gaussian-splatting/spherical-harmonics.example.mjs index 851c7228032..dff3d07e587 100644 --- a/examples/src/examples/gaussian-splatting/spherical-harmonics.example.mjs +++ b/examples/src/examples/gaussian-splatting/spherical-harmonics.example.mjs @@ -1,4 +1,5 @@ // @config DESCRIPTION Shows view-dependent color effects using spherical harmonics with Gaussian Splats. +import { data } from 'examples/observer'; import { deviceType, rootPath } from 'examples/utils'; import * as pc from 'playcanvas'; @@ -52,20 +53,36 @@ const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets assetListLoader.load(() => { app.start(); + data.on('renderer:set', () => { + app.scene.gsplat.renderer = data.get('renderer'); + const current = app.scene.gsplat.currentRenderer; + if (current !== data.get('renderer')) { + setTimeout(() => data.set('renderer', current), 0); + } + }); + data.set('renderer', pc.GSPLAT_RENDERER_AUTO); + // create a splat entity and place it in the world const skull = new pc.Entity(); skull.addComponent('gsplat', { asset: assets.skull, - castShadows: true + castShadows: true, + unified: true }); skull.setLocalPosition(-1.5, 0.05, 0); skull.setLocalEulerAngles(180, 90, 0); skull.setLocalScale(0.7, 0.7, 0.7); app.root.addChild(skull); - // set alpha clip value, used by shadows and picking - skull.gsplat.material.setParameter('alphaClip', 0.4); - skull.gsplat.material.setParameter('alphaClip', 0.1); + // Orbit pivot at splat (unified gsplats have no mesh AABB for focusEntity framing). + const ORBIT_PIVOT = new pc.Vec3().copy(skull.getPosition()); + ORBIT_PIVOT.y += 0.2; + const ORBIT_DISTANCE = 4; + const ORBIT_INITIAL_YAW = 32; + const ORBIT_INITIAL_PITCH = -10; + + // alpha clip for unified splats (shadows / cutout); scene-level for unified path + app.scene.gsplat.alphaClip = 0.1; // Create an Entity with a camera component const camera = new pc.Entity(); @@ -73,21 +90,23 @@ assetListLoader.load(() => { clearColor: new pc.Color(0.2, 0.2, 0.2), toneMapping: pc.TONEMAP_ACES }); - camera.setLocalPosition(-2, 1.5, 2); + app.root.addChild(camera); - // add orbit camera script with a mouse and a touch support camera.addComponent('script'); - camera.script.create('orbitCamera', { + const orbitCam = /** @type {any} */ (camera.script.create('orbitCamera', { attributes: { inertiaFactor: 0.2, - focusEntity: skull, distanceMax: 60, frameOnStart: false } - }); + })); + if (orbitCam) { + orbitCam.pivotPoint.copy(ORBIT_PIVOT); + orbitCam.reset(ORBIT_INITIAL_YAW, ORBIT_INITIAL_PITCH, ORBIT_DISTANCE); + orbitCam._updatePosition(); + } camera.script.create('orbitCameraInputMouse'); camera.script.create('orbitCameraInputTouch'); - app.root.addChild(camera); // create ground to receive shadows const material = new pc.StandardMaterial();