Skip to content

[Audit][High] DDA voxel traversal face selection bug in castThroughVoxels #501

@MichaelFisher1997

Description

@MichaelFisher1997

🔍 Module Scanned

src/engine/math/ (automated audit scan)

📝 Summary

The DDA (Digital Differential Analyzer) voxel traversal algorithm in castThroughVoxels has an incorrect tie-breaking condition that causes wrong face selection when Y and Z t_max values are equal. This results in blocks being placed on incorrect faces when looking at blocks from certain angles.

📍 Location

  • File: src/engine/math/ray.zig:212-224
  • Function/Scope: castThroughVoxels function, main DDA loop tie-breaking section

🔴 Severity: High

  • High: Incorrect rendering, broken features (block placement targets wrong face)

💥 Impact

When a player aims at a block and the DDA algorithm encounters a tie between t_max_y and t_max_z, the code at lines 212-224 incorrectly defaults to Y-axis stepping instead of properly handling ties. Specifically, the condition t_max_y < t_max_z misses the equal case, causing blocks to be placed on the wrong face when the ray passes through a Y-Z boundary at the same distance.

🔎 Evidence

The problematic code at lines 212-224:

if (t_max_x < t_max_y) {
if (t_max_x < t_max_z) {
x += step_x;
distance = t_max_x;
t_max_x += t_delta_x;
last_face = if (step_x > 0) .west else .east;
} else {
z += step_z;
distance = t_max_z;
t_max_z += t_delta_z;
last_face = if (step_z > 0) .north else .south;
}
} else {
if (t_max_y < t_max_z) { // line 213 - misses 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 { // line 218 - handles ties but wrong preference
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_y == t_max_z, the condition t_max_y < t_max_z at line 213 is false, so the else branch executes, stepping in Z. This is inconsistent with the X-axis logic which handles ties in its else branch. The correct behavior should handle the equal case explicitly with <= instead of <.

🛠️ Proposed Fix

Modify the tie-breaking condition at line 213 to use <= for consistent tie-breaking:

if (t_max_y <= t_max_z) { // Changed < to <= for consistent tie-breaking
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;
}

✅ Acceptance Criteria

  • The castThroughVoxels function correctly identifies block faces when t_max_y == t_max_z
  • A test case is added that verifies correct face selection for a raycast that ties on Y/Z
  • All existing tests in ray.zig still pass after the fix
  • Verified with nix develop --command zig build test

📚 References

  • Standard DDA algorithm: "A Fast Voxel Traversal Algorithm for Ray Tracing" by John Amanatides and Andrew Woo
  • Related code: src/game/player.zig:376-393 uses target.face.getOffset() to determine block placement position
  • Face enum definition: src/world/block.zig:128-134

Metadata

Metadata

Assignees

No one assigned

    Labels

    automated-auditIssues found by automated opencode audit scansbugSomething isn't workingenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions