-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Init XR camera's transform correctly (#72)
* Init XR camera's transform correctly * Lint fixes
- Loading branch information
1 parent
c523176
commit 936764e
Showing
11 changed files
with
172 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
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<XrInputSource>} */ | ||
inputSources = new Set(); | ||
|
||
/** @type {Map<XrInputSource, boolean>} */ | ||
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<XrInputSource, { handleSelectStart: Function, handleSelectEnd: Function }>} */ | ||
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 | ||
); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters