🔍 Module Scanned
libs/zig-math/ (engine-math module, re-exported via engine-math)
📝 Summary
The Mat4.lookAt function does not handle degenerate cases where the forward direction is parallel or anti-parallel to the world up vector. When a camera looks straight up (+Y) or down (-Y), the computed right vector becomes zero, leading to an invalid view matrix with zero-length axes. This causes incorrect vertex transformation and could crash GPU operations or produce invisible geometry.
📍 Location
- File:
libs/zig-math/mat4.zig:79-103
- Function/Scope:
Mat4.lookAt function
🔴 Severity: High
- Critical: Crashes, data corruption, security vulnerabilities, GPU device loss
- High: Memory leaks, race conditions, incorrect rendering, broken features
- Medium: Performance degradation, missing error handling, suboptimal patterns
- Low: Code style, dead code, minor improvements
💥 Impact
When the camera direction is aligned with the world up vector (e.g., player looking straight up or down), lookAt produces an invalid view matrix. The right vector r = f.cross(world_up) becomes zero when f is parallel to world_up. When normalize() is called on a zero vector in libs/zig-math/vec3.zig:65-68, it returns Vec3.zero, which is not a valid direction. This causes:
- Incorrect vertex transformation in shaders
- Potential invisible geometry (if all directions collapse)
- Could cause undefined behavior in frustum culling calculations that rely on the matrix
User-visible symptoms: Camera jumps or rendering glitches when looking straight up or down.
🔎 Evidence
In libs/zig-math/mat4.zig:79-103:
pub fn lookAt(eye: Vec3, target: Vec3, world_up: Vec3) Mat4 {
const f = target.sub(eye).normalize();
const r = f.cross(world_up).normalize(); // <-- r becomes zero when f || world_up
const u = r.cross(f);
// ... fill matrix rows with r, u, -f vectors
}
The normalize() function in libs/zig-math/vec3.zig:65-68:
pub fn normalize(self: Vec3) Vec3 {
const len = self.length();
if (len == 0) return Vec3.zero; // <-- Returns zero vector on degenerate input
return self.scale(1.0 / len);
}
When f is parallel to world_up, f.cross(world_up) produces zero vector. Normalizing zero returns zero, so r = Vec3.zero. Then u = r.cross(f) also produces zero. The resulting matrix has degenerate rows, producing incorrect transformations.
🛠️ Proposed Fix
Add validation and fallback behavior in Mat4.lookAt:
- Detect near-parallel case by checking if
f.cross(world_up) magnitude is small
- Use an alternative up vector when this occurs (e.g., use X-axis as fallback when looking along Y)
- Or use the existing
lookAt in libs/zig-math/math.zig which may already handle this edge case
pub fn lookAt(eye: Vec3, target: Vec3, world_up: Vec3) Mat4 {
var f = target.sub(eye).normalize();
var r = f.cross(world_up).normalize();
// Handle degenerate case: f is parallel to world_up
if (r.lengthSquared() < 0.0001) {
// Use a different reference vector to construct orthonormal basis
const alternate_up = if (@abs(world_up.y) > 0.9) Vec3.right else Vec3.up;
r = f.cross(alternate_up).normalize();
}
const u = r.cross(f);
// ... rest of implementation
}
Also consider adding a compile-time assertion or debug-only check that validates the output matrix has orthonormal rows (or close to it).
✅ Acceptance Criteria
📚 References
- Standard lookAt implementation pattern: Gram-Schmidt orthonormalization handles degenerate cases
- OpenGL
gluLookAt and DirectX XMMatrixLookAtLH both handle this edge case explicitly
- Related:
Vec3.normalize returns zero vector for zero input (line 65-68 in vec3.zig)
🔍 Module Scanned
libs/zig-math/(engine-math module, re-exported via engine-math)📝 Summary
The
Mat4.lookAtfunction does not handle degenerate cases where the forward direction is parallel or anti-parallel to the world up vector. When a camera looks straight up (+Y) or down (-Y), the computed right vector becomes zero, leading to an invalid view matrix with zero-length axes. This causes incorrect vertex transformation and could crash GPU operations or produce invisible geometry.📍 Location
libs/zig-math/mat4.zig:79-103Mat4.lookAtfunction🔴 Severity: High
💥 Impact
When the camera direction is aligned with the world up vector (e.g., player looking straight up or down),
lookAtproduces an invalid view matrix. The right vectorr = f.cross(world_up)becomes zero whenfis parallel toworld_up. Whennormalize()is called on a zero vector inlibs/zig-math/vec3.zig:65-68, it returnsVec3.zero, which is not a valid direction. This causes:User-visible symptoms: Camera jumps or rendering glitches when looking straight up or down.
🔎 Evidence
In
libs/zig-math/mat4.zig:79-103:The
normalize()function inlibs/zig-math/vec3.zig:65-68:When
fis parallel toworld_up,f.cross(world_up)produces zero vector. Normalizing zero returns zero, sor = Vec3.zero. Thenu = r.cross(f)also produces zero. The resulting matrix has degenerate rows, producing incorrect transformations.🛠️ Proposed Fix
Add validation and fallback behavior in
Mat4.lookAt:f.cross(world_up)magnitude is smalllookAtinlibs/zig-math/math.zigwhich may already handle this edge caseAlso consider adding a compile-time assertion or debug-only check that validates the output matrix has orthonormal rows (or close to it).
✅ Acceptance Criteria
lookAtwith degenerate input vectors produces identity-like fallback matrix📚 References
gluLookAtand DirectXXMMatrixLookAtLHboth handle this edge case explicitlyVec3.normalizereturns zero vector for zero input (line 65-68 in vec3.zig)