Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 102 additions & 7 deletions examples/jsm/effects/AnaglyphEffect.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,38 @@
import {
LinearFilter,
MathUtils,
Matrix3,
NearestFilter,
PerspectiveCamera,
RGBAFormat,
ShaderMaterial,
StereoCamera,
Vector3,
WebGLRenderTarget
} from 'three';
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}.
* When using {@link WebGPURenderer}, use {@link AnaglyphPassNode}.
Expand Down Expand Up @@ -42,13 +64,38 @@ class AnaglyphEffect {
- 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 _renderTargetL = new WebGLRenderTarget( width, height, _params );
const _renderTargetR = new WebGLRenderTarget( width, height, _params );

_cameraL.layers.enable( 1 );
_cameraR.layers.enable( 2 );

const _material = new ShaderMaterial( {

uniforms: {
Expand Down Expand Up @@ -141,16 +188,64 @@ class AnaglyphEffect {

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.clear();
renderer.render( scene, _stereo.cameraL );
renderer.render( scene, _cameraL );

// Render right eye
renderer.setRenderTarget( _renderTargetR );
renderer.clear();
renderer.render( scene, _stereo.cameraR );
renderer.render( scene, _cameraR );

// Composite anaglyph
renderer.setRenderTarget( null );
_quad.render( renderer );

Expand Down
107 changes: 105 additions & 2 deletions examples/jsm/tsl/display/AnaglyphPassNode.js
Original file line number Diff line number Diff line change
@@ -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 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.
Expand Down Expand Up @@ -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
* @three_import import { anaglyphPass, AnaglyphAlgorithm, AnaglyphColorMode } from 'three/addons/tsl/display/AnaglyphPassNode.js';
Expand Down Expand Up @@ -291,6 +307,30 @@ class AnaglyphPassNode extends StereoCompositePassNode {
*/
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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
Binary file modified examples/screenshots/webgl_effects_anaglyph.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/screenshots/webgpu_display_stereo.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions examples/webgl_effects_anaglyph.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@
effect = new AnaglyphEffect( renderer );
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 );
Expand Down
17 changes: 13 additions & 4 deletions examples/webgpu_display_stereo.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
const params = {
effect: 'stereo',
eyeSep: 0.064,
screenDistance: 3,
anaglyphAlgorithm: 'dubois',
anaglyphColorMode: 'redCyan'
};
Expand Down Expand Up @@ -101,7 +102,7 @@

mesh = new THREE.InstancedMesh( geometry, material, 500 );
mesh.instanceMatrix.setUsage( THREE.DynamicDrawUsage );

dummy = new THREE.Mesh();

for ( let i = 0; i < 500; i ++ ) {
Expand All @@ -110,7 +111,7 @@
dummy.position.y = 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.updateMatrix();

mesh.setMatrixAt( i, dummy.matrix );
Expand All @@ -133,15 +134,18 @@
anaglyph = anaglyphPass( 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;

const gui = renderer.inspector.createParameters( 'Stereo Settings' );
gui.add( params, 'effect', effects ).onChange( update );
gui.add( params, 'eyeSep', 0.001, 0.15, 0.001 ).onChange( function ( value ) {

stereo.stereo.eyeSep = value;

anaglyph.stereo.eyeSep = value;
anaglyph.eyeSep = value; // Anaglyph has direct eyeSep property
parallaxBarrier.stereo.eyeSep = value;

} );
Expand All @@ -157,6 +161,11 @@

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';

Expand Down