Pārlūkot izejas kodu

WebGLRenderer: Improve offscreen rendering in WebXR. (#26160)

* WebXRManager: add getRenderTarget

* WebGLRenderer: bind to base layer instead of canvas in XR

* WebXRManager: update drawingBufferSize

* EffectComposer: resize with XR layer

* Pass: toggle XR rendering for fullscreen effects

* Build & test xr postprocessing

* Add simple vr postprocessing demo

* Add vr/postprocessing to files.json

* Lint

* Remove builds

* Cleanup example

* WebXRManager: make getRenderTarget private

* WebXRManager: cleanup resize

* Update webxr_vr_postprocessing.jpg
Cody Bennett 1 gadu atpakaļ
vecāks
revīzija
afdfa33b35

+ 1 - 0
examples/files.json

@@ -367,6 +367,7 @@
 		"webxr_vr_handinput_pressbutton",
 		"webxr_vr_layers",
 		"webxr_vr_panorama",
+		"webxr_vr_postprocessing",
 		"webxr_vr_panorama_depth",
 		"webxr_vr_rollercoaster",
 		"webxr_vr_sandbox",

+ 23 - 2
examples/jsm/postprocessing/EffectComposer.js

@@ -10,6 +10,8 @@ import { ShaderPass } from './ShaderPass.js';
 import { MaskPass } from './MaskPass.js';
 import { ClearMaskPass } from './MaskPass.js';
 
+const size = /* @__PURE__ */ new Vector2();
+
 class EffectComposer {
 
 	constructor( renderer, renderTarget ) {
@@ -20,7 +22,7 @@ class EffectComposer {
 
 		if ( renderTarget === undefined ) {
 
-			const size = renderer.getSize( new Vector2() );
+			renderer.getSize( size );
 			this._width = size.width;
 			this._height = size.height;
 
@@ -50,6 +52,22 @@ class EffectComposer {
 
 		this.clock = new Clock();
 
+		this.onSessionStateChange = this.onSessionStateChange.bind( this );
+		this.renderer.xr.addEventListener( 'sessionstart', this.onSessionStateChange );
+		this.renderer.xr.addEventListener( 'sessionend', this.onSessionStateChange );
+
+	}
+
+	onSessionStateChange() {
+
+		this.renderer.getSize( size );
+		this._width = size.width;
+		this._height = size.height;
+
+		this._pixelRatio = this.renderer.xr.isPresenting ? 1 : this.renderer.getPixelRatio();
+
+		this.setSize( this._width, this._height );
+
 	}
 
 	swapBuffers() {
@@ -170,7 +188,7 @@ class EffectComposer {
 
 		if ( renderTarget === undefined ) {
 
-			const size = this.renderer.getSize( new Vector2() );
+			this.renderer.getSize( size );
 			this._pixelRatio = this.renderer.getPixelRatio();
 			this._width = size.width;
 			this._height = size.height;
@@ -224,6 +242,9 @@ class EffectComposer {
 
 		this.copyPass.dispose();
 
+		this.renderer.xr.removeEventListener( 'sessionstart', this.onSessionStateChange );
+		this.renderer.xr.removeEventListener( 'sessionend', this.onSessionStateChange );
+
 	}
 
 }

+ 6 - 0
examples/jsm/postprocessing/Pass.js

@@ -63,7 +63,13 @@ class FullScreenQuad {
 
 	render( renderer ) {
 
+		// Disable XR projection for fullscreen effects
+		// https://github.com/mrdoob/three.js/pull/18846
+		const xrEnabled = renderer.xr.enabled;
+
+		renderer.xr.enabled = false;
 		renderer.render( this._mesh, _camera );
+		renderer.xr.enabled = xrEnabled;
 
 	}
 

BIN
examples/screenshots/webxr_vr_postprocessing.jpg


+ 121 - 0
examples/webxr_vr_postprocessing.html

@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js vr - postprocessing</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+		<style>
+			#info > * {
+				max-width: 650px;
+				margin-left: auto;
+				margin-right: auto;
+			}
+		</style>
+	</head>
+	<body>
+
+		<div id="container"></div>
+
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> vr - postprocessing<br/>
+			(Oculus Browser 15.1+)
+		</div>
+
+		<!-- Import maps polyfill -->
+		<!-- Remove this when import maps will be widely supported -->
+		<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+			import { VRButton } from 'three/addons/webxr/VRButton.js';
+			import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
+			import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
+			import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
+			import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
+
+			let renderer, camera, scene, composer;
+
+			init();
+			animate();
+
+			function init() {
+
+				renderer = new THREE.WebGLRenderer();
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				document.body.appendChild( renderer.domElement );
+
+				renderer.xr.enabled = true;
+				document.body.appendChild( VRButton.createButton( renderer ) );
+
+				camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight );
+
+				scene = new THREE.Scene();
+				scene.position.z = - 8; // XR camera starts at 0,0
+
+				const triangleGeometry = new THREE.BufferGeometry();
+
+				const triangleVertices = [ - 1, - 1, 1, 1, - 1, 1, 0, 1, 1 ];
+				triangleGeometry.setAttribute( 'position', new THREE.Float32BufferAttribute( triangleVertices, 3 ) );
+
+				const orangeMaterial = new THREE.MeshBasicMaterial( { color: 'orange', side: THREE.DoubleSide } );
+				const orange = new THREE.Mesh( triangleGeometry, orangeMaterial );
+				orange.position.x = - 3;
+				scene.add( orange );
+
+				const redMaterial = new THREE.MeshBasicMaterial( { color: 0xff2060, side: THREE.DoubleSide } );
+				const red = new THREE.Mesh( triangleGeometry, redMaterial );
+				scene.add( red );
+
+				const blueMaterial = new THREE.MeshBasicMaterial( { color: 'cyan', side: THREE.DoubleSide } );
+				const blue = new THREE.Mesh( triangleGeometry, blueMaterial );
+				blue.position.x = 3;
+				scene.add( blue );
+
+				composer = new EffectComposer( renderer );
+				composer.setSize( window.innerWidth, window.innerHeight );
+				composer.addPass( new RenderPass( scene, camera ) );
+				composer.addPass( new UnrealBloomPass( undefined, 0.5, 1, 0 ) );
+				composer.addPass( new OutputPass( THREE.ACESFilmicToneMapping ) );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				composer.setSize( window.innerWidth, window.innerHeight );
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+			}
+
+			function animate() {
+
+				renderer.setAnimationLoop( render );
+
+			}
+
+			function render() {
+
+				composer.render();
+
+			}
+
+		</script>
+
+	</body>
+
+</html>

+ 7 - 0
src/renderers/WebGLRenderer.js

@@ -2060,6 +2060,13 @@ class WebGLRenderer {
 
 		this.setRenderTarget = function ( renderTarget, activeCubeFace = 0, activeMipmapLevel = 0 ) {
 
+			// Render to base layer instead of canvas in WebXR
+			if ( renderTarget === null && this.xr.isPresenting ) {
+
+				renderTarget = this.xr._getRenderTarget();
+
+			}
+
 			_currentRenderTarget = renderTarget;
 			_currentActiveCubeFace = activeCubeFace;
 			_currentActiveMipmapLevel = activeMipmapLevel;

+ 22 - 0
src/renderers/webxr/WebXRManager.js

@@ -1,6 +1,7 @@
 import { ArrayCamera } from '../../cameras/ArrayCamera.js';
 import { EventDispatcher } from '../../core/EventDispatcher.js';
 import { PerspectiveCamera } from '../../cameras/PerspectiveCamera.js';
+import { Vector2 } from '../../math/Vector2.js';
 import { Vector3 } from '../../math/Vector3.js';
 import { Vector4 } from '../../math/Vector4.js';
 import { RAD2DEG } from '../../math/MathUtils.js';
@@ -40,6 +41,9 @@ class WebXRManager extends EventDispatcher {
 		const controllers = [];
 		const controllerInputSources = [];
 
+		const currentSize = new Vector2();
+		let currentPixelRatio = null;
+
 		//
 
 		const cameraL = new PerspectiveCamera();
@@ -172,6 +176,9 @@ class WebXRManager extends EventDispatcher {
 
 			//
 
+			renderer.setPixelRatio( currentPixelRatio );
+			renderer.setSize( currentSize.width, currentSize.height, false );
+
 			animation.stop();
 
 			scope.isPresenting = false;
@@ -228,6 +235,12 @@ class WebXRManager extends EventDispatcher {
 
 		};
 
+		this._getRenderTarget = function () {
+
+			return newRenderTarget;
+
+		};
+
 		this.getFrame = function () {
 
 			return xrFrame;
@@ -277,6 +290,9 @@ class WebXRManager extends EventDispatcher {
 
 					session.updateRenderState( { baseLayer: glBaseLayer } );
 
+					renderer.setPixelRatio( 1 );
+					renderer.setSize( glBaseLayer.framebufferWidth, glBaseLayer.framebufferHeight, false );
+
 					newRenderTarget = new WebGLRenderTarget(
 						glBaseLayer.framebufferWidth,
 						glBaseLayer.framebufferHeight,
@@ -314,6 +330,9 @@ class WebXRManager extends EventDispatcher {
 
 					session.updateRenderState( { layers: [ glProjLayer ] } );
 
+					renderer.setPixelRatio( 1 );
+					renderer.setSize( glProjLayer.textureWidth, glProjLayer.textureHeight, false );
+
 					newRenderTarget = new WebGLRenderTarget(
 						glProjLayer.textureWidth,
 						glProjLayer.textureHeight,
@@ -338,6 +357,9 @@ class WebXRManager extends EventDispatcher {
 				customReferenceSpace = null;
 				referenceSpace = await session.requestReferenceSpace( referenceSpaceType );
 
+				currentPixelRatio = renderer.getPixelRatio();
+				renderer.getSize( currentSize );
+
 				animation.setContext( session );
 				animation.start();