Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
565a6cc
add grip update event to WebXRController if enabled
danrossi Nov 25, 2025
a889fe2
add XRGamepad helper and example
danrossi Nov 25, 2025
81f64d3
Revert "add XRGamepad helper and example"
danrossi Nov 25, 2025
1799d13
Merge branch 'xr-controller-gamepad' of github.com:danrossi/three.js …
danrossi Nov 25, 2025
761a288
add XRGamepad helper and example
danrossi Nov 25, 2025
3e34816
update examples files
danrossi Nov 25, 2025
88deda8
Add gamepad example screenshot
danrossi Nov 25, 2025
424dbf0
Merge branch 'mrdoob:dev' into xr-controller-gamepad
danrossi Nov 27, 2025
168de19
Fix gamepad linting
danrossi Dec 3, 2025
0369a08
Merge branch 'mrdoob:dev' into xr-controller-gamepad
danrossi Dec 4, 2025
8848d34
fix controller manager and intersections
danrossi Dec 4, 2025
c9db21e
add visibilioty management to controller models
danrossi Dec 6, 2025
ae876c6
Merge branch 'mrdoob:dev' into xr-controller-gamepad
danrossi Dec 6, 2025
2be4a04
fix event targets
danrossi Dec 6, 2025
419b7df
fix grip model config
danrossi Dec 7, 2025
21c6f8a
add option to configure the grip cursor radius. The default is small.
danrossi Dec 7, 2025
3a20c5a
Add missing cursorRadius config
danrossi Dec 7, 2025
82162b3
transient-pointer intersection fixes.
danrossi Dec 8, 2025
5ffb4b5
Merge branch 'mrdoob:dev' into xr-controller-gamepad
danrossi Dec 12, 2025
ce54ed9
Merge branch 'mrdoob:dev' into xr-controller-gamepad
danrossi Dec 20, 2025
04c4d08
Merge branch 'mrdoob:dev' into xr-controller-gamepad
danrossi Dec 22, 2025
8e4873c
Merge branch 'mrdoob:dev' into xr-controller-gamepad
danrossi Jan 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,9 @@
"webgpu_water",
"webgpu_xr_rollercoaster",
"webgpu_xr_cubes",
"webgpu_xr_native_layers"
"webgpu_xr_native_layers",
"webgpu_xr_gamepad",
"webgpu_xr_controller_manager"
],
"webaudio": [
"webaudio_orientation",
Expand Down
243 changes: 243 additions & 0 deletions examples/jsm/webxr/GazePointerModel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import {
Object3D,
Vector3,
SphereGeometry,
Mesh,
Raycaster,
MeshBasicMaterial,
Color
} from 'three';


const POINTER_COLOR = 0xffffff;

const CURSOR_RADIUS = 0.02;
const CURSOR_MAX_DISTANCE = 1.5;

/**
* Represents a Gaze pointer model.
*
* @augments Object3D
* @three_import import { GazePointerModel } from 'three/addons/webxr/GazePointerModel.js';
*/
class GazePointerModel extends Object3D {

/**
* Constructs a new Gaze pointer model.
*
* @param {Group} controller - The WebXR controller in target ray space.
*/
constructor( controller ) {

super();

/**
* The WebXR controller in target ray space.
*
* @type {Group}
*/
this.controller = controller;

/**
* The pointer object that holds the pointer mesh.
*
* @type {?Object3D}
* @default null
*/
this.pointerObject = null;

/**
* The cursor object.
*
* @type {?Mesh}
* @default null
*/
this.cursorObject = null;

/**
* The internal raycaster used for detecting
* intersections.
*
* @type {?Raycaster}
* @default null
*/
this.raycaster = null;

this._onConnected = this._onConnected.bind( this );
this._onDisconnected = this._onDisconnected.bind( this );
this.controller.addEventListener( 'connected', this._onConnected );
this.controller.addEventListener( 'disconnected', this._onDisconnected );

this.createPointer();

}

/**
* Set the cursor color.
*
* @param {number} color - The color.
*/
set cursorColor( color ) {

if ( this.cursorObject ) {

this.cursorObject.material.color = new Color( color );

}

}

_onConnected( event ) {

const xrInputSource = event.data;
if ( ! xrInputSource.hand ) {

this.visible = true;
this.xrInputSource = xrInputSource;

this.createPointer();

}


}

_onDisconnected() {

this.visible = false;
this.xrInputSource = null;

this.clear();

}

/**
* Creates a pointer mesh and adds it to this model.
*/
createPointer() {

this.pointerObject = new Object3D();

this.raycaster = new Raycaster();


// create cursor
const cursorGeometry = new SphereGeometry( CURSOR_RADIUS, 10, 10 );
const cursorMaterial = new MeshBasicMaterial( { color: POINTER_COLOR, opacity: 1, transparent: true, depthTest: false } );

this.cursorObject = new Mesh( cursorGeometry, cursorMaterial );
this.pointerObject.add( this.cursorObject );

this.add( this.pointerObject );

}



/**
* Performs an intersection test with the model's raycaster and the given object.
*
* @param {Object3D} object - The 3D object to check for intersection with the ray.
* @param {boolean} [recursive=true] - If set to `true`, it also checks all descendants.
* Otherwise it only checks intersection with the object.
* @return {Array<Raycaster~Intersection>} An array holding the intersection points.
*/
intersectObject( object, recursive = true ) {

if ( this.raycaster ) {

this.controller.updateMatrixWorld();
this.raycaster.setFromXRController( this.controller );

return this.raycaster.intersectObject( object, recursive );

}

}

/**
* Performs an intersection test with the model's raycaster and the given objects.
*
* @param {Array<Object3D>} objects - The 3D objects to check for intersection with the ray.
* @param {boolean} [recursive=true] - If set to `true`, it also checks all descendants.
* Otherwise it only checks intersection with the object.
* @return {Array<Raycaster~Intersection>} An array holding the intersection points.
*/
intersectObjects( objects, recursive = true ) {

if ( this.raycaster ) {

this.controller.updateMatrixWorld();
this.raycaster.setFromXRController( this.controller );

return this.raycaster.intersectObjects( objects, recursive );

}

}

/**
* Checks for intersections between the model's raycaster and the given objects. The method
* updates the cursor object to the intersection point.
*
* @param {Array<Object3D>} objects - The 3D objects to check for intersection with the ray.
* @param {boolean} [recursive=false] - If set to `true`, it also checks all descendants.
* Otherwise it only checks intersection with the object.
*/
checkIntersections( objects, recursive = false ) {

if ( this.raycaster ) {

this.controller.updateMatrixWorld();
this.raycaster.setFromXRController( this.controller );


const intersections = this.raycaster.intersectObjects( objects, recursive );
const direction = new Vector3( 0, 0, - 1 );
if ( intersections.length > 0 ) {

const intersection = intersections[ 0 ];
const distance = intersection.distance;
this.cursorObject.position.copy( direction.multiplyScalar( distance ) );

} else {

this.cursorObject.position.copy( direction.multiplyScalar( CURSOR_MAX_DISTANCE ) );

}

}

}

/**
* Sets the cursor to the given distance.
*
* @param {number} distance - The distance to set the cursor to.
*/
setCursor( distance ) {

const direction = new Vector3( 0, 0, - 1 );
if ( this.raycaster ) {

this.cursorObject.position.copy( direction.multiplyScalar( distance ) );

}

}

/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever this instance is no longer used in your app.
*/
dispose() {

this._onDisconnected();
this.controller.removeEventListener( 'connected', this._onConnected );
this.controller.removeEventListener( 'disconnected', this._onDisconnected );

}

}

export { GazePointerModel };
Loading
Loading