forked from EXT/VR180-Web-Player
This commit is contained in:
@@ -54,10 +54,11 @@ When the page loads, the media is embedded normally with an entry button over it
|
|||||||
- In WebXR, `plane` maps the left and right halves onto the matching eyes of a floating 16:9 plane.
|
- In WebXR, `plane` maps the left and right halves onto the matching eyes of a floating 16:9 plane.
|
||||||
- Outside WebXR, both modes render only the left half of the SBS media so viewers do not see the raw double image.
|
- Outside WebXR, both modes render only the left half of the SBS media so viewers do not see the raw double image.
|
||||||
- Static images show only applicable controls; playback, seek, and mute controls are video-only.
|
- Static images show only applicable controls; playback, seek, and mute controls are video-only.
|
||||||
|
- Controller pointers and lightweight hand/controller overlays appear after controller interaction, then fade away after a short idle period so they do not distract from viewing.
|
||||||
- Control icons are embedded from [Lucide](https://lucide.dev/) SVG definitions, so no PNG icon assets are required.
|
- Control icons are embedded from [Lucide](https://lucide.dev/) SVG definitions, so no PNG icon assets are required.
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
Run `npm run build`, then open this repository's `index.html` through a local web server. The index page links to focused test pages for flat 3D image, VR180 3D image, flat 3D video, and VR180 3D video.
|
Run `npm run build`, then open `test-pages/index.html` through a local web server. The test hub links to focused pages for flat 3D image, VR180 3D image, flat 3D video, and VR180 3D video. The root `index.html` redirects there for convenience.
|
||||||
|
|
||||||
For local experimentation, run:
|
For local experimentation, run:
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { createMediaTexture as createMediaTextureCore } from './rendering/three-
|
|||||||
import { FallbackCameraControls } from './modes/fallback-camera-controls.js';
|
import { FallbackCameraControls } from './modes/fallback-camera-controls.js';
|
||||||
import { MediaController } from './media/media-controller.js';
|
import { MediaController } from './media/media-controller.js';
|
||||||
import {
|
import {
|
||||||
createVrController,
|
createVrInputRig,
|
||||||
handleVrControllerSelect
|
handleVrControllerSelect
|
||||||
} from './xr/vr-controller-interactions.js';
|
} from './xr/vr-controller-interactions.js';
|
||||||
import { bindVideoEvents } from './media/video-events.js';
|
import { bindVideoEvents } from './media/video-events.js';
|
||||||
@@ -42,6 +42,7 @@ let scene, camera, renderer, video, sphereMaterial;
|
|||||||
let vr180Mesh, planeMesh, activeContentMesh;
|
let vr180Mesh, planeMesh, activeContentMesh;
|
||||||
let xrSession = null;
|
let xrSession = null;
|
||||||
let raycaster, uiElements = [];
|
let raycaster, uiElements = [];
|
||||||
|
let xrInputRig;
|
||||||
let mediaAdapter: SupportedMediaAdapter | undefined;
|
let mediaAdapter: SupportedMediaAdapter | undefined;
|
||||||
let playBtn;
|
let playBtn;
|
||||||
let frameCounter = 0;
|
let frameCounter = 0;
|
||||||
@@ -202,7 +203,8 @@ function init() {
|
|||||||
vrPanelVisibility.setPanel(vrPanel);
|
vrPanelVisibility.setPanel(vrPanel);
|
||||||
uiElements.push(...vrPanel.interactables);
|
uiElements.push(...vrPanel.interactables);
|
||||||
|
|
||||||
raycaster = createVrController(scene, renderer, onSelectStartVR).raycaster;
|
xrInputRig = createVrInputRig(scene, renderer, onSelectStartVR);
|
||||||
|
raycaster = xrInputRig.raycaster;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("INIT_ERROR (Phase 2 - VR Controls Setup):", e);
|
console.error("INIT_ERROR (Phase 2 - VR Controls Setup):", e);
|
||||||
}
|
}
|
||||||
@@ -422,6 +424,7 @@ async function actualSessionToggle() {
|
|||||||
try {
|
try {
|
||||||
const session = await navigator.xr.requestSession('immersive-vr', {
|
const session = await navigator.xr.requestSession('immersive-vr', {
|
||||||
requiredFeatures: ['local-floor'],
|
requiredFeatures: ['local-floor'],
|
||||||
|
optionalFeatures: ['hand-tracking'],
|
||||||
});
|
});
|
||||||
if (!session) { throw new Error("requestSession returned no session."); }
|
if (!session) { throw new Error("requestSession returned no session."); }
|
||||||
|
|
||||||
@@ -462,6 +465,7 @@ async function actualSessionToggle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await renderer.xr.setSession(xrSession);
|
await renderer.xr.setSession(xrSession);
|
||||||
|
xrInputRig?.showOverlays();
|
||||||
isXrLoopActive = true;
|
isXrLoopActive = true;
|
||||||
renderer.setAnimationLoop(renderXR);
|
renderer.setAnimationLoop(renderXR);
|
||||||
frameCounter = 0;
|
frameCounter = 0;
|
||||||
@@ -521,6 +525,7 @@ function onVRSessionEnd(event) {
|
|||||||
if (vrControlPanel) {
|
if (vrControlPanel) {
|
||||||
vrPanelVisibility.hideImmediately();
|
vrPanelVisibility.hideImmediately();
|
||||||
}
|
}
|
||||||
|
xrInputRig?.hideOverlays();
|
||||||
|
|
||||||
if (endedSession && typeof endedSession.removeEventListener === 'function') {
|
if (endedSession && typeof endedSession.removeEventListener === 'function') {
|
||||||
endedSession.removeEventListener('end', onVRSessionEnd);
|
endedSession.removeEventListener('end', onVRSessionEnd);
|
||||||
@@ -558,6 +563,7 @@ function renderXR(timestamp, frame) {
|
|||||||
if (vrPanelVisibility.isFading) {
|
if (vrPanelVisibility.isFading) {
|
||||||
animatePanelFade(timestamp);
|
animatePanelFade(timestamp);
|
||||||
}
|
}
|
||||||
|
xrInputRig?.update(timestamp);
|
||||||
|
|
||||||
if (!frame) {
|
if (!frame) {
|
||||||
console.warn("renderXR called without an XRFrame. Skipping render.");
|
console.warn("renderXR called without an XRFrame. Skipping render.");
|
||||||
|
|||||||
@@ -19,38 +19,130 @@ type VrControllerSelectionOptions = {
|
|||||||
vrPanel: VrControlPanel | undefined;
|
vrPanel: VrControlPanel | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type VrInputRig = {
|
||||||
|
hideOverlays: () => void;
|
||||||
|
raycaster: any;
|
||||||
|
showOverlays: (timestamp?: number) => void;
|
||||||
|
update: (timestamp: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AimRay = {
|
||||||
|
direction: any;
|
||||||
|
origin: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HandPointerOverlay = {
|
||||||
|
fallbackPointerOverlay: any;
|
||||||
|
hand: any;
|
||||||
|
pointerOverlay: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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();
|
const tempMatrix = new THREE.Matrix4();
|
||||||
|
|
||||||
export function createVrController(scene: any, renderer: any, onSelectStart: (event: any) => void): {
|
export function createVrInputRig(scene: any, renderer: any, onSelectStart: (event: any) => void): VrInputRig {
|
||||||
controller: any;
|
const overlayVisibility = new VrOverlayVisibility();
|
||||||
raycaster: any;
|
const handPointerOverlays: HandPointerOverlay[] = [];
|
||||||
} {
|
|
||||||
const controller = renderer.xr.getController(0);
|
|
||||||
controller.addEventListener('selectstart', onSelectStart);
|
|
||||||
|
|
||||||
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.5 });
|
|
||||||
const lineGeometry = new THREE.BufferGeometry().setFromPoints([
|
|
||||||
new THREE.Vector3(0, 0, 0),
|
|
||||||
new THREE.Vector3(0, 0, -5)
|
|
||||||
]);
|
|
||||||
controller.add(new THREE.Line(lineGeometry, lineMaterial));
|
|
||||||
scene.add(controller);
|
|
||||||
|
|
||||||
const raycaster = new THREE.Raycaster();
|
const raycaster = new THREE.Raycaster();
|
||||||
raycaster.near = 0.1;
|
raycaster.near = 0.1;
|
||||||
raycaster.far = 5;
|
raycaster.far = POINTER_LENGTH;
|
||||||
|
|
||||||
return { controller, raycaster };
|
for (let index = 0; index < 2; index += 1) {
|
||||||
|
const controller = renderer.xr.getController(index);
|
||||||
|
controller.addEventListener('selectstart', (event: any) => {
|
||||||
|
overlayVisibility.show();
|
||||||
|
onSelectStart(event);
|
||||||
|
});
|
||||||
|
bindOverlayActivity(controller, overlayVisibility);
|
||||||
|
const controllerPointerOverlay = createPointerOverlay(index, overlayVisibility);
|
||||||
|
controller.add(controllerPointerOverlay);
|
||||||
|
scene.add(controller);
|
||||||
|
|
||||||
|
const grip = renderer.xr.getControllerGrip?.(index);
|
||||||
|
if (grip) {
|
||||||
|
bindOverlayActivity(grip, overlayVisibility);
|
||||||
|
grip.add(createControllerOverlay(index, overlayVisibility));
|
||||||
|
scene.add(grip);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hand = renderer.xr.getHand?.(index);
|
||||||
|
if (hand) {
|
||||||
|
controller.userData = {
|
||||||
|
...controller.userData,
|
||||||
|
vrwpHand: hand
|
||||||
|
};
|
||||||
|
bindOverlayActivity(hand, overlayVisibility);
|
||||||
|
createHandOverlay(hand, index, overlayVisibility);
|
||||||
|
hand.addEventListener?.('connected', () => {
|
||||||
|
createHandOverlay(hand, index, overlayVisibility);
|
||||||
|
overlayVisibility.show();
|
||||||
|
});
|
||||||
|
scene.add(hand);
|
||||||
|
|
||||||
|
const handPointerOverlay = createWorldPointerOverlay(index, overlayVisibility);
|
||||||
|
scene.add(handPointerOverlay);
|
||||||
|
handPointerOverlays.push({
|
||||||
|
fallbackPointerOverlay: controllerPointerOverlay,
|
||||||
|
hand,
|
||||||
|
pointerOverlay: handPointerOverlay
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayVisibility.hideImmediately();
|
||||||
|
|
||||||
|
return {
|
||||||
|
hideOverlays: () => overlayVisibility.hideImmediately(),
|
||||||
|
raycaster,
|
||||||
|
showOverlays: (timestamp?: number) => overlayVisibility.show(timestamp),
|
||||||
|
update: (timestamp: number) => {
|
||||||
|
updateHandPointerOverlays(handPointerOverlays);
|
||||||
|
overlayVisibility.update(timestamp);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleVrControllerSelect(event: any, options: VrControllerSelectionOptions): void {
|
export function handleVrControllerSelect(event: any, options: VrControllerSelectionOptions): void {
|
||||||
const controller = event.target;
|
const controller = event.target;
|
||||||
if (!options.raycaster) return;
|
if (!options.raycaster) return;
|
||||||
|
|
||||||
controller.updateMatrixWorld();
|
applySelectionRay(controller, options.raycaster);
|
||||||
tempMatrix.identity().extractRotation(controller.matrixWorld);
|
|
||||||
options.raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
|
|
||||||
options.raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
|
|
||||||
|
|
||||||
const directIntersects = options.raycaster.intersectObjects(options.uiElements, true);
|
const directIntersects = options.raycaster.intersectObjects(options.uiElements, true);
|
||||||
if (directIntersects.length === 0) {
|
if (directIntersects.length === 0) {
|
||||||
@@ -107,3 +199,296 @@ function togglePanel(options: VrControllerSelectionOptions): void {
|
|||||||
options.showPanel();
|
options.showPanel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySelectionRay(controller: any, raycaster: any): void {
|
||||||
|
const handRay = getHandAimRay(controller.userData?.vrwpHand);
|
||||||
|
if (handRay) {
|
||||||
|
raycaster.ray.origin.copy(handRay.origin);
|
||||||
|
raycaster.ray.direction.copy(handRay.direction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.updateMatrixWorld();
|
||||||
|
tempMatrix.identity().extractRotation(controller.matrixWorld);
|
||||||
|
raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
|
||||||
|
raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHandPointerOverlays(handPointerOverlays: HandPointerOverlay[]): void {
|
||||||
|
handPointerOverlays.forEach(({ fallbackPointerOverlay, hand, pointerOverlay }) => {
|
||||||
|
const handRay = getHandAimRay(hand);
|
||||||
|
const hasHandRay = Boolean(handRay);
|
||||||
|
pointerOverlay.userData = {
|
||||||
|
...pointerOverlay.userData,
|
||||||
|
vrwpOverlayAvailable: hasHandRay
|
||||||
|
};
|
||||||
|
fallbackPointerOverlay.userData = {
|
||||||
|
...fallbackPointerOverlay.userData,
|
||||||
|
vrwpOverlayAvailable: !hasHandRay
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!handRay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pointerOverlay.position.copy(handRay.origin);
|
||||||
|
pointerOverlay.quaternion.setFromUnitVectors(DEFAULT_RAY_DIRECTION, handRay.direction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHandAimRay(hand: any): AimRay | null {
|
||||||
|
const joints = hand?.joints;
|
||||||
|
const tipJoint = joints?.['index-finger-tip'];
|
||||||
|
const baseJoint = joints?.['index-finger-phalanx-proximal'] ||
|
||||||
|
joints?.['index-finger-metacarpal'] ||
|
||||||
|
joints?.wrist;
|
||||||
|
|
||||||
|
if (!tipJoint || !baseJoint) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
tipJoint.updateMatrixWorld?.(true);
|
||||||
|
baseJoint.updateMatrixWorld?.(true);
|
||||||
|
|
||||||
|
const origin = tipJoint.getWorldPosition(new THREE.Vector3());
|
||||||
|
const base = baseJoint.getWorldPosition(new THREE.Vector3());
|
||||||
|
const direction = origin.clone().sub(base);
|
||||||
|
if (direction.lengthSq() < 0.000001) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
direction.normalize();
|
||||||
|
return { direction, origin };
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<p class="demo-note">Image tests use <code>media/169_3d_test.png</code>. Video tests expect <code>media/sbs-video.mp4</code>.</p>
|
<p class="demo-note">Image tests use files in <code>../media/</code>. Video tests expect <code>../media/sbs-video.mp4</code>.</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
<title>3D Image Test</title>
|
<title>3D Image Test</title>
|
||||||
<link rel="stylesheet" href="./demo.css">
|
<link rel="stylesheet" href="./demo.css">
|
||||||
<link rel="stylesheet" href="./vr180player/vr180-player.css" data-vr-web-player-stylesheet>
|
<link rel="stylesheet" href="../vr180player/vr180-player.css" data-vr-web-player-stylesheet>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="demo-page">
|
<main class="demo-page">
|
||||||
@@ -21,11 +21,11 @@
|
|||||||
<p class="demo-xr-status" data-demo-xr-status>Checking immersive WebXR support...</p>
|
<p class="demo-xr-status" data-demo-xr-status>Checking immersive WebXR support...</p>
|
||||||
|
|
||||||
<div class="demo-player-frame" data-vr-web-player data-projection="plane">
|
<div class="demo-player-frame" data-vr-web-player data-projection="plane">
|
||||||
<img src="media/169_3d_test.png" alt="Demo SBS image" title="3D Image Plane" crossorigin="anonymous">
|
<img src="../media/169_3d_test.png" alt="Demo SBS image" title="3D Image Plane" crossorigin="anonymous">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script type="module" src="./demo-xr-status.js"></script>
|
<script type="module" src="./demo-xr-status.js"></script>
|
||||||
<script type="module" src="./vr180player/vr180-player.js"></script>
|
<script type="module" src="../vr180player/vr180-player.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
<title>3D Video Test</title>
|
<title>3D Video Test</title>
|
||||||
<link rel="stylesheet" href="./demo.css">
|
<link rel="stylesheet" href="./demo.css">
|
||||||
<link rel="stylesheet" href="./vr180player/vr180-player.css" data-vr-web-player-stylesheet>
|
<link rel="stylesheet" href="../vr180player/vr180-player.css" data-vr-web-player-stylesheet>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="demo-page">
|
<main class="demo-page">
|
||||||
@@ -21,13 +21,13 @@
|
|||||||
<p class="demo-xr-status" data-demo-xr-status>Checking immersive WebXR support...</p>
|
<p class="demo-xr-status" data-demo-xr-status>Checking immersive WebXR support...</p>
|
||||||
|
|
||||||
<div class="demo-player-frame" data-vr-web-player data-projection="plane">
|
<div class="demo-player-frame" data-vr-web-player data-projection="plane">
|
||||||
<video poster="poster.jpg" title="3D Video Plane" crossorigin="anonymous" playsinline preload="metadata">
|
<video poster="../poster.jpg" title="3D Video Plane" crossorigin="anonymous" playsinline preload="metadata">
|
||||||
<source src="media/sbs-video.mp4" type="video/mp4">
|
<source src="../media/sbs-video.mp4" type="video/mp4">
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script type="module" src="./demo-xr-status.js"></script>
|
<script type="module" src="./demo-xr-status.js"></script>
|
||||||
<script type="module" src="./vr180player/vr180-player.js"></script>
|
<script type="module" src="../vr180player/vr180-player.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
<title>VR180 3D Image Test</title>
|
<title>VR180 3D Image Test</title>
|
||||||
<link rel="stylesheet" href="./demo.css">
|
<link rel="stylesheet" href="./demo.css">
|
||||||
<link rel="stylesheet" href="./vr180player/vr180-player.css" data-vr-web-player-stylesheet>
|
<link rel="stylesheet" href="../vr180player/vr180-player.css" data-vr-web-player-stylesheet>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="demo-page">
|
<main class="demo-page">
|
||||||
@@ -21,11 +21,11 @@
|
|||||||
<p class="demo-xr-status" data-demo-xr-status>Checking immersive WebXR support...</p>
|
<p class="demo-xr-status" data-demo-xr-status>Checking immersive WebXR support...</p>
|
||||||
|
|
||||||
<div class="demo-player-frame" data-vr-web-player data-projection="vr180">
|
<div class="demo-player-frame" data-vr-web-player data-projection="vr180">
|
||||||
<img src="media/VR180_SBS_Test.png" alt="Demo VR180 SBS image" title="VR180 3D Image" crossorigin="anonymous">
|
<img src="../media/VR180_SBS_Test.png" alt="Demo VR180 SBS image" title="VR180 3D Image" crossorigin="anonymous">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script type="module" src="./demo-xr-status.js"></script>
|
<script type="module" src="./demo-xr-status.js"></script>
|
||||||
<script type="module" src="./vr180player/vr180-player.js"></script>
|
<script type="module" src="../vr180player/vr180-player.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
<title>VR180 3D Video Test</title>
|
<title>VR180 3D Video Test</title>
|
||||||
<link rel="stylesheet" href="./demo.css">
|
<link rel="stylesheet" href="./demo.css">
|
||||||
<link rel="stylesheet" href="./vr180player/vr180-player.css" data-vr-web-player-stylesheet>
|
<link rel="stylesheet" href="../vr180player/vr180-player.css" data-vr-web-player-stylesheet>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="demo-page">
|
<main class="demo-page">
|
||||||
@@ -21,13 +21,13 @@
|
|||||||
<p class="demo-xr-status" data-demo-xr-status>Checking immersive WebXR support...</p>
|
<p class="demo-xr-status" data-demo-xr-status>Checking immersive WebXR support...</p>
|
||||||
|
|
||||||
<div class="demo-player-frame" data-vr-web-player data-projection="vr180">
|
<div class="demo-player-frame" data-vr-web-player data-projection="vr180">
|
||||||
<video poster="poster.jpg" title="VR180 3D Video" crossorigin="anonymous" playsinline preload="metadata">
|
<video poster="../poster.jpg" title="VR180 3D Video" crossorigin="anonymous" playsinline preload="metadata">
|
||||||
<source src="media/sbs-video.mp4" type="video/mp4">
|
<source src="../media/sbs-video.mp4" type="video/mp4">
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script type="module" src="./demo-xr-status.js"></script>
|
<script type="module" src="./demo-xr-status.js"></script>
|
||||||
<script type="module" src="./vr180player/vr180-player.js"></script>
|
<script type="module" src="../vr180player/vr180-player.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user