forked from EXT/VR180-Web-Player
More reffactors
This commit is contained in:
@@ -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,
|
||||
|
||||
52
src/vr180player/xr/input-mode.ts
Normal file
52
src/vr180player/xr/input-mode.ts
Normal 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';
|
||||
}
|
||||
445
src/vr180player/xr/input-rig.ts
Normal file
445
src/vr180player/xr/input-rig.ts
Normal 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();
|
||||
}
|
||||
328
src/vr180player/xr/pointer-overlays.ts
Normal file
328
src/vr180player/xr/pointer-overlays.ts
Normal 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));
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user