Skip to content

Commit 8677e2d

Browse files
mvaligurskyMartin Valigursky
andauthored
Add Gaussian Splat AABB Crop Example with Edge Clipping (#8236)
* Add Gaussian Splat AABB Crop Example with Edge Clipping * update bounds * lint --------- Co-authored-by: Martin Valigursky <[email protected]>
1 parent dc37e4d commit 8677e2d

File tree

5 files changed

+437
-0
lines changed

5 files changed

+437
-0
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* @param {import('../../app/components/Example.mjs').ControlOptions} options - The options.
3+
* @returns {JSX.Element} The returned JSX Element.
4+
*/
5+
export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
6+
const { BindingTwoWay, LabelGroup, BooleanInput, Button, SliderInput } = ReactPCUI;
7+
8+
return fragment(
9+
jsx(
10+
LabelGroup,
11+
{ text: 'Precise' },
12+
jsx(BooleanInput, {
13+
type: 'toggle',
14+
binding: new BindingTwoWay(),
15+
link: { observer, path: 'precise' }
16+
})
17+
),
18+
jsx(
19+
LabelGroup,
20+
{ text: 'Edge Scale' },
21+
jsx(SliderInput, {
22+
binding: new BindingTwoWay(),
23+
link: { observer, path: 'edgeScale' },
24+
min: 0.1,
25+
max: 1.0,
26+
precision: 2
27+
})
28+
),
29+
jsx(Button, {
30+
text: 'Pause / Play',
31+
onClick: () => {
32+
observer.emit('togglePause');
33+
}
34+
})
35+
);
36+
};
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// @config DESCRIPTION This example demonstrates AABB-based cropping of gaussian splats with animated bounds.
2+
import { data } from 'examples/observer';
3+
import { deviceType, rootPath, fileImport } from 'examples/utils';
4+
import * as pc from 'playcanvas';
5+
6+
const { GsplatCropShaderEffect } = await fileImport(`${rootPath}/static/scripts/esm/gsplat/shader-effect-crop.mjs`);
7+
8+
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
9+
window.focus();
10+
11+
const gfxOptions = {
12+
deviceTypes: [deviceType],
13+
14+
// disable antialiasing as gaussian splats do not benefit from it and it's expensive
15+
antialias: false
16+
};
17+
18+
const device = await pc.createGraphicsDevice(canvas, gfxOptions);
19+
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
20+
21+
const createOptions = new pc.AppOptions();
22+
createOptions.graphicsDevice = device;
23+
createOptions.mouse = new pc.Mouse(document.body);
24+
createOptions.touch = new pc.TouchDevice(document.body);
25+
26+
createOptions.componentSystems = [
27+
pc.RenderComponentSystem,
28+
pc.CameraComponentSystem,
29+
pc.LightComponentSystem,
30+
pc.ScriptComponentSystem,
31+
pc.GSplatComponentSystem
32+
];
33+
createOptions.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler, pc.GSplatHandler];
34+
35+
const app = new pc.AppBase(canvas);
36+
app.init(createOptions);
37+
38+
// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
39+
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
40+
app.setCanvasResolution(pc.RESOLUTION_AUTO);
41+
42+
// Ensure canvas is resized when window changes size
43+
const resize = () => app.resizeCanvas();
44+
window.addEventListener('resize', resize);
45+
app.on('destroy', () => {
46+
window.removeEventListener('resize', resize);
47+
});
48+
49+
const assets = {
50+
hotel: new pc.Asset('gsplat', 'gsplat', { url: `${rootPath}/static/assets/splats/hotel-culpture.compressed.ply` }),
51+
orbit: new pc.Asset('script', 'script', { url: `${rootPath}/static/scripts/camera/orbit-camera.js` })
52+
};
53+
54+
const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
55+
assetListLoader.load(() => {
56+
app.start();
57+
58+
// Default precise mode to true, paused to false, edge scale to 0.5
59+
data.set('precise', true);
60+
data.set('edgeScale', 0.5);
61+
let paused = false;
62+
63+
// Handle pause/play toggle
64+
data.on('togglePause', () => {
65+
paused = !paused;
66+
});
67+
68+
// Create hotel gsplat with unified set to true
69+
const hotel = new pc.Entity('hotel');
70+
hotel.addComponent('gsplat', {
71+
asset: assets.hotel,
72+
unified: true
73+
});
74+
hotel.setLocalEulerAngles(180, 0, 0);
75+
app.root.addChild(hotel);
76+
77+
// Add script component to the hotel entity
78+
hotel.addComponent('script');
79+
80+
// Create the crop effect script
81+
const cropScript = hotel.script?.create(GsplatCropShaderEffect);
82+
83+
// Set initial edge scale factor
84+
if (cropScript) {
85+
cropScript.edgeScaleFactor = data.get('edgeScale');
86+
}
87+
88+
// Handle edge scale changes
89+
data.on('edgeScale:set', () => {
90+
if (cropScript) {
91+
cropScript.edgeScaleFactor = data.get('edgeScale');
92+
}
93+
});
94+
95+
// Get the gsplat material
96+
const getMaterial = () => app.scene.gsplat?.material;
97+
98+
// Set initial define state
99+
/**
100+
* @param {boolean} precise - Whether to enable precise cropping
101+
*/
102+
const updatePreciseDefine = (precise) => {
103+
const material = getMaterial();
104+
if (material) {
105+
if (precise) {
106+
material.setDefine('GSPLAT_PRECISE_CROP', '');
107+
} else {
108+
material.defines.delete('GSPLAT_PRECISE_CROP');
109+
}
110+
material.update();
111+
}
112+
};
113+
114+
// Wait for material to be available, then set initial state
115+
const checkMaterial = () => {
116+
const material = getMaterial();
117+
if (material) {
118+
updatePreciseDefine(data.get('precise'));
119+
} else {
120+
setTimeout(checkMaterial, 100);
121+
}
122+
};
123+
checkMaterial();
124+
125+
// Handle precise toggle changes
126+
data.on('precise:set', () => {
127+
updatePreciseDefine(data.get('precise'));
128+
});
129+
130+
// Create an Entity with a camera component
131+
const camera = new pc.Entity();
132+
camera.addComponent('camera', {
133+
clearColor: pc.Color.BLACK,
134+
fov: 80
135+
});
136+
camera.setLocalPosition(3, 1, 0.5);
137+
138+
// add orbit camera script with a mouse and a touch support
139+
camera.addComponent('script');
140+
camera.script?.create('orbitCamera', {
141+
attributes: {
142+
inertiaFactor: 0.2,
143+
focusEntity: hotel,
144+
distanceMax: 2,
145+
frameOnStart: false
146+
}
147+
});
148+
camera.script?.create('orbitCameraInputMouse');
149+
camera.script?.create('orbitCameraInputTouch');
150+
app.root.addChild(camera);
151+
152+
// Setup bloom post-processing
153+
if (camera.camera) {
154+
const cameraFrame = new pc.CameraFrame(app, camera.camera);
155+
cameraFrame.rendering.samples = 4;
156+
cameraFrame.rendering.toneMapping = pc.TONEMAP_ACES;
157+
cameraFrame.bloom.intensity = 0.03;
158+
cameraFrame.bloom.blurLevel = 6;
159+
cameraFrame.update();
160+
}
161+
162+
// Auto-rotate camera when idle
163+
let autoRotateEnabled = true;
164+
let lastInteractionTime = 0;
165+
const autoRotateDelay = 2; // seconds of inactivity before auto-rotate resumes
166+
const autoRotateSpeed = 10; // degrees per second
167+
168+
// Detect user interaction (click/touch only, not mouse movement)
169+
const onUserInteraction = () => {
170+
autoRotateEnabled = false;
171+
lastInteractionTime = Date.now();
172+
};
173+
174+
// Listen for click and touch events only
175+
if (app.mouse) {
176+
app.mouse.on('mousedown', onUserInteraction);
177+
app.mouse.on('mousewheel', onUserInteraction);
178+
}
179+
if (app.touch) {
180+
app.touch.on('touchstart', onUserInteraction);
181+
}
182+
183+
// Animate AABB size with soft bounce
184+
const period = 9.0; // seconds for one cycle
185+
const minSize = 0.4;
186+
const maxSize = 1.75;
187+
let elapsedTime = 0;
188+
189+
app.on('update', (dt) => {
190+
// Re-enable auto-rotate after delay
191+
if (!autoRotateEnabled && (Date.now() - lastInteractionTime) / 1000 > autoRotateDelay) {
192+
autoRotateEnabled = true;
193+
}
194+
195+
// Apply auto-rotation
196+
if (autoRotateEnabled) {
197+
const orbitCamera = camera.script?.get('orbitCamera');
198+
if (orbitCamera) {
199+
orbitCamera.yaw += autoRotateSpeed * dt;
200+
}
201+
}
202+
203+
// Animate AABB with soft bounce (sin-based easing)
204+
if (cropScript && !paused) {
205+
elapsedTime += dt;
206+
const t = (Math.sin(elapsedTime * Math.PI * 2 / period) + 1) / 2; // 0 to 1, soft bounce
207+
const size = minSize + t * (maxSize - minSize);
208+
const sizeXZ = size * 1.5; // 50% wider in X and Z directions
209+
cropScript.aabbMin.set(-sizeXZ, -size, -sizeXZ);
210+
cropScript.aabbMax.set(sizeXZ, size, sizeXZ);
211+
}
212+
});
213+
});
214+
215+
export { app };
3.81 KB
Loading
360 Bytes
Loading

0 commit comments

Comments
 (0)