Răsfoiți Sursa

Update WebXR Layers example to compare quality. (#22083)

* Modified the WebXR Layers example to render a Snellen eye chart using
  both standard rendering to the eye buffer and WebXR layers. This
  demonstrates the quality improvement of layers for text legibility.
* Added snellen.png which is a custom image rendered with a JS
  canvas based on the standard eye chart.

Co-authored-by: Sameer Padala <[email protected]>
SxP 4 ani în urmă
părinte
comite
cdd242f9a6
2 a modificat fișierele cu 78 adăugiri și 16 ștergeri
  1. BIN
      examples/textures/snellen.png
  2. 78 16
      examples/webxr_vr_layers.html

BIN
examples/textures/snellen.png


+ 78 - 16
examples/webxr_vr_layers.html

@@ -29,7 +29,34 @@
 
 			let controls;
 			let video;
-			let video2;
+			let snellenTexture;
+			let quadLayer;
+
+			// Data shared between the THREE.Mesh on the left side and WebXR Layer on the right side for
+			// the eye chart. See https://en.wikipedia.org/wiki/Snellen_chart for details about the math.
+			//
+			// The image was designed so that each 2x2px block on the 20/20 line subtends 1 minute of
+			// arc. That is
+			//    tan(1/60 deg) * 6.1m * 160px/142mm = 2px
+			// per block on line 8.
+			//
+			// This fidelity is beyond any modern consumer headset since it would require ~60px/deg of
+			// resolution. The Quest has ~16ppd and the Quest 2 has ~20ppd so only lines 3 or 4 will be
+			// legible when using layers. Without layers, you lose ~sqrt(2) in resolution due to the
+			// extra resampling.
+			const snellenConfig = {
+				// The height & width of the WebXR layer needs to match the given image size.
+				widthPx: 320,
+				heightPx: 450,
+
+				x: 0, // +/- widthMeters/2
+				y: 1.5,
+
+				z: - 6.1, // 20/20 vision @ 20ft = 6.1m
+
+				widthMeters: .268, // 320px image * (142mm/160px scale factor)
+				heightMeters: .382 // 450px image * (142mm/160px scale factor)
+			};
 
 			init();
 			animate();
@@ -116,17 +143,23 @@
 				controller1.add( line.clone() );
 				controller2.add( line.clone() );
 
+				// Eye chart
+				snellenTexture = new THREE.TextureLoader().load( "textures/snellen.png" );
+				const snellenMesh = new THREE.Mesh(
+					new THREE.PlaneGeometry( snellenConfig.widthMeters, snellenConfig.heightMeters ),
+					new THREE.MeshBasicMaterial( { map: snellenTexture } ) );
+				snellenMesh.position.x = snellenConfig.x - snellenConfig.widthMeters / 2;
+				snellenMesh.position.y = snellenConfig.y;
+				snellenMesh.position.z = snellenConfig.z;
+				scene.add( snellenMesh );
+
 				//
 
 				window.addEventListener( 'resize', onWindowResize, false );
 
 				video = document.createElement('video');
 				video.loop = true;
-				video.src = 'textures/pano.webm';
-
-				video2 = document.createElement('video');
-				video2.loop = true;
-				video2.src = 'textures/MaryOculus.webm';
+				video.src = 'textures/MaryOculus.webm';
 			}
 
 			function onWindowResize() {
@@ -145,21 +178,50 @@
 
 			}
 
-			function render() {
+			function render( t, frame ) {
+
 				const xr = renderer.xr;
 				const session = xr.getSession();
+				const gl = renderer.getContext();
+
+				// Init layers once in immersive mode and video is ready.
+				if ( session && session.renderState.layers !== undefined && session.hasMediaLayer === undefined && video.readyState >= 2 ) {
 
-				if ( session && session.renderState.layers !== undefined && session.hasMediaLayer === undefined && video.readyState >= 2 && video2.readyState >= 2) {
 					session.hasMediaLayer = true;
-					session.requestReferenceSpace('local').then((refSpace) => {
-						const mediaBinding = new XRMediaBinding(session);
-						const equirectLayer = mediaBinding.createEquirectLayer(video, {space: refSpace, layout: "mono"});
-						const quadLayer = mediaBinding.createQuadLayer(video2, {space: refSpace, layout: "stereo-left-right"});
-						quadLayer.transform = new XRRigidTransform({x: 1.5, y: 1.0, z: -2.0});
-						session.updateRenderState( { layers: [ equirectLayer, quadLayer, session.renderState.layers[0] ] } );
+					session.requestReferenceSpace( 'local' ).then( ( refSpace ) => {
+
+						// Create Quad layer for Snellen chart.
+						const glBinding = new XRWebGLBinding( session, gl );
+						quadLayer = glBinding.createQuadLayer( {
+							width: snellenConfig.widthMeters / 2,
+							height: snellenConfig.heightMeters / 2,
+							viewPixelWidth: snellenConfig.widthPx,
+							viewPixelHeight: snellenConfig.heightPx,
+							space: refSpace, layout: "mono",
+							transform: new XRRigidTransform( { x: snellenConfig.x + snellenConfig.widthMeters / 2, y: snellenConfig.y, z: snellenConfig.z } )
+						} );
+
+						// Create background EQR video layer.
+						const mediaBinding = new XRMediaBinding( session );
+						const equirectLayer = mediaBinding.createEquirectLayer( video, { space: refSpace, layout: "stereo-left-right" } );
+
+						session.updateRenderState( { layers: [ equirectLayer, quadLayer, session.renderState.layers[ 0 ] ] } );
 						video.play();
-						video2.play();
-					});
+
+					} );
+
+				}
+
+				// Copy image to canvas as required.
+				// needsRedraw is set on creation or if the underlying GL resources of a layer are lost.
+				if ( quadLayer && quadLayer.needsRedraw ) {
+
+					const glBinding = new XRWebGLBinding( session, gl );
+					const glayer = glBinding.getSubImage( quadLayer, frame );
+					renderer.state.bindTexture( gl.TEXTURE_2D, glayer.colorTexture );
+					gl.pixelStorei( gl.UNPACK_FLIP_Y_WEBGL, true );
+					gl.texSubImage2D( gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, snellenTexture.image );
+
 				}
 
 				renderer.render( scene, camera );