浏览代码

Add XRControllerModelLoader

This utility makes it super simple to load controller meshes from the webxr-input-profile registry, giving users a controller visualization that matches what they're holding in their hand, with individual components animated in response to real-world input.
Brandon Jones 5 年之前
父节点
当前提交
b0210cac23
共有 2 个文件被更改,包括 310 次插入0 次删除
  1. 24 0
      examples/jsm/webxr/XRControllerModelLoader.d.ts
  2. 286 0
      examples/jsm/webxr/XRControllerModelLoader.js

+ 24 - 0
examples/jsm/webxr/XRControllerModelLoader.d.ts

@@ -0,0 +1,24 @@
+import {
+	Group,
+	Object3D,
+	Texture
+} from '../../../src/Three';
+
+import { GLTFLoader } from '../loaders/GLTFLoader';
+
+export class XRControllerModel extends Object3D {
+	constructor( );
+
+	motionController: any;
+
+	setEnvironmentMap( envMap: Texture ): XRControllerModel;
+}
+
+export class XRControllerModelLoader {
+	constructor( gltfLoader?: GLTFLoader );
+	gltfLoader: GLTFLoader | null;
+	path: string;
+
+	setGLTFLoader( gltfLoader: GLTFLoader ): XRControllerModelLoader;
+	getControllerModel( controller: Group ): XRControllerModel;
+}

+ 286 - 0
examples/jsm/webxr/XRControllerModelLoader.js

@@ -0,0 +1,286 @@
+/**
+ * @author Nell Waliczek / https://github.com/NellWaliczek
+ * @author Brandon Jones / https://github.com/toji
+ */
+
+import {
+	Mesh,
+	MeshBasicMaterial,
+	Object3D,
+	Quaternion,
+	SphereGeometry,
+} from "../../../build/three.module.js";
+
+import {
+	Constants as MotionControllerConstants,
+	fetchProfile,
+	MotionController
+} from 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/motion-controllers.module.js'
+
+const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles'
+
+function XRControllerModel( ) {
+
+	Object3D.call( this );
+
+	this.motionController = null;
+	this.envMap = null;
+
+}
+
+XRControllerModel.prototype = Object.assign( Object.create( Object3D.prototype ), {
+
+	constructor: XRControllerModel,
+
+	setEnvironmentMap: function ( envMap ) {
+
+		if ( this.envMap == envMap ) {
+
+			return this;
+
+		}
+
+		this.envMap = envMap;
+		this.traverse(( child ) => {
+
+			if ( child.isMesh ) {
+
+				child.material.envMap = this.envMap;
+				child.material.needsUpdate = true;
+
+			}
+
+		});
+
+		return this;
+
+	},
+
+	/**
+	 * Polls data from the XRInputSource and updates the model's components to match
+	 * the real world data
+	 */
+	updateMatrixWorld: function ( force ) {
+
+		Object3D.prototype.updateMatrixWorld.call( this, force );
+
+		if ( !this.motionController ) return;
+
+		// Cause the MotionController to poll the Gamepad for data
+		this.motionController.updateFromGamepad();
+
+		// Update the 3D model to reflect the button, thumbstick, and touchpad state
+		Object.values( this.motionController.components ).forEach(( component ) => {
+
+			// Update node data based on the visual responses' current states
+			Object.values( component.visualResponses ).forEach(( visualResponse ) => {
+
+				const { valueNode, minNode, maxNode, value, valueNodeProperty } = visualResponse;
+
+				// Skip if the visual response node is not found. No error is needed,
+				// because it will have been reported at load time.
+				if ( !valueNode ) return;
+
+				// Calculate the new properties based on the weight supplied
+				if ( valueNodeProperty === MotionControllerConstants.VisualResponseProperty.VISIBILITY ) {
+
+					valueNode.visible = value;
+
+				} else if ( valueNodeProperty === MotionControllerConstants.VisualResponseProperty.TRANSFORM ) {
+
+					Quaternion.slerp(
+						minNode.quaternion,
+						maxNode.quaternion,
+						valueNode.quaternion,
+						value
+					);
+
+					valueNode.position.lerpVectors(
+						minNode.position,
+						maxNode.position,
+						value
+					);
+
+				}
+
+			});
+
+		});
+
+	}
+
+} );
+
+/**
+ * Walks the model's tree to find the nodes needed to animate the components and
+ * saves them to the motionContoller components for use in the frame loop. When
+ * touchpads are found, attaches a touch dot to them.
+ */
+function findNodes( motionController, scene ) {
+
+	// Loop through the components and find the nodes needed for each components' visual responses
+	Object.values( motionController.components ).forEach(( component ) => {
+
+		const { type, touchPointNodeName, visualResponses } = component;
+
+		if (type === MotionControllerConstants.ComponentType.TOUCHPAD) {
+
+			component.touchPointNode = scene.getObjectByName( touchPointNodeName );
+			if ( component.touchPointNode ) {
+
+				// Attach a touch dot to the touchpad.
+				const sphereGeometry = new SphereGeometry( 0.001 );
+				const material = new MeshBasicMaterial({ color: 0x0000FF });
+				const sphere = new Mesh( sphereGeometry, material );
+				component.touchPointNode.add( sphere );
+
+			} else {
+
+				console.warn(`Could not find touch dot, ${component.touchPointNodeName}, in touchpad component ${componentId}`);
+
+			}
+
+		}
+
+		// Loop through all the visual responses to be applied to this component
+		Object.values( visualResponses ).forEach(( visualResponse ) => {
+
+			const { valueNodeName, minNodeName, maxNodeName, valueNodeProperty } = visualResponse;
+
+			// If animating a transform, find the two nodes to be interpolated between.
+			if ( valueNodeProperty === MotionControllerConstants.VisualResponseProperty.TRANSFORM ) {
+
+				visualResponse.minNode = scene.getObjectByName(minNodeName);
+				visualResponse.maxNode = scene.getObjectByName(maxNodeName);
+
+				// If the extents cannot be found, skip this animation
+				if ( !visualResponse.minNode ) {
+
+					console.warn(`Could not find ${minNodeName} in the model`);
+					return;
+
+				}
+
+				if ( !visualResponse.maxNode ) {
+
+					console.warn(`Could not find ${maxNodeName} in the model`);
+					return;
+
+				}
+			}
+
+			// If the target node cannot be found, skip this animation
+			visualResponse.valueNode = scene.getObjectByName(valueNodeName);
+			if ( !visualResponse.valueNode ) {
+
+				console.warn(`Could not find ${valueNodeName} in the model`);
+
+			}
+		});
+	});
+}
+
+var XRControllerModelLoader = ( function () {
+
+	function XRControllerModelLoader( gltfLoader ) {
+
+		this.gltfLoader = gltfLoader;
+		this.path = DEFAULT_PROFILES_PATH;
+
+	}
+
+	XRControllerModelLoader.prototype = {
+
+		constructor: XRControllerModelLoader,
+
+		setGLTFLoader: function ( gltfLoader ) {
+
+			this.gltfLoader = gltfLoader;
+			return this;
+
+		},
+
+		getControllerModel: function ( controller ) {
+
+			if ( !this.gltfLoader ) {
+
+				throw new Error(`GLTFLoader not set.`);
+
+			}
+
+			const controllerModel = new XRControllerModel();
+			let scene = null;
+
+			controller.addEventListener( 'connected', ( event ) => {
+
+				const xrInputSource = event.data;
+
+				fetchProfile(xrInputSource, this.path).then(({ profile, assetPath }) => {
+
+					controllerModel.motionController = new MotionController(
+						xrInputSource,
+						profile,
+						assetPath
+					);
+
+					this.gltfLoader.setPath('');
+					this.gltfLoader.load( controllerModel.motionController.assetUrl, ( asset ) => {
+
+						scene = asset.scene;
+
+						// Find the nodes needed for animation and cache them on the motionController.
+						findNodes( controllerModel.motionController, scene );
+
+						// Apply any environment map that the mesh already has set.
+						if ( controllerModel.envMap ) {
+
+							scene.traverse(( child ) => {
+
+								if ( child.isMesh ) {
+
+									child.material.envMap = controllerModel.envMap;
+									child.material.needsUpdate = true;
+
+								}
+
+							});
+
+						}
+
+						// Add the glTF scene to the controllerModel.
+						controllerModel.add( scene );
+
+					},
+					null,
+					() => {
+
+						throw new Error(`Asset ${motionController.assetUrl} missing or malformed.`);
+
+					});
+
+				}).catch((err) => {
+
+					console.warn(err);
+
+				});
+
+			});
+
+			controller.addEventListener( 'disconnected', () => {
+
+				controllerModel.motionController = null;
+				controllerModel.remove( scene );
+
+			});
+
+			return controllerModel;
+
+		}
+
+	};
+
+	return XRControllerModelLoader;
+
+} )();
+
+export { XRControllerModelLoader };