ShaderMaterial: getting same color output for when rendering to screen and via a texture

Hi, after upgrading to the recent versions of Threejs, I figured out that (unlike, let’s say, MeshBasicMaterial preserving colors specified by the color parameter regardless of the render target) ShaderMaterial produces the expected color only when rendered to screen. Here is an example:

<html>
	<head><script src="three158.js"></script></head>
	<body>
		<canvas id="main" style="color: transparent; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;"></canvas>
	</body>
	<script>
	window.onload = function() {
		var bg_clr = 0x0000c0,
		cnv = document.createElement('canvas'), w = 100, h = 100,
		rndr = new THREE.WebGLRenderer({ canvas: cnv }),
		tgt = new THREE.WebGLRenderTarget(w, h, { depthBuffer: false, stencilBuffer: false, format: THREE.RGBAFormat }),
		tex_data = new Uint8Array(w * h * 4),
		tex = new THREE.DataTexture(tex_data, w, h, THREE.RGBAFormat, THREE.UnsignedByteType, THREE.UVMapping, THREE.RepeatWrapping, THREE.RepeatWrapping, THREE.LinearFilter, THREE.LinearFilter, 1),
		scene = new THREE.Scene();
		
		scene.add(new THREE.Mesh(new THREE.CircleGeometry(0.66, 4),
								 new THREE.MeshBasicMaterial({ color: 0x00c000 })));
		scene.add(new THREE.Mesh(new THREE.CircleGeometry(0.33, 4),
								 new THREE.ShaderMaterial({ fragmentShader: 'void main() { gl_FragColor = vec4(0.75,0.0,0.0,1.0); }' })));
		
		// Render to texture
		rndr.setSize(cnv.width = w, cnv.height = h, false);
		rndr.setRenderTarget(tgt);
		rndr.setClearColor(bg_clr, 1);
		rndr.clear();
		rndr.render(scene, new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1));
		rndr.readRenderTargetPixels(tgt, 0, 0, w, h, tex_data);
		tex.needsUpdate = true;
		
		var cnv2 = document.getElementById('main');
		rndr2 = new THREE.WebGLRenderer({ canvas: cnv2 });
		rndr2.setClearColor(bg_clr, 1);
		rndr2.clear();
		rndr2.setSize(cnv2.width, cnv2.height, false);

		rndr2.render(new THREE.Mesh(new THREE.CircleGeometry(1, 4), new THREE.MeshBasicMaterial({ map: tex })), new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1));//render the texture
		rndr2.render(scene, new THREE.OrthographicCamera(-1, 1, 1, -1, -1, 1));//or render the scene directly
	};
	</script>
</html>

Here are the results of rendering the scene with MeshBasicMaterial (green) and ShaderMaterial (red) directly (correct red color, #c00000).


and rendering those two materials to a texture first before using it as a map for another MeshBasicMaterial (lighter red color, #e00000)

How should that be tuned to get the red hardcoded in the fragment shader to be kept as is? In other words, how to properly mimic the MeshBasicMaterial’s color conversions (or lack of) for all cases?

1 Like

Depending your scene and assets you have multiple options.
First you can try to disable color managment, it’s used mostly for textures and can mess with shader colors codes. THREE.ColorManagement.enabled = false; Otherwise you can modify your GLSL code shader to match Three’s color rules, but it never worked for me. Then you may ask the renderer to assume your colors are always perfect (sRGB) and does not require further change, by using renderer.outputColorSpace = THREE.LinearSRGBColorSpace;

To be honest, the new color management is quite counter-productive when all your assets are not meant to be “corrected” and instead should follow user’s browsers/desktop settings. This is a personal view, but devs trying to “dictate” how colors look on user’s screen is pointless (it’s was never possible and will stay impossible)

1 Like

Thanks, seems like disabling ColorManagement in combination with LinearSRGBColorSpace actually produces the correct result. I tried that before, but I was mistakenly setting the outputColorSpace of the first renderer that renders to the texture, which had no effect. Now with rndr2.outputColorSpace = THREE.LinearSRGBColorSpace from my example it works as expected.

1 Like

three.js color management does no “color correction”, and makes no attempt to adjust for a user’s screen as of this writing. The goal of the color management is that the renderer must know what color spaces are being used, to compute lighting correctly and avoid problems like this illustration. The settings suggested above will cause that type of problem in lit scenes. In unlit scenes, it depends on what you’re doing.

With color management enabled, the correct approach would be to tag your color textures with texture.colorSpace = THREE.SRGBColorSpace, and include the colorspace fragment at the end your ShaderMaterial to convert from the linear space used for rendering to the sRGB colorspace that an HTML canvas requires.

3 Likes

Thanks for this information! Didn’t knew about the texture colorSpace being a mandatory step for glsl color space. (that’s probably why my attempts failed).

I mostly use custom shaders and pseudo fake lights for performance, so color management is still not my friend (yet) :grin: but who knows?

your answer is definitively the correct one and should be marked as such :+1: (if op is still around)

1 Like