🔍 Module Scanned
modules/engine-math/src/ (automated audit scan)
📝 Summary
The castThroughVoxels DDA algorithm in ray.zig has a bug where the last_face is not updated when the ray hits two or three voxel boundaries simultaneously (within floating-point epsilon). This causes the function to return the face from the previous step instead of the correct face for the hit voxel.
📍 Location
- File:
modules/engine-math/src/ray.zig:199-214
- Function/Scope:
castThroughVoxels (lines 132-218)
🔴 Severity: High
- High: Incorrect block selection in raycasts, wrong face normal for block interactions, broken block placement/destruction in game
💥 Impact
When a player points at a block corner/edge (ray passing exactly through a voxel boundary), the game will report the wrong face as the hit face. This causes:
- Block placement on wrong face (placing blocks where player isn't looking)
- Block breaking targets wrong face
- Raycast hit info is inconsistent with actual traversal
🔎 Evidence
// Lines 199-214: The problem is that last_face is NOT updated when t_max_x == t_max_y or t_max_y == t_max_z
if (t_max_x <= t_max_y and t_max_x <= t_max_z) {
x += step_x;
distance = t_max_x;
t_max_x += t_delta_x;
// BUG: last_face is NOT set here when t_max_x == t_max_y or t_max_x == t_max_z
last_face = if (step_x > 0) .west else .east; // Only set in this branch
} else if (t_max_y <= t_max_z) {
y += step_y;
distance = t_max_y;
t_max_y += t_delta_y;
last_face = if (step_y > 0) .bottom else .top;
} else {
z += step_z;
distance = t_max_z;
t_max_z += t_delta_z;
last_face = if (step_z > 0) .north else .south;
}
When t_max_x <= t_max_y and t_max_x <= t_max_z is true, last_face is set. However, when t_max_x == t_max_y or t_max_x == t_max_z, the ray is hitting multiple boundaries simultaneously and the face should reflect ALL axes that moved. The condition t_max_x <= t_max_y and t_max_x <= t_max_z is still true in this case, so we take this branch, but we don't update last_face when we also moved in another axis.
🛠️ Proposed Fix
When axes tie, last_face should be set based on all axes that stepped, not just the first one in the priority order. For example, when stepping in X and Y simultaneously (t_max_x == t_max_y < t_max_z), last_face should be determined by which axis had the larger step direction or by a tie-breaking rule.
The fix should be:
- After determining which axes stepped, update
last_face to reflect the actual face where the hit occurred
- When multiple axes tie,
last_face should represent the most significant face of the hit
// Example fix approach:
if (t_max_x <= t_max_y and t_max_x <= t_max_z) {
x += step_x;
distance = t_max_x;
t_max_x += t_delta_x;
// Determine face BEFORE potentially stepping in other axes
if (t_max_y <= t_max_z and @abs(t_max_x - t_max_y) < 1e-8) {
// Also stepping in Y - face depends on combined movement
last_face = if (step_y > 0) .bottom else .top;
} else {
last_face = if (step_x > 0) .west else .east;
}
}
Alternatively, the face could be computed at the end based on the direction that caused the hit, taking into account that multiple axes may have moved simultaneously.
✅ Acceptance Criteria
📚 References
🔍 Module Scanned
modules/engine-math/src/(automated audit scan)📝 Summary
The
castThroughVoxelsDDA algorithm inray.zighas a bug where thelast_faceis not updated when the ray hits two or three voxel boundaries simultaneously (within floating-point epsilon). This causes the function to return the face from the previous step instead of the correct face for the hit voxel.📍 Location
modules/engine-math/src/ray.zig:199-214castThroughVoxels(lines 132-218)🔴 Severity: High
💥 Impact
When a player points at a block corner/edge (ray passing exactly through a voxel boundary), the game will report the wrong face as the hit face. This causes:
🔎 Evidence
When
t_max_x <= t_max_y and t_max_x <= t_max_zis true,last_faceis set. However, whent_max_x == t_max_yort_max_x == t_max_z, the ray is hitting multiple boundaries simultaneously and the face should reflect ALL axes that moved. The conditiont_max_x <= t_max_y and t_max_x <= t_max_zis still true in this case, so we take this branch, but we don't updatelast_facewhen we also moved in another axis.🛠️ Proposed Fix
When axes tie,
last_faceshould be set based on all axes that stepped, not just the first one in the priority order. For example, when stepping in X and Y simultaneously (t_max_x == t_max_y < t_max_z),last_faceshould be determined by which axis had the larger step direction or by a tie-breaking rule.The fix should be:
last_faceto reflect the actual face where the hit occurredlast_faceshould represent the most significant face of the hitAlternatively, the face could be computed at the end based on the direction that caused the hit, taking into account that multiple axes may have moved simultaneously.
✅ Acceptance Criteria
castThroughVoxelsreturns correctfacewhen ray passes through voxel boundariesmodules/engine-math/src/ray.zigcontinue to passnix develop --command zig build testpasses📚 References
std.math.approxEqor direct epsilon comparison