Kaynağa Gözat

Add an example that uses WebXR's estimated AR lighting in feature. (#20876)

* Add an example that uses WebXR's estimated AR lighting in feature.

* Fix optional features being dropped accidentally

* Added ability to turn of environment map estimation
Brandon Jones 4 yıl önce
ebeveyn
işleme
a9112d54c3

+ 1 - 0
examples/files.json

@@ -328,6 +328,7 @@
 	"webxr": [
 		"webxr_ar_cones",
 		"webxr_ar_hittest",
+		"webxr_ar_lighting",
 		"webxr_ar_paint",
 		"webxr_vr_ballshooter",
 		"webxr_vr_cubes",

+ 8 - 2
examples/jsm/webxr/ARButton.js

@@ -31,7 +31,13 @@ class ARButton {
 				path.setAttribute( 'stroke-width', 2 );
 				svg.appendChild( path );
 
-				sessionInit.optionalFeatures = [ 'dom-overlay' ];
+				if ( sessionInit.optionalFeatures === undefined ) {
+
+					sessionInit.optionalFeatures = [];
+
+				}
+
+				sessionInit.optionalFeatures.push( 'dom-overlay' );
 				sessionInit.domOverlay = { root: overlay };
 
 			}
@@ -47,7 +53,7 @@ class ARButton {
 				renderer.xr.setReferenceSpaceType( 'local' );
 
 				await renderer.xr.setSession( session );
-        
+
 				button.textContent = 'STOP AR';
 				sessionInit.domOverlay.root.style.display = '';
 

+ 221 - 0
examples/jsm/webxr/XREstimatedLight.js

@@ -0,0 +1,221 @@
+import {
+	DirectionalLight,
+	Group,
+	LightProbe,
+	WebGLCubeRenderTarget
+} from "../../../build/three.module.js";
+
+class SessionLightProbe {
+
+	constructor( xrLight, renderer, lightProbe, environmentEstimation, estimationStartCallback ) {
+
+		this.xrLight = xrLight;
+		this.renderer = renderer;
+		this.lightProbe = lightProbe;
+		this.xrWebGLBinding = null;
+		this.estimationStartCallback = estimationStartCallback;
+		this.frameCallback = this.onXRFrame.bind( this );
+
+		const session = renderer.xr.getSession();
+
+		// If the XRWebGLBinding class is available then we can also query an
+		// estimated reflection cube map.
+		if ( environmentEstimation && 'XRWebGLBinding' in window ) {
+
+			// This is the simplest way I know of to initialize a WebGL cubemap in Three.
+			const cubeRenderTarget = new WebGLCubeRenderTarget( 16 );
+			xrLight.environment = cubeRenderTarget.texture;
+
+			const gl = renderer.getContext();
+
+			// Ensure that we have any extensions needed to use the preferred cube map format.
+			switch ( session.preferredReflectionFormat ) {
+
+				case 'srgba8':
+					gl.getExtension( 'EXT_sRGB' );
+					break;
+
+				case 'rgba16f':
+					gl.getExtension( 'OES_texture_half_float' );
+					break;
+
+			}
+
+			this.xrWebGLBinding = new XRWebGLBinding( session, gl );
+
+			this.lightProbe.addEventListener('reflectionchange', () => {
+
+				this.updateReflection();
+
+			});
+
+		}
+
+		// Start monitoring the XR animation frame loop to look for lighting
+		// estimation changes.
+		session.requestAnimationFrame( this.frameCallback );
+
+	}
+
+	updateReflection() {
+
+		const textureProperties = this.renderer.properties.get( this.xrLight.environment );
+
+		if ( textureProperties ) {
+
+			const cubeMap = this.xrWebGLBinding.getReflectionCubeMap( this.lightProbe );
+
+			if ( cubeMap ) {
+
+				textureProperties.__webglTexture = cubeMap;
+
+			}
+
+		}
+
+	}
+
+	onXRFrame( time, xrFrame ) {
+
+		// If either this obejct or the XREstimatedLight has been destroyed, stop
+		// running the frame loop.
+		if ( ! this.xrLight ) {
+
+			return;
+
+		}
+
+		const session = xrFrame.session;
+		session.requestAnimationFrame( this.frameCallback );
+
+		const lightEstimate = xrFrame.getLightEstimate( this.lightProbe );
+		if ( lightEstimate ) {
+
+			// We can copy the estimate's spherical harmonics array directly into the light probe.
+			this.xrLight.lightProbe.sh.fromArray( lightEstimate.sphericalHarmonicsCoefficients );
+			this.xrLight.lightProbe.intensity = 1.0;
+
+			// For the directional light we have to normalize the color and set the scalar as the
+			// intensity, since WebXR can return color values that exceed 1.0.
+			const intensityScalar = Math.max( 1.0,
+				Math.max( lightEstimate.primaryLightIntensity.x,
+					Math.max( lightEstimate.primaryLightIntensity.y,
+						lightEstimate.primaryLightIntensity.z ) ) );
+
+			this.xrLight.directionalLight.color.setRGB(
+				lightEstimate.primaryLightIntensity.x / intensityScalar,
+				lightEstimate.primaryLightIntensity.y / intensityScalar,
+				lightEstimate.primaryLightIntensity.z / intensityScalar );
+			this.xrLight.directionalLight.intensity = intensityScalar;
+			this.xrLight.directionalLight.position.copy( lightEstimate.primaryLightDirection );
+
+			if ( this.estimationStartCallback ) {
+
+				this.estimationStartCallback();
+				this.estimationStartCallback = null;
+
+			}
+
+		}
+
+	}
+
+	dispose() {
+
+		this.xrLight = null;
+		this.renderer = null;
+		this.lightProbe = null;
+		this.xrWebGLBinding = null;
+
+	}
+
+}
+
+export class XREstimatedLight extends Group {
+
+	constructor( renderer, environmentEstimation = true ) {
+
+		super();
+
+		this.lightProbe = new LightProbe();
+		this.lightProbe.intensity = 0;
+		this.add( this.lightProbe );
+
+		this.directionalLight = new DirectionalLight();
+		this.directionalLight.intensity = 0;
+		this.add( this.directionalLight );
+
+		// Will be set to a cube map in the SessionLightProbe is environment estimation is
+		// available and requested.
+		this.environment = null;
+
+		let sessionLightProbe = null;
+		let estimationStarted = false;
+		renderer.xr.addEventListener( 'sessionstart', () => {
+
+			const session = renderer.xr.getSession();
+
+			if ( 'requestLightProbe' in session ) {
+
+				session.requestLightProbe( {
+
+					reflectionFormat: session.preferredReflectionFormat
+
+				} ).then( ( probe ) => {
+
+					sessionLightProbe = new SessionLightProbe( this, renderer, probe, environmentEstimation, () => {
+
+						estimationStarted = true;
+
+						// Fired to indicate that the estimated lighting values are now being updated.
+						this.dispatchEvent( { type: 'estimationstart' } );
+
+					} );
+
+				} );
+
+			}
+
+		} );
+
+		renderer.xr.addEventListener( 'sessionend', () => {
+
+			if ( sessionLightProbe ) {
+
+				sessionLightProbe.dispose();
+				sessionLightProbe = null;
+
+			}
+
+			if ( estimationStarted ) {
+
+				// Fired to indicate that the estimated lighting values are no longer being updated.
+				this.dispatchEvent( { type: 'estimationend' } );
+
+			}
+
+		} );
+
+		// Done inline to provide access to sessionLightProbe.
+		this.dispose = () => {
+
+			if ( sessionLightProbe ) {
+
+				sessionLightProbe.dispose();
+				sessionLightProbe = null;
+
+			}
+
+			this.remove( this.lightProbe );
+			this.lightProbe = null;
+
+			this.remove( this.directionalLight );
+			this.directionalLight = null;
+
+			this.environment = null;
+
+		};
+
+	}
+
+}

BIN
examples/screenshots/webxr_ar_lighting.jpg


+ 180 - 0
examples/webxr_ar_lighting.html

@@ -0,0 +1,180 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js ar - cones</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> ar - Lighting Estimation<br/>(Chrome Android 88+)
+		</div>
+
+		<script type="module">
+
+			import * as THREE from '../build/three.module.js';
+			import { RGBELoader } from './jsm/loaders/RGBELoader.js';
+			import { ARButton } from './jsm/webxr/ARButton.js';
+			import { XREstimatedLight } from './jsm/webxr/XREstimatedLight.js';
+
+			let camera, scene, renderer;
+			let controller;
+			let defaultEnvironment;
+			let ballGroup;
+
+			init();
+			animate();
+
+			function init() {
+
+				const container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				scene = new THREE.Scene();
+
+				camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.01, 20 );
+
+				const defaultLight = new THREE.HemisphereLight( 0xffffff, 0xbbbbff, 1 );
+				defaultLight.position.set( 0.5, 1, 0.25 );
+				scene.add( defaultLight );
+
+				//
+
+				renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.outputEncoding = THREE.sRGBEncoding;
+				renderer.physicallyCorrectLights = true;
+				renderer.xr.enabled = true;
+				container.appendChild( renderer.domElement );
+
+				// Don't add the XREstimatedLight to the scene initially.
+				// It doesn't have any estimated lighting values until an AR session starts.
+
+				const xrLight = new XREstimatedLight( renderer );
+
+				xrLight.addEventListener( 'estimationstart', () => {
+
+					// Swap the default light out for the estimated one one we start getting some estimated values.
+					scene.add(xrLight);
+					scene.remove(defaultLight);
+
+					// The estimated lighting also provides an environment cubemap, which we can apply here.
+					if ( xrLight.environment ) {
+
+						scene.environment = xrLight.environment;
+
+					}
+
+				});
+
+				xrLight.addEventListener( 'estimationend', () => {
+
+					// Swap the lights back when we stop receiving estimated values.
+					scene.add(defaultLight);
+					scene.remove(xrLight);
+
+					// Revert back to the default environment.
+					scene.environment = defaultEnvironment;
+
+				});
+
+				//
+
+				const pmremGenerator = new THREE.PMREMGenerator( renderer );
+				pmremGenerator.compileEquirectangularShader();
+
+				new RGBELoader()
+					.setDataType( THREE.UnsignedByteType )
+					.setPath( 'textures/equirectangular/' )
+					.load( 'royal_esplanade_1k.hdr', function ( texture ) {
+
+						defaultEnvironment = pmremGenerator.fromEquirectangular( texture ).texture;
+
+						scene.environment = defaultEnvironment;
+
+						texture.dispose();
+						pmremGenerator.dispose();
+
+					} );
+
+				//
+
+				// In order for lighting estimation to work, 'light-estimation' must be included as either an optional or required feature.
+				document.body.appendChild( ARButton.createButton( renderer, { optionalFeatures: [ 'light-estimation' ] } ) );
+
+				//
+
+				let ballGeometry = new THREE.SphereBufferGeometry(0.175, 32, 32);
+				let ballGroup = new THREE.Group();
+				ballGroup.position.z = -2;
+
+				const rows = 3;
+				const cols = 3;
+
+				for ( let i = 0; i < rows; ++i ) {
+
+					for ( let j = 0; j < cols; ++j ) {
+
+						const ballMaterial = new THREE.MeshStandardMaterial({
+							color: 0xdddddd,
+							roughness: i / rows,
+							metalness: j / cols
+						});
+						const ballMesh = new THREE.Mesh( ballGeometry, ballMaterial );
+						ballMesh.position.set( (i + 0.5 - rows * 0.5) * 0.4, (j + 0.5 - cols * 0.5) * 0.4, 0 );
+						ballGroup.add( ballMesh );
+
+					}
+
+				}
+
+				scene.add( ballGroup );
+
+				//
+
+				function onSelect() {
+
+					ballGroup.position.set( 0, 0, -2 ).applyMatrix4( controller.matrixWorld );
+					ballGroup.quaternion.setFromRotationMatrix( controller.matrixWorld );
+
+				}
+
+				controller = renderer.xr.getController( 0 );
+				controller.addEventListener( 'select', onSelect );
+				scene.add( controller );
+
+				//
+
+				window.addEventListener( 'resize', onWindowResize, false );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function animate() {
+
+				renderer.setAnimationLoop( render );
+
+			}
+
+			function render() {
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>