1
0

More reffactors

This commit is contained in:
Aiden
2026-06-10 17:23:06 +10:00
parent 707cad3719
commit ea184ba448
5 changed files with 828 additions and 710 deletions

View File

@@ -18,10 +18,8 @@ import {
import { createMediaTexture as createMediaTextureCore } from './rendering/three-utils.js';
import { FallbackCameraControls } from './modes/fallback-camera-controls.js';
import { MediaController } from './media/media-controller.js';
import {
createVrInputRig,
handleVrControllerSelect
} from './xr/vr-controller-interactions.js';
import { createVrInputRig } from './xr/input-rig.js';
import { handleVrControllerSelect } from './xr/vr-controller-interactions.js';
import { bindVideoEvents } from './media/video-events.js';
import {
createVrControlPanel,

View File

@@ -0,0 +1,52 @@
export type PointerInputMode = 'controller' | 'hand';
export type PointerInputModeCarrier = {
controller?: {
userData?: any;
};
pointerInputMode?: PointerInputMode;
};
export function rememberPointerInputMode(
inputSource: PointerInputModeCarrier,
event: any,
fallbackMode: PointerInputMode
): void {
const eventInputSource = event?.data?.inputSource || event?.inputSource || event?.data;
const nextMode = getPointerInputMode(eventInputSource) || fallbackMode;
inputSource.pointerInputMode = nextMode;
if (!inputSource.controller) {
return;
}
inputSource.controller.userData = {
...inputSource.controller.userData,
vrwpInputSource: inputSource
};
}
export function getPointerInputMode(eventInputSource: any): PointerInputMode | null {
if (!eventInputSource) {
return null;
}
if (eventInputSource.hand) {
return 'hand';
}
if (Array.isArray(eventInputSource.profiles) &&
eventInputSource.profiles.some((profile: string) => profile.toLowerCase().includes('hand'))) {
return 'hand';
}
if (eventInputSource.gamepad || eventInputSource.targetRayMode === 'tracked-pointer') {
return 'controller';
}
return null;
}
export function shouldUseHandPointer(inputSource: PointerInputModeCarrier | undefined): boolean {
return inputSource?.pointerInputMode === 'hand';
}

View File

@@ -0,0 +1,445 @@
import * as THREE from 'https://unpkg.com/three/build/three.module.js';
import {
getSeekProgressFromIntersection,
type VrControlPanel
} from './vr-control-panel.js';
import {
beginPalmAimSelection,
computePalmAimRay,
createPalmAimLatch,
endPalmAimSelection,
getPalmAimSelectionRay,
recordStablePalmAimRay,
type PalmAimLatch,
type PalmAimRay,
type VectorLike
} from './hand-aim.js';
import {
rememberPointerInputMode,
shouldUseHandPointer,
type PointerInputMode
} from './input-mode.js';
import {
bindOverlayActivity,
createControllerOverlay,
createHandOverlay,
createPointerOverlay,
createWorldPointerOverlay,
POINTER_HIT_SURFACE_OFFSET,
POINTER_LENGTH,
POINTER_MIN_LENGTH,
setPointerOverlayLength,
VrOverlayVisibility
} from './pointer-overlays.js';
export type VrInputRig = {
beginSeekDrag: (controller: any, panel: VrControlPanel | undefined, onSeek: (progress: number) => void) => void;
hideOverlays: () => void;
raycaster: any;
showOverlays: (timestamp?: number) => void;
update: (timestamp: number, hoverTargets?: any[]) => boolean;
};
type AimRay = {
direction: any;
origin: any;
};
type ActiveSeekDrag = {
inputSource: VrInputSource;
onSeek: (progress: number) => void;
panel: VrControlPanel;
};
type HandPointerOverlay = {
fallbackPointerOverlay: any;
hand: any;
handAimLatch: PalmAimLatch;
inputSource: VrInputSource;
pointerOverlay: any;
};
type VrInputSource = {
controller: any;
controllerPointerOverlay: any;
hand?: any;
handAimLatch?: PalmAimLatch;
handPointerOverlay?: any;
pointerInputMode: PointerInputMode;
};
const DEFAULT_RAY_DIRECTION = new THREE.Vector3(0, 0, -1);
const tempMatrix = new THREE.Matrix4();
export function createVrInputRig(scene: any, renderer: any, onSelectStart: (event: any) => void): VrInputRig {
const overlayVisibility = new VrOverlayVisibility();
const handPointerOverlays: HandPointerOverlay[] = [];
const inputSources: VrInputSource[] = [];
const raycaster = createPointerRaycaster();
const hoverRaycaster = createPointerRaycaster();
const dragRaycaster = createPointerRaycaster();
let activeSeekDrag: ActiveSeekDrag | null = null;
for (let index = 0; index < 2; index += 1) {
const controller = renderer.xr.getController(index);
const controllerPointerOverlay = createPointerOverlay(index, overlayVisibility);
const inputSource: VrInputSource = {
controller,
controllerPointerOverlay,
pointerInputMode: 'controller'
};
controller.userData = {
...controller.userData,
vrwpInputSource: inputSource
};
inputSources.push(inputSource);
controller.addEventListener('connected', (event: any) => {
rememberPointerInputMode(inputSource, event, 'controller');
});
controller.addEventListener('selectstart', (event: any) => {
const timestamp = getEventTimestamp(event);
rememberPointerInputMode(inputSource, event, inputSource.pointerInputMode);
if (shouldUseHandPointer(inputSource)) {
beginPalmAimSelection(controller.userData?.vrwpHandAimLatch, timestamp);
}
overlayVisibility.show(timestamp);
onSelectStart(event);
});
controller.addEventListener('selectend', () => {
endPalmAimSelection(controller.userData?.vrwpHandAimLatch);
if (activeSeekDrag?.inputSource.controller === controller) {
activeSeekDrag = null;
}
});
controller.addEventListener('select', () => {
endPalmAimSelection(controller.userData?.vrwpHandAimLatch);
});
bindOverlayActivity(controller, overlayVisibility);
controller.add(controllerPointerOverlay);
scene.add(controller);
const grip = renderer.xr.getControllerGrip?.(index);
if (grip) {
grip.addEventListener('connected', (event: any) => {
rememberPointerInputMode(inputSource, event, 'controller');
});
bindOverlayActivity(grip, overlayVisibility);
grip.add(createControllerOverlay(index, overlayVisibility));
scene.add(grip);
}
const hand = renderer.xr.getHand?.(index);
if (hand) {
const handAimLatch = createPalmAimLatch();
inputSource.hand = hand;
inputSource.handAimLatch = handAimLatch;
controller.userData = {
...controller.userData,
vrwpHand: hand,
vrwpHandAimLatch: handAimLatch
};
hand.userData = {
...hand.userData,
vrwpAimLatch: handAimLatch
};
bindOverlayActivity(hand, overlayVisibility);
rememberHandedness(hand, { data: hand.inputState });
createHandOverlay(hand, index, overlayVisibility);
hand.addEventListener?.('connected', (event: any) => {
rememberPointerInputMode(inputSource, event, 'hand');
rememberHandedness(hand, event);
createHandOverlay(hand, index, overlayVisibility);
overlayVisibility.show();
});
scene.add(hand);
const handPointerOverlay = createWorldPointerOverlay(index, overlayVisibility);
inputSource.handPointerOverlay = handPointerOverlay;
scene.add(handPointerOverlay);
handPointerOverlays.push({
fallbackPointerOverlay: controllerPointerOverlay,
hand,
handAimLatch,
inputSource,
pointerOverlay: handPointerOverlay
});
}
}
overlayVisibility.hideImmediately();
return {
beginSeekDrag: (controller: any, panel: VrControlPanel | undefined, onSeek: (progress: number) => void) => {
const inputSource = getInputSourceByController(inputSources, controller);
if (!inputSource || !panel?.seekBarHitAreaMesh) {
activeSeekDrag = null;
return;
}
activeSeekDrag = { inputSource, onSeek, panel };
},
hideOverlays: () => overlayVisibility.hideImmediately(),
raycaster,
showOverlays: (timestamp?: number) => overlayVisibility.show(timestamp),
update: (timestamp: number, hoverTargets: any[] = []) => {
updateHandPointerOverlays(handPointerOverlays, timestamp);
updateActiveSeekDrag(activeSeekDrag, dragRaycaster, timestamp);
const isHovering = updateInputPointerIntersections(inputSources, hoverTargets, hoverRaycaster, timestamp);
if (isHovering) {
overlayVisibility.show(timestamp);
}
overlayVisibility.update(timestamp);
return isHovering;
}
};
}
function createPointerRaycaster(): any {
const raycaster = new THREE.Raycaster();
raycaster.near = 0.1;
raycaster.far = POINTER_LENGTH;
return raycaster;
}
function getInputSourceByController(inputSources: VrInputSource[], controller: any): VrInputSource | undefined {
return inputSources.find((inputSource) => inputSource.controller === controller);
}
function updateInputPointerIntersections(
inputSources: VrInputSource[],
hoverTargets: any[],
hoverRaycaster: any,
timestamp: number
): boolean {
let isHoveringAnyTarget = false;
inputSources.forEach((inputSource) => {
resetInputPointerLengths(inputSource);
const aimRay = getInputSourceAimRay(inputSource, timestamp);
const pointerOverlay = getActivePointerOverlay(inputSource);
if (!aimRay || !pointerOverlay || hoverTargets.length === 0) {
return;
}
hoverRaycaster.ray.origin.copy(aimRay.origin);
hoverRaycaster.ray.direction.copy(aimRay.direction);
const intersections = hoverRaycaster.intersectObjects(hoverTargets, true);
if (intersections.length === 0) {
return;
}
isHoveringAnyTarget = true;
setPointerOverlayLength(pointerOverlay, getPointerIntersectionLength(intersections[0].distance));
});
return isHoveringAnyTarget;
}
function updateActiveSeekDrag(activeSeekDrag: ActiveSeekDrag | null, dragRaycaster: any, timestamp: number): void {
if (!activeSeekDrag?.panel.seekBarHitAreaMesh) {
return;
}
const aimRay = getInputSourceAimRay(activeSeekDrag.inputSource, timestamp, { preferLiveHandAim: true });
if (!aimRay) {
return;
}
dragRaycaster.ray.origin.copy(aimRay.origin);
dragRaycaster.ray.direction.copy(aimRay.direction);
const intersections = dragRaycaster.intersectObject(activeSeekDrag.panel.seekBarHitAreaMesh, true);
if (intersections.length === 0) {
return;
}
activeSeekDrag.onSeek(getSeekProgressFromIntersection(activeSeekDrag.panel, intersections[0].point));
}
function getInputSourceAimRay(
inputSource: VrInputSource,
timestamp: number,
{ preferLiveHandAim = false }: { preferLiveHandAim?: boolean } = {}
): AimRay | null {
if (shouldUseHandPointer(inputSource) && inputSource.hand && inputSource.handAimLatch) {
if (preferLiveHandAim) {
const handRay = getHandAimRay(inputSource.hand);
if (handRay) {
return handRay;
}
}
const latchedRay = getPalmAimSelectionRay(inputSource.handAimLatch, timestamp);
if (latchedRay) {
return toAimRay(latchedRay);
}
const handRay = getHandAimRay(inputSource.hand);
if (handRay) {
return handRay;
}
}
return getControllerAimRay(inputSource.controller);
}
function getControllerAimRay(controller: any): AimRay | null {
if (!controller) {
return null;
}
controller.updateMatrixWorld();
tempMatrix.identity().extractRotation(controller.matrixWorld);
const origin = new THREE.Vector3().setFromMatrixPosition(controller.matrixWorld);
const direction = new THREE.Vector3(0, 0, -1).applyMatrix4(tempMatrix);
return { direction, origin };
}
function resetInputPointerLengths(inputSource: VrInputSource): void {
setPointerOverlayLength(inputSource.controllerPointerOverlay, POINTER_LENGTH);
if (inputSource.handPointerOverlay) {
setPointerOverlayLength(inputSource.handPointerOverlay, POINTER_LENGTH);
}
}
function getActivePointerOverlay(inputSource: VrInputSource): any {
if (inputSource.handPointerOverlay && inputSource.handPointerOverlay.userData?.vrwpOverlayAvailable !== false) {
return inputSource.handPointerOverlay;
}
if (inputSource.controllerPointerOverlay && inputSource.controllerPointerOverlay.userData?.vrwpOverlayAvailable !== false) {
return inputSource.controllerPointerOverlay;
}
return null;
}
function getPointerIntersectionLength(distance: number): number {
return Math.max(
POINTER_MIN_LENGTH,
Math.min(POINTER_LENGTH, distance - POINTER_HIT_SURFACE_OFFSET)
);
}
function updateHandPointerOverlays(handPointerOverlays: HandPointerOverlay[], timestamp: number): void {
handPointerOverlays.forEach(({ fallbackPointerOverlay, hand, handAimLatch, inputSource, pointerOverlay }) => {
if (!shouldUseHandPointer(inputSource)) {
pointerOverlay.userData = {
...pointerOverlay.userData,
vrwpOverlayAvailable: false
};
fallbackPointerOverlay.userData = {
...fallbackPointerOverlay.userData,
vrwpOverlayAvailable: true
};
return;
}
const currentHandRay = getHandAimRay(hand);
if (currentHandRay) {
recordStablePalmAimRay(handAimLatch, toPalmAimRay(currentHandRay), timestamp);
}
const selectionPalmRay = getPalmAimSelectionRay(handAimLatch, timestamp);
const displayHandRay = selectionPalmRay ? toAimRay(selectionPalmRay) : currentHandRay;
const hasHandRay = Boolean(displayHandRay);
pointerOverlay.userData = {
...pointerOverlay.userData,
vrwpOverlayAvailable: hasHandRay
};
fallbackPointerOverlay.userData = {
...fallbackPointerOverlay.userData,
vrwpOverlayAvailable: !hasHandRay
};
if (!displayHandRay) {
return;
}
pointerOverlay.position.copy(displayHandRay.origin);
pointerOverlay.quaternion.setFromUnitVectors(DEFAULT_RAY_DIRECTION, displayHandRay.direction);
});
}
function getHandAimRay(hand: any): AimRay | null {
const joints = hand?.joints;
if (!joints) {
return null;
}
const palmAimRay = computePalmAimRay({
handedness: getHandedness(hand),
indexMetacarpal: getJointWorldPosition(joints['index-finger-metacarpal']),
middleMetacarpal: getJointWorldPosition(joints['middle-finger-metacarpal']),
pinkyMetacarpal: getJointWorldPosition(joints['pinky-finger-metacarpal']),
ringMetacarpal: getJointWorldPosition(joints['ring-finger-metacarpal']),
wrist: getJointWorldPosition(joints.wrist)
});
if (!palmAimRay) {
return null;
}
const origin = toThreeVector(palmAimRay.origin);
const direction = toThreeVector(palmAimRay.direction);
return { direction, origin };
}
function toPalmAimRay(ray: AimRay): PalmAimRay {
return {
direction: fromThreeVector(ray.direction),
origin: fromThreeVector(ray.origin)
};
}
function toAimRay(ray: PalmAimRay): AimRay {
return {
direction: toThreeVector(ray.direction),
origin: toThreeVector(ray.origin)
};
}
function rememberHandedness(hand: any, event: any): void {
const handedness = event?.data?.handedness ||
event?.data?.inputSource?.handedness ||
hand?.inputState?.handedness;
if (handedness !== 'left' && handedness !== 'right') {
return;
}
hand.userData = {
...hand.userData,
vrwpHandedness: handedness
};
}
function getHandedness(hand: any): string | undefined {
return hand?.userData?.vrwpHandedness ||
hand?.inputState?.handedness ||
hand?.userData?.inputSource?.handedness;
}
function getJointWorldPosition(joint: any): VectorLike | null {
if (!joint?.getWorldPosition) {
return null;
}
joint.updateMatrixWorld?.(true);
return joint.getWorldPosition(new THREE.Vector3());
}
function toThreeVector(vector: VectorLike): any {
return new THREE.Vector3(vector.x, vector.y, vector.z);
}
function fromThreeVector(vector: any): VectorLike {
return {
x: vector.x,
y: vector.y,
z: vector.z
};
}
function getEventTimestamp(event: any): number {
return Number.isFinite(event?.timeStamp) ? event.timeStamp : performance.now();
}

View File

@@ -0,0 +1,328 @@
import * as THREE from 'https://unpkg.com/three/build/three.module.js';
export type PointerOverlayInputSource = {
controllerPointerOverlay: any;
handPointerOverlay?: any;
};
export type VrOverlayVisibilityOptions = {
fadeDurationMs?: number;
hideDelayMs?: number;
};
export const INPUT_OVERLAY_HIDE_DELAY_MS = 2500;
export const INPUT_OVERLAY_FADE_DURATION_MS = 200;
export const INPUT_OVERLAY_RENDER_ORDER = 10000;
export const POINTER_LENGTH = 5;
export const POINTER_MIN_LENGTH = 0.06;
export const POINTER_HIT_SURFACE_OFFSET = 0.015;
const HAND_JOINT_NAMES = [
'wrist',
'thumb-metacarpal',
'thumb-phalanx-proximal',
'thumb-phalanx-distal',
'thumb-tip',
'index-finger-metacarpal',
'index-finger-phalanx-proximal',
'index-finger-phalanx-intermediate',
'index-finger-phalanx-distal',
'index-finger-tip',
'middle-finger-metacarpal',
'middle-finger-phalanx-proximal',
'middle-finger-phalanx-intermediate',
'middle-finger-phalanx-distal',
'middle-finger-tip',
'ring-finger-metacarpal',
'ring-finger-phalanx-proximal',
'ring-finger-phalanx-intermediate',
'ring-finger-phalanx-distal',
'ring-finger-tip',
'pinky-finger-metacarpal',
'pinky-finger-phalanx-proximal',
'pinky-finger-phalanx-intermediate',
'pinky-finger-phalanx-distal',
'pinky-finger-tip'
];
export class VrOverlayVisibility {
private readonly fadeDurationMs: number;
private readonly hideDelayMs: number;
private readonly objects: any[] = [];
private opacity = 0;
private targetOpacity = 0;
private visibleUntil = 0;
constructor({
fadeDurationMs = INPUT_OVERLAY_FADE_DURATION_MS,
hideDelayMs = INPUT_OVERLAY_HIDE_DELAY_MS
}: VrOverlayVisibilityOptions = {}) {
this.fadeDurationMs = fadeDurationMs;
this.hideDelayMs = hideDelayMs;
}
register(object: any): void {
this.objects.push(object);
this.setObjectVisible(object, this.targetOpacity > 0 || this.opacity > 0.001);
this.setObjectOpacity(object, this.opacity);
}
show(timestamp = performance.now()): void {
this.visibleUntil = timestamp + this.hideDelayMs;
this.targetOpacity = 1;
this.objects.forEach((object) => this.setObjectVisible(object, true));
}
hideImmediately(): void {
this.visibleUntil = 0;
this.opacity = 0;
this.targetOpacity = 0;
this.objects.forEach((object) => {
this.setObjectOpacity(object, 0);
this.setObjectVisible(object, false);
});
}
update(timestamp: number): void {
if (this.targetOpacity > 0 && timestamp >= this.visibleUntil) {
this.targetOpacity = 0;
}
const shouldBeVisible = this.targetOpacity > 0 || this.opacity > 0.001;
this.objects.forEach((object) => this.setObjectVisible(object, shouldBeVisible));
if (this.opacity === this.targetOpacity) {
return;
}
const fadeStep = this.fadeDurationMs <= 0
? 1
: Math.min(1, 16.67 / this.fadeDurationMs);
const direction = this.opacity < this.targetOpacity ? 1 : -1;
this.opacity = Math.max(0, Math.min(1, this.opacity + fadeStep * direction));
if ((direction > 0 && this.opacity >= this.targetOpacity) || (direction < 0 && this.opacity <= this.targetOpacity)) {
this.opacity = this.targetOpacity;
}
this.objects.forEach((object) => {
this.setObjectOpacity(object, this.opacity);
this.setObjectVisible(object, this.opacity > 0.001);
});
}
private setObjectVisible(object: any, isVisible: boolean): void {
const objectVisible = isVisible && object.userData?.vrwpOverlayAvailable !== false;
object.visible = objectVisible;
object.traverse?.((child: any) => {
child.visible = objectVisible;
});
}
private setObjectOpacity(object: any, opacity: number): void {
object.traverse?.((child: any) => {
const materials = Array.isArray(child.material) ? child.material : [child.material];
materials.filter(Boolean).forEach((material: any) => {
material.opacity = opacity;
material.transparent = true;
material.depthTest = false;
material.depthWrite = false;
material.needsUpdate = true;
});
});
}
}
export function bindOverlayActivity(target: any, overlayVisibility: VrOverlayVisibility): void {
[
'connected',
'disconnected',
'select',
'selectend',
'squeezestart',
'squeeze',
'squeezeend',
'pinchstart',
'pinchend'
].forEach((eventName) => {
target.addEventListener?.(eventName, () => overlayVisibility.show());
});
}
export function createPointerOverlay(index: number, overlayVisibility: VrOverlayVisibility): any {
const group = new THREE.Group();
group.name = `vrPointerOverlay${index}`;
const pointerMaterial = createOverlayLineMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.75);
const pointerGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, -POINTER_LENGTH)
]);
const pointerLine = new THREE.Line(pointerGeometry, pointerMaterial);
pointerLine.name = `vrPointerRay${index}`;
pointerLine.renderOrder = INPUT_OVERLAY_RENDER_ORDER;
group.add(pointerLine);
const tipMaterial = createOverlayMeshMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.9);
const tipMesh = new THREE.Mesh(new THREE.SphereGeometry(0.018, 12, 8), tipMaterial);
tipMesh.name = `vrPointerTip${index}`;
tipMesh.position.z = -POINTER_LENGTH;
tipMesh.renderOrder = INPUT_OVERLAY_RENDER_ORDER + 1;
group.add(tipMesh);
group.userData = {
...group.userData,
vrwpPointerLength: POINTER_LENGTH,
vrwpPointerLine: pointerLine,
vrwpPointerTip: tipMesh
};
overlayVisibility.register(group);
return group;
}
export function createWorldPointerOverlay(index: number, overlayVisibility: VrOverlayVisibility): any {
const group = createPointerOverlay(index, overlayVisibility);
group.name = `vrHandPointerOverlay${index}`;
group.userData = {
...group.userData,
vrwpOverlayAvailable: false
};
return group;
}
export function createControllerOverlay(index: number, overlayVisibility: VrOverlayVisibility): any {
const group = new THREE.Group();
group.name = `vrControllerOverlay${index}`;
const material = createOverlayLineMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.8);
const outlineGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-0.045, -0.025, -0.08),
new THREE.Vector3(0.045, -0.025, -0.08),
new THREE.Vector3(0.055, 0.025, -0.02),
new THREE.Vector3(0.025, 0.035, 0.05),
new THREE.Vector3(-0.025, 0.035, 0.05),
new THREE.Vector3(-0.055, 0.025, -0.02),
new THREE.Vector3(-0.045, -0.025, -0.08)
]);
const outline = new THREE.Line(outlineGeometry, material);
outline.name = `vrControllerOutline${index}`;
outline.renderOrder = INPUT_OVERLAY_RENDER_ORDER;
group.add(outline);
const originMaterial = createOverlayMeshMaterial(0xffffff, 0.75);
const origin = new THREE.Mesh(new THREE.SphereGeometry(0.012, 10, 6), originMaterial);
origin.name = `vrControllerOrigin${index}`;
origin.renderOrder = INPUT_OVERLAY_RENDER_ORDER + 1;
group.add(origin);
overlayVisibility.register(group);
return group;
}
export function createHandOverlay(hand: any, index: number, overlayVisibility: VrOverlayVisibility): void {
const joints = getHandJoints(hand);
if (joints.length === 0) {
return;
}
const material = createOverlayMeshMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.85);
joints.forEach(({ joint, name }) => {
if (!joint || joint.userData?.vrwpHandOverlayMarker) {
return;
}
const isTip = name.endsWith('tip');
const isWrist = name === 'wrist';
const radius = isWrist ? 0.014 : isTip ? 0.011 : 0.008;
const marker = new THREE.Mesh(new THREE.SphereGeometry(radius, 10, 6), material);
marker.name = `vrHandJointOverlay${index}-${name}`;
marker.renderOrder = INPUT_OVERLAY_RENDER_ORDER + 2;
marker.frustumCulled = false;
joint.add(marker);
joint.userData = {
...joint.userData,
vrwpHandOverlayMarker: marker
};
overlayVisibility.register(marker);
});
}
export function resetInputPointerLengths(inputSource: PointerOverlayInputSource): void {
setPointerOverlayLength(inputSource.controllerPointerOverlay, POINTER_LENGTH);
if (inputSource.handPointerOverlay) {
setPointerOverlayLength(inputSource.handPointerOverlay, POINTER_LENGTH);
}
}
export function getPointerIntersectionLength(distance: number): number {
return Math.max(
POINTER_MIN_LENGTH,
Math.min(POINTER_LENGTH, distance - POINTER_HIT_SURFACE_OFFSET)
);
}
export function setPointerOverlayLength(pointerOverlay: any, length: number): void {
if (!pointerOverlay || pointerOverlay.userData?.vrwpPointerLength === length) {
return;
}
const pointerLine = pointerOverlay.userData?.vrwpPointerLine;
const pointerTip = pointerOverlay.userData?.vrwpPointerTip;
const positionAttribute = pointerLine?.geometry?.getAttribute?.('position') ||
pointerLine?.geometry?.attributes?.position;
if (positionAttribute?.setXYZ) {
positionAttribute.setXYZ(1, 0, 0, -length);
positionAttribute.needsUpdate = true;
pointerLine.geometry.computeBoundingSphere?.();
}
if (pointerTip) {
pointerTip.position.z = -length;
}
pointerOverlay.userData = {
...pointerOverlay.userData,
vrwpPointerLength: length
};
}
export function createOverlayLineMaterial(color: number, opacity: number): any {
return new THREE.LineBasicMaterial({
color,
depthTest: false,
depthWrite: false,
opacity,
transparent: true
});
}
export function createOverlayMeshMaterial(color: number, opacity: number): any {
return new THREE.MeshBasicMaterial({
color,
depthTest: false,
depthWrite: false,
opacity,
transparent: true
});
}
function getHandJoints(hand: any): Array<{ joint: any; name: string }> {
const joints = hand?.joints;
if (!joints) {
return [];
}
const namedJoints = HAND_JOINT_NAMES
.map((name) => ({ joint: joints[name], name }))
.filter(({ joint }) => Boolean(joint));
if (namedJoints.length > 0) {
return namedJoints;
}
return Object.entries(joints)
.map(([name, joint]) => ({ joint, name }))
.filter(({ joint }) => Boolean(joint));
}

View File

@@ -4,16 +4,12 @@ import {
type VrControlPanel
} from './vr-control-panel.js';
import {
beginPalmAimSelection,
computePalmAimRay,
createPalmAimLatch,
endPalmAimSelection,
getPalmAimSelectionRay,
recordStablePalmAimRay,
type PalmAimLatch,
type PalmAimRay,
type VectorLike
} from './hand-aim.js';
import { shouldUseHandPointer } from './input-mode.js';
type VrControllerSelectionOptions = {
beginSeekDrag?: (controller: any) => void;
@@ -32,214 +28,13 @@ type VrControllerSelectionOptions = {
vrPanel: VrControlPanel | undefined;
};
type VrInputRig = {
beginSeekDrag: (controller: any, panel: VrControlPanel | undefined, onSeek: (progress: number) => void) => void;
hideOverlays: () => void;
raycaster: any;
showOverlays: (timestamp?: number) => void;
update: (timestamp: number, hoverTargets?: any[]) => boolean;
};
type AimRay = {
direction: any;
origin: any;
};
type ActiveSeekDrag = {
inputSource: VrInputSource;
onSeek: (progress: number) => void;
panel: VrControlPanel;
};
type HandPointerOverlay = {
fallbackPointerOverlay: any;
hand: any;
handAimLatch: PalmAimLatch;
inputSource: VrInputSource;
pointerOverlay: any;
};
type PointerInputMode = 'controller' | 'hand';
type VrInputSource = {
controller: any;
controllerPointerOverlay: any;
hand?: any;
handAimLatch?: PalmAimLatch;
handPointerOverlay?: any;
pointerInputMode: PointerInputMode;
};
type VrOverlayVisibilityOptions = {
fadeDurationMs?: number;
hideDelayMs?: number;
};
const INPUT_OVERLAY_HIDE_DELAY_MS = 2500;
const INPUT_OVERLAY_FADE_DURATION_MS = 200;
const INPUT_OVERLAY_RENDER_ORDER = 10000;
const POINTER_LENGTH = 5;
const POINTER_MIN_LENGTH = 0.06;
const POINTER_HIT_SURFACE_OFFSET = 0.015;
const HAND_JOINT_NAMES = [
'wrist',
'thumb-metacarpal',
'thumb-phalanx-proximal',
'thumb-phalanx-distal',
'thumb-tip',
'index-finger-metacarpal',
'index-finger-phalanx-proximal',
'index-finger-phalanx-intermediate',
'index-finger-phalanx-distal',
'index-finger-tip',
'middle-finger-metacarpal',
'middle-finger-phalanx-proximal',
'middle-finger-phalanx-intermediate',
'middle-finger-phalanx-distal',
'middle-finger-tip',
'ring-finger-metacarpal',
'ring-finger-phalanx-proximal',
'ring-finger-phalanx-intermediate',
'ring-finger-phalanx-distal',
'ring-finger-tip',
'pinky-finger-metacarpal',
'pinky-finger-phalanx-proximal',
'pinky-finger-phalanx-intermediate',
'pinky-finger-phalanx-distal',
'pinky-finger-tip'
];
const DEFAULT_RAY_DIRECTION = new THREE.Vector3(0, 0, -1);
const tempMatrix = new THREE.Matrix4();
export function createVrInputRig(scene: any, renderer: any, onSelectStart: (event: any) => void): VrInputRig {
const overlayVisibility = new VrOverlayVisibility();
const handPointerOverlays: HandPointerOverlay[] = [];
const inputSources: VrInputSource[] = [];
const raycaster = new THREE.Raycaster();
raycaster.near = 0.1;
raycaster.far = POINTER_LENGTH;
const hoverRaycaster = new THREE.Raycaster();
hoverRaycaster.near = 0.1;
hoverRaycaster.far = POINTER_LENGTH;
const dragRaycaster = new THREE.Raycaster();
dragRaycaster.near = 0.1;
dragRaycaster.far = POINTER_LENGTH;
let activeSeekDrag: ActiveSeekDrag | null = null;
for (let index = 0; index < 2; index += 1) {
const controller = renderer.xr.getController(index);
const controllerPointerOverlay = createPointerOverlay(index, overlayVisibility);
const inputSource: VrInputSource = {
controller,
controllerPointerOverlay,
pointerInputMode: 'controller'
};
controller.userData = {
...controller.userData,
vrwpInputSource: inputSource
};
inputSources.push(inputSource);
controller.addEventListener('connected', (event: any) => {
rememberPointerInputMode(inputSource, event, 'controller');
});
controller.addEventListener('selectstart', (event: any) => {
const timestamp = getEventTimestamp(event);
rememberPointerInputMode(inputSource, event, inputSource.pointerInputMode);
if (shouldUseHandPointer(inputSource)) {
beginPalmAimSelection(controller.userData?.vrwpHandAimLatch, timestamp);
}
overlayVisibility.show(timestamp);
onSelectStart(event);
});
controller.addEventListener('selectend', () => {
endPalmAimSelection(controller.userData?.vrwpHandAimLatch);
if (activeSeekDrag?.inputSource.controller === controller) {
activeSeekDrag = null;
}
});
controller.addEventListener('select', () => {
endPalmAimSelection(controller.userData?.vrwpHandAimLatch);
});
bindOverlayActivity(controller, overlayVisibility);
controller.add(controllerPointerOverlay);
scene.add(controller);
const grip = renderer.xr.getControllerGrip?.(index);
if (grip) {
grip.addEventListener('connected', (event: any) => {
rememberPointerInputMode(inputSource, event, 'controller');
});
bindOverlayActivity(grip, overlayVisibility);
grip.add(createControllerOverlay(index, overlayVisibility));
scene.add(grip);
}
const hand = renderer.xr.getHand?.(index);
if (hand) {
const handAimLatch = createPalmAimLatch();
inputSource.hand = hand;
inputSource.handAimLatch = handAimLatch;
controller.userData = {
...controller.userData,
vrwpHand: hand,
vrwpHandAimLatch: handAimLatch
};
hand.userData = {
...hand.userData,
vrwpAimLatch: handAimLatch
};
bindOverlayActivity(hand, overlayVisibility);
rememberHandedness(hand, { data: hand.inputState });
createHandOverlay(hand, index, overlayVisibility);
hand.addEventListener?.('connected', (event: any) => {
rememberPointerInputMode(inputSource, event, 'hand');
rememberHandedness(hand, event);
createHandOverlay(hand, index, overlayVisibility);
overlayVisibility.show();
});
scene.add(hand);
const handPointerOverlay = createWorldPointerOverlay(index, overlayVisibility);
inputSource.handPointerOverlay = handPointerOverlay;
scene.add(handPointerOverlay);
handPointerOverlays.push({
fallbackPointerOverlay: controllerPointerOverlay,
hand,
handAimLatch,
inputSource,
pointerOverlay: handPointerOverlay
});
}
}
overlayVisibility.hideImmediately();
return {
beginSeekDrag: (controller: any, panel: VrControlPanel | undefined, onSeek: (progress: number) => void) => {
const inputSource = getInputSourceByController(inputSources, controller);
if (!inputSource || !panel?.seekBarHitAreaMesh) {
activeSeekDrag = null;
return;
}
activeSeekDrag = { inputSource, onSeek, panel };
},
hideOverlays: () => overlayVisibility.hideImmediately(),
raycaster,
showOverlays: (timestamp?: number) => overlayVisibility.show(timestamp),
update: (timestamp: number, hoverTargets: any[] = []) => {
updateHandPointerOverlays(handPointerOverlays, timestamp);
updateActiveSeekDrag(activeSeekDrag, dragRaycaster, timestamp);
const isHovering = updateInputPointerIntersections(inputSources, hoverTargets, hoverRaycaster, timestamp);
if (isHovering) {
overlayVisibility.show(timestamp);
}
overlayVisibility.update(timestamp);
return isHovering;
}
};
}
export function handleVrControllerSelect(event: any, options: VrControllerSelectionOptions): void {
const controller = event.target;
if (!options.raycaster) return;
@@ -325,230 +120,6 @@ function applySelectionRay(controller: any, raycaster: any): void {
raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
}
function getInputSourceByController(inputSources: VrInputSource[], controller: any): VrInputSource | undefined {
return inputSources.find((inputSource) => inputSource.controller === controller);
}
function updateInputPointerIntersections(
inputSources: VrInputSource[],
hoverTargets: any[],
hoverRaycaster: any,
timestamp: number
): boolean {
let isHoveringAnyTarget = false;
inputSources.forEach((inputSource) => {
resetInputPointerLengths(inputSource);
const aimRay = getInputSourceAimRay(inputSource, timestamp);
const pointerOverlay = getActivePointerOverlay(inputSource);
if (!aimRay || !pointerOverlay || hoverTargets.length === 0) {
return;
}
hoverRaycaster.ray.origin.copy(aimRay.origin);
hoverRaycaster.ray.direction.copy(aimRay.direction);
const intersections = hoverRaycaster.intersectObjects(hoverTargets, true);
if (intersections.length === 0) {
return;
}
isHoveringAnyTarget = true;
setPointerOverlayLength(pointerOverlay, getPointerIntersectionLength(intersections[0].distance));
});
return isHoveringAnyTarget;
}
function updateActiveSeekDrag(activeSeekDrag: ActiveSeekDrag | null, dragRaycaster: any, timestamp: number): void {
if (!activeSeekDrag?.panel.seekBarHitAreaMesh) {
return;
}
const aimRay = getInputSourceAimRay(activeSeekDrag.inputSource, timestamp, { preferLiveHandAim: true });
if (!aimRay) {
return;
}
dragRaycaster.ray.origin.copy(aimRay.origin);
dragRaycaster.ray.direction.copy(aimRay.direction);
const intersections = dragRaycaster.intersectObject(activeSeekDrag.panel.seekBarHitAreaMesh, true);
if (intersections.length === 0) {
return;
}
activeSeekDrag.onSeek(getSeekProgressFromIntersection(activeSeekDrag.panel, intersections[0].point));
}
function getInputSourceAimRay(
inputSource: VrInputSource,
timestamp: number,
{ preferLiveHandAim = false }: { preferLiveHandAim?: boolean } = {}
): AimRay | null {
if (shouldUseHandPointer(inputSource) && inputSource.hand && inputSource.handAimLatch) {
if (preferLiveHandAim) {
const handRay = getHandAimRay(inputSource.hand);
if (handRay) {
return handRay;
}
}
const latchedRay = getPalmAimSelectionRay(inputSource.handAimLatch, timestamp);
if (latchedRay) {
return toAimRay(latchedRay);
}
const handRay = getHandAimRay(inputSource.hand);
if (handRay) {
return handRay;
}
}
return getControllerAimRay(inputSource.controller);
}
function getControllerAimRay(controller: any): AimRay | null {
if (!controller) {
return null;
}
controller.updateMatrixWorld();
tempMatrix.identity().extractRotation(controller.matrixWorld);
const origin = new THREE.Vector3().setFromMatrixPosition(controller.matrixWorld);
const direction = new THREE.Vector3(0, 0, -1).applyMatrix4(tempMatrix);
return { direction, origin };
}
function resetInputPointerLengths(inputSource: VrInputSource): void {
setPointerOverlayLength(inputSource.controllerPointerOverlay, POINTER_LENGTH);
if (inputSource.handPointerOverlay) {
setPointerOverlayLength(inputSource.handPointerOverlay, POINTER_LENGTH);
}
}
function getActivePointerOverlay(inputSource: VrInputSource): any {
if (inputSource.handPointerOverlay && inputSource.handPointerOverlay.userData?.vrwpOverlayAvailable !== false) {
return inputSource.handPointerOverlay;
}
if (inputSource.controllerPointerOverlay && inputSource.controllerPointerOverlay.userData?.vrwpOverlayAvailable !== false) {
return inputSource.controllerPointerOverlay;
}
return null;
}
function getPointerIntersectionLength(distance: number): number {
return Math.max(
POINTER_MIN_LENGTH,
Math.min(POINTER_LENGTH, distance - POINTER_HIT_SURFACE_OFFSET)
);
}
function setPointerOverlayLength(pointerOverlay: any, length: number): void {
if (!pointerOverlay || pointerOverlay.userData?.vrwpPointerLength === length) {
return;
}
const pointerLine = pointerOverlay.userData?.vrwpPointerLine;
const pointerTip = pointerOverlay.userData?.vrwpPointerTip;
const positionAttribute = pointerLine?.geometry?.getAttribute?.('position') ||
pointerLine?.geometry?.attributes?.position;
if (positionAttribute?.setXYZ) {
positionAttribute.setXYZ(1, 0, 0, -length);
positionAttribute.needsUpdate = true;
pointerLine.geometry.computeBoundingSphere?.();
}
if (pointerTip) {
pointerTip.position.z = -length;
}
pointerOverlay.userData = {
...pointerOverlay.userData,
vrwpPointerLength: length
};
}
function updateHandPointerOverlays(handPointerOverlays: HandPointerOverlay[], timestamp: number): void {
handPointerOverlays.forEach(({ fallbackPointerOverlay, hand, handAimLatch, inputSource, pointerOverlay }) => {
if (!shouldUseHandPointer(inputSource)) {
pointerOverlay.userData = {
...pointerOverlay.userData,
vrwpOverlayAvailable: false
};
fallbackPointerOverlay.userData = {
...fallbackPointerOverlay.userData,
vrwpOverlayAvailable: true
};
return;
}
const currentHandRay = getHandAimRay(hand);
if (currentHandRay) {
recordStablePalmAimRay(handAimLatch, toPalmAimRay(currentHandRay), timestamp);
}
const selectionPalmRay = getPalmAimSelectionRay(handAimLatch, timestamp);
const displayHandRay = selectionPalmRay ? toAimRay(selectionPalmRay) : currentHandRay;
const hasHandRay = Boolean(displayHandRay);
pointerOverlay.userData = {
...pointerOverlay.userData,
vrwpOverlayAvailable: hasHandRay
};
fallbackPointerOverlay.userData = {
...fallbackPointerOverlay.userData,
vrwpOverlayAvailable: !hasHandRay
};
if (!displayHandRay) {
return;
}
pointerOverlay.position.copy(displayHandRay.origin);
pointerOverlay.quaternion.setFromUnitVectors(DEFAULT_RAY_DIRECTION, displayHandRay.direction);
});
}
function rememberPointerInputMode(
inputSource: VrInputSource,
event: any,
fallbackMode: PointerInputMode
): void {
const eventInputSource = event?.data?.inputSource || event?.inputSource || event?.data;
const nextMode = getPointerInputMode(eventInputSource) || fallbackMode;
inputSource.pointerInputMode = nextMode;
inputSource.controller.userData = {
...inputSource.controller.userData,
vrwpInputSource: inputSource
};
}
function getPointerInputMode(eventInputSource: any): PointerInputMode | null {
if (!eventInputSource) {
return null;
}
if (eventInputSource.hand) {
return 'hand';
}
if (Array.isArray(eventInputSource.profiles) &&
eventInputSource.profiles.some((profile: string) => profile.toLowerCase().includes('hand'))) {
return 'hand';
}
if (eventInputSource.gamepad || eventInputSource.targetRayMode === 'tracked-pointer') {
return 'controller';
}
return null;
}
function shouldUseHandPointer(inputSource: VrInputSource | undefined): boolean {
return inputSource?.pointerInputMode === 'hand';
}
function getSelectionHandAimRay(controller: any): AimRay | null {
const latch = controller.userData?.vrwpHandAimLatch ||
controller.userData?.vrwpHand?.userData?.vrwpAimLatch;
@@ -583,13 +154,6 @@ function getHandAimRay(hand: any): AimRay | null {
return { direction, origin };
}
function toPalmAimRay(ray: AimRay): PalmAimRay {
return {
direction: fromThreeVector(ray.direction),
origin: fromThreeVector(ray.origin)
};
}
function toAimRay(ray: PalmAimRay): AimRay {
return {
direction: toThreeVector(ray.direction),
@@ -597,21 +161,6 @@ function toAimRay(ray: PalmAimRay): AimRay {
};
}
function rememberHandedness(hand: any, event: any): void {
const handedness = event?.data?.handedness ||
event?.data?.inputSource?.handedness ||
hand?.inputState?.handedness;
if (handedness !== 'left' && handedness !== 'right') {
return;
}
hand.userData = {
...hand.userData,
vrwpHandedness: handedness
};
}
function getHandedness(hand: any): string | undefined {
return hand?.userData?.vrwpHandedness ||
hand?.inputState?.handedness ||
@@ -630,257 +179,3 @@ function getJointWorldPosition(joint: any): VectorLike | null {
function toThreeVector(vector: VectorLike): any {
return new THREE.Vector3(vector.x, vector.y, vector.z);
}
function fromThreeVector(vector: any): VectorLike {
return {
x: vector.x,
y: vector.y,
z: vector.z
};
}
function getEventTimestamp(event: any): number {
return Number.isFinite(event?.timeStamp) ? event.timeStamp : performance.now();
}
class VrOverlayVisibility {
private readonly fadeDurationMs: number;
private readonly hideDelayMs: number;
private readonly objects: any[] = [];
private opacity = 0;
private targetOpacity = 0;
private visibleUntil = 0;
constructor({
fadeDurationMs = INPUT_OVERLAY_FADE_DURATION_MS,
hideDelayMs = INPUT_OVERLAY_HIDE_DELAY_MS
}: VrOverlayVisibilityOptions = {}) {
this.fadeDurationMs = fadeDurationMs;
this.hideDelayMs = hideDelayMs;
}
register(object: any): void {
this.objects.push(object);
this.setObjectVisible(object, this.targetOpacity > 0 || this.opacity > 0.001);
this.setObjectOpacity(object, this.opacity);
}
show(timestamp = performance.now()): void {
this.visibleUntil = timestamp + this.hideDelayMs;
this.targetOpacity = 1;
this.objects.forEach((object) => this.setObjectVisible(object, true));
}
hideImmediately(): void {
this.visibleUntil = 0;
this.opacity = 0;
this.targetOpacity = 0;
this.objects.forEach((object) => {
this.setObjectOpacity(object, 0);
this.setObjectVisible(object, false);
});
}
update(timestamp: number): void {
if (this.targetOpacity > 0 && timestamp >= this.visibleUntil) {
this.targetOpacity = 0;
}
const shouldBeVisible = this.targetOpacity > 0 || this.opacity > 0.001;
this.objects.forEach((object) => this.setObjectVisible(object, shouldBeVisible));
if (this.opacity === this.targetOpacity) {
return;
}
const fadeStep = this.fadeDurationMs <= 0
? 1
: Math.min(1, 16.67 / this.fadeDurationMs);
const direction = this.opacity < this.targetOpacity ? 1 : -1;
this.opacity = Math.max(0, Math.min(1, this.opacity + fadeStep * direction));
if ((direction > 0 && this.opacity >= this.targetOpacity) || (direction < 0 && this.opacity <= this.targetOpacity)) {
this.opacity = this.targetOpacity;
}
this.objects.forEach((object) => {
this.setObjectOpacity(object, this.opacity);
this.setObjectVisible(object, this.opacity > 0.001);
});
}
private setObjectVisible(object: any, isVisible: boolean): void {
const objectVisible = isVisible && object.userData?.vrwpOverlayAvailable !== false;
object.visible = objectVisible;
object.traverse?.((child: any) => {
child.visible = objectVisible;
});
}
private setObjectOpacity(object: any, opacity: number): void {
object.traverse?.((child: any) => {
const materials = Array.isArray(child.material) ? child.material : [child.material];
materials.filter(Boolean).forEach((material: any) => {
material.opacity = opacity;
material.transparent = true;
material.depthTest = false;
material.depthWrite = false;
material.needsUpdate = true;
});
});
}
}
function bindOverlayActivity(target: any, overlayVisibility: VrOverlayVisibility): void {
[
'connected',
'disconnected',
'select',
'selectend',
'squeezestart',
'squeeze',
'squeezeend',
'pinchstart',
'pinchend'
].forEach((eventName) => {
target.addEventListener?.(eventName, () => overlayVisibility.show());
});
}
function createPointerOverlay(index: number, overlayVisibility: VrOverlayVisibility): any {
const group = new THREE.Group();
group.name = `vrPointerOverlay${index}`;
const pointerMaterial = createOverlayLineMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.75);
const pointerGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, -POINTER_LENGTH)
]);
const pointerLine = new THREE.Line(pointerGeometry, pointerMaterial);
pointerLine.name = `vrPointerRay${index}`;
pointerLine.renderOrder = INPUT_OVERLAY_RENDER_ORDER;
group.add(pointerLine);
const tipMaterial = createOverlayMeshMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.9);
const tipMesh = new THREE.Mesh(new THREE.SphereGeometry(0.018, 12, 8), tipMaterial);
tipMesh.name = `vrPointerTip${index}`;
tipMesh.position.z = -POINTER_LENGTH;
tipMesh.renderOrder = INPUT_OVERLAY_RENDER_ORDER + 1;
group.add(tipMesh);
group.userData = {
...group.userData,
vrwpPointerLength: POINTER_LENGTH,
vrwpPointerLine: pointerLine,
vrwpPointerTip: tipMesh
};
overlayVisibility.register(group);
return group;
}
function createWorldPointerOverlay(index: number, overlayVisibility: VrOverlayVisibility): any {
const group = createPointerOverlay(index, overlayVisibility);
group.name = `vrHandPointerOverlay${index}`;
group.userData = {
...group.userData,
vrwpOverlayAvailable: false
};
return group;
}
function createControllerOverlay(index: number, overlayVisibility: VrOverlayVisibility): any {
const group = new THREE.Group();
group.name = `vrControllerOverlay${index}`;
const material = createOverlayLineMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.8);
const outlineGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-0.045, -0.025, -0.08),
new THREE.Vector3(0.045, -0.025, -0.08),
new THREE.Vector3(0.055, 0.025, -0.02),
new THREE.Vector3(0.025, 0.035, 0.05),
new THREE.Vector3(-0.025, 0.035, 0.05),
new THREE.Vector3(-0.055, 0.025, -0.02),
new THREE.Vector3(-0.045, -0.025, -0.08)
]);
const outline = new THREE.Line(outlineGeometry, material);
outline.name = `vrControllerOutline${index}`;
outline.renderOrder = INPUT_OVERLAY_RENDER_ORDER;
group.add(outline);
const originMaterial = createOverlayMeshMaterial(0xffffff, 0.75);
const origin = new THREE.Mesh(new THREE.SphereGeometry(0.012, 10, 6), originMaterial);
origin.name = `vrControllerOrigin${index}`;
origin.renderOrder = INPUT_OVERLAY_RENDER_ORDER + 1;
group.add(origin);
overlayVisibility.register(group);
return group;
}
function createHandOverlay(hand: any, index: number, overlayVisibility: VrOverlayVisibility): void {
const joints = getHandJoints(hand);
if (joints.length === 0) {
return;
}
const material = createOverlayMeshMaterial(index === 0 ? 0x8fd8ff : 0xffc16b, 0.85);
joints.forEach(({ joint, name }) => {
if (!joint || joint.userData?.vrwpHandOverlayMarker) {
return;
}
const isTip = name.endsWith('tip');
const isWrist = name === 'wrist';
const radius = isWrist ? 0.014 : isTip ? 0.011 : 0.008;
const marker = new THREE.Mesh(new THREE.SphereGeometry(radius, 10, 6), material);
marker.name = `vrHandJointOverlay${index}-${name}`;
marker.renderOrder = INPUT_OVERLAY_RENDER_ORDER + 2;
marker.frustumCulled = false;
joint.add(marker);
joint.userData = {
...joint.userData,
vrwpHandOverlayMarker: marker
};
overlayVisibility.register(marker);
});
}
function getHandJoints(hand: any): Array<{ joint: any; name: string }> {
const joints = hand?.joints;
if (!joints) {
return [];
}
const namedJoints = HAND_JOINT_NAMES
.map((name) => ({ joint: joints[name], name }))
.filter(({ joint }) => Boolean(joint));
if (namedJoints.length > 0) {
return namedJoints;
}
return Object.entries(joints)
.map(([name, joint]) => ({ joint, name }))
.filter(({ joint }) => Boolean(joint));
}
function createOverlayLineMaterial(color: number, opacity: number): any {
return new THREE.LineBasicMaterial({
color,
depthTest: false,
depthWrite: false,
opacity,
transparent: true
});
}
function createOverlayMeshMaterial(color: number, opacity: number): any {
return new THREE.MeshBasicMaterial({
color,
depthTest: false,
depthWrite: false,
opacity,
transparent: true
});
}