Tangential distortion
How a decentered or tilted lens breaks radial symmetry — and how an iterative shader reconstructs the source coordinate.
Visible effect
Look for broken symmetry
A centred grid reveals what radial distortion cannot explain.
The lines do not bow with rotational symmetry. Instead, opposite sides lean in the same direction: the mapping has acquired an orientation.
Physics
When the lens groups do not share one axis
An ideal centred objective is rotationally symmetric around its optical axis. Real elements can be shifted sideways or tilted during assembly. The effective optical axis then separates from the mechanical axis, and off-axis image points acquire a directional position error.
Mathematics
Two coefficients describe an oriented offset
Brown–Conrady adds a decentering term to an ideal normalized point. The coefficients p1 and p2 set the two components of the asymmetry:
These equations are a forward camera model: ideal point → distorted point. The live vector field shows that direction. The fragment shader needs the reverse question, distorted output point → source point, and solves it iteratively.
Shader
GLSL inverse mapping function
The playground compiles the complete function below. Nine fixed-point iterations are enough for the deliberately bounded interactive coefficient range.
// WHAT: Find the source coordinate displaced by decentered-lens distortion.
// HOW: Evaluate the Brown–Conrady tangential offset repeatedly in
// aspect-correct space and subtract it from the requested destination point.
// WHY: The tangential model is defined forward, but rendering needs its inverse
// so every destination pixel receives exactly one sample.
vec2 tangentialSourceCoordinate(
vec2 destinationUv01,
vec2 centerUv01,
vec2 tangential,
float aspect
) {
// Isotropic coordinates keep x and y coefficients physically comparable.
vec2 scale = vec2(aspect, 1.0);
vec2 distorted = (destinationUv01 - centerUv01) * scale;
vec2 source = distorted;
for (int iteration = 0; iteration < 9; iteration++) {
float x = source.x;
float y = source.y;
float radiusSquared = dot(source, source);
// p1 and p2 encode the two decentring directions of the lens system.
vec2 offset = vec2(
2.0 * tangential.x * x * y +
tangential.y * (radiusSquared + 2.0 * x * x),
tangential.x * (radiusSquared + 2.0 * y * y) +
2.0 * tangential.y * x * y
);
// Fixed-point refinement estimates the source that maps to this output.
source = distorted - offset;
}
// Convert the converged point back to normalized texture coordinates.
return centerUv01 + source / scale;
}- Source texture
- Iterative coordinate remapOne texture read per output pixel
- Iteratively invert the tangential displacement
- Sample the source coordinate
- Display output
Why these steps are here
- Centre and scale. The optical centre becomes the origin, and the x coordinate is scaled by the render-target aspect ratio so equal distances remain circular.
- Evaluate the forward offset. Each iteration applies the same
p1/p2terms as the mathematical model. - Correct the estimate. Subtracting the predicted offset from the known distorted coordinate converges on the source point without pushing pixels or leaving raster holes.
- Return to texture space. The function removes the aspect correction and restores the optical centre before the texture lookup.
No history buffer is required. Large coefficients can make fixed-point inversion unstable.
Notes
- “Tangential” is the calibration-model name; it does not mean every point moves exactly along a circle tangent.
- The polynomial approximates the image mapping rather than tracing the actual decentered glass.
- Radial and tangential terms are commonly fitted together during camera calibration.
- Coefficients are meaningful only with the coordinate normalization used by the calibration and shader.
References
OpenCV camera calibration documentation — defines radial and tangential camera-distortion models and their calibration coefficients.