Răsfoiți Sursa

AnaglyphEffect: Use frameCorners() for physically-correct stereo (#32929)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Johnathon Selstad 3 săptămâni în urmă
părinte
comite
f9e33fa65e

+ 102 - 7
examples/jsm/effects/AnaglyphEffect.js

@@ -1,16 +1,38 @@
 import {
 import {
 	LinearFilter,
 	LinearFilter,
+	MathUtils,
 	Matrix3,
 	Matrix3,
 	NearestFilter,
 	NearestFilter,
+	PerspectiveCamera,
 	RGBAFormat,
 	RGBAFormat,
 	ShaderMaterial,
 	ShaderMaterial,
-	StereoCamera,
+	Vector3,
 	WebGLRenderTarget
 	WebGLRenderTarget
 } from 'three';
 } from 'three';
 import { FullScreenQuad } from '../postprocessing/Pass.js';
 import { FullScreenQuad } from '../postprocessing/Pass.js';
+import { frameCorners } from '../utils/CameraUtils.js';
+
+const _cameraL = /*@__PURE__*/ new PerspectiveCamera();
+const _cameraR = /*@__PURE__*/ new PerspectiveCamera();
+
+// Reusable vectors for screen corner calculations
+const _eyeL = /*@__PURE__*/ new Vector3();
+const _eyeR = /*@__PURE__*/ new Vector3();
+const _screenCenter = /*@__PURE__*/ new Vector3();
+const _screenBottomLeft = /*@__PURE__*/ new Vector3();
+const _screenBottomRight = /*@__PURE__*/ new Vector3();
+const _screenTopLeft = /*@__PURE__*/ new Vector3();
+const _right = /*@__PURE__*/ new Vector3();
+const _up = /*@__PURE__*/ new Vector3();
+const _forward = /*@__PURE__*/ new Vector3();
 
 
 /**
 /**
- * A class that creates an anaglyph effect.
+ * A class that creates an anaglyph effect using physically-correct
+ * off-axis stereo projection.
+ *
+ * This implementation uses CameraUtils.frameCorners() to align stereo
+ * camera frustums to a virtual screen plane, providing accurate depth
+ * perception with zero parallax at the screen distance.
  *
  *
  * Note that this class can only be used with {@link WebGLRenderer}.
  * Note that this class can only be used with {@link WebGLRenderer}.
  * When using {@link WebGPURenderer}, use {@link AnaglyphPassNode}.
  * When using {@link WebGPURenderer}, use {@link AnaglyphPassNode}.
@@ -42,13 +64,38 @@ class AnaglyphEffect {
 			- 0.00155529, - 0.0184503, 1.2264
 			- 0.00155529, - 0.0184503, 1.2264
 		] );
 		] );
 
 
-		const _stereo = new StereoCamera();
+		/**
+		 * The interpupillary distance (eye separation) in world units.
+		 * Typical human IPD is 0.064 meters (64mm).
+		 *
+		 * @type {number}
+		 * @default 0.064
+		 */
+		this.eyeSep = 0.064;
+
+		/**
+		 * The distance from the viewer to the virtual screen plane
+		 * where zero parallax (screen depth) occurs.
+		 * Objects at this distance appear at the screen surface.
+		 * Objects closer appear in front of the screen (negative parallax).
+		 * Objects further appear behind the screen (positive parallax).
+		 *
+		 * The screen dimensions are derived from the camera's FOV and aspect ratio
+		 * at this distance, ensuring the stereo view matches the camera's field of view.
+		 *
+		 * @type {number}
+		 * @default 0.5
+		 */
+		this.screenDistance = 0.5;
 
 
 		const _params = { minFilter: LinearFilter, magFilter: NearestFilter, format: RGBAFormat };
 		const _params = { minFilter: LinearFilter, magFilter: NearestFilter, format: RGBAFormat };
 
 
 		const _renderTargetL = new WebGLRenderTarget( width, height, _params );
 		const _renderTargetL = new WebGLRenderTarget( width, height, _params );
 		const _renderTargetR = new WebGLRenderTarget( width, height, _params );
 		const _renderTargetR = new WebGLRenderTarget( width, height, _params );
 
 
+		_cameraL.layers.enable( 1 );
+		_cameraR.layers.enable( 2 );
+
 		const _material = new ShaderMaterial( {
 		const _material = new ShaderMaterial( {
 
 
 			uniforms: {
 			uniforms: {
@@ -141,16 +188,64 @@ class AnaglyphEffect {
 
 
 			if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();
 			if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();
 
 
-			_stereo.update( camera );
-
+			// Get the camera's local coordinate axes from its world matrix
+			camera.matrixWorld.extractBasis( _right, _up, _forward );
+			_right.normalize();
+			_up.normalize();
+			_forward.normalize();
+
+			// Calculate eye positions
+			const halfSep = this.eyeSep / 2;
+			_eyeL.copy( camera.position ).addScaledVector( _right, - halfSep );
+			_eyeR.copy( camera.position ).addScaledVector( _right, halfSep );
+
+			// Calculate screen center (at screenDistance in front of the camera center)
+			_screenCenter.copy( camera.position ).addScaledVector( _forward, - this.screenDistance );
+
+			// Calculate screen dimensions from camera FOV and aspect ratio
+			const halfHeight = this.screenDistance * Math.tan( MathUtils.DEG2RAD * camera.fov / 2 );
+			const halfWidth = halfHeight * camera.aspect;
+
+			// Calculate screen corners
+			_screenBottomLeft.copy( _screenCenter )
+				.addScaledVector( _right, - halfWidth )
+				.addScaledVector( _up, - halfHeight );
+
+			_screenBottomRight.copy( _screenCenter )
+				.addScaledVector( _right, halfWidth )
+				.addScaledVector( _up, - halfHeight );
+
+			_screenTopLeft.copy( _screenCenter )
+				.addScaledVector( _right, - halfWidth )
+				.addScaledVector( _up, halfHeight );
+
+			// Set up left eye camera
+			_cameraL.position.copy( _eyeL );
+			_cameraL.near = camera.near;
+			_cameraL.far = camera.far;
+			frameCorners( _cameraL, _screenBottomLeft, _screenBottomRight, _screenTopLeft, true );
+			_cameraL.matrixWorld.compose( _cameraL.position, _cameraL.quaternion, _cameraL.scale );
+			_cameraL.matrixWorldInverse.copy( _cameraL.matrixWorld ).invert();
+
+			// Set up right eye camera
+			_cameraR.position.copy( _eyeR );
+			_cameraR.near = camera.near;
+			_cameraR.far = camera.far;
+			frameCorners( _cameraR, _screenBottomLeft, _screenBottomRight, _screenTopLeft, true );
+			_cameraR.matrixWorld.compose( _cameraR.position, _cameraR.quaternion, _cameraR.scale );
+			_cameraR.matrixWorldInverse.copy( _cameraR.matrixWorld ).invert();
+
+			// Render left eye
 			renderer.setRenderTarget( _renderTargetL );
 			renderer.setRenderTarget( _renderTargetL );
 			renderer.clear();
 			renderer.clear();
-			renderer.render( scene, _stereo.cameraL );
+			renderer.render( scene, _cameraL );
 
 
+			// Render right eye
 			renderer.setRenderTarget( _renderTargetR );
 			renderer.setRenderTarget( _renderTargetR );
 			renderer.clear();
 			renderer.clear();
-			renderer.render( scene, _stereo.cameraR );
+			renderer.render( scene, _cameraR );
 
 
+			// Composite anaglyph
 			renderer.setRenderTarget( null );
 			renderer.setRenderTarget( null );
 			_quad.render( renderer );
 			_quad.render( renderer );
 
 

+ 105 - 2
examples/jsm/tsl/display/AnaglyphPassNode.js

@@ -1,6 +1,17 @@
-import { Matrix3, NodeMaterial } from 'three/webgpu';
+import { Matrix3, NodeMaterial, Vector3 } from 'three/webgpu';
 import { clamp, nodeObject, Fn, vec4, uv, uniform, max } from 'three/tsl';
 import { clamp, nodeObject, Fn, vec4, uv, uniform, max } from 'three/tsl';
 import StereoCompositePassNode from './StereoCompositePassNode.js';
 import StereoCompositePassNode from './StereoCompositePassNode.js';
+import { frameCorners } from '../../utils/CameraUtils.js';
+
+const _eyeL = /*@__PURE__*/ new Vector3();
+const _eyeR = /*@__PURE__*/ new Vector3();
+const _screenBottomLeft = /*@__PURE__*/ new Vector3();
+const _screenBottomRight = /*@__PURE__*/ new Vector3();
+const _screenTopLeft = /*@__PURE__*/ new Vector3();
+const _right = /*@__PURE__*/ new Vector3();
+const _up = /*@__PURE__*/ new Vector3();
+const _forward = /*@__PURE__*/ new Vector3();
+const _screenCenter = /*@__PURE__*/ new Vector3();
 
 
 /**
 /**
  * Anaglyph algorithm types.
  * Anaglyph algorithm types.
@@ -259,7 +270,12 @@ const ANAGLYPH_MATRICES = {
 };
 };
 
 
 /**
 /**
- * A render pass node that creates an anaglyph effect.
+ * A render pass node that creates an anaglyph effect using physically-correct
+ * off-axis stereo projection.
+ *
+ * This implementation uses CameraUtils.frameCorners() to align stereo
+ * camera frustums to a virtual screen plane, providing accurate depth
+ * perception with zero parallax at the screen distance.
  *
  *
  * @augments StereoCompositePassNode
  * @augments StereoCompositePassNode
  * @three_import import { anaglyphPass, AnaglyphAlgorithm, AnaglyphColorMode } from 'three/addons/tsl/display/AnaglyphPassNode.js';
  * @three_import import { anaglyphPass, AnaglyphAlgorithm, AnaglyphColorMode } from 'three/addons/tsl/display/AnaglyphPassNode.js';
@@ -291,6 +307,30 @@ class AnaglyphPassNode extends StereoCompositePassNode {
 		 */
 		 */
 		this.isAnaglyphPassNode = true;
 		this.isAnaglyphPassNode = true;
 
 
+		/**
+		 * The interpupillary distance (eye separation) in world units.
+		 * Typical human IPD is 0.064 meters (64mm).
+		 *
+		 * @type {number}
+		 * @default 0.064
+		 */
+		this.eyeSep = 0.064;
+
+		/**
+		 * The distance from the viewer to the virtual screen plane
+		 * where zero parallax (screen depth) occurs.
+		 * Objects at this distance appear at the screen surface.
+		 * Objects closer appear in front of the screen (negative parallax).
+		 * Objects further appear behind the screen (positive parallax).
+		 *
+		 * The screen dimensions are derived from the camera's FOV and aspect ratio
+		 * at this distance, ensuring the stereo view matches the camera's field of view.
+		 *
+		 * @type {number}
+		 * @default 0.5
+		 */
+		this.screenDistance = 0.5;
+
 		/**
 		/**
 		 * The current anaglyph algorithm.
 		 * The current anaglyph algorithm.
 		 *
 		 *
@@ -398,6 +438,69 @@ class AnaglyphPassNode extends StereoCompositePassNode {
 
 
 	}
 	}
 
 
+	/**
+	 * Updates the internal stereo camera using frameCorners for
+	 * physically-correct off-axis projection.
+	 *
+	 * @param {number} coordinateSystem - The current coordinate system.
+	 */
+	updateStereoCamera( coordinateSystem ) {
+
+		const { stereo, camera } = this;
+
+		stereo.cameraL.coordinateSystem = coordinateSystem;
+		stereo.cameraR.coordinateSystem = coordinateSystem;
+
+		// Get the camera's local coordinate axes from its world matrix
+		camera.matrixWorld.extractBasis( _right, _up, _forward );
+		_right.normalize();
+		_up.normalize();
+		_forward.normalize();
+
+		// Calculate eye positions
+		const halfSep = this.eyeSep / 2;
+		_eyeL.copy( camera.position ).addScaledVector( _right, - halfSep );
+		_eyeR.copy( camera.position ).addScaledVector( _right, halfSep );
+
+		// Calculate screen center (at screenDistance in front of the camera center)
+		_screenCenter.copy( camera.position ).addScaledVector( _forward, - this.screenDistance );
+
+		// Calculate screen dimensions from camera FOV and aspect ratio
+		const DEG2RAD = Math.PI / 180;
+		const halfHeight = this.screenDistance * Math.tan( DEG2RAD * camera.fov / 2 );
+		const halfWidth = halfHeight * camera.aspect;
+
+		// Calculate screen corners
+		_screenBottomLeft.copy( _screenCenter )
+			.addScaledVector( _right, - halfWidth )
+			.addScaledVector( _up, - halfHeight );
+
+		_screenBottomRight.copy( _screenCenter )
+			.addScaledVector( _right, halfWidth )
+			.addScaledVector( _up, - halfHeight );
+
+		_screenTopLeft.copy( _screenCenter )
+			.addScaledVector( _right, - halfWidth )
+			.addScaledVector( _up, halfHeight );
+
+		// Set up left eye camera
+		stereo.cameraL.position.copy( _eyeL );
+		stereo.cameraL.near = camera.near;
+		stereo.cameraL.far = camera.far;
+		frameCorners( stereo.cameraL, _screenBottomLeft, _screenBottomRight, _screenTopLeft, true );
+		stereo.cameraL.matrixWorld.compose( stereo.cameraL.position, stereo.cameraL.quaternion, stereo.cameraL.scale );
+		stereo.cameraL.matrixWorldInverse.copy( stereo.cameraL.matrixWorld ).invert();
+
+		// Set up right eye camera
+		stereo.cameraR.position.copy( _eyeR );
+		stereo.cameraR.near = camera.near;
+		stereo.cameraR.far = camera.far;
+		frameCorners( stereo.cameraR, _screenBottomLeft, _screenBottomRight, _screenTopLeft, true );
+		stereo.cameraR.matrixWorld.compose( stereo.cameraR.position, stereo.cameraR.quaternion, stereo.cameraR.scale );
+		stereo.cameraR.matrixWorldInverse.copy( stereo.cameraR.matrixWorld ).invert();
+
+	}
+
 	/**
 	/**
 	 * This method is used to setup the effect's TSL code.
 	 * This method is used to setup the effect's TSL code.
 	 *
 	 *

BIN
examples/screenshots/webgl_effects_anaglyph.jpg


BIN
examples/screenshots/webgpu_display_stereo.jpg


+ 6 - 0
examples/webgl_effects_anaglyph.html

@@ -94,6 +94,12 @@
 				effect = new AnaglyphEffect( renderer );
 				effect = new AnaglyphEffect( renderer );
 				effect.setSize( width, height );
 				effect.setSize( width, height );
 
 
+				// Configure stereo parameters for physically-correct rendering
+				// eyeSep: interpupillary distance (default 0.064m / 64mm for humans)
+				// screenDistance: distance to the zero-parallax plane (objects here appear at screen depth)
+				effect.eyeSep = 0.064;
+				effect.screenDistance = 3; // Match camera distance to origin for zero parallax at scene center
+
 				//
 				//
 
 
 				window.addEventListener( 'resize', onWindowResize );
 				window.addEventListener( 'resize', onWindowResize );

+ 13 - 4
examples/webgpu_display_stereo.html

@@ -54,6 +54,7 @@
 			const params = {
 			const params = {
 				effect: 'stereo',
 				effect: 'stereo',
 				eyeSep: 0.064,
 				eyeSep: 0.064,
+				screenDistance: 3,
 				anaglyphAlgorithm: 'dubois',
 				anaglyphAlgorithm: 'dubois',
 				anaglyphColorMode: 'redCyan'
 				anaglyphColorMode: 'redCyan'
 			};
 			};
@@ -101,7 +102,7 @@
 
 
 				mesh = new THREE.InstancedMesh( geometry, material, 500 );
 				mesh = new THREE.InstancedMesh( geometry, material, 500 );
 				mesh.instanceMatrix.setUsage( THREE.DynamicDrawUsage );
 				mesh.instanceMatrix.setUsage( THREE.DynamicDrawUsage );
-			
+
 				dummy = new THREE.Mesh();
 				dummy = new THREE.Mesh();
 
 
 				for ( let i = 0; i < 500; i ++ ) {
 				for ( let i = 0; i < 500; i ++ ) {
@@ -110,7 +111,7 @@
 					dummy.position.y = Math.random() * 10 - 5;
 					dummy.position.y = Math.random() * 10 - 5;
 					dummy.position.z = Math.random() * 10 - 5;
 					dummy.position.z = Math.random() * 10 - 5;
 					dummy.scale.x = dummy.scale.y = dummy.scale.z = Math.random() * 3 + 1;
 					dummy.scale.x = dummy.scale.y = dummy.scale.z = Math.random() * 3 + 1;
-			
+
 					dummy.updateMatrix();
 					dummy.updateMatrix();
 
 
 					mesh.setMatrixAt( i, dummy.matrix );
 					mesh.setMatrixAt( i, dummy.matrix );
@@ -133,6 +134,10 @@
 				anaglyph = anaglyphPass( scene, camera );
 				anaglyph = anaglyphPass( scene, camera );
 				parallaxBarrier = parallaxBarrierPass( scene, camera );
 				parallaxBarrier = parallaxBarrierPass( scene, camera );
 
 
+				// Configure anaglyph for physically-correct stereo with zero parallax at scene center
+				anaglyph.eyeSep = params.eyeSep;
+				anaglyph.screenDistance = params.screenDistance;
+
 				renderPipeline.outputNode = stereo;
 				renderPipeline.outputNode = stereo;
 
 
 				const gui = renderer.inspector.createParameters( 'Stereo Settings' );
 				const gui = renderer.inspector.createParameters( 'Stereo Settings' );
@@ -140,8 +145,7 @@
 				gui.add( params, 'eyeSep', 0.001, 0.15, 0.001 ).onChange( function ( value ) {
 				gui.add( params, 'eyeSep', 0.001, 0.15, 0.001 ).onChange( function ( value ) {
 
 
 					stereo.stereo.eyeSep = value;
 					stereo.stereo.eyeSep = value;
-
-					anaglyph.stereo.eyeSep = value;
+					anaglyph.eyeSep = value; // Anaglyph has direct eyeSep property
 					parallaxBarrier.stereo.eyeSep = value;
 					parallaxBarrier.stereo.eyeSep = value;
 
 
 				} );
 				} );
@@ -157,6 +161,11 @@
 
 
 					anaglyph.colorMode = value;
 					anaglyph.colorMode = value;
 
 
+				} );
+				anaglyphFolder.add( params, 'screenDistance', 0.5, 10, 0.1 ).name( 'Screen Distance' ).onChange( function ( value ) {
+
+					anaglyph.screenDistance = value;
+
 				} );
 				} );
 				anaglyphFolder.paramList.domElement.style.display = 'none';
 				anaglyphFolder.paramList.domElement.style.display = 'none';
 
 

粤ICP备19079148号