|
@@ -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 );
|