GLSL Contrast Adjustment Using S-Curve Polynomials

Evaluating color contrast adjustment via S-curve polynomial functions in GLSL involves analyzing mathematical formulations, shader performance, color space selection, and visual continuity. S-curves enhance perceived contrast by stretching midtones while smoothly compressing shadows and highlights to prevent harsh clipping. In real-time rendering, selecting the appropriate polynomial—ranging from cubic Hermite interpolations to customizable higher-order equations—requires balancing GPU arithmetic instruction costs against the risk of color distortion, dynamic range loss, and banding artifacts.

Mathematical Formulations of S-Curves

An S-curve maps normalized input values \([0.0, 1.0]\) back to \([0.0, 1.0]\) with an inflection point typically situated at the midtone value \(0.5\). The derivative of the function determines the contrast: a first derivative greater than \(1.0\) at the inflection point increases contrast, while values less than \(1.0\) decrease it.

Standard Cubic Hermite (Smoothstep)

The most common hardware-accelerated polynomial in GLSL is the cubic Hermite curve:

\[f(x) = 3x^2 - 2x^3\]

In GLSL, this is natively evaluated using smoothstep(0.0, 1.0, x). It provides \(C^1\) continuity, meaning its first derivative equals zero at both \(x = 0\) and \(x = 1\), ensuring a seamless transition into full black and full white.

Quintic Polynomial (Smootherstep)

To prevent subtle visual discontinuities at the boundaries caused by a non-zero second derivative, Ken Perlin’s quintic polynomial can be used:

\[f(x) = 6x^5 - 15x^4 + 10x^3\]

This yields \(C^2\) continuity (both first and second derivatives equal zero at \(x = 0\) and \(x = 1\)), eliminating abrupt changes in the rate of color compression at the dynamic range extremes.

Parameterized Contrast Polynomial

When contrast needs to be adjusted dynamically via a uniform, standard fixed curves must be replaced by parameterized functions. A common cubic formulation centered at \(0.5\) is:

\[f(x, k) = 0.5 + 4^{k-1} \cdot (x - 0.5) \cdot |2x - 1|^{k-1}\]

Alternatively, a lightweight controllable polynomial blends between a linear pass-through and a cubic S-curve:

vec3 adjustContrast(vec3 color, float contrast) {
    // contrast range: 0.0 (flat) to 1.0 (normal) to >1.0 (high contrast)
    vec3 cubic = color * color * (3.0 - 2.0 * color);
    return mix(color, cubic, contrast);
}

Color Space Considerations

Applying polynomial curves directly to raw non-linear sRGB values leads to severe hue shifts and unnatural over-saturation, as the RGB channels are modified independently. Contrast evaluation must account for the color space in which the curve is executed:

  1. Per-Channel RGB: Modifying \(R\), \(G\), and \(B\) individually is computationally cheap but shifts hues because color balance changes unevenly across channels.
  2. Luminance Extraction: Extracting luminance via standard colorimetric coefficients (e.g., Rec. 709: \(Y = 0.2126R + 0.7152G + 0.0722B\)), applying the S-curve polynomial to \(Y\), and scaling the original RGB channels preserves hue: \[\text{Color}_{\text{out}} = \text{Color}_{\text{in}} \cdot \left(\frac{Y_{\text{new}}}{Y_{\text{old}} + \epsilon}\right)\]
  3. Perceptual Spaces (Oklab / CIELAB): Transforming to a perceptual space such as Oklab allows the S-curve to be applied strictly to the \(L\) (lightness) channel, ensuring perceptual uniformity and minimizing chromatic drift.

GPU Execution and Arithmetic Efficiency

Evaluating polynomials on the GPU requires measuring Instruction Level Parallelism (ILP) and instruction count. Polynomials are favored over trigonometric functions (\(\sin\), \(\tanh\)) or logarithmic functions because GPUs can execute them via Fused Multiply-Add (FMA) instructions.

  • Cubic Evaluation: A curve such as \(3x^2 - 2x^3\) can be factored into \(x \cdot x \cdot (3.0 - 2.0 \cdot x)\). Modern GPU architectures compute this in two cycle-efficient FMA operations per component.
  • Native smoothstep: While often compiled into a dedicated hardware instruction or optimized FMA sequence, custom parameterized polynomials allow runtime variability without branching.
  • Vectorization: GLSL executes polynomial evaluations on vec3 or vec4 data types across SIMD lanes simultaneously, ensuring that luminance-based or per-channel calculations saturate the execution pipeline efficiently.

Evaluating Visual Artifacts and Dynamic Range

Evaluating an S-curve implementation also requires verifying how the polynomial behaves across non-standard data ranges:

  • Black and White Clipping: Polynomials that exceed the \([0.0, 1.0]\) bounds due to incorrect coefficients cause harsh clamping. Proper functions must strictly maintain \(f(0) = 0\) and \(f(1) = 1\).
  • High Dynamic Range (HDR): Standard polynomial S-curves are fundamentally designed for Standard Dynamic Range (SDR) bounded inputs. In HDR pipelines, inputs greater than \(1.0\) will diverge rapidly to infinity if evaluated with standard polynomials, necessitating input normalization, Reinhard-style pre-tonemapping, or rational polynomial approximations (e.g., Michaelis-Menten kinetics) to ensure an asymptotic shoulder.
  • Quantization and Banding: Steep polynomial slopes in high-contrast configurations amplify quantization errors in 8-bit-per-channel buffers. Evaluating the curve within an intermediate 16-bit floating-point (RGBA16F) framebuffer prevents posterization artifacts before presentation.