Radial distortion
Why straight lines bend near the edge of a real lens — and how to reproduce the mapping in a shader.
Visible effect
Start with a grid
A regular grid makes the mapping impossible to hide.
Move away from the optical axis and the lines begin to bow.
Physics
Field-dependent magnification
Solid green rays show the actual image positions; dashed grey rays show the ideal positions. A real lens does not keep exactly the same lateral magnification at every field angle, so off-axis points land at the wrong radius even when they remain sharp. Falling magnification moves them inward and produces barreldistortion; rising magnification moves them outward and producespincushion distortion.
Mathematics
Forward model, inverse sampling
Brown–Conrady does not simulate the glass. It fits the position error with a radial polynomial. Starting from an ideal normalized point:
A fragment shader works backwards: for every output pixel it finds the source coordinate to sample. Confusing the model direction with the sampling direction is the classic reason barrel and pincushion presets appear reversed.
Shader
Shader approximation
The playground compiles and uses the complete function below. Its job is deliberately narrow: receive a destination pixel coordinate in normalized [0, 1] texture space and return the source coordinate that should be sampled in that same space.
GLSL mapping function
// Arguments use normalized texture coordinates [0, 1].
// The returned source UV uses the same space, but may fall outside its bounds.
vec2 radialSourceCoordinate(
vec2 destinationUv01,
vec2 centerUv01,
vec3 radialCoefficients,
float widthOverHeight
) {
vec2 centered = destinationUv01 - centerUv01;
centered.x *= widthOverHeight;
float radius2 = dot(centered, centered);
float scale = 1.0 + radialCoefficients.x * radius2
+ radialCoefficients.y * radius2 * radius2
+ radialCoefficients.z * radius2 * radius2 * radius2;
centered *= scale;
centered.x /= widthOverHeight;
return centerUv01 + centered;
}Why these steps are here
- Lines 1–7 — state the coordinate contract.
destinationUv01andcenterUv01are both normalized texture coordinates. The returned source coordinate uses the same coordinate system, although strong distortion can move it outside the texture bounds. - Lines 9–10 — centre and correct the aspect ratio. Subtracting
centerUv01already produces signed coordinates around the optical centre; a separate conversion to[-1, 1]is unnecessary. Scaling x by width divided by height makes the radius circular rather than elliptical on a non-square target. - Lines 12–15 — evaluate the polynomial.
k1controls the broad low-order bend.k2andk3increasingly concentrate their influence near the frame edge because they multiply r⁴ and r⁶. - Lines 17–19 — apply and undo the coordinate changes. The radial scale moves the centred point, then x is returned to texture-space units and
centerUv01is added back.
Why the function returns a source coordinate
The polynomial is usually introduced as a forward camera model: ideal point → distorted point. A shader cannot safely push one source pixel into the output because several pixels may collide and leave holes. Rasterization visits every output pixel instead, so the runtime evaluates the mapping as an inverse sampling approximation and asks: “which source coordinate belongs here?”
Pass order: source → radial coordinate mapping → output. One texture read per output pixel; no history buffer. Coefficients are meaningful only together with the coordinate normalization used by this function.
Notes
- The polynomial imitates a mapping; it does not trace the actual lens.
- Strong coefficients can fold coordinates or expose the source boundary.
- Decentering, tangential distortion, and wavelength dependence are omitted here.
- Calibration parameters from a camera may use a different normalization convention.