@@ -2,11 +2,267 @@ import { Matrix3, NodeMaterial } from 'three/webgpu';
22import { clamp , nodeObject , Fn , vec4 , uv , uniform , max } from 'three/tsl' ;
33import StereoCompositePassNode from './StereoCompositePassNode.js' ;
44
5+ /**
6+ * Anaglyph algorithm types.
7+ * @readonly
8+ * @enum {string}
9+ */
10+ const AnaglyphAlgorithm = {
11+ TRUE : 'true' ,
12+ GREY : 'grey' ,
13+ COLOUR : 'colour' ,
14+ HALF_COLOUR : 'halfColour' ,
15+ DUBOIS : 'dubois' ,
16+ OPTIMISED : 'optimised' ,
17+ COMPROMISE : 'compromise'
18+ } ;
19+
20+ /**
21+ * Anaglyph color modes.
22+ * @readonly
23+ * @enum {string}
24+ */
25+ const AnaglyphColorMode = {
26+ RED_CYAN : 'redCyan' ,
27+ MAGENTA_CYAN : 'magentaCyan' ,
28+ MAGENTA_GREEN : 'magentaGreen'
29+ } ;
30+
31+ /**
32+ * Standard luminance coefficients (ITU-R BT.601).
33+ * @private
34+ */
35+ const LUMINANCE = { R : 0.299 , G : 0.587 , B : 0.114 } ;
36+
37+ /**
38+ * Creates an anaglyph matrix pair from left and right channel specifications.
39+ * This provides a more intuitive way to define how source RGB channels map to output RGB channels.
40+ *
41+ * Each specification object has keys 'r', 'g', 'b' for output channels.
42+ * Each output channel value is [rCoef, gCoef, bCoef] defining how much of each input channel contributes.
43+ *
44+ * @private
45+ * @param {Object } leftSpec - Specification for left eye contribution
46+ * @param {Object } rightSpec - Specification for right eye contribution
47+ * @returns {{left: number[], right: number[]} } Column-major arrays for Matrix3
48+ */
49+ function createMatrixPair ( leftSpec , rightSpec ) {
50+
51+ // Convert row-major specification to column-major array for Matrix3
52+ // Matrix3.fromArray expects [col0row0, col0row1, col0row2, col1row0, col1row1, col1row2, col2row0, col2row1, col2row2]
53+ // Which represents:
54+ // | col0row0 col1row0 col2row0 | | m[0] m[3] m[6] |
55+ // | col0row1 col1row1 col2row1 | = | m[1] m[4] m[7] |
56+ // | col0row2 col1row2 col2row2 | | m[2] m[5] m[8] |
57+
58+ function specToColumnMajor ( spec ) {
59+
60+ const r = spec . r || [ 0 , 0 , 0 ] ; // Output red channel coefficients [fromR, fromG, fromB]
61+ const g = spec . g || [ 0 , 0 , 0 ] ; // Output green channel coefficients
62+ const b = spec . b || [ 0 , 0 , 0 ] ; // Output blue channel coefficients
63+
64+ // Row-major matrix would be:
65+ // | r[0] r[1] r[2] | (how input RGB maps to output R)
66+ // | g[0] g[1] g[2] | (how input RGB maps to output G)
67+ // | b[0] b[1] b[2] | (how input RGB maps to output B)
68+
69+ // Column-major for Matrix3:
70+ return [
71+ r [ 0 ] , g [ 0 ] , b [ 0 ] , // Column 0: coefficients for input R
72+ r [ 1 ] , g [ 1 ] , b [ 1 ] , // Column 1: coefficients for input G
73+ r [ 2 ] , g [ 2 ] , b [ 2 ] // Column 2: coefficients for input B
74+ ] ;
75+
76+ }
77+
78+ return {
79+ left : specToColumnMajor ( leftSpec ) ,
80+ right : specToColumnMajor ( rightSpec )
81+ } ;
82+
83+ }
84+
85+ /**
86+ * Shorthand for luminance coefficients.
87+ * @private
88+ */
89+ const LUM = [ LUMINANCE . R , LUMINANCE . G , LUMINANCE . B ] ;
90+
91+ /**
92+ * Conversion matrices for different anaglyph algorithms.
93+ * Based on research from "Introducing a New Anaglyph Method: Compromise Anaglyph" by Jure Ahtik
94+ * and various other sources.
95+ *
96+ * Matrices are defined using createMatrixPair for clarity:
97+ * - Each spec object defines how input RGB maps to output RGB
98+ * - Keys 'r', 'g', 'b' represent output channels
99+ * - Values are [rCoef, gCoef, bCoef] for input channel contribution
100+ *
101+ * @private
102+ */
103+ const ANAGLYPH_MATRICES = {
104+
105+ // True Anaglyph - Red channel from left, luminance to cyan channel for right
106+ // Paper: Left=[R,0,0], Right=[0,0,Lum]
107+ [ AnaglyphAlgorithm . TRUE ] : {
108+ [ AnaglyphColorMode . RED_CYAN ] : createMatrixPair (
109+ { r : [ 1 , 0 , 0 ] } , // Left: R -> outR
110+ { g : LUM , b : LUM } // Right: Lum -> outG, Lum -> outB
111+ ) ,
112+ [ AnaglyphColorMode . MAGENTA_CYAN ] : createMatrixPair (
113+ { r : [ 1 , 0 , 0 ] , b : [ 0 , 0 , 0.5 ] } , // Left: R -> outR, partial B -> outB
114+ { g : LUM , b : [ 0 , 0 , 0.5 ] } // Right: Lum -> outG, partial B
115+ ) ,
116+ [ AnaglyphColorMode . MAGENTA_GREEN ] : createMatrixPair (
117+ { r : [ 1 , 0 , 0 ] , b : LUM } , // Left: R -> outR, Lum -> outB
118+ { g : LUM } // Right: Lum -> outG
119+ )
120+ } ,
121+
122+ // Grey Anaglyph - Luminance-based, no color, minimal ghosting
123+ // Paper: Left=[Lum,0,0], Right=[0,0,Lum]
124+ [ AnaglyphAlgorithm . GREY ] : {
125+ [ AnaglyphColorMode . RED_CYAN ] : createMatrixPair (
126+ { r : LUM } , // Left: Lum -> outR
127+ { g : LUM , b : LUM } // Right: Lum -> outG, Lum -> outB
128+ ) ,
129+ [ AnaglyphColorMode . MAGENTA_CYAN ] : createMatrixPair (
130+ { r : LUM , b : [ 0.15 , 0.29 , 0.06 ] } , // Left: Lum -> outR, half-Lum -> outB
131+ { g : LUM , b : [ 0.15 , 0.29 , 0.06 ] } // Right: Lum -> outG, half-Lum -> outB
132+ ) ,
133+ [ AnaglyphColorMode . MAGENTA_GREEN ] : createMatrixPair (
134+ { r : LUM , b : LUM } , // Left: Lum -> outR, Lum -> outB
135+ { g : LUM } // Right: Lum -> outG
136+ )
137+ } ,
138+
139+ // Colour Anaglyph - Full color, high retinal rivalry
140+ // Paper: Left=[R,0,0], Right=[0,G,B]
141+ [ AnaglyphAlgorithm . COLOUR ] : {
142+ [ AnaglyphColorMode . RED_CYAN ] : createMatrixPair (
143+ { r : [ 1 , 0 , 0 ] } , // Left: R -> outR
144+ { g : [ 0 , 1 , 0 ] , b : [ 0 , 0 , 1 ] } // Right: G -> outG, B -> outB
145+ ) ,
146+ [ AnaglyphColorMode . MAGENTA_CYAN ] : createMatrixPair (
147+ { r : [ 1 , 0 , 0 ] , b : [ 0 , 0 , 0.5 ] } , // Left: R -> outR, partial B -> outB
148+ { g : [ 0 , 1 , 0 ] , b : [ 0 , 0 , 0.5 ] } // Right: G -> outG, partial B -> outB
149+ ) ,
150+ [ AnaglyphColorMode . MAGENTA_GREEN ] : createMatrixPair (
151+ { r : [ 1 , 0 , 0 ] , b : [ 0 , 0 , 1 ] } , // Left: R -> outR, B -> outB
152+ { g : [ 0 , 1 , 0 ] } // Right: G -> outG
153+ )
154+ } ,
155+
156+ // Half-Colour Anaglyph - Luminance for left red, full color for right cyan
157+ // Paper: Left=[Lum,0,0], Right=[0,G,B]
158+ [ AnaglyphAlgorithm . HALF_COLOUR ] : {
159+ [ AnaglyphColorMode . RED_CYAN ] : createMatrixPair (
160+ { r : LUM } , // Left: Lum -> outR
161+ { g : [ 0 , 1 , 0 ] , b : [ 0 , 0 , 1 ] } // Right: G -> outG, B -> outB
162+ ) ,
163+ [ AnaglyphColorMode . MAGENTA_CYAN ] : createMatrixPair (
164+ { r : LUM , b : [ 0.15 , 0.29 , 0.06 ] } , // Left: Lum -> outR, half-Lum -> outB
165+ { g : [ 0 , 1 , 0 ] , b : [ 0.15 , 0.29 , 0.06 ] } // Right: G -> outG, half-Lum -> outB
166+ ) ,
167+ [ AnaglyphColorMode . MAGENTA_GREEN ] : createMatrixPair (
168+ { r : LUM , b : LUM } , // Left: Lum -> outR, Lum -> outB
169+ { g : [ 0 , 1 , 0 ] } // Right: G -> outG
170+ )
171+ } ,
172+
173+ // Dubois Anaglyph - Least-squares optimized for specific glasses
174+ // From https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.7.6968&rep=rep1&type=pdf
175+ [ AnaglyphAlgorithm . DUBOIS ] : {
176+ [ AnaglyphColorMode . RED_CYAN ] : createMatrixPair (
177+ {
178+ r : [ 0.4561 , 0.500484 , 0.176381 ] ,
179+ g : [ - 0.0400822 , - 0.0378246 , - 0.0157589 ] ,
180+ b : [ - 0.0152161 , - 0.0205971 , - 0.00546856 ]
181+ } ,
182+ {
183+ r : [ - 0.0434706 , - 0.0879388 , - 0.00155529 ] ,
184+ g : [ 0.378476 , 0.73364 , - 0.0184503 ] ,
185+ b : [ - 0.0721527 , - 0.112961 , 1.2264 ]
186+ }
187+ ) ,
188+ [ AnaglyphColorMode . MAGENTA_CYAN ] : createMatrixPair (
189+ {
190+ r : [ 0.4561 , 0.500484 , 0.176381 ] ,
191+ g : [ - 0.0400822 , - 0.0378246 , - 0.0157589 ] ,
192+ b : [ 0.088 , 0.088 , - 0.003 ]
193+ } ,
194+ {
195+ r : [ - 0.0434706 , - 0.0879388 , - 0.00155529 ] ,
196+ g : [ 0.378476 , 0.73364 , - 0.0184503 ] ,
197+ b : [ 0.088 , 0.088 , 0.613 ]
198+ }
199+ ) ,
200+ [ AnaglyphColorMode . MAGENTA_GREEN ] : createMatrixPair (
201+ {
202+ r : [ 0.4561 , 0.500484 , 0.176381 ] ,
203+ b : [ - 0.0434706 , - 0.0879388 , - 0.00155529 ]
204+ } ,
205+ {
206+ g : [ 0.378476 + 0.4561 , 0.73364 + 0.500484 , - 0.0184503 + 0.176381 ]
207+ }
208+ )
209+ } ,
210+
211+ // Optimised Anaglyph - Improved color with reduced retinal rivalry
212+ // Paper: Left=[0,0.7G+0.3B,0,0], Right=[0,G,B]
213+ [ AnaglyphAlgorithm . OPTIMISED ] : {
214+ [ AnaglyphColorMode . RED_CYAN ] : createMatrixPair (
215+ { r : [ 0 , 0.7 , 0.3 ] } , // Left: 0.7G+0.3B -> outR
216+ { g : [ 0 , 1 , 0 ] , b : [ 0 , 0 , 1 ] } // Right: G -> outG, B -> outB
217+ ) ,
218+ [ AnaglyphColorMode . MAGENTA_CYAN ] : createMatrixPair (
219+ { r : [ 0 , 0.7 , 0.3 ] , b : [ 0 , 0 , 0.5 ] } , // Left: 0.7G+0.3B -> outR, partial B
220+ { g : [ 0 , 1 , 0 ] , b : [ 0 , 0 , 0.5 ] } // Right: G -> outG, partial B
221+ ) ,
222+ [ AnaglyphColorMode . MAGENTA_GREEN ] : createMatrixPair (
223+ { r : [ 0 , 0.7 , 0.3 ] , b : [ 0 , 0 , 1 ] } , // Left: 0.7G+0.3B -> outR, B -> outB
224+ { g : [ 0 , 1 , 0 ] } // Right: G -> outG
225+ )
226+ } ,
227+
228+ // Compromise Anaglyph - Best balance of color and stereo effect
229+ // From Ahtik, J., "Techniques of Rendering Anaglyphs for Use in Art"
230+ // 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]
231+ [ AnaglyphAlgorithm . COMPROMISE ] : {
232+ [ AnaglyphColorMode . RED_CYAN ] : createMatrixPair (
233+ { r : [ 0.439 , 0.447 , 0.148 ] } , // Left: weighted RGB -> outR
234+ {
235+ g : [ 0.095 , 0.934 , 0.005 ] , // Right: weighted RGB -> outG
236+ b : [ 0.018 , 0.028 , 1.057 ] // Right: weighted RGB -> outB
237+ }
238+ ) ,
239+ [ AnaglyphColorMode . MAGENTA_CYAN ] : createMatrixPair (
240+ {
241+ r : [ 0.439 , 0.447 , 0.148 ] ,
242+ b : [ 0.009 , 0.014 , 0.074 ] // Partial blue from left
243+ } ,
244+ {
245+ g : [ 0.095 , 0.934 , 0.005 ] ,
246+ b : [ 0.009 , 0.014 , 0.528 ] // Partial blue from right
247+ }
248+ ) ,
249+ [ AnaglyphColorMode . MAGENTA_GREEN ] : createMatrixPair (
250+ {
251+ r : [ 0.439 , 0.447 , 0.148 ] ,
252+ b : [ 0.018 , 0.028 , 1.057 ]
253+ } ,
254+ {
255+ g : [ 0.095 + 0.439 , 0.934 + 0.447 , 0.005 + 0.148 ]
256+ }
257+ )
258+ }
259+ } ;
260+
5261/**
6262 * A render pass node that creates an anaglyph effect.
7263 *
8264 * @augments StereoCompositePassNode
9- * @three_import import { anaglyphPass } from 'three/addons/tsl/display/AnaglyphPassNode.js';
265+ * @three_import import { anaglyphPass, AnaglyphAlgorithm, AnaglyphColorMode } from 'three/addons/tsl/display/AnaglyphPassNode.js';
10266 */
11267class AnaglyphPassNode extends StereoCompositePassNode {
12268
@@ -35,31 +291,110 @@ class AnaglyphPassNode extends StereoCompositePassNode {
35291 */
36292 this . isAnaglyphPassNode = true ;
37293
38- // Dubois matrices from https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.7.6968&rep=rep1&type=pdf#page=4
294+ /**
295+ * The current anaglyph algorithm.
296+ *
297+ * @private
298+ * @type {string }
299+ * @default 'dubois'
300+ */
301+ this . _algorithm = AnaglyphAlgorithm . DUBOIS ;
302+
303+ /**
304+ * The current color mode.
305+ *
306+ * @private
307+ * @type {string }
308+ * @default 'redCyan'
309+ */
310+ this . _colorMode = AnaglyphColorMode . RED_CYAN ;
39311
40312 /**
41313 * Color matrix node for the left eye.
42314 *
43315 * @private
44316 * @type {UniformNode<mat3> }
45317 */
46- this . _colorMatrixLeft = uniform ( new Matrix3 ( ) . fromArray ( [
47- 0.456100 , - 0.0400822 , - 0.0152161 ,
48- 0.500484 , - 0.0378246 , - 0.0205971 ,
49- 0.176381 , - 0.0157589 , - 0.00546856
50- ] ) ) ;
318+ this . _colorMatrixLeft = uniform ( new Matrix3 ( ) ) ;
51319
52320 /**
53321 * Color matrix node for the right eye.
54322 *
55323 * @private
56324 * @type {UniformNode<mat3> }
57325 */
58- this . _colorMatrixRight = uniform ( new Matrix3 ( ) . fromArray ( [
59- - 0.0434706 , 0.378476 , - 0.0721527 ,
60- - 0.0879388 , 0.73364 , - 0.112961 ,
61- - 0.00155529 , - 0.0184503 , 1.2264
62- ] ) ) ;
326+ this . _colorMatrixRight = uniform ( new Matrix3 ( ) ) ;
327+
328+ // Initialize with default matrices
329+ this . _updateMatrices ( ) ;
330+
331+ }
332+
333+ /**
334+ * Gets the current anaglyph algorithm.
335+ *
336+ * @type {string }
337+ */
338+ get algorithm ( ) {
339+
340+ return this . _algorithm ;
341+
342+ }
343+
344+ /**
345+ * Sets the anaglyph algorithm.
346+ *
347+ * @type {string }
348+ */
349+ set algorithm ( value ) {
350+
351+ if ( this . _algorithm !== value ) {
352+
353+ this . _algorithm = value ;
354+ this . _updateMatrices ( ) ;
355+
356+ }
357+
358+ }
359+
360+ /**
361+ * Gets the current color mode.
362+ *
363+ * @type {string }
364+ */
365+ get colorMode ( ) {
366+
367+ return this . _colorMode ;
368+
369+ }
370+
371+ /**
372+ * Sets the color mode.
373+ *
374+ * @type {string }
375+ */
376+ set colorMode ( value ) {
377+
378+ if ( this . _colorMode !== value ) {
379+
380+ this . _colorMode = value ;
381+ this . _updateMatrices ( ) ;
382+
383+ }
384+
385+ }
386+
387+ /**
388+ * Updates the color matrices based on current algorithm and color mode.
389+ *
390+ * @private
391+ */
392+ _updateMatrices ( ) {
393+
394+ const matrices = ANAGLYPH_MATRICES [ this . _algorithm ] [ this . _colorMode ] ;
395+
396+ this . _colorMatrixLeft . value . fromArray ( matrices . left ) ;
397+ this . _colorMatrixRight . value . fromArray ( matrices . right ) ;
63398
64399 }
65400
@@ -97,6 +432,8 @@ class AnaglyphPassNode extends StereoCompositePassNode {
97432
98433export default AnaglyphPassNode ;
99434
435+ export { AnaglyphAlgorithm , AnaglyphColorMode } ;
436+
100437/**
101438 * TSL function for creating an anaglyph pass node.
102439 *
0 commit comments