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

Three field angles pass through a real lens. The central image point stays in place, while off-axis points land closer to the optical axis than their ideal positions. The same inward position error turns an even grid into a barrel-distorted grid.real lensimage planeequally spaced image pointsedge points move inward; lines bow outward

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

radialSourceCoordinateGLSL
// 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

  1. Lines 1–7 — state the coordinate contract. destinationUv01 and centerUv01 are both normalized texture coordinates. The returned source coordinate uses the same coordinate system, although strong distortion can move it outside the texture bounds.
  2. Lines 9–10 — centre and correct the aspect ratio. Subtracting centerUv01 already 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.
  3. Lines 12–15 — evaluate the polynomial. k1 controls the broad low-order bend. k2and k3 increasingly concentrate their influence near the frame edge because they multiply r⁴ and r⁶.
  4. 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 centerUv01 is 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.