I’ve been investigating a core VTK issue with colors for a little while now. This issue is pervasive in VTK and VTK.js, and most likely other derived software. Luckily, I think there’s a gradual fix for it.
The core issue is that VTK never clearly says (as far as I can find) what color space a color value is in.
For those not steeped in this: a color space defines what a set of numbers actually means: how (0.8, 0.2, 0.2) maps to a physical stimulus and then to a perceived color. It’s the same reason we insist on units and a scale before mapping raw values into a visualization. Without it, (0.8, 0.2, 0.2) isn’t a color, it’s three numbers.
Essentially, colors are just (r, g, b) values throughout VTK / VTK.js, without saying what space they are in.
Colors on the web are sRGB by default: CSS defines hex, rgb() and the named colors that way. That makes it a reasonable default for VTK too.
The problem is that sRGB values are not proportional to light. The transfer function exists because it spends 8-bit code values roughly where the eye can tell them apart, which is what makes 256 levels sufficient. That makes it an efficient perceptual encoding, not a perceptually uniform space; for that you want Lab or Oklab.
The consequence is that any arithmetic that models light, like multiplying by N·L, interpolating between transfer-function stops, volume rendering, or compositing with alpha, is only correct on linear-light values. VTK does all three on sRGB-encoded numbers which isn’t a linear space. (The color transfer functions have an option to interpolate in different color spaces, but the colors themselves don’t have a specified color space).
What’s the result of confusing sRGB with linear light? Depending on the direction, washed out or excessively dark gradients and a general color mismatch comparing to other renderers like THREE.js.
How big is the error? The maximum difference between linear RGB and sRGB is about 73 out of 255 or about 29%.
There are places that clearly need this conversion and don’t do it. The glTF exporter writes vtkProperty::GetDiffuseColor directly into baseColorFactor, which the glTF spec defines as linear. No conversion anywhere in the file. Elsewhere, the rendering path approximates the conversion with pow(x, 2.2) 18 times across 8 files with no shared helper. Coverage is uneven even within a single file: vtkOpenGLPolyDataMapper linearizes vertex colors and color maps under PBR, but the branch that handles plain material color, what you get from SetColor(), never converts at all.
The better solution is to define vtkMath::SRGBToLinear / LinearToSRGB (there’s already code in the vtkMath module to do it, it just isn’t brought out as its own function) and systematically identify the places that need this conversion.
Since OpenGL and other graphics libraries already have sRGB calibrated canvases, handling colors right can actually be more efficient rather than less.
The first step is to state explicitly that VTK and VTK.js colors are implicitly sRGB (adding an explicit color space mechanism also makes sense in the future).
The next challenge is making the code live up to that statement.
The biggest issue is that fixing this issue will change test renders all over VTK. One way to handle it, I think, is to hide the fixes behind a flag (UseSRGBColorSpace). Exporters can be updated sooner, since there output should match that of external software that is often already color space compliant.
I’d love to hear thoughts on how this challenge could be addressed without too much disruption.