styled with CSS. When HTML-in-Canvas is
+// supported it gets appended to the canvas (so it's composited into the WebGL
+// surface and can be used as a texture). Otherwise it falls back to a fixed DOM
+// overlay on top of the canvas.
+const PANEL_WIDTH = 280;
+const PANEL_HEIGHT = 380;
+
+let variants = ['Beach', 'Midnight', 'Street'];
+let activeVariant = '';
+
+// Color swatches for each shoe variant, displayed as conic gradients
+const variantColors = {
+ beach: ['#e8a0b0', '#d4828f', '#f0c0c8'],
+ midnight: ['#2196c8', '#1565a0', '#4fc3f7'],
+ street: ['#2a2a2a', '#e94560', '#1a1a1a']
+};
+
+// Check if the device can use HTML elements as texture sources (HTML-in-Canvas API)
+const supportsHtmlInCanvas = device.supportsHtmlTextures;
+
+const htmlPanel = document.createElement('div');
+htmlPanel.style.width = `${PANEL_WIDTH}px`;
+htmlPanel.style.height = `${PANEL_HEIGHT}px`;
+htmlPanel.style.padding = '20px';
+htmlPanel.style.background = 'rgba(15, 15, 25, 0.375)';
+htmlPanel.style.backdropFilter = 'blur(12px)';
+htmlPanel.style.webkitBackdropFilter = 'blur(12px)';
+htmlPanel.style.borderRadius = '20px';
+htmlPanel.style.fontFamily = '\'Segoe UI\', Arial, sans-serif';
+htmlPanel.style.color = 'white';
+htmlPanel.style.display = 'flex';
+htmlPanel.style.flexDirection = 'column';
+htmlPanel.style.gap = '14px';
+htmlPanel.style.boxSizing = 'border-box';
+htmlPanel.style.border = '1px solid rgba(255,255,255,0.18)';
+htmlPanel.style.boxShadow = '0 8px 40px rgba(0,0,0,0.25)';
+
+if (supportsHtmlInCanvas) {
+ // Positioned at (0,0) with top-left origin — the HtmlSync class will
+ // override the CSS transform each frame to project it onto the 3D plane.
+ htmlPanel.style.position = 'absolute';
+ htmlPanel.style.left = '0';
+ htmlPanel.style.top = '0';
+ htmlPanel.style.transformOrigin = '0 0';
+} else {
+ // Fallback: render as a standard DOM overlay when HTML-in-Canvas is
+ // not available. The panel remains interactive via normal DOM events.
+ htmlPanel.style.position = 'fixed';
+ htmlPanel.style.right = '40px';
+ htmlPanel.style.top = '50%';
+ htmlPanel.style.transform = 'translateY(-50%)';
+ htmlPanel.style.zIndex = '100';
+}
+
+const updatePanel = () => {
+ htmlPanel.innerHTML = `
+
+
Product Configurator
+
Shoe Style
+
Click to change variant
+ ${variants.map(v => `
+
+
+ ${v.charAt(0).toUpperCase() + v.slice(1)}
+
+ `).join('')}
+
+ Powered by HTML-in-Canvas
+
+ `;
+};
+updatePanel();
+
+// --- HTML-to-WebGL texture pipeline ---
+// When HTML-in-Canvas is available, the HTML panel is appended as a child of
+// the canvas and captured into a WebGL texture via texElementImage2D. The
+// browser fires a "paint" event whenever the panel's visual content changes;
+// we respond by re-uploading the texture. The first paint uses setSource() to
+// bind the element, subsequent paints just call upload().
+/** @type {pc.Texture|null} */
+let panelTexture = null;
+
+const onPaintUpload = () => {
+ if (!app.graphicsDevice || !panelTexture) return;
+ panelTexture.upload();
+};
+
+app.on('destroy', () => {
+ window.removeEventListener('resize', resize);
+ canvas.removeEventListener('paint', onPaintUpload);
+ if (htmlPanel.parentNode) htmlPanel.parentNode.removeChild(htmlPanel);
+});
+
+if (supportsHtmlInCanvas) {
+ canvas.appendChild(htmlPanel);
+
+ panelTexture = new pc.Texture(device, {
+ width: PANEL_WIDTH,
+ height: PANEL_HEIGHT,
+ format: pc.PIXELFORMAT_RGBA8,
+ minFilter: pc.FILTER_LINEAR,
+ magFilter: pc.FILTER_LINEAR,
+ name: 'panelTexture'
+ });
+
+ canvas.addEventListener('paint', () => {
+ panelTexture.setSource(/** @type {any} */ (htmlPanel));
+ }, { once: true });
+ canvas.requestPaint();
+ canvas.addEventListener('paint', onPaintUpload);
+} else {
+ document.body.appendChild(htmlPanel);
+}
+
+// --- Load assets and build scene ---
+const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
+assetListLoader.load(() => {
+ app.start();
+
+ // Environment lighting (skybox excluded from camera so background stays white)
+ app.scene.envAtlas = assets.envatlas.resource;
+ app.scene.skyboxIntensity = 2;
+
+ // Layers setup for reflective ground
+ const worldLayer = app.scene.layers.getLayerByName('World');
+ const uiLayer = app.scene.layers.getLayerByName('UI');
+ const depthLayer = app.scene.layers.getLayerById(pc.LAYERID_DEPTH);
+
+ const excludedLayer = new pc.Layer({ name: 'Excluded' });
+ app.scene.layers.insertOpaque(excludedLayer, app.scene.layers.getOpaqueIndex(worldLayer) + 1);
+ app.scene.layers.insertTransparent(excludedLayer, app.scene.layers.getTransparentIndex(worldLayer) + 1);
+
+ // Background plane behind the scene
+ const bgMaterial = new pc.StandardMaterial();
+ bgMaterial.diffuse = new pc.Color(0, 0, 0);
+ bgMaterial.emissiveMap = assets.background.resource;
+ bgMaterial.emissive = pc.Color.WHITE;
+ bgMaterial.useLighting = false;
+ bgMaterial.update();
+
+ const bgPlane = new pc.Entity('background');
+ bgPlane.addComponent('render', {
+ type: 'plane',
+ material: bgMaterial
+ });
+ bgPlane.setLocalPosition(2.2, 2.5, -8);
+ bgPlane.setLocalEulerAngles(90, 0, 0);
+ bgPlane.setLocalScale(30, 1, 30);
+ app.root.addChild(bgPlane);
+
+ // Shoe model
+ const shoeEntity = assets.shoe.resource.instantiateRenderEntity();
+ shoeEntity.setLocalScale(3, 3, 3);
+ shoeEntity.setLocalEulerAngles(0, 0, -20);
+ shoeEntity.setLocalPosition(0, 1.7, 0);
+ app.root.addChild(shoeEntity);
+
+ // Read variant names from the model
+ const modelVariants = assets.shoe.resource.getMaterialVariants();
+ if (modelVariants.length > 0) {
+ variants = modelVariants;
+ }
+ activeVariant = variants[0];
+ updatePanel();
+
+ // 3D panel entity — a plane textured with the live HTML panel texture.
+ // It uses emissive rendering (unlit) with premultiplied alpha blending so
+ // the glassmorphism transparency from CSS is preserved in 3D.
+ let panel = null;
+ if (panelTexture) {
+ const panelMaterial = new pc.StandardMaterial();
+ panelMaterial.diffuse = new pc.Color(0, 0, 0);
+ panelMaterial.emissiveMap = panelTexture;
+ panelMaterial.emissive = pc.Color.WHITE;
+ panelMaterial.useLighting = false;
+ panelMaterial.blendType = pc.BLEND_PREMULTIPLIED;
+ panelMaterial.opacityMap = panelTexture;
+ panelMaterial.opacityMapChannel = 'a';
+ panelMaterial.alphaTest = 0.1;
+ panelMaterial.depthWrite = true;
+ panelMaterial.update();
+
+ panel = new pc.Entity('ui-panel');
+ panel.addComponent('render', {
+ type: 'plane',
+ material: panelMaterial
+ });
+ panel.setLocalPosition(4.5, 2.5, 0);
+ panel.setLocalEulerAngles(90, 0, 0);
+ panel.setLocalScale(2.8, 1, 3.8);
+ app.root.addChild(panel);
+ }
+
+ // Reflective ground plane (in excluded layer so it doesn't render into its own reflection)
+ const groundReflector = new pc.Entity('ground');
+ groundReflector.addComponent('render', {
+ type: 'plane',
+ layers: [excludedLayer.id],
+ castShadows: false
+ });
+ groundReflector.setLocalPosition(0, -0.5, 0);
+ groundReflector.setLocalScale(20, 1, 20);
+
+ groundReflector.addComponent('script');
+ /** @type {BlurredPlanarReflection} */
+ const reflectionScript = groundReflector.script.create(BlurredPlanarReflection);
+ reflectionScript.resolution = 1.0;
+ reflectionScript.blurAmount = 0.3;
+ reflectionScript.intensity = 1.5;
+ reflectionScript.fadeStrength = 0.4;
+ reflectionScript.angleFade = 0.3;
+ reflectionScript.heightRange = 0.15;
+ reflectionScript.fadeColor = new pc.Color(1, 1, 1, 1);
+
+ app.root.addChild(groundReflector);
+
+ // Camera - exclude skybox layer, include depth layer for reflection
+ const camera = new pc.Entity('camera');
+ camera.addComponent('camera', {
+ clearColor: new pc.Color(1, 1, 1, 1),
+ fov: 45,
+ nearClip: 0.01,
+ layers: [worldLayer.id, excludedLayer.id, depthLayer.id, uiLayer.id],
+ toneMapping: pc.TONEMAP_LINEAR
+ });
+ camera.setPosition(2.5, 3.0, 14);
+ camera.lookAt(2.2, 1.5, 0);
+
+ app.root.addChild(camera);
+
+ // Subtle camera sway — orbit around the look target at constant distance
+ const lookTarget = new pc.Vec3(2.2, 1.5, 0);
+ const baseDir = camera.getPosition().clone().sub(lookTarget);
+ const baseDist = baseDir.length();
+ const baseYaw = Math.atan2(baseDir.x, baseDir.z);
+ const basePitch = Math.asin(baseDir.y / baseDist);
+ let targetYaw = 0;
+ let targetPitch = 0;
+ let currentYaw = 0;
+ let currentPitch = 0;
+ canvas.addEventListener('mousemove', (e) => {
+ const rect = canvas.getBoundingClientRect();
+ const nx = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
+ const ny = ((e.clientY - rect.top) / rect.height - 0.5) * 2;
+ targetYaw = -nx * 0.45;
+ targetPitch = ny * 0.15;
+ });
+
+ // Set the main camera for the reflection script
+ reflectionScript.mainCamera = camera;
+
+ // Light
+ const light = new pc.Entity('light');
+ light.addComponent('light', {
+ type: 'directional',
+ color: new pc.Color(1, 1, 1),
+ intensity: 3,
+ castShadows: true,
+ shadowBias: 0.2,
+ normalOffsetBias: 0.05,
+ shadowResolution: 2048
+ });
+ light.setEulerAngles(45, 30, 0);
+ app.root.addChild(light);
+
+ // Click handling — the HTML panel receives real DOM click events in both
+ // modes: via getElementTransform hit testing (HTML-in-Canvas) or via
+ // standard DOM events (overlay fallback). When a variant button is clicked
+ // we apply the glTF KHR_materials_variants extension and repaint.
+ htmlPanel.addEventListener('click', (e) => {
+ const btn = /** @type {HTMLElement} */ (e.target).closest('[data-variant]');
+ if (!btn) return;
+ const variant = btn.getAttribute('data-variant');
+ if (variant && variant !== activeVariant) {
+ activeVariant = variant;
+ assets.shoe.resource.applyMaterialVariant(shoeEntity, activeVariant);
+ updatePanel();
+ if (supportsHtmlInCanvas) {
+ canvas.requestPaint();
+ }
+ }
+ });
+
+ // Per-frame sync: HtmlSync projects the 3D panel position into screen
+ // space and sets the HTML element's CSS transform so the browser's hit
+ // testing aligns with where the panel appears in the 3D scene.
+ const supportsGetElementTransform = typeof canvas.getElementTransform === 'function';
+ const htmlSync = (panel && supportsGetElementTransform) ?
+ new HtmlSync(canvas, htmlPanel, panel, PANEL_WIDTH, PANEL_HEIGHT) : null;
+
+ app.on('update', (/** @type {number} */ dt) => {
+ // Smooth camera sway — orbit at constant radius
+ currentYaw += (targetYaw - currentYaw) * 2 * dt;
+ currentPitch += (targetPitch - currentPitch) * 2 * dt;
+
+ const yaw = baseYaw + currentYaw;
+ const pitch = Math.max(-Math.PI * 0.45, Math.min(Math.PI * 0.45, basePitch + currentPitch));
+
+ camera.setPosition(
+ lookTarget.x + Math.sin(yaw) * Math.cos(pitch) * baseDist,
+ lookTarget.y + Math.sin(pitch) * baseDist,
+ lookTarget.z + Math.cos(yaw) * Math.cos(pitch) * baseDist
+ );
+ camera.lookAt(lookTarget);
+
+ htmlSync?.update(camera.camera);
+ });
+});
+
+export { app };
diff --git a/examples/src/examples/misc/html-texture.example.mjs b/examples/src/examples/misc/html-texture.example.mjs
new file mode 100644
index 00000000000..f88643bf1eb
--- /dev/null
+++ b/examples/src/examples/misc/html-texture.example.mjs
@@ -0,0 +1,222 @@
+// @config DESCRIPTION
Renders live HTML content directly as a WebGL texture via the HTML-in-Canvas API (texElementImage2D).
Includes animated CSS gradients, text glow, and a pulsing circle — all driven by standard CSS.
+//
+// This example demonstrates the HTML-in-Canvas API: a styled HTML element with
+// CSS animations is appended to a canvas marked with the "layoutsubtree"
+// attribute, then captured into a WebGL texture via texElementImage2D.
+//
+// Fallback: when device.supportsHtmlTextures is false, a static 2D canvas with
+// hand-drawn placeholder graphics is used as the texture source instead.
+//
+import { deviceType, rootPath } from 'examples/utils';
+import * as pc from 'playcanvas';
+
+const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
+
+// Enable layoutsubtree for HTML-in-Canvas support
+canvas.setAttribute('layoutsubtree', 'true');
+
+window.focus();
+
+const assets = {
+ envatlas: new pc.Asset(
+ 'env-atlas',
+ 'texture',
+ { url: `${rootPath}/static/assets/cubemaps/helipad-env-atlas.png` },
+ { type: pc.TEXTURETYPE_RGBP, mipmaps: false }
+ )
+};
+
+const gfxOptions = {
+ deviceTypes: [deviceType]
+};
+
+const device = await pc.createGraphicsDevice(canvas, gfxOptions);
+device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
+
+const createOptions = new pc.AppOptions();
+createOptions.graphicsDevice = device;
+
+createOptions.componentSystems = [pc.RenderComponentSystem, pc.CameraComponentSystem, pc.LightComponentSystem];
+createOptions.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler];
+
+const app = new pc.AppBase(canvas);
+app.init(createOptions);
+
+app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
+app.setCanvasResolution(pc.RESOLUTION_AUTO);
+
+const resize = () => app.resizeCanvas();
+window.addEventListener('resize', resize);
+
+// Create an HTML element to use as texture source.
+// Per the HTML-in-Canvas proposal, the element must be a direct child of the canvas.
+// The 'inert' attribute prevents hit testing on the element.
+const htmlElement = document.createElement('div');
+htmlElement.setAttribute('inert', '');
+htmlElement.style.width = '512px';
+htmlElement.style.height = '512px';
+htmlElement.style.padding = '10px';
+htmlElement.style.background = 'linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #f9ca24, #ff6b6b, #4ecdc4, #45b7d1, #f9ca24)';
+htmlElement.style.backgroundSize = '400% 400%';
+htmlElement.style.animation = 'gradient-shift 4s ease infinite';
+htmlElement.style.borderRadius = '0';
+htmlElement.style.fontFamily = 'Arial, sans-serif';
+htmlElement.style.fontSize = '24px';
+htmlElement.style.color = 'white';
+htmlElement.style.textAlign = 'center';
+htmlElement.style.display = 'flex';
+htmlElement.style.flexDirection = 'column';
+htmlElement.style.justifyContent = 'center';
+htmlElement.style.alignItems = 'center';
+htmlElement.innerHTML = `
+
HTML in Canvas!
+
This texture is rendered from HTML using texElementImage2D
+
+`;
+
+const style = document.createElement('style');
+style.textContent = `
+ @keyframes glow {
+ 0%, 100% { color: white; text-shadow: 0 0 10px rgba(0,0,0,0.8), 0 0 20px rgba(0,0,0,0.4); font-size: 42px; }
+ 50% { color: #f9ca24; text-shadow: 0 0 15px rgba(0,0,0,0.8), 0 0 30px #f9ca24, 0 0 60px #f9ca24, 0 0 90px rgba(249,202,36,0.4); font-size: 48px; }
+ }
+ @keyframes gradient-shift {
+ 0% { background-position: 0% 50%; }
+ 50% { background-position: 100% 50%; }
+ 100% { background-position: 0% 50%; }
+ }
+ @keyframes pulse {
+ 0% { transform: scale(1); background: #ff6b6b; }
+ 25% { transform: scale(1.2); background: #f9ca24; }
+ 50% { transform: scale(1); background: #4ecdc4; }
+ 75% { transform: scale(1.2); background: #45b7d1; }
+ 100% { transform: scale(1); background: #ff6b6b; }
+ }
+`;
+document.head.appendChild(style);
+
+canvas.appendChild(htmlElement);
+
+// Create texture
+const htmlTexture = new pc.Texture(device, {
+ width: 512,
+ height: 512,
+ format: pc.PIXELFORMAT_RGBA8,
+ name: 'htmlTexture'
+});
+
+// Fallback canvas texture for browsers without texElementImage2D support
+const createFallbackTexture = () => {
+ const fallbackCanvas = document.createElement('canvas');
+ fallbackCanvas.width = 512;
+ fallbackCanvas.height = 512;
+ const ctx = fallbackCanvas.getContext('2d');
+ if (!ctx) return null;
+
+ const gradient = ctx.createLinearGradient(0, 0, 512, 512);
+ gradient.addColorStop(0, '#ff6b6b');
+ gradient.addColorStop(0.33, '#4ecdc4');
+ gradient.addColorStop(0.66, '#45b7d1');
+ gradient.addColorStop(1, '#f9ca24');
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, 512, 512);
+
+ ctx.fillStyle = 'white';
+ ctx.font = 'bold 36px Arial';
+ ctx.textAlign = 'center';
+ ctx.shadowColor = 'rgba(0,0,0,0.5)';
+ ctx.shadowBlur = 4;
+ ctx.shadowOffsetX = 2;
+ ctx.shadowOffsetY = 2;
+ ctx.fillText('HTML in Canvas!', 256, 180);
+
+ ctx.font = '20px Arial';
+ ctx.fillText('(Canvas Fallback)', 256, 220);
+ ctx.fillText('texElementImage2D not available', 256, 260);
+
+ ctx.beginPath();
+ ctx.arc(256, 320, 25, 0, 2 * Math.PI);
+ ctx.fillStyle = 'white';
+ ctx.fill();
+
+ return fallbackCanvas;
+};
+
+// Start with fallback texture, then switch to HTML source once the paint record is ready
+const fallbackCanvas = createFallbackTexture();
+if (fallbackCanvas) {
+ htmlTexture.setSource(fallbackCanvas);
+}
+
+const onPaintUpload = () => {
+ if (!app.graphicsDevice) return;
+ htmlTexture.upload();
+};
+
+app.on('destroy', () => {
+ window.removeEventListener('resize', resize);
+ canvas.removeEventListener('paint', onPaintUpload);
+ if (htmlElement.parentNode) htmlElement.parentNode.removeChild(htmlElement);
+ if (style.parentNode) style.parentNode.removeChild(style);
+});
+
+if (device.supportsHtmlTextures) {
+ // The browser must paint the HTML element before texElementImage2D can use it.
+ // Wait for the 'paint' event, then set the HTML element as the texture source.
+ canvas.addEventListener('paint', () => {
+ htmlTexture.setSource(/** @type {any} */ (htmlElement));
+ }, { once: true });
+ canvas.requestPaint();
+
+ // Re-upload the texture whenever the browser repaints the HTML children
+ canvas.addEventListener('paint', onPaintUpload);
+} else {
+ console.warn('HTML textures are not supported - using canvas fallback');
+}
+
+const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
+assetListLoader.load(() => {
+ app.start();
+
+ // setup skydome
+ app.scene.envAtlas = assets.envatlas.resource;
+ app.scene.skyboxMip = 0;
+ app.scene.skyboxIntensity = 2;
+ app.scene.exposure = 1.5;
+
+ // Create metallic material with the HTML texture for mirror-like reflections
+ const material = new pc.StandardMaterial();
+ material.diffuseMap = htmlTexture;
+ material.useMetalness = true;
+ material.metalness = 0.7;
+ material.gloss = 0.9;
+ material.update();
+
+ const box = new pc.Entity('cube');
+ box.addComponent('render', {
+ type: 'box',
+ material: material
+ });
+ app.root.addChild(box);
+
+ const camera = new pc.Entity('camera');
+ camera.addComponent('camera', {
+ clearColor: new pc.Color(0.2, 0.2, 0.2)
+ });
+ app.root.addChild(camera);
+ camera.setPosition(0, 0, 3);
+
+ const light = new pc.Entity('light');
+ light.addComponent('light', {
+ type: 'directional',
+ intensity: 1.5
+ });
+ app.root.addChild(light);
+ light.setEulerAngles(45, 30, 0);
+
+ app.on('update', (/** @type {number} */ dt) => {
+ box.rotate(3 * dt, 5 * dt, 6 * dt);
+ });
+});
+
+export { app };
diff --git a/examples/src/examples/misc/mini-stats.example.mjs b/examples/src/examples/misc/mini-stats.example.mjs
new file mode 100644
index 00000000000..7ab901de3d6
--- /dev/null
+++ b/examples/src/examples/misc/mini-stats.example.mjs
@@ -0,0 +1,263 @@
+// @config ENGINE performance
+// @config NO_MINISTATS
+// @config WEBGPU_DISABLED
+import { deviceType } from 'examples/utils';
+import * as pc from 'playcanvas';
+
+const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
+window.focus();
+
+const gfxOptions = {
+ deviceTypes: [deviceType]
+};
+
+const device = await pc.createGraphicsDevice(canvas, gfxOptions);
+device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
+
+const createOptions = new pc.AppOptions();
+createOptions.graphicsDevice = device;
+
+createOptions.componentSystems = [
+ pc.ModelComponentSystem,
+ pc.RenderComponentSystem,
+ pc.CameraComponentSystem,
+ pc.LightComponentSystem
+];
+
+const app = new pc.AppBase(canvas);
+app.init(createOptions);
+app.start();
+
+// 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);
+});
+
+// set up options for mini-stats, start with the default options
+const options = pc.MiniStats.getDefaultOptions();
+
+// configure sizes
+options.sizes = [
+ { width: 128, height: 16, spacing: 0, graphs: false },
+ { width: 256, height: 32, spacing: 2, graphs: true },
+ { width: 500, height: 64, spacing: 2, graphs: true }
+];
+
+// when the application starts, use the largest size
+options.startSizeIndex = 2;
+
+// display additional counters
+// Note: for most of these to report values, either debug or profiling engine build needs to be used.
+options.stats = [
+ // frame update time in ms
+ {
+ name: 'Update',
+ stats: ['frame.updateTime'],
+ decimalPlaces: 1,
+ unitsName: 'ms',
+ watermark: 33
+ },
+
+ // total number of draw calls
+ {
+ name: 'DrawCalls',
+ stats: ['drawCalls.total'],
+ watermark: 2000
+ },
+
+ // total number of triangles, in 1000s
+ {
+ name: 'triCount',
+ stats: ['frame.triangles'],
+ decimalPlaces: 1,
+ multiplier: 1 / 1000,
+ unitsName: 'k',
+ watermark: 500
+ },
+
+ // number of materials used in a frame
+ {
+ name: 'materials',
+ stats: ['frame.materials'],
+ watermark: 2000
+ },
+
+ // frame time it took to do frustum culling
+ {
+ name: 'cull',
+ stats: ['frame.cullTime'],
+ decimalPlaces: 1,
+ watermark: 1,
+ unitsName: 'ms'
+ },
+
+ // used VRAM in MB
+ {
+ name: 'VRAM',
+ stats: ['vram.totalUsed'],
+ decimalPlaces: 1,
+ multiplier: 1 / (1024 * 1024),
+ unitsName: 'MB',
+ watermark: 100
+ },
+
+ // frames per second
+ {
+ name: 'FPS',
+ stats: ['frame.fps'],
+ watermark: 60
+ },
+
+ // delta time
+ {
+ name: 'Frame',
+ stats: ['frame.ms'],
+ decimalPlaces: 1,
+ unitsName: 'ms',
+ watermark: 33
+ }
+];
+
+// create mini-stats system
+const miniStats = new pc.MiniStats(app, options); // eslint-disable-line no-unused-vars
+
+// add directional lights to the scene
+const light = new pc.Entity();
+light.addComponent('light', {
+ type: 'directional'
+});
+app.root.addChild(light);
+light.setLocalEulerAngles(45, 30, 0);
+
+// Create an entity with a camera component
+const camera = new pc.Entity();
+camera.addComponent('camera', {
+ clearColor: new pc.Color(0.1, 0.1, 0.1)
+});
+app.root.addChild(camera);
+camera.setLocalPosition(20, 10, 10);
+camera.lookAt(pc.Vec3.ZERO);
+
+/**
+ * Helper function to create a primitive with shape type, position, scale.
+ *
+ * @param {string} primitiveType - The primitive type.
+ * @param {number | pc.Vec3} position - The position.
+ * @param {number | pc.Vec3} scale - The scale.
+ * @returns {pc.Entity} The new primitive entity.
+ */
+function createPrimitive(primitiveType, position, scale) {
+ // create material of random color
+ const material = new pc.StandardMaterial();
+ material.diffuse = new pc.Color(Math.random(), Math.random(), Math.random());
+ material.update();
+
+ // create primitive
+ const primitive = new pc.Entity();
+ primitive.addComponent('model', {
+ type: primitiveType
+ });
+ primitive.model.material = material;
+
+ // set position and scale
+ primitive.setLocalPosition(position);
+ primitive.setLocalScale(scale);
+
+ return primitive;
+}
+
+// list of all created engine resources
+/** @type {pc.Entity[]} */
+const entities = [];
+/** @type {any[]} */
+const vertexBuffers = [];
+/** @type {any[]} */
+const textures = [];
+
+// update function called every frame
+let adding = true;
+const step = 10,
+ max = 2000;
+/** @type {pc.Entity} */
+let entity;
+/** @type {pc.VertexBuffer} */
+let vertexBuffer;
+/** @type {{ destroy: () => void}} */
+let texture;
+app.on('update', () => {
+ // execute some tasks multiple times per frame
+ for (let i = 0; i < step; i++) {
+ // allocating resources
+ if (adding) {
+ // add entity (they used shared geometry internally, and we create individual material for each)
+ const shape = Math.random() < 0.5 ? 'box' : 'sphere';
+ const position = new pc.Vec3(Math.random() * 10, Math.random() * 10, Math.random() * 10);
+ const scale = 0.5 + Math.random();
+ entity = createPrimitive(shape, position, new pc.Vec3(scale, scale, scale));
+ entities.push(entity);
+ app.root.addChild(entity);
+
+ // if allocation reached the max limit, switch to removing mode
+ if (entities.length >= max) {
+ adding = false;
+ }
+
+ // add vertex buffer
+ const vertexCount = 500;
+ const data = new Float32Array(vertexCount * 16);
+ const format = pc.VertexFormat.getDefaultInstancingFormat(app.graphicsDevice);
+ vertexBuffer = new pc.VertexBuffer(app.graphicsDevice, format, vertexCount, {
+ data: data
+ });
+ vertexBuffers.push(vertexBuffer);
+
+ // allocate texture
+ const texture = new pc.Texture(app.graphicsDevice, {
+ width: 64,
+ height: 64,
+ format: pc.PIXELFORMAT_RGB8,
+ mipmaps: false
+ });
+ textures.push(texture);
+
+ // ensure texture is uploaded (actual VRAM is allocated)
+ texture.lock();
+ texture.unlock();
+
+ if (!app.graphicsDevice.isWebGPU) {
+ // @ts-ignore engine-tsd
+ app.graphicsDevice.setTexture(texture, 0);
+ }
+ } else {
+ // de-allocating resources
+
+ if (entities.length > 0) {
+ // destroy entities
+ entity = entities[entities.length - 1];
+ // @ts-ignore engine-tsd
+ entity.destroy();
+ entities.length--;
+
+ // destroy vertex buffer
+ vertexBuffer = vertexBuffers[vertexBuffers.length - 1];
+ vertexBuffer.destroy();
+ vertexBuffers.length--;
+
+ // destroy texture
+ texture = textures[textures.length - 1];
+ texture.destroy();
+ textures.length--;
+ } else {
+ adding = true;
+ }
+ }
+ }
+});
+
+export { app };
diff --git a/examples/src/examples/misc/mini-stats.tsx b/examples/src/examples/misc/mini-stats.tsx
deleted file mode 100644
index d5486ffc0b6..00000000000
--- a/examples/src/examples/misc/mini-stats.tsx
+++ /dev/null
@@ -1,232 +0,0 @@
-// @ts-ignore: library file import
-import * as pc from 'playcanvas/build/playcanvas.dbg.js';
-// @ts-ignore: library file import
-import * as pcx from 'playcanvas/build/playcanvas-extras.js';
-import Example from '../../app/example';
-
-class MiniStatsExample extends Example {
- static CATEGORY = 'Misc';
- static NAME = 'Mini Stats';
- static ENGINE = 'DEBUG';
-
- // @ts-ignore: override class function
- example(canvas: HTMLCanvasElement, pcx: any): void {
- // Create the application and start the update loop
- const app = new pc.Application(canvas, {});
- app.start();
-
- // 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);
-
- window.addEventListener("resize", function () {
- app.resizeCanvas(canvas.width, canvas.height);
- });
-
- // set up options for mini-stats, start with the default options
- const options = pcx.MiniStats.getDefaultOptions();
-
- // configure sizes
- options.sizes = [
- { width: 128, height: 16, spacing: 0, graphs: false },
- { width: 256, height: 32, spacing: 2, graphs: true },
- { width: 500, height: 64, spacing: 2, graphs: true }
- ];
-
- // when the application starts, use the largest size
- options.startSizeIndex = 2;
-
- // display additional counters
- // Note: for most of these to report values, either debug or profiling engine build needs to be used.
- options.stats = [
-
- // frame update time in ms
- {
- name: "Update",
- stats: ["frame.updateTime"],
- decimalPlaces: 1,
- unitsName: "ms",
- watermark: 33
- },
-
- // total number of draw calls
- {
- name: "DrawCalls",
- stats: ["drawCalls.total"],
- watermark: 2000
- },
-
- // total number of triangles, in 1000s
- {
- name: "triCount",
- stats: ["frame.triangles"],
- decimalPlaces: 1,
- multiplier: 1 / 1000,
- unitsName: "k",
- watermark: 500
- },
-
- // number of materials used in a frame
- {
- name: "materials",
- stats: ["frame.materials"],
- watermark: 2000
- },
-
- // frame time it took to do frustum culling
- {
- name: "cull",
- stats: ["frame.cullTime"],
- decimalPlaces: 1,
- watermark: 1,
- unitsName: "ms"
- },
-
- // used VRAM, displayed using 2 colors - red for textures, green for geometry
- {
- name: "VRAM",
- stats: ["vram.tex", "vram.geom"],
- decimalPlaces: 1,
- multiplier: 1 / (1024 * 1024),
- unitsName: "MB",
- watermark: 100
- },
-
- // frames per second
- {
- name: "FPS",
- stats: ["frame.fps"],
- watermark: 60
- },
-
- // delta time
- {
- name: "Frame",
- stats: ["frame.ms"],
- decimalPlaces: 1,
- unitsName: "ms",
- watermark: 33
- }
- ];
-
- // create mini-stats system
- const miniStats = new pcx.MiniStats(app, options);
-
- // add directional lights to the scene
- const light = new pc.Entity();
- light.addComponent("light", {
- type: "directional"
- });
- app.root.addChild(light);
- light.setLocalEulerAngles(45, 30, 0);
-
- // Create an entity with a camera component
- const camera = new pc.Entity();
- camera.addComponent("camera", {
- clearColor: new pc.Color(0.1, 0.1, 0.1)
- });
- app.root.addChild(camera);
- camera.setLocalPosition(20, 10, 10);
- camera.lookAt(pc.Vec3.ZERO);
-
- // helper function to create a primitive with shape type, position, scale
- function createPrimitive(primitiveType: string, position: number | pc.Vec3, scale: number | pc.Vec3) {
- // create material of random color
- const material = new pc.StandardMaterial();
- material.diffuse = new pc.Color(Math.random(), Math.random(), Math.random());
- material.update();
-
- // create primitive
- const primitive = new pc.Entity();
- primitive.addComponent('model', {
- type: primitiveType
- });
- primitive.model.material = material;
-
- // set position and scale
- primitive.setLocalPosition(position);
- primitive.setLocalScale(scale);
-
- return primitive;
- }
-
- // list of all created engine resources
- const entities: any[] = [];
- const vertexBuffers: any[] = [];
- const textures: any[] = [];
-
- // update function called every frame
- let adding = true;
- const step = 10, max = 2000;
- let entity: pc.GraphNode, vertexBuffer: pc.VertexBuffer, texture: { destroy: () => void; };
- app.on("update", function (dt: any) {
-
- // execute some tasks multiple times per frame
- for (let i = 0; i < step; i++) {
-
- // allocating resouces
- if (adding) {
-
- // add entity (they used shared geometry internally, and we create individual material for each)
- const shape = Math.random() < 0.5 ? "box" : "sphere";
- const position = new pc.Vec3(Math.random() * 10, Math.random() * 10, Math.random() * 10);
- const scale = 0.5 + Math.random();
- entity = createPrimitive(shape, position, new pc.Vec3(scale, scale, scale));
- entities.push(entity);
- app.root.addChild(entity);
-
- // if allocation reached the max limit, switch to removing mode
- if (entities.length >= max) {
- adding = false;
- }
-
- // add vertex buffer
- const vertexCount = 500;
- const data = new Float32Array(vertexCount * 16);
- vertexBuffer = new pc.VertexBuffer(app.graphicsDevice, pc.VertexFormat.defaultInstancingFormat, vertexCount, pc.BUFFER_STATIC, data);
- vertexBuffers.push(vertexBuffer);
-
- // allocate texture
- const texture = new pc.Texture(app.graphicsDevice, {
- width: 64,
- height: 64,
- format: pc.PIXELFORMAT_R8_G8_B8,
- mipmaps: false
- });
- textures.push(texture);
-
- // ensure texture is uploaded (actual VRAM is allocated)
- texture.lock();
- texture.unlock();
- // @ts-ignore engine-tsd
- app.graphicsDevice.setTexture(texture, 0);
-
- } else { // de-allocating resources
-
- if (entities.length > 0) {
-
- // desotry entities
- entity = entities[entities.length - 1];
- // @ts-ignore engine-tsd
- entity.destroy();
- entities.length--;
-
- // destroy vertex buffer
- vertexBuffer = vertexBuffers[vertexBuffers.length - 1];
- vertexBuffer.destroy();
- vertexBuffers.length--;
-
- // destroy texture
- texture = textures[textures.length - 1];
- texture.destroy();
- textures.length--;
- } else {
- adding = true;
- }
- }
- }
- });
- }
-}
-
-export default MiniStatsExample;
diff --git a/examples/src/examples/misc/multi-app.controls.mjs b/examples/src/examples/misc/multi-app.controls.mjs
new file mode 100644
index 00000000000..79282ba700a
--- /dev/null
+++ b/examples/src/examples/misc/multi-app.controls.mjs
@@ -0,0 +1,60 @@
+/**
+ * @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, Panel, Label, Button } = ReactPCUI;
+ return fragment(
+ jsx(
+ Panel,
+ { headerText: 'WebGPU' },
+ jsx(Button, {
+ text: 'Add',
+ onClick: () => observer.emit('add:webgpu')
+ }),
+ jsx(Button, {
+ text: 'Remove',
+ onClick: () => observer.emit('remove:webgpu')
+ }),
+ jsx(Label, {
+ binding: new BindingTwoWay(),
+ link: { observer, path: 'webgpu' },
+ value: observer.get('webgpu')
+ })
+ ),
+ jsx(
+ Panel,
+ { headerText: 'WebGL 2' },
+ jsx(Button, {
+ text: 'Add',
+ onClick: () => observer.emit('add:webgl2')
+ }),
+ jsx(Button, {
+ text: 'Remove',
+ onClick: () => observer.emit('remove:webgl2')
+ }),
+ jsx(Label, {
+ binding: new BindingTwoWay(),
+ link: { observer, path: 'webgl2' },
+ value: observer.get('webgl2')
+ })
+ ),
+ jsx(
+ Panel,
+ { headerText: 'Null' },
+ jsx(Button, {
+ text: 'Add',
+ onClick: () => observer.emit('add:null')
+ }),
+ jsx(Button, {
+ text: 'Remove',
+ onClick: () => observer.emit('remove:null')
+ }),
+ jsx(Label, {
+ binding: new BindingTwoWay(),
+ link: { observer, path: 'null' },
+ value: observer.get('null')
+ })
+ )
+ );
+};
diff --git a/examples/src/examples/misc/multi-app.example.mjs b/examples/src/examples/misc/multi-app.example.mjs
new file mode 100644
index 00000000000..1c80708892a
--- /dev/null
+++ b/examples/src/examples/misc/multi-app.example.mjs
@@ -0,0 +1,197 @@
+// @config NO_MINISTATS
+// @config NO_DEVICE_SELECTOR
+// @config WEBGPU_DISABLED
+// @config WEBGL_DISABLED
+import { data } from 'examples/observer';
+import { rootPath } from 'examples/utils';
+import * as pc from 'playcanvas';
+
+// Use custom createGraphicsDevice function to not automatically include fall backs
+/**
+ * @param {HTMLCanvasElement} canvas - The canvas element.
+ * @param {string} deviceType - The device type.
+ * @returns {Promise
} The graphics device.
+ */
+async function createGraphicsDevice(canvas, deviceType) {
+ let device;
+ if (deviceType === 'webgpu') {
+ device = new pc.WebgpuGraphicsDevice(canvas, {});
+ await device.initWebGpu(`${rootPath}/static/lib/glslang/glslang.js`, `${rootPath}/static/lib/twgsl/twgsl.js`);
+ } else if (deviceType === 'webgl2') {
+ device = new pc.WebglGraphicsDevice(canvas);
+ } else {
+ device = new pc.NullGraphicsDevice(canvas, {});
+ }
+ return device;
+}
+
+/**
+ * @param {string} deviceType - The device type.
+ * @returns {Promise} The example application.
+ */
+async function createApp(deviceType) {
+ const assets = {
+ font: new pc.Asset('font', 'font', { url: `${rootPath}/static/assets/fonts/courier.json` })
+ };
+
+ const canvas = document.createElement('canvas');
+ canvas.id = `app-${Math.random().toString(36).substring(7)}`; // generate a random id
+ document.getElementById('appInner')?.appendChild(canvas);
+
+ const device = await createGraphicsDevice(canvas, deviceType);
+
+ const createOptions = new pc.AppOptions();
+ createOptions.graphicsDevice = device;
+ createOptions.componentSystems = [
+ pc.RenderComponentSystem,
+ pc.CameraComponentSystem,
+ pc.LightComponentSystem,
+ pc.ScreenComponentSystem,
+ pc.ElementComponentSystem
+ ];
+ createOptions.resourceHandlers = [
+ pc.TextureHandler,
+ pc.FontHandler
+ ];
+
+ const app = new pc.AppBase(canvas);
+ app.init(createOptions);
+
+ app.setCanvasFillMode(pc.FILLMODE_NONE);
+ 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);
+ });
+
+ await new Promise((resolve) => {
+ new pc.AssetListLoader(Object.values(assets), app.assets).load(resolve);
+ });
+
+ // create box entity
+ const box = new pc.Entity('cube', app);
+ box.addComponent('render', {
+ type: 'box'
+ });
+ app.root.addChild(box);
+
+ // create camera entity
+ const clearValue = 0.3 + Math.random() * 0.3;
+ const camera = new pc.Entity('camera', app);
+ camera.addComponent('camera', {
+ clearColor: new pc.Color(clearValue, clearValue, clearValue)
+ });
+ app.root.addChild(camera);
+ camera.setPosition(0, -0.4, 3);
+
+ // create directional light entity
+ const light = new pc.Entity('light', app);
+ light.addComponent('light');
+ app.root.addChild(light);
+ light.setEulerAngles(45, 0, 0);
+
+ // Create a 2D screen
+ const screen = new pc.Entity('screen', app);
+ screen.addComponent('screen', {
+ referenceResolution: new pc.Vec2(1280, 720),
+ scaleBlend: 0.5,
+ scaleMode: pc.SCALEMODE_BLEND,
+ screenSpace: true
+ });
+ app.root.addChild(screen);
+
+ // Text with outline to identify the platform
+ const text = new pc.Entity('text', app);
+ text.setLocalPosition(0, -100, 0);
+ text.addComponent('element', {
+ pivot: new pc.Vec2(0.5, 0.5),
+ anchor: new pc.Vec4(0.5, -0.2, 0.5, 0.5),
+ fontAsset: assets.font.id,
+ fontSize: 130,
+ text: app.graphicsDevice.isWebGL2 ? 'WebGL 2' : 'WebGPU',
+ color: new pc.Color(0.9, 0.9, 0.9),
+ outlineColor: new pc.Color(0, 0, 0),
+ outlineThickness: 1,
+ type: pc.ELEMENTTYPE_TEXT
+ });
+ screen.addChild(text);
+
+ // rotate the box according to the delta time since the last frame
+ app.on('update', (/** @type {number} */ dt) => box.rotate(10 * dt, 20 * dt, 30 * dt));
+
+ app.start();
+
+ return app;
+}
+
+/**
+ * @type {Record}
+ */
+const apps = {
+ webgpu: [],
+ webgl2: [],
+ null: []
+};
+
+// Remove existing canvas
+const existingCanvas = document.getElementById('application-canvas');
+if (existingCanvas) {
+ existingCanvas.remove();
+}
+
+/**
+ * @param {string} deviceType - The device type.
+ */
+async function addApp(deviceType) {
+ try {
+ const app = await createApp(deviceType);
+ apps[deviceType].push(app);
+ data.set(deviceType, apps[deviceType].length);
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+// Add event listers for adding and removing apps
+for (const deviceType in apps) {
+ data.set(deviceType, 0);
+
+ data.on(`add:${deviceType}`, () => addApp(deviceType));
+
+ data.on(`remove:${deviceType}`, () => {
+ const app = apps[deviceType].pop();
+ if (app && app.graphicsDevice) {
+ const canvas = app.graphicsDevice.canvas;
+ try {
+ app.destroy();
+ } catch (e) {
+ // FIX: Throws error when hot reloading
+ console.error(e);
+ }
+ canvas.remove();
+ data.set(deviceType, apps[deviceType].length);
+ }
+ });
+}
+
+// Make sure to remove all apps when the example is destroyed or hot reloaded
+const destroy = () => {
+ for (const deviceType in apps) {
+ let i = 0;
+ while (apps[deviceType].length) {
+ data.emit(`remove:${deviceType}`);
+ if (i++ > 1e3) {
+ break;
+ }
+ }
+ }
+};
+
+// Start with a webgl2 and webgpu app
+await addApp('webgl2');
+await addApp('webgpu');
+
+export { destroy };
diff --git a/examples/src/examples/misc/spineboy.example.mjs b/examples/src/examples/misc/spineboy.example.mjs
new file mode 100644
index 00000000000..9ec391d9af8
--- /dev/null
+++ b/examples/src/examples/misc/spineboy.example.mjs
@@ -0,0 +1,86 @@
+import { deviceType, rootPath } from 'examples/utils';
+import * as pc from 'playcanvas';
+
+const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
+window.focus();
+
+const assets = {
+ skeleton: new pc.Asset('skeleton', 'json', { url: `${rootPath}/static/assets//spine/spineboy-pro.json` }),
+ atlas: new pc.Asset('atlas', 'text', { url: `${rootPath}/static/assets//spine/spineboy-pro.atlas` }),
+ texture: new pc.Asset('spineboy-pro.png', 'texture', { url: `${rootPath}/static/assets//spine/spineboy-pro.png` }),
+ spinescript: new pc.Asset('spinescript', 'script', {
+ url: `${rootPath}/static/scripts/spine/playcanvas-spine.3.8.js`
+ })
+};
+
+const gfxOptions = {
+ deviceTypes: [deviceType]
+};
+
+const device = await pc.createGraphicsDevice(canvas, gfxOptions);
+device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
+
+const createOptions = new pc.AppOptions();
+createOptions.graphicsDevice = device;
+
+createOptions.componentSystems = [pc.CameraComponentSystem, pc.ScriptComponentSystem];
+createOptions.resourceHandlers = [pc.TextureHandler, pc.ScriptHandler, pc.JsonHandler, pc.TextHandler];
+
+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 assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
+assetListLoader.load(() => {
+ app.start();
+
+ // create camera entity
+ const camera = new pc.Entity('camera');
+ camera.addComponent('camera', {
+ clearColor: new pc.Color(0.5, 0.6, 0.9)
+ });
+ app.root.addChild(camera);
+ camera.translateLocal(0, 7, 20);
+
+ /**
+ * @param {pc.Vec3} position - The local-space position.
+ * @param {pc.Vec3} scale - The local-space scale.
+ * @param {number} timeScale - The animation time scale.
+ */
+ const createSpineInstance = (position, scale, timeScale) => {
+ const spineEntity = new pc.Entity();
+ spineEntity.addComponent('spine', {
+ atlasAsset: assets.atlas.id,
+ skeletonAsset: assets.skeleton.id,
+ textureAssets: [assets.texture.id]
+ });
+ spineEntity.setLocalPosition(position);
+ spineEntity.setLocalScale(scale);
+ app.root.addChild(spineEntity);
+
+ // play spine animation
+ // @ts-ignore
+ spineEntity.spine.state.setAnimation(0, 'portal', true);
+
+ // @ts-ignore
+ spineEntity.spine.state.timeScale = timeScale;
+ };
+
+ // create spine entity 1
+ createSpineInstance(new pc.Vec3(2, 2, 0), new pc.Vec3(1, 1, 1), 1);
+
+ // create spine entity 2
+ createSpineInstance(new pc.Vec3(2, 10, 0), new pc.Vec3(-0.5, 0.5, 0.5), 0.5);
+});
+
+export { app };
diff --git a/examples/src/examples/physics/compound-collision.example.mjs b/examples/src/examples/physics/compound-collision.example.mjs
new file mode 100644
index 00000000000..dfe23bae097
--- /dev/null
+++ b/examples/src/examples/physics/compound-collision.example.mjs
@@ -0,0 +1,418 @@
+import { deviceType, rootPath } from 'examples/utils';
+import * as pc from 'playcanvas';
+
+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`
+});
+await new Promise((resolve) => {
+ pc.WasmModule.getInstance('Ammo', () => resolve());
+});
+
+const gfxOptions = {
+ deviceTypes: [deviceType]
+};
+
+const device = await pc.createGraphicsDevice(canvas, gfxOptions);
+device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
+
+
+const createOptions = new pc.AppOptions();
+createOptions.graphicsDevice = device;
+createOptions.keyboard = new pc.Keyboard(document.body);
+
+createOptions.componentSystems = [
+ pc.RenderComponentSystem,
+ pc.CameraComponentSystem,
+ pc.LightComponentSystem,
+ pc.ScriptComponentSystem,
+ pc.CollisionComponentSystem,
+ pc.RigidBodyComponentSystem,
+ pc.ElementComponentSystem
+];
+createOptions.resourceHandlers = [
+ pc.TextureHandler,
+ pc.ContainerHandler,
+ pc.ScriptHandler,
+ pc.JsonHandler,
+ pc.FontHandler
+];
+
+const app = new pc.AppBase(canvas);
+app.init(createOptions);
+app.start();
+
+// 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);
+});
+
+app.scene.ambientLight = new pc.Color(0.2, 0.2, 0.2);
+/**
+ * @param {pc.Color} color - The diffuse color.
+ * @returns {pc.StandardMaterial} The standard material.
+ */
+function createMaterial(color) {
+ const material = new pc.StandardMaterial();
+ material.diffuse = color;
+ material.update();
+ return material;
+}
+
+// Create a couple of materials for our objects
+const red = createMaterial(new pc.Color(0.7, 0.3, 0.3));
+const gray = createMaterial(new pc.Color(0.7, 0.7, 0.7));
+
+// Define a scene hierarchy in JSON format. This is loaded/parsed in
+// the parseScene function below
+const scene = [
+ {
+ // The Chair entity has a collision component of type 'compound' and a
+ // rigidbody component. This means that any descendent entity with a
+ // collision component is added to a compound collision shape on the
+ // Chair entity. You can use compound collision shapes to define
+ // complex, rigid shapes.
+ name: 'Chair',
+ pos: [0, 1, 0],
+ components: [
+ {
+ type: 'collision',
+ options: {
+ type: 'compound'
+ }
+ },
+ {
+ type: 'rigidbody',
+ options: {
+ type: 'dynamic',
+ friction: 0.5,
+ mass: 10,
+ restitution: 0.5
+ }
+ }
+ ],
+ children: [
+ {
+ name: 'Seat',
+ components: [
+ {
+ type: 'collision',
+ options: {
+ type: 'box',
+ halfExtents: [0.25, 0.025, 0.25]
+ }
+ }
+ ],
+ children: [
+ {
+ name: 'Seat Model',
+ scl: [0.5, 0.05, 0.5],
+ components: [
+ {
+ type: 'render',
+ options: {
+ type: 'box',
+ material: gray
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Seat Back',
+ pos: [0, 0.3, -0.2],
+ components: [
+ {
+ type: 'collision',
+ options: {
+ type: 'box',
+ halfExtents: [0.25, 0.2, 0.025]
+ }
+ }
+ ],
+ children: [
+ {
+ name: 'Seat Back Model',
+ scl: [0.5, 0.4, 0.05],
+ components: [
+ {
+ type: 'render',
+ options: {
+ type: 'box',
+ material: gray
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Leg 1',
+ pos: [0.2, -0.25, 0.2],
+ components: [
+ {
+ type: 'collision',
+ options: {
+ type: 'cylinder',
+ height: 0.5,
+ radius: 0.025
+ }
+ }
+ ],
+ children: [
+ {
+ name: 'Leg 1 Model',
+ scl: [0.05, 0.5, 0.05],
+ components: [
+ {
+ type: 'render',
+ options: {
+ type: 'cylinder',
+ material: gray
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Leg 2',
+ pos: [-0.2, -0.25, 0.2],
+ components: [
+ {
+ type: 'collision',
+ options: {
+ type: 'cylinder',
+ height: 0.5,
+ radius: 0.025
+ }
+ }
+ ],
+ children: [
+ {
+ name: 'Leg 2 Model',
+ scl: [0.05, 0.5, 0.05],
+ components: [
+ {
+ type: 'render',
+ options: {
+ type: 'cylinder',
+ material: gray
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Leg 3',
+ pos: [0.2, 0, -0.2],
+ components: [
+ {
+ type: 'collision',
+ options: {
+ type: 'cylinder',
+ height: 1,
+ radius: 0.025
+ }
+ }
+ ],
+ children: [
+ {
+ name: 'Leg 3 Model',
+ scl: [0.05, 1, 0.05],
+ components: [
+ {
+ type: 'render',
+ options: {
+ type: 'cylinder',
+ material: gray
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Leg 4',
+ pos: [-0.2, 0, -0.2],
+ components: [
+ {
+ type: 'collision',
+ options: {
+ type: 'cylinder',
+ height: 1,
+ radius: 0.025
+ }
+ }
+ ],
+ children: [
+ {
+ name: 'Leg 4 Model',
+ scl: [0.05, 1, 0.05],
+ components: [
+ {
+ type: 'render',
+ options: {
+ type: 'cylinder',
+ material: gray
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Ground',
+ pos: [0, -0.5, 0],
+ components: [
+ {
+ type: 'collision',
+ options: {
+ type: 'box',
+ halfExtents: [5, 0.5, 5]
+ }
+ },
+ {
+ type: 'rigidbody',
+ options: {
+ type: 'static',
+ restitution: 0.5
+ }
+ }
+ ],
+ children: [
+ {
+ name: 'Ground Model',
+ scl: [10, 1, 10],
+ components: [
+ {
+ type: 'render',
+ options: {
+ type: 'box',
+ material: gray
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Directional Light',
+ rot: [45, 130, 0],
+ components: [
+ {
+ type: 'light',
+ options: {
+ type: 'directional',
+ castShadows: true,
+ shadowDistance: 8,
+ shadowBias: 0.1,
+ intensity: 1,
+ normalOffsetBias: 0.05
+ }
+ }
+ ]
+ },
+ {
+ name: 'Camera',
+ pos: [0, 4, 7],
+ rot: [-30, 0, 0],
+ components: [
+ {
+ type: 'camera',
+ options: {
+ color: [0.5, 0.5, 0.5]
+ }
+ }
+ ]
+ }
+];
+
+/**
+ * Convert an entity definition in the structure above to a pc.Entity object
+ *
+ * @param {typeof scene} e - The scene definition.
+ * @returns {pc.Entity} The entity.
+ */
+function parseEntity(e) {
+ const entity = new pc.Entity(e.name);
+
+ if (e.pos) {
+ entity.setLocalPosition(e.pos[0], e.pos[1], e.pos[2]);
+ }
+ if (e.rot) {
+ entity.setLocalEulerAngles(e.rot[0], e.rot[1], e.rot[2]);
+ }
+ if (e.scl) {
+ entity.setLocalScale(e.scl[0], e.scl[1], e.scl[2]);
+ }
+
+ if (e.components) {
+ e.components.forEach((c) => {
+ entity.addComponent(c.type, c.options);
+ });
+ }
+
+ if (e.children) {
+ e.children.forEach((/** @type {typeof scene} */ child) => {
+ entity.addChild(parseEntity(child));
+ });
+ }
+
+ return entity;
+}
+
+// Parse the scene data above into entities and add them to the scene's root entity
+function parseScene(s) {
+ s.forEach((e) => {
+ app.root.addChild(parseEntity(e));
+ });
+}
+
+parseScene(scene);
+
+let numChairs = 0;
+
+// Clone the chair entity hierarchy and add it to the scene root
+function spawnChair() {
+ /** @type {pc.Entity} */
+ const chair = app.root.findByName('Chair');
+ const clone = chair.clone();
+ clone.setLocalPosition(Math.random() * 1 - 0.5, Math.random() * 2 + 1, Math.random() * 1 - 0.5);
+ app.root.addChild(clone);
+ numChairs++;
+}
+
+// Set an update function on the application's update event
+let time = 0;
+app.on('update', (dt) => {
+ // Add a new chair every 250 ms
+ time += dt;
+ if (time > 0.25 && numChairs < 20) {
+ spawnChair();
+ time = 0;
+ }
+
+ // Show active bodies in red and frozen bodies in gray
+ app.root.findComponents('rigidbody').forEach((/** @type {pc.RigidBodyComponent} */ body) => {
+ body.entity.findComponents('render').forEach((/** @type {pc.RenderComponent} */ render) => {
+ render.material = body.isActive() ? red : gray;
+ });
+ });
+});
+
+export { app };
diff --git a/examples/src/examples/physics/compound-collision.tsx b/examples/src/examples/physics/compound-collision.tsx
deleted file mode 100644
index 5679f686210..00000000000
--- a/examples/src/examples/physics/compound-collision.tsx
+++ /dev/null
@@ -1,363 +0,0 @@
-import * as pc from 'playcanvas/build/playcanvas.js';
-import Example from '../../app/example';
-
-class CompoundCollisionExample extends Example {
- static CATEGORY = 'Physics';
- static NAME = 'Compound Collision';
-
- // @ts-ignore: override class function
- example(canvas: HTMLCanvasElement, wasmSupported: any, loadWasmModuleAsync: any): void {
- if (wasmSupported()) {
- loadWasmModuleAsync('Ammo', 'static/lib/ammo/ammo.wasm.js', 'static/lib/ammo/ammo.wasm.wasm', demo);
- } else {
- loadWasmModuleAsync('Ammo', 'static/lib/ammo/ammo.js', '', demo);
- }
-
- function demo() {
- // Create the application and start the update loop
- const app = new pc.Application(canvas, {});
- app.start();
-
- app.scene.ambientLight = new pc.Color(0.2, 0.2, 0.2);
-
- function createMaterial(color: pc.Color) {
- const material = new pc.StandardMaterial();
- material.diffuse = color;
- material.update();
-
- return material;
- }
-
- // Create a couple of materials for our objects
- const red = createMaterial(new pc.Color(1, 0.3, 0.3));
- const gray = createMaterial(new pc.Color(0.7, 0.7, 0.7));
-
- // Define a scene hierarchy in JSON format. This is loaded/parsed in
- // the parseScene function below
- const scene = [
- {
- // The Chair entity has a collision component of type 'compound' and a
- // rigidbody component. This means that any descendent entity with a
- // collision component is added to a compound collision shape on the
- // Chair entity. You can use compound collision shapes to define
- // complex, rigid shapes.
- name: 'Chair',
- pos: [0, 1, 0],
- components: [
- {
- type: 'collision',
- options: {
- type: 'compound'
- }
- }, {
- type: 'rigidbody',
- options: {
- type: 'dynamic',
- friction: 0.5,
- mass: 10,
- restitution: 0.5
- }
- }
- ],
- children: [
- {
- name: 'Seat',
- components: [
- {
- type: 'collision',
- options: {
- type: 'box',
- halfExtents: [0.25, 0.025, 0.25]
- }
- }
- ],
- children: [
- {
- name: 'Seat Model',
- scl: [0.5, 0.05, 0.5],
- components: [
- {
- type: 'render',
- options: {
- type: 'box',
- material: gray
- }
- }
- ]
- }
- ]
- }, {
- name: 'Seat Back',
- pos: [0, 0.3, -0.2],
- components: [
- {
- type: 'collision',
- options: {
- type: 'box',
- halfExtents: [0.25, 0.2, 0.025]
- }
- }
- ],
- children: [
- {
- name: 'Seat Back Model',
- scl: [0.5, 0.4, 0.05],
- components: [
- {
- type: 'render',
- options: {
- type: 'box',
- material: gray
- }
- }
- ]
- }
- ]
- }, {
- name: 'Leg 1',
- pos: [0.2, -0.25, 0.2],
- components: [
- {
- type: 'collision',
- options: {
- type: 'cylinder',
- height: 0.5,
- radius: 0.025
- }
- }
- ],
- children: [
- {
- name: 'Leg 1 Model',
- scl: [0.05, 0.5, 0.05],
- components: [
- {
- type: 'render',
- options: {
- type: 'cylinder',
- material: gray
- }
- }
- ]
- }
- ]
- }, {
- name: 'Leg 2',
- pos: [-0.2, -0.25, 0.2],
- components: [
- {
- type: 'collision',
- options: {
- type: 'cylinder',
- height: 0.5,
- radius: 0.025
- }
- }
- ],
- children: [
- {
- name: 'Leg 2 Model',
- scl: [0.05, 0.5, 0.05],
- components: [
- {
- type: 'render',
- options: {
- type: 'cylinder',
- material: gray
- }
- }
- ]
- }
- ]
- }, {
- name: 'Leg 3',
- pos: [0.2, 0, -0.2],
- components: [
- {
- type: 'collision',
- options: {
- type: 'cylinder',
- height: 1,
- radius: 0.025
- }
- }
- ],
- children: [
- {
- name: 'Leg 3 Model',
- scl: [0.05, 1, 0.05],
- components: [
- {
- type: 'render',
- options: {
- type: 'cylinder',
- material: gray
- }
- }
- ]
- }
- ]
- }, {
- name: 'Leg 4',
- pos: [-0.2, 0, -0.2],
- components: [
- {
- type: 'collision',
- options: {
- type: 'cylinder',
- height: 1,
- radius: 0.025
- }
- }
- ],
- children: [
- {
- name: 'Leg 4 Model',
- scl: [0.05, 1, 0.05],
- components: [
- {
- type: 'render',
- options: {
- type: 'cylinder',
- material: gray
- }
- }
- ]
- }
- ]
- }
- ]
- }, {
- name: 'Ground',
- pos: [0, -0.5, 0],
- components: [
- {
- type: 'collision',
- options: {
- type: 'box',
- halfExtents: [5, 0.5, 5]
- }
- }, {
- type: 'rigidbody',
- options: {
- type: 'static',
- restitution: 0.5
- }
- }
- ],
- children: [
- {
- name: 'Ground Model',
- scl: [10, 1, 10],
- components: [
- {
- type: 'render',
- options: {
- type: 'box',
- material: gray
- }
- }
- ]
- }
- ]
- }, {
- name: 'Directional Light',
- rot: [45, 30, 0],
- components: [
- {
- type: 'light',
- options: {
- type: 'directional',
- castShadows: true,
- shadowDistance: 8,
- shadowBias: 0.1,
- normalOffsetBias: 0.05
- }
- }
- ]
- }, {
- name: 'Camera',
- pos: [0, 4, 7],
- rot: [-30, 0, 0],
- components: [
- {
- type: 'camera',
- options: {
- color: [0.5, 0.5, 0.5]
- }
- }
- ]
- }
- ];
-
- // Convert an entity definition in the structure above to a pc.Entity object
- function parseEntity(e: any) {
- const entity = new pc.Entity(e.name);
-
- if (e.pos) {
- entity.setLocalPosition(e.pos[0], e.pos[1], e.pos[2]);
- }
- if (e.rot) {
- entity.setLocalEulerAngles(e.rot[0], e.rot[1], e.rot[2]);
- }
- if (e.scl) {
- entity.setLocalScale(e.scl[0], e.scl[1], e.scl[2]);
- }
-
- if (e.components) {
- e.components.forEach(function (c: any) {
- entity.addComponent(c.type, c.options);
- });
- }
-
- if (e.children) {
- e.children.forEach(function (child: pc.Entity) {
- entity.addChild(parseEntity(child));
- });
- }
-
- return entity;
- }
-
- // Parse the scene data above into entities and add them to the scene's root entity
- function parseScene(s: any) {
- s.forEach(function (e: any) {
- app.root.addChild(parseEntity(e));
- });
- }
-
- parseScene(scene);
-
- let numChairs = 0;
-
- // Clone the chair entity hierarchy and add it to the scene root
- function spawnChair() {
- const chair: pc.Entity = app.root.findByName('Chair') as pc.Entity;
- const clone = chair.clone();
- clone.setLocalPosition(Math.random() * 5 - 2.5, Math.random() * 2 + 1, Math.random() * 5 - 2.5);
- app.root.addChild(clone);
- numChairs++;
- }
-
- // Set an update function on the application's update event
- let time = 0;
- app.on("update", function (dt) {
- // Add a new chair every 250 ms
- time += dt;
- if (time > 0.25 && numChairs < 100) {
- spawnChair();
- time = 0;
- }
-
- // Show active bodies in red and frozen bodies in gray
- app.root.findComponents('rigidbody').forEach(function (body: pc.RigidBodyComponent) {
- body.entity.findComponents('render').forEach(function (render: pc.RenderComponent) {
- render.material = body.isActive() ? red : gray;
- });
- });
- });
- }
- }
-}
-
-export default CompoundCollisionExample;
diff --git a/examples/src/examples/physics/falling-shapes.example.mjs b/examples/src/examples/physics/falling-shapes.example.mjs
new file mode 100644
index 00000000000..9398f9beddb
--- /dev/null
+++ b/examples/src/examples/physics/falling-shapes.example.mjs
@@ -0,0 +1,268 @@
+import { deviceType, rootPath } from 'examples/utils';
+import * as pc from 'playcanvas';
+
+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`
+});
+await new Promise((resolve) => {
+ pc.WasmModule.getInstance('Ammo', () => resolve());
+});
+
+const assets = {
+ torus: new pc.Asset('torus', 'container', { url: `${rootPath}/static/assets/models/torus.glb` })
+};
+
+const gfxOptions = {
+ deviceTypes: [deviceType]
+};
+
+const device = await pc.createGraphicsDevice(canvas, gfxOptions);
+device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
+
+const createOptions = new pc.AppOptions();
+createOptions.graphicsDevice = device;
+createOptions.keyboard = new pc.Keyboard(document.body);
+
+createOptions.componentSystems = [
+ pc.RenderComponentSystem,
+ pc.CameraComponentSystem,
+ pc.LightComponentSystem,
+ pc.ScriptComponentSystem,
+ pc.CollisionComponentSystem,
+ pc.RigidBodyComponentSystem,
+ pc.ElementComponentSystem
+];
+createOptions.resourceHandlers = [
+ pc.TextureHandler,
+ pc.ContainerHandler,
+ pc.ScriptHandler,
+ pc.JsonHandler,
+ pc.FontHandler
+];
+
+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 assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
+assetListLoader.load(() => {
+ app.start();
+
+ app.scene.ambientLight = new pc.Color(0.2, 0.2, 0.2);
+
+ // Set the gravity for our rigid bodies
+ app.systems.rigidbody.gravity.set(0, -9.81, 0);
+ /**
+ * @param {pc.Color} color - The color of the material.
+ * @returns {pc.StandardMaterial} The new material.
+ */
+ function createMaterial(color) {
+ const material = new pc.StandardMaterial();
+ material.diffuse = color;
+ // we need to call material.update when we change its properties
+ material.update();
+ return material;
+ }
+
+ // create a few materials for our objects
+ const red = createMaterial(new pc.Color(1, 0.3, 0.3));
+ const gray = createMaterial(new pc.Color(0.7, 0.7, 0.7));
+
+ // *********** Create our floor *******************
+
+ const floor = new pc.Entity();
+ floor.addComponent('render', {
+ type: 'box',
+ material: gray
+ });
+
+ // scale it
+ floor.setLocalScale(10, 1, 10);
+
+ // add a rigidbody component so that other objects collide with it
+ floor.addComponent('rigidbody', {
+ type: 'static',
+ restitution: 0.5
+ });
+
+ // add a collision component
+ floor.addComponent('collision', {
+ type: 'box',
+ halfExtents: new pc.Vec3(5, 0.5, 5)
+ });
+
+ // add the floor to the hierarchy
+ app.root.addChild(floor);
+
+ // *********** Create lights *******************
+
+ // make our scene prettier by adding a directional light
+ const light = new pc.Entity();
+ light.addComponent('light', {
+ type: 'directional',
+ color: new pc.Color(1, 1, 1),
+ castShadows: true,
+ shadowBias: 0.2,
+ shadowDistance: 25,
+ normalOffsetBias: 0.05,
+ shadowResolution: 2048
+ });
+
+ // set the direction for our light
+ light.setLocalEulerAngles(45, 30, 0);
+
+ // Add the light to the hierarchy
+ app.root.addChild(light);
+
+ // *********** Create camera *******************
+
+ // Create an Entity with a camera component
+ const camera = new pc.Entity();
+ camera.addComponent('camera', {
+ clearColor: new pc.Color(0.5, 0.5, 0.8),
+ farClip: 50
+ });
+
+ // add the camera to the hierarchy
+ app.root.addChild(camera);
+
+ // Move the camera a little further away
+ camera.translate(0, 10, 15);
+ camera.lookAt(0, 2, 0);
+
+ /**
+ * Helper function which creates a template for a collider.
+ *
+ * @param {string} type - The render component type.
+ * @param {object} collisionOptions - The options for the collision component.
+ * @param {pc.Entity} [template] - The template entity to use.
+ * @returns {pc.Entity} The new template entity.
+ */
+ const createTemplate = function (type, collisionOptions, template) {
+ // add a render component (visible mesh)
+ if (!template) {
+ template = new pc.Entity();
+ template.addComponent('render', {
+ type: type
+ });
+ }
+
+ // ...a rigidbody component of type 'dynamic' so that it is simulated by the physics engine...
+ template.addComponent('rigidbody', {
+ type: 'dynamic',
+ mass: 50,
+ restitution: 0.5
+ });
+
+ // ... and a collision component
+ template.addComponent('collision', collisionOptions);
+
+ return template;
+ };
+
+ // *********** Create templates *******************
+
+ // Create a template for a falling box
+ const boxTemplate = createTemplate('box', {
+ type: 'box',
+ halfExtents: new pc.Vec3(0.5, 0.5, 0.5)
+ });
+
+ // A sphere...
+ const sphereTemplate = createTemplate('sphere', {
+ type: 'sphere',
+ radius: 0.5
+ });
+
+ // A capsule...
+ const capsuleTemplate = createTemplate('capsule', {
+ type: 'capsule',
+ radius: 0.5,
+ height: 2
+ });
+
+ // A cylinder...
+ const cylinderTemplate = createTemplate('cylinder', {
+ type: 'cylinder',
+ radius: 0.5,
+ height: 1
+ });
+
+ // A torus mesh...
+ const container = assets.torus.resource;
+ const meshTemplate = container.instantiateRenderEntity();
+
+ createTemplate(
+ null,
+ {
+ type: 'mesh',
+ renderAsset: container.renders[0]
+ },
+ meshTemplate
+ );
+
+ // add all the templates to an array so that
+ // we can randomly spawn them
+ const templates = [boxTemplate, sphereTemplate, capsuleTemplate, cylinderTemplate, meshTemplate];
+
+ // disable the templates because we don't want them to be visible
+ // we'll just use them to clone other Entities
+ templates.forEach((template) => {
+ template.enabled = false;
+ });
+
+ // *********** Update Function *******************
+
+ // initialize variables for our update function
+ let timer = 0;
+ let count = 40;
+
+ // Set an update function on the application's update event
+ app.on('update', (dt) => {
+ // create a falling box every 0.2 seconds
+ if (count > 0) {
+ timer -= dt;
+ if (timer <= 0) {
+ count--;
+ timer = 0.2;
+
+ // Clone a random template and position it above the floor
+ const template = templates[Math.floor(Math.random() * templates.length)];
+ const clone = template.clone();
+ // enable the clone because the template is disabled
+ clone.enabled = true;
+
+ app.root.addChild(clone);
+
+ clone.rigidbody.teleport(pc.math.random(-1, 1), 10, pc.math.random(-1, 1));
+ clone.rigidbody.angularVelocity = new pc.Vec3(
+ Math.random() * 10 - 5,
+ Math.random() * 10 - 5,
+ Math.random() * 10 - 5
+ );
+ }
+ }
+
+ // Show active bodies in red and frozen bodies in gray
+ app.root.findComponents('rigidbody').forEach((/** @type {pc.RigidBodyComponent} */ body) => {
+ body.entity.render.meshInstances[0].material = body.isActive() ? red : gray;
+ });
+ });
+});
+
+export { app };
diff --git a/examples/src/examples/physics/falling-shapes.tsx b/examples/src/examples/physics/falling-shapes.tsx
deleted file mode 100644
index 73b68049150..00000000000
--- a/examples/src/examples/physics/falling-shapes.tsx
+++ /dev/null
@@ -1,219 +0,0 @@
-import React from 'react';
-import * as pc from 'playcanvas/build/playcanvas.js';
-import Example from '../../app/example';
-import { AssetLoader } from '../../app/helpers/loader';
-
-class FallingShapesExample extends Example {
- static CATEGORY = 'Physics';
- static NAME = 'Falling Shapes';
-
- load() {
- return <>
-
- >;
- }
-
- // @ts-ignore: override class function
- example(canvas: HTMLCanvasElement, assets: { torus: pc.Asset }, wasmSupported: any, loadWasmModuleAsync: any): void {
-
- // Create the application and start the update loop
- const app = new pc.Application(canvas, {});
-
- if (wasmSupported()) {
- loadWasmModuleAsync('Ammo', 'static/lib/ammo/ammo.wasm.js', 'static/lib/ammo/ammo.wasm.wasm', demo);
- } else {
- loadWasmModuleAsync('Ammo', 'static/lib/ammo/ammo.js', '', demo);
- }
-
- function demo() {
- app.start();
-
- app.scene.ambientLight = new pc.Color(0.2, 0.2, 0.2);
-
- // Set the gravity for our rigid bodies
- // @ts-ignore engine-tsd
- app.systems.rigidbody.gravity.set(0, -9.81, 0);
-
- function createMaterial(color: pc.Color) {
- const material = new pc.StandardMaterial();
- material.diffuse = color;
- // we need to call material.update when we change its properties
- material.update();
- return material;
- }
-
- // create a few materials for our objects
- const red = createMaterial(new pc.Color(1, 0.3, 0.3));
- const gray = createMaterial(new pc.Color(0.7, 0.7, 0.7));
-
- // *********** Create our floor *******************
-
- const floor = new pc.Entity();
- floor.addComponent("render", {
- type: "box",
- material: gray
- });
-
- // scale it
- floor.setLocalScale(10, 1, 10);
-
- // add a rigidbody component so that other objects collide with it
- floor.addComponent("rigidbody", {
- type: "static",
- restitution: 0.5
- });
-
- // add a collision component
- floor.addComponent("collision", {
- type: "box",
- halfExtents: new pc.Vec3(5, 0.5, 5)
- });
-
- // add the floor to the hierarchy
- app.root.addChild(floor);
-
- // *********** Create lights *******************
-
- // make our scene prettier by adding a directional light
- const light = new pc.Entity();
- light.addComponent("light", {
- type: "directional",
- color: new pc.Color(1, 1, 1),
- castShadows: true,
- shadowBias: 0.2,
- shadowDistance: 25,
- normalOffsetBias: 0.05,
- shadowResolution: 2048
- });
-
- // set the direction for our light
- light.setLocalEulerAngles(45, 30, 0);
-
- // Add the light to the hierarchy
- app.root.addChild(light);
-
- // *********** Create camera *******************
-
- // Create an Entity with a camera component
- const camera = new pc.Entity();
- camera.addComponent("camera", {
- clearColor: new pc.Color(0.5, 0.5, 0.8),
- farClip: 50
- });
-
- // add the camera to the hierarchy
- app.root.addChild(camera);
-
- // Move the camera a little further away
- camera.translate(0, 10, 15);
- camera.lookAt(0, 2, 0);
-
- // helper function which creates a template for a collider
- const createTemplate = function (type: any, collisionOptions: any, template?: any) {
-
- // add a render component (visible mesh)
- if (!template) {
- template = new pc.Entity();
- template.addComponent("render", {
- type: type
- });
- }
-
- // ...a rigidbody component of type 'dynamic' so that it is simulated by the physics engine...
- template.addComponent("rigidbody", {
- type: "dynamic",
- mass: 50,
- restitution: 0.5
- });
-
- // ... and a collision component
- template.addComponent("collision", collisionOptions);
-
- return template;
- };
-
- // *********** Create templates *******************
-
- // Create a template for a falling box
- const boxTemplate = createTemplate("box", {
- type: "box",
- halfExtents: new pc.Vec3(0.5, 0.5, 0.5)
- });
-
- // A sphere...
- const sphereTemplate = createTemplate("sphere", {
- type: "sphere",
- radius: 0.5
- });
-
- // A capsule...
- const capsuleTemplate = createTemplate("capsule", {
- type: "capsule",
- radius: 0.5,
- height: 2
- });
-
- // A cylinder...
- const cylinderTemplate = createTemplate("cylinder", {
- type: "cylinder",
- radius: 0.5,
- height: 1
- });
-
- // A torus mesh...
- const container = assets.torus.resource;
- const meshTemplate = container.instantiateRenderEntity();
-
- createTemplate(null, {
- type: 'mesh',
- renderAsset: container.renders[0]
- }, meshTemplate);
-
- // add all the templates to an array so that
- // we can randomly spawn them
- const templates = [boxTemplate, sphereTemplate, capsuleTemplate, cylinderTemplate, meshTemplate];
-
- // disable the templates because we don't want them to be visible
- // we'll just use them to clone other Entities
- templates.forEach(function (template) {
- template.enabled = false;
- });
-
- // *********** Update Function *******************
-
- // initialize constiables for our update function
- let timer = 0;
- let count = 40;
-
- // Set an update function on the application's update event
- app.on("update", function (dt) {
- // create a falling box every 0.2 seconds
- if (count > 0) {
- timer -= dt;
- if (timer <= 0) {
- count--;
- timer = 0.2;
-
- // Clone a random template and position it above the floor
- const template = templates[Math.floor(Math.random() * templates.length)];
- const clone = template.clone();
- // enable the clone because the template is disabled
- clone.enabled = true;
-
- app.root.addChild(clone);
-
- clone.rigidbody.teleport(pc.math.random(-1, 1), 10, pc.math.random(-1, 1));
- clone.rigidbody.angularVelocity = new pc.Vec3(Math.random() * 10 - 5, Math.random() * 10 - 5, Math.random() * 10 - 5);
- }
- }
-
- // Show active bodies in red and frozen bodies in gray
- app.root.findComponents('rigidbody').forEach(function (body: pc.RigidBodyComponent) {
- body.entity.render.meshInstances[0].material = body.isActive() ? red : gray;
- });
- });
- }
- }
-}
-
-export default FallingShapesExample;
diff --git a/examples/src/examples/physics/offset-collision.example.mjs b/examples/src/examples/physics/offset-collision.example.mjs
new file mode 100644
index 00000000000..6145ed8c7eb
--- /dev/null
+++ b/examples/src/examples/physics/offset-collision.example.mjs
@@ -0,0 +1,264 @@
+import { deviceType, rootPath } from 'examples/utils';
+import * as pc from 'playcanvas';
+
+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`
+});
+await new Promise((resolve) => {
+ pc.WasmModule.getInstance('Ammo', () => resolve());
+});
+
+const assets = {
+ model: new pc.Asset('model', 'container', { url: `${rootPath}/static/assets/models/bitmoji.glb` }),
+ idleAnim: new pc.Asset('idleAnim', 'container', { url: `${rootPath}/static/assets/animations/bitmoji/idle.glb` }),
+ helipad: new pc.Asset(
+ 'helipad-env-atlas',
+ 'texture',
+ { url: `${rootPath}/static/assets/cubemaps/helipad-env-atlas.png` },
+ { type: pc.TEXTURETYPE_RGBP, mipmaps: false }
+ )
+};
+
+const gfxOptions = {
+ deviceTypes: [deviceType]
+};
+
+const device = await pc.createGraphicsDevice(canvas, gfxOptions);
+device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
+
+const createOptions = new pc.AppOptions();
+createOptions.graphicsDevice = device;
+createOptions.keyboard = new pc.Keyboard(document.body);
+
+createOptions.componentSystems = [
+ pc.RenderComponentSystem,
+ pc.CameraComponentSystem,
+ pc.LightComponentSystem,
+ pc.ScriptComponentSystem,
+ pc.CollisionComponentSystem,
+ pc.RigidBodyComponentSystem,
+ pc.AnimComponentSystem
+];
+createOptions.resourceHandlers = [
+ pc.TextureHandler,
+ pc.ContainerHandler,
+ pc.ScriptHandler,
+ pc.AnimClipHandler,
+ pc.AnimStateGraphHandler
+];
+
+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 assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
+assetListLoader.load(() => {
+ app.start();
+
+ // setup skydome
+ app.scene.exposure = 2;
+ app.scene.skyboxMip = 2;
+ app.scene.envAtlas = assets.helipad.resource;
+
+ // Create an entity with a light component
+ const lightEntity = new pc.Entity();
+ lightEntity.addComponent('light', {
+ castShadows: true,
+ intensity: 1.5,
+ normalOffsetBias: 0.2,
+ shadowType: pc.SHADOW_PCF5_32F,
+ shadowDistance: 12,
+ shadowResolution: 4096,
+ shadowBias: 0.2
+ });
+ app.root.addChild(lightEntity);
+ lightEntity.setLocalEulerAngles(45, 30, 0);
+
+ // Set the gravity for our rigid bodies
+ app.systems.rigidbody.gravity.set(0, -9.81, 0);
+
+ /**
+ * @param {pc.Color} color - The color.
+ * @returns {pc.StandardMaterial} The material.
+ */
+ function createMaterial(color) {
+ const material = new pc.StandardMaterial();
+ material.diffuse = color;
+ // we need to call material.update when we change its properties
+ material.update();
+ return material;
+ }
+
+ // create a few materials for our objects
+ const red = createMaterial(new pc.Color(1, 0.3, 0.3));
+ const gray = createMaterial(new pc.Color(0.7, 0.7, 0.7));
+
+ const floor = new pc.Entity();
+ floor.addComponent('render', {
+ type: 'box',
+ material: gray
+ });
+
+ // Scale it and move it so that the top is at 0 on the y axis
+ floor.setLocalScale(10, 1, 10);
+ floor.translateLocal(0, -0.5, 0);
+
+ // Add a rigidbody component so that other objects collide with it
+ floor.addComponent('rigidbody', {
+ type: 'static',
+ restitution: 0.5
+ });
+
+ // Add a collision component
+ floor.addComponent('collision', {
+ type: 'box',
+ halfExtents: new pc.Vec3(5, 0.5, 5)
+ });
+
+ // Add the floor to the hierarchy
+ app.root.addChild(floor);
+
+ // Create an entity from the loaded model using the render component
+ const modelEntity = assets.model.resource.instantiateRenderEntity({
+ castShadows: true
+ });
+
+ // Add an anim component to the entity
+ modelEntity.addComponent('anim', {
+ activate: true
+ });
+
+ // create an anim state graph
+ const animStateGraphData = {
+ layers: [
+ {
+ name: 'characterState',
+ states: [
+ {
+ name: 'START'
+ },
+ {
+ name: 'Idle',
+ speed: 1.0,
+ loop: true
+ }
+ ],
+ transitions: [
+ {
+ from: 'START',
+ to: 'Idle'
+ }
+ ]
+ }
+ ],
+ parameters: {}
+ };
+
+ // load the state graph into the anim component
+ modelEntity.anim.loadStateGraph(animStateGraphData);
+
+ // Add a rigid body and collision for the head with offset as the model's origin is
+ // at the feet on the floor
+ modelEntity.addComponent('rigidbody', {
+ type: 'static',
+ restitution: 0.5
+ });
+
+ modelEntity.addComponent('collision', {
+ type: 'sphere',
+ radius: 0.3,
+ linearOffset: [0, 1.25, 0]
+ });
+
+ // load the state graph asset resource into the anim component
+ const characterStateLayer = modelEntity.anim.baseLayer;
+ characterStateLayer.assignAnimation('Idle', assets.idleAnim.resource.animations[0].resource);
+
+ app.root.addChild(modelEntity);
+
+ // Create an Entity with a camera component
+ const cameraEntity = new pc.Entity();
+ cameraEntity.addComponent('camera');
+ cameraEntity.translate(0, 2, 5);
+ const lookAtPosition = modelEntity.getPosition();
+ cameraEntity.lookAt(lookAtPosition.x, lookAtPosition.y + 0.75, lookAtPosition.z);
+
+ app.root.addChild(cameraEntity);
+
+ // create a ball template that we can clone in the update loop
+ const ball = new pc.Entity();
+ ball.tags.add('shape');
+ ball.setLocalScale(0.4, 0.4, 0.4);
+ ball.translate(0, -1, 0);
+ ball.addComponent('render', {
+ type: 'sphere'
+ });
+
+ ball.addComponent('rigidbody', {
+ type: 'dynamic',
+ mass: 50,
+ restitution: 0.5
+ });
+
+ ball.addComponent('collision', {
+ type: 'sphere',
+ radius: 0.2
+ });
+
+ ball.enabled = false;
+
+ // initialize variables for our update function
+ let timer = 0;
+ let count = 40;
+
+ // Set an update function on the application's update event
+ app.on('update', (dt) => {
+ // create a falling box every 0.2 seconds
+ if (count > 0) {
+ timer -= dt;
+ if (timer <= 0) {
+ count--;
+ timer = 0.5;
+
+ // Create a new ball to drop
+ const clone = ball.clone();
+ clone.rigidbody.teleport(pc.math.random(-0.25, 0.25), 5, pc.math.random(-0.25, 0.25));
+
+ app.root.addChild(clone);
+ clone.enabled = true;
+ }
+ }
+
+ // Show active bodies in red and frozen bodies in gray
+ app.root.findByTag('shape').forEach((/** @type {pc.Entity} */ entity) => {
+ entity.render.meshInstances[0].material = entity.rigidbody.isActive() ? red : gray;
+ });
+
+ // Render the offset collision
+ app.scene.immediate.drawWireSphere(
+ modelEntity.collision.getShapePosition(),
+ 0.3,
+ pc.Color.GREEN,
+ 16,
+ true,
+ app.scene.layers.getLayerByName('World')
+ );
+ });
+});
+
+export { app };
diff --git a/examples/src/examples/physics/raycast.example.mjs b/examples/src/examples/physics/raycast.example.mjs
new file mode 100644
index 00000000000..9558c431b38
--- /dev/null
+++ b/examples/src/examples/physics/raycast.example.mjs
@@ -0,0 +1,222 @@
+import { deviceType, rootPath } from 'examples/utils';
+import * as pc from 'playcanvas';
+
+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`
+});
+await new Promise((resolve) => {
+ pc.WasmModule.getInstance('Ammo', () => resolve());
+});
+
+const assets = {
+ font: new pc.Asset('font', 'font', { url: `${rootPath}/static/assets/fonts/arial.json` })
+};
+
+const gfxOptions = {
+ deviceTypes: [deviceType]
+};
+
+const device = await pc.createGraphicsDevice(canvas, gfxOptions);
+device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
+
+const createOptions = new pc.AppOptions();
+createOptions.graphicsDevice = device;
+createOptions.keyboard = new pc.Keyboard(document.body);
+
+createOptions.componentSystems = [
+ pc.RenderComponentSystem,
+ pc.CameraComponentSystem,
+ pc.LightComponentSystem,
+ pc.ScriptComponentSystem,
+ pc.CollisionComponentSystem,
+ pc.RigidBodyComponentSystem,
+ pc.ElementComponentSystem
+];
+createOptions.resourceHandlers = [
+ pc.TextureHandler,
+ pc.ContainerHandler,
+ pc.ScriptHandler,
+ pc.JsonHandler,
+ pc.FontHandler
+];
+
+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 assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
+assetListLoader.load(() => {
+ app.start();
+
+ app.scene.ambientLight = new pc.Color(0.2, 0.2, 0.2);
+
+ /**
+ * @param {pc.Color} color - The color.
+ * @returns {pc.StandardMaterial} - The material.
+ */
+ function createMaterial(color) {
+ const material = new pc.StandardMaterial();
+ material.diffuse = color;
+ material.update();
+ return material;
+ }
+
+ // Create a couple of materials
+ const red = createMaterial(new pc.Color(1, 0, 0));
+ const green = createMaterial(new pc.Color(0, 1, 0));
+
+ // Create light
+ const light = new pc.Entity();
+ light.addComponent('light', {
+ type: 'directional'
+ });
+
+ app.root.addChild(light);
+ light.setEulerAngles(45, 30, 0);
+
+ // Create camera
+ const camera = new pc.Entity();
+ camera.addComponent('camera', {
+ clearColor: new pc.Color(0.5, 0.5, 0.8)
+ });
+
+ app.root.addChild(camera);
+ camera.setPosition(5, 0, 15);
+
+ /**
+ * @param {string} type - The shape type.
+ * @param {pc.Material} material - The material.
+ * @param {number} x - The x coordinate.
+ * @param {number} y - The y coordinate.
+ * @param {number} z - The z coordinate.
+ * @returns {pc.Entity} - The created entity.
+ */
+ function createPhysicalShape(type, material, x, y, z) {
+ const e = new pc.Entity();
+
+ // Have to set the position of the entity before adding the static rigidbody
+ // component because static bodies cannot be moved after creation
+ app.root.addChild(e);
+ e.setPosition(x, y, z);
+
+ e.addComponent('render', {
+ type: type,
+ material: material
+ });
+ e.addComponent('rigidbody', {
+ type: 'static'
+ });
+ e.addComponent('collision', {
+ type: type,
+ height: type === 'capsule' ? 2 : 1
+ });
+
+ return e;
+ }
+
+ // Create two rows of physical geometric shapes
+ const types = ['box', 'capsule', 'cone', 'cylinder', 'sphere'];
+ types.forEach((type, idx) => {
+ createPhysicalShape(type, green, idx * 2 + 1, 2, 0);
+ });
+ types.forEach((type, idx) => {
+ createPhysicalShape(type, green, idx * 2 + 1, -2, 0);
+ });
+
+ // Allocate some colors
+ const white = new pc.Color(1, 1, 1);
+ const blue = new pc.Color(0, 0, 1);
+
+ // Allocate some vectors
+ const start = new pc.Vec3();
+ const end = new pc.Vec3();
+ const temp = new pc.Vec3();
+
+ // Set an update function on the application's update event
+ let time = 0;
+ let y = 0;
+ app.on('update', function (dt) {
+ time += dt;
+
+ // Reset all shapes to green
+ app.root.findComponents('render').forEach((/** @type {pc.RenderComponent}*/ render) => {
+ render.material = green;
+ });
+
+ y = 2 + 1.2 * Math.sin(time);
+ start.set(0, y, 0);
+ end.set(10, y, 0);
+
+ // Render the ray used in the raycast
+ app.drawLine(start, end, white);
+
+ const result = app.systems.rigidbody.raycastFirst(start, end);
+ if (result) {
+ result.entity.render.material = red;
+
+ // Render the normal on the surface from the hit point
+ temp.copy(result.normal).mulScalar(0.3).add(result.point);
+ app.drawLine(result.point, temp, blue);
+ }
+
+ y = -2 + 1.2 * Math.sin(time);
+ start.set(0, y, 0);
+ end.set(10, y, 0);
+
+ // Render the ray used in the raycast
+ app.drawLine(start, end, white);
+
+ const results = app.systems.rigidbody.raycastAll(start, end);
+ results.forEach((result) => {
+ result.entity.render.material = red;
+
+ // Render the normal on the surface from the hit point
+ temp.copy(result.normal).mulScalar(0.3).add(result.point);
+ app.drawLine(result.point, temp, blue);
+ }, this);
+ });
+
+ /**
+ * @param {pc.Asset} fontAsset - The font asset.
+ * @param {string} message - The message.
+ * @param {number} x - The x coordinate.
+ * @param {number} y - The y coordinate.
+ * @param {number} z - The z coordinate.
+ * @param {number} rot - Euler-rotation around z coordinate.
+ */
+ const createText = function (fontAsset, message, x, y, z, rot) {
+ // Create a text element-based entity
+ const text = new pc.Entity();
+ text.addComponent('element', {
+ anchor: [0.5, 0.5, 0.5, 0.5],
+ fontAsset: fontAsset,
+ fontSize: 0.5,
+ pivot: [0, 0.5],
+ text: message,
+ type: pc.ELEMENTTYPE_TEXT
+ });
+ text.setLocalPosition(x, y, z);
+ text.setLocalEulerAngles(0, 0, rot);
+ app.root.addChild(text);
+ };
+
+ createText(assets.font, 'raycastFirst', 0.5, 3.75, 0, 0);
+ createText(assets.font, 'raycastAll', 0.5, -0.25, 0, 0);
+});
+
+export { app };
diff --git a/examples/src/examples/physics/raycast.tsx b/examples/src/examples/physics/raycast.tsx
deleted file mode 100644
index eac3858a291..00000000000
--- a/examples/src/examples/physics/raycast.tsx
+++ /dev/null
@@ -1,173 +0,0 @@
-import React from 'react';
-import * as pc from 'playcanvas/build/playcanvas.js';
-import Example from '../../app/example';
-import { AssetLoader } from '../../app/helpers/loader';
-
-class RaycastExample extends Example {
- static CATEGORY = 'Physics';
- static NAME = 'Raycast';
-
- load() {
- return <>
-
- >;
- }
-
- // @ts-ignore: override class function
- example(canvas: HTMLCanvasElement, assets: { font: pc.Asset }, wasmSupported: any, loadWasmModuleAsync: any): void {
-
- if (wasmSupported()) {
- loadWasmModuleAsync('Ammo', 'static/lib/ammo/ammo.wasm.js', 'static/lib/ammo/ammo.wasm.wasm', demo);
- } else {
- loadWasmModuleAsync('Ammo', 'static/lib/ammo/ammo.js', '', demo);
- }
-
- function demo() {
- // Create the application and start the update loop
- const app = new pc.Application(canvas, {});
- app.start();
-
- app.scene.ambientLight = new pc.Color(0.2, 0.2, 0.2);
-
- function createMaterial(color: pc.Color) {
- const material = new pc.StandardMaterial();
- material.diffuse = color;
- material.update();
-
- return material;
- }
-
- // Create a couple of materials
- const red = createMaterial(new pc.Color(1, 0, 0));
- const green = createMaterial(new pc.Color(0, 1, 0));
-
- // Create light
- const light = new pc.Entity();
- light.addComponent("light", {
- type: "directional"
- });
-
- app.root.addChild(light);
- light.setEulerAngles(45, 30, 0);
-
- // Create camera
- const camera = new pc.Entity();
- camera.addComponent("camera", {
- clearColor: new pc.Color(0.5, 0.5, 0.8)
- });
-
- app.root.addChild(camera);
- camera.setPosition(5, 0, 15);
-
- function createPhysicalShape(type: string, material: pc.Material, x: number, y: number, z: number) {
- const e = new pc.Entity();
-
- // Have to set the position of the entity before adding the static rigidbody
- // component because static bodies cannot be moved after creation
- app.root.addChild(e);
- e.setPosition(x, y, z);
-
- e.addComponent("render", {
- type: type,
- material: material
- });
- e.addComponent("rigidbody", {
- type: "static"
- });
- e.addComponent("collision", {
- type: type,
- height: type === 'capsule' ? 2 : 1
- });
-
- return e;
- }
-
- // Create two rows of physical geometric shapes
- const types = ['box', 'capsule', 'cone', 'cylinder', 'sphere'];
- types.forEach(function (type, idx) {
- createPhysicalShape(type, green, idx * 2 + 1, 2, 0);
- });
- types.forEach(function (type, idx) {
- createPhysicalShape(type, green, idx * 2 + 1, -2, 0);
- });
-
- // Allocate some colors
- const white = new pc.Color(1, 1, 1);
- const blue = new pc.Color(0, 0, 1);
-
- // Allocate some vectors
- const start = new pc.Vec3();
- const end = new pc.Vec3();
- const temp = new pc.Vec3();
-
- // Set an update function on the application's update event
- let time = 0;
- let y = 0;
- app.on("update", function (dt) {
- time += dt;
-
- // Reset all shapes to green
- app.root.findComponents('render').forEach(function (render: pc.RenderComponent) {
- render.material = green;
- });
-
- y = 2 + 1.2 * Math.sin(time);
- start.set(0, y, 0);
- end.set(10, y, 0);
-
- // Render the ray used in the raycast
- app.renderLine(start, end, white);
-
- // @ts-ignore engine-tsd
- const result = app.systems.rigidbody.raycastFirst(start, end);
- if (result) {
- result.entity.render.material = red;
-
- // Render the normal on the surface from the hit point
- // @ts-ignore engine-tsd
- temp.copy(result.normal).scale(0.3).add(result.point);
- app.renderLine(result.point, temp, blue);
- }
-
- y = -2 + 1.2 * Math.sin(time);
- start.set(0, y, 0);
- end.set(10, y, 0);
-
- // Render the ray used in the raycast
- app.renderLine(start, end, white);
-
- // @ts-ignore engine-tsd
- const results = app.systems.rigidbody.raycastAll(start, end);
- results.forEach(function (result: { entity: pc.Entity, point: pc.Vec3 }) {
- result.entity.render.material = red;
-
- // Render the normal on the surface from the hit point
- // @ts-ignore engine-tsd
- temp.copy(result.normal).scale(0.3).add(result.point);
- app.renderLine(result.point, temp, blue);
- }, this);
- });
-
- const createText = function (fontAsset: pc.Asset, message: string, x: number, y: number, z: number, rot: number) {
- // Create a text element-based entity
- const text = new pc.Entity();
- text.addComponent("element", {
- anchor: [0.5, 0.5, 0.5, 0.5],
- fontAsset: fontAsset,
- fontSize: 0.5,
- pivot: [0, 0.5],
- text: message,
- type: pc.ELEMENTTYPE_TEXT
- });
- text.setLocalPosition(x, y, z);
- text.setLocalEulerAngles(0, 0, rot);
- app.root.addChild(text);
- };
-
- createText(assets.font, 'raycastFirst', 0.5, 3.75, 0, 0);
- createText(assets.font, 'raycastAll', 0.5, -0.25, 0, 0);
- }
- }
-}
-
-export default RaycastExample;
diff --git a/examples/src/examples/physics/vehicle.example.mjs b/examples/src/examples/physics/vehicle.example.mjs
new file mode 100644
index 00000000000..c8a58f49571
--- /dev/null
+++ b/examples/src/examples/physics/vehicle.example.mjs
@@ -0,0 +1,220 @@
+import { deviceType, rootPath } from 'examples/utils';
+import * as pc from 'playcanvas';
+
+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`
+});
+
+await new Promise((resolve) => {
+ pc.WasmModule.getInstance('Ammo', () => resolve());
+});
+
+const assets = {
+ helipad: new pc.Asset(
+ 'helipad-env-atlas',
+ 'texture',
+ { url: `${rootPath}/static/assets/cubemaps/helipad-env-atlas.png` },
+ { type: pc.TEXTURETYPE_RGBP, mipmaps: false }
+ ),
+ script1: new pc.Asset('script1', 'script', { url: `${rootPath}/static/scripts/camera/tracking-camera.js` }),
+ script2: new pc.Asset('script2', 'script', { url: `${rootPath}/static/scripts/physics/render-physics.js` }),
+ script3: new pc.Asset('script3', 'script', { url: `${rootPath}/static/scripts/physics/action-physics-reset.js` }),
+ script4: new pc.Asset('script4', 'script', { url: `${rootPath}/static/scripts/physics/vehicle.js` })
+};
+
+const gfxOptions = {
+ deviceTypes: [deviceType]
+};
+
+const device = await pc.createGraphicsDevice(canvas, gfxOptions);
+device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
+
+const createOptions = new pc.AppOptions();
+createOptions.graphicsDevice = device;
+createOptions.keyboard = new pc.Keyboard(document.body);
+
+createOptions.componentSystems = [
+ pc.ModelComponentSystem,
+ pc.CameraComponentSystem,
+ pc.LightComponentSystem,
+ pc.ScriptComponentSystem,
+ pc.CollisionComponentSystem,
+ pc.RigidBodyComponentSystem
+];
+createOptions.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler, pc.ScriptHandler, pc.JsonHandler];
+
+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 assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
+assetListLoader.load(() => {
+ app.start();
+
+ // setup skydome
+ app.scene.skyboxMip = 2;
+ app.scene.exposure = 0.3;
+ app.scene.envAtlas = assets.helipad.resource;
+
+ const lighting = app.scene.lighting;
+ lighting.shadowsEnabled = false;
+
+ // Create a static ground shape for our car to drive on
+ const ground = new pc.Entity('Ground');
+ ground.addComponent('rigidbody', {
+ type: 'static'
+ });
+ ground.addComponent('collision', {
+ type: 'box',
+ halfExtents: new pc.Vec3(50, 0.5, 50)
+ });
+ ground.setLocalPosition(0, -0.5, 0);
+ app.root.addChild(ground);
+
+ // Create 4 wheels for our vehicle
+ const wheels = [
+ { name: 'Front Left Wheel', pos: new pc.Vec3(0.8, 0.4, 1.2), front: true },
+ { name: 'Front Right Wheel', pos: new pc.Vec3(-0.8, 0.4, 1.2), front: true },
+ { name: 'Back Left Wheel', pos: new pc.Vec3(0.8, 0.4, -1.2), front: false },
+ { name: 'Back Right Wheel', pos: new pc.Vec3(-0.8, 0.4, -1.2), front: false }
+ ].map((wheelDef) => {
+ // Create a wheel
+ const wheel = new pc.Entity(wheelDef.name);
+ wheel.addComponent('script');
+ wheel.script.create('vehicleWheel', {
+ attributes: {
+ debugRender: true,
+ isFront: wheelDef.front
+ }
+ });
+ wheel.setLocalPosition(wheelDef.pos);
+ return wheel;
+ });
+
+ // Create a physical vehicle
+ const vehicle = new pc.Entity('Vehicle');
+ vehicle.addComponent('rigidbody', {
+ mass: 800,
+ type: 'dynamic'
+ });
+ vehicle.addComponent('collision', {
+ type: 'compound'
+ });
+ vehicle.addComponent('script');
+ vehicle.script.create('vehicle', {
+ attributes: {
+ wheels: wheels
+ }
+ });
+ vehicle.script.create('vehicleControls');
+ vehicle.script.create('actionPhysicsReset', {
+ attributes: {
+ event: 'reset'
+ }
+ });
+ vehicle.setLocalPosition(0, 2, 0);
+
+ // Create the car chassis, offset upwards in Y from the compound body
+ const chassis = new pc.Entity('Chassis');
+ chassis.addComponent('collision', {
+ type: 'box',
+ halfExtents: [0.6, 0.35, 1.65]
+ });
+ chassis.setLocalPosition(0, 0.65, 0);
+
+ // Create the car chassis, offset upwards in Y from the compound body
+ const cab = new pc.Entity('Cab');
+ cab.addComponent('collision', {
+ type: 'box',
+ halfExtents: [0.5, 0.2, 1]
+ });
+ cab.setLocalPosition(0, 1.2, -0.25);
+
+ // Add the vehicle to the hierarchy
+ wheels.forEach((wheel) => {
+ vehicle.addChild(wheel);
+ });
+ vehicle.addChild(chassis);
+ vehicle.addChild(cab);
+ app.root.addChild(vehicle);
+
+ // Build a wall of blocks for the car to smash through
+ for (let i = 0; i < 10; i++) {
+ for (let j = 0; j < 5; j++) {
+ const block = new pc.Entity('Block');
+ block.addComponent('rigidbody', {
+ type: 'dynamic'
+ });
+ block.addComponent('collision', {
+ type: 'box'
+ });
+ block.addComponent('script');
+ block.script.create('actionPhysicsReset', {
+ attributes: {
+ event: 'reset'
+ }
+ });
+ block.setLocalPosition(i - 4.5, j + 0.5, -10);
+ app.root.addChild(block);
+ }
+ }
+
+ // Create a directional light source
+ const light = new pc.Entity('Directional Light');
+ light.addComponent('light', {
+ type: 'directional',
+ color: new pc.Color(1, 1, 1),
+ castShadows: true,
+ shadowBias: 0.2,
+ shadowDistance: 40,
+ normalOffsetBias: 0.05,
+ shadowResolution: 2048
+ });
+ light.setLocalEulerAngles(45, 30, 0);
+ app.root.addChild(light);
+
+ // Create a camera to render the scene
+ const camera = new pc.Entity('Camera');
+ camera.addComponent('camera');
+ camera.addComponent('script');
+ camera.script.create('trackingCamera', {
+ attributes: {
+ target: vehicle
+ }
+ });
+ camera.translate(0, 10, 15);
+ camera.lookAt(0, 0, 0);
+ app.root.addChild(camera);
+
+ // Enable rendering and resetting of all rigid bodies in the scene
+ app.root.addComponent('script');
+ app.root.script.create('renderPhysics', {
+ attributes: {
+ drawShapes: true,
+ opacity: 1
+ }
+ });
+
+ app.keyboard.on('keydown', (e) => {
+ if (e.key === pc.KEY_R) {
+ app.fire('reset');
+ }
+ });
+});
+
+export { app };
diff --git a/examples/src/examples/physics/vehicle.tsx b/examples/src/examples/physics/vehicle.tsx
deleted file mode 100644
index ac700dca11d..00000000000
--- a/examples/src/examples/physics/vehicle.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-import React from 'react';
-import * as pc from 'playcanvas/build/playcanvas.js';
-import Example from '../../app/example';
-import { AssetLoader } from '../../app/helpers/loader';
-
-class VehicleExample extends Example {
- static CATEGORY = 'Physics';
- static NAME = 'Vehicle';
-
- load() {
- return <>
-
-
-
-
- >;
- }
-
- // @ts-ignore: override class function
- example(canvas: HTMLCanvasElement, assets: { script1: pc.Asset, script2: pc.Asset, script3: pc.Asset, script4: pc.Asset }, wasmSupported: any, loadWasmModuleAsync: any): void {
-
- if (wasmSupported()) {
- loadWasmModuleAsync('Ammo', 'static/lib/ammo/ammo.wasm.js', 'static/lib/ammo/ammo.wasm.wasm', demo);
- } else {
- loadWasmModuleAsync('Ammo', 'static/lib/ammo/ammo.js', '', demo);
- }
-
- function demo() {
- // Create the application and start the update loop
- const app = new pc.Application(canvas, {
- keyboard: new pc.Keyboard(window)
- });
- app.start();
-
- // Create a static ground shape for our car to drive on
- const ground = new pc.Entity('Ground');
- ground.addComponent('rigidbody', {
- type: 'static'
- });
- ground.addComponent('collision', {
- type: 'box',
- halfExtents: new pc.Vec3(50, 0.5, 50)
- });
- ground.setLocalPosition(0, -0.5, 0);
- app.root.addChild(ground);
-
- // Create 4 wheels for our vehicle
- const wheels: any = [];
- [
- { name: 'Front Left Wheel', pos: new pc.Vec3(0.8, 0.4, 1.2), front: true },
- { name: 'Front Right Wheel', pos: new pc.Vec3(-0.8, 0.4, 1.2), front: true },
- { name: 'Back Left Wheel', pos: new pc.Vec3(0.8, 0.4, -1.2), front: false },
- { name: 'Back Right Wheel', pos: new pc.Vec3(-0.8, 0.4, -1.2), front: false }
- ].forEach(function (wheelDef) {
- // Create a wheel
- const wheel = new pc.Entity(wheelDef.name);
- wheel.addComponent('script');
- wheel.script.create('vehicleWheel', {
- attributes: {
- debugRender: true,
- isFront: wheelDef.front
- }
- });
- wheel.setLocalPosition(wheelDef.pos);
- wheels.push(wheel);
- });
-
- // Create a physical vehicle
- const vehicle = new pc.Entity('Vehicle');
- vehicle.addComponent('rigidbody', {
- mass: 800,
- type: 'dynamic'
- });
- vehicle.addComponent('collision', {
- type: 'compound'
- });
- vehicle.addComponent('script');
- vehicle.script.create('vehicle', {
- attributes: {
- wheels: wheels
- }
- });
- vehicle.script.create('vehicleControls');
- vehicle.script.create('actionPhysicsReset', {
- attributes: {
- event: 'reset'
- }
- });
- vehicle.setLocalPosition(0, 2, 0);
-
- // Create the car chassis, offset upwards in Y from the compound body
- const chassis = new pc.Entity('Chassis');
- chassis.addComponent('collision', {
- type: 'box',
- halfExtents: [0.6, 0.35, 1.65]
- });
- chassis.setLocalPosition(0, 0.65, 0);
-
- // Create the car chassis, offset upwards in Y from the compound body
- const cab = new pc.Entity('Cab');
- cab.addComponent('collision', {
- type: 'box',
- halfExtents: [0.5, 0.2, 1]
- });
- cab.setLocalPosition(0, 1.2, -0.25);
-
- // Add the vehicle to the hierarchy
- wheels.forEach(function (wheel: pc.Entity) {
- vehicle.addChild(wheel);
- });
- vehicle.addChild(chassis);
- vehicle.addChild(cab);
- app.root.addChild(vehicle);
-
- // Build a wall of blocks for the car to smash through
- for (let i = 0; i < 10; i++) {
- for (let j = 0; j < 5; j++) {
- const block = new pc.Entity('Block');
- block.addComponent('rigidbody', {
- type: 'dynamic'
- });
- block.addComponent('collision', {
- type: 'box'
- });
- block.addComponent('script');
- block.script.create('actionPhysicsReset', {
- attributes: {
- event: 'reset'
- }
- });
- block.setLocalPosition(i - 4.5, j + 0.5, -10);
- app.root.addChild(block);
- }
- }
-
- // Create a directional light source
- const light = new pc.Entity('Directional Light');
- light.addComponent("light", {
- type: "directional",
- color: new pc.Color(1, 1, 1),
- castShadows: true,
- shadowBias: 0.2,
- shadowDistance: 40,
- normalOffsetBias: 0.05,
- shadowResolution: 2048
- });
- light.setLocalEulerAngles(45, 30, 0);
- app.root.addChild(light);
-
- // Create a camera to render the scene
- const camera = new pc.Entity('Camera');
- camera.addComponent("camera");
- camera.addComponent('script');
- camera.script.create('trackingCamera', {
- attributes: {
- target: vehicle
- }
- });
- camera.translate(0, 10, 15);
- camera.lookAt(0, 0, 0);
- app.root.addChild(camera);
-
- // Enable rendering and resetting of all rigid bodies in the scene
- app.root.addComponent('script');
- app.root.script.create('renderPhysics', {
- attributes: {
- drawShapes: true,
- opacity: 1
- }
- });
-
- app.keyboard.on(pc.EVENT_KEYDOWN, function (e) {
- if (e.key === pc.KEY_R) {
- app.fire('reset');
- }
- });
- }
- }
-}
-
-export default VehicleExample;
diff --git a/examples/src/examples/shaders/cloud-shadows.controls.mjs b/examples/src/examples/shaders/cloud-shadows.controls.mjs
new file mode 100644
index 00000000000..0b8a9f57656
--- /dev/null
+++ b/examples/src/examples/shaders/cloud-shadows.controls.mjs
@@ -0,0 +1,57 @@
+/**
+ * @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, SliderInput } = ReactPCUI;
+ return fragment(
+ jsx(
+ Panel,
+ { headerText: 'Cloud Shadows' },
+ jsx(
+ LabelGroup,
+ { text: 'Speed' },
+ jsx(SliderInput, {
+ binding: new BindingTwoWay(),
+ link: { observer, path: 'data.speed' },
+ min: 0,
+ max: 0.2,
+ precision: 3
+ })
+ ),
+ jsx(
+ LabelGroup,
+ { text: 'Direction' },
+ jsx(SliderInput, {
+ binding: new BindingTwoWay(),
+ link: { observer, path: 'data.direction' },
+ min: 0,
+ max: 360,
+ precision: 0
+ })
+ ),
+ jsx(
+ LabelGroup,
+ { text: 'Intensity' },
+ jsx(SliderInput, {
+ binding: new BindingTwoWay(),
+ link: { observer, path: 'data.intensity' },
+ min: 0,
+ max: 1,
+ precision: 2
+ })
+ ),
+ jsx(
+ LabelGroup,
+ { text: 'Scale' },
+ jsx(SliderInput, {
+ binding: new BindingTwoWay(),
+ link: { observer, path: 'data.scale' },
+ min: 0.01,
+ max: 0.5,
+ precision: 3
+ })
+ )
+ )
+ );
+};
diff --git a/examples/src/examples/shaders/cloud-shadows.example.mjs b/examples/src/examples/shaders/cloud-shadows.example.mjs
new file mode 100644
index 00000000000..8edc6439567
--- /dev/null
+++ b/examples/src/examples/shaders/cloud-shadows.example.mjs
@@ -0,0 +1,172 @@
+// @config DESCRIPTION This example demonstrates scrolling cloud shadows using a shader chunk override on StandardMaterial.
+import { data } from 'examples/observer';
+import { deviceType, rootPath, localImport } from 'examples/utils';
+import * as pc from 'playcanvas';
+
+const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
+window.focus();
+
+const assets = {
+ tree: new pc.Asset('tree', 'container', { url: `${rootPath}/static/assets/models/low-poly-tree.glb` }),
+ clouds: new pc.Asset('clouds', 'texture', { url: `${rootPath}/static/assets/textures/clouds.jpg` }),
+ envAtlas: new pc.Asset(
+ 'env-atlas',
+ 'texture',
+ { url: `${rootPath}/static/assets/cubemaps/morning-env-atlas.png` },
+ { type: pc.TEXTURETYPE_RGBP, mipmaps: false }
+ )
+};
+
+const gfxOptions = {
+ deviceTypes: [deviceType]
+};
+
+const device = await pc.createGraphicsDevice(canvas, gfxOptions);
+device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
+
+const shaderLanguage = device.isWebGPU ? pc.SHADERLANGUAGE_WGSL : pc.SHADERLANGUAGE_GLSL;
+const shaderChunkFile = device.isWebGPU ? 'shader-chunks.wgsl.mjs' : 'shader-chunks.glsl.mjs';
+const shaderChunks = await localImport(shaderChunkFile);
+
+const createOptions = new pc.AppOptions();
+createOptions.graphicsDevice = device;
+
+createOptions.componentSystems = [pc.RenderComponentSystem, pc.CameraComponentSystem, pc.LightComponentSystem];
+createOptions.resourceHandlers = [pc.TextureHandler, pc.ContainerHandler];
+
+const app = new pc.AppBase(canvas);
+app.init(createOptions);
+
+app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
+app.setCanvasResolution(pc.RESOLUTION_AUTO);
+
+const resize = () => app.resizeCanvas();
+window.addEventListener('resize', resize);
+app.on('destroy', () => {
+ window.removeEventListener('resize', resize);
+});
+
+const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
+assetListLoader.load(() => {
+ app.start();
+
+ app.scene.envAtlas = assets.envAtlas.resource;
+ app.scene.skyboxMip = 1;
+ app.scene.exposure = 0.4;
+
+ // camera
+ const camera = new pc.Entity();
+ camera.addComponent('camera', {
+ toneMapping: pc.TONEMAP_ACES,
+ clearColor: new pc.Color(0.55, 0.7, 0.9)
+ });
+ app.root.addChild(camera);
+
+ // directional light with shadows
+ const light = new pc.Entity();
+ light.addComponent('light', {
+ type: 'directional',
+ castShadows: true,
+ shadowBias: 0.2,
+ normalOffsetBias: 0.06,
+ shadowDistance: 35
+ });
+ app.root.addChild(light);
+ light.setLocalEulerAngles(45, 30, 0);
+
+ // instanced trees
+ const instanceCount = 1000;
+ const matrices = new Float32Array(instanceCount * 16);
+ let matrixIndex = 0;
+
+ const pos = new pc.Vec3();
+ const rot = new pc.Quat();
+ const scl = new pc.Vec3();
+ const matrix = new pc.Mat4();
+
+ for (let i = 0; i < instanceCount; i++) {
+ const maxRadius = 20;
+ const angle = Math.random() * 2 * Math.PI;
+ const radius = Math.sqrt(Math.random() * (maxRadius ** 2));
+
+ pos.set(radius * Math.cos(angle), 0, radius * Math.sin(angle));
+ scl.set(0.1 + Math.random() * 0.2, 0.1 + Math.random() * 0.3, 0.1 + Math.random() * 0.2);
+ pos.y = -1.5 + scl.y * 4.5;
+ matrix.setTRS(pos, rot, scl);
+
+ for (let m = 0; m < 16; m++) matrices[matrixIndex++] = matrix.data[m];
+ }
+
+ const vbFormat = pc.VertexFormat.getDefaultInstancingFormat(app.graphicsDevice);
+ const vertexBuffer = new pc.VertexBuffer(app.graphicsDevice, vbFormat, instanceCount, {
+ data: matrices
+ });
+
+ const forest = assets.tree.resource.instantiateRenderEntity();
+ app.root.addChild(forest);
+ const meshInstance = forest.findComponent('render').meshInstances[0];
+ meshInstance.setInstancing(vertexBuffer);
+
+ // apply cloud shadow chunks to tree material
+ const treeMaterial = meshInstance.material;
+ treeMaterial.getShaderChunks(shaderLanguage).add(shaderChunks);
+ treeMaterial.shaderChunksVersion = '2.8';
+
+ // ground plane with cloud shadow chunks
+ const groundMaterial = new pc.StandardMaterial();
+ groundMaterial.getShaderChunks(shaderLanguage).add(shaderChunks);
+ groundMaterial.shaderChunksVersion = '2.8';
+
+ const ground = new pc.Entity('Ground');
+ ground.addComponent('render', {
+ type: 'cylinder',
+ material: groundMaterial
+ });
+ ground.setLocalScale(50, 1, 50);
+ ground.setLocalPosition(0, -2, 0);
+ app.root.addChild(ground);
+
+ // ensure the cloud texture wraps so the scrolling tiles seamlessly
+ const cloudTexture = assets.clouds.resource;
+ cloudTexture.addressU = pc.ADDRESS_REPEAT;
+ cloudTexture.addressV = pc.ADDRESS_REPEAT;
+
+ // set default control values
+ data.set('data', {
+ speed: 0.03,
+ direction: 30,
+ intensity: 0.9,
+ scale: 0.01
+ });
+
+ const scope = app.graphicsDevice.scope;
+ let time = 0;
+ let offsetX = 0;
+ let offsetY = 0;
+
+ app.on('update', (dt) => {
+ time += dt;
+
+ const speed = data.get('data.speed');
+ const directionDeg = data.get('data.direction');
+ const intensity = data.get('data.intensity');
+ const scale = data.get('data.scale');
+
+ // scroll direction from angle
+ const dirRad = directionDeg * Math.PI / 180;
+ offsetX += Math.cos(dirRad) * speed * dt;
+ offsetY += Math.sin(dirRad) * speed * dt;
+
+ // set cloud shadow uniforms globally - all materials with the chunk receive them
+ scope.resolve('cloudShadowTexture').setValue(cloudTexture);
+ scope.resolve('cloudShadowOffset').setValue([offsetX, offsetY]);
+ scope.resolve('cloudShadowScale').setValue(scale);
+ scope.resolve('cloudShadowIntensity').setValue(intensity);
+
+ // orbit camera
+ camera.setLocalPosition(18 * Math.sin(time * 0.05), 10, 18 * Math.cos(time * 0.05));
+ camera.lookAt(pc.Vec3.ZERO);
+ });
+});
+
+export { app };
diff --git a/examples/src/examples/shaders/cloud-shadows.shader-chunks.glsl.mjs b/examples/src/examples/shaders/cloud-shadows.shader-chunks.glsl.mjs
new file mode 100644
index 00000000000..01a63369aba
--- /dev/null
+++ b/examples/src/examples/shaders/cloud-shadows.shader-chunks.glsl.mjs
@@ -0,0 +1,25 @@
+/**
+ * GLSL shader chunks for the cloud shadows example.
+ * These chunks override StandardMaterial to add scrolling cloud shadow modulation.
+ */
+
+export const litUserDeclarationPS = /* glsl */ `
+ uniform sampler2D cloudShadowTexture;
+ uniform vec2 cloudShadowOffset;
+ uniform float cloudShadowScale;
+ uniform float cloudShadowIntensity;
+`;
+
+// Override endPS to apply cloud shadow after combineColor but before emission/fog/tonemap/gamma
+export const endPS = /* glsl */ `
+ gl_FragColor.rgb = combineColor(litArgs_albedo, litArgs_sheen_specularity, litArgs_clearcoat_specularity);
+
+ vec2 cloudUV = vPositionW.xz * cloudShadowScale + cloudShadowOffset;
+ float cloud = texture2D(cloudShadowTexture, cloudUV).r;
+ gl_FragColor.rgb *= mix(1.0, cloud, cloudShadowIntensity);
+
+ gl_FragColor.rgb += litArgs_emission;
+ gl_FragColor.rgb = addFog(gl_FragColor.rgb);
+ gl_FragColor.rgb = toneMap(gl_FragColor.rgb);
+ gl_FragColor.rgb = gammaCorrectOutput(gl_FragColor.rgb);
+`;
diff --git a/examples/src/examples/shaders/cloud-shadows.shader-chunks.wgsl.mjs b/examples/src/examples/shaders/cloud-shadows.shader-chunks.wgsl.mjs
new file mode 100644
index 00000000000..715e679644d
--- /dev/null
+++ b/examples/src/examples/shaders/cloud-shadows.shader-chunks.wgsl.mjs
@@ -0,0 +1,27 @@
+/**
+ * WGSL shader chunks for the cloud shadows example.
+ * These chunks override StandardMaterial to add scrolling cloud shadow modulation.
+ */
+
+export const litUserDeclarationPS = /* wgsl */ `
+ var cloudShadowTexture: texture_2d;
+ var cloudShadowTextureSampler: sampler;
+ uniform cloudShadowOffset: vec2f;
+ uniform cloudShadowScale: f32;
+ uniform cloudShadowIntensity: f32;
+`;
+
+// Override endPS to apply cloud shadow after combineColor but before emission/fog/tonemap/gamma
+export const endPS = /* wgsl */ `
+ var finalRgb: vec3f = combineColor(litArgs_albedo, litArgs_sheen_specularity, litArgs_clearcoat_specularity);
+
+ let cloudUV: vec2f = vPositionW.xz * uniform.cloudShadowScale + uniform.cloudShadowOffset;
+ let cloud: f32 = textureSample(cloudShadowTexture, cloudShadowTextureSampler, cloudUV).r;
+ finalRgb = finalRgb * mix(1.0, cloud, uniform.cloudShadowIntensity);
+
+ finalRgb = finalRgb + litArgs_emission;
+ finalRgb = addFog(finalRgb);
+ finalRgb = toneMap(finalRgb);
+ finalRgb = gammaCorrectOutput(finalRgb);
+ output.color = vec4f(finalRgb, output.color.a);
+`;
diff --git a/examples/src/examples/shaders/grab-pass.example.mjs b/examples/src/examples/shaders/grab-pass.example.mjs
new file mode 100644
index 00000000000..9c3ad8e3c0f
--- /dev/null
+++ b/examples/src/examples/shaders/grab-pass.example.mjs
@@ -0,0 +1,190 @@
+import files from 'examples/files';
+import { deviceType, rootPath } from 'examples/utils';
+import * as pc from 'playcanvas';
+
+const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
+window.focus();
+
+const assets = {
+ normal: new pc.Asset('normal', 'texture', { url: `${rootPath}/static/assets/textures/normal-map.png` }),
+ roughness: new pc.Asset('roughness', 'texture', { url: `${rootPath}/static/assets/textures/pc-gray.png` }),
+ helipad: new pc.Asset(
+ 'helipad-env-atlas',
+ 'texture',
+ { url: `${rootPath}/static/assets/cubemaps/helipad-env-atlas.png` },
+ { type: pc.TEXTURETYPE_RGBP, mipmaps: false }
+ )
+};
+
+const gfxOptions = {
+ deviceTypes: [deviceType]
+};
+
+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];
+createOptions.resourceHandlers = [pc.TextureHandler];
+
+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 assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
+assetListLoader.load(() => {
+ app.start();
+
+ // setup skydome
+ app.scene.skyboxMip = 0;
+ app.scene.exposure = 2;
+ app.scene.envAtlas = assets.helipad.resource;
+
+ // Depth layer is where the framebuffer is copied to a texture to be used in the following layers.
+ // Move the depth layer to take place after World and Skydome layers, to capture both of them.
+ const depthLayer = app.scene.layers.getLayerById(pc.LAYERID_DEPTH);
+ app.scene.layers.remove(depthLayer);
+ app.scene.layers.insertOpaque(depthLayer, 2);
+
+ /**
+ * Helper function to create a primitive with shape type, position, scale, color.
+ *
+ * @param {string} primitiveType - The primitive type.
+ * @param {pc.Vec3} position - The position.
+ * @param {pc.Vec3} scale - The scale.
+ * @param {pc.Color} color - The color.
+ * @returns {pc.Entity} - The created primitive entity.
+ */
+ function createPrimitive(primitiveType, position, scale, color) {
+ // create material of specified color
+ const material = new pc.StandardMaterial();
+ material.diffuse = color;
+ material.gloss = 0.6;
+ material.metalness = 0.4;
+ material.useMetalness = true;
+ material.update();
+
+ // create primitive
+ const primitive = new pc.Entity();
+ primitive.addComponent('render', {
+ type: primitiveType,
+ material: material
+ });
+
+ // set position and scale and add it to scene
+ primitive.setLocalPosition(position);
+ primitive.setLocalScale(scale);
+ app.root.addChild(primitive);
+
+ return primitive;
+ }
+
+ /**
+ * create few primitives, keep their references to rotate them later
+ * @type {pc.Entity[]}
+ */
+ const primitives = [];
+ const count = 7;
+ const shapes = ['box', 'cone', 'cylinder', 'sphere', 'capsule'];
+ for (let i = 0; i < count; i++) {
+ const shapeName = shapes[Math.floor(Math.random() * shapes.length)];
+ const color = new pc.Color(Math.random(), Math.random(), Math.random());
+ const angle = (2 * Math.PI * i) / count;
+ const pos = new pc.Vec3(12 * Math.sin(angle), 0, 12 * Math.cos(angle));
+ primitives.push(createPrimitive(shapeName, pos, new pc.Vec3(4, 8, 4), color));
+ }
+
+ // Create the camera, which renders entities
+ const camera = new pc.Entity('SceneCamera');
+ camera.addComponent('camera', {
+ clearColor: new pc.Color(0.2, 0.2, 0.2),
+ toneMapping: pc.TONEMAP_ACES
+ });
+ app.root.addChild(camera);
+ camera.setLocalPosition(0, 10, 20);
+ camera.lookAt(pc.Vec3.ZERO);
+
+ // enable the camera to render the scene's color map.
+ camera.camera.requestSceneColorMap(true);
+
+ // create a primitive which uses refraction shader to distort the view behind it
+ const glass = createPrimitive('box', new pc.Vec3(1, 3, 0), new pc.Vec3(10, 10, 10), new pc.Color(1, 1, 1));
+ glass.render.castShadows = false;
+ glass.render.receiveShadows = false;
+
+ // reflection material using the shader
+ const refractionMaterial = new pc.ShaderMaterial({
+ uniqueName: 'RefractionShader',
+ vertexGLSL: files['shader.glsl.vert'],
+ fragmentGLSL: files['shader.glsl.frag'],
+ vertexWGSL: files['shader.wgsl.vert'],
+ fragmentWGSL: files['shader.wgsl.frag'],
+ attributes: {
+ vertex_position: pc.SEMANTIC_POSITION,
+ vertex_texCoord0: pc.SEMANTIC_TEXCOORD0
+ }
+ });
+ glass.render.material = refractionMaterial;
+
+ // set an offset map on the material
+ refractionMaterial.setParameter('uOffsetMap', assets.normal.resource);
+
+ // set roughness map
+ refractionMaterial.setParameter('uRoughnessMap', assets.roughness.resource);
+
+ // tint colors
+ refractionMaterial.setParameter(
+ 'tints[0]',
+ new Float32Array([
+ 1,
+ 0.7,
+ 0.7, // red
+ 1,
+ 1,
+ 1, // white
+ 0.7,
+ 0.7,
+ 1, // blue
+ 1,
+ 1,
+ 1 // white
+ ])
+ );
+
+ // transparency
+ refractionMaterial.blendType = pc.BLEND_NORMAL;
+ refractionMaterial.update();
+
+ // update things each frame
+ let time = 0;
+ app.on('update', (dt) => {
+ time += dt;
+
+ // rotate the primitives
+ primitives.forEach((prim) => {
+ prim.rotate(0.3, 0.2, 0.1);
+ });
+
+ glass.rotate(-0.1, 0.1, -0.15);
+
+ // orbit the camera
+ camera.setLocalPosition(20 * Math.sin(time * 0.2), 7, 20 * Math.cos(time * 0.2));
+ camera.lookAt(new pc.Vec3(0, 2, 0));
+ });
+});
+
+export { app };
diff --git a/examples/src/examples/shaders/grab-pass.shader.glsl.frag b/examples/src/examples/shaders/grab-pass.shader.glsl.frag
new file mode 100644
index 00000000000..5f6469cbca1
--- /dev/null
+++ b/examples/src/examples/shaders/grab-pass.shader.glsl.frag
@@ -0,0 +1,46 @@
+// use the special uSceneColorMap texture, which is a built-in texture containing
+// a copy of the color buffer at the point of capture, inside the Depth layer.
+uniform sampler2D uSceneColorMap;
+
+// normal map providing offsets
+uniform sampler2D uOffsetMap;
+
+// roughness map
+uniform sampler2D uRoughnessMap;
+
+// tint colors
+uniform vec3 tints[4];
+
+// engine built-in constant storing render target size in .xy and inverse size in .zw
+uniform vec4 uScreenSize;
+
+varying vec2 texCoord;
+
+void main(void)
+{
+ float roughness = 1.0 - texture2D(uRoughnessMap, texCoord).r;
+
+ // sample offset texture - used to add distortion to the sampled background
+ vec2 offset = texture2D(uOffsetMap, texCoord).rg;
+ offset = 2.0 * offset - 1.0;
+
+ // offset strength
+ offset *= (0.2 + roughness) * 0.015;
+
+ // get normalized uv coordinates for canvas
+ vec2 grabUv = gl_FragCoord.xy * uScreenSize.zw;
+
+ // roughness dictates which mipmap level gets used, in 0..4 range
+ float mipmap = roughness * 5.0;
+
+ // get background pixel color with distorted offset
+ vec3 grabColor = texture2DLod(uSceneColorMap, grabUv + offset, mipmap).rgb;
+
+ // tint the material based on mipmap
+ float tintIndex = clamp(mipmap, 0.0, 3.0);
+ grabColor *= tints[int(tintIndex)];
+
+ // brighten the refracted texture a little bit
+ // brighten even more the rough parts of the glass
+ gl_FragColor = vec4(grabColor * 1.1, 1.0) + roughness * 0.09;
+}
diff --git a/examples/src/examples/shaders/grab-pass.shader.glsl.vert b/examples/src/examples/shaders/grab-pass.shader.glsl.vert
new file mode 100644
index 00000000000..e1d3a1ab0c7
--- /dev/null
+++ b/examples/src/examples/shaders/grab-pass.shader.glsl.vert
@@ -0,0 +1,16 @@
+attribute vec4 vertex_position;
+attribute vec2 vertex_texCoord0;
+
+uniform mat4 matrix_model;
+uniform mat4 matrix_viewProjection;
+
+varying vec2 texCoord;
+
+void main(void)
+{
+ // project the position
+ vec4 pos = matrix_model * vertex_position;
+ gl_Position = matrix_viewProjection * pos;
+
+ texCoord = vertex_texCoord0;
+}
diff --git a/examples/src/examples/shaders/grab-pass.shader.wgsl.frag b/examples/src/examples/shaders/grab-pass.shader.wgsl.frag
new file mode 100644
index 00000000000..444a34f7196
--- /dev/null
+++ b/examples/src/examples/shaders/grab-pass.shader.wgsl.frag
@@ -0,0 +1,52 @@
+// use the special uSceneColorMap texture, which is a built-in texture containing
+// a copy of the color buffer at the point of capture, inside the Depth layer.
+var uSceneColorMap: texture_2d