diff --git a/examples/animation.html b/examples/animation.html index a6d6acf..c78ae12 100644 --- a/examples/animation.html +++ b/examples/animation.html @@ -20,7 +20,7 @@ - + diff --git a/examples/basic-shapes.html b/examples/basic-shapes.html index b9ed1b0..7db8c07 100644 --- a/examples/basic-shapes.html +++ b/examples/basic-shapes.html @@ -19,7 +19,7 @@ - + diff --git a/examples/car-configurator.html b/examples/car-configurator.html index 300547f..950720f 100644 --- a/examples/car-configurator.html +++ b/examples/car-configurator.html @@ -23,7 +23,7 @@ - + diff --git a/examples/glb.html b/examples/glb.html index 12ce866..234d227 100644 --- a/examples/glb.html +++ b/examples/glb.html @@ -20,7 +20,7 @@ - + diff --git a/examples/physics.html b/examples/physics.html index 5a4c1b4..22640a2 100644 --- a/examples/physics.html +++ b/examples/physics.html @@ -20,9 +20,9 @@ - + - + diff --git a/examples/positional-sound.html b/examples/positional-sound.html index ef2bde8..8cb77bc 100644 --- a/examples/positional-sound.html +++ b/examples/positional-sound.html @@ -19,7 +19,7 @@ - + diff --git a/examples/scripts/grid.mjs b/examples/scripts/grid.mjs deleted file mode 100644 index be00275..0000000 --- a/examples/scripts/grid.mjs +++ /dev/null @@ -1,271 +0,0 @@ -import { - ShaderMaterial, - SEMANTIC_POSITION, - SEMANTIC_TEXCOORD0, - BLEND_NORMAL, - CULLFACE_NONE, - PlaneGeometry, - Mesh, - MeshInstance, - Color, - Script, - Vec2 -} from 'playcanvas'; - -const tmpVa = new Vec2(); - -const EPISILON = 1e-3; - -const vertexCode = /* glsl */ ` - attribute vec3 vertex_position; - attribute vec2 aUv0; - - uniform mat4 matrix_model; - uniform mat4 matrix_viewProjection; - - varying vec2 uv0; - - void main(void) { - gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0); - uv0 = aUv0; - } -`; - -const fragmentCode = /* glsl */ ` - uniform vec2 uHalfExtents; - uniform vec3 uColorX; - uniform vec3 uColorZ; - - varying vec2 uv0; - - // https://bgolus.medium.com/the-best-darn-grid-shader-yet-727f9278b9d8#1e7c - float pristineGrid(in vec2 uv, in vec2 ddx, in vec2 ddy, vec2 lineWidth) { - vec2 uvDeriv = vec2(length(vec2(ddx.x, ddy.x)), length(vec2(ddx.y, ddy.y))); - bvec2 invertLine = bvec2(lineWidth.x > 0.5, lineWidth.y > 0.5); - vec2 targetWidth = vec2( - invertLine.x ? 1.0 - lineWidth.x : lineWidth.x, - invertLine.y ? 1.0 - lineWidth.y : lineWidth.y - ); - vec2 drawWidth = clamp(targetWidth, uvDeriv, vec2(0.5)); - vec2 lineAA = uvDeriv * 1.5; - vec2 gridUV = abs(fract(uv) * 2.0 - 1.0); - gridUV.x = invertLine.x ? gridUV.x : 1.0 - gridUV.x; - gridUV.y = invertLine.y ? gridUV.y : 1.0 - gridUV.y; - vec2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV); - - grid2 *= clamp(targetWidth / drawWidth, 0.0, 1.0); - grid2 = mix(grid2, targetWidth, clamp(uvDeriv * 2.0 - 1.0, 0.0, 1.0)); - grid2.x = invertLine.x ? 1.0 - grid2.x : grid2.x; - grid2.y = invertLine.y ? 1.0 - grid2.y : grid2.y; - - return mix(grid2.x, 1.0, grid2.y); - } - - void main(void) { - vec2 uv = uv0; - - vec2 pos = (uv * 2.0 - 1.0) * uHalfExtents; - vec2 ddx = dFdx(pos); - vec2 ddy = dFdy(pos); - - float epsilon = 1.0 / 255.0; - - vec2 levelPos; - float levelSize; - float levelAlpha; - - levelPos = pos * 0.1; - levelSize = 2.0 / 1000.0; - levelAlpha = pristineGrid(levelPos, ddx * 0.1, ddy * 0.1, vec2(levelSize)); - if (levelAlpha > epsilon) { - vec3 color; - if (abs(levelPos.x) < levelSize) { - if (abs(levelPos.y) < levelSize) { - color = vec3(1.0); - } else { - color = uColorZ; - } - } else if (abs(levelPos.y) < levelSize) { - color = uColorX; - } else { - color = vec3(0.9); - } - gl_FragColor = vec4(color, levelAlpha); - return; - } - - levelPos = pos; - levelSize = 1.0 / 100.0; - levelAlpha = pristineGrid(levelPos, ddx, ddy, vec2(levelSize)); - if (levelAlpha > epsilon) { - gl_FragColor = vec4(vec3(0.7), levelAlpha); - return; - } - - levelPos = pos * 10.0; - levelSize = 1.0 / 100.0; - levelAlpha = pristineGrid(levelPos, ddx * 10.0, ddy * 10.0, vec2(levelSize)); - if (levelAlpha > epsilon) { - gl_FragColor = vec4(vec3(0.7), levelAlpha); - return; - } - - discard; - } -`; - -class Grid extends Script { - /** - * @type {ShaderMaterial} - * @private - */ - _material; - - /** - * @type {MeshInstance} - * @private - */ - _meshInstance; - - /** - * @type {Vec2} - * @private - */ - _halfExtents = new Vec2(); - - /** - * @type {Color} - * @private - */ - _colorX = new Color(1, 0.3, 0.3); - - /** - * @type {Color} - * @private - */ - _colorZ = new Color(0.3, 0.3, 1); - - constructor(args) { - super(args); - const { - colorX, - colorZ - } = args.attributes; - - // ensure the entity has a render component - if (!this.entity.render) { - this.entity.addComponent('render'); - } - - // create shader material - this._material = new ShaderMaterial({ - uniqueName: 'grid-shader', - vertexCode, - fragmentCode, - attributes: { - vertex_position: SEMANTIC_POSITION, - aUv0: SEMANTIC_TEXCOORD0 - } - }); - this._material.blendType = BLEND_NORMAL; - this._material.cull = CULLFACE_NONE; - this._material.update(); - - // create mesh - const mesh = Mesh.fromGeometry(this.app.graphicsDevice, new PlaneGeometry()); - this._meshInstance = new MeshInstance(mesh, this._material); - this.entity.render.meshInstances = [this._meshInstance]; - - // set the initial values - this.halfExtents = this._calcHalfExtents(tmpVa) ?? this.halfExtents; - this.colorX = colorX ?? this.colorX; - this.colorZ = colorZ ?? this.colorZ; - - // update the half extents when the entity scale changes - this.app.on('prerender', () => { - const halfExtents = this._calcHalfExtents(tmpVa); - if (this.halfExtents.distance(halfExtents) > EPISILON) { - this.halfExtents = halfExtents; - } - }); - } - - /** - * @param {Vec2} vec - The vector to copy the half extents to. - * @returns {Vec2} - The half extents. - * @private - */ - _calcHalfExtents(vec) { - const scale = this.entity.getLocalScale(); - return vec.set(scale.x / 2, scale.z / 2); - } - - /** - * @param {string} name - The name of the parameter. - * @param {Color|Vec2} value - The value of the parameter. - * @private - */ - _set(name, value) { - if (value instanceof Color) { - this._material.setParameter(name, [value.r, value.g, value.b]); - } - - if (value instanceof Vec2) { - this._material.setParameter(name, [value.x, value.y]); - } - - this._material.update(); - - this._meshInstance.material = this._material; - } - - /** - * @attribute - * @type {Vec2} - */ - set halfExtents(value) { - if (!(value instanceof Vec2)) { - return; - } - this._halfExtents.copy(value); - this._set('uHalfExtents', this._halfExtents); - } - - get halfExtents() { - return this._halfExtents; - } - - /** - * @attribute - * @type {Color} - */ - set colorX(value) { - if (!(value instanceof Color)) { - return; - } - this._colorX.copy(value); - this._set('uColorX', this._colorX); - } - - get colorX() { - return this._colorX; - } - - /** - * @attribute - * @type {Color} - */ - set colorZ(value) { - if (!(value instanceof Color)) { - return; - } - this._colorZ.copy(value); - this._set('uColorZ', this._colorZ); - } - - get colorZ() { - return this._colorZ; - } -} - -export { Grid }; diff --git a/examples/scripts/xr-navigation.mjs b/examples/scripts/xr-navigation.mjs deleted file mode 100644 index 451906a..0000000 --- a/examples/scripts/xr-navigation.mjs +++ /dev/null @@ -1,162 +0,0 @@ -import { Color, Quat, Script, Vec3 } from 'playcanvas'; - -/** @import { XrInputSource } from 'playcanvas' */ - -/** - * Handles VR teleportation navigation by allowing users to point and teleport using either - * hands or tracked controllers. Shows a visual ray and target indicator when the user holds - * the select button (trigger) or makes a pinch gesture with hand tracking, and teleports to - * the target location when released. - * - * This script should be attached to a parent entity of the camera entity used for the XR - * session. Use it in conjunction with the `XrControllers` script to handle the rendering of - * the controllers. - */ -export class XrNavigation extends Script { - /** @type {Set} */ - inputSources = new Set(); - - /** @type {Map} */ - activePointers = new Map(); - - validColor = new Color(0, 1, 0); // Green for valid teleport - - invalidColor = new Color(1, 0, 0); // Red for invalid teleport - - /** @type {Map} */ - inputHandlers = new Map(); - - initialize() { - const positionRoot = new Vec3(); - const rotationRoot = new Quat(); - const positionCamera = new Vec3(); - const rotationCamera = new Quat(); - - this.app.xr.on('start', () => { - positionRoot.copy(this.entity.getPosition()); - rotationRoot.copy(this.entity.getRotation()); - - const camera = this.entity.findComponent('camera')?.entity; - if (camera) { - positionCamera.copy(camera.getPosition()); - rotationCamera.copy(camera.getRotation()); - - this.entity.setPosition(positionCamera.x, 0, positionCamera.z); - this.entity.lookAt(Vec3.ZERO, Vec3.UP); - } - }); - - this.app.xr.on('end', () => { - this.entity.setPosition(positionRoot); - this.entity.setRotation(rotationRoot); - - const camera = this.entity.findComponent('camera')?.entity; - if (camera) { - camera.setPosition(positionCamera); - camera.setRotation(rotationCamera); - } - }); - - this.app.xr.input.on('add', (inputSource) => { - const handleSelectStart = () => { - this.activePointers.set(inputSource, true); - }; - - const handleSelectEnd = () => { - this.activePointers.set(inputSource, false); - this.tryTeleport(inputSource); - }; - - // Attach the handlers - inputSource.on('selectstart', handleSelectStart); - inputSource.on('selectend', handleSelectEnd); - - // Store the handlers in the map - this.inputHandlers.set(inputSource, { handleSelectStart, handleSelectEnd }); - this.inputSources.add(inputSource); - }); - - this.app.xr.input.on('remove', (inputSource) => { - const handlers = this.inputHandlers.get(inputSource); - if (handlers) { - inputSource.off('selectstart', handlers.handleSelectStart); - inputSource.off('selectend', handlers.handleSelectEnd); - this.inputHandlers.delete(inputSource); - } - this.activePointers.delete(inputSource); - this.inputSources.delete(inputSource); - }); - } - - findPlaneIntersection(origin, direction) { - // Find intersection with y=0 plane - if (Math.abs(direction.y) < 0.00001) return null; // Ray is parallel to plane - - const t = -origin.y / direction.y; - if (t < 0) return null; // Intersection is behind the ray - - return new Vec3( - origin.x + direction.x * t, - 0, - origin.z + direction.z * t - ); - } - - tryTeleport(inputSource) { - const origin = inputSource.getOrigin(); - const direction = inputSource.getDirection(); - - const hitPoint = this.findPlaneIntersection(origin, direction); - if (hitPoint) { - const cameraY = this.entity.getPosition().y; - hitPoint.y = cameraY; - this.entity.setPosition(hitPoint); - } - } - - update() { - for (const inputSource of this.inputSources) { - // Only show ray when trigger is pressed - if (!this.activePointers.get(inputSource)) continue; - - const start = inputSource.getOrigin(); - const direction = inputSource.getDirection(); - - const hitPoint = this.findPlaneIntersection(start, direction); - - if (hitPoint) { - // Draw line to intersection point - this.app.drawLine(start, hitPoint, this.validColor); - this.drawTeleportIndicator(hitPoint); - } else { - // Draw full length ray if no intersection - const end = start.clone().add( - direction.clone().mulScalar(100) - ); - this.app.drawLine(start, end, this.invalidColor); - } - } - } - - drawTeleportIndicator(point) { - // Draw a circle at the teleport point - const segments = 32; - const radius = 0.2; - - for (let i = 0; i < segments; i++) { - const angle1 = (i / segments) * Math.PI * 2; - const angle2 = ((i + 1) / segments) * Math.PI * 2; - - const x1 = point.x + Math.cos(angle1) * radius; - const z1 = point.z + Math.sin(angle1) * radius; - const x2 = point.x + Math.cos(angle2) * radius; - const z2 = point.z + Math.sin(angle2) * radius; - - this.app.drawLine( - new Vec3(x1, 0.01, z1), // Slightly above ground to avoid z-fighting - new Vec3(x2, 0.01, z2), - this.validColor - ); - } - } -} diff --git a/examples/shoe-configurator.html b/examples/shoe-configurator.html index 60b9fd1..ee57549 100644 --- a/examples/shoe-configurator.html +++ b/examples/shoe-configurator.html @@ -20,7 +20,7 @@ - + diff --git a/examples/splat.html b/examples/splat.html index 0661f02..bcc45c5 100644 --- a/examples/splat.html +++ b/examples/splat.html @@ -19,7 +19,7 @@ - + diff --git a/examples/text3d.html b/examples/text3d.html index e0d571b..ff5aa3a 100644 --- a/examples/text3d.html +++ b/examples/text3d.html @@ -20,10 +20,10 @@ - + - + diff --git a/examples/video-texture.html b/examples/video-texture.html index fdf9cdb..25d6195 100644 --- a/examples/video-texture.html +++ b/examples/video-texture.html @@ -24,7 +24,7 @@ - + diff --git a/src/app.ts b/src/app.ts index c3da1b2..214603e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,4 @@ -import { Application, CameraComponent, FILLMODE_FILL_WINDOW, Keyboard, Mouse, Picker, RESOLUTION_AUTO } from 'playcanvas'; +import { Application, CameraComponent, FILLMODE_FILL_WINDOW, GraphNode, Keyboard, Mouse, Picker, RESOLUTION_AUTO } from 'playcanvas'; import { AssetElement } from './asset'; import { AsyncElement } from './async-element'; @@ -209,13 +209,14 @@ class AppElement extends AsyncElement { // Get the currently hovered entity by walking up the hierarchy let newHoverEntity = null; if (selection.length > 0) { - let node = selection[0].node; - while (node && !newHoverEntity) { - const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`) as EntityElement; + let currentNode: GraphNode | null = selection[0].node; + while (currentNode !== null) { + const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`) as EntityElement; if (entityElement) { newHoverEntity = entityElement; + break; } - node = node.parent; + currentNode = currentNode.parent; } } @@ -252,14 +253,14 @@ class AppElement extends AsyncElement { const selection = this._picker.getSelection(x, y); if (selection.length > 0) { - let node = selection[0].node; - while (node) { - const entityElement = this.querySelector(`pc-entity[name="${node.name}"]`) as EntityElement; + let currentNode: GraphNode | null = selection[0].node; + while (currentNode !== null) { + const entityElement = this.querySelector(`pc-entity[name="${currentNode.name}"]`) as EntityElement; if (entityElement && entityElement.hasListeners('pointerdown')) { entityElement.dispatchEvent(new PointerEvent('pointerdown', event)); break; } - node = node.parent; + currentNode = currentNode.parent; } } }