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
// WHAT: Find the undistorted source coordinate for a radial-distorted output.
// HOW: Iteratively invert a sixth-order radial polynomial in aspect-correct
// coordinates, then return the result to normalized [0, 1] texture space.
// WHY: Inverse mapping gives every output pixel a source sample and avoids the
// holes that appear when source pixels are pushed forward.
// The returned source UV may fall outside [0, 1]; the caller defines edge policy.
vec2 radialSourceCoordinate(
vec2 destinationUv01,
vec2 centerUv01,
vec3 radialCoefficients,
float widthOverHeight
) {
// Measure a circular optical radius even when the texture is not square.
vec2 destination = destinationUv01 - centerUv01;
destination.x *= widthOverHeight;
vec2 source = destination;
for (int iteration = 0; iteration < 9; iteration += 1) {
// Fixed-point inversion solves source * scale(source) = destination.
float radius2 = dot(source, source);
float scale = max(
1.0 + radialCoefficients.x * radius2
+ radialCoefficients.y * radius2 * radius2
+ radialCoefficients.z * radius2 * radius2 * radius2,
0.000001
);
source = destination / scale;
}
// Undo aspect correction before sampling the normalized source texture.
source.x /= widthOverHeight;
return centerUv01 + source;
}- Source texture
- Coordinate remapOne texture read per output pixel
- Calculate the inverse radial mapping
- Sample the source coordinate
- Display output
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 13–22 — invert the forward polynomial. Starting at the destination radius, nine fixed-point iterations recover the ideal source radius that the forward model would move into this output pixel.
k1controls the broad low-order bend.k2andk3increasingly concentrate their influence near the frame edge because they multiply r⁴ and r⁶. - Lines 24–25 — undo the coordinate changes. The recovered source 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?”
No history buffer is required. 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.
References
OpenCV camera calibration documentation — defines radial and tangential camera-distortion models and their calibration coefficients.
Edmund Optics — Distortion — explains field-dependent magnification, barrel and pincushion distortion, and calibration limits.