Skip to content

Commit ccd2e6d

Browse files
authored
WebGPU_Display_StereoExample: Add new Anaglyph Techniques (#32905)
1 parent 686e56a commit ccd2e6d

File tree

2 files changed

+387
-13
lines changed

2 files changed

+387
-13
lines changed

examples/jsm/tsl/display/AnaglyphPassNode.js

Lines changed: 349 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,267 @@ import { Matrix3, NodeMaterial } from 'three/webgpu';
22
import { clamp, nodeObject, Fn, vec4, uv, uniform, max } from 'three/tsl';
33
import 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
*/
11267
class 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

98433
export default AnaglyphPassNode;
99434

435+
export { AnaglyphAlgorithm, AnaglyphColorMode };
436+
100437
/**
101438
* TSL function for creating an anaglyph pass node.
102439
*

0 commit comments

Comments
 (0)