Przeglądaj źródła

XRManager: Add more integrated support for WebXR Layers. (#30730)

* Add more integrated support for WebXR Layers

* Address review comments of #30730
Rik Cabanier 10 miesięcy temu
rodzic
commit
2b0adf7fa7

+ 2 - 1
examples/files.json

@@ -451,7 +451,8 @@
 		"webgpu_volume_lighting_rectarea",
 		"webgpu_volume_lighting_rectarea",
 		"webgpu_volume_perlin",
 		"webgpu_volume_perlin",
 		"webgpu_water",
 		"webgpu_water",
-		"webgpu_xr_cubes"
+		"webgpu_xr_cubes",
+		"webgpu_xr_native_layers"
 	],
 	],
 	"webaudio": [
 	"webaudio": [
 		"webaudio_orientation",
 		"webaudio_orientation",

BIN
examples/screenshots/webgpu_xr_native_layers.jpg


+ 713 - 0
examples/webgpu_xr_native_layers.html

@@ -0,0 +1,713 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js vr - xr layers</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 - xr layers
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/webgpu": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.tsl.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import { BoxLineGeometry } from 'three/addons/geometries/BoxLineGeometry.js';
+			import { VRButton } from 'three/addons/webxr/VRButton.js';
+			import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js';
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+			import { InteractiveGroup } from 'three/addons/interactive/InteractiveGroup.js';
+			import {
+				RollerCoasterGeometry,
+				RollerCoasterShadowGeometry,
+				RollerCoasterLiftersGeometry,
+				TreesGeometry,
+				SkyGeometry
+			} from 'three/addons/misc/RollerCoaster.js';
+			import { HTMLMesh } from 'three/addons/interactive/HTMLMesh.js';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			let camera, scene, renderer;
+			let controller1, controller2;
+			let controllerGrip1, controllerGrip2;
+
+			let room;
+
+			let count = 0;
+			const radius = 0.08;
+			let normal = new THREE.Vector3();
+			const relativeVelocity = new THREE.Vector3();
+
+			const clock = new THREE.Clock();
+			const funfairs = [];
+			const train = new THREE.Object3D();
+			const rcdelta = clock.getDelta() * 0.8; // slow down simulation
+			const PI2 = Math.PI * 2;
+			let rccamera = null;
+			let rcscene = null;
+
+			const tempMatrix = new THREE.Matrix4();
+			let raycaster = null;
+
+			const curve = ( function () {
+
+				const vector = new THREE.Vector3();
+				const vector2 = new THREE.Vector3();
+
+				return {
+
+					getPointAt: function ( t ) {
+
+						t = t * PI2;
+
+						const x = Math.sin( t * 3 ) * Math.cos( t * 4 ) * 50;
+						const y = Math.sin( t * 10 ) * 2 + Math.cos( t * 17 ) * 2 + 5;
+						const z = Math.sin( t ) * Math.sin( t * 4 ) * 50;
+
+						return vector.set( x, y, z ).multiplyScalar( 2 );
+
+					},
+
+					getTangentAt: function ( t ) {
+
+						const delta = 0.0001;
+						const t1 = Math.max( 0, t - delta );
+						const t2 = Math.min( 1, t + delta );
+
+						return vector2.copy( this.getPointAt( t2 ) )
+							.sub( this.getPointAt( t1 ) ).normalize();
+
+					}
+
+				};
+
+			} )();
+
+			let horseCamera = null;
+			let horseScene = null;
+			let horseMixer = null;
+			let horseTheta = 0;
+			let horseMesh = null;
+			const horseRadius = 600;
+
+			let guiScene = null;
+			let guiCamera = null;
+			let guiGroup = null;
+
+			let rollercoasterLayer = null;
+			let horseLayer = null;
+			let guiLayer = null;
+
+			const parameters = {
+				radius: 0.6,
+				tube: 0.2,
+				tubularSegments: 150,
+				radialSegments: 20,
+				p: 2,
+				q: 3,
+				thickness: 0.5
+			};
+
+			init();
+
+			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( scene.children, false );
+
+			}
+
+			function init() {
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0x505050 );
+
+				raycaster = new THREE.Raycaster();
+
+				camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 10 );
+				camera.position.set( 0, 1.6, 3 );
+
+				room = new THREE.LineSegments(
+					new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ),
+					new THREE.LineBasicMaterial( { color: 0x808080 } )
+				);
+				room.geometry.translate( 0, 3, 0 );
+				scene.add( room );
+
+				scene.add( new THREE.HemisphereLight( 0x606060, 0x404040 ) );
+
+				const light = new THREE.DirectionalLight( 0xffffff );
+				light.position.set( 1, 1, 1 ).normalize();
+				scene.add( light );
+
+				const geometry = new THREE.IcosahedronGeometry( radius, 3 );
+
+				for ( let i = 0; i < 200; i ++ ) {
+
+					const object = new THREE.Mesh( geometry, new THREE.MeshLambertMaterial( { color: Math.random() * 0xffffff } ) );
+
+					object.position.x = Math.random() * 4 - 2;
+					object.position.y = Math.random() * 4;
+					object.position.z = Math.random() * 4 - 2;
+
+					object.userData.velocity = new THREE.Vector3();
+					object.userData.velocity.x = Math.random() * 0.01 - 0.005;
+					object.userData.velocity.y = Math.random() * 0.01 - 0.005;
+					object.userData.velocity.z = Math.random() * 0.01 - 0.005;
+
+					room.add( object );
+
+				}
+
+				//
+
+				renderer = new THREE.WebGPURenderer( { antialias: true, forceWebGL: true, colorBufferType: THREE.UnsignedByteType } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( render );
+				renderer.xr.enabled = true;
+				document.body.appendChild( renderer.domElement );
+
+				//
+
+				document.body.appendChild( VRButton.createButton( renderer ) );
+
+				// controllers
+
+				function onSqueezeStart( ) {
+
+					this.userData.isSelecting = true;
+
+				}
+
+				function onSqueezeEnd() {
+
+					this.userData.isSelecting = false;
+
+				}
+
+				function onSelectStart( event ) {
+
+					const controller = event.target;
+
+					const intersections = getIntersections( controller );
+					let hadSelection = false;
+
+					for ( let x = 0; x < intersections.length; x ++ ) {
+
+						if ( intersections[ x ].object == horseLayer ) {
+
+							horseLayer.visible = false;
+							hadSelection = true;
+
+						}
+
+						if ( intersections[ x ].object == rollercoasterLayer ) {
+
+							controller.attach( rollercoasterLayer );
+							hadSelection = true;
+
+						}
+
+						if ( intersections[ x ].object == guiLayer ) {
+
+							const uv = intersections[ x ].uv;
+							guiGroup.children[ 0 ].dispatchEvent( { type: 'mousedown', data: { x: uv.x, y: 1 - uv.y }, target: guiGroup } );
+							hadSelection = true;
+
+						}
+
+					}
+
+					this.userData.isSelecting = hadSelection === false;
+
+				}
+
+				function onSelectEnd( ) {
+
+					horseLayer.visible = true;
+					scene.attach( rollercoasterLayer );
+					guiGroup.children[ 0 ].dispatchEvent( { type: 'mouseup', data: { x: 0, y: 0 }, target: guiGroup } );
+					this.userData.isSelecting = false;
+
+				}
+
+				controller1 = renderer.xr.getController( 0 );
+				controller1.addEventListener( 'selectstart', onSelectStart );
+				controller1.addEventListener( 'selectend', onSelectEnd );
+				controller1.addEventListener( 'squeezestart', onSqueezeStart );
+				controller1.addEventListener( 'squeezeend', onSqueezeEnd );
+				controller1.addEventListener( 'connected', function ( event ) {
+
+					this.add( buildController( event.data ) );
+
+				} );
+				controller1.addEventListener( 'disconnected', function () {
+
+					this.remove( this.children[ 0 ] );
+
+				} );
+				scene.add( controller1 );
+
+				controller2 = renderer.xr.getController( 1 );
+				controller2.addEventListener( 'selectstart', onSelectStart );
+				controller2.addEventListener( 'selectend', onSelectEnd );
+				controller2.addEventListener( 'squeezestart', onSqueezeStart );
+				controller2.addEventListener( 'squeezeend', onSqueezeEnd );
+				controller2.addEventListener( 'connected', function ( event ) {
+
+					this.add( buildController( event.data ) );
+
+				} );
+				controller2.addEventListener( 'disconnected', function () {
+
+					this.remove( this.children[ 0 ] );
+
+				} );
+				scene.add( controller2 );
+
+				// The XRControllerModelFactory will automatically fetch controller models
+				// that match what the user is holding as closely as possible. The models
+				// should be attached to the object returned from getControllerGrip in
+				// order to match the orientation of the held device.
+
+				const 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 );
+
+				//
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				// set up rollercoaster
+				rollercoasterLayer = renderer.xr.createCylinderLayer( 1, Math.PI / 2, 2, new THREE.Vector3( 0, 1.5, - 0.5 ), new THREE.Quaternion(), 1024, 1024, renderRollercoaster );
+				scene.add( rollercoasterLayer );
+
+				rcscene = new THREE.Scene();
+				rcscene.background = new THREE.Color( 0xf0f0ff );
+
+				const rclight = new THREE.HemisphereLight( 0xfff0f0, 0x606066 );
+				rclight.position.set( 1, 1, 1 );
+				rcscene.add( rclight );
+
+				rcscene.add( train );
+
+				rccamera = new THREE.PerspectiveCamera( 50, 1, 0.1, 500 );
+				train.add( rccamera );
+
+				// environment
+
+				let rcgeometry = new THREE.PlaneGeometry( 500, 500, 15, 15 );
+				rcgeometry.rotateX( - Math.PI / 2 );
+
+				const positions = rcgeometry.attributes.position.array;
+				const vertex = new THREE.Vector3();
+
+				for ( let i = 0; i < positions.length; i += 3 ) {
+
+					vertex.fromArray( positions, i );
+
+					vertex.x += Math.random() * 10 - 5;
+					vertex.z += Math.random() * 10 - 5;
+
+					const distance = ( vertex.distanceTo( scene.position ) / 5 ) - 25;
+					vertex.y = Math.random() * Math.max( 0, distance );
+
+					vertex.toArray( positions, i );
+
+				}
+
+				rcgeometry.computeVertexNormals();
+
+				let rcmaterial = new THREE.MeshLambertMaterial( {
+					color: 0x407000
+				} );
+
+				let rcmesh = new THREE.Mesh( rcgeometry, rcmaterial );
+				rcscene.add( rcmesh );
+
+				rcgeometry = new TreesGeometry( rcmesh );
+				rcmaterial = new THREE.MeshBasicMaterial( {
+					side: THREE.DoubleSide, vertexColors: true
+				} );
+				rcmesh = new THREE.Mesh( rcgeometry, rcmaterial );
+				rcscene.add( rcmesh );
+
+				rcgeometry = new SkyGeometry();
+				rcmaterial = new THREE.MeshBasicMaterial( { color: 0xffffff } );
+				rcmesh = new THREE.Mesh( rcgeometry, rcmaterial );
+				rcscene.add( rcmesh );
+
+				//
+
+				rcgeometry = new RollerCoasterGeometry( curve, 1500 );
+				rcmaterial = new THREE.MeshPhongMaterial( {
+					vertexColors: true
+				} );
+				rcmesh = new THREE.Mesh( rcgeometry, rcmaterial );
+				rcscene.add( rcmesh );
+
+				rcgeometry = new RollerCoasterLiftersGeometry( curve, 100 );
+				rcmaterial = new THREE.MeshPhongMaterial();
+				rcmesh = new THREE.Mesh( rcgeometry, rcmaterial );
+				rcmesh.position.y = 0.1;
+				rcscene.add( rcmesh );
+
+				rcgeometry = new RollerCoasterShadowGeometry( curve, 500 );
+				rcmaterial = new THREE.MeshBasicMaterial( {
+					color: 0x305000, depthWrite: false, transparent: true
+				} );
+				rcmesh = new THREE.Mesh( rcgeometry, rcmaterial );
+				rcmesh.position.y = 0.1;
+				rcscene.add( rcmesh );
+
+				//
+
+				rcgeometry = new THREE.CylinderGeometry( 10, 10, 5, 15 );
+				rcmaterial = new THREE.MeshLambertMaterial( {
+					color: 0xff8080
+				} );
+				rcmesh = new THREE.Mesh( rcgeometry, rcmaterial );
+				rcmesh.position.set( - 80, 10, - 70 );
+				rcmesh.rotation.x = Math.PI / 2;
+				rcscene.add( rcmesh );
+
+				funfairs.push( rcmesh );
+
+				rcgeometry = new THREE.CylinderGeometry( 5, 6, 4, 10 );
+				rcmaterial = new THREE.MeshLambertMaterial( {
+					color: 0x8080ff
+				} );
+				rcmesh = new THREE.Mesh( rcgeometry, rcmaterial );
+				rcmesh.position.set( 50, 2, 30 );
+				rcscene.add( rcmesh );
+
+				funfairs.push( rcmesh );
+
+				// set up horse animation
+				horseLayer = renderer.xr.createQuadLayer( 1, 1, new THREE.Vector3( - 1.5, 1.5, - 1.5 ), new THREE.Quaternion(), 1024, 1024, renderQuad );
+				scene.add( horseLayer );
+
+				horseLayer.geometry = new THREE.CircleGeometry( .5, 64 );
+
+				horseCamera = new THREE.PerspectiveCamera( 50, 1, 1, 10000 );
+				horseCamera.position.y = 300;
+
+				horseScene = new THREE.Scene();
+				horseScene.background = new THREE.Color( 0xf0f0f0 );
+
+				//
+
+				const light1 = new THREE.DirectionalLight( 0xefefff, 1.5 );
+				light1.position.set( 1, 1, 1 ).normalize();
+				horseScene.add( light1 );
+
+				const light2 = new THREE.DirectionalLight( 0xffefef, 1.5 );
+				light2.position.set( - 1, - 1, - 1 ).normalize();
+				horseScene.add( light2 );
+
+				const loader = new GLTFLoader();
+				loader.load( 'models/gltf/Horse.glb', function ( gltf ) {
+
+					horseMesh = gltf.scene.children[ 0 ];
+					horseMesh.scale.set( 1.5, 1.5, 1.5 );
+					horseScene.add( horseMesh );
+
+					horseMixer = new THREE.AnimationMixer( horseMesh );
+
+					horseMixer.clipAction( gltf.animations[ 0 ] ).setDuration( 1 ).play();
+
+				} );
+
+				function onChange() { }
+
+				function onThicknessChange() { }
+
+				// set up ui
+				guiScene = new THREE.Scene();
+				guiScene.background = new THREE.Color( 0x0 );
+
+				guiCamera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
+				guiScene.add( guiCamera );
+
+				const gui = new GUI( { width: 300 } );
+				gui.add( parameters, 'radius', 0.0, 1.0 ).onChange( onChange );
+				gui.add( parameters, 'tube', 0.0, 1.0 ).onChange( onChange );
+				gui.add( parameters, 'tubularSegments', 10, 150, 1 ).onChange( onChange );
+				gui.add( parameters, 'radialSegments', 2, 20, 1 ).onChange( onChange );
+				gui.add( parameters, 'p', 1, 10, 1 ).onChange( onChange );
+				gui.add( parameters, 'q', 0, 10, 1 ).onChange( onChange );
+				gui.add( parameters, 'thickness', 0, 1 ).onChange( onThicknessChange );
+				gui.domElement.style.visibility = 'hidden';
+
+				guiGroup = new InteractiveGroup( renderer, guiCamera );
+				guiScene.add( guiGroup );
+
+				const mesh = new HTMLMesh( gui.domElement );
+				guiGroup.add( mesh );
+
+				const bbox = new THREE.Box3().setFromObject( guiScene );
+
+				guiLayer = renderer.xr.createQuadLayer( 1.2, .8, new THREE.Vector3( 1.5, 1.5, - 1.5 ), new THREE.Quaternion(), 1024, 1024, renderGui );
+				scene.add( guiLayer );
+
+				guiCamera.left = bbox.min.x;
+				guiCamera.right = bbox.max.x;
+				guiCamera.top = bbox.max.y;
+				guiCamera.bottom = bbox.min.y;
+				guiCamera.updateProjectionMatrix();
+			}
+
+			function renderGui() {
+
+				renderer.render( guiScene, guiCamera );
+
+			}
+
+			function renderQuad() {
+
+				horseTheta += 0.1;
+
+				horseCamera.position.x = horseRadius * Math.sin( THREE.MathUtils.degToRad( horseTheta ) );
+				horseCamera.position.z = horseRadius * Math.cos( THREE.MathUtils.degToRad( horseTheta ) );
+
+				horseCamera.lookAt( 0, 150, 0 );
+
+				if ( horseMixer ) {
+
+					const time = Date.now();
+
+					horseMixer.update( ( time - prevTime ) * 0.001 );
+
+					prevTime = time;
+
+				}
+
+				renderer.render( horseScene, horseCamera );
+
+			}
+
+			const rcposition = new THREE.Vector3();
+			const tangent = new THREE.Vector3();
+
+			const lookAt = new THREE.Vector3();
+
+			let rcvelocity = 0;
+			let progress = 0;
+
+			let prevTime = performance.now();
+
+			function renderRollercoaster() {
+
+				const time = performance.now();
+				for ( let i = 0; i < funfairs.length; i ++ ) {
+
+					funfairs[ i ].rotation.y = time * 0.0004;
+
+				}
+
+				//
+
+				progress += rcvelocity;
+				progress = progress % 1;
+
+				rcposition.copy( curve.getPointAt( progress ) );
+				rcposition.y += 0.3;
+
+				train.position.copy( rcposition );
+
+				tangent.copy( curve.getTangentAt( progress ) );
+
+				rcvelocity -= tangent.y * 0.0000001 * rcdelta;
+				rcvelocity = Math.max( 0.00004, Math.min( 0.0002, rcvelocity ) );
+
+				train.lookAt( lookAt.copy( rcposition ).sub( tangent ) );
+
+				//
+
+				renderer.render( rcscene, rccamera );
+
+			}
+
+			function buildController( data ) {
+
+				let geometry, material;
+
+				switch ( data.targetRayMode ) {
+
+					case 'tracked-pointer':
+
+						geometry = new THREE.BufferGeometry();
+						geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( [ 0, 0, 0, 0, 0, - 1 ], 3 ) );
+						geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( [ 0.5, 0.5, 0.5, 0, 0, 0 ], 3 ) );
+
+						material = new THREE.LineBasicMaterial( { vertexColors: true, blending: THREE.AdditiveBlending } );
+
+						return new THREE.Line( geometry, material );
+
+					case 'gaze':
+
+						geometry = new THREE.RingGeometry( 0.02, 0.04, 32 ).translate( 0, 0, - 1 );
+						material = new THREE.MeshBasicMaterial( { opacity: 0.5, transparent: true } );
+						return new THREE.Mesh( geometry, material );
+
+				}
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function handleController( controller ) {
+
+				if ( controller.userData.isSelecting ) {
+
+					const object = room.children[ count ++ ];
+
+					object.position.copy( controller.position );
+					object.userData.velocity.x = ( Math.random() - 0.5 ) * 3;
+					object.userData.velocity.y = ( Math.random() - 0.5 ) * 3;
+					object.userData.velocity.z = ( Math.random() - 9 );
+					object.userData.velocity.applyQuaternion( controller.quaternion );
+
+					if ( count === room.children.length ) count = 0;
+
+				}
+
+				const intersections = getIntersections( controller );
+				for ( let x = 0; x < intersections.length; x ++ ) {
+
+					if ( intersections[ x ].object == guiLayer ) {
+
+						const uv = intersections[ x ].uv;
+						guiGroup.children[ 0 ].dispatchEvent( { type: 'mousemove', data: { x: uv.x, y: 1 - uv.y }, target: guiGroup } );
+
+					}
+
+
+				}
+
+
+			}
+
+			//
+
+			function render() {
+
+				renderer.xr.renderLayers( );
+
+				handleController( controller1 );
+				handleController( controller2 );
+
+				// rotate horse
+				horseLayer.rotation.y -= 0.02;
+
+				//
+				const delta = clock.getDelta() * 0.8;
+
+				const range = 3 - radius;
+
+				for ( let i = 0; i < room.children.length; i ++ ) {
+
+					const object = room.children[ i ];
+
+					object.position.x += object.userData.velocity.x * delta;
+					object.position.y += object.userData.velocity.y * delta;
+					object.position.z += object.userData.velocity.z * delta;
+
+					// keep objects inside room
+
+					if ( object.position.x < - range || object.position.x > range ) {
+
+						object.position.x = THREE.MathUtils.clamp( object.position.x, - range, range );
+						object.userData.velocity.x = - object.userData.velocity.x;
+
+					}
+
+					if ( object.position.y < radius || object.position.y > 6 ) {
+
+						object.position.y = Math.max( object.position.y, radius );
+
+						object.userData.velocity.x *= 0.98;
+						object.userData.velocity.y = - object.userData.velocity.y * 0.8;
+						object.userData.velocity.z *= 0.98;
+
+					}
+
+					if ( object.position.z < - range || object.position.z > range ) {
+
+						object.position.z = THREE.MathUtils.clamp( object.position.z, - range, range );
+						object.userData.velocity.z = - object.userData.velocity.z;
+
+					}
+
+					for ( let j = i + 1; j < room.children.length; j ++ ) {
+
+						const object2 = room.children[ j ];
+
+						normal.copy( object.position ).sub( object2.position );
+
+						const distance = normal.length();
+
+						if ( distance < 2 * radius ) {
+
+							normal.multiplyScalar( 0.5 * distance - radius );
+
+							object.position.sub( normal );
+							object2.position.add( normal );
+
+							normal.normalize();
+
+							relativeVelocity.copy( object.userData.velocity ).sub( object2.userData.velocity );
+
+							normal = normal.multiplyScalar( relativeVelocity.dot( normal ) );
+
+							object.userData.velocity.sub( normal );
+							object2.userData.velocity.add( normal );
+
+						}
+
+					}
+
+					object.userData.velocity.y -= 9.8 * delta;
+
+				}
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 326 - 6
src/renderers/common/XRManager.js

@@ -1,14 +1,19 @@
 import { ArrayCamera } from '../../cameras/ArrayCamera.js';
 import { ArrayCamera } from '../../cameras/ArrayCamera.js';
 import { EventDispatcher } from '../../core/EventDispatcher.js';
 import { EventDispatcher } from '../../core/EventDispatcher.js';
 import { PerspectiveCamera } from '../../cameras/PerspectiveCamera.js';
 import { PerspectiveCamera } from '../../cameras/PerspectiveCamera.js';
+import { Quaternion } from '../../math/Quaternion.js';
 import { RAD2DEG } from '../../math/MathUtils.js';
 import { RAD2DEG } from '../../math/MathUtils.js';
 import { Vector2 } from '../../math/Vector2.js';
 import { Vector2 } from '../../math/Vector2.js';
 import { Vector3 } from '../../math/Vector3.js';
 import { Vector3 } from '../../math/Vector3.js';
 import { Vector4 } from '../../math/Vector4.js';
 import { Vector4 } from '../../math/Vector4.js';
 import { WebXRController } from '../webxr/WebXRController.js';
 import { WebXRController } from '../webxr/WebXRController.js';
-import { DepthFormat, DepthStencilFormat, RGBAFormat, UnsignedByteType, UnsignedInt248Type, UnsignedIntType } from '../../constants.js';
+import { AddEquation, BackSide, CustomBlending, DepthFormat, DepthStencilFormat, FrontSide, RGBAFormat, UnsignedByteType, UnsignedInt248Type, UnsignedIntType, ZeroFactor } from '../../constants.js';
 import { DepthTexture } from '../../textures/DepthTexture.js';
 import { DepthTexture } from '../../textures/DepthTexture.js';
 import { XRRenderTarget } from './XRRenderTarget.js';
 import { XRRenderTarget } from './XRRenderTarget.js';
+import { CylinderGeometry } from '../../geometries/CylinderGeometry.js';
+import { PlaneGeometry } from '../../geometries/PlaneGeometry.js';
+import { MeshBasicMaterial } from '../../materials/MeshBasicMaterial.js';
+import { Mesh } from '../../objects/Mesh.js';
 
 
 const _cameraLPos = /*@__PURE__*/ new Vector3();
 const _cameraLPos = /*@__PURE__*/ new Vector3();
 const _cameraRPos = /*@__PURE__*/ new Vector3();
 const _cameraRPos = /*@__PURE__*/ new Vector3();
@@ -146,6 +151,40 @@ class XRManager extends EventDispatcher {
 		 */
 		 */
 		this._xrRenderTarget = null;
 		this._xrRenderTarget = null;
 
 
+		/**
+		 * An array holding all the non-projection layers
+		 *
+		 * @private
+		 * @type {Array<Object>}
+		 * @default []
+		 */
+		this._layers = [];
+
+		/**
+		 * Whether the device has support for all layer types.
+		 *
+		 * @type {boolean}
+		 * @default false
+		 */
+		this._supportsLayers = false;
+
+		/**
+		 * Helper function to create native WebXR Layer.
+		 *
+		 * @private
+		 * @type {Function}
+		 */
+		this._createXRLayer = createXRLayer.bind( this );
+
+		/**
+		* The current WebGL context.
+		*
+		* @private
+		* @type {?WebGL2RenderingContext}
+		* @default null
+		*/
+		this._gl = null;
+
 		/**
 		/**
 		 * The current animation context.
 		 * The current animation context.
 		 *
 		 *
@@ -525,6 +564,190 @@ class XRManager extends EventDispatcher {
 
 
 	}
 	}
 
 
+	createQuadLayer( width, height, translation, quaternion, pixelwidth, pixelheight, rendercall, attributes = [] ) {
+
+		const geometry = new PlaneGeometry( width, height );
+		const renderTarget = new XRRenderTarget(
+			pixelwidth,
+			pixelheight,
+			{
+				format: RGBAFormat,
+				type: UnsignedByteType,
+				depthTexture: new DepthTexture(
+					pixelwidth,
+					pixelheight,
+					attributes.stencil ? UnsignedInt248Type : UnsignedIntType,
+					undefined,
+					undefined,
+					undefined,
+					undefined,
+					undefined,
+					undefined,
+					attributes.stencil ? DepthStencilFormat : DepthFormat
+				),
+				stencilBuffer: attributes.stencil,
+				resolveDepthBuffer: false,
+				resolveStencilBuffer: false
+			} );
+
+		const material = new MeshBasicMaterial( { color: 0xffffff, side: FrontSide } );
+		material.map = renderTarget.texture;
+		material.map.offset.y = 1;
+		material.map.repeat.y = - 1;
+		const plane = new Mesh( geometry, material );
+		plane.position.copy( translation );
+		plane.quaternion.copy( quaternion );
+
+		const layer = {
+			type: 'quad',
+			width: width,
+			height: height,
+			translation: translation,
+			quaternion: quaternion,
+			pixelwidth: pixelwidth,
+			pixelheight: pixelheight,
+			plane: plane,
+			material: material,
+			rendercall: rendercall,
+			renderTarget: renderTarget };
+
+		this._layers.push( layer );
+
+		if ( this._session !== null ) {
+
+			layer.plane.material = new MeshBasicMaterial( { color: 0xffffff, side: FrontSide } );
+			layer.plane.material.blending = CustomBlending;
+			layer.plane.material.blendEquation = AddEquation;
+			layer.plane.material.blendSrc = ZeroFactor;
+			layer.plane.material.blendDst = ZeroFactor;
+
+			layer.xrlayer = this._createXRLayer( layer );
+
+			const xrlayers = this._session.renderState.layers;
+			xrlayers.unshift( layer.xrlayer );
+			this._session.updateRenderState( { layers: xrlayers } );
+
+		} else {
+
+			renderTarget.isXRRenderTarget = false;
+
+		}
+
+		return plane;
+
+	}
+
+	createCylinderLayer( radius, centralAngle, aspectratio, translation, quaternion, pixelwidth, pixelheight, rendercall, attributes = [] ) {
+
+		const geometry = new CylinderGeometry( radius, radius, radius * centralAngle / aspectratio, 64, 64, true, Math.PI - centralAngle / 2, centralAngle );
+		const renderTarget = new XRRenderTarget(
+			pixelwidth,
+			pixelheight,
+			{
+				format: RGBAFormat,
+				type: UnsignedByteType,
+				depthTexture: new DepthTexture(
+					pixelwidth,
+					pixelheight,
+					attributes.stencil ? UnsignedInt248Type : UnsignedIntType,
+					undefined,
+					undefined,
+					undefined,
+					undefined,
+					undefined,
+					undefined,
+					attributes.stencil ? DepthStencilFormat : DepthFormat
+				),
+				stencilBuffer: attributes.stencil,
+				resolveDepthBuffer: false,
+				resolveStencilBuffer: false
+			} );
+
+		const material = new MeshBasicMaterial( { color: 0xffffff, side: BackSide } );
+		material.map = renderTarget.texture;
+		material.map.offset.y = 1;
+		material.map.repeat.y = - 1;
+		const plane = new Mesh( geometry, material );
+		plane.position.copy( translation );
+		plane.quaternion.copy( quaternion );
+
+		const layer = {
+			type: 'cylinder',
+			radius: radius,
+			centralAngle: centralAngle,
+			aspectratio: aspectratio,
+			translation: translation,
+			quaternion: quaternion,
+			pixelwidth: pixelwidth,
+			pixelheight: pixelheight,
+			plane: plane,
+			material: material,
+			rendercall: rendercall,
+			renderTarget: renderTarget };
+
+		this._layers.push( layer );
+
+		if ( this._session !== null ) {
+
+			layer.plane.material = new MeshBasicMaterial( { color: 0xffffff, side: BackSide } );
+			layer.plane.material.blending = CustomBlending;
+			layer.plane.material.blendEquation = AddEquation;
+			layer.plane.material.blendSrc = ZeroFactor;
+			layer.plane.material.blendDst = ZeroFactor;
+
+			layer.xrlayer = this._createXRLayer( layer );
+
+			const xrlayers = this._session.renderState.layers;
+			xrlayers.unshift( layer.xrlayer );
+			this._session.updateRenderState( { layers: xrlayers } );
+
+		} else {
+
+			renderTarget.isXRRenderTarget = false;
+
+		}
+
+		return plane;
+
+	}
+
+	renderLayers( ) {
+
+		const translationObject = new Vector3();
+		const quaternionObject = new Quaternion();
+
+		const wasPresenting = this.isPresenting;
+		this.isPresenting = false;
+
+		for ( const layer of this._layers ) {
+
+			layer.renderTarget.isXRRenderTarget = this._session !== null;
+			layer.renderTarget.hasExternalTextures = layer.renderTarget.isXRRenderTarget;
+			layer.renderTarget.autoAllocateDepthBuffer = ! layer.renderTarget.isXRRenderTarget;
+
+			if ( layer.renderTarget.isXRRenderTarget && this._supportsLayers ) {
+
+				layer.xrlayer.transform = new XRRigidTransform( layer.plane.getWorldPosition( translationObject ), layer.plane.getWorldQuaternion( quaternionObject ) );
+
+				const glSubImage = this._glBinding.getSubImage( layer.xrlayer, this._xrFrame );
+				this._renderer.backend.setXRRenderTargetTextures(
+					layer.renderTarget,
+					glSubImage.colorTexture,
+					glSubImage.depthStencilTexture );
+
+			}
+
+			this._renderer.setRenderTarget( layer.renderTarget );
+			layer.rendercall();
+
+		}
+
+		this.isPresenting = wasPresenting;
+		this._renderer.setRenderTarget( null );
+
+	}
+
+
 	/**
 	/**
 	 * Returns the current XR session.
 	 * Returns the current XR session.
 	 *
 	 *
@@ -550,7 +773,8 @@ class XRManager extends EventDispatcher {
 		const renderer = this._renderer;
 		const renderer = this._renderer;
 		const backend = renderer.backend;
 		const backend = renderer.backend;
 
 
-		const gl = renderer.getContext();
+		this._gl = renderer.getContext();
+		const gl = this._gl;
 		const attributes = gl.getContextAttributes();
 		const attributes = gl.getContextAttributes();
 
 
 		this._session = session;
 		this._session = session;
@@ -603,12 +827,11 @@ class XRManager extends EventDispatcher {
 
 
 				const glBinding = new XRWebGLBinding( session, gl );
 				const glBinding = new XRWebGLBinding( session, gl );
 				const glProjLayer = glBinding.createProjectionLayer( projectionlayerInit );
 				const glProjLayer = glBinding.createProjectionLayer( projectionlayerInit );
+				const layersArray = [ glProjLayer ];
 
 
 				this._glBinding = glBinding;
 				this._glBinding = glBinding;
 				this._glProjLayer = glProjLayer;
 				this._glProjLayer = glProjLayer;
 
 
-				session.updateRenderState( { layers: [ glProjLayer ] } );
-
 				renderer.setPixelRatio( 1 );
 				renderer.setPixelRatio( 1 );
 				renderer.setSize( glProjLayer.textureWidth, glProjLayer.textureHeight, false );
 				renderer.setSize( glProjLayer.textureWidth, glProjLayer.textureHeight, false );
 
 
@@ -628,6 +851,32 @@ class XRManager extends EventDispatcher {
 
 
 				this._xrRenderTarget.hasExternalTextures = true;
 				this._xrRenderTarget.hasExternalTextures = true;
 
 
+				this._referenceSpace = await session.requestReferenceSpace( this.getReferenceSpaceType() );
+
+				this._supportsLayers = session.enabledFeatures.includes( 'layers' );
+
+				if ( this._supportsLayers ) {
+
+					// switch layers to native
+					for ( const layer of this._layers ) {
+
+						// change material so it "punches" out a hole to show the XR Layer.
+						layer.plane.material = new MeshBasicMaterial( { color: 0xffffff, side: layer.type === 'cylinder' ? BackSide : FrontSide } );
+						layer.plane.material.blending = CustomBlending;
+						layer.plane.material.blendEquation = AddEquation;
+						layer.plane.material.blendSrc = ZeroFactor;
+						layer.plane.material.blendDst = ZeroFactor;
+
+						layer.xrlayer = this._createXRLayer( layer );
+
+						layersArray.unshift( layer.xrlayer );
+
+					}
+
+				}
+
+				session.updateRenderState( { layers: layersArray } );
+
 			} else {
 			} else {
 
 
 				// fallback to XRWebGLLayer
 				// fallback to XRWebGLLayer
@@ -667,8 +916,6 @@ class XRManager extends EventDispatcher {
 
 
 			this.setFoveation( this.getFoveation() );
 			this.setFoveation( this.getFoveation() );
 
 
-			this._referenceSpace = await session.requestReferenceSpace( this.getReferenceSpaceType() );
-
 			renderer._animation.setAnimationLoop( this._onAnimationFrame );
 			renderer._animation.setAnimationLoop( this._onAnimationFrame );
 			renderer._animation.setContext( session );
 			renderer._animation.setContext( session );
 			renderer._animation.start();
 			renderer._animation.start();
@@ -972,6 +1219,45 @@ function onSessionEnd() {
 	this._session = null;
 	this._session = null;
 	this._xrRenderTarget = null;
 	this._xrRenderTarget = null;
 
 
+	// switch layers back to emulated
+	if ( this._supportsLayers === true ) {
+
+		for ( const layer of this._layers ) {
+
+			// Recreate layer render target to reset state
+			layer.renderTarget = new XRRenderTarget(
+				layer.pixelwidth,
+				layer.pixelheight,
+				{
+					format: RGBAFormat,
+					type: UnsignedByteType,
+					depthTexture: new DepthTexture(
+						layer.pixelwidth,
+						layer.pixelheight,
+						layer.stencilBuffer ? UnsignedInt248Type : UnsignedIntType,
+						undefined,
+						undefined,
+						undefined,
+						undefined,
+						undefined,
+						undefined,
+						layer.stencilBuffer ? DepthStencilFormat : DepthFormat
+					),
+					stencilBuffer: layer.stencilBuffer,
+					resolveDepthBuffer: false,
+					resolveStencilBuffer: false
+				} );
+
+			layer.renderTarget.isXRRenderTarget = false;
+
+			layer.plane.material = layer.material;
+			layer.material.map = layer.renderTarget.texture;
+			delete layer.xrlayer;
+
+		}
+
+	}
+
 	//
 	//
 
 
 	this.isPresenting = false;
 	this.isPresenting = false;
@@ -1058,6 +1344,40 @@ function onInputSourcesChange( event ) {
 
 
 }
 }
 
 
+// Creation method for native WebXR layers
+function createXRLayer( layer ) {
+
+	if ( layer.type === 'quad' ) {
+
+		return this._glBinding.createQuadLayer( {
+			transform: new XRRigidTransform( layer.translation, layer.quaternion ),
+			depthFormat: this._gl.DEPTH_COMPONENT,
+			width: layer.width / 2,
+			height: layer.height / 2,
+			space: this._referenceSpace,
+			viewPixelWidth: layer.pixelwidth,
+			viewPixelHeight: layer.pixelheight
+		} );
+
+	} else {
+
+		return this._glBinding.createCylinderLayer( {
+			transform: new XRRigidTransform( layer.translation, layer.quaternion ),
+			depthFormat: this._gl.DEPTH_COMPONENT,
+			radius: layer.radius,
+			centralAngle: layer.centralAngle,
+			aspectRatio: layer.aspectRatio,
+			space: this._referenceSpace,
+			viewPixelWidth: layer.pixelwidth,
+			viewPixelHeight: layer.pixelheight
+		} );
+
+	}
+
+}
+
+// Animation Loop
+
 function onAnimationFrame( time, frame ) {
 function onAnimationFrame( time, frame ) {
 
 
 	if ( frame === undefined ) return;
 	if ( frame === undefined ) return;

+ 3 - 3
src/renderers/webgl-fallback/WebGLBackend.js

@@ -358,16 +358,16 @@ class WebGLBackend extends Backend {
 
 
 			this.set( renderTarget.depthTexture, { textureGPU: depthTexture, glInternalFormat: glInternalFormat } );
 			this.set( renderTarget.depthTexture, { textureGPU: depthTexture, glInternalFormat: glInternalFormat } );
 
 
-			renderTarget.autoAllocateDepthBuffer = false;
-
 			// The multisample_render_to_texture extension doesn't work properly if there
 			// The multisample_render_to_texture extension doesn't work properly if there
 			// are midframe flushes and an external depth texture.
 			// are midframe flushes and an external depth texture.
-			if ( this.extensions.has( 'WEBGL_multisampled_render_to_texture' ) === true ) {
+			if ( ( this.extensions.has( 'WEBGL_multisampled_render_to_texture' ) === true ) && renderTarget.autoAllocateDepthBuffer ) {
 
 
 				console.warn( 'THREE.WebGLBackend: Render-to-texture extension was disabled because an external texture was provided' );
 				console.warn( 'THREE.WebGLBackend: Render-to-texture extension was disabled because an external texture was provided' );
 
 
 			}
 			}
 
 
+			renderTarget.autoAllocateDepthBuffer = false;
+
 		}
 		}
 
 
 	}
 	}

+ 1 - 0
test/e2e/puppeteer.js

@@ -160,6 +160,7 @@ const exceptionList = [
 	'webgpu_postprocessing_sobel',
 	'webgpu_postprocessing_sobel',
 	'webgpu_postprocessing_3dlut',
 	'webgpu_postprocessing_3dlut',
 	'webgpu_postprocessing_afterimage',
 	'webgpu_postprocessing_afterimage',
+	'webgpu_xr_native_layers',
 
 
 	// WebGPU idleTime and parseTime too low
 	// WebGPU idleTime and parseTime too low
 	'webgpu_compute_particles',
 	'webgpu_compute_particles',

粤ICP备19079148号