未验证 提交 29dd2170 编写于 作者: T Tianrui "Felix" Zhang 提交者: GitHub

Fixing Oculus hands rendering broken by recent WebXR Hands API change (#21712)

* Adding advanced hand examples

* Update samples with ECSY implementation

* Fix hands examples broken by the recent oculus WebXR Hands API change

* Replace fbx hand models with glb

* Generate screenshots for new examples

* Clean up in Text2D.js

* Clean up in Text2D.js

* Update XRHandModelFactory and OculusHandModel to fetch hand model from webxr input profile cdn
上级 49b9c25b
......@@ -342,6 +342,9 @@
"webxr_vr_handinput",
"webxr_vr_handinput_cubes",
"webxr_vr_handinput_profiles",
"webxr_vr_handinput_pointerclick",
"webxr_vr_handinput_pointerdrag",
"webxr_vr_handinput_pressbutton",
"webxr_vr_haptics",
"webxr_vr_lorenzattractor",
"webxr_vr_panorama",
......
import { Object3D, Sphere, Box3 } from "../../../build/three.module.js";
import { fetchProfile } from '../libs/motion-controllers.module.js';
import { XRHandMeshModel } from "./XRHandMeshModel.js";
const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles';
const DEFAULT_PROFILE = 'generic-hand';
class OculusHandModel extends Object3D {
constructor(controller) {
super();
this.controller = controller;
this.motionController = null;
this.envMap = null;
this.mesh = null;
controller.addEventListener("connected", (event) => {
const xrInputSource = event.data;
if (xrInputSource.hand && !this.motionController) {
this.visible = true;
this.xrInputSource = xrInputSource;
fetchProfile(xrInputSource, DEFAULT_PROFILES_PATH, DEFAULT_PROFILE).then(({ profile, assetPath }) => {
this.motionController = new XRHandMeshModel(
this,
controller,
assetPath
);
}).catch((err) => {
console.warn(err);
});
}
});
controller.addEventListener("disconnected", () => {
this.clear();
this.motionController = null;
})
}
updateMatrixWorld(force) {
super.updateMatrixWorld(force);
if (this.motionController) {
this.motionController.updateMesh();
}
}
getPointerPosition() {
let indexFingerTip = this.controller.joints[POINTING_JOINT];
if (indexFingerTip) {
return indexFingerTip.position;
} else {
return null;
}
}
intersectBoxObject(boxObject) {
let pointerPosition = this.getPointerPosition();
if (pointerPosition) {
let indexSphere = new Sphere(pointerPosition, TOUCH_RADIUS);
let box = new Box3().setFromObject(boxObject);
return indexSphere.intersectsBox(box);
} else {
return false;
}
}
checkButton(button) {
if (this.intersectBoxObject(button)) {
button.onPress();
} else {
button.onClear();
}
if (button.isPressed()) {
button.whilePressed();
}
}
}
export { OculusHandModel };
import * as THREE from "../../../build/three.module.js";
const PINCH_MAX = 0.05;
const PINCH_THRESHOLD = 0.02;
const PINCH_MIN = 0.01;
const POINTER_ADVANCE_MAX = 0.02;
const POINTER_OPACITY_MAX = 1;
const POINTER_OPACITY_MIN = 0.4;
const POINTER_FRONT_RADIUS = 0.002;
const POINTER_REAR_RADIUS = 0.01;
const POINTER_REAR_RADIUS_MIN = 0.003;
const POINTER_LENGTH = 0.035;
const POINTER_SEGMENTS = 16;
const POINTER_RINGS = 12;
const POINTER_HEMISPHERE_ANGLE = 110;
const YAXIS = new THREE.Vector3(0, 1, 0);
const ZAXIS = new THREE.Vector3(0, 0, 1);
const CURSOR_RADIUS = 0.02;
const CURSOR_MAX_DISTANCE = 1.5;
class OculusHandPointerModel extends THREE.Object3D {
constructor(hand, controller) {
super();
this.hand = hand;
this.controller = controller;
this.motionController = null;
this.envMap = null;
this.mesh = null;
this.pointerGeometry = null;
this.pointerMesh = null;
this.pointerObject = null;
this.pinched = false;
this.attached = false;
this.cursorObject = null;
this.raycaster = null;
hand.addEventListener("connected", (event) => {
const xrInputSource = event.data;
if (xrInputSource.hand) {
this.visible = true;
this.xrInputSource = xrInputSource;
this.createPointer();
}
});
}
_drawVerticesRing(vertices, baseVector, ringIndex) {
const segmentVector = baseVector.clone();
for (var i = 0; i < POINTER_SEGMENTS; i++) {
segmentVector.applyAxisAngle(ZAXIS, (Math.PI * 2) / POINTER_SEGMENTS);
let vid = ringIndex * POINTER_SEGMENTS + i;
vertices[3 * vid] = segmentVector.x;
vertices[3 * vid + 1] = segmentVector.y;
vertices[3 * vid + 2] = segmentVector.z;
}
}
_updatePointerVertices(rearRadius) {
const vertices = this.pointerGeometry.attributes.position.array;
// first ring for front face
const frontFaceBase = new THREE.Vector3(
POINTER_FRONT_RADIUS,
0,
-1 * (POINTER_LENGTH - rearRadius)
);
this._drawVerticesRing(vertices, frontFaceBase, 0);
// rings for rear hemisphere
const rearBase = new THREE.Vector3(
Math.sin((Math.PI * POINTER_HEMISPHERE_ANGLE) / 180) * rearRadius,
Math.cos((Math.PI * POINTER_HEMISPHERE_ANGLE) / 180) * rearRadius,
0
);
for (var i = 0; i < POINTER_RINGS; i++) {
this._drawVerticesRing(vertices, rearBase, i + 1);
rearBase.applyAxisAngle(
YAXIS,
(Math.PI * POINTER_HEMISPHERE_ANGLE) / 180 / (POINTER_RINGS * -2)
);
}
// front and rear face center vertices
const frontCenterIndex = POINTER_SEGMENTS * (1 + POINTER_RINGS);
const rearCenterIndex = POINTER_SEGMENTS * (1 + POINTER_RINGS) + 1;
const frontCenter = new THREE.Vector3(
0,
0,
-1 * (POINTER_LENGTH - rearRadius)
);
vertices[frontCenterIndex * 3] = frontCenter.x;
vertices[frontCenterIndex * 3 + 1] = frontCenter.y;
vertices[frontCenterIndex * 3 + 2] = frontCenter.z;
const rearCenter = new THREE.Vector3(0, 0, rearRadius);
vertices[rearCenterIndex * 3] = rearCenter.x;
vertices[rearCenterIndex * 3 + 1] = rearCenter.y;
vertices[rearCenterIndex * 3 + 2] = rearCenter.z;
this.pointerGeometry.setAttribute(
"position",
new THREE.Float32BufferAttribute(vertices, 3)
);
// verticesNeedUpdate = true;
}
createPointer() {
var i, j;
const vertices = new Array(
((POINTER_RINGS + 1) * POINTER_SEGMENTS + 2) * 3
).fill(0);
// const vertices = [];
const indices = [];
this.pointerGeometry = new THREE.BufferGeometry();
this.pointerGeometry.setAttribute(
"position",
new THREE.Float32BufferAttribute(vertices, 3)
);
this._updatePointerVertices(POINTER_REAR_RADIUS);
// construct faces to connect rings
for (i = 0; i < POINTER_RINGS; i++) {
for (j = 0; j < POINTER_SEGMENTS - 1; j++) {
indices.push(
i * POINTER_SEGMENTS + j,
i * POINTER_SEGMENTS + j + 1,
(i + 1) * POINTER_SEGMENTS + j
);
indices.push(
i * POINTER_SEGMENTS + j + 1,
(i + 1) * POINTER_SEGMENTS + j + 1,
(i + 1) * POINTER_SEGMENTS + j
);
}
indices.push(
(i + 1) * POINTER_SEGMENTS - 1,
i * POINTER_SEGMENTS,
(i + 2) * POINTER_SEGMENTS - 1
);
indices.push(
i * POINTER_SEGMENTS,
(i + 1) * POINTER_SEGMENTS,
(i + 2) * POINTER_SEGMENTS - 1
);
}
// construct front and rear face
const frontCenterIndex = POINTER_SEGMENTS * (1 + POINTER_RINGS);
const rearCenterIndex = POINTER_SEGMENTS * (1 + POINTER_RINGS) + 1;
for (i = 0; i < POINTER_SEGMENTS - 1; i++) {
indices.push(frontCenterIndex, i + 1, i);
indices.push(
rearCenterIndex,
i + POINTER_SEGMENTS * POINTER_RINGS,
i + POINTER_SEGMENTS * POINTER_RINGS + 1
);
}
indices.push(frontCenterIndex, 0, POINTER_SEGMENTS - 1);
indices.push(
rearCenterIndex,
POINTER_SEGMENTS * (POINTER_RINGS + 1) - 1,
POINTER_SEGMENTS * POINTER_RINGS
);
const material = new THREE.MeshBasicMaterial();
material.transparent = true;
material.opacity = POINTER_OPACITY_MIN;
this.pointerGeometry.setIndex(indices);
this.pointerMesh = new THREE.Mesh(this.pointerGeometry, material);
this.pointerMesh.position.set(0, 0, -1 * POINTER_REAR_RADIUS);
this.pointerObject = new THREE.Object3D();
this.pointerObject.add(this.pointerMesh);
this.raycaster = new THREE.Raycaster();
// create cursor
const cursorGeometry = new THREE.SphereGeometry(CURSOR_RADIUS, 10, 10);
const cursorMaterial = new THREE.MeshBasicMaterial();
cursorMaterial.transparent = true;
cursorMaterial.opacity = POINTER_OPACITY_MIN;
this.cursorObject = new THREE.Mesh(cursorGeometry, cursorMaterial);
this.pointerObject.add(this.cursorObject);
this.add(this.pointerObject);
}
_updateRaycaster() {
if (this.raycaster) {
const pointerMatrix = this.pointerObject.matrixWorld;
const tempMatrix = new THREE.Matrix4();
tempMatrix.identity().extractRotation(pointerMatrix);
this.raycaster.ray.origin.setFromMatrixPosition(pointerMatrix);
this.raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
}
}
_updatePointer() {
this.pointerObject.visible = this.controller.visible;
const indexTip = this.hand.joints["index-finger-tip"];
const thumbTip = this.hand.joints["thumb-tip"];
const distance = indexTip.position.distanceTo(thumbTip.position);
const position = indexTip.position
.clone()
.add(thumbTip.position)
.multiplyScalar(0.5);
this.pointerObject.position.copy(position);
this.pointerObject.quaternion.copy(this.controller.quaternion);
this.pinched = distance <= PINCH_THRESHOLD;
const pinchScale = (distance - PINCH_MIN) / (PINCH_MAX - PINCH_MIN);
const focusScale = (distance - PINCH_MIN) / (PINCH_THRESHOLD - PINCH_MIN);
if (pinchScale > 1) {
this._updatePointerVertices(POINTER_REAR_RADIUS);
this.pointerMesh.position.set(0, 0, -1 * POINTER_REAR_RADIUS);
this.pointerMesh.material.opacity = POINTER_OPACITY_MIN;
} else if (pinchScale > 0) {
const rearRadius =
(POINTER_REAR_RADIUS - POINTER_REAR_RADIUS_MIN) * pinchScale +
POINTER_REAR_RADIUS_MIN;
this._updatePointerVertices(rearRadius);
if (focusScale < 1) {
this.pointerMesh.position.set(
0,
0,
-1 * rearRadius - (1 - focusScale) * POINTER_ADVANCE_MAX
);
this.pointerMesh.material.opacity =
POINTER_OPACITY_MIN +
(1 - focusScale) * (POINTER_OPACITY_MAX - POINTER_OPACITY_MIN);
} else {
this.pointerMesh.position.set(0, 0, -1 * rearRadius);
this.pointerMesh.material.opacity = POINTER_OPACITY_MIN;
}
} else {
this._updatePointerVertices(POINTER_REAR_RADIUS_MIN);
this.pointerMesh.position.set(
0,
0,
-1 * POINTER_REAR_RADIUS_MIN - POINTER_ADVANCE_MAX
);
this.pointerMesh.material.opacity = POINTER_OPACITY_MAX;
}
this.cursorObject.material.opacity = this.pointerMesh.material.opacity;
}
updateMatrixWorld(force) {
THREE.Object3D.prototype.updateMatrixWorld.call(this, force);
if (this.pointerGeometry) {
this._updatePointer();
this._updateRaycaster();
}
}
isPinched() {
return this.pinched;
}
setAttached(attached) {
this.attached = attached;
}
isAttached() {
return this.attached;
}
intersectObject(object) {
if (this.raycaster) {
return this.raycaster.intersectObject(object);
}
}
intersectObjects(objects) {
if (this.raycaster) {
return this.raycaster.intersectObjects(objects);
}
}
checkIntersections(objects) {
if (this.raycaster && !this.attached) {
let intersections = this.raycaster.intersectObjects(objects);
let direction = new THREE.Vector3(0, 0, -1);
if (intersections.length > 0) {
let intersection = intersections[0];
let distance = intersection.distance;
this.cursorObject.position.copy(direction.multiplyScalar(distance));
} else {
this.cursorObject.position.copy(direction.multiplyScalar(CURSOR_MAX_DISTANCE));
}
}
}
setCursor(distance) {
let direction = new THREE.Vector3(0, 0, -1);
if (this.raycaster && !this.attached) {
this.cursorObject.position.copy(direction.multiplyScalar(distance));
}
}
}
export { OculusHandPointerModel };
import * as THREE from "../../../build/three.module.js";
function createText(message, height) {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
let metrics = null,
textHeight = 100;
context.font = "normal " + textHeight + "px Arial";
metrics = context.measureText(message);
const textWidth = metrics.width;
canvas.width = textWidth;
canvas.height = textHeight;
context.font = "normal " + textHeight + "px Arial";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillStyle = "#ffffff";
context.fillText(message, textWidth / 2, textHeight / 2);
const texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
//var spriteAlignment = new THREE.Vector2(0,0) ;
const material = new THREE.MeshBasicMaterial({
color: 0xffffff,
side: THREE.DoubleSide,
map: texture,
transparent: true,
});
const geometry = new THREE.PlaneGeometry(
(height * textWidth) / textHeight,
height
);
let plane = new THREE.Mesh(geometry, material);
return plane;
}
export { createText };
import { GLTFLoader } from '../loaders/GLTFLoader.js';
class XRHandMeshModel {
constructor(handModel, controller, assetUrl) {
this.controller = controller;
this.handModel = handModel;
this.bones = [];
const loader = new GLTFLoader();
loader.setPath('');
loader.load(assetUrl, gltf => {
const object = gltf.scene.children[0];
this.handModel.add(object);
const mesh = object.getObjectByProperty('type', 'SkinnedMesh');
mesh.frustumCulled = false;
mesh.castShadow = true;
mesh.receiveShadow = true;
const joints = [
'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',
];
joints.forEach(jointName => {
const bone = object.getObjectByName(jointName);
if (bone !== undefined) {
bone.jointName = jointName;
} else {
console.warn(`Couldn't find ${jointName} in ${handedness} hand mesh`);
}
this.bones.push(bone);
});
});
}
updateMesh() {
// XR Joints
const XRJoints = this.controller.joints;
for (let i = 0; i < this.bones.length; i++) {
const bone = this.bones[i];
if (bone) {
const XRJoint = XRJoints[bone.jointName];
if (XRJoint.visible) {
const position = XRJoint.position;
if (bone) {
bone.position.copy(position);
bone.quaternion.copy(XRJoint.quaternion);
// bone.scale.setScalar( XRJoint.jointRadius || defaultRadius );
}
}
}
}
}
}
export { XRHandMeshModel };
......@@ -6,9 +6,16 @@ import {
XRHandPrimitiveModel
} from './XRHandPrimitiveModel.js';
import {
XRHandOculusMeshModel
} from './XRHandOculusMeshModel.js';
import {
XRHandMeshModel
} from "./XRHandMeshModel.js";
import {
fetchProfile
} from '../libs/motion-controllers.module.js';
const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles';
const DEFAULT_PROFILE = 'generic-hand';
class XRHandModel extends Object3D {
......@@ -77,7 +84,15 @@ class XRHandModelFactory {
} else if ( profile === 'oculus' ) {
handModel.motionController = new XRHandOculusMeshModel( handModel, controller, this.path, xrInputSource.handedness, options );
fetchProfile(xrInputSource, DEFAULT_PROFILES_PATH, DEFAULT_PROFILE).then(({ profile, assetPath }) => {
handModel.motionController = new XRHandMeshModel( handModel, controller, assetPath);
}).catch((err) => {
console.warn(err);
});
}
......
import { FBXLoader } from '../loaders/FBXLoader.js';
class XRHandOculusMeshModel {
constructor( handModel, controller, path, handedness, options ) {
this.controller = controller;
this.handModel = handModel;
this.bones = [];
const loader = new FBXLoader();
const low = options && options.model === 'lowpoly' ? '_low' : '';
loader.setPath( path );
loader.load( `OculusHand_${handedness === 'right' ? 'R' : 'L'}${low}.fbx`, object => {
this.handModel.add( object );
// Hack because of the scale of the skinnedmesh
object.scale.setScalar( 0.01 );
const mesh = object.getObjectByProperty( 'type', 'SkinnedMesh' );
mesh.frustumCulled = false;
mesh.castShadow = true;
mesh.receiveShadow = true;
const bonesMapping = [
'b_%_wrist', // XRHand.WRIST,
'b_%_thumb1', // XRHand.THUMB_METACARPAL,
'b_%_thumb2', // XRHand.THUMB_PHALANX_PROXIMAL,
'b_%_thumb3', // XRHand.THUMB_PHALANX_DISTAL,
'b_%_thumb_null', // XRHand.THUMB_PHALANX_TIP,
null, //'b_%_index1', // XRHand.INDEX_METACARPAL,
'b_%_index1', // XRHand.INDEX_PHALANX_PROXIMAL,
'b_%_index2', // XRHand.INDEX_PHALANX_INTERMEDIATE,
'b_%_index3', // XRHand.INDEX_PHALANX_DISTAL,
'b_%_index_null', // XRHand.INDEX_PHALANX_TIP,
null, //'b_%_middle1', // XRHand.MIDDLE_METACARPAL,
'b_%_middle1', // XRHand.MIDDLE_PHALANX_PROXIMAL,
'b_%_middle2', // XRHand.MIDDLE_PHALANX_INTERMEDIATE,
'b_%_middle3', // XRHand.MIDDLE_PHALANX_DISTAL,
'b_%_middlenull', // XRHand.MIDDLE_PHALANX_TIP,
null, //'b_%_ring1', // XRHand.RING_METACARPAL,
'b_%_ring1', // XRHand.RING_PHALANX_PROXIMAL,
'b_%_ring2', // XRHand.RING_PHALANX_INTERMEDIATE,
'b_%_ring3', // XRHand.RING_PHALANX_DISTAL,
'b_%_ring_inull', // XRHand.RING_PHALANX_TIP,
'b_%_pinky0', // XRHand.LITTLE_METACARPAL,
'b_%_pinky1', // XRHand.LITTLE_PHALANX_PROXIMAL,
'b_%_pinky2', // XRHand.LITTLE_PHALANX_INTERMEDIATE,
'b_%_pinky3', // XRHand.LITTLE_PHALANX_DISTAL,
'b_%_pinkynull', // XRHand.LITTLE_PHALANX_TIP
];
const joints = [
'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',
];
let i = 0;
bonesMapping.forEach( boneName => {
if ( boneName ) {
const bone = object.getObjectByName( boneName.replace( /%/g, handedness === 'right' ? 'r' : 'l' ) );
if ( bone !== undefined ) {
bone.jointName = joints[ i ];
}
this.bones.push( bone );
} else {
this.bones.push( null );
}
i ++;
} );
} );
}
updateMesh() {
// XR Joints
const XRJoints = this.controller.joints;
for ( let i = 0; i < this.bones.length; i ++ ) {
const bone = this.bones[ i ];
if ( bone ) {
const XRJoint = XRJoints[ bone.jointName ];
if ( XRJoint.visible ) {
const position = XRJoint.position;
if ( bone ) {
bone.position.copy( position.clone().multiplyScalar( 100 ) );
bone.quaternion.copy( XRJoint.quaternion );
// bone.scale.setScalar( XRJoint.jointRadius || defaultRadius );
}
}
}
}
}
}
export { XRHandOculusMeshModel };
......@@ -87,7 +87,7 @@
scene.add( controller2 );
const controllerModelFactory = new XRControllerModelFactory();
const handModelFactory = new XRHandModelFactory().setPath( "./models/fbx/" );
const handModelFactory = new XRHandModelFactory().setPath( "./models/gltf/" );
// Hand 1
controllerGrip1 = renderer.xr.getControllerGrip( 0 );
......
......@@ -100,7 +100,7 @@
scene.add( controller2 );
const controllerModelFactory = new XRControllerModelFactory();
const handModelFactory = new XRHandModelFactory().setPath( "./models/fbx/" );
const handModelFactory = new XRHandModelFactory().setPath( "./models/gltf/" );
// Hand 1
controllerGrip1 = renderer.xr.getControllerGrip( 0 );
......
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webxr hands - point and click</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> vr - handinput - point and click<br />
(Oculus Browser with #webxr-hands flag enabled)
</div>
<script type="module">
import * as THREE from '../build/three.module.js';
import { VRButton } from './jsm/webxr/VRButton.js';
import { XRControllerModelFactory } from './jsm/webxr/XRControllerModelFactory.js';
import { OculusHandModel } from './jsm/webxr/OculusHandModel.js';
import { OculusHandPointerModel } from './jsm/webxr/OculusHandPointerModel.js';
import { createText } from './jsm/webxr/Text2D.js';
import { World, System, Component, TagComponent, Types } from "https://ecsy.io/build/ecsy.module.js";
class Object3D extends Component { }
Object3D.schema = {
object: { type: Types.Ref }
};
class Button extends Component { }
Button.schema = {
// button states: [none, hovered, pressed]
currState: { type: Types.String, default: 'none' },
prevState: { type: Types.String, default: 'none' },
action: { type: Types.Ref, default: () => { } }
}
class ButtonSystem extends System {
execute(delta, time) {
this.queries.buttons.results.forEach(entity => {
var button = entity.getMutableComponent(Button);
var buttonMesh = entity.getComponent(Object3D).object;
if (button.currState == 'none') {
buttonMesh.scale.set(1, 1, 1);
} else {
buttonMesh.scale.set(1.1, 1.1, 1.1);
}
if (button.currState == "pressed" && button.prevState != "pressed") {
button.action();
}
// preserve prevState, clear currState
// HandRaySystem will update currState
button.prevState = button.currState;
button.currState = 'none';
});
}
}
ButtonSystem.queries = {
buttons: {
components: [Button]
}
}
class Intersectable extends TagComponent { }
class HandRaySystem extends System {
init(attributes) {
this.handPointers = attributes.handPointers;
}
execute(delta, time) {
this.handPointers.forEach(hp => {
var distance = null;
var intersectingEntity = null;
this.queries.intersectable.results.forEach(entity => {
var object = entity.getComponent(Object3D).object;
let intersections = hp.intersectObject(object);
if (intersections && intersections.length > 0) {
if (distance == null || intersections[0].distance < distance) {
distance = intersections[0].distance;
intersectingEntity = entity;
}
}
});
if (distance) {
hp.setCursor(distance);
if (intersectingEntity.hasComponent(Button)) {
let button = intersectingEntity.getMutableComponent(Button);
if (hp.isPinched()) {
button.currState = 'pressed';
} else if (button.currState != 'pressed') {
button.currState = 'hovered';
}
}
} else {
hp.setCursor(1.5);
}
});
}
}
HandRaySystem.queries = {
intersectable: {
components: [Intersectable]
}
};
class Rotating extends TagComponent { }
class RotatingSystem extends System {
execute(delta, time) {
this.queries.rotatingObjects.results.forEach(entity => {
var object = entity.getComponent(Object3D).object;
object.rotation.x += 0.4 * delta;
object.rotation.y += 0.4 * delta;
});
}
}
RotatingSystem.queries = {
rotatingObjects: {
components: [Rotating]
}
}
class HandsInstructionText extends TagComponent { }
class InstructionSystem extends System {
init(attributes) {
this.controllers = attributes.controllers;
}
execute(delta, time) {
let visible = false;
this.controllers.forEach(controller => {
if (controller.visible) {
visible = true;
}
})
this.queries.instructionTexts.results.forEach(entity => {
var object = entity.getComponent(Object3D).object;
object.visible = visible;
});
}
}
InstructionSystem.queries = {
instructionTexts: {
components: [HandsInstructionText]
}
}
class OffsetFromCamera extends Component { }
OffsetFromCamera.schema = {
x: { type: Types.Number, default: 0 },
y: { type: Types.Number, default: 0 },
z: { type: Types.Number, default: 0 },
}
class NeedCalibration extends TagComponent { }
class CalibrationSystem extends System {
init(attributes) {
this.camera = attributes.camera;
this.renderer = attributes.renderer;
}
execute(delta, time) {
this.queries.needCalibration.results.forEach(entity => {
if (this.renderer.xr.getSession()) {
let offset = entity.getComponent(OffsetFromCamera);
let object = entity.getComponent(Object3D).object;
let xrCamera = renderer.xr.getCamera(this.camera);
object.position.x = xrCamera.position.x + offset.x;
object.position.y = xrCamera.position.y + offset.y;
object.position.z = xrCamera.position.z + offset.z;
entity.removeComponent(NeedCalibration);
}
});
}
}
CalibrationSystem.queries = {
needCalibration: {
components: [NeedCalibration]
}
}
let world = new World();
var clock = new THREE.Clock();
let camera, scene, renderer;
init();
animate();
function makeButtonMesh(x, y, z, color) {
const geometry = new THREE.BoxGeometry(x, y, z);
const material = new THREE.MeshPhongMaterial({ color: color });
const buttonMesh = new THREE.Mesh(geometry, material);
return buttonMesh;
}
function init() {
let container = document.createElement('div');
document.body.appendChild(container);
scene = new THREE.Scene();
scene.background = new THREE.Color(0x444444);
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10);
camera.position.set(0, 1.2, 0.3);
scene.add(new THREE.HemisphereLight(0x808080, 0x606060));
const light = new THREE.DirectionalLight(0xffffff);
light.position.set(0, 6, 0);
light.castShadow = true;
light.shadow.camera.top = 2;
light.shadow.camera.bottom = - 2;
light.shadow.camera.right = 2;
light.shadow.camera.left = - 2;
light.shadow.mapSize.set(4096, 4096);
scene.add(light);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.shadowMap.enabled = true;
renderer.xr.enabled = true;
container.appendChild(renderer.domElement);
document.body.appendChild(VRButton.createButton(renderer));
// controllers
let controller1 = renderer.xr.getController(0);
scene.add(controller1);
let controller2 = renderer.xr.getController(1);
scene.add(controller2);
const controllerModelFactory = new XRControllerModelFactory();
// Hand 1
let controllerGrip1 = renderer.xr.getControllerGrip(0);
controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1));
scene.add(controllerGrip1);
let hand1 = renderer.xr.getHand(0);
hand1.add(new OculusHandModel(hand1));
let handPointer1 = new OculusHandPointerModel(hand1, controller1);
hand1.add(handPointer1);
scene.add(hand1);
// Hand 2
let controllerGrip2 = renderer.xr.getControllerGrip(1);
controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2));
scene.add(controllerGrip2);
let hand2 = renderer.xr.getHand(1);
hand2.add(new OculusHandModel(hand2));
let handPointer2 = new OculusHandPointerModel(hand2, controller2);
hand2.add(handPointer2);
scene.add(hand2);
// setup objects in scene and entities
const floorGeometry = new THREE.PlaneGeometry(4, 4);
const floorMaterial = new THREE.MeshPhongMaterial({ color: 0x222222 });
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = - Math.PI / 2;
scene.add(floor);
const menuGeometry = new THREE.PlaneGeometry(0.24, 0.5);
const menuMaterial = new THREE.MeshPhongMaterial({
opacity: 0,
transparent: true,
});
let menuMesh = new THREE.Mesh(menuGeometry, menuMaterial);
menuMesh.position.set(0.4, 1, -1);
menuMesh.rotation.y = - Math.PI / 12;
scene.add(menuMesh);
let orangeButton = makeButtonMesh(0.2, 0.1, 0.01, 0xffd3b5);
orangeButton.position.set(0, 0.18, 0);
menuMesh.add(orangeButton);
let pinkButton = makeButtonMesh(0.2, 0.1, 0.01, 0xe84a5f);
pinkButton.position.set(0, 0.06, 0);
menuMesh.add(pinkButton);
let resetButton = makeButtonMesh(0.2, 0.1, 0.01, 0x355c7d);
let resetButtonText = createText("reset", 0.06);
resetButton.add(resetButtonText);
resetButtonText.position.set(0, 0, 0.0051);
resetButton.position.set(0, -0.06, 0);
menuMesh.add(resetButton);
let exitButton = makeButtonMesh(0.2, 0.1, 0.01, 0xff0000);
let exitButtonText = createText("exit", 0.06);
exitButton.add(exitButtonText);
exitButtonText.position.set(0, 0, 0.0051);
exitButton.position.set(0, -0.18, 0);
menuMesh.add(exitButton);
let tkGeometry = new THREE.TorusKnotGeometry(0.5, 0.2, 200, 32);
let tkMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff });
tkMaterial.metalness = 0.8;
let torusKnot = new THREE.Mesh(tkGeometry, tkMaterial);
torusKnot.position.set(0, 1, -5);
scene.add(torusKnot);
let instructionText = createText("This is a WebXR Hands demo, please explore with hands.", 0.04);
instructionText.position.set(0, 1.6, -0.6);
scene.add(instructionText);
let exitText = createText("Exiting session...", 0.04);
exitText.position.set(0, 1.5, -0.6);
exitText.visible = false;
scene.add(exitText);
world
.registerComponent(Object3D)
.registerComponent(Button)
.registerComponent(Intersectable)
.registerComponent(Rotating)
.registerComponent(HandsInstructionText)
.registerComponent(OffsetFromCamera)
.registerComponent(NeedCalibration);
world
.registerSystem(RotatingSystem)
.registerSystem(InstructionSystem, { controllers: [controllerGrip1, controllerGrip2] })
.registerSystem(CalibrationSystem, { renderer: renderer, camera: camera })
.registerSystem(ButtonSystem)
.registerSystem(HandRaySystem, { handPointers: [handPointer1, handPointer2] });
var menuEntity = world.createEntity();
menuEntity.addComponent(Intersectable);
menuEntity.addComponent(OffsetFromCamera, { x: 0.4, y: 0, z: -1 });
menuEntity.addComponent(NeedCalibration);
menuEntity.addComponent(Object3D, { object: menuMesh });
var obEntity = world.createEntity();
obEntity.addComponent(Intersectable);
obEntity.addComponent(Object3D, { object: orangeButton });
let obAction = function () { torusKnot.material.color.setHex(0xffd3b5); };
obEntity.addComponent(Button, { action: obAction });
var pbEntity = world.createEntity();
pbEntity.addComponent(Intersectable);
pbEntity.addComponent(Object3D, { object: pinkButton });
let pbAction = function () { torusKnot.material.color.setHex(0xe84a5f); };
pbEntity.addComponent(Button, { action: pbAction });
var rbEntity = world.createEntity();
rbEntity.addComponent(Intersectable);
rbEntity.addComponent(Object3D, { object: resetButton });
let rbAction = function () { torusKnot.material.color.setHex(0xffffff); };
rbEntity.addComponent(Button, { action: rbAction });
var ebEntity = world.createEntity();
ebEntity.addComponent(Intersectable);
ebEntity.addComponent(Object3D, { object: exitButton });
let ebAction = function () {
exitText.visible = true;
setTimeout(function () { exitText.visible = false; renderer.xr.getSession().end(); }, 2000);
};
ebEntity.addComponent(Button, { action: ebAction });
var tkEntity = world.createEntity();
tkEntity.addComponent(Rotating);
tkEntity.addComponent(Object3D, { object: torusKnot });
var itEntity = world.createEntity();
itEntity.addComponent(HandsInstructionText);
itEntity.addComponent(Object3D, { object: instructionText });
window.addEventListener('resize', onWindowResize);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
renderer.setAnimationLoop(render);
}
function render() {
var delta = clock.getDelta();
var elapsedTime = clock.elapsedTime;
world.execute(delta, elapsedTime);
renderer.render(scene, camera);
}
</script>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webxr hands - point and drag</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> vr - handinput - point and drag<br />
(Oculus Browser with #webxr-hands flag enabled)
</div>
<script type="module">
import * as THREE from '../build/three.module.js';
import { VRButton } from './jsm/webxr/VRButton.js';
import { XRControllerModelFactory } from './jsm/webxr/XRControllerModelFactory.js';
import { OculusHandModel } from './jsm/webxr/OculusHandModel.js';
import { OculusHandPointerModel } from './jsm/webxr/OculusHandPointerModel.js';
import { createText } from './jsm/webxr/Text2D.js';
import { World, System, Component, TagComponent, Types } from "https://ecsy.io/build/ecsy.module.js";
class Object3D extends Component { }
Object3D.schema = {
object: { type: Types.Ref }
};
class Button extends Component { }
Button.schema = {
// button states: [none, hovered, pressed]
currState: { type: Types.String, default: 'none' },
prevState: { type: Types.String, default: 'none' },
action: { type: Types.Ref, default: () => { } }
}
class ButtonSystem extends System {
execute(delta, time) {
this.queries.buttons.results.forEach(entity => {
var button = entity.getMutableComponent(Button);
var buttonMesh = entity.getComponent(Object3D).object;
if (button.currState == 'none') {
buttonMesh.scale.set(1, 1, 1);
} else {
buttonMesh.scale.set(1.1, 1.1, 1.1);
}
if (button.currState == "pressed" && button.prevState != "pressed") {
button.action();
}
// preserve prevState, clear currState
// HandRaySystem will update currState
button.prevState = button.currState;
button.currState = 'none';
});
}
}
ButtonSystem.queries = {
buttons: {
components: [Button]
}
}
class Draggable extends Component { }
Draggable.schema = {
// draggable states: [detached, hovered, to-be-attached, attached, to-be-detached]
state: { type: Types.String, default: 'none' },
originalParent: { type: Types.Ref, default: null },
attachedPointer: { type: Types.Ref, default: null }
}
class DraggableSystem extends System {
execute(delta, time) {
this.queries.draggable.results.forEach(entity => {
let draggable = entity.getMutableComponent(Draggable);
let object = entity.getComponent(Object3D).object;
if (draggable.originalParent == null) {
draggable.originalParent = object.parent;
}
switch (draggable.state) {
case 'to-be-attached':
draggable.attachedPointer.children[0].attach(object);
draggable.state = 'attached';
break;
case 'to-be-detached':
draggable.originalParent.attach(object);
draggable.state = 'detached';
break;
default:
object.scale.set(1, 1, 1);
}
});
}
}
DraggableSystem.queries = {
draggable: {
components: [Draggable]
}
}
class Intersectable extends TagComponent { }
class HandRaySystem extends System {
init(attributes) {
this.handPointers = attributes.handPointers;
}
execute(delta, time) {
this.handPointers.forEach(hp => {
var distance = null;
var intersectingEntity = null;
this.queries.intersectable.results.forEach(entity => {
let object = entity.getComponent(Object3D).object;
let intersections = hp.intersectObject(object);
if (intersections && intersections.length > 0) {
if (distance == null || intersections[0].distance < distance) {
distance = intersections[0].distance;
intersectingEntity = entity;
}
}
});
if (distance) {
hp.setCursor(distance);
if (intersectingEntity.hasComponent(Button)) {
let button = intersectingEntity.getMutableComponent(Button);
if (hp.isPinched()) {
button.currState = 'pressed';
} else if (button.currState != 'pressed') {
button.currState = 'hovered';
}
}
if (intersectingEntity.hasComponent(Draggable)) {
let draggable = intersectingEntity.getMutableComponent(Draggable);
let object = intersectingEntity.getComponent(Object3D).object;
object.scale.set(1.1, 1.1, 1.1);
if (hp.isPinched()) {
if (!hp.isAttached() && draggable.state != 'attached') {
draggable.state = 'to-be-attached';
draggable.attachedPointer = hp;
hp.setAttached(true);
}
} else {
if (hp.isAttached() && draggable.state == 'attached') {
console.log('hello');
draggable.state = 'to-be-detached';
draggable.attachedPointer = null;
hp.setAttached(false);
}
}
}
} else {
hp.setCursor(1.5);
}
});
}
}
HandRaySystem.queries = {
intersectable: {
components: [Intersectable]
}
};
class HandsInstructionText extends TagComponent { }
class InstructionSystem extends System {
init(attributes) {
this.controllers = attributes.controllers;
}
execute(delta, time) {
let visible = false;
this.controllers.forEach(controller => {
if (controller.visible) {
visible = true;
}
})
this.queries.instructionTexts.results.forEach(entity => {
var object = entity.getComponent(Object3D).object;
object.visible = visible;
});
}
}
InstructionSystem.queries = {
instructionTexts: {
components: [HandsInstructionText]
}
}
class OffsetFromCamera extends Component { }
OffsetFromCamera.schema = {
x: { type: Types.Number, default: 0 },
y: { type: Types.Number, default: 0 },
z: { type: Types.Number, default: 0 },
}
class NeedCalibration extends TagComponent { }
class CalibrationSystem extends System {
init(attributes) {
this.camera = attributes.camera;
this.renderer = attributes.renderer;
}
execute(delta, time) {
this.queries.needCalibration.results.forEach(entity => {
if (this.renderer.xr.getSession()) {
let offset = entity.getComponent(OffsetFromCamera);
let object = entity.getComponent(Object3D).object;
let xrCamera = renderer.xr.getCamera(this.camera);
object.position.x = xrCamera.position.x + offset.x;
object.position.y = xrCamera.position.y + offset.y;
object.position.z = xrCamera.position.z + offset.z;
entity.removeComponent(NeedCalibration);
}
});
}
}
CalibrationSystem.queries = {
needCalibration: {
components: [NeedCalibration]
}
}
class Randomizable extends TagComponent { }
class RandomizerSystem extends System {
init(attributes) {
this.needRandomizing = true;
}
execute(delta, time) {
if (!this.needRandomizing) { return; }
this.queries.randomizable.results.forEach(entity => {
let object = entity.getComponent(Object3D).object;
object.material.color.setHex(Math.random() * 0xffffff);
object.position.x = Math.random() * 2 - 1;
object.position.y = Math.random() * 2;
object.position.z = Math.random() * 2 - 1;
object.rotation.x = Math.random() * 2 * Math.PI;
object.rotation.y = Math.random() * 2 * Math.PI;
object.rotation.z = Math.random() * 2 * Math.PI;
object.scale.x = Math.random() + 0.5;
object.scale.y = Math.random() + 0.5;
object.scale.z = Math.random() + 0.5;
this.needRandomizing = false;
});
}
}
RandomizerSystem.queries = {
randomizable: {
components: [Randomizable]
}
}
let world = new World();
var clock = new THREE.Clock();
let camera, scene, renderer;
init();
animate();
function makeButtonMesh(x, y, z, color) {
const geometry = new THREE.BoxGeometry(x, y, z);
const material = new THREE.MeshPhongMaterial({ color: color });
const buttonMesh = new THREE.Mesh(geometry, material);
return buttonMesh;
}
function init() {
let container = document.createElement('div');
document.body.appendChild(container);
scene = new THREE.Scene();
scene.background = new THREE.Color(0x444444);
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10);
camera.position.set(0, 1.2, 0.3);
scene.add(new THREE.HemisphereLight(0x808080, 0x606060));
const light = new THREE.DirectionalLight(0xffffff);
light.position.set(0, 6, 0);
light.castShadow = true;
light.shadow.camera.top = 2;
light.shadow.camera.bottom = - 2;
light.shadow.camera.right = 2;
light.shadow.camera.left = - 2;
light.shadow.mapSize.set(4096, 4096);
scene.add(light);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.shadowMap.enabled = true;
renderer.xr.enabled = true;
container.appendChild(renderer.domElement);
document.body.appendChild(VRButton.createButton(renderer));
// controllers
let controller1 = renderer.xr.getController(0);
scene.add(controller1);
let controller2 = renderer.xr.getController(1);
scene.add(controller2);
const controllerModelFactory = new XRControllerModelFactory();
// Hand 1
let controllerGrip1 = renderer.xr.getControllerGrip(0);
controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1));
scene.add(controllerGrip1);
let hand1 = renderer.xr.getHand(0);
hand1.add(new OculusHandModel(hand1));
let handPointer1 = new OculusHandPointerModel(hand1, controller1);
hand1.add(handPointer1);
scene.add(hand1);
// Hand 2
let controllerGrip2 = renderer.xr.getControllerGrip(1);
controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2));
scene.add(controllerGrip2);
let hand2 = renderer.xr.getHand(1);
hand2.add(new OculusHandModel(hand2));
let handPointer2 = new OculusHandPointerModel(hand2, controller2);
hand2.add(handPointer2);
scene.add(hand2);
// setup objects in scene and entities
const floorGeometry = new THREE.PlaneGeometry(4, 4);
const floorMaterial = new THREE.MeshPhongMaterial({ color: 0x222222 });
let floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = - Math.PI / 2;
scene.add(floor);
const menuGeometry = new THREE.PlaneGeometry(0.24, 0.5);
const menuMaterial = new THREE.MeshPhongMaterial({
opacity: 0,
transparent: true,
});
let menuMesh = new THREE.Mesh(menuGeometry, menuMaterial);
menuMesh.position.set(0.4, 1, -1);
menuMesh.rotation.y = - Math.PI / 12;
scene.add(menuMesh);
let resetButton = makeButtonMesh(0.2, 0.1, 0.01, 0x355c7d);
let resetButtonText = createText("reset", 0.06);
resetButton.add(resetButtonText);
resetButtonText.position.set(0, 0, 0.0051);
resetButton.position.set(0, -0.06, 0);
menuMesh.add(resetButton);
let exitButton = makeButtonMesh(0.2, 0.1, 0.01, 0xff0000);
let exitButtonText = createText("exit", 0.06);
exitButton.add(exitButtonText);
exitButtonText.position.set(0, 0, 0.0051);
exitButton.position.set(0, -0.18, 0);
menuMesh.add(exitButton);
let instructionText = createText("This is a WebXR Hands demo, please explore with hands.", 0.04);
instructionText.position.set(0, 1.6, -0.6);
scene.add(instructionText);
let exitText = createText("Exiting session...", 0.04);
exitText.position.set(0, 1.5, -0.6);
exitText.visible = false;
scene.add(exitText);
world
.registerComponent(Object3D)
.registerComponent(Button)
.registerComponent(Intersectable)
.registerComponent(HandsInstructionText)
.registerComponent(OffsetFromCamera)
.registerComponent(NeedCalibration)
.registerComponent(Randomizable)
.registerComponent(Draggable);
world
.registerSystem(RandomizerSystem)
.registerSystem(InstructionSystem, { controllers: [controllerGrip1, controllerGrip2] })
.registerSystem(CalibrationSystem, { renderer: renderer, camera: camera })
.registerSystem(ButtonSystem)
.registerSystem(DraggableSystem)
.registerSystem(HandRaySystem, { handPointers: [handPointer1, handPointer2] });
for (let i = 0; i < 20; i++) {
const object = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.15, 0.15), new THREE.MeshLambertMaterial({ color: 0xffffff }));
scene.add(object);
let entity = world.createEntity();
entity.addComponent(Intersectable);
entity.addComponent(Randomizable);
entity.addComponent(Object3D, { object: object });
entity.addComponent(Draggable);
}
var menuEntity = world.createEntity();
menuEntity.addComponent(Intersectable);
menuEntity.addComponent(OffsetFromCamera, { x: 0.4, y: 0, z: -1 });
menuEntity.addComponent(NeedCalibration);
menuEntity.addComponent(Object3D, { object: menuMesh });
var rbEntity = world.createEntity();
rbEntity.addComponent(Intersectable);
rbEntity.addComponent(Object3D, { object: resetButton });
let rbAction = function () { world.getSystem(RandomizerSystem).needRandomizing = true; };
rbEntity.addComponent(Button, { action: rbAction });
var ebEntity = world.createEntity();
ebEntity.addComponent(Intersectable);
ebEntity.addComponent(Object3D, { object: exitButton });
let ebAction = function () {
exitText.visible = true;
setTimeout(function () { exitText.visible = false; renderer.xr.getSession().end(); }, 2000);
};
ebEntity.addComponent(Button, { action: ebAction });
var itEntity = world.createEntity();
itEntity.addComponent(HandsInstructionText);
itEntity.addComponent(Object3D, { object: instructionText });
window.addEventListener('resize', onWindowResize);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
renderer.setAnimationLoop(render);
}
function render() {
var delta = clock.getDelta();
var elapsedTime = clock.elapsedTime;
world.execute(delta, elapsedTime);
renderer.render(scene, camera);
}
</script>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webxr hands - press button</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> vr - handinput - press button<br />
(Oculus Browser with #webxr-hands flag enabled)
</div>
<script type="module">
import * as THREE from '../build/three.module.js';
import { VRButton } from './jsm/webxr/VRButton.js';
import { XRControllerModelFactory } from './jsm/webxr/XRControllerModelFactory.js';
import { OculusHandModel } from './jsm/webxr/OculusHandModel.js';
import { createText } from './jsm/webxr/Text2D.js';
import { World, System, Component, TagComponent, Types } from "https://ecsy.io/build/ecsy.module.js";
class Object3D extends Component { }
Object3D.schema = {
object: { type: Types.Ref }
};
class Button extends Component { }
Button.schema = {
// button states: [resting, pressed, fully_pressed, recovering]
currState: { type: Types.String, default: 'resting' },
prevState: { type: Types.String, default: 'resting' },
pressSound: { type: Types.Ref, default: null },
releaseSound: { type: Types.Ref, default: null },
restingY: { type: Types.Number, default: null },
surfaceY: { type: Types.Number, default: null },
recoverySpeed: { type: Types.Number, default: 0.4 },
fullPressDistance: { type: Types.Number, default: null },
action: { type: Types.Ref, default: () => { } }
}
class ButtonSystem extends System {
init(attributes) {
this.renderer = attributes.renderer;
this.soundAdded = false;
}
execute(delta, time) {
let buttonPressSound, buttonReleaseSound;
if (this.renderer.xr.getSession() && !this.soundAdded) {
let xrCamera = this.renderer.xr.getCamera(camera);
const listener = new THREE.AudioListener();
xrCamera.add(listener);
// create a global audio source
buttonPressSound = new THREE.Audio(listener);
buttonReleaseSound = new THREE.Audio(listener);
// load a sound and set it as the Audio object's buffer
const audioLoader = new THREE.AudioLoader();
audioLoader.load('sounds/button-press.ogg', function (buffer) {
buttonPressSound.setBuffer(buffer);
});
audioLoader.load('sounds/button-release.ogg', function (buffer) {
buttonReleaseSound.setBuffer(buffer);
});
this.soundAdded = true;
}
this.queries.buttons.results.forEach(entity => {
var button = entity.getMutableComponent(Button);
var buttonMesh = entity.getComponent(Object3D).object;
// populate restingY
if (button.restingY == null) {
button.restingY = buttonMesh.position.y;
}
if (buttonPressSound) {
button.pressSound = buttonPressSound;
}
if (buttonReleaseSound) {
button.releaseSound = buttonReleaseSound;
}
if (button.currState == 'fully_pressed' && button.prevState != 'fully_pressed') {
button.pressSound?.play();
button.action();
}
if (button.currState == 'recovering' && button.prevState != 'recovering') {
button.releaseSound?.play();
}
// preserve prevState, clear currState
// FingerInputSystem will update currState
button.prevState = button.currState;
button.currState = 'resting';
});
}
}
ButtonSystem.queries = {
buttons: {
components: [Button]
}
}
class Pressable extends TagComponent { }
class FingerInputSystem extends System {
init(attributes) {
this.hands = attributes.hands;
}
execute(delta, time) {
this.queries.pressable.results.forEach(entity => {
var button = entity.getMutableComponent(Button);
let object = entity.getComponent(Object3D).object;
let pressingDistances = [];
this.hands.forEach(hand => {
if (hand && hand.intersectBoxObject(object)) {
let pressingPosition = hand.getPointerPosition();
pressingDistances.push(button.surfaceY - object.worldToLocal(pressingPosition).y);
}
});
if (pressingDistances.length == 0) { // not pressed this frame
if (object.position.y < button.restingY) {
object.position.y += button.recoverySpeed * delta;
button.currState = "recovering";
} else {
object.position.y = button.restingY;
button.currState = "resting";
}
} else {
button.currState = "pressed";
let pressingDistance = Math.max(pressingDistances);
if (pressingDistance > 0) {
object.position.y -= pressingDistance;
}
if (object.position.y <= button.restingY - button.fullPressDistance) {
button.currState = "fully_pressed";
object.position.y = button.restingY - button.fullPressDistance;
}
}
});
}
}
FingerInputSystem.queries = {
pressable: {
components: [Pressable]
}
};
class Rotating extends TagComponent { }
class RotatingSystem extends System {
execute(delta, time) {
this.queries.rotatingObjects.results.forEach(entity => {
var object = entity.getComponent(Object3D).object;
object.rotation.x += 0.4 * delta;
object.rotation.y += 0.4 * delta;
});
}
}
RotatingSystem.queries = {
rotatingObjects: {
components: [Rotating]
}
}
class HandsInstructionText extends TagComponent { }
class InstructionSystem extends System {
init(attributes) {
this.controllers = attributes.controllers;
}
execute(delta, time) {
let visible = false;
this.controllers.forEach(controller => {
if (controller.visible) {
visible = true;
}
})
this.queries.instructionTexts.results.forEach(entity => {
var object = entity.getComponent(Object3D).object;
object.visible = visible;
});
}
}
InstructionSystem.queries = {
instructionTexts: {
components: [HandsInstructionText]
}
}
class OffsetFromCamera extends Component { }
OffsetFromCamera.schema = {
x: { type: Types.Number, default: 0 },
y: { type: Types.Number, default: 0 },
z: { type: Types.Number, default: 0 },
}
class NeedCalibration extends TagComponent { }
class CalibrationSystem extends System {
init(attributes) {
this.camera = attributes.camera;
this.renderer = attributes.renderer;
}
execute(delta, time) {
this.queries.needCalibration.results.forEach(entity => {
if (this.renderer.xr.getSession()) {
let offset = entity.getComponent(OffsetFromCamera);
let object = entity.getComponent(Object3D).object;
let xrCamera = renderer.xr.getCamera(this.camera);
object.position.x = xrCamera.position.x + offset.x;
object.position.y = xrCamera.position.y + offset.y;
object.position.z = xrCamera.position.z + offset.z;
entity.removeComponent(NeedCalibration);
}
});
}
}
CalibrationSystem.queries = {
needCalibration: {
components: [NeedCalibration]
}
}
let world = new World();
var clock = new THREE.Clock();
let camera, scene, renderer;
init();
animate();
function makeButtonMesh(x, y, z, color) {
const geometry = new THREE.BoxGeometry(x, y, z);
const material = new THREE.MeshPhongMaterial({ color: color });
const buttonMesh = new THREE.Mesh(geometry, material);
return buttonMesh;
}
function init() {
let container = document.createElement('div');
document.body.appendChild(container);
scene = new THREE.Scene();
scene.background = new THREE.Color(0x444444);
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10);
camera.position.set(0, 1.2, 0.3);
scene.add(new THREE.HemisphereLight(0x808080, 0x606060));
const light = new THREE.DirectionalLight(0xffffff);
light.position.set(0, 6, 0);
light.castShadow = true;
light.shadow.camera.top = 2;
light.shadow.camera.bottom = - 2;
light.shadow.camera.right = 2;
light.shadow.camera.left = - 2;
light.shadow.mapSize.set(4096, 4096);
scene.add(light);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.shadowMap.enabled = true;
renderer.xr.enabled = true;
container.appendChild(renderer.domElement);
document.body.appendChild(VRButton.createButton(renderer));
// controllers
let controller1 = renderer.xr.getController(0);
scene.add(controller1);
let controller2 = renderer.xr.getController(1);
scene.add(controller2);
const controllerModelFactory = new XRControllerModelFactory();
// Hand 1
let controllerGrip1 = renderer.xr.getControllerGrip(0);
controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1));
scene.add(controllerGrip1);
let hand1 = renderer.xr.getHand(0);
let handModel1 = new OculusHandModel(hand1)
hand1.add(handModel1);
scene.add(hand1);
// Hand 2
let controllerGrip2 = renderer.xr.getControllerGrip(1);
controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2));
scene.add(controllerGrip2);
let hand2 = renderer.xr.getHand(1);
let handModel2 = new OculusHandModel(hand2);
hand2.add(handModel2);
scene.add(hand2);
// buttons
const floorGeometry = new THREE.PlaneGeometry(4, 4);
const floorMaterial = new THREE.MeshPhongMaterial({ color: 0x222222 });
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = - Math.PI / 2;
scene.add(floor);
const consoleGeometry = new THREE.BoxGeometry(0.5, 0.12, 0.15);
const consoleMaterial = new THREE.MeshPhongMaterial({ color: 0x595959 });
let consoleMesh = new THREE.Mesh(consoleGeometry, consoleMaterial);
consoleMesh.position.set(0, 1, -0.3);
scene.add(consoleMesh);
let orangeButton = makeButtonMesh(0.08, 0.1, 0.08, 0xffd3b5);
orangeButton.position.set(-0.15, 0.04, 0);
consoleMesh.add(orangeButton);
let pinkButton = makeButtonMesh(0.08, 0.1, 0.08, 0xe84a5f);
pinkButton.position.set(-0.05, 0.04, 0);
consoleMesh.add(pinkButton);
let resetButton = makeButtonMesh(0.08, 0.1, 0.08, 0x355c7d);
let resetButtonText = createText("reset", 0.03);
resetButton.add(resetButtonText);
resetButtonText.rotation.x = - Math.PI / 2;
resetButtonText.position.set(0, 0.051, 0);
resetButton.position.set(0.05, 0.04, 0);
consoleMesh.add(resetButton);
let exitButton = makeButtonMesh(0.08, 0.1, 0.08, 0xff0000);
let exitButtonText = createText("exit", 0.03);
exitButton.add(exitButtonText);
exitButtonText.rotation.x = - Math.PI / 2;
exitButtonText.position.set(0, 0.051, 0);
exitButton.position.set(0.15, 0.04, 0);
consoleMesh.add(exitButton);
let tkGeometry = new THREE.TorusKnotGeometry(0.5, 0.2, 200, 32);
let tkMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff });
tkMaterial.metalness = 0.8;
let torusKnot = new THREE.Mesh(tkGeometry, tkMaterial);
torusKnot.position.set(0, 1, -5);
scene.add(torusKnot);
let instructionText = createText("This is a WebXR Hands demo, please explore with hands.", 0.04);
instructionText.position.set(0, 1.6, -0.6);
scene.add(instructionText);
let exitText = createText("Exiting session...", 0.04);
exitText.position.set(0, 1.5, -0.6);
exitText.visible = false;
scene.add(exitText);
world
.registerComponent(Object3D)
.registerComponent(Button)
.registerComponent(Pressable)
.registerComponent(Rotating)
.registerComponent(HandsInstructionText)
.registerComponent(OffsetFromCamera)
.registerComponent(NeedCalibration);
world
.registerSystem(RotatingSystem)
.registerSystem(InstructionSystem, { controllers: [controllerGrip1, controllerGrip2] })
.registerSystem(CalibrationSystem, { renderer: renderer, camera: camera })
.registerSystem(ButtonSystem, { renderer: renderer, camera: camera })
.registerSystem(FingerInputSystem, { hands: [handModel1, handModel2] });
var csEntity = world.createEntity();
csEntity.addComponent(OffsetFromCamera, { x: 0, y: -0.4, z: -0.3 });
csEntity.addComponent(NeedCalibration);
csEntity.addComponent(Object3D, { object: consoleMesh });
var obEntity = world.createEntity();
obEntity.addComponent(Pressable);
obEntity.addComponent(Object3D, { object: orangeButton });
let obAction = function () { torusKnot.material.color.setHex(0xffd3b5); };
obEntity.addComponent(Button, { action: obAction, surfaceY: 0.05, fullPressDistance: 0.02 });
var pbEntity = world.createEntity();
pbEntity.addComponent(Pressable);
pbEntity.addComponent(Object3D, { object: pinkButton });
let pbAction = function () { torusKnot.material.color.setHex(0xe84a5f); };
pbEntity.addComponent(Button, { action: pbAction, surfaceY: 0.05, fullPressDistance: 0.02 });
var rbEntity = world.createEntity();
rbEntity.addComponent(Pressable);
rbEntity.addComponent(Object3D, { object: resetButton });
let rbAction = function () { torusKnot.material.color.setHex(0xffffff); };
rbEntity.addComponent(Button, { action: rbAction, surfaceY: 0.05, fullPressDistance: 0.02 });
var ebEntity = world.createEntity();
ebEntity.addComponent(Pressable);
ebEntity.addComponent(Object3D, { object: exitButton });
let ebAction = function () {
exitText.visible = true;
setTimeout(function () { exitText.visible = false; renderer.xr.getSession().end(); }, 2000);
};
ebEntity.addComponent(Button, { action: ebAction, surfaceY: 0.05, recoverySpeed: 0.2, fullPressDistance: 0.03 });
var tkEntity = world.createEntity();
tkEntity.addComponent(Rotating);
tkEntity.addComponent(Object3D, { object: torusKnot });
var itEntity = world.createEntity();
itEntity.addComponent(HandsInstructionText);
itEntity.addComponent(Object3D, { object: instructionText });
window.addEventListener('resize', onWindowResize);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
renderer.setAnimationLoop(render);
}
function render() {
var delta = clock.getDelta();
var elapsedTime = clock.elapsedTime;
world.execute(delta, elapsedTime);
renderer.render(scene, camera);
}
</script>
</body>
</html>
\ No newline at end of file
......@@ -99,7 +99,7 @@
scene.add( controller2 );
const controllerModelFactory = new XRControllerModelFactory();
const handModelFactory = new XRHandModelFactory().setPath( "./models/fbx/" );
const handModelFactory = new XRHandModelFactory().setPath( "./models/gltf/" );
// Hand 1
......@@ -113,7 +113,8 @@
handModels.left = [
handModelFactory.createHandModel( hand1, "boxes" ),
handModelFactory.createHandModel( hand1, "spheres" ),
handModelFactory.createHandModel( hand1, "oculus", { model: "lowpoly" } ),
// low poly option disabled until low poly hands model is fixed
// handModelFactory.createHandModel( hand1, "oculus", { model: "lowpoly" } ),
handModelFactory.createHandModel( hand1, "oculus" )
];
......@@ -152,7 +153,8 @@
handModels.right = [
handModelFactory.createHandModel( hand2, "boxes" ),
handModelFactory.createHandModel( hand2, "spheres" ),
handModelFactory.createHandModel( hand2, "oculus", { model: "lowpoly" } ),
// low poly option disabled until low poly hands model is fixed
// handModelFactory.createHandModel( hand2, "oculus", { model: "lowpoly" } ),
handModelFactory.createHandModel( hand2, "oculus" )
];
handModels.right.forEach( model => {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册