AnaglyphEffect.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import {
  2. LinearFilter,
  3. MathUtils,
  4. Matrix3,
  5. NearestFilter,
  6. PerspectiveCamera,
  7. RGBAFormat,
  8. ShaderMaterial,
  9. Vector3,
  10. WebGLRenderTarget
  11. } from 'three';
  12. import { FullScreenQuad } from '../postprocessing/Pass.js';
  13. import { frameCorners } from '../utils/CameraUtils.js';
  14. const _cameraL = /*@__PURE__*/ new PerspectiveCamera();
  15. const _cameraR = /*@__PURE__*/ new PerspectiveCamera();
  16. // Reusable vectors for screen corner calculations
  17. const _eyeL = /*@__PURE__*/ new Vector3();
  18. const _eyeR = /*@__PURE__*/ new Vector3();
  19. const _screenCenter = /*@__PURE__*/ new Vector3();
  20. const _screenBottomLeft = /*@__PURE__*/ new Vector3();
  21. const _screenBottomRight = /*@__PURE__*/ new Vector3();
  22. const _screenTopLeft = /*@__PURE__*/ new Vector3();
  23. const _right = /*@__PURE__*/ new Vector3();
  24. const _up = /*@__PURE__*/ new Vector3();
  25. const _forward = /*@__PURE__*/ new Vector3();
  26. /**
  27. * A class that creates an anaglyph effect using physically-correct
  28. * off-axis stereo projection.
  29. *
  30. * This implementation uses CameraUtils.frameCorners() to align stereo
  31. * camera frustums to a virtual screen plane, providing accurate depth
  32. * perception with zero parallax at the plane distance.
  33. *
  34. * Note that this class can only be used with {@link WebGLRenderer}.
  35. * When using {@link WebGPURenderer}, use {@link AnaglyphPassNode}.
  36. *
  37. * @three_import import { AnaglyphEffect } from 'three/addons/effects/AnaglyphEffect.js';
  38. */
  39. class AnaglyphEffect {
  40. /**
  41. * Constructs a new anaglyph effect.
  42. *
  43. * @param {WebGLRenderer} renderer - The renderer.
  44. * @param {number} width - The width of the effect in physical pixels.
  45. * @param {number} height - The height of the effect in physical pixels.
  46. */
  47. constructor( renderer, width = 512, height = 512 ) {
  48. // Dubois matrices from https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.7.6968&rep=rep1&type=pdf#page=4
  49. this.colorMatrixLeft = new Matrix3().fromArray( [
  50. 0.456100, - 0.0400822, - 0.0152161,
  51. 0.500484, - 0.0378246, - 0.0205971,
  52. 0.176381, - 0.0157589, - 0.00546856
  53. ] );
  54. this.colorMatrixRight = new Matrix3().fromArray( [
  55. - 0.0434706, 0.378476, - 0.0721527,
  56. - 0.0879388, 0.73364, - 0.112961,
  57. - 0.00155529, - 0.0184503, 1.2264
  58. ] );
  59. /**
  60. * The interpupillary distance (eye separation) in world units.
  61. * Typical human IPD is 0.064 meters (64mm).
  62. *
  63. * @type {number}
  64. * @default 0.064
  65. */
  66. this.eyeSep = 0.064;
  67. /**
  68. * The distance in world units from the viewer to the virtual
  69. * screen plane where zero parallax (screen depth) occurs.
  70. * Objects at this distance appear at the screen surface.
  71. * Objects closer appear in front of the screen (negative parallax).
  72. * Objects further appear behind the screen (positive parallax).
  73. *
  74. * The screen dimensions are derived from the camera's FOV and aspect ratio
  75. * at this distance, ensuring the stereo view matches the camera's field of view.
  76. *
  77. * @type {number}
  78. * @default 0.5
  79. */
  80. this.planeDistance = 0.5;
  81. const _params = { minFilter: LinearFilter, magFilter: NearestFilter, format: RGBAFormat };
  82. const _renderTargetL = new WebGLRenderTarget( width, height, _params );
  83. const _renderTargetR = new WebGLRenderTarget( width, height, _params );
  84. _cameraL.layers.enable( 1 );
  85. _cameraR.layers.enable( 2 );
  86. const _material = new ShaderMaterial( {
  87. uniforms: {
  88. 'mapLeft': { value: _renderTargetL.texture },
  89. 'mapRight': { value: _renderTargetR.texture },
  90. 'colorMatrixLeft': { value: this.colorMatrixLeft },
  91. 'colorMatrixRight': { value: this.colorMatrixRight }
  92. },
  93. vertexShader: [
  94. 'varying vec2 vUv;',
  95. 'void main() {',
  96. ' vUv = vec2( uv.x, uv.y );',
  97. ' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
  98. '}'
  99. ].join( '\n' ),
  100. fragmentShader: [
  101. 'uniform sampler2D mapLeft;',
  102. 'uniform sampler2D mapRight;',
  103. 'varying vec2 vUv;',
  104. 'uniform mat3 colorMatrixLeft;',
  105. 'uniform mat3 colorMatrixRight;',
  106. 'void main() {',
  107. ' vec2 uv = vUv;',
  108. ' vec4 colorL = texture2D( mapLeft, uv );',
  109. ' vec4 colorR = texture2D( mapRight, uv );',
  110. ' vec3 color = clamp(',
  111. ' colorMatrixLeft * colorL.rgb +',
  112. ' colorMatrixRight * colorR.rgb, 0., 1. );',
  113. ' gl_FragColor = vec4(',
  114. ' color.r, color.g, color.b,',
  115. ' max( colorL.a, colorR.a ) );',
  116. ' #include <tonemapping_fragment>',
  117. ' #include <colorspace_fragment>',
  118. '}'
  119. ].join( '\n' )
  120. } );
  121. const _quad = new FullScreenQuad( _material );
  122. /**
  123. * Resizes the effect.
  124. *
  125. * @param {number} width - The width of the effect in logical pixels.
  126. * @param {number} height - The height of the effect in logical pixels.
  127. */
  128. this.setSize = function ( width, height ) {
  129. renderer.setSize( width, height );
  130. const pixelRatio = renderer.getPixelRatio();
  131. _renderTargetL.setSize( width * pixelRatio, height * pixelRatio );
  132. _renderTargetR.setSize( width * pixelRatio, height * pixelRatio );
  133. };
  134. /**
  135. * When using this effect, this method should be called instead of the
  136. * default {@link WebGLRenderer#render}.
  137. *
  138. * @param {Object3D} scene - The scene to render.
  139. * @param {Camera} camera - The camera.
  140. */
  141. this.render = function ( scene, camera ) {
  142. const currentRenderTarget = renderer.getRenderTarget();
  143. if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();
  144. if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();
  145. // Get the camera's local coordinate axes from its world matrix
  146. camera.matrixWorld.extractBasis( _right, _up, _forward );
  147. _right.normalize();
  148. _up.normalize();
  149. _forward.normalize();
  150. // Calculate eye positions
  151. const halfSep = this.eyeSep / 2;
  152. _eyeL.copy( camera.position ).addScaledVector( _right, - halfSep );
  153. _eyeR.copy( camera.position ).addScaledVector( _right, halfSep );
  154. // Calculate screen center (at planeDistance in front of the camera center)
  155. _screenCenter.copy( camera.position ).addScaledVector( _forward, - this.planeDistance );
  156. // Calculate screen dimensions from camera FOV and aspect ratio
  157. const halfHeight = this.planeDistance * Math.tan( MathUtils.DEG2RAD * camera.fov / 2 );
  158. const halfWidth = halfHeight * camera.aspect;
  159. // Calculate screen corners
  160. _screenBottomLeft.copy( _screenCenter )
  161. .addScaledVector( _right, - halfWidth )
  162. .addScaledVector( _up, - halfHeight );
  163. _screenBottomRight.copy( _screenCenter )
  164. .addScaledVector( _right, halfWidth )
  165. .addScaledVector( _up, - halfHeight );
  166. _screenTopLeft.copy( _screenCenter )
  167. .addScaledVector( _right, - halfWidth )
  168. .addScaledVector( _up, halfHeight );
  169. // Set up left eye camera
  170. _cameraL.position.copy( _eyeL );
  171. _cameraL.near = camera.near;
  172. _cameraL.far = camera.far;
  173. frameCorners( _cameraL, _screenBottomLeft, _screenBottomRight, _screenTopLeft, true );
  174. _cameraL.matrixWorld.compose( _cameraL.position, _cameraL.quaternion, _cameraL.scale );
  175. _cameraL.matrixWorldInverse.copy( _cameraL.matrixWorld ).invert();
  176. // Set up right eye camera
  177. _cameraR.position.copy( _eyeR );
  178. _cameraR.near = camera.near;
  179. _cameraR.far = camera.far;
  180. frameCorners( _cameraR, _screenBottomLeft, _screenBottomRight, _screenTopLeft, true );
  181. _cameraR.matrixWorld.compose( _cameraR.position, _cameraR.quaternion, _cameraR.scale );
  182. _cameraR.matrixWorldInverse.copy( _cameraR.matrixWorld ).invert();
  183. // Render left eye
  184. renderer.setRenderTarget( _renderTargetL );
  185. renderer.clear();
  186. renderer.render( scene, _cameraL );
  187. // Render right eye
  188. renderer.setRenderTarget( _renderTargetR );
  189. renderer.clear();
  190. renderer.render( scene, _cameraR );
  191. // Composite anaglyph
  192. renderer.setRenderTarget( null );
  193. _quad.render( renderer );
  194. renderer.setRenderTarget( currentRenderTarget );
  195. };
  196. /**
  197. * Frees internal resources. This method should be called
  198. * when the effect is no longer required.
  199. */
  200. this.dispose = function () {
  201. _renderTargetL.dispose();
  202. _renderTargetR.dispose();
  203. _material.dispose();
  204. _quad.dispose();
  205. };
  206. }
  207. }
  208. export { AnaglyphEffect };
粤ICP备19079148号