WebXR控制器,用于按三.js按钮



我想弄清楚如何使用三.js和webXR为我的Oculus Quest和其他设备绘制控件。该代码有效,允许我移动控制器,将圆柱映射到每个控件,并允许我使用触发器到控件来更改圆柱体的颜色。这很棒,但我找不到任何有关如何为操纵杆、握把和其他按钮使用轴控件的文档。我的一部分想要相信这就像知道要调用哪个事件一样简单,因为我不知道还有哪些其他事件可用。

这是我基于此教程的链接。 https://github.com/as-ideas/webvr-with-threejs

请注意,此代码按预期工作,但我不知道如何进一步处理它并做更多的事情。

function createController(controllerID, videoinput) { 
//RENDER CONTROLLER AS YELLOW TUBE
const controller = renderer.vr.getController(controllerID);
const cylinderGeometry = new CylinderGeometry(0.025, 0.025, 1, 32);
const cylinderMaterial = new MeshPhongMaterial({ color: 0xffff00 });
const cylinder = new Mesh(cylinderGeometry, cylinderMaterial);
cylinder.geometry.translate(0, 0.5, 0);
cylinder.rotateX(-0.25 * Math.PI);
controller.add(cylinder);
cameraFixture.add(controller);
//TRIGGER
controller.addEventListener('selectstart', () => {
if (controllerID === 0) {
cylinderMaterial.color.set('pink')
} else {
cylinderMaterial.color.set('orange');
videoinput.play()
}
});
controller.addEventListener('selectend', () => {
cylinderMaterial.color.set(0xffff00);
videoinput.pause();
console.log('I pressed play');
});
}

截至三.js 0.119,不提供来自触摸控制器的其他按钮、触控板、触觉和操纵杆的集成"事件",仅提供选择和挤压事件。 三.js具有"正常工作"的功能模型,无论您拥有哪种类型的输入设备,并且仅提供管理所有输入设备可以生成的事件(即选择(

幸运的是,我们不受 Three .js 提供的限制,可以直接轮询控制器数据。


触摸控制器遵循"游戏手柄"控件的模型,只需报告其瞬时值。 我们将轮询游戏手柄以获取各种按钮的当前值,并跟踪它们的状态,并在代码中创建"事件",用于按钮按下、触控板和操纵杆轴更改。

在 webXR 会话中从触摸控制器访问瞬时数据

const session = renderer.xr.getSession();
let i = 0;
if (session) {
for (const source of session.inputSources) {
if (source && source.handedness) {
handedness = source.handedness; //left or right controllers
}
if (!source.gamepad) continue;
const controller = renderer.xr.getController(i++);
const old = prevGamePads.get(source);
const data = {
handedness: handedness,
buttons: source.gamepad.buttons.map((b) => b.value),
axes: source.gamepad.axes.slice(0)
};
//process data accordingly to create 'events'

触觉反馈是通过承诺提供的(请注意,目前并非所有浏览器都支持webXR触觉反馈,但Oculus Browser和Firefox Reality on quest支持(当可用时,触觉反馈是通过承诺产生的:

var didPulse = sourceXR.gamepad.hapticActuators[0].pulse(0.8, 100);
//80% intensity for 100ms
//subsequent promises cancel any previous promise still underway

为了演示此解决方案,我修改了 threejs.org/examples/#webXR_vr_dragging 示例,将相机添加到"推车"中,该推车可以在webXR会话中使用触摸控制器操纵杆四处移动,并为事件提供各种触觉反馈,例如光线投射到对象上或操纵杆上的轴移动。

对于每一帧,我们轮询来自触摸控制器的数据并做出相应的响应。 我们必须将数据从一帧存储到另一帧以检测更改并创建我们的事件,并过滤掉一些数据(在某些控制器上,操纵杆轴值中为 0 的假 0 和高达 20% 的随机漂移(为了正确的"向前和横向">小车运动,还需要每帧和通过以下方式访问 webXR 相机的当前航向和姿态:

let xrCamera = renderer.xr.getCamera(camera);
xrCamera.getWorldDirection(cameraVector);
//heading vector for webXR camera now within cameraVector

示例代码笔在这里: codepen.io/jason-buchheim/pen/zYqYGXM

在此处显示"进入VR">按钮(调试视图(:cdpn.io/jason-buchheim/debug/zYqYGXM


完整代码,修改了原始 threejs 示例,并用注释块突出显示

//// From webxr_vr_dragging example https://threejs.org/examples/#webxr_vr_dragging
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.119.1/build/three.module.min.js";
import { OrbitControls } from "https://cdn.jsdelivr.net/npm/three@0.119.1/examples/jsm/controls/OrbitControls.min.js";
import { VRButton } from "https://cdn.jsdelivr.net/npm/three@0.119.1/examples/jsm/webxr/VRButton.min.js";
import { XRControllerModelFactory } from "https://cdn.jsdelivr.net/npm/three@0.119.1/examples/jsm/webxr/XRControllerModelFactory.min.js";
var container;
var camera, scene, renderer;
var controller1, controller2;
var controllerGrip1, controllerGrip2;
var raycaster,
intersected = [];
var tempMatrix = new THREE.Matrix4();
var controls, group;
////////////////////////////////////////
//// MODIFICATIONS FROM THREEJS EXAMPLE
//// a camera dolly to move camera within webXR
//// a vector to reuse each frame to store webXR camera heading
//// a variable to store previous frames polling of gamepads
//// a variable to store accumulated accelerations along axis with continuous movement
var dolly;
var cameraVector = new THREE.Vector3(); // create once and reuse it!
const prevGamePads = new Map();
var speedFactor = [0.1, 0.1, 0.1, 0.1];
////
//////////////////////////////////////////
init();
animate();
function init() {
container = document.createElement("div");
document.body.appendChild(container);
scene = new THREE.Scene();
scene.background = new THREE.Color(0x808080);
camera = new THREE.PerspectiveCamera(
50,
window.innerWidth / window.innerHeight,
0.1,
500  //MODIFIED FOR LARGER SCENE
);
camera.position.set(0, 1.6, 3);
controls = new OrbitControls(camera, container);
controls.target.set(0, 1.6, 0);
controls.update();
var geometry = new THREE.PlaneBufferGeometry(100, 100);
var material = new THREE.MeshStandardMaterial({
color: 0xeeeeee,
roughness: 1.0,
metalness: 0.0
});
var floor = new THREE.Mesh(geometry, material);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
scene.add(new THREE.HemisphereLight(0x808080, 0x606060));
var light = new THREE.DirectionalLight(0xffffff);
light.position.set(0, 200, 0);           // MODIFIED SIZE OF SCENE AND SHADOW
light.castShadow = true;
light.shadow.camera.top = 200;           // MODIFIED FOR LARGER SCENE
light.shadow.camera.bottom = -200;       // MODIFIED FOR LARGER SCENE
light.shadow.camera.right = 200;         // MODIFIED FOR LARGER SCENE
light.shadow.camera.left = -200;         // MODIFIED FOR LARGER SCENE
light.shadow.mapSize.set(4096, 4096);
scene.add(light);
group = new THREE.Group();
scene.add(group);
var geometries = [
new THREE.BoxBufferGeometry(0.2, 0.2, 0.2),
new THREE.ConeBufferGeometry(0.2, 0.2, 64),
new THREE.CylinderBufferGeometry(0.2, 0.2, 0.2, 64),
new THREE.IcosahedronBufferGeometry(0.2, 3),
new THREE.TorusBufferGeometry(0.2, 0.04, 64, 32)
];
for (var i = 0; i < 100; i++) {
var geometry = geometries[Math.floor(Math.random() * geometries.length)];
var material = new THREE.MeshStandardMaterial({
color: Math.random() * 0xffffff,
roughness: 0.7,
side: THREE.DoubleSide,   // MODIFIED TO DoubleSide
metalness: 0.0
});
var object = new THREE.Mesh(geometry, material);
object.position.x = Math.random() * 200 - 100;  // MODIFIED FOR LARGER SCENE
object.position.y = Math.random() * 100;        // MODIFIED FOR LARGER SCENE
object.position.z = Math.random() * 200 - 100;  // MODIFIED FOR LARGER SCENE
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.setScalar(Math.random() * 20 + 0.5);  // MODIFIED FOR LARGER SCENE
object.castShadow = true;
object.receiveShadow = true;
group.add(object);
}
// renderer
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;
//the following increases the resolution on Quest
renderer.xr.setFramebufferScaleFactor(2.0);
container.appendChild(renderer.domElement);
document.body.appendChild(VRButton.createButton(renderer));
// controllers
controller1 = renderer.xr.getController(0);
controller1.name="left";    ////MODIFIED, added .name="left"
controller1.addEventListener("selectstart", onSelectStart);
controller1.addEventListener("selectend", onSelectEnd);
scene.add(controller1);
controller2 = renderer.xr.getController(1);
controller2.name="right";  ////MODIFIED added .name="right"
controller2.addEventListener("selectstart", onSelectStart);
controller2.addEventListener("selectend", onSelectEnd);
scene.add(controller2);
var controllerModelFactory = new XRControllerModelFactory();
controllerGrip1 = renderer.xr.getControllerGrip(0);
controllerGrip1.add(
controllerModelFactory.createControllerModel(controllerGrip1)
);
scene.add(controllerGrip1);
controllerGrip2 = renderer.xr.getControllerGrip(1);
controllerGrip2.add(
controllerModelFactory.createControllerModel(controllerGrip2)
);
scene.add(controllerGrip2);
//Raycaster Geometry
var geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, -1)
]);
var line = new THREE.Line(geometry);
line.name = "line";
line.scale.z = 50;   //MODIFIED FOR LARGER SCENE
controller1.add(line.clone());
controller2.add(line.clone());
raycaster = new THREE.Raycaster();
////////////////////////////////////////
//// MODIFICATIONS FROM THREEJS EXAMPLE
//// create group named 'dolly' and add camera and controllers to it
//// will move dolly to move camera and controllers in webXR
dolly = new THREE.Group();
dolly.position.set(0, 0, 0);
dolly.name = "dolly";
scene.add(dolly);
dolly.add(camera);
dolly.add(controller1);
dolly.add(controller2);
dolly.add(controllerGrip1);
dolly.add(controllerGrip2);
////
///////////////////////////////////
window.addEventListener("resize", onWindowResize, false);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function onSelectStart(event) {
var controller = event.target;
var intersections = getIntersections(controller);
if (intersections.length > 0) {
var intersection = intersections[0];
var object = intersection.object;
object.material.emissive.b = 1;
controller.attach(object);
controller.userData.selected = object;
}
}
function onSelectEnd(event) {
var controller = event.target;
if (controller.userData.selected !== undefined) {
var object = controller.userData.selected;
object.material.emissive.b = 0;
group.attach(object);
controller.userData.selected = undefined;
}
}
function getIntersections(controller) {
tempMatrix.identity().extractRotation(controller.matrixWorld);
raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
return raycaster.intersectObjects(group.children);
}
function intersectObjects(controller) {
// Do not highlight when already selected
if (controller.userData.selected !== undefined) return;
var line = controller.getObjectByName("line");
var intersections = getIntersections(controller);
if (intersections.length > 0) {
var intersection = intersections[0];
////////////////////////////////////////
//// MODIFICATIONS FROM THREEJS EXAMPLE
//// check if in webXR session
//// if so, provide haptic feedback to the controller that raycasted onto object
//// (only if haptic actuator is available)
const session = renderer.xr.getSession();
if (session) {  //only if we are in a webXR session
for (const sourceXR of session.inputSources) {
if (!sourceXR.gamepad) continue;
if (
sourceXR &&
sourceXR.gamepad &&
sourceXR.gamepad.hapticActuators &&
sourceXR.gamepad.hapticActuators[0] &&
sourceXR.handedness == controller.name              
) {
var didPulse = sourceXR.gamepad.hapticActuators[0].pulse(0.8, 100);
}
}
}
////
////////////////////////////////
var object = intersection.object;
object.material.emissive.r = 1;
intersected.push(object);
line.scale.z = intersection.distance;
} else {
line.scale.z = 50;   //MODIFIED AS OUR SCENE IS LARGER
}
}
function cleanIntersected() {
while (intersected.length) {
var object = intersected.pop();
object.material.emissive.r = 0;
}
}
function animate() {
renderer.setAnimationLoop(render);
}
function render() {
cleanIntersected();
intersectObjects(controller1);
intersectObjects(controller2);
////////////////////////////////////////
//// MODIFICATIONS FROM THREEJS EXAMPLE
//add gamepad polling for webxr to renderloop
dollyMove();
////
//////////////////////////////////////
renderer.render(scene, camera);
}

////////////////////////////////////////
//// MODIFICATIONS FROM THREEJS EXAMPLE
//// New dollyMove() function
//// this function polls gamepad and keeps track of its state changes to create 'events'
function dollyMove() {
var handedness = "unknown";
//determine if we are in an xr session
const session = renderer.xr.getSession();
let i = 0;
if (session) {
let xrCamera = renderer.xr.getCamera(camera);
xrCamera.getWorldDirection(cameraVector);
//a check to prevent console errors if only one input source
if (isIterable(session.inputSources)) {
for (const source of session.inputSources) {
if (source && source.handedness) {
handedness = source.handedness; //left or right controllers
}
if (!source.gamepad) continue;
const controller = renderer.xr.getController(i++);
const old = prevGamePads.get(source);
const data = {
handedness: handedness,
buttons: source.gamepad.buttons.map((b) => b.value),
axes: source.gamepad.axes.slice(0)
};
if (old) {
data.buttons.forEach((value, i) => {
//handlers for buttons
if (value !== old.buttons[i] || Math.abs(value) > 0.8) {
//check if it is 'all the way pushed'
if (value === 1) {
//console.log("Button" + i + "Down");
if (data.handedness == "left") {
//console.log("Left Paddle Down");
if (i == 1) {
dolly.rotateY(-THREE.Math.degToRad(1));
}
if (i == 3) {
//reset teleport to home position
dolly.position.x = 0;
dolly.position.y = 5;
dolly.position.z = 0;
}
} else {
//console.log("Right Paddle Down");
if (i == 1) {
dolly.rotateY(THREE.Math.degToRad(1));
}
}
} else {
// console.log("Button" + i + "Up");
if (i == 1) {
//use the paddle buttons to rotate
if (data.handedness == "left") {
//console.log("Left Paddle Down");
dolly.rotateY(-THREE.Math.degToRad(Math.abs(value)));
} else {
//console.log("Right Paddle Down");
dolly.rotateY(THREE.Math.degToRad(Math.abs(value)));
}
}
}
}
});
data.axes.forEach((value, i) => {
//handlers for thumbsticks
//if thumbstick axis has moved beyond the minimum threshold from center, windows mixed reality seems to wander up to about .17 with no input
if (Math.abs(value) > 0.2) {
//set the speedFactor per axis, with acceleration when holding above threshold, up to a max speed
speedFactor[i] > 1 ? (speedFactor[i] = 1) : (speedFactor[i] *= 1.001);
console.log(value, speedFactor[i], i);
if (i == 2) {
//left and right axis on thumbsticks
if (data.handedness == "left") {
// (data.axes[2] > 0) ? console.log('left on left thumbstick') : console.log('right on left thumbstick')
//move our dolly
//we reverse the vectors 90degrees so we can do straffing side to side movement
dolly.position.x -= cameraVector.z * speedFactor[i] * data.axes[2];
dolly.position.z += cameraVector.x * speedFactor[i] * data.axes[2];
//provide haptic feedback if available in browser
if (
source.gamepad.hapticActuators &&
source.gamepad.hapticActuators[0]
) {
var pulseStrength = Math.abs(data.axes[2]) + Math.abs(data.axes[3]);
if (pulseStrength > 0.75) {
pulseStrength = 0.75;
}
var didPulse = source.gamepad.hapticActuators[0].pulse(
pulseStrength,
100
);
}
} else {
// (data.axes[2] > 0) ? console.log('left on right thumbstick') : console.log('right on right thumbstick')
dolly.rotateY(-THREE.Math.degToRad(data.axes[2]));
}
controls.update();
}
if (i == 3) {
//up and down axis on thumbsticks
if (data.handedness == "left") {
// (data.axes[3] > 0) ? console.log('up on left thumbstick') : console.log('down on left thumbstick')
dolly.position.y -= speedFactor[i] * data.axes[3];
//provide haptic feedback if available in browser
if (
source.gamepad.hapticActuators &&
source.gamepad.hapticActuators[0]
) {
var pulseStrength = Math.abs(data.axes[3]);
if (pulseStrength > 0.75) {
pulseStrength = 0.75;
}
var didPulse = source.gamepad.hapticActuators[0].pulse(
pulseStrength,
100
);
}
} else {
// (data.axes[3] > 0) ? console.log('up on right thumbstick') : console.log('down on right thumbstick')
dolly.position.x -= cameraVector.x * speedFactor[i] * data.axes[3];
dolly.position.z -= cameraVector.z * speedFactor[i] * data.axes[3];
//provide haptic feedback if available in browser
if (
source.gamepad.hapticActuators &&
source.gamepad.hapticActuators[0]
) {
var pulseStrength = Math.abs(data.axes[2]) + Math.abs(data.axes[3]);
if (pulseStrength > 0.75) {
pulseStrength = 0.75;
}
var didPulse = source.gamepad.hapticActuators[0].pulse(
pulseStrength,
100
);
}
}
controls.update();
}
} else {
//axis below threshold - reset the speedFactor if it is greater than zero  or 0.025 but below our threshold
if (Math.abs(value) > 0.025) {
speedFactor[i] = 0.025;
}
}
});
}
///store this frames data to compate with in the next frame
prevGamePads.set(source, data);
}
}
}
}
function isIterable(obj) {  //function to check if object is iterable
// checks for null and undefined
if (obj == null) {
return false;
}
return typeof obj[Symbol.iterator] === "function";
}
////
/////////////////////////////////////

相关内容

  • 没有找到相关文章

最新更新