Accueil / npm / three@0.186.1 / examples / jsm / tsl / display / AnaglyphPassNode.js

AnaglyphPassNode.js

import { Matrix3, NodeMaterial, Vector3 } from 'three/webgpu';
import { clamp, Fn, vec4, uv, uniform, max, context } 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.
 * @readonly
 * @enum {string}
 */
const AnaglyphAlgorithm = {
	TRUE: 'true',
	GREY: 'grey',
	COLOUR: 'colour',
	HALF_COLOUR: 'halfColour',
	DUBOIS: 'dubois',
	OPTIMISED: 'optimised',
	COMPROMISE: 'compromise'
};

/**
 * Anaglyph color modes.
 * @readonly
 * @enum {string}
 */
const AnaglyphColorMode = {
	RED_CYAN: 'redCyan',
	MAGENTA_CYAN: 'magentaCyan',
	MAGENTA_GREEN: 'magentaGreen'
};

/**
 * Standard luminance coefficients (ITU-R BT.601).
 * @private
 */
const LUMINANCE = { R: 0.299, G: 0.587, B: 0.114 };

/**
 * Creates an anaglyph matrix pair from left and right channel specifications.
 * This provides a more intuitive way to define how source RGB channels map to output RGB channels.
 *
 * Each specification object has keys 'r', 'g', 'b' for output channels.
 * Each output channel value is [rCoef, gCoef, bCoef] defining how much of each input channel contributes.
 *
 * @private
 * @param {Object} leftSpec - Specification for left eye contribution
 * @param {Object} rightSpec - Specification for right eye contribution
 * @returns {{left: number[], right: number[]}} Column-major arrays for Matrix3
 */
function createMatrixPair( leftSpec, rightSpec ) {

	// Convert row-major specification to column-major array for Matrix3
	// Matrix3.fromArray expects [col0row0, col0row1, col0row2, col1row0, col1row1, col1row2, col2row0, col2row1, col2row2]
	// Which represents:
	// | col0row0  col1row0  col2row0 |   | m[0]  m[3]  m[6] |
	// | col0row1  col1row1  col2row1 | = | m[1]  m[4]  m[7] |
	// | col0row2  col1row2  col2row2 |   | m[2]  m[5]  m[8] |

	function specToColumnMajor( spec ) {

		const r = spec.r || [ 0, 0, 0 ]; // Output red channel coefficients [fromR, fromG, fromB]
		const g = spec.g || [ 0, 0, 0 ]; // Output green channel coefficients
		const b = spec.b || [ 0, 0, 0 ]; // Output blue channel coefficients

		// Row-major matrix would be:
		// | r[0]  r[1]  r[2] |  (how input RGB maps to output R)
		// | g[0]  g[1]  g[2] |  (how input RGB maps to output G)
		// | b[0]  b[1]  b[2] |  (how input RGB maps to output B)

		// Column-major for Matrix3:
		return [
			r[ 0 ], g[ 0 ], b[ 0 ], // Column 0: coefficients for input R
			r[ 1 ], g[ 1 ], b[ 1 ], // Column 1: coefficients for input G
			r[ 2 ], g[ 2 ], b[ 2 ] // Column 2: coefficients for input B
		];

	}

	return {
		left: specToColumnMajor( leftSpec ),
		right: specToColumnMajor( rightSpec )
	};

}

/**
 * Shorthand for luminance coefficients.
 * @private
 */
const LUM = [ LUMINANCE.R, LUMINANCE.G, LUMINANCE.B ];

/**
 * Conversion matrices for different anaglyph algorithms.
 * Based on research from "Introducing a New Anaglyph Method: Compromise Anaglyph" by Jure Ahtik
 * and various other sources.
 *
 * Matrices are defined using createMatrixPair for clarity:
 * - Each spec object defines how input RGB maps to output RGB
 * - Keys 'r', 'g', 'b' represent output channels
 * - Values are [rCoef, gCoef, bCoef] for input channel contribution
 *
 * @private
 */
const ANAGLYPH_MATRICES = {

	// True Anaglyph - Red channel from left, luminance to cyan channel for right
	// Paper: Left=[R,0,0], Right=[0,0,Lum]
	[ AnaglyphAlgorithm.TRUE ]: {
		[ AnaglyphColorMode.RED_CYAN ]: createMatrixPair(
			{ r: [ 1, 0, 0 ] }, // Left: R -> outR
			{ g: LUM, b: LUM } // Right: Lum -> outG, Lum -> outB
		),
		[ AnaglyphColorMode.MAGENTA_CYAN ]: createMatrixPair(
			{ r: [ 1, 0, 0 ], b: [ 0, 0, 0.5 ] }, // Left: R -> outR, partial B -> outB
			{ g: LUM, b: [ 0, 0, 0.5 ] } // Right: Lum -> outG, partial B
		),
		[ AnaglyphColorMode.MAGENTA_GREEN ]: createMatrixPair(
			{ r: [ 1, 0, 0 ], b: LUM }, // Left: R -> outR, Lum -> outB
			{ g: LUM } // Right: Lum -> outG
		)
	},

	// Grey Anaglyph - Luminance-based, no color, minimal ghosting
	// Paper: Left=[Lum,0,0], Right=[0,0,Lum]
	[ AnaglyphAlgorithm.GREY ]: {
		[ AnaglyphColorMode.RED_CYAN ]: createMatrixPair(
			{ r: LUM }, // Left: Lum -> outR
			{ g: LUM, b: LUM } // Right: Lum -> outG, Lum -> outB
		),
		[ AnaglyphColorMode.MAGENTA_CYAN ]: createMatrixPair(
			{ r: LUM, b: [ 0.15, 0.29, 0.06 ] }, // Left: Lum -> outR, half-Lum -> outB
			{ g: LUM, b: [ 0.15, 0.29, 0.06 ] } // Right: Lum -> outG, half-Lum -> outB
		),
		[ AnaglyphColorMode.MAGENTA_GREEN ]: createMatrixPair(
			{ r: LUM, b: LUM }, // Left: Lum -> outR, Lum -> outB
			{ g: LUM } // Right: Lum -> outG
		)
	},

	// Colour Anaglyph - Full color, high retinal rivalry
	// Paper: Left=[R,0,0], Right=[0,G,B]
	[ AnaglyphAlgorithm.COLOUR ]: {
		[ AnaglyphColorMode.RED_CYAN ]: createMatrixPair(
			{ r: [ 1, 0, 0 ] }, // Left: R -> outR
			{ g: [ 0, 1, 0 ], b: [ 0, 0, 1 ] } // Right: G -> outG, B -> outB
		),
		[ AnaglyphColorMode.MAGENTA_CYAN ]: createMatrixPair(
			{ r: [ 1, 0, 0 ], b: [ 0, 0, 0.5 ] }, // Left: R -> outR, partial B -> outB
			{ g: [ 0, 1, 0 ], b: [ 0, 0, 0.5 ] } // Right: G -> outG, partial B -> outB
		),
		[ AnaglyphColorMode.MAGENTA_GREEN ]: createMatrixPair(
			{ r: [ 1, 0, 0 ], b: [ 0, 0, 1 ] }, // Left: R -> outR, B -> outB
			{ g: [ 0, 1, 0 ] } // Right: G -> outG
		)
	},

	// Half-Colour Anaglyph - Luminance for left red, full color for right cyan
	// Paper: Left=[Lum,0,0], Right=[0,G,B]
	[ AnaglyphAlgorithm.HALF_COLOUR ]: {
		[ AnaglyphColorMode.RED_CYAN ]: createMatrixPair(
			{ r: LUM }, // Left: Lum -> outR
			{ g: [ 0, 1, 0 ], b: [ 0, 0, 1 ] } // Right: G -> outG, B -> outB
		),
		[ AnaglyphColorMode.MAGENTA_CYAN ]: createMatrixPair(
			{ r: LUM, b: [ 0.15, 0.29, 0.06 ] }, // Left: Lum -> outR, half-Lum -> outB
			{ g: [ 0, 1, 0 ], b: [ 0.15, 0.29, 0.06 ] } // Right: G -> outG, half-Lum -> outB
		),
		[ AnaglyphColorMode.MAGENTA_GREEN ]: createMatrixPair(
			{ r: LUM, b: LUM }, // Left: Lum -> outR, Lum -> outB
			{ g: [ 0, 1, 0 ] } // Right: G -> outG
		)
	},

	// Dubois Anaglyph - Least-squares optimized for specific glasses
	// From https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.7.6968&rep=rep1&type=pdf
	[ AnaglyphAlgorithm.DUBOIS ]: {
		[ AnaglyphColorMode.RED_CYAN ]: createMatrixPair(
			{
				r: [ 0.4561, 0.500484, 0.176381 ],
				g: [ - 0.0400822, - 0.0378246, - 0.0157589 ],
				b: [ - 0.0152161, - 0.0205971, - 0.00546856 ]
			},
			{
				r: [ - 0.0434706, - 0.0879388, - 0.00155529 ],
				g: [ 0.378476, 0.73364, - 0.0184503 ],
				b: [ - 0.0721527, - 0.112961, 1.2264 ]
			}
		),
		[ AnaglyphColorMode.MAGENTA_CYAN ]: createMatrixPair(
			{
				r: [ 0.4561, 0.500484, 0.176381 ],
				g: [ - 0.0400822, - 0.0378246, - 0.0157589 ],
				b: [ 0.088, 0.088, - 0.003 ]
			},
			{
				r: [ - 0.0434706, - 0.0879388, - 0.00155529 ],
				g: [ 0.378476, 0.73364, - 0.0184503 ],
				b: [ 0.088, 0.088, 0.613 ]
			}
		),
		[ AnaglyphColorMode.MAGENTA_GREEN ]: createMatrixPair(
			{
				r: [ 0.4561, 0.500484, 0.176381 ],
				b: [ - 0.0434706, - 0.0879388, - 0.00155529 ]
			},
			{
				g: [ 0.378476 + 0.4561, 0.73364 + 0.500484, - 0.0184503 + 0.176381 ]
			}
		)
	},

	// Optimised Anaglyph - Improved color with reduced retinal rivalry
	// Paper: Left=[0,0.7G+0.3B,0,0], Right=[0,G,B]
	[ AnaglyphAlgorithm.OPTIMISED ]: {
		[ AnaglyphColorMode.RED_CYAN ]: createMatrixPair(
			{ r: [ 0, 0.7, 0.3 ] }, // Left: 0.7G+0.3B -> outR
			{ g: [ 0, 1, 0 ], b: [ 0, 0, 1 ] } // Right: G -> outG, B -> outB
		),
		[ AnaglyphColorMode.MAGENTA_CYAN ]: createMatrixPair(
			{ r: [ 0, 0.7, 0.3 ], b: [ 0, 0, 0.5 ] }, // Left: 0.7G+0.3B -> outR, partial B
			{ g: [ 0, 1, 0 ], b: [ 0, 0, 0.5 ] } // Right: G -> outG, partial B
		),
		[ AnaglyphColorMode.MAGENTA_GREEN ]: createMatrixPair(
			{ r: [ 0, 0.7, 0.3 ], b: [ 0, 0, 1 ] }, // Left: 0.7G+0.3B -> outR, B -> outB
			{ g: [ 0, 1, 0 ] } // Right: G -> outG
		)
	},

	// Compromise Anaglyph - Best balance of color and stereo effect
	// From Ahtik, J., "Techniques of Rendering Anaglyphs for Use in Art"
	// Paper matrix [8]: Left=[0.439R+0.447G+0.148B, 0, 0], Right=[0, 0.095R+0.934G+0.005B, 0.018R+0.028G+1.057B]
	[ AnaglyphAlgorithm.COMPROMISE ]: {
		[ AnaglyphColorMode.RED_CYAN ]: createMatrixPair(
			{ r: [ 0.439, 0.447, 0.148 ] }, // Left: weighted RGB -> outR
			{
				g: [ 0.095, 0.934, 0.005 ], // Right: weighted RGB -> outG
				b: [ 0.018, 0.028, 1.057 ] // Right: weighted RGB -> outB
			}
		),
		[ AnaglyphColorMode.MAGENTA_CYAN ]: createMatrixPair(
			{
				r: [ 0.439, 0.447, 0.148 ],
				b: [ 0.009, 0.014, 0.074 ] // Partial blue from left
			},
			{
				g: [ 0.095, 0.934, 0.005 ],
				b: [ 0.009, 0.014, 0.528 ] // Partial blue from right
			}
		),
		[ AnaglyphColorMode.MAGENTA_GREEN ]: createMatrixPair(
			{
				r: [ 0.439, 0.447, 0.148 ],
				b: [ 0.018, 0.028, 1.057 ]
			},
			{
				g: [ 0.095 + 0.439, 0.934 + 0.447, 0.005 + 0.148 ]
			}
		)
	}
};

/**
 * 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 plane distance.
 *
 * @augments StereoCompositePassNode
 * @three_import import { anaglyphPass, AnaglyphAlgorithm, AnaglyphColorMode } from 'three/addons/tsl/display/AnaglyphPassNode.js';
 */
class AnaglyphPassNode extends StereoCompositePassNode {

	static get type() {

		return 'AnaglyphPassNode';

	}

	/**
	 * Constructs a new anaglyph pass node.
	 *
	 * @param {Scene} scene - The scene to render.
	 * @param {Camera} camera - The camera to render the scene with.
	 */
	constructor( scene, camera ) {

		super( scene, camera );

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default 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 in world units 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.planeDistance = 0.5;

		/**
		 * The current anaglyph algorithm.
		 *
		 * @private
		 * @type {string}
		 * @default 'dubois'
		 */
		this._algorithm = AnaglyphAlgorithm.DUBOIS;

		/**
		 * The current color mode.
		 *
		 * @private
		 * @type {string}
		 * @default 'redCyan'
		 */
		this._colorMode = AnaglyphColorMode.RED_CYAN;

		/**
		 * Color matrix node for the left eye.
		 *
		 * @private
		 * @type {UniformNode<mat3>}
		 */
		this._colorMatrixLeft = uniform( new Matrix3() );

		/**
		 * Color matrix node for the right eye.
		 *
		 * @private
		 * @type {UniformNode<mat3>}
		 */
		this._colorMatrixRight = uniform( new Matrix3() );

		// Initialize with default matrices
		this._updateMatrices();

	}

	/**
	 * Gets the current anaglyph algorithm.
	 *
	 * @type {string}
	 */
	get algorithm() {

		return this._algorithm;

	}

	/**
	 * Sets the anaglyph algorithm.
	 *
	 * @type {string}
	 */
	set algorithm( value ) {

		if ( this._algorithm !== value ) {

			this._algorithm = value;
			this._updateMatrices();

		}

	}

	/**
	 * Gets the current color mode.
	 *
	 * @type {string}
	 */
	get colorMode() {

		return this._colorMode;

	}

	/**
	 * Sets the color mode.
	 *
	 * @type {string}
	 */
	set colorMode( value ) {

		if ( this._colorMode !== value ) {

			this._colorMode = value;
			this._updateMatrices();

		}

	}

	/**
	 * Updates the color matrices based on current algorithm and color mode.
	 *
	 * @private
	 */
	_updateMatrices() {

		const matrices = ANAGLYPH_MATRICES[ this._algorithm ][ this._colorMode ];

		this._colorMatrixLeft.value.fromArray( matrices.left );
		this._colorMatrixRight.value.fromArray( matrices.right );

	}

	/**
	 * 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 planeDistance in front of the camera center)
		_screenCenter.copy( camera.position ).addScaledVector( _forward, - this.planeDistance );

		// Calculate screen dimensions from camera FOV and aspect ratio
		const DEG2RAD = Math.PI / 180;
		const halfHeight = this.planeDistance * 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.
	 *
	 * @param {NodeBuilder} builder - The current node builder.
	 * @return {PassTextureNode}
	 */
	setup( builder ) {

		const uvNode = uv();

		const anaglyph = Fn( () => {

			const colorL = this._mapLeft.sample( uvNode );
			const colorR = this._mapRight.sample( 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 = new NodeMaterial() );
		material.contextNode = context( builder.getSharedContext() );
		material.fragmentNode = anaglyph();
		material.name = 'Anaglyph';
		material.needsUpdate = true;

		return super.setup( builder );

	}

}

export default AnaglyphPassNode;

export { AnaglyphAlgorithm, AnaglyphColorMode };

/**
 * TSL function for creating an anaglyph pass node.
 *
 * @tsl
 * @function
 * @param {Scene} scene - The scene to render.
 * @param {Camera} camera - The camera to render the scene with.
 * @returns {AnaglyphPassNode}
 */
export const anaglyphPass = ( scene, camera ) => new AnaglyphPassNode( scene, camera );

Exemple