AnaglyphPassNode.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. import { Matrix3, NodeMaterial, Vector3 } from 'three/webgpu';
  2. import { clamp, Fn, vec4, uv, uniform, max } from 'three/tsl';
  3. import StereoCompositePassNode from './StereoCompositePassNode.js';
  4. import { frameCorners } from '../../utils/CameraUtils.js';
  5. const _eyeL = /*@__PURE__*/ new Vector3();
  6. const _eyeR = /*@__PURE__*/ new Vector3();
  7. const _screenBottomLeft = /*@__PURE__*/ new Vector3();
  8. const _screenBottomRight = /*@__PURE__*/ new Vector3();
  9. const _screenTopLeft = /*@__PURE__*/ new Vector3();
  10. const _right = /*@__PURE__*/ new Vector3();
  11. const _up = /*@__PURE__*/ new Vector3();
  12. const _forward = /*@__PURE__*/ new Vector3();
  13. const _screenCenter = /*@__PURE__*/ new Vector3();
  14. /**
  15. * Anaglyph algorithm types.
  16. * @readonly
  17. * @enum {string}
  18. */
  19. const AnaglyphAlgorithm = {
  20. TRUE: 'true',
  21. GREY: 'grey',
  22. COLOUR: 'colour',
  23. HALF_COLOUR: 'halfColour',
  24. DUBOIS: 'dubois',
  25. OPTIMISED: 'optimised',
  26. COMPROMISE: 'compromise'
  27. };
  28. /**
  29. * Anaglyph color modes.
  30. * @readonly
  31. * @enum {string}
  32. */
  33. const AnaglyphColorMode = {
  34. RED_CYAN: 'redCyan',
  35. MAGENTA_CYAN: 'magentaCyan',
  36. MAGENTA_GREEN: 'magentaGreen'
  37. };
  38. /**
  39. * Standard luminance coefficients (ITU-R BT.601).
  40. * @private
  41. */
  42. const LUMINANCE = { R: 0.299, G: 0.587, B: 0.114 };
  43. /**
  44. * Creates an anaglyph matrix pair from left and right channel specifications.
  45. * This provides a more intuitive way to define how source RGB channels map to output RGB channels.
  46. *
  47. * Each specification object has keys 'r', 'g', 'b' for output channels.
  48. * Each output channel value is [rCoef, gCoef, bCoef] defining how much of each input channel contributes.
  49. *
  50. * @private
  51. * @param {Object} leftSpec - Specification for left eye contribution
  52. * @param {Object} rightSpec - Specification for right eye contribution
  53. * @returns {{left: number[], right: number[]}} Column-major arrays for Matrix3
  54. */
  55. function createMatrixPair( leftSpec, rightSpec ) {
  56. // Convert row-major specification to column-major array for Matrix3
  57. // Matrix3.fromArray expects [col0row0, col0row1, col0row2, col1row0, col1row1, col1row2, col2row0, col2row1, col2row2]
  58. // Which represents:
  59. // | col0row0 col1row0 col2row0 | | m[0] m[3] m[6] |
  60. // | col0row1 col1row1 col2row1 | = | m[1] m[4] m[7] |
  61. // | col0row2 col1row2 col2row2 | | m[2] m[5] m[8] |
  62. function specToColumnMajor( spec ) {
  63. const r = spec.r || [ 0, 0, 0 ]; // Output red channel coefficients [fromR, fromG, fromB]
  64. const g = spec.g || [ 0, 0, 0 ]; // Output green channel coefficients
  65. const b = spec.b || [ 0, 0, 0 ]; // Output blue channel coefficients
  66. // Row-major matrix would be:
  67. // | r[0] r[1] r[2] | (how input RGB maps to output R)
  68. // | g[0] g[1] g[2] | (how input RGB maps to output G)
  69. // | b[0] b[1] b[2] | (how input RGB maps to output B)
  70. // Column-major for Matrix3:
  71. return [
  72. r[ 0 ], g[ 0 ], b[ 0 ], // Column 0: coefficients for input R
  73. r[ 1 ], g[ 1 ], b[ 1 ], // Column 1: coefficients for input G
  74. r[ 2 ], g[ 2 ], b[ 2 ] // Column 2: coefficients for input B
  75. ];
  76. }
  77. return {
  78. left: specToColumnMajor( leftSpec ),
  79. right: specToColumnMajor( rightSpec )
  80. };
  81. }
  82. /**
  83. * Shorthand for luminance coefficients.
  84. * @private
  85. */
  86. const LUM = [ LUMINANCE.R, LUMINANCE.G, LUMINANCE.B ];
  87. /**
  88. * Conversion matrices for different anaglyph algorithms.
  89. * Based on research from "Introducing a New Anaglyph Method: Compromise Anaglyph" by Jure Ahtik
  90. * and various other sources.
  91. *
  92. * Matrices are defined using createMatrixPair for clarity:
  93. * - Each spec object defines how input RGB maps to output RGB
  94. * - Keys 'r', 'g', 'b' represent output channels
  95. * - Values are [rCoef, gCoef, bCoef] for input channel contribution
  96. *
  97. * @private
  98. */
  99. const ANAGLYPH_MATRICES = {
  100. // True Anaglyph - Red channel from left, luminance to cyan channel for right
  101. // Paper: Left=[R,0,0], Right=[0,0,Lum]
  102. [ AnaglyphAlgorithm.TRUE ]: {
  103. [ AnaglyphColorMode.RED_CYAN ]: createMatrixPair(
  104. { r: [ 1, 0, 0 ] }, // Left: R -> outR
  105. { g: LUM, b: LUM } // Right: Lum -> outG, Lum -> outB
  106. ),
  107. [ AnaglyphColorMode.MAGENTA_CYAN ]: createMatrixPair(
  108. { r: [ 1, 0, 0 ], b: [ 0, 0, 0.5 ] }, // Left: R -> outR, partial B -> outB
  109. { g: LUM, b: [ 0, 0, 0.5 ] } // Right: Lum -> outG, partial B
  110. ),
  111. [ AnaglyphColorMode.MAGENTA_GREEN ]: createMatrixPair(
  112. { r: [ 1, 0, 0 ], b: LUM }, // Left: R -> outR, Lum -> outB
  113. { g: LUM } // Right: Lum -> outG
  114. )
  115. },
  116. // Grey Anaglyph - Luminance-based, no color, minimal ghosting
  117. // Paper: Left=[Lum,0,0], Right=[0,0,Lum]
  118. [ AnaglyphAlgorithm.GREY ]: {
  119. [ AnaglyphColorMode.RED_CYAN ]: createMatrixPair(
  120. { r: LUM }, // Left: Lum -> outR
  121. { g: LUM, b: LUM } // Right: Lum -> outG, Lum -> outB
  122. ),
  123. [ AnaglyphColorMode.MAGENTA_CYAN ]: createMatrixPair(
  124. { r: LUM, b: [ 0.15, 0.29, 0.06 ] }, // Left: Lum -> outR, half-Lum -> outB
  125. { g: LUM, b: [ 0.15, 0.29, 0.06 ] } // Right: Lum -> outG, half-Lum -> outB
  126. ),
  127. [ AnaglyphColorMode.MAGENTA_GREEN ]: createMatrixPair(
  128. { r: LUM, b: LUM }, // Left: Lum -> outR, Lum -> outB
  129. { g: LUM } // Right: Lum -> outG
  130. )
  131. },
  132. // Colour Anaglyph - Full color, high retinal rivalry
  133. // Paper: Left=[R,0,0], Right=[0,G,B]
  134. [ AnaglyphAlgorithm.COLOUR ]: {
  135. [ AnaglyphColorMode.RED_CYAN ]: createMatrixPair(
  136. { r: [ 1, 0, 0 ] }, // Left: R -> outR
  137. { g: [ 0, 1, 0 ], b: [ 0, 0, 1 ] } // Right: G -> outG, B -> outB
  138. ),
  139. [ AnaglyphColorMode.MAGENTA_CYAN ]: createMatrixPair(
  140. { r: [ 1, 0, 0 ], b: [ 0, 0, 0.5 ] }, // Left: R -> outR, partial B -> outB
  141. { g: [ 0, 1, 0 ], b: [ 0, 0, 0.5 ] } // Right: G -> outG, partial B -> outB
  142. ),
  143. [ AnaglyphColorMode.MAGENTA_GREEN ]: createMatrixPair(
  144. { r: [ 1, 0, 0 ], b: [ 0, 0, 1 ] }, // Left: R -> outR, B -> outB
  145. { g: [ 0, 1, 0 ] } // Right: G -> outG
  146. )
  147. },
  148. // Half-Colour Anaglyph - Luminance for left red, full color for right cyan
  149. // Paper: Left=[Lum,0,0], Right=[0,G,B]
  150. [ AnaglyphAlgorithm.HALF_COLOUR ]: {
  151. [ AnaglyphColorMode.RED_CYAN ]: createMatrixPair(
  152. { r: LUM }, // Left: Lum -> outR
  153. { g: [ 0, 1, 0 ], b: [ 0, 0, 1 ] } // Right: G -> outG, B -> outB
  154. ),
  155. [ AnaglyphColorMode.MAGENTA_CYAN ]: createMatrixPair(
  156. { r: LUM, b: [ 0.15, 0.29, 0.06 ] }, // Left: Lum -> outR, half-Lum -> outB
  157. { g: [ 0, 1, 0 ], b: [ 0.15, 0.29, 0.06 ] } // Right: G -> outG, half-Lum -> outB
  158. ),
  159. [ AnaglyphColorMode.MAGENTA_GREEN ]: createMatrixPair(
  160. { r: LUM, b: LUM }, // Left: Lum -> outR, Lum -> outB
  161. { g: [ 0, 1, 0 ] } // Right: G -> outG
  162. )
  163. },
  164. // Dubois Anaglyph - Least-squares optimized for specific glasses
  165. // From https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.7.6968&rep=rep1&type=pdf
  166. [ AnaglyphAlgorithm.DUBOIS ]: {
  167. [ AnaglyphColorMode.RED_CYAN ]: createMatrixPair(
  168. {
  169. r: [ 0.4561, 0.500484, 0.176381 ],
  170. g: [ - 0.0400822, - 0.0378246, - 0.0157589 ],
  171. b: [ - 0.0152161, - 0.0205971, - 0.00546856 ]
  172. },
  173. {
  174. r: [ - 0.0434706, - 0.0879388, - 0.00155529 ],
  175. g: [ 0.378476, 0.73364, - 0.0184503 ],
  176. b: [ - 0.0721527, - 0.112961, 1.2264 ]
  177. }
  178. ),
  179. [ AnaglyphColorMode.MAGENTA_CYAN ]: createMatrixPair(
  180. {
  181. r: [ 0.4561, 0.500484, 0.176381 ],
  182. g: [ - 0.0400822, - 0.0378246, - 0.0157589 ],
  183. b: [ 0.088, 0.088, - 0.003 ]
  184. },
  185. {
  186. r: [ - 0.0434706, - 0.0879388, - 0.00155529 ],
  187. g: [ 0.378476, 0.73364, - 0.0184503 ],
  188. b: [ 0.088, 0.088, 0.613 ]
  189. }
  190. ),
  191. [ AnaglyphColorMode.MAGENTA_GREEN ]: createMatrixPair(
  192. {
  193. r: [ 0.4561, 0.500484, 0.176381 ],
  194. b: [ - 0.0434706, - 0.0879388, - 0.00155529 ]
  195. },
  196. {
  197. g: [ 0.378476 + 0.4561, 0.73364 + 0.500484, - 0.0184503 + 0.176381 ]
  198. }
  199. )
  200. },
  201. // Optimised Anaglyph - Improved color with reduced retinal rivalry
  202. // Paper: Left=[0,0.7G+0.3B,0,0], Right=[0,G,B]
  203. [ AnaglyphAlgorithm.OPTIMISED ]: {
  204. [ AnaglyphColorMode.RED_CYAN ]: createMatrixPair(
  205. { r: [ 0, 0.7, 0.3 ] }, // Left: 0.7G+0.3B -> outR
  206. { g: [ 0, 1, 0 ], b: [ 0, 0, 1 ] } // Right: G -> outG, B -> outB
  207. ),
  208. [ AnaglyphColorMode.MAGENTA_CYAN ]: createMatrixPair(
  209. { r: [ 0, 0.7, 0.3 ], b: [ 0, 0, 0.5 ] }, // Left: 0.7G+0.3B -> outR, partial B
  210. { g: [ 0, 1, 0 ], b: [ 0, 0, 0.5 ] } // Right: G -> outG, partial B
  211. ),
  212. [ AnaglyphColorMode.MAGENTA_GREEN ]: createMatrixPair(
  213. { r: [ 0, 0.7, 0.3 ], b: [ 0, 0, 1 ] }, // Left: 0.7G+0.3B -> outR, B -> outB
  214. { g: [ 0, 1, 0 ] } // Right: G -> outG
  215. )
  216. },
  217. // Compromise Anaglyph - Best balance of color and stereo effect
  218. // From Ahtik, J., "Techniques of Rendering Anaglyphs for Use in Art"
  219. // 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]
  220. [ AnaglyphAlgorithm.COMPROMISE ]: {
  221. [ AnaglyphColorMode.RED_CYAN ]: createMatrixPair(
  222. { r: [ 0.439, 0.447, 0.148 ] }, // Left: weighted RGB -> outR
  223. {
  224. g: [ 0.095, 0.934, 0.005 ], // Right: weighted RGB -> outG
  225. b: [ 0.018, 0.028, 1.057 ] // Right: weighted RGB -> outB
  226. }
  227. ),
  228. [ AnaglyphColorMode.MAGENTA_CYAN ]: createMatrixPair(
  229. {
  230. r: [ 0.439, 0.447, 0.148 ],
  231. b: [ 0.009, 0.014, 0.074 ] // Partial blue from left
  232. },
  233. {
  234. g: [ 0.095, 0.934, 0.005 ],
  235. b: [ 0.009, 0.014, 0.528 ] // Partial blue from right
  236. }
  237. ),
  238. [ AnaglyphColorMode.MAGENTA_GREEN ]: createMatrixPair(
  239. {
  240. r: [ 0.439, 0.447, 0.148 ],
  241. b: [ 0.018, 0.028, 1.057 ]
  242. },
  243. {
  244. g: [ 0.095 + 0.439, 0.934 + 0.447, 0.005 + 0.148 ]
  245. }
  246. )
  247. }
  248. };
  249. /**
  250. * A render pass node that creates an anaglyph effect using physically-correct
  251. * off-axis stereo projection.
  252. *
  253. * This implementation uses CameraUtils.frameCorners() to align stereo
  254. * camera frustums to a virtual screen plane, providing accurate depth
  255. * perception with zero parallax at the plane distance.
  256. *
  257. * @augments StereoCompositePassNode
  258. * @three_import import { anaglyphPass, AnaglyphAlgorithm, AnaglyphColorMode } from 'three/addons/tsl/display/AnaglyphPassNode.js';
  259. */
  260. class AnaglyphPassNode extends StereoCompositePassNode {
  261. static get type() {
  262. return 'AnaglyphPassNode';
  263. }
  264. /**
  265. * Constructs a new anaglyph pass node.
  266. *
  267. * @param {Scene} scene - The scene to render.
  268. * @param {Camera} camera - The camera to render the scene with.
  269. */
  270. constructor( scene, camera ) {
  271. super( scene, camera );
  272. /**
  273. * This flag can be used for type testing.
  274. *
  275. * @type {boolean}
  276. * @readonly
  277. * @default true
  278. */
  279. this.isAnaglyphPassNode = true;
  280. /**
  281. * The interpupillary distance (eye separation) in world units.
  282. * Typical human IPD is 0.064 meters (64mm).
  283. *
  284. * @type {number}
  285. * @default 0.064
  286. */
  287. this.eyeSep = 0.064;
  288. /**
  289. * The distance in world units from the viewer to the virtual
  290. * screen plane where zero parallax (screen depth) occurs.
  291. * Objects at this distance appear at the screen surface.
  292. * Objects closer appear in front of the screen (negative parallax).
  293. * Objects further appear behind the screen (positive parallax).
  294. *
  295. * The screen dimensions are derived from the camera's FOV and aspect ratio
  296. * at this distance, ensuring the stereo view matches the camera's field of view.
  297. *
  298. * @type {number}
  299. * @default 0.5
  300. */
  301. this.planeDistance = 0.5;
  302. /**
  303. * The current anaglyph algorithm.
  304. *
  305. * @private
  306. * @type {string}
  307. * @default 'dubois'
  308. */
  309. this._algorithm = AnaglyphAlgorithm.DUBOIS;
  310. /**
  311. * The current color mode.
  312. *
  313. * @private
  314. * @type {string}
  315. * @default 'redCyan'
  316. */
  317. this._colorMode = AnaglyphColorMode.RED_CYAN;
  318. /**
  319. * Color matrix node for the left eye.
  320. *
  321. * @private
  322. * @type {UniformNode<mat3>}
  323. */
  324. this._colorMatrixLeft = uniform( new Matrix3() );
  325. /**
  326. * Color matrix node for the right eye.
  327. *
  328. * @private
  329. * @type {UniformNode<mat3>}
  330. */
  331. this._colorMatrixRight = uniform( new Matrix3() );
  332. // Initialize with default matrices
  333. this._updateMatrices();
  334. }
  335. /**
  336. * Gets the current anaglyph algorithm.
  337. *
  338. * @type {string}
  339. */
  340. get algorithm() {
  341. return this._algorithm;
  342. }
  343. /**
  344. * Sets the anaglyph algorithm.
  345. *
  346. * @type {string}
  347. */
  348. set algorithm( value ) {
  349. if ( this._algorithm !== value ) {
  350. this._algorithm = value;
  351. this._updateMatrices();
  352. }
  353. }
  354. /**
  355. * Gets the current color mode.
  356. *
  357. * @type {string}
  358. */
  359. get colorMode() {
  360. return this._colorMode;
  361. }
  362. /**
  363. * Sets the color mode.
  364. *
  365. * @type {string}
  366. */
  367. set colorMode( value ) {
  368. if ( this._colorMode !== value ) {
  369. this._colorMode = value;
  370. this._updateMatrices();
  371. }
  372. }
  373. /**
  374. * Updates the color matrices based on current algorithm and color mode.
  375. *
  376. * @private
  377. */
  378. _updateMatrices() {
  379. const matrices = ANAGLYPH_MATRICES[ this._algorithm ][ this._colorMode ];
  380. this._colorMatrixLeft.value.fromArray( matrices.left );
  381. this._colorMatrixRight.value.fromArray( matrices.right );
  382. }
  383. /**
  384. * Updates the internal stereo camera using frameCorners for
  385. * physically-correct off-axis projection.
  386. *
  387. * @param {number} coordinateSystem - The current coordinate system.
  388. */
  389. updateStereoCamera( coordinateSystem ) {
  390. const { stereo, camera } = this;
  391. stereo.cameraL.coordinateSystem = coordinateSystem;
  392. stereo.cameraR.coordinateSystem = coordinateSystem;
  393. // Get the camera's local coordinate axes from its world matrix
  394. camera.matrixWorld.extractBasis( _right, _up, _forward );
  395. _right.normalize();
  396. _up.normalize();
  397. _forward.normalize();
  398. // Calculate eye positions
  399. const halfSep = this.eyeSep / 2;
  400. _eyeL.copy( camera.position ).addScaledVector( _right, - halfSep );
  401. _eyeR.copy( camera.position ).addScaledVector( _right, halfSep );
  402. // Calculate screen center (at planeDistance in front of the camera center)
  403. _screenCenter.copy( camera.position ).addScaledVector( _forward, - this.planeDistance );
  404. // Calculate screen dimensions from camera FOV and aspect ratio
  405. const DEG2RAD = Math.PI / 180;
  406. const halfHeight = this.planeDistance * Math.tan( DEG2RAD * camera.fov / 2 );
  407. const halfWidth = halfHeight * camera.aspect;
  408. // Calculate screen corners
  409. _screenBottomLeft.copy( _screenCenter )
  410. .addScaledVector( _right, - halfWidth )
  411. .addScaledVector( _up, - halfHeight );
  412. _screenBottomRight.copy( _screenCenter )
  413. .addScaledVector( _right, halfWidth )
  414. .addScaledVector( _up, - halfHeight );
  415. _screenTopLeft.copy( _screenCenter )
  416. .addScaledVector( _right, - halfWidth )
  417. .addScaledVector( _up, halfHeight );
  418. // Set up left eye camera
  419. stereo.cameraL.position.copy( _eyeL );
  420. stereo.cameraL.near = camera.near;
  421. stereo.cameraL.far = camera.far;
  422. frameCorners( stereo.cameraL, _screenBottomLeft, _screenBottomRight, _screenTopLeft, true );
  423. stereo.cameraL.matrixWorld.compose( stereo.cameraL.position, stereo.cameraL.quaternion, stereo.cameraL.scale );
  424. stereo.cameraL.matrixWorldInverse.copy( stereo.cameraL.matrixWorld ).invert();
  425. // Set up right eye camera
  426. stereo.cameraR.position.copy( _eyeR );
  427. stereo.cameraR.near = camera.near;
  428. stereo.cameraR.far = camera.far;
  429. frameCorners( stereo.cameraR, _screenBottomLeft, _screenBottomRight, _screenTopLeft, true );
  430. stereo.cameraR.matrixWorld.compose( stereo.cameraR.position, stereo.cameraR.quaternion, stereo.cameraR.scale );
  431. stereo.cameraR.matrixWorldInverse.copy( stereo.cameraR.matrixWorld ).invert();
  432. }
  433. /**
  434. * This method is used to setup the effect's TSL code.
  435. *
  436. * @param {NodeBuilder} builder - The current node builder.
  437. * @return {PassTextureNode}
  438. */
  439. setup( builder ) {
  440. const uvNode = uv();
  441. const anaglyph = Fn( () => {
  442. const colorL = this._mapLeft.sample( uvNode );
  443. const colorR = this._mapRight.sample( uvNode );
  444. const color = clamp( this._colorMatrixLeft.mul( colorL.rgb ).add( this._colorMatrixRight.mul( colorR.rgb ) ) );
  445. return vec4( color.rgb, max( colorL.a, colorR.a ) );
  446. } );
  447. const material = this._material || ( this._material = new NodeMaterial() );
  448. material.fragmentNode = anaglyph().context( builder.getSharedContext() );
  449. material.name = 'Anaglyph';
  450. material.needsUpdate = true;
  451. return super.setup( builder );
  452. }
  453. }
  454. export default AnaglyphPassNode;
  455. export { AnaglyphAlgorithm, AnaglyphColorMode };
  456. /**
  457. * TSL function for creating an anaglyph pass node.
  458. *
  459. * @tsl
  460. * @function
  461. * @param {Scene} scene - The scene to render.
  462. * @param {Camera} camera - The camera to render the scene with.
  463. * @returns {AnaglyphPassNode}
  464. */
  465. export const anaglyphPass = ( scene, camera ) => new AnaglyphPassNode( scene, camera );
粤ICP备19079148号