|
| 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 }; |
0 commit comments