Dual-Paraboloid Mapping Coordinates in GLSL
Dual-paraboloid environment mapping encodes a full 360-degree panoramic view into two parabolic textures: one for the forward hemisphere and one for the backward hemisphere. This article provides the mathematical derivation for transforming a 3D direction or reflection vector into 2D texture coordinates \((u, v)\) for both paraboloids, followed by an efficient implementation in OpenGL Shading Language (GLSL).
Geometric Principles of the Paraboloid
A paraboloid with its focus at the origin and opening towards the negative \(z\)-axis can be defined implicitly by:
\[z = \frac{1}{2} - \frac{1}{2}(x^2 + y^2)\]
The key optical property of a paraboloid is that any ray originating from the focus \((0, 0, 0)\) reflects off the inner surface parallel to the principal axis—in this case, along the vector \(\vec{d} = (0, 0, 1)\).
To find the surface normal \(\vec{n}\) at a given point \((x, y, z)\) on this surface, express the paraboloid as the zero level-set of a function \(F(x, y, z)\):
\[F(x, y, z) = \frac{1}{2}(x^2 + y^2) + z - \frac{1}{2} = 0\]
The gradient \(\nabla F\) gives an unnormalized normal vector:
\[\nabla F = (x, y, 1)\]
Normalizing this vector yields the unit surface normal \(\vec{N}\):
\[\vec{N} = \frac{(x, y, 1)}{\sqrt{x^2 + y^2 + 1}}\]
Forward Derivation: Relating the Ray to Surface Coordinates
When a ray leaves the origin in direction \(\vec{R} = (R_x, R_y, R_z)\) (where \(\|\vec{R}\| = 1\)), it strikes the paraboloid at \((x, y, z)\) and reflects along the direction \(\vec{V} = (0, 0, 1)\). By the law of specular reflection:
\[\vec{R} = 2(\vec{N} \cdot \vec{V})\vec{N} - \vec{V}\]
Substitute \(\vec{V} = (0, 0, 1)\) and \(\vec{N} = \frac{(x, y, 1)}{\sqrt{x^2 + y^2 + 1}}\):
\[\vec{N} \cdot \vec{V} = \frac{1}{\sqrt{x^2 + y^2 + 1}}\]
Now substitute this dot product back into the reflection equation:
\[\vec{R} = 2\left(\frac{1}{\sqrt{x^2 + y^2 + 1}}\right) \left(\frac{(x, y, 1)}{\sqrt{x^2 + y^2 + 1}}\right) - (0, 0, 1)\]
\[\vec{R} = \frac{2}{x^2 + y^2 + 1}(x, y, 1) - (0, 0, 1)\]
Breaking this into vector components:
\[R_x = \frac{2x}{x^2 + y^2 + 1}\]
\[R_y = \frac{2y}{x^2 + y^2 + 1}\]
\[R_z = \frac{2}{x^2 + y^2 + 1} - 1 = \frac{1 - (x^2 + y^2)}{x^2 + y^2 + 1}\]
Inverting to Solve for \((x, y)\)
To sample a texture, \((x, y)\) must be solved in terms of the known unit vector \(\vec{R}\). Adding \(1\) to the \(R_z\) expression:
\[R_z + 1 = \frac{1 - (x^2 + y^2)}{x^2 + y^2 + 1} + 1 = \frac{2}{x^2 + y^2 + 1}\]
Dividing \(R_x\) and \(R_y\) by \((R_z + 1)\):
\[\frac{R_x}{R_z + 1} = \frac{\frac{2x}{x^2 + y^2 + 1}}{\frac{2}{x^2 + y^2 + 1}} = x\]
\[\frac{R_y}{R_z + 1} = \frac{\frac{2y}{x^2 + y^2 + 1}}{\frac{2}{x^2 + y^2 + 1}} = y\]
Because the hemisphere covers the domain \(x^2 + y^2 \le 1\), the coordinates \(x\) and \(y\) are constrained to \([-1, 1]\).
Texture Coordinate Mapping
To map \(x, y \in [-1, 1]\) into standard normalized UV coordinates \([0, 1]\):
\[u = \frac{x}{2} + 0.5 = \frac{R_x}{2(1 + R_z)} + 0.5\]
\[v = \frac{y}{2} + 0.5 = \frac{R_y}{2(1 + R_z)} + 0.5\]
Derivation for the Backward Hemisphere
The second paraboloid faces the opposite direction along the negative \(z\)-axis. Repeating the reflection geometry with \(\vec{V} = (0, 0, -1)\) flips the sign of \(R_z\). To preserve clockwise/counter-clockwise orientation and prevent mirror inversion, the resulting coordinates are:
\[u = \frac{R_x}{2(1 - R_z)} + 0.5\]
\[v = \frac{R_y}{2(1 - R_z)} + 0.5\]
GLSL Implementation
In a GLSL shader, determine whether the reflection ray points forward (\(R_z \ge 0\)) or backward (\(R_z < 0\)), compute the UV coordinates using the respective denominator, and sample the appropriate texture:
// R: Normalized 3D reflection or view vector
vec4 sampleDualParaboloid(sampler2D frontMap, sampler2D backMap, vec3 R) {
vec2 uv;
if (R.z >= 0.0) {
// Forward hemisphere
uv.x = (R.x / (2.0 * (1.0 + R.z))) + 0.5;
uv.y = (R.y / (2.0 * (1.0 + R.z))) + 0.5;
return texture(frontMap, uv);
} else {
// Backward hemisphere
uv.x = (R.x / (2.0 * (1.0 - R.z))) + 0.5;
uv.y = (R.y / (2.0 * (1.0 - R.z))) + 0.5;
return texture(backMap, uv);
}
}When storing both maps inside a single sampler2DArray,
the layer index can be selected directly using a condition such as
float layer = R.z >= 0.0 ? 0.0 : 1.0;, avoiding multiple
branching texture fetch routines.