Possible bug in ndShapeStatic_bvh::RayCast – ray.m_normal.m_

Report any bugs here and we'll post fixes

Moderators: Sascha Willems, Thomas

Possible bug in ndShapeStatic_bvh::RayCast – ray.m_normal.m_

Postby Lax » Sat Dec 06, 2025 7:03 pm

Hi Julio,

As I'm proceeding in the OgreNewt4 wrapper and I’ve run into a reproducible assert in the ray casting code against a ndShapeStatic_bvh.

Rays: basic vertical rays from player controller (downwards ray to the ground, plus some forward rays)
Integration: OgreNewt4 wrapper calling ndWorld::RayCast(...) with a closest-hit callback

In debug, I get this assertion inside ndShapeStatic_bvh::RayCast:

Code: Select all
ndFloat32 ndShapeStatic_bvh::RayCast(ndRayCastNotify& callback,
                                     const ndVector& localP0,
                                     const ndVector& localP1,
                                     ndFloat32 maxT,
                                     const ndBody* const body,
                                     ndContactPoint& contactOut) const
{
    ndBvhRay ray(localP0, localP1);
    ray.m_t = ndFloat32(1.0f);
    ray.m_me = this;
    ray.m_myBody = ((ndBody*)body)->GetAsBodyKinematic();
    ray.m_callback = &callback;
    ndFloat32 t = ndFloat32 (1.2f);

    ForAllSectorsRayHit(ray, maxT, RayHit, &ray);

    if (ray.m_t < maxT)
    {
        t = ray.m_t;
        ndAssert(ray.m_normal.m_w == ndFloat32(0.0f));                // <- asserts here
        ndAssert(ray.m_normal.DotProduct(ray.m_normal).GetScalar() > ndFloat32(0.0f));
        contactOut.m_normal = ray.m_normal.Normalize();
        contactOut.m_shapeId0 = ray.m_id;
        contactOut.m_shapeId1 = ray.m_id;
    }
    return t;
}


After ForAllSectorsRayHit, the ray.m_normal looks like this in the debugger:

Code: Select all
ray.m_normal = {
    x ≈ 8.5e-12,
    y ≈ 9.1e-43,
    z ≈ 8.5e-12,
    w ≈ 9.1e-43
}



So xyz are finite and small, but w is non-zero garbage (looks like an uninitialized stack float). Because of the debug assert that m_w must be exactly 0, this hits every time the ray actually hits the mesh.

If I comment out the assert, the raycast behaves correctly (the hit normal passed into my callback is fine in xyz, and gameplay works), so this is a debug-only cleanliness issue.

How I call the raycast

From my OgreNewt4 wrapper:

Code: Select all
void Raycast::go(const World* world,
                 const Ogre::Vector3& startpt,
                 const Ogre::Vector3& endpt,
                 int /*threadIndex*/)
{
    mLastBody   = nullptr;
    mBodyAdded  = false;
    mStart      = startpt;
    mEnd        = endpt;

    mCallback.m_param   = ndFloat32(1.0f);
    mCallback.m_contact = ndContactPoint();

    ndWorld* const ndworld = world->getNewtonWorld();

    const ndVector p0(startpt.x, startpt.y, startpt.z, ndFloat32(1.0f));
    const ndVector p1(endpt.x,   endpt.y,   endpt.z,   ndFloat32(1.0f));

    ndworld->RayCast(mCallback, p0, p1);
}


Its a common scenario in my NOWA-Engine and nothing has changed and in OgreNewt3 (ND3) it did work.

And in ndScene::RayCast Newton masks the w-component anyway:

Code: Select all
bool ndScene::RayCast(ndRayCastNotify& callback,
                      const ndVector& globalOrigin,
                      const ndVector& globalDest) const
{
    const ndVector p0(globalOrigin & ndVector::m_triplexMask);
    const ndVector p1(globalDest   & ndVector::m_triplexMask);
    ...
}


So from my side, xyz are valid and w is don’t-care; you already zero it inside ndScene::RayCast.

The player ray in this particular case is something like:

Code: Select all
start: (-2.5917597,  0.29416525, -8.9903698)
end  : (-2.5917597, -499.70584,  -8.9903698)


The scene has a ndShapeStatic_bvh built from a triangle soup; degenerate triangles are already filtered out on my side (I skip faces with any edge² < 1e-12 and slightly extrude almost-flat faces), so this assert is not related to the triangle soup degeneracy I fixed earlier.

Analysis

Looking at the behavior:

ray.m_t is less than maxT so the ray did hit something.

ray.m_normal has valid xyz, but m_w is never explicitly set to 0 anywhere in the callback path.

The only place that writes ray.m_normal is the internal callback passed to ForAllSectorsRayHit, which is ndShapeStatic_bvh::RayHit(...).

From that, it looks like RayHit sets the normal’s xyz but leaves w uninitialized, and then ndShapeStatic_bvh::RayCast asserts that w is exactly 0. That’s what I’m observing in the debugger: x,z ≈ 8.5e-12, but w is tiny non-zero stack junk like 9.136e-43.

In release builds this doesn’t matter, since no code uses w and everything works fine.

Proposed minimal fix

In ndShapeStatic_bvh::RayHit(...) (wherever you finalize the hit and assign ray.m_normal), you can force w = 0 before storing it:

Code: Select all
ndFloat32 ndShapeStatic_bvh::RayHit(
    void* context,
    const ndFloat32* vertex,
    ndInt32 strideInBytes,
    const ndInt32* indexArray,
    ndInt32 indexCount)
{
    ndBvhRay& ray = *(ndBvhRay*)context;

    // ... compute face normal as ndVector normal (xyz valid, w arbitrary) ...

    if (t < ray.m_t)
    {
        ray.m_t = t;

        // Ensure w = 0 to satisfy RayCast debug assert
        normal = normal & ndVector::m_triplexMask;  // (1,1,1,0)
        ray.m_normal = normal;

        ...
    }

    return t;
}


or equivalently:

Code: Select all
ray.m_normal = normal & ndVector::m_triplexMask;


And in another place the same issue:
Code: Select all
ndFloat32 ndShapeStatic_bvh::RayCast(ndRayCastNotify& callback, const ndVector& localP0, const ndVector& localP1, ndFloat32 maxT, const ndBody* const body, ndContactPoint& contactOut) const
{
   ndBvhRay ray(localP0, localP1);
   ray.m_t = ndFloat32(1.0f);
   ray.m_me = this;
   ray.m_myBody = ((ndBody*)body)->GetAsBodyKinematic();
   ray.m_callback = &callback;
   ndFloat32 t = ndFloat32 (1.2f);
   ForAllSectorsRayHit(ray, maxT, RayHit, &ray);
   if (ray.m_t < maxT)
   {
      t = ray.m_t;
      ndAssert(ray.m_normal.m_w == ndFloat32(0.0f));
      ndAssert(ray.m_normal.DotProduct(ray.m_normal).GetScalar() > ndFloat32(0.0f));
      contactOut.m_normal = ray.m_normal.Normalize();
      contactOut.m_shapeId0 = ray.m_id;
      contactOut.m_shapeId1 = ray.m_id;
   }
   return t;
}


Same solution:
after the:
Code: Select all
t = ray.m_t;
ray.m_normal = ray.m_normal & ndVector::m_triplexMask;


This keeps xyz exactly as computed, but guarantees ray.m_normal.m_w == 0.0f, so the assert in ndShapeStatic_bvh::RayCast becomes valid and never fires.

Why this is safe

My code only ever reads the xyz components of the normal (I convert it to an Ogre::Vector3), so modifying w has no behavioral impact.

Your ndScene::RayCast already uses m_triplexMask on positions, so treating w as “must be 0” is consistent with the rest of the codebase.

The bug only manifests in debug builds (due to the assert); release already works, which supports the idea that only w is not being initialized.

Best Regards
Lax
Lax
 
Posts: 196
Joined: Sat Jan 08, 2011 8:24 am

Re: Possible bug in ndShapeStatic_bvh::RayCast – ray.m_norma

Postby Julio Jerez » Sat Dec 06, 2025 9:42 pm

no sure where those functions are from.
The one in the latest sdk does mask the w component

Code: Select all
ndFloat32 ndShapeStatic_bvh::RayHit(void* const context, const ndFloat32* const polygon, ndInt32 strideInBytes, const ndInt32* const indexArray, ndInt32 indexCount)
{
   ndBvhRay& me = *((ndBvhRay*)context);
   ndVector normal(&polygon[indexArray[indexCount + 1] * (strideInBytes / sizeof(ndFloat32))]);
   normal = normal & ndVector::m_triplexMask;


the second only only did the assert, but I added the masking, sync and see if that is what you need.

Also these results look very suspicions for a normal result.
Code: Select all
ray.m_normal = {
    x ≈ 8.5e-12,
    y ≈ 9.1e-43,
    z ≈ 8.5e-12,
    w ≈ 9.1e-43
}


the normal is the area of the triangle hit, are the triangles ridicules tiny?
0ne 1e-43 is a denormal float in 32 bit floats that should trigger an exception, unless you are using double you using double precision?

Seems like something else is wrong. It looks like some tringles is you mesh are too small.

can you save your mesh as a fbx file?
if so, then is can be tested SDK sandbox.
Julio Jerez
Moderator
Moderator
 
Posts: 12478
Joined: Sun Sep 14, 2003 2:18 pm
Location: Los Angeles


Return to Bugs and Fixes

Who is online

Users browsing this forum: No registered users and 4 guests