Quick answer: Use highp in fragment shaders. Render lights to a surface with format surface_rgba16float if available. Avoid 8-bit alpha quantization in light accumulation.
A 2D dungeon game uses a custom light shader on a surface, then blends to screen. On Android, the gradient turns into hard rings — mobile GPUs use mediump by default, quantizing values.
Force High Precision
// fragment shader
precision highp float;
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
void main() {
float d = length(v_vTexcoord - vec2(0.5));
float intensity = smoothstep(0.5, 0.0, d);
gl_FragColor = vec4(1.0, 1.0, 1.0, intensity);
}
Mediump (~10-bit float) quantizes the gradient into bands. Highp (32-bit) gives smooth falloff.
Use Float Surface
light_surface = surface_create(w, h, surface_rgba16float);
HDR-precision surface. Accumulates many lights without saturation. Standard surface_rgba8unorm clips to [0, 1].
Tone Map to Display
// post-pass shader: HDR -> LDR
void main() {
vec3 hdr = texture2D(s_HDRTex, v_vTexcoord).rgb;
vec3 ldr = hdr / (hdr + vec3(1.0)); // Reinhard
gl_FragColor = vec4(ldr, 1.0);
}
Map HDR back to 8-bit display range smoothly.
Verifying
Run on Android device. Light gradient smooth. No banding. Cross-test on iOS — precision should match desktop.
“Mobile defaults to mediump. For lighting, explicitly request highp and float surfaces.”
surface_rgba16float isn’t universal on Android — test capability and fallback to rgba8 + dithering if unavailable.