Hidden Color Functions in THREE.js

THREE.js' ShaderMaterial lets you write your own shader code, but still leverage all the great boilerplate that THREE.js uses internally for its other materials (like StandardMaterial). Pretty neat!

Built-in uniforms, attributes, and varyings are all documented in WebGLProgram. But did you know - there's also a handful of color functions that get included in your fragment shader? Apparently they're used internally as helpers to decode HDR texture data provided through a material's map (or envMap, matCap, emissiveMap, or lightMap).

Since these functions aren't documented, there's a good chance they'll change over time without any notice. Use at your own risk!

vec4 LinearToLinear( in vec4 value )

vec4 GammaToLinear( in vec4 value, in float gammaFactor )
vec4 LinearToGamma( in vec4 value, in float gammaFactor )

vec4 sRGBToLinear( in vec4 value )
vec4 LinearTosRGB( in vec4 value )

vec4 RGBEToLinear( in vec4 value )
vec4 LinearToRGBE( in vec4 value )

vec4 RGBMToLinear( in vec4 value, in float maxRange )
vec4 LinearToRGBM( in vec4 value, in float maxRange )

vec4 RGBDToLinear( in vec4 value, in float maxRange )
vec4 LinearToRGBD( in vec4 value, in float maxRange )

vec4 LinearToLogLuv( in vec4 value )
vec4 LogLuvToLinear( in vec4 value )

Source here. Each function pair represents a decoding or encoding operation, respectively.

What can I use these for?

There are two groups of functions here. The first group consists of operations that are useful for preparing "regular" color data for further manipulation, or preparing computed colors for output to the screen.

vec4 GammaToLinear( in vec4 value, in float gammaFactor )
vec4 LinearToGamma( in vec4 value, in float gammaFactor )

vec4 sRGBToLinear( in vec4 value )
vec4 LinearTosRGB( in vec4 value )

The second group helps you work with HDR color data that's been compressed to fit inside a standard 8-bits-per-channel buffer.

vec4 RGBEToLinear( in vec4 value )
vec4 LinearToRGBE( in vec4 value )

vec4 RGBMToLinear( in vec4 value, in float maxRange )
vec4 LinearToRGBM( in vec4 value, in float maxRange )

vec4 RGBDToLinear( in vec4 value, in float maxRange )
vec4 LinearToRGBD( in vec4 value, in float maxRange )

vec4 LinearToLogLuv( in vec4 value )
vec4 LogLuvToLinear( in vec4 value )

But why encoded? Why are these any different than the "regular" color data you're used to working with? There are a couple of primary reasons:

  • 8-bits-per-channel (aka RGBA8) has the widest support across devices.
  • An explicit encoding controls where your bit precision goes (e.g. more bits devoted to the darker range, or more bits devoted to the SDR range).

Put another way, from http://lousodrome.net/blog/light/2013/05/26/gamma-correct-and-hdr-rendering-in-a-32-bits-buffer/:

Gamma correct means you need higher precision for low values [...] HDR means you may have values greater than 1, and since your range is getting wider, you want higher precision everywhere.

How did I find these in the first place?

In the process of debugging a shader, I wanted to print the source of my fragment shader at runtime. Following the advice on Andrew Ray's blog, I added these lines to the project near where my scene is initialized:

renderer.compile(scene, camera);
const gl = renderer.getContext();
console.log(
  "fragment shader",
  gl.getShaderSource(material.program.fragmentShader)
);

© 2021 David Mann. All content provided on this site, code or otherwise, is licensed and/or copyright as specified below: