EN-011 V2 — modify a projection matrix so its near plane is clipped at the given eye-space plane. Used for planar reflection to prevent geometry below the water from polluting the reflection along the shoreline edge. Reference: Eric Lengyel, "Oblique View Frustum Depth Projection and Clipping" (Journal of Game Development, 2005). The technique shifts the projection's near plane to coincide with t
(
proj: [[f32; 4]; 4],
plane_eye_space: [f32; 4],
)
| 228 | /// plane satisfy `N · p_eye + d = 0`. Use `world_plane_to_eye_space` |
| 229 | /// to convert from a world-space plane. |
| 230 | pub fn oblique_proj( |
| 231 | proj: [[f32; 4]; 4], |
| 232 | plane_eye_space: [f32; 4], |
| 233 | ) -> [[f32; 4]; 4] { |
| 234 | let c = plane_eye_space; |
| 235 | |
| 236 | // Far-plane corner in clip space is in the direction of (sgn(c.x), |
| 237 | // sgn(c.y), 1, 1) — Lengyel §2. Pulled back into eye space by |
| 238 | // multiplying with `inv(proj)`. |
| 239 | let sx = if c[0] >= 0.0 { 1.0 } else { -1.0 }; |
| 240 | let sy = if c[1] >= 0.0 { 1.0 } else { -1.0 }; |
| 241 | let q_clip = [sx, sy, 1.0, 1.0]; |
| 242 | let inv_p = mat4_invert(proj); |
| 243 | let q = mat4_mul_vec4(&inv_p, &q_clip); |
| 244 | |
| 245 | // Scale `c` so the near-plane crosses through the supplied plane: |
| 246 | // M = (2 / dot(c, q)) · c |
| 247 | // Then the new third row of P (which controls the depth output) is |
| 248 | // P_row2 = M - P_row3 |
| 249 | // (P_row3 is the standard perspective w-row, all stays the same.) |
| 250 | let denom = c[0] * q[0] + c[1] * q[1] + c[2] * q[2] + c[3] * q[3]; |
| 251 | if denom.abs() < 1e-10 { |
| 252 | // Degenerate plane orientation w.r.t. the frustum — leave |
| 253 | // projection unchanged rather than divide-by-zero. The |
| 254 | // reflection still renders, just without near-plane clipping. |
| 255 | return proj; |
| 256 | } |
| 257 | let scale = 2.0 / denom; |
| 258 | let m = [c[0] * scale, c[1] * scale, c[2] * scale, c[3] * scale]; |
| 259 | |
| 260 | // proj is column-major: proj[col][row]. The "third row" we want |
| 261 | // to replace is at row index 2 across all four columns. The |
| 262 | // "fourth row" is at row index 3. New row-2 = M - row3 — but |
| 263 | // since wgpu's clip-space z range is [0, 1] (not [-1, 1] like |
| 264 | // OpenGL), the depth-rescaling pre-step is `M - P_row3` exactly |
| 265 | // as in Lengyel's original derivation; the [-1, 1] vs [0, 1] |
| 266 | // difference is absorbed by the scale. |
| 267 | let mut out = proj; |
| 268 | out[0][2] = m[0] - proj[0][3]; |
| 269 | out[1][2] = m[1] - proj[1][3]; |
| 270 | out[2][2] = m[2] - proj[2][3]; |
| 271 | out[3][2] = m[3] - proj[3][3]; |
| 272 | out |
| 273 | } |
| 274 | |
| 275 | fn normalise(v: [f32; 3]) -> [f32; 3] { |
| 276 | let len_sq = v[0] * v[0] + v[1] * v[1] + v[2] * v[2]; |