Просмотр исходного кода

Nodes: Add anaglyph and parallax barrier pass nodes. (#29184)

* Nodes: Add anaglyph and parallax barrier pass nodes.y

* Nodes: Fix `dispose()`.

* E2E: Update screenshot.
Michael Herzog 1 год назад
Родитель
Сommit
1524acf4a8

BIN
examples/screenshots/webgpu_backdrop_water.jpg


BIN
examples/screenshots/webgpu_display_stereo.jpg


+ 54 - 27
examples/webgpu_display_stereo.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html lang="en">
 	<head>
-		<title>three.js webgpu - stereo</title>
+		<title>three.js webgpu - stereo effects</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">
@@ -9,7 +9,7 @@
 
 	<body>
 		<div id="info">
-			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - stereo. skybox by <a href="http://www.zfight.com/" target="_blank" rel="noopener">Jochum Skoglund</a>
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - stereo effects. skybox by <a href="http://www.zfight.com/" target="_blank" rel="noopener">Jochum Skoglund</a>
 		</div>
 
 		<script type="importmap">
@@ -26,19 +26,25 @@
 
 			import * as THREE from 'three';
 
-			import { stereoPass } from 'three/tsl';
+			import { stereoPass, anaglyphPass, parallaxBarrierPass } from 'three/tsl';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { Timer } from 'three/addons/misc/Timer.js';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 			let camera, scene, renderer, postProcessing;
 
-			let mesh, dummy;
+			let stereo, anaglyph, parallaxBarrier;
 
-			let mouseX = 0, mouseY = 0;
-
-			let windowHalfX = window.innerWidth / 2;
-			let windowHalfY = window.innerHeight / 2;
+			let mesh, dummy, timer;
 
 			const position = new THREE.Vector3();
 
+			const params = {
+				effect: 'stereo'
+			};
+
+			const effects = { Stereo: 'stereo', Anaglyph: 'anaglyph', ParallaxBarrier: 'parallaxBarrier' };
+
 			init();
 
 			function init() {
@@ -51,14 +57,15 @@
 					.setPath( 'textures/cube/Park3Med/' )
 					.load( [ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg' ] );
 
+				timer = new Timer();
+
 				const geometry = new THREE.SphereGeometry( 0.1, 32, 16 );
 
 				const textureCube = new THREE.CubeTextureLoader()
 					.setPath( 'textures/cube/Park3Med/' )
 					.load( [ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg' ] );
-				textureCube.mapping = THREE.CubeRefractionMapping;
 
-				const material = new THREE.MeshBasicMaterial( { color: 0xffffff, envMap: textureCube, refractionRatio: 0.95 } );
+				const material = new THREE.MeshBasicMaterial( { color: 0xffffff, envMap: textureCube } );
 
 				mesh = new THREE.InstancedMesh( geometry, material, 500 );
 				mesh.instanceMatrix.setUsage( THREE.DynamicDrawUsage );
@@ -89,31 +96,53 @@
 				document.body.appendChild( renderer.domElement );
 
 				postProcessing = new THREE.PostProcessing( renderer );
-				const pass = stereoPass( scene, camera );
-				postProcessing.outputNode = pass;
+				stereo = stereoPass( scene, camera );
+				anaglyph = anaglyphPass( scene, camera );
+				parallaxBarrier = parallaxBarrierPass( scene, camera );
+
+				postProcessing.outputNode = stereo;
+
+				//
+
+				const gui = new GUI();
+				gui.add( params, 'effect', effects ).onChange( update );
 
 				//
 
 				window.addEventListener( 'resize', onWindowResize );
 
-				document.addEventListener( 'mousemove', onDocumentMouseMove );
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 1;
+				controls.maxDistance = 25;
 
 			}
 
-			function onWindowResize() {
+			function update( value ) {
 
-				windowHalfX = window.innerWidth / 2;
-				windowHalfY = window.innerHeight / 2;
+				if ( value === 'stereo' ) {
 
-				camera.aspect = window.innerWidth / window.innerHeight;
-				camera.updateProjectionMatrix();
+					postProcessing.outputNode = stereo;
+
+				} else if ( value === 'anaglyph' ) {
+
+					postProcessing.outputNode = anaglyph;
+
+				} else if ( value === 'parallaxBarrier' ) {
+
+					postProcessing.outputNode = parallaxBarrier;
+
+				}
+
+				postProcessing.needsUpdate = true;
 
 			}
 
-			function onDocumentMouseMove( event ) {
+			function onWindowResize() {
 
-				mouseX = ( event.clientX - windowHalfX ) * 0.01;
-				mouseY = ( event.clientY - windowHalfY ) * 0.01;
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
 
 			}
 
@@ -129,11 +158,9 @@
 
 			function animate() {
 
-				const timer = 0.0001 * Date.now();
+				timer.update();
 
-				camera.position.x += ( mouseX - camera.position.x ) * .05;
-				camera.position.y += ( - mouseY - camera.position.y ) * .05;
-				camera.lookAt( scene.position );
+				const elapsedTime = timer.getElapsed() * 0.1;
 
 				for ( let i = 0; i < mesh.count; i ++ ) {
 
@@ -141,8 +168,8 @@
 
 					extractPosition( dummy.matrix, position );
 
-					position.x = 5 * Math.cos( timer + i );
-					position.y = 5 * Math.sin( timer + i * 1.1 );
+					position.x = 5 * Math.cos( elapsedTime + i );
+					position.y = 5 * Math.sin( elapsedTime + i * 1.1 );
 
 					dummy.matrix.setPosition( position );
 

+ 2 - 0
src/nodes/Nodes.js

@@ -147,6 +147,8 @@ export { default as RenderOutputNode, renderOutput } from './display/RenderOutpu
 export { default as PixelationPassNode, pixelationPass } from './display/PixelationPassNode.js';
 export { default as SSAAPassNode, ssaaPass } from './display/SSAAPassNode.js';
 export { default as StereoPassNode, stereoPass } from './display/StereoPassNode.js';
+export { default as AnaglyphPassNode, anaglyphPass } from './display/AnaglyphPassNode.js';
+export { default as ParallaxBarrierPassNode, parallaxBarrierPass } from './display/ParallaxBarrierPassNode.js';
 export { bleach } from './display/BleachBypassNode.js';
 export { sepia } from './display/SepiaNode.js';
 

+ 144 - 0
src/nodes/display/AnaglyphPassNode.js

@@ -0,0 +1,144 @@
+import { Fn, nodeObject, vec4 } from '../shadernode/ShaderNode.js';
+import PassNode from './PassNode.js';
+import { Vector2 } from '../../math/Vector2.js';
+import { StereoCamera } from '../../cameras/StereoCamera.js';
+import { HalfFloatType, LinearFilter, NearestFilter } from '../../constants.js';
+import { RenderTarget } from '../../core/RenderTarget.js';
+import { Matrix3 } from '../../math/Matrix3.js';
+import { uniform } from '../core/UniformNode.js';
+import QuadMesh from '../../renderers/common/QuadMesh.js';
+import { uv } from '../accessors/UVNode.js';
+import { clamp, max } from '../math/MathNode.js';
+import { texture } from '../accessors/TextureNode.js';
+
+const _size = /*@__PURE__*/ new Vector2();
+const _quadMesh = /*@__PURE__*/ new QuadMesh();
+
+class AnaglyphPassNode extends PassNode {
+
+	constructor( scene, camera ) {
+
+		super( PassNode.COLOR, scene, camera );
+
+		this.isAnaglyphPassNode = true;
+
+		this.stereo = new StereoCamera();
+
+		const _params = { minFilter: LinearFilter, magFilter: NearestFilter, type: HalfFloatType };
+
+		this._renderTargetL = new RenderTarget( 1, 1, _params );
+		this._renderTargetR = new RenderTarget( 1, 1, _params );
+
+		// Dubois matrices from https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.7.6968&rep=rep1&type=pdf#page=4
+
+		this._colorMatrixLeft = uniform( new Matrix3().fromArray( [
+			0.456100, - 0.0400822, - 0.0152161,
+			0.500484, - 0.0378246, - 0.0205971,
+			0.176381, - 0.0157589, - 0.00546856
+		] ) );
+
+		this._colorMatrixRight = uniform( new Matrix3().fromArray( [
+			- 0.0434706, 0.378476, - 0.0721527,
+			- 0.0879388, 0.73364, - 0.112961,
+			- 0.00155529, - 0.0184503, 1.2264
+		] ) );
+
+		this._mapLeft = texture( this._renderTargetL.texture );
+		this._mapRight = texture( this._renderTargetR.texture );
+
+		//
+
+		this._material = null;
+
+	}
+
+	setSize( width, height ) {
+
+		super.setSize( width, height );
+
+		this._renderTargetL.setSize( this.renderTarget.width, this.renderTarget.height );
+		this._renderTargetR.setSize( this.renderTarget.width, this.renderTarget.height );
+
+	}
+
+	updateBefore( frame ) {
+
+		const { renderer } = frame;
+		const { scene, camera, stereo, renderTarget } = this;
+
+		this._pixelRatio = renderer.getPixelRatio();
+
+		stereo.cameraL.coordinateSystem = renderer.coordinateSystem;
+		stereo.cameraR.coordinateSystem = renderer.coordinateSystem;
+		stereo.update( camera );
+
+		const size = renderer.getSize( _size );
+		this.setSize( size.width, size.height );
+
+		const currentRenderTarget = renderer.getRenderTarget();
+
+		// left
+
+		renderer.setRenderTarget( this._renderTargetL );
+		renderer.render( scene, stereo.cameraL );
+
+		// right
+
+		renderer.setRenderTarget( this._renderTargetR );
+		renderer.render( scene, stereo.cameraR );
+
+		// composite
+
+		renderer.setRenderTarget( renderTarget );
+		_quadMesh.material = this._material;
+		_quadMesh.render( renderer );
+
+		// restore
+
+		renderer.setRenderTarget( currentRenderTarget );
+
+	}
+
+	setup( builder ) {
+
+		const uvNode = uv();
+
+		const anaglyph = Fn( () => {
+
+			const colorL = this._mapLeft.uv( uvNode );
+			const colorR = this._mapRight.uv( uvNode );
+
+			const color = clamp( this._colorMatrixLeft.mul( colorL.rgb ).add( this._colorMatrixRight.mul( colorR.rgb ) ) );
+
+			return vec4( color.rgb, max( colorL.a, colorR.a ) );
+
+		} );
+
+		const material = this._material || ( this._material = builder.createNodeMaterial() );
+		material.fragmentNode = anaglyph().context( builder.getSharedContext() );
+		material.needsUpdate = true;
+
+		return super.setup( builder );
+
+	}
+
+	dispose() {
+
+		super.dispose();
+
+		this._renderTargetL.dispose();
+		this._renderTargetR.dispose();
+
+		if ( this._material !== null ) {
+
+			this._material.dispose();
+
+		}
+
+	}
+
+}
+
+export const anaglyphPass = ( scene, camera ) => nodeObject( new AnaglyphPassNode( scene, camera ) );
+
+export default AnaglyphPassNode;

+ 136 - 0
src/nodes/display/ParallaxBarrierPassNode.js

@@ -0,0 +1,136 @@
+import { Fn, If, nodeObject, vec4 } from '../shadernode/ShaderNode.js';
+import PassNode from './PassNode.js';
+import { Vector2 } from '../../math/Vector2.js';
+import { StereoCamera } from '../../cameras/StereoCamera.js';
+import { HalfFloatType, LinearFilter, NearestFilter } from '../../constants.js';
+import { RenderTarget } from '../../core/RenderTarget.js';
+import QuadMesh from '../../renderers/common/QuadMesh.js';
+import { uv } from '../accessors/UVNode.js';
+import { mod } from '../math/MathNode.js';
+import { texture } from '../accessors/TextureNode.js';
+import { viewportCoordinate } from './ViewportNode.js';
+
+const _size = /*@__PURE__*/ new Vector2();
+const _quadMesh = /*@__PURE__*/ new QuadMesh();
+
+class ParallaxBarrierPassNode extends PassNode {
+
+	constructor( scene, camera ) {
+
+		super( PassNode.COLOR, scene, camera );
+
+		this.isParallaxBarrierPassNode = true;
+
+		this.stereo = new StereoCamera();
+
+		const _params = { minFilter: LinearFilter, magFilter: NearestFilter, type: HalfFloatType };
+
+		this._renderTargetL = new RenderTarget( 1, 1, _params );
+		this._renderTargetR = new RenderTarget( 1, 1, _params );
+
+		this._mapLeft = texture( this._renderTargetL.texture );
+		this._mapRight = texture( this._renderTargetR.texture );
+
+		//
+
+		this._material = null;
+
+	}
+
+	setSize( width, height ) {
+
+		super.setSize( width, height );
+
+		this._renderTargetL.setSize( this.renderTarget.width, this.renderTarget.height );
+		this._renderTargetR.setSize( this.renderTarget.width, this.renderTarget.height );
+
+	}
+
+	updateBefore( frame ) {
+
+		const { renderer } = frame;
+		const { scene, camera, stereo, renderTarget } = this;
+
+		this._pixelRatio = renderer.getPixelRatio();
+
+		stereo.cameraL.coordinateSystem = renderer.coordinateSystem;
+		stereo.cameraR.coordinateSystem = renderer.coordinateSystem;
+		stereo.update( camera );
+
+		const size = renderer.getSize( _size );
+		this.setSize( size.width, size.height );
+
+		const currentRenderTarget = renderer.getRenderTarget();
+
+		// left
+
+		renderer.setRenderTarget( this._renderTargetL );
+		renderer.render( scene, stereo.cameraL );
+
+		// right
+
+		renderer.setRenderTarget( this._renderTargetR );
+		renderer.render( scene, stereo.cameraR );
+
+		// composite
+
+		renderer.setRenderTarget( renderTarget );
+		_quadMesh.material = this._material;
+		_quadMesh.render( renderer );
+
+		// restore
+
+		renderer.setRenderTarget( currentRenderTarget );
+
+	}
+
+	setup( builder ) {
+
+		const uvNode = uv();
+
+		const parallaxBarrier = Fn( () => {
+
+			const color = vec4().toVar();
+
+			If( mod( viewportCoordinate.y, 2 ).greaterThan( 1 ), () => {
+
+				color.assign( this._mapLeft.uv( uvNode ) );
+
+			} ).Else( () => {
+
+				color.assign( this._mapRight.uv( uvNode ) );
+
+			} );
+
+			return color;
+
+		} );
+
+		const material = this._material || ( this._material = builder.createNodeMaterial() );
+		material.fragmentNode = parallaxBarrier().context( builder.getSharedContext() );
+		material.needsUpdate = true;
+
+		return super.setup( builder );
+
+	}
+
+	dispose() {
+
+		super.dispose();
+
+		this._renderTargetL.dispose();
+		this._renderTargetR.dispose();
+
+		if ( this._material !== null ) {
+
+			this._material.dispose();
+
+		}
+
+	}
+
+}
+
+export const parallaxBarrierPass = ( scene, camera ) => nodeObject( new ParallaxBarrierPassNode( scene, camera ) );
+
+export default ParallaxBarrierPassNode;

粤ICP备19079148号