Convert shader to linear color pipeline #44

Closed
opened 2026-03-19 17:48:03 +00:00 by kit · 1 comment
Owner

Problem

The uber-shader in material.js mixes gamma and linear color space operations throughout, causing incorrect brightness levels — particularly visible in envmap reflections where dark surfaces are too dim and bright surfaces blow out.

Current state:

  • Base textures: sRGB (gamma) space, used as-is
  • Lightmap: linear space, gamma-encoded via pow(light, 1/2.2) before multiplying with base
  • Cubemap envmaps: sRGB, linearized with pow(sample, 2.2), scaled, gamma-encoded with pow(result, 1/2.2) before adding
  • Color tint, entity color, vertex color: applied in gamma space
  • Self-illumination, fog: applied in gamma space
  • Final output: gamma space (no final conversion)

This piecemeal approach compounds gamma errors at each step.

Solution

Convert the entire shader to a linear pipeline:

  1. Linearize all inputs at the start:

    • baseColor = pow(texture2D(baseTexture, vUv).rgb, vec3(2.2)) (sRGB → linear)
    • colorTint — linearize if stored in sRGB
    • Cubemap samples — linearize
    • Lightmap — already linear, remove the gamma-encode step
  2. All math in linear space:

    • Lighting multiplication (lightmap × base, ambient cube × base)
    • Envmap addition (cubemap × tint × mask × fresnel)
    • Self-illumination blending
    • Color tint, vertex color, entity color
    • Fog blending
  3. Single gamma encode at final output:

    • gl_FragColor = vec4(pow(color, vec3(1.0/2.2)), alpha)

Impact

This will affect every visual in the scene — lighting brightness, envmap reflections, fog blending, self-illumination, color tinting. Ground truth comparisons should be done carefully with pixel sampling.

Considerations

  • Source Engine itself operates mostly in gamma space for LDR rendering. The linear pipeline is more physically correct but may not exactly match Source's output. We should compare against ground truth and adjust.
  • ENV_MAP_SCALE value may need adjustment once everything is in linear space.
  • Three.js's renderer.outputColorSpace setting may interact with our manual gamma encode — need to ensure we're not double-encoding.
  • #8 — VMT shader types (shader is in material.js)
  • #43 — Skybox brightness regression (root cause was mixed gamma)
  • #40 — Bump mapping (will need to work in whatever color space we settle on)
## Problem The uber-shader in `material.js` mixes gamma and linear color space operations throughout, causing incorrect brightness levels — particularly visible in envmap reflections where dark surfaces are too dim and bright surfaces blow out. Current state: - Base textures: sRGB (gamma) space, used as-is - Lightmap: linear space, gamma-encoded via `pow(light, 1/2.2)` before multiplying with base - Cubemap envmaps: sRGB, linearized with `pow(sample, 2.2)`, scaled, gamma-encoded with `pow(result, 1/2.2)` before adding - Color tint, entity color, vertex color: applied in gamma space - Self-illumination, fog: applied in gamma space - Final output: gamma space (no final conversion) This piecemeal approach compounds gamma errors at each step. ## Solution Convert the entire shader to a linear pipeline: 1. **Linearize all inputs at the start:** - `baseColor = pow(texture2D(baseTexture, vUv).rgb, vec3(2.2))` (sRGB → linear) - `colorTint` — linearize if stored in sRGB - Cubemap samples — linearize - Lightmap — already linear, remove the gamma-encode step 2. **All math in linear space:** - Lighting multiplication (lightmap × base, ambient cube × base) - Envmap addition (cubemap × tint × mask × fresnel) - Self-illumination blending - Color tint, vertex color, entity color - Fog blending 3. **Single gamma encode at final output:** - `gl_FragColor = vec4(pow(color, vec3(1.0/2.2)), alpha)` ## Impact This will affect every visual in the scene — lighting brightness, envmap reflections, fog blending, self-illumination, color tinting. Ground truth comparisons should be done carefully with pixel sampling. ## Considerations - Source Engine itself operates mostly in gamma space for LDR rendering. The linear pipeline is more physically correct but may not exactly match Source's output. We should compare against ground truth and adjust. - `ENV_MAP_SCALE` value may need adjustment once everything is in linear space. - Three.js's `renderer.outputColorSpace` setting may interact with our manual gamma encode — need to ensure we're not double-encoding. ## Related - #8 — VMT shader types (shader is in material.js) - #43 — Skybox brightness regression (root cause was mixed gamma) - #40 — Bump mapping (will need to work in whatever color space we settle on)
Author
Owner

Implemented in 36368a2.

All texture inputs (base, detail blend result, cubemap, reflection RT, vertex colors) are linearized at the start of the pipeline. All lighting and blending math happens in linear space. Single gamma encode at output (including water, modulate, and lightmapOnly early-return paths).

CPU-side linearization for static uniforms: colorTint, selfIllumTint, entityColor, fog colors. envmapTint is intentionally NOT linearized — it was already applied in linear space in Source's pipeline (between cubemap linearization and re-gamma), so it's a linear multiplier.

ENV_MAP_SCALE reduced from 2.0 to 1.0 for the linear pipeline. Detail blending stays in gamma space before linearization so Source-designed blend modes (mod2x neutral at 0.5, etc.) work correctly.

Implemented in 36368a2. All texture inputs (base, detail blend result, cubemap, reflection RT, vertex colors) are linearized at the start of the pipeline. All lighting and blending math happens in linear space. Single gamma encode at output (including water, modulate, and lightmapOnly early-return paths). CPU-side linearization for static uniforms: `colorTint`, `selfIllumTint`, `entityColor`, fog colors. `envmapTint` is intentionally NOT linearized — it was already applied in linear space in Source's pipeline (between cubemap linearization and re-gamma), so it's a linear multiplier. ENV_MAP_SCALE reduced from 2.0 to 1.0 for the linear pipeline. Detail blending stays in gamma space before linearization so Source-designed blend modes (mod2x neutral at 0.5, etc.) work correctly.
kit closed this issue 2026-03-19 18:07:49 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kit/gmod-web-stream#44
No description provided.