123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487 |
- import { IDisposable, Scene } from '../../scene';
- import { WebXRControllerComponent } from './webXRControllerComponent';
- import { Observable } from '../../Misc/observable';
- import { Logger } from '../../Misc/logger';
- import { SceneLoader } from '../../Loading/sceneLoader';
- import { AbstractMesh } from '../../Meshes/abstractMesh';
- import { Nullable } from '../../types';
- import { Quaternion, Vector3 } from '../../Maths/math.vector';
- import { Mesh } from '../../Meshes/mesh';
- /**
- * Handness type in xrInput profiles. These can be used to define layouts in the Layout Map.
- */
- export type MotionControllerHandness = "none" | "left" | "right";
- /**
- * The type of components available in motion controllers.
- * This is not the name of the component.
- */
- export type MotionControllerComponentType = "trigger" | "squeeze" | "touchpad" | "thumbstick" | "button";
- /**
- * The state of a controller component
- */
- export type MotionControllerComponentStateType = "default" | "touched" | "pressed";
- /**
- * The schema of motion controller layout.
- * No object will be initialized using this interface
- * This is used just to define the profile.
- */
- export interface IMotionControllerLayout {
- /**
- * Defines the main button component id
- */
- selectComponentId: string;
- /**
- * Available components (unsorted)
- */
- components: {
- /**
- * A map of component Ids
- */
- [componentId: string]: {
- /**
- * The type of input the component outputs
- */
- type: MotionControllerComponentType;
- /**
- * The indices of this component in the gamepad object
- */
- gamepadIndices: {
- /**
- * Index of button
- */
- button?: number;
- /**
- * If available, index of x-axis
- */
- xAxis?: number;
- /**
- * If available, index of y-axis
- */
- yAxis?: number;
- };
- /**
- * The mesh's root node name
- */
- rootNodeName: string;
- /**
- * Animation definitions for this model
- */
- visualResponses: {
- [stateKey: string]: {
- /**
- * What property will be animated
- */
- componentProperty: "xAxis" | "yAxis" | "button" | "state";
- /**
- * What states influence this visual reponse
- */
- states: MotionControllerComponentStateType[];
- /**
- * Type of animation - movement or visibility
- */
- valueNodeProperty: "transform" | "visibility";
- /**
- * Base node name to move. Its position will be calculated according to the min and max nodes
- */
- valueNodeName?: string;
- /**
- * Minimum movement node
- */
- minNodeName?: string;
- /**
- * Max movement node
- */
- maxNodeName?: string;
- }
- }
- /**
- * If touch enabled, what is the name of node to display user feedback
- */
- touchPointNodeName?: string;
- }
- };
- /**
- * Is it xr standard mapping or not
- */
- gamepadMapping: "" | "xr-standard";
- /**
- * Base root node of this entire model
- */
- rootNodeName: string;
- /**
- * Path to load the assets. Usually relative to the base path
- */
- assetPath: string;
- }
- /**
- * A definition for the layout map in the input profile
- */
- export interface IMotionControllerLayoutMap {
- /**
- * Layouts with handness type as a key
- */
- [handness: string /* handness */]: IMotionControllerLayout;
- }
- /**
- * The XR Input profile schema
- * Profiles can be found here:
- * https://github.com/immersive-web/webxr-input-profiles/tree/master/packages/registry/profiles
- */
- export interface IMotionControllerProfile {
- /**
- * The id of this profile
- * correlates to the profile(s) in the xrInput.profiles array
- */
- profileId: string;
- /**
- * fallback profiles for this profileId
- */
- fallbackProfileIds: string[];
- /**
- * The layout map, with handness as key
- */
- layouts: IMotionControllerLayoutMap;
- }
- /**
- * A helper-interface for the 3 meshes needed for controller button animation
- * The meshes are provided to the _lerpButtonTransform function to calculate the current position of the value mesh
- */
- export interface IMotionControllerButtonMeshMap {
- /**
- * The mesh that will be changed when value changes
- */
- valueMesh: AbstractMesh;
- /**
- * the mesh that defines the pressed value mesh position.
- * This is used to find the max-position of this button
- */
- pressedMesh: AbstractMesh;
- /**
- * the mesh that defines the unpressed value mesh position.
- * This is used to find the min (or initial) position of this button
- */
- unpressedMesh: AbstractMesh;
- }
- /**
- * A helper-interface for the 3 meshes needed for controller axis animation.
- * This will be expanded when touchpad animations are fully supported
- * The meshes are provided to the _lerpAxisTransform function to calculate the current position of the value mesh
- */
- export interface IMotionControllerMeshMap {
- /**
- * The mesh that will be changed when axis value changes
- */
- valueMesh: AbstractMesh;
- /**
- * the mesh that defines the minimum value mesh position.
- */
- minMesh?: AbstractMesh;
- /**
- * the mesh that defines the maximum value mesh position.
- */
- maxMesh?: AbstractMesh;
- }
- /**
- * The elements needed for change-detection of the gamepad objects in motion controllers
- */
- export interface IMinimalMotionControllerObject {
- /**
- * An array of available buttons
- */
- buttons: Array<{
- /**
- * Value of the button/trigger
- */
- value: number;
- /**
- * If the button/trigger is currently touched
- */
- touched: boolean;
- /**
- * If the button/trigger is currently pressed
- */
- pressed: boolean;
- }>;
- /**
- * Available axes of this controller
- */
- axes: number[];
- }
- /**
- * An Abstract Motion controller
- * This class receives an xrInput and a profile layout and uses those to initialize the components
- * Each component has an observable to check for changes in value and state
- */
- export abstract class WebXRAbstractMotionController implements IDisposable {
- /**
- * The profile id of this motion controller
- */
- public abstract profileId: string;
- /**
- * A map of components (WebXRControllerComponent) in this motion controller
- * Components have a ComponentType and can also have both button and axis definitions
- */
- public readonly components: {
- [id: string]: WebXRControllerComponent
- } = {};
- /**
- * Observers registered here will be triggered when the model of this controller is done loading
- */
- public onModelLoadedObservable: Observable<WebXRAbstractMotionController> = new Observable();
- /**
- * The root mesh of the model. It is null if the model was not yet initialized
- */
- public rootMesh: Nullable<AbstractMesh>;
- /**
- * Disable the model's animation. Can be set at any time.
- */
- public disableAnimation: boolean = false;
- private _modelReady: boolean = false;
- /**
- * constructs a new abstract motion controller
- * @param scene the scene to which the model of the controller will be added
- * @param layout The profile layout to load
- * @param gamepadObject The gamepad object correlating to this controller
- * @param handness handness (left/right/none) of this controller
- * @param _doNotLoadControllerMesh set this flag to ignore the mesh loading
- */
- constructor(protected scene: Scene, protected layout: IMotionControllerLayout,
- /**
- * The gamepad object correlating to this controller
- */
- public gamepadObject: IMinimalMotionControllerObject,
- /**
- * handness (left/right/none) of this controller
- */
- public handness: MotionControllerHandness,
- _doNotLoadControllerMesh: boolean = false) {
- // initialize the components
- if (layout.components) {
- Object.keys(layout.components).forEach(this._initComponent);
- }
- // Model is loaded in WebXRInput
- }
- private _initComponent = (id: string) => {
- if (!id) { return; }
- const componentDef = this.layout.components[id];
- const type = componentDef.type;
- const buttonIndex = componentDef.gamepadIndices.button;
- // search for axes
- let axes: number[] = [];
- if (componentDef.gamepadIndices.xAxis !== undefined && componentDef.gamepadIndices.yAxis !== undefined) {
- axes.push(componentDef.gamepadIndices.xAxis, componentDef.gamepadIndices.yAxis);
- }
- this.components[id] = new WebXRControllerComponent(id, type, buttonIndex, axes);
- }
- /**
- * Update this model using the current XRFrame
- * @param xrFrame the current xr frame to use and update the model
- */
- public updateFromXRFrame(xrFrame: XRFrame): void {
- this.getComponentIds().forEach((id) => this.getComponent(id).update(this.gamepadObject));
- this.updateModel(xrFrame);
- }
- /**
- * Get the list of components available in this motion controller
- * @returns an array of strings correlating to available components
- */
- public getComponentIds(): string[] {
- return Object.keys(this.components);
- }
- /**
- * Get the main (Select) component of this controller as defined in the layout
- * @returns the main component of this controller
- */
- public getMainComponent(): WebXRControllerComponent {
- return this.getComponent(this.layout.selectComponentId);
- }
- /**
- * get a component based an its component id as defined in layout.components
- * @param id the id of the component
- * @returns the component correlates to the id or undefined if not found
- */
- public getComponent(id: string): WebXRControllerComponent {
- return this.components[id];
- }
- /**
- * Get the first component of specific type
- * @param type type of component to find
- * @return a controller component or null if not found
- */
- public getComponentOfType(type: MotionControllerComponentType): Nullable<WebXRControllerComponent> {
- return this.getAllComponentsOfType(type)[0] || null;
- }
- /**
- * Returns all components of specific type
- * @param type the type to search for
- * @return an array of components with this type
- */
- public getAllComponentsOfType(type: MotionControllerComponentType): WebXRControllerComponent[] {
- return this.getComponentIds().map((id) => this.components[id]).filter((component) => component.type === type);
- }
- /**
- * Loads the model correlating to this controller
- * When the mesh is loaded, the onModelLoadedObservable will be triggered
- * @returns A promise fulfilled with the result of the model loading
- */
- public async loadModel(): Promise<boolean> {
- let useGeneric = !this._getModelLoadingConstraints();
- let loadingParams = this._getGenericFilenameAndPath();
- // Checking if GLB loader is present
- if (useGeneric) {
- Logger.Warn("You need to reference GLTF loader to load Windows Motion Controllers model. Falling back to generic models");
- } else {
- loadingParams = this._getFilenameAndPath();
- }
- return new Promise((resolve, reject) => {
- SceneLoader.ImportMesh("", loadingParams.path, loadingParams.filename, this.scene, (meshes: AbstractMesh[]) => {
- if (useGeneric) {
- this._getGenericParentMesh(meshes);
- } else {
- this._setRootMesh(meshes);
- }
- this._processLoadedModel(meshes);
- this._modelReady = true;
- this.onModelLoadedObservable.notifyObservers(this);
- resolve(true);
- }, null, (_scene: Scene, message: string) => {
- Logger.Log(message);
- Logger.Warn(`Failed to retrieve controller model of type ${this.profileId} from the remote server: ${loadingParams.path}${loadingParams.filename}`);
- reject(message);
- });
- });
- }
- /**
- * Update the model itself with the current frame data
- * @param xrFrame the frame to use for updating the model mesh
- */
- protected updateModel(xrFrame: XRFrame): void {
- if (!this._modelReady) {
- return;
- }
- this._updateModel(xrFrame);
- }
- /**
- * Moves the axis on the controller mesh based on its current state
- * @param axis the index of the axis
- * @param axisValue the value of the axis which determines the meshes new position
- * @hidden
- */
- protected _lerpTransform(axisMap: IMotionControllerMeshMap, axisValue: number, fixValueCoordinates?: boolean): void {
- if (!axisMap.minMesh || !axisMap.maxMesh) {
- return;
- }
- if (!axisMap.minMesh.rotationQuaternion || !axisMap.maxMesh.rotationQuaternion || !axisMap.valueMesh.rotationQuaternion) {
- return;
- }
- // Convert from gamepad value range (-1 to +1) to lerp range (0 to 1)
- let lerpValue = fixValueCoordinates ? axisValue * 0.5 + 0.5 : axisValue;
- Quaternion.SlerpToRef(
- axisMap.minMesh.rotationQuaternion,
- axisMap.maxMesh.rotationQuaternion,
- lerpValue,
- axisMap.valueMesh.rotationQuaternion);
- Vector3.LerpToRef(
- axisMap.minMesh.position,
- axisMap.maxMesh.position,
- lerpValue,
- axisMap.valueMesh.position);
- }
- // Look through all children recursively. This will return null if no mesh exists with the given name.
- protected _getChildByName(node: AbstractMesh, name: string): AbstractMesh {
- return <AbstractMesh>node.getChildren((n) => n.name === name, false)[0];
- }
- // Look through only immediate children. This will return null if no mesh exists with the given name.
- protected _getImmediateChildByName(node: AbstractMesh, name: string): AbstractMesh {
- return <AbstractMesh>node.getChildren((n) => n.name == name, true)[0];
- }
- private _getGenericFilenameAndPath(): { filename: string, path: string } {
- return {
- filename: "generic.babylon",
- path: "https://controllers.babylonjs.com/generic/"
- };
- }
- private _getGenericParentMesh(meshes: AbstractMesh[]): void {
- this.rootMesh = new Mesh(this.profileId + " " + this.handness, this.scene);
- meshes.forEach((mesh) => {
- if (!mesh.parent) {
- mesh.isPickable = false;
- mesh.setParent(this.rootMesh);
- }
- });
- this.rootMesh.rotationQuaternion = Quaternion.FromEulerAngles(0, Math.PI, 0);
- }
- /**
- * Get the filename and path for this controller's model
- * @returns a map of filename and path
- */
- protected abstract _getFilenameAndPath(): { filename: string, path: string };
- /**
- * This function will be called after the model was successfully loaded and can be used
- * for mesh transformations before it is available for the user
- * @param meshes the loaded meshes
- */
- protected abstract _processLoadedModel(meshes: AbstractMesh[]): void;
- /**
- * Set the root mesh for this controller. Important for the WebXR controller class
- * @param meshes the loaded meshes
- */
- protected abstract _setRootMesh(meshes: AbstractMesh[]): void;
- /**
- * A function executed each frame that updates the mesh (if needed)
- * @param xrFrame the current xrFrame
- */
- protected abstract _updateModel(xrFrame: XRFrame): void;
- /**
- * This function is called before the mesh is loaded. It checks for loading constraints.
- * For example, this function can check if the GLB loader is available
- * If this function returns false, the generic controller will be loaded instead
- * @returns Is the client ready to load the mesh
- */
- protected abstract _getModelLoadingConstraints(): boolean;
- /**
- * Dispose this controller, the model mesh and all its components
- */
- public dispose(): void {
- this.getComponentIds().forEach((id) => this.getComponent(id).dispose());
- if (this.rootMesh) {
- this.rootMesh.dispose();
- }
- }
- }
|