@threlte/xr
Version:
Tools to more easily create VR and AR experiences with Threlte
96 lines (95 loc) • 4.26 kB
JavaScript
import { Matrix4, Quaternion, Vector3 } from 'three';
import { useThrelte } from '@threlte/core';
import { useXROrigin } from './useXROrigin.svelte.js';
const defaultOrientation = new Quaternion();
const offset = { x: 0, y: 0, z: 0 };
const targetPosition = new Vector3();
const localOffset = new Vector3();
const worldOffset = new Vector3();
const originWorldPosition = new Vector3();
const parentWorldQuaternion = new Quaternion();
const localQuaternion = new Quaternion();
const inverseParentQuaternion = new Quaternion();
const localBasis = new Matrix4();
const worldBasis = new Matrix4();
const inverseParentMatrix = new Matrix4();
/**
* Returns a callback that teleports the player to a target position and optional orientation.
*
* When used inside `<XROrigin>`, the origin group is translated directly — the
* user's feet end up at the target, and their room-scale offset from the origin is preserved.
*
* When used outside `<XROrigin>`, the underlying `XRReferenceSpace` is mutated to compensate
* for the viewer's current position so the feet end up at the target regardless of where the
* user has walked in their physical space.
*
* @example
* const teleport = useTeleport()
* teleport([5, 0, 5])
*
* const quat = new THREE.Quaternion()
* teleport(new THREE.Vector3(5, 0, 5), quat)
*/
export const useTeleport = () => {
const { xr } = useThrelte().renderer;
const xrOrigin = useXROrigin();
return (position, orientation = defaultOrientation) => {
const currentOrigin = xrOrigin.current;
if (currentOrigin !== undefined) {
if (Array.isArray(position)) {
targetPosition.set(position[0], position[1], position[2]);
}
else {
targetPosition.copy(position);
}
const parent = currentOrigin.parent;
const space = xr.getReferenceSpace();
const pose = space === null ? undefined : xr.getFrame()?.getViewerPose(space);
localOffset.set(0, 0, 0);
if (pose !== undefined && pose !== null) {
localOffset.set(pose.transform.position.x, 0, pose.transform.position.z);
}
if (parent === null) {
localQuaternion.copy(orientation);
worldBasis.compose(originWorldPosition.set(0, 0, 0), localQuaternion, currentOrigin.scale);
worldOffset.copy(localOffset).applyMatrix4(worldBasis);
currentOrigin.position.copy(targetPosition).sub(worldOffset);
currentOrigin.quaternion.copy(localQuaternion);
}
else {
parent.updateWorldMatrix(true, false);
parent.getWorldQuaternion(parentWorldQuaternion);
inverseParentQuaternion.copy(parentWorldQuaternion).invert();
localQuaternion.copy(inverseParentQuaternion).multiply(orientation);
localBasis.compose(originWorldPosition.set(0, 0, 0), localQuaternion, currentOrigin.scale);
worldBasis.copy(parent.matrixWorld).multiply(localBasis);
worldOffset.copy(localOffset).applyMatrix4(worldBasis);
originWorldPosition.copy(targetPosition).sub(worldOffset);
inverseParentMatrix.copy(parent.matrixWorld).invert();
currentOrigin.position.copy(originWorldPosition.applyMatrix4(inverseParentMatrix));
currentOrigin.quaternion.copy(localQuaternion);
}
return;
}
const space = xr.getReferenceSpace();
if (space === null)
return;
if (Array.isArray(position)) {
offset.x = -position[0];
offset.y = -position[1];
offset.z = -position[2];
}
else {
offset.x = -position.x;
offset.y = -position.y;
offset.z = -position.z;
}
const pose = xr.getFrame()?.getViewerPose(space);
if (pose !== undefined) {
offset.x += pose.transform.position.x;
offset.z += pose.transform.position.z;
}
const teleportOffset = new XRRigidTransform(offset, orientation);
xr.setReferenceSpace(space.getOffsetReferenceSpace(teleportOffset));
};
};