Skip to content

Possible floating point precision issue in corner case using BoundingBox.clip() #2556

Open

Description

@riccardobl

While working on getting jme compiled to javascript I've encountered an issue: clicks went through some ui elements (lemur).
After a very long debugging session i've isolated the problem to a floating point precision issue in BoundingBox.clip that happens when bounding boxes have zExtent=0.

private boolean clip(float denom, float numerator, float[] t) {
// Return value is 'true' if line segment intersects the current test
// plane. Otherwise, 'false' is returned, in which case the line segment
// is entirely clipped.
if (denom > 0.0f) {
// This is the old if statement...
// if (numerator > denom * t[1]) {
//
// The problem is that what is actually stored is
// numerator/denom. In non-floating point, this math should
// work out the same but in floating point there can
// be subtle math errors. The multiply will exaggerate
// errors that may have been introduced when the value
// was originally divided.
//
// This is especially true when the bounding box has zero
// extents in some plane because the error rate is critical.
// comparing a to b * c is not the same as comparing a/b to c
// in this case. In fact, I tried converting this method to
// double and the and the error was in the last decimal place.
//
// So, instead, we now compare the divided version to the divided
// version. We lose some slight performance here as divide
// will be more expensive than the divide. Some microbenchmarks
// show divide to be 3x slower than multiple on Java 1.6.
// BUT... we also saved a multiply in the non-clipped case because
// we can reuse the divided version in both if checks.
// I think it's better to be right in this case.
//
// Bug that I'm fixing: rays going right through quads at certain
// angles and distances because they fail the bounding box test.
// Many Bothans died bring you this fix.
// -pspeed
float newT = numerator / denom;
if (newT > t[1]) {
return false;
}
if (newT > t[0]) {
t[0] = newT;
}
return true;
} else if (denom < 0.0f) {
// Old if statement... see above
// if (numerator > denom * t[0]) {
//
// Note though that denom is always negative in this block.
// When we move it over to the other side we have to flip
// the comparison. Algebra for the win.
float newT = numerator / denom;
if (newT < t[0]) {
return false;
}
if (newT < t[1]) {
t[1] = newT;
}
return true;
} else {
return numerator <= 0.0;
}

As you can see, in this code there is a note by @pspeed42 regarding a similar issue that he fixed in the past.

My assumption is that, the way floating point numbers are represented in javascript (doubles?) caused this issue to reappear when the engine was compiled with teavm. I've ended up fixing it in this way.

I’m opening this issue to make it searchable in the future, in case this corner case ever arises during normal use of jME.

Activity

  1. coderabbitai commented on Mar 3, 2026

    @coderabbitai

    Coding Plan

    Summary

    • Introduce a local EPSILON = 1e-6f constant in BoundingBox, matching the user's proven fix and standard geometric tolerance recommendations
    • Modify all floating-point comparisons in clip() to use epsilon-based thresholds, preventing accumulated precision errors from causing false misses
    • Apply the fix universally (not platform-specific) following JME's established pattern for precision fixes
    • Add comprehensive unit tests covering both the zero-extent edge cases and normal operation to prevent regression
    Design Choices
    Design Choice 1: Should the fix use a local epsilon constant or reuse FastMath constants?

    Options Considered:

    1. Use FastMath.FLT_EPSILON - machine epsilon, may be too small for accumulated errors
    2. Use FastMath.ZERO_TOLERANCE - may be too large, could cause false positives
    3. Define a local constant EPSILON = 1e-6f matching the user's proven fix

    Chosen Option: 3

    Rationale: A local constant provides the appropriate tolerance for geometric intersection tests (per web search recommendations of 1e-6 to 1e-5 for single-precision), matches the user's tested fix, and follows the precedent of BoundingSphere.RADIUS_EPSILON.

    Design Choice 2: Should this fix be applied universally or only for JavaScript/TeaVM builds?

    Options Considered:

    1. Add a platform check and apply epsilon only for JavaScript/TeaVM
    2. Apply the fix universally to all platforms

    Chosen Option: 2

    Rationale: JME's established pattern is to apply precision fixes universally (e.g., Vector3f.length() uses double-precision arithmetic on all platforms). Platform-specific code is reserved for system delegates, not core math operations.

    💡 User Tips

    Regenerate the plan with different choices with @coderabbitai <feedback>.

    Implementation Steps

    Phase 1: Apply Epsilon-Based Precision Fix to clip() Method

    This phase modifies the BoundingBox.clip() method to use epsilon-based comparisons instead of exact floating-point comparisons. The changes prevent rays from incorrectly passing through or missing zero-extent bounding boxes due to accumulated floating-point errors.

    Task 1: Add Epsilon Constant to BoundingBox Class

    Add a private static epsilon constant for use in geometric intersection tolerance checks.

    • Add private static final float EPSILON = 1e-6f; as a class-level constant in jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java
    • Place near other constants at the top of the class for consistency
    • Add a brief comment explaining its purpose: tolerance for floating-point precision in ray intersection tests
    Task 2: Modify clip() Method Comparisons

    Update the clip() method to use epsilon-based comparisons that tolerate small floating-point errors while maintaining algorithm correctness.

    • In jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java, modify the clip() method (lines 1015-1074):
      • Change the denom > 0 check: if (denom > 0.0f) → if (denom > EPSILON) to better handle near-parallel rays
      • Change the denom < 0 check: else if (denom < 0.0f) → else if (denom < -EPSILON) for symmetry
      • In the positive denom branch: change if (newT > t[1]) → if (newT > t[1] + EPSILON) and if (newT > t[0]) → if (newT > t[0] + EPSILON)
      • In the negative denom branch: change if (newT < t[0]) → if (newT < t[0] - EPSILON) and if (newT < t[1]) → if (newT < t[1] - EPSILON)
      • In the parallel ray case (else branch): change return numerator <= 0.0; → return numerator <= EPSILON;
    • Add a comment block explaining this addresses floating-point precision issues with zero-extent bounding boxes, referencing this ticket for context
    🤖 Prompt for AI agents
    Modify the `BoundingBox.clip()` method to use epsilon-based comparisons instead
    of exact floating-point comparisons to fix ray intersection failures with
    zero-extent bounding boxes.
    
    **Add Epsilon Constant:**
    - In `jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java`, add `private
    static final float EPSILON = 1e-6f;` as a class-level constant near other
    constants at the top of the class
    - Add a brief comment explaining its purpose: tolerance for floating-point
    precision in ray intersection tests
    
    **Modify clip() Method (lines 1015-1074):**
    - Replace exact floating-point comparisons with epsilon-based comparisons:
    - Change `if (denom > 0.0f)` to `if (denom > EPSILON)` to better handle
    near-parallel rays
      - Change `else if (denom < 0.0f)` to `else if (denom < -EPSILON)` for symmetry
      - In the positive denom branch:
        - Change `if (newT > t[1])` to `if (newT > t[1] + EPSILON)`
        - Change `if (newT > t[0])` to `if (newT > t[0] + EPSILON)`
      - In the negative denom branch:
        - Change `if (newT < t[0])` to `if (newT < t[0] - EPSILON)`
        - Change `if (newT < t[1])` to `if (newT < t[1] - EPSILON)`
    - In the parallel ray case (else branch): change `return numerator <= 0.0;` to
    `return numerator <= EPSILON;`
    - Add a comment block explaining this addresses floating-point precision issues
    with zero-extent bounding boxes, referencing the ticket for context
    

    Phase 2: Add Unit Tests for clip() Method Edge Cases

    Create comprehensive tests to verify the fix works correctly and prevent regression. The clip() method currently has no test coverage.

    Task 1: Create Test Cases for Zero-Extent Bounding Box Intersections

    Add unit tests that specifically exercise the problematic scenarios with flat bounding boxes.

    • Create or extend test class at jme3-core/src/test/java/com/jme3/bounding/TestBoundingBox.java
    • Add test method for ray hitting a flat bounding box (zExtent=0) head-on - should detect collision
    • Add test method for ray hitting a flat bounding box at various angles - should detect collision
    • Add test method for ray missing a flat bounding box - should correctly report no collision
    • Add test methods for yExtent=0 and xExtent=0 cases to ensure all axes are covered
    • Add test method for ray parallel to the flat face, positioned exactly on the plane - edge case that was failing
    Task 2: Add Regression Tests for Normal Bounding Box Behavior

    Ensure the epsilon changes don't break normal (non-zero-extent) bounding box intersection behavior.

    • Add test methods in jme3-core/src/test/java/com/jme3/bounding/TestBoundingBox.java for:
      • Ray intersecting a normal 3D bounding box from outside - should return 2 collision points (entry and exit)
      • Ray starting inside a bounding box - should return appropriate collision count
      • Ray missing a bounding box entirely - should return 0 collisions
      • Ray tangent to a bounding box surface - should correctly detect the single contact point
    • Verify that collision distances/points are accurate within expected tolerance
    🤖 Prompt for AI agents
    Create comprehensive unit tests to verify the epsilon-based precision fix works
    correctly for both zero-extent edge cases and normal bounding box operations.
    
    **Create or extend test class:**
    - File: `jme3-core/src/test/java/com/jme3/bounding/TestBoundingBox.java`
    
    **Add test methods for zero-extent bounding box intersections:**
    - Test ray hitting a flat bounding box (zExtent=0) head-on - should detect
    collision
    - Test ray hitting a flat bounding box at various angles - should detect
    collision
    - Test ray missing a flat bounding box - should correctly report no collision
    - Test yExtent=0 and xExtent=0 cases to ensure all axes are covered
    - Test ray parallel to the flat face, positioned exactly on the plane - edge
    case that was failing
    
    **Add regression tests for normal (non-zero-extent) bounding box behavior:**
    - Test ray intersecting a normal 3D bounding box from outside - should return 2
    collision points (entry and exit)
    - Test ray starting inside a bounding box - should return appropriate collision
    count
    - Test ray missing a bounding box entirely - should return 0 collisions
    - Test ray tangent to a bounding box surface - should correctly detect the
    single contact point
    - Verify that collision distances/points are accurate within expected tolerance
    
    Research

    The BoundingBox class in jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java implements axis-aligned bounding box collision detection. The clip() method (lines 1015-1074) is a private helper implementing the Liang-Barsky line clipping algorithm for ray-box intersection. It's called 6 times per intersection test (once per box face) from collideWithRay() methods. The method already contains a previous precision fix by @pspeed42 that switched from multiplication-based to division-based comparisons, with extensive comments explaining why zero-extent boxes are particularly sensitive to floating-point errors. Currently, no unit tests exist for the clip() method.


    🚀 Next Steps

    🤖 All AI agent prompts combined
    Task: 1
    
    Modify the `BoundingBox.clip()` method to use epsilon-based comparisons instead
    of exact floating-point comparisons to fix ray intersection failures with
    zero-extent bounding boxes.
    
    **Add Epsilon Constant:**
    - In `jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java`, add `private
    static final float EPSILON = 1e-6f;` as a class-level constant near other
    constants at the top of the class
    - Add a brief comment explaining its purpose: tolerance for floating-point
    precision in ray intersection tests
    
    **Modify clip() Method (lines 1015-1074):**
    - Replace exact floating-point comparisons with epsilon-based comparisons:
    - Change `if (denom > 0.0f)` to `if (denom > EPSILON)` to better handle
    near-parallel rays
      - Change `else if (denom < 0.0f)` to `else if (denom < -EPSILON)` for symmetry
      - In the positive denom branch:
        - Change `if (newT > t[1])` to `if (newT > t[1] + EPSILON)`
        - Change `if (newT > t[0])` to `if (newT > t[0] + EPSILON)`
      - In the negative denom branch:
        - Change `if (newT < t[0])` to `if (newT < t[0] - EPSILON)`
        - Change `if (newT < t[1])` to `if (newT < t[1] - EPSILON)`
    - In the parallel ray case (else branch): change `return numerator <= 0.0;` to
    `return numerator <= EPSILON;`
    - Add a comment block explaining this addresses floating-point precision issues
    with zero-extent bounding boxes, referencing the ticket for context
    ===============================================================================
    
    Task: 2
    
    Create comprehensive unit tests to verify the epsilon-based precision fix works
    correctly for both zero-extent edge cases and normal bounding box operations.
    
    **Create or extend test class:**
    - File: `jme3-core/src/test/java/com/jme3/bounding/TestBoundingBox.java`
    
    **Add test methods for zero-extent bounding box intersections:**
    - Test ray hitting a flat bounding box (zExtent=0) head-on - should detect
    collision
    - Test ray hitting a flat bounding box at various angles - should detect
    collision
    - Test ray missing a flat bounding box - should correctly report no collision
    - Test yExtent=0 and xExtent=0 cases to ensure all axes are covered
    - Test ray parallel to the flat face, positioned exactly on the plane - edge
    case that was failing
    
    **Add regression tests for normal (non-zero-extent) bounding box behavior:**
    - Test ray intersecting a normal 3D bounding box from outside - should return 2
    collision points (entry and exit)
    - Test ray starting inside a bounding box - should return appropriate collision
    count
    - Test ray missing a bounding box entirely - should return 0 collisions
    - Test ray tangent to a bounding box surface - should correctly detect the
    single contact point
    - Verify that collision distances/points are accurate within expected tolerance
    
    💡 Iterate on the plan with: @coderabbitai <feedback>
    Example Feedback
    - `@coderabbitai` You can skip phase 3. Add a simple unit test case for phase 2.
    - `@coderabbitai` For design choice 1 go ahead with option 3 and replan.
    

    💬 Have feedback or questions? Drop into our discord!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs investigationdefectSomething that is supposed to work, but doesn't. Less severe than a "bug"

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions