diff --git a/examples/files.json b/examples/files.json
index b5e001af515a8b..7bdc29100ec131 100644
--- a/examples/files.json
+++ b/examples/files.json
@@ -355,7 +355,8 @@
"webxr_vr_rollercoaster",
"webxr_vr_sandbox",
"webxr_vr_sculpt",
- "webxr_vr_video"
+ "webxr_vr_video",
+ "webxr_vr_layers"
],
"games": [
"games_fps"
diff --git a/examples/jsm/webxr/VRButton.js b/examples/jsm/webxr/VRButton.js
index f40ed854865d53..20687329b6f110 100644
--- a/examples/jsm/webxr/VRButton.js
+++ b/examples/jsm/webxr/VRButton.js
@@ -68,7 +68,7 @@ class VRButton {
// ('local' is always available for immersive sessions and doesn't need to
// be requested separately.)
- const sessionInit = { optionalFeatures: [ 'local-floor', 'bounded-floor', 'hand-tracking' ] };
+ const sessionInit = { optionalFeatures: [ 'local-floor', 'bounded-floor', 'hand-tracking', 'layers' ] };
navigator.xr.requestSession( 'immersive-vr', sessionInit ).then( onSessionStarted );
} else {
diff --git a/examples/screenshots/webxr_vr_layers.jpg b/examples/screenshots/webxr_vr_layers.jpg
new file mode 100644
index 00000000000000..001fe203e0b94e
Binary files /dev/null and b/examples/screenshots/webxr_vr_layers.jpg differ
diff --git a/examples/webxr_vr_layers.html b/examples/webxr_vr_layers.html
new file mode 100644
index 00000000000000..dc8686ff404bc0
--- /dev/null
+++ b/examples/webxr_vr_layers.html
@@ -0,0 +1,171 @@
+
+
+
+ three.js vr - layers
+
+
+
+
+
+
+
+
three.js media and projection layers
+ (Oculus Browser with #webxr-hands and #webxr-layers flags enabled)
+
+
+
+
+
diff --git a/src/renderers/webxr/WebXRManager.js b/src/renderers/webxr/WebXRManager.js
index f56fd33a0b9d1e..54f12ca32d780a 100644
--- a/src/renderers/webxr/WebXRManager.js
+++ b/src/renderers/webxr/WebXRManager.js
@@ -16,13 +16,15 @@ class WebXRManager extends EventDispatcher {
const state = renderer.state;
let session = null;
-
let framebufferScaleFactor = 1.0;
let referenceSpace = null;
let referenceSpaceType = 'local-floor';
let pose = null;
+ let glBinding = null;
+ let glFramebuffer = null;
+ let glProjLayer = null;
const controllers = [];
const inputSourcesMap = new Map();
@@ -199,18 +201,47 @@ class WebXRManager extends EventDispatcher {
}
- const layerInit = {
- antialias: attributes.antialias,
- alpha: attributes.alpha,
- depth: attributes.depth,
- stencil: attributes.stencil,
- framebufferScaleFactor: framebufferScaleFactor
- };
+ if ( session.renderState.layers === undefined ) {
+
+ const layerInit = {
+ antialias: attributes.antialias,
+ alpha: attributes.alpha,
+ depth: attributes.depth,
+ stencil: attributes.stencil,
+ framebufferScaleFactor: framebufferScaleFactor
+ };
+
+ // eslint-disable-next-line no-undef
+ const baseLayer = new XRWebGLLayer( session, gl, layerInit );
+
+ session.updateRenderState( { baseLayer: baseLayer } );
+
+ } else {
+
+ let depthFormat = 0;
+
+ if ( attributes.depth ) {
+
+ depthFormat = attributes.stencil ? gl.DEPTH_STENCIL : gl.DEPTH_COMPONENT;
+
+ }
+
+ const projectionlayerInit = {
+ colorFormat: attributes.alpha ? gl.RGBA : gl.RGB,
+ depthFormat: depthFormat,
+ scaleFactor: framebufferScaleFactor
+ };
+
+ // eslint-disable-next-line no-undef
+ glBinding = new XRWebGLBinding( session, gl );
+
+ glProjLayer = glBinding.createProjectionLayer( projectionlayerInit );
- // eslint-disable-next-line no-undef
- const baseLayer = new XRWebGLLayer( session, gl, layerInit );
+ glFramebuffer = gl.createFramebuffer();
- session.updateRenderState( { baseLayer: baseLayer } );
+ session.updateRenderState( { layers: [ glProjLayer ] } );
+
+ }
referenceSpace = await session.requestReferenceSpace( referenceSpaceType );
@@ -429,9 +460,14 @@ class WebXRManager extends EventDispatcher {
if ( pose !== null ) {
const views = pose.views;
+
const baseLayer = session.renderState.baseLayer;
- state.bindXRFramebuffer( baseLayer.framebuffer );
+ if ( session.renderState.layers === undefined ) {
+
+ state.bindXRFramebuffer( baseLayer.framebuffer );
+
+ }
let cameraVRNeedsUpdate = false;
@@ -440,18 +476,50 @@ class WebXRManager extends EventDispatcher {
if ( views.length !== cameraVR.cameras.length ) {
cameraVR.cameras.length = 0;
+
cameraVRNeedsUpdate = true;
+
}
for ( let i = 0; i < views.length; i ++ ) {
const view = views[ i ];
- const viewport = baseLayer.getViewport( view );
+
+ let viewport = null;
+
+ if ( session.renderState.layers === undefined ) {
+
+ viewport = baseLayer.getViewport( view );
+
+ } else {
+
+ const glSubImage = glBinding.getViewSubImage( glProjLayer, view );
+
+ gl.bindFramebuffer( gl.FRAMEBUFFER, glFramebuffer );
+
+ gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, glSubImage.colorTexture, 0 );
+
+ if ( glSubImage.depthStencilTexture !== undefined ) {
+
+ gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, glSubImage.depthStencilTexture, 0 );
+
+ }
+
+ gl.bindFramebuffer( gl.FRAMEBUFFER, null );
+
+ state.bindXRFramebuffer( glFramebuffer );
+
+ viewport = glSubImage.viewport;
+
+ }
const camera = cameras[ i ];
+
camera.matrix.fromArray( view.transform.matrix );
+
camera.projectionMatrix.fromArray( view.projectionMatrix );
+
camera.viewport.set( viewport.x, viewport.y, viewport.width, viewport.height );
if ( i === 0 ) {