Skip to content
Open
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
63 changes: 63 additions & 0 deletions examples/src/examples/gaussian-splatting/first-person.controls.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @param {import('../../app/components/Example.mjs').ControlOptions} options - The options.
* @returns {JSX.Element} The returned JSX Element.
*/
export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
const { BindingTwoWay, LabelGroup, Panel, SelectInput, SliderInput, Label } = ReactPCUI;
return fragment(
jsx(
Panel,
{ headerText: 'Settings' },
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' }
]
})
),
jsx(
LabelGroup,
{ text: 'Splat Budget (M)' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'splatBudget' },
min: 0,
max: 10,
precision: 2,
step: 0.05
})
)
),
jsx(
Panel,
{ headerText: 'Stats' },
jsx(
LabelGroup,
{ text: 'Resolution' },
jsx(Label, {
binding: new BindingTwoWay(),
link: { observer, path: 'data.stats.resolution' },
value: observer.get('data.stats.resolution')
})
),
jsx(
LabelGroup,
{ text: 'GSplat Count' },
jsx(Label, {
binding: new BindingTwoWay(),
link: { observer, path: 'data.stats.gsplats' },
value: observer.get('data.stats.gsplats')
})
)
)
);
};
204 changes: 204 additions & 0 deletions examples/src/examples/gaussian-splatting/first-person.example.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// @config DESCRIPTION <div style='text-align:center'><div>(<b>WASD</b>) Move</div><div>(<b>Space</b>) Jump</div><div>(<b>Mouse</b>) Look</div></div>
//
// Scene attribution:
// Title: Sunnyvale Heritage Park Museum
// Author: zeitgeistarchivescans
// Source: https://superspl.at/scene/d5d397aa
// License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)

import { data } from 'examples/observer';
import { deviceType, rootPath, fileImport } from 'examples/utils';
import * as pc from 'playcanvas';

const { FirstPersonController } = await fileImport(`${rootPath}/static/scripts/esm/first-person-controller.mjs`);

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

pc.WasmModule.setConfig('Ammo', {
glueUrl: `${rootPath}/static/lib/ammo/ammo.wasm.js`,
wasmUrl: `${rootPath}/static/lib/ammo/ammo.wasm.wasm`,
fallbackUrl: `${rootPath}/static/lib/ammo/ammo.js`
});

// the collision GLB uses Draco-compressed meshes, so the Draco decoder is required
pc.WasmModule.setConfig('DracoDecoderModule', {
glueUrl: `${rootPath}/static/lib/draco/draco.wasm.js`,
wasmUrl: `${rootPath}/static/lib/draco/draco.wasm.wasm`,
fallbackUrl: `${rootPath}/static/lib/draco/draco.js`
});

await Promise.all([
new Promise((resolve) => {
pc.WasmModule.getInstance('Ammo', () => resolve(true));
}),
new Promise((resolve) => {
pc.WasmModule.getInstance('DracoDecoderModule', () => resolve(true));
})
]);

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.gamepads = new pc.GamePads();
createOptions.keyboard = new pc.Keyboard(window);

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

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

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 = {
splat: new pc.Asset('sunnyvale-splat', 'gsplat', { url: 'https://s3.eu-west-1.amazonaws.com/code.playcanvas.com/examples_data/example_sunnyvale/sunnyvale.sog' }),
collision: new pc.Asset('sunnyvale-collision', 'container', { url: 'https://s3.eu-west-1.amazonaws.com/code.playcanvas.com/examples_data/example_sunnyvale/sunnyvale.glb' })
};

await new Promise((resolve) => {
new pc.AssetListLoader(Object.values(assets), app.assets).load(resolve);
});

app.start();

// Initial control values
data.set('renderer', pc.GSPLAT_RENDERER_AUTO);
data.set('splatBudget', 4);
data.set('data.stats.gsplats', '—');
data.set('data.stats.resolution', '—');

// Renderer selection
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);
}
});

// Splat budget (in millions)
const applySplatBudget = () => {
const millions = data.get('splatBudget');
app.scene.gsplat.splatBudget = Math.round(millions * 1000000);
};
applySplatBudget();
data.on('splatBudget:set', applySplatBudget);

// Gravity
app.systems.rigidbody?.gravity.set(0, -10, 0);

// Camera (attached to the character controller below)
const camera = new pc.Entity('camera');
camera.addComponent('camera', {
clearColor: new pc.Color(0.1, 0.1, 0.1),
farClip: 1000,
fov: 75,
toneMapping: pc.TONEMAP_LINEAR
});
camera.setLocalPosition(0, 0.9, 0);

// Parent that holds both the splat and the collision mesh, keeping them aligned.
// The splat data is authored upside-down relative to PlayCanvas's Y-up convention,
// so a 180° rotation around Z flips both the visual and the collision together.
const sceneRoot = new pc.Entity('sunnyvale');
sceneRoot.setLocalEulerAngles(0, 0, 180);
app.root.addChild(sceneRoot);

// Gaussian splat (visual)
const splat = new pc.Entity('sunnyvale-gsplat');
splat.addComponent('gsplat', {
asset: assets.splat,
unified: true
});
sceneRoot.addChild(splat);

// Collision mesh instantiated from the GLB; attached to each render component as
// a static rigidbody using the actual triangle mesh. The mesh itself is hidden -
// it is only used for collision.
const collisionRoot = assets.collision.resource.instantiateRenderEntity();
collisionRoot.findComponents('render').forEach((/** @type {pc.RenderComponent} */ render) => {
const entity = render.entity;
entity.addComponent('rigidbody', {
type: 'static',
friction: 0.5,
restitution: 0
});
entity.addComponent('collision', {
type: 'mesh',
renderAsset: render.asset
});
render.enabled = false;
});
sceneRoot.addChild(collisionRoot);

// First-person character controller
const characterController = new pc.Entity('character-controller');
characterController.setPosition(0, 2, 0);
characterController.addChild(camera);
characterController.addComponent('collision', {
type: 'capsule',
radius: 0.5,
height: 2
});
characterController.addComponent('rigidbody', {
type: 'dynamic',
mass: 100,
linearDamping: 0,
angularDamping: 0,
linearFactor: pc.Vec3.ONE,
angularFactor: pc.Vec3.ZERO,
friction: 0.5,
restitution: 0
});
characterController.addComponent('script');
characterController.script.create(FirstPersonController, {
properties: {
camera,
jumpForce: 420,
speedGround: 65,
sprintMult: 1.73
}
});
app.root.addChild(characterController);

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

export { app };
115 changes: 115 additions & 0 deletions examples/src/examples/gaussian-splatting/third-person.controls.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* @param {import('../../app/components/Example.mjs').ControlOptions} options - The options.
* @returns {JSX.Element} The returned JSX Element.
*/
export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
const { BindingTwoWay, LabelGroup, Panel, SelectInput, SliderInput, Label } = ReactPCUI;
return fragment(
jsx(
Panel,
{ headerText: 'Settings' },
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' }
]
})
),
jsx(
LabelGroup,
{ text: 'Splat Budget (M)' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'splatBudget' },
min: 0,
max: 10,
precision: 2,
step: 0.05
})
)
),
jsx(
Panel,
{ headerText: 'Camera' },
jsx(
LabelGroup,
{ text: 'Distance' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'cameraDistance' },
min: 1,
max: 15,
precision: 2,
step: 0.1
})
),
jsx(
LabelGroup,
{ text: 'Height' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'cameraHeight' },
min: 0,
max: 4,
precision: 2,
step: 0.05
})
),
jsx(
LabelGroup,
{ text: 'Smoothing' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'cameraSmoothing' },
min: 0,
max: 0.01,
precision: 5,
step: 0.0001
})
),
jsx(
LabelGroup,
{ text: 'Look Sens' },
jsx(SliderInput, {
binding: new BindingTwoWay(),
link: { observer, path: 'lookSens' },
min: 0.01,
max: 0.5,
precision: 3,
step: 0.005
})
)
),
jsx(
Panel,
{ headerText: 'Stats' },
jsx(
LabelGroup,
{ text: 'Resolution' },
jsx(Label, {
binding: new BindingTwoWay(),
link: { observer, path: 'data.stats.resolution' },
value: observer.get('data.stats.resolution')
})
),
jsx(
LabelGroup,
{ text: 'GSplat Count' },
jsx(Label, {
binding: new BindingTwoWay(),
link: { observer, path: 'data.stats.gsplats' },
value: observer.get('data.stats.gsplats')
})
)
)
);
};
Loading