|
@@ -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 );
|
|
|
|
|
|