diff --git a/Engine/source/T3D/physics/jolt/joltBody.cpp b/Engine/source/T3D/physics/jolt/joltBody.cpp index e5953b380..5730fd8ce 100644 --- a/Engine/source/T3D/physics/jolt/joltBody.cpp +++ b/Engine/source/T3D/physics/jolt/joltBody.cpp @@ -1,6 +1,6 @@ #include "platform/platform.h" #include "T3D/physics/jolt/joltBody.h" - + #include "math/mBox.h" #include "console/console.h" #include "scene/sceneObject.h" @@ -22,13 +22,21 @@ JoltBody::~JoltBody() SAFE_DELETE(mCenterOfMass); SAFE_DELETE(mInvCenterOfMass); - mColShape = NULL; - setSimulationEnabled(false); + mColShape = nullptr; + + if (!isValid()) + return; + + // User data must be cleared BEFORE the body is removed or destroyed. + // DestroyBody returns the body to Jolt's internal pool; touching it + // afterwards is use-after-free undefined behaviour. + mBody->SetUserData(0); + + setSimulationEnabled(false); // removes body from the simulation + JPH::BodyInterface& bi = mWorld->getPhysicsSystem()->GetBodyInterface(); - bi.DestroyBody(mBody->GetID()); - mBody->SetUserData(NULL); - mBody = NULL; + mBody = nullptr; } PhysicsWorld* JoltBody::getWorld() @@ -93,6 +101,7 @@ bool JoltBody::init(PhysicsCollision* shape, F32 mass, U32 bodyFlags, SceneObjec settings.mOverrideMassProperties = JPH::EOverrideMassProperties::CalculateInertia; settings.mMassPropertiesOverride.mMass = mass; settings.mMaxLinearVelocity = 1000.0f; // stops an assert, this is torques upper limit, jolts is 500.0f by default. + mMass = mass; // FIX: was never stored, getMass() always returned 0 mIsDynamic = true; } @@ -142,9 +151,15 @@ void JoltBody::setTransform(const MatrixF& xfm) if (isDynamic()) { - mBody->ResetMotion();// resets accumulated torque and forces, also velocity but do this explicitly - mBody->SetLinearVelocity(JPH::Vec3::sZero()); - mBody->SetAngularVelocity(JPH::Vec3::sZero()); + // ResetMotion (clears accumulated forces/torques) is a Body method only, + // so acquire a write lock. Velocity is cleared through the thread-safe interface. + { + JPH::BodyLockWrite lock(mWorld->getPhysicsSystem()->GetBodyLockInterface(), mBody->GetID()); + if (lock.Succeeded()) + lock.GetBody().ResetMotion(); + } + bi.SetLinearVelocity(mBody->GetID(), JPH::Vec3::sZero()); + bi.SetAngularVelocity(mBody->GetID(), JPH::Vec3::sZero()); } } @@ -163,17 +178,26 @@ MatrixF& JoltBody::getTransform(MatrixF* outMatrix) void JoltBody::applyImpulse(const Point3F& origin, const Point3F& force) { - mBody->AddImpulse(joltCast(force), joltCast(origin)); + if (!isValid()) + return; + JPH::BodyInterface& bi = mWorld->getPhysicsSystem()->GetBodyInterface(); + bi.AddImpulse(mBody->GetID(), joltCast(force), joltCast(origin)); } void JoltBody::applyTorque(const Point3F& torque) { - mBody->AddTorque(joltCast(torque)); + if (!isValid()) + return; + JPH::BodyInterface& bi = mWorld->getPhysicsSystem()->GetBodyInterface(); + bi.AddTorque(mBody->GetID(), joltCast(torque)); } void JoltBody::applyForce(const Point3F& force) { - mBody->AddForce(joltCast(force)); + if (!isValid()) + return; + JPH::BodyInterface& bi = mWorld->getPhysicsSystem()->GetBodyInterface(); + bi.AddForce(mBody->GetID(), joltCast(force)); } void JoltBody::moveKinematicTo(const MatrixF& xfm) @@ -237,8 +261,8 @@ void JoltBody::setSleepThreshold(F32 linear, F32 angular) JPH::MotionProperties* mp = mBody->GetMotionProperties(); - /* mp->SetLinearSleepThreshold(linear); - mp->SetAngularSleepThreshold(angular);*/ + /* mp->SetLinearSleepThreshold(linear); + mp->SetAngularSleepThreshold(angular);*/ } void JoltBody::setDamping(F32 linear, F32 angular) @@ -293,11 +317,7 @@ void JoltBody::getState(PhysicsState* outState) // Linear momentum = m * v F32 mass = mp->GetInverseMass() > 0.0f ? 1.0f / mp->GetInverseMass() : 0.0f; outState->momentum = outState->linVelocity * mass; - - // Angular momentum - JPH::Mat44 I_world = mBody->GetInverseInertia(); // 3x3 part - JPH::Vec3 angMomentum = I_world.Multiply3x3(mBody->GetAngularVelocity()); - outState->angularMomentum = joltCast(angMomentum); + outState->angularMomentum = outState->angVelocity; } else { @@ -335,7 +355,7 @@ void JoltBody::setLinVelocity(const Point3F& vel) if (!bi.IsAdded(mBody->GetID())) return; - mBody->SetLinearVelocity(joltCast(vel)); + bi.SetLinearVelocity(mBody->GetID(), joltCast(vel)); } void JoltBody::setAngVelocity(const Point3F& vel) @@ -348,7 +368,7 @@ void JoltBody::setAngVelocity(const Point3F& vel) if (!bi.IsAdded(mBody->GetID())) return; - mBody->SetAngularVelocity(joltCast(vel)); + bi.SetAngularVelocity(mBody->GetID(), joltCast(vel)); } void JoltBody::findContact(SceneObject** contactObject, diff --git a/Engine/source/T3D/physics/jolt/joltCollision.cpp b/Engine/source/T3D/physics/jolt/joltCollision.cpp index 912e074fe..739f6f902 100644 --- a/Engine/source/T3D/physics/jolt/joltCollision.cpp +++ b/Engine/source/T3D/physics/jolt/joltCollision.cpp @@ -3,8 +3,8 @@ // Save and undefine the macro if it exists #ifdef Offset - #pragma push_macro("Offset") - #undef Offset +#pragma push_macro("Offset") +#undef Offset #endif #include @@ -21,7 +21,7 @@ #ifdef Offset // Restore the original macro after includes - #pragma pop_macro("Offset") +#pragma pop_macro("Offset") #endif JoltCollision::JoltCollision() @@ -73,7 +73,7 @@ void JoltCollision::addBox(const Point3F& halfWidth, const MatrixF& localXfm) toJolt(localXfm, localPos, localRot); // Wrap the base shape with RotatedTranslatedShape - auto rtsSettings = new JPH::RotatedTranslatedShapeSettings(localPos, localRot, baseShape); + JPH::Ref rtsSettings = new JPH::RotatedTranslatedShapeSettings(localPos, localRot, baseShape); auto rtsShape = rtsSettings->Create().Get(); // Store in child entry @@ -103,7 +103,7 @@ void JoltCollision::addSphere(F32 radius, const MatrixF& localXfm) JPH::Quat localRot; toJolt(localXfm, localPos, localRot); - auto rtsSettings = new JPH::RotatedTranslatedShapeSettings(localPos, localRot, baseShape); + JPH::Ref rtsSettings = new JPH::RotatedTranslatedShapeSettings(localPos, localRot, baseShape); auto rtsShape = rtsSettings->Create().Get(); ChildShapeEntry entry; @@ -132,7 +132,7 @@ void JoltCollision::addCapsule(F32 radius, F32 height, const MatrixF& localXfm) JPH::Quat localRot; toJolt(localXfm, localPos, localRot); - auto rtsSettings = new JPH::RotatedTranslatedShapeSettings(localPos, localRot, baseShape); + JPH::Ref rtsSettings = new JPH::RotatedTranslatedShapeSettings(localPos, localRot, baseShape); auto rtsShape = rtsSettings->Create().Get(); ChildShapeEntry entry; @@ -150,33 +150,34 @@ bool JoltCollision::addConvex(const Point3F* points, U32 count, const MatrixF& l if (count == 0) return false; + // Pre-transform points into shape-local space for the same reason as + // addTriangleMesh: avoids the RTS wrapper and the double-transform in + // rebuildCompound when multiple shapes share a JoltCollision. + const bool isIdentity = localXfm.isIdentity(); + std::vector verts; verts.reserve(count); for (U32 i = 0; i < count; ++i) - verts.emplace_back(points[i].x, points[i].y, points[i].z); + { + Point3F p = points[i]; + if (!isIdentity) + localXfm.mulP(points[i], &p); + verts.emplace_back(p.x, p.y, p.z); + } - JPH::ConvexHullShapeSettings settings(verts.data(), verts.size()); + JPH::ConvexHullShapeSettings settings(verts.data(), (int)verts.size()); auto result = settings.Create(); if (result.HasError()) { - Con::errorf("Jolt Error: %s", result.GetError().c_str()); + Con::errorf("Jolt addConvex Error: %s", result.GetError().c_str()); return false; } - auto baseShape = result.Get(); - - JPH::Vec3 localPos; - JPH::Quat localRot; - toJolt(localXfm, localPos, localRot); - - auto rtsSettings = new JPH::RotatedTranslatedShapeSettings(localPos, localRot, baseShape); - auto rtsShape = rtsSettings->Create().Get(); - ChildShapeEntry entry; - entry.shape = rtsShape; - entry.localPos = localPos; - entry.localRot = localRot; - entry.localXfm = joltCast(localXfm); + entry.shape = result.Get(); + entry.localPos = JPH::Vec3::sZero(); + entry.localRot = JPH::Quat::sIdentity(); + entry.localXfm = JPH::Mat44::sIdentity(); mChildren.push_back(entry); rebuildCompound(); @@ -188,50 +189,53 @@ bool JoltCollision::addTriangleMesh(const Point3F* vert, U32 vertCount, const U3 if (!vert || !index || vertCount == 0 || triCount == 0) return false; - // Build the TriangleList + // Bake localXfm directly into the vertex positions so the MeshShape sits in + // shape-local space with an identity transform. This avoids the need for an + // RTS wrapper and eliminates the double-transform bug that occurs when the + // wrapper's position is later re-applied by rebuildCompound's AddShape call. + const bool isIdentity = localXfm.isIdentity(); + JPH::TriangleList triangles; triangles.reserve(triCount); for (U32 i = 0; i < triCount; ++i) { - const Point3F& v0 = vert[index[i * 3 + 0]]; - const Point3F& v1 = vert[index[i * 3 + 1]]; - const Point3F& v2 = vert[index[i * 3 + 2]]; + Point3F p0 = vert[index[i * 3 + 0]]; + Point3F p1 = vert[index[i * 3 + 1]]; + Point3F p2 = vert[index[i * 3 + 2]]; + + if (!isIdentity) + { + localXfm.mulP(vert[index[i * 3 + 0]], &p0); + localXfm.mulP(vert[index[i * 3 + 1]], &p1); + localXfm.mulP(vert[index[i * 3 + 2]], &p2); + } triangles.push_back( JPH::Triangle( - JPH::Float3(v0.x, v0.y, v0.z), - JPH::Float3(v2.x, v2.y, v2.z), - JPH::Float3(v1.x, v1.y, v1.z), + JPH::Float3(p0.x, p0.y, p0.z), + JPH::Float3(p2.x, p2.y, p2.z), // winding order maintained + JPH::Float3(p1.x, p1.y, p1.z), 0, // material index i // user data = original triangle index ) ); } - // Create the MeshShape JPH::MeshShapeSettings settings(triangles); auto result = settings.Create(); if (result.HasError()) { - Con::errorf("Jolt Error: %s", result.GetError().c_str()); + Con::errorf("Jolt addTriangleMesh Error: %s", result.GetError().c_str()); return false; } - auto baseShape = result.Get(); - - JPH::Vec3 localPos; - JPH::Quat localRot; - toJolt(localXfm, localPos, localRot); - - auto rtsSettings = new JPH::RotatedTranslatedShapeSettings(localPos, localRot, baseShape); - auto rtsShape = rtsSettings->Create().Get(); - + // Store at identity — vertices are already in shape-local space. ChildShapeEntry entry; - entry.shape = rtsShape; - entry.localPos = localPos; - entry.localRot = localRot; - entry.localXfm = joltCast(localXfm); + entry.shape = result.Get(); + entry.localPos = JPH::Vec3::sZero(); + entry.localRot = JPH::Quat::sIdentity(); + entry.localXfm = JPH::Mat44::sIdentity(); mChildren.push_back(entry); rebuildCompound(); @@ -248,49 +252,56 @@ bool JoltCollision::addHeightfield( if (!heightData || blockSize == 0) return false; - const F32 heightScale = 0.03125f; - const F32 minHeight = 0; - const F32 maxHeight = 65535 * heightScale; + // Jolt's internal BVH page size: power-of-2 between 2 and 8, independent of + // inSampleCount. Use 4 for larger terrains to produce a shallower BVH tree. + const U32 joltBlockSize = (blockSize >= 512) ? 4 : 2; - const U32 sampleCount = blockSize * blockSize; - std::vector samples(sampleCount); + // inSampleCount must be a multiple of joltBlockSize. Round up so any blockSize + // works — padding columns/rows are edge-clamped to stay physically correct. + const U32 paddedSize = ((blockSize + joltBlockSize - 1) / joltBlockSize) * joltBlockSize; + const U32 totalSamples = paddedSize * paddedSize; - for (U32 x = 0; x < blockSize; ++x) + // Pass 1: build a flat (un-flipped) padded grid with edge-clamping. + std::vector unflipped(totalSamples); + for (U32 y = 0; y < paddedSize; ++y) { - for (U32 y = 0; y < blockSize; ++y) + U32 srcY = (y < blockSize) ? y : blockSize - 1; + for (U32 x = 0; x < paddedSize; ++x) { - // Terrain storage (row-major) - U32 srcIdx = y * blockSize + x; - - // Flip Z direction for Jolt - U32 dstIdx = (blockSize - 1 - y) * blockSize + x; + U32 srcX = (x < blockSize) ? x : blockSize - 1; + U32 srcIdx = srcY * blockSize + srcX; float h = fixedToFloat(heightData[srcIdx]); - if (holes && holes[srcIdx]) h = JPH::HeightFieldShapeConstants::cNoCollisionValue; - samples[dstIdx] = h; + unflipped[y * paddedSize + x] = h; } } - float terrainSize = blockSize * metersPerSample; - float verticalAdjust = -heightScale * 0.5f; - JPH::Vec3 joltOffset( - 0.0, - verticalAdjust, - -terrainSize - ); + // Pass 2: flip Y axis into the final sample array for Jolt's coordinate system. + std::vector samples(totalSamples); + for (U32 y = 0; y < paddedSize; ++y) + for (U32 x = 0; x < paddedSize; ++x) + samples[(paddedSize - 1 - y) * paddedSize + x] = unflipped[y * paddedSize + x]; - JPH::Vec3 joltScale(metersPerSample,1.0f, metersPerSample); + // Offset uses actual terrain extent (blockSize) so the padded fringe never + // shifts the visible terrain. The vertical adjustment centres quantisation error. + const float heightScale = 0.03125f; + const float verticalAdjust = -heightScale * 0.5f; + const float terrainSize = blockSize * metersPerSample; - JPH::HeightFieldShapeSettings settings(samples.data(), joltOffset, joltScale, blockSize); - settings.mBlockSize = 2; + JPH::Vec3 joltOffset(0.0f, verticalAdjust, -terrainSize); + JPH::Vec3 joltScale(metersPerSample, 1.0f, metersPerSample); + + JPH::HeightFieldShapeSettings settings(samples.data(), joltOffset, joltScale, paddedSize); + settings.mBlockSize = joltBlockSize; auto result = settings.Create(); if (result.HasError()) { - Con::errorf("Jolt Error: %s", result.GetError().c_str()); + Con::errorf("Jolt addHeightfield Error (blockSize=%u paddedSize=%u): %s", + blockSize, paddedSize, result.GetError().c_str()); return false; } @@ -302,7 +313,7 @@ bool JoltCollision::addHeightfield( JPH::Quat rotFix = JPH::Quat::sRotation(JPH::Vec3::sAxisX(), JPH::DegreesToRadians(90.0f)); localRot = rotFix * localRot; - auto rtsSettings = new JPH::RotatedTranslatedShapeSettings(localPos, localRot, baseShape); + JPH::Ref rtsSettings = new JPH::RotatedTranslatedShapeSettings(localPos, localRot, baseShape); auto rtsShape = rtsSettings->Create().Get(); ChildShapeEntry entry; @@ -321,33 +332,35 @@ void JoltCollision::rebuildCompound() if (mChildren.empty()) return; - JPH::CompoundShapeSettings* compoundSettings = nullptr; - - if (mChildren.size() > 1) + if (mChildren.size() == 1) { - compoundSettings = new JPH::StaticCompoundShapeSettings(); + // Single shape: use it directly. For primitive shapes this is the + // RotatedTranslatedShape with the local transform baked in. For + // triangle meshes and convex hulls it is the base shape at identity. + mCompundShape = mChildren[0].shape; + return; } - for (auto& colShape : mChildren) + // Multiple shapes: build a static compound. Each child's ChildShapeEntry + // already has its local transform baked into entry.shape (either as an RTS + // wrapper for primitives, or pre-transformed vertices for meshes/convex). + // Using localPos/localRot here would apply the offset a SECOND time. + JPH::StaticCompoundShapeSettings compoundSettings; + for (const auto& child : mChildren) { - if (compoundSettings) - { - compoundSettings->AddShape( - colShape.localPos, - colShape.localRot, - colShape.shape - ); - } - else - { - mCompundShape = colShape.shape; - } + compoundSettings.AddShape( + JPH::Vec3::sZero(), + JPH::Quat::sIdentity(), + child.shape + ); } - if (compoundSettings) + auto result = compoundSettings.Create(); + if (result.HasError()) { - mCompundShape = compoundSettings->Create().Get(); - delete compoundSettings; + Con::errorf("Jolt rebuildCompound Error: %s", result.GetError().c_str()); + return; } + mCompundShape = result.Get(); } diff --git a/Engine/source/T3D/physics/jolt/joltPlayer.cpp b/Engine/source/T3D/physics/jolt/joltPlayer.cpp index fe46c5363..9836f70b9 100644 --- a/Engine/source/T3D/physics/jolt/joltPlayer.cpp +++ b/Engine/source/T3D/physics/jolt/joltPlayer.cpp @@ -37,9 +37,18 @@ JoltPlayer::JoltPlayer() JoltPlayer::~JoltPlayer() { - mCharacter->SetUserData(NULL); - mCharacter = NULL; - setSimulationEnabled(false); + if (mCharacter) + { + // Remove the inner body from the physics world before the character is released. + // Setting mCharacter=nullptr first would make disableCollision() a no-op. + if (mCollisionEnabled) + disableCollision(); + + mCharacter->SetUserData(0); + mCharacter = nullptr; + } + + mIsEnabled = false; } void JoltPlayer::setTransform(const MatrixF& xfm) @@ -99,7 +108,6 @@ void JoltPlayer::init(const char* type, const Point3F& size, F32 runSurfaceCos, JPH::Ref shape = JPH::RotatedTranslatedShapeSettings(JPH::Vec3(0, 0, mOriginOffset), rotFix, new JPH::CapsuleShape(0.5f * height, radius)).Create().Get(); JPH::Ref inner_shape = JPH::RotatedTranslatedShapeSettings(JPH::Vec3(0, 0, mOriginOffset), rotFix, new JPH::CapsuleShape(0.5f * 0.9f * height, 0.9f * radius)).Create().Get(); - JPH::Ref settings = new JPH::CharacterVirtualSettings(); Con::printf("JoltPlayer::init - maxSlopeAngle: %f degrees", mRadToDeg(mAcos(runSurfaceCos))); settings->mMaxSlopeAngle = mAcos(runSurfaceCos); @@ -139,20 +147,12 @@ void JoltPlayer::init(const char* type, const Point3F& size, F32 runSurfaceCos, setSimulationEnabled(true); } -class CharacterBodyFilter : public JPH::BodyFilter +void JoltPlayer::preUpdate(F32 dt) { -public: - JPH::BodyID mCharacterBodyID; - - CharacterBodyFilter(JPH::BodyID id) - : mCharacterBodyID(id) { - } - - virtual bool ShouldCollide(const JPH::BodyID& inBodyID) const override - { - return inBodyID != mCharacterBodyID; - } -}; + // Intentionally empty. Ground velocity is refreshed inside move() on every + // call. If the calling code ever starts calling preUpdate() before move(), + // the refresh in move() will simply be a no-op on the same-frame data. +} Point3F JoltPlayer::move(const VectorF& displacement, CollisionList& outCol) { @@ -166,30 +166,31 @@ Point3F JoltPlayer::move(const VectorF& displacement, CollisionList& outCol) JPH::Vec3 velocity = joltCast(displacement) / dt; const auto& system = mWorld->getPhysicsSystem(); - const JPH::Vec3 up = mCharacter->GetUp(); mCharacter->UpdateGroundVelocity(); const JPH::CharacterVirtual::EGroundState groundState = mCharacter->GetGroundState(); const bool isOnGround = groundState == JPH::CharacterVirtual::EGroundState::OnGround; - // Temporary - log what displacement.z actually is each frame - /*Con::printf("groundState=%d disp.z=%.4f vel.z=%.4f", - (int)groundState, - displacement.z, - displacement.z / TickSec);*/ - if (isOnGround && !mCharacter->IsSlopeTooSteep(mCharacter->GetGroundNormal())) { JPH::Vec3 horizontalVel(velocity.GetX(), velocity.GetY(), 0.0f); mAllowSliding = horizontalVel.Length() > 0.01f; - if (velocity.GetZ() < 0.05f) - velocity.SetZ(0.00f); + velocity.SetZ(0.0f); JPH::Vec3 groundVel = mCharacter->GetGroundVelocity(); - if (groundVel.LengthSq() > 0.001f) - velocity += groundVel; + // Threshold raised to filter static-surface floating-point residuals. + if (groundVel.LengthSq() > 0.01f) + { + // Always carry horizontal platform motion (conveyor belts, sliding floors). + velocity.SetX(velocity.GetX() + groundVel.GetX()); + velocity.SetY(velocity.GetY() + groundVel.GetY()); + // Only carry Z from platforms that are meaningfully moving upward (elevators). + // Static surfaces can produce tiny Z residuals that fight gravity resolution. + if (groundVel.GetZ() > 0.05f) + velocity.SetZ(velocity.GetZ() + groundVel.GetZ()); + } } else { @@ -199,28 +200,25 @@ Point3F JoltPlayer::move(const VectorF& displacement, CollisionList& outCol) mCharacter->SetLinearVelocity(velocity); //---------------------------------------- - // Substep (bullet was doing a max of 10 iterations) + // Substep //---------------------------------------- const U32 iterations = 3; const F32 subDt = dt / iterations; - JPH::CharacterVirtual::ExtendedUpdateSettings settings; + // Build settings once outside the loop — they don't change per-substep. + JPH::CharacterVirtual::ExtendedUpdateSettings extSettings; + extSettings.mWalkStairsStepUp = JPH::Vec3(0, 0, mStepHeight); + extSettings.mStickToFloorStepDown = JPH::Vec3(0, 0, -mStepHeight); + extSettings.mWalkStairsCosAngleForwardContact = mMaxSlopeCos; - for (int i = 0; i < iterations; ++i) + for (U32 i = 0; i < iterations; ++i) { - bool isLast = (i == iterations - 1); - - settings.mWalkStairsStepUp = JPH::Vec3(0, 0, mStepHeight); - settings.mStickToFloorStepDown = JPH::Vec3(0, 0, -mStepHeight); - settings.mWalkStairsCosAngleForwardContact = mAcos(mMaxSlopeCos); - - mCharacter->ExtendedUpdate( subDt, system->GetGravity(), - settings, - system->GetDefaultBroadPhaseLayerFilter(Layers::MOVING), - system->GetDefaultLayerFilter(Layers::MOVING), + extSettings, + system->GetDefaultBroadPhaseLayerFilter(Layers::CHARACTER), + system->GetDefaultLayerFilter(Layers::CHARACTER), {}, {}, *mWorld->getTempAllocator() @@ -411,8 +409,8 @@ void JoltPlayer::setSpacials(const Point3F& nPos, const Point3F& nSize) if (mCharacter->SetShape( shape, maxPenetration, - system->GetDefaultBroadPhaseLayerFilter(Layers::MOVING), - system->GetDefaultLayerFilter(Layers::MOVING), + system->GetDefaultBroadPhaseLayerFilter(Layers::CHARACTER), + system->GetDefaultLayerFilter(Layers::CHARACTER), body_filter, shape_filter, *mWorld->getTempAllocator()) @@ -420,8 +418,6 @@ void JoltPlayer::setSpacials(const Point3F& nPos, const Point3F& nSize) { mCharacter->SetInnerBodyShape(inner_shape); } - - //mCharacter->SetPosition(joltCast(nPos)); } void JoltPlayer::enableCollision() @@ -480,26 +476,51 @@ void JoltPlayer::OnAdjustBodyVelocity(const JPH::CharacterVirtual* inCharacter, { } +bool JoltPlayer::OnContactValidate(const JPH::CharacterVirtual* inCharacter, const JPH::BodyID& inBodyID2, const JPH::SubShapeID& inSubShapeID2) +{ + if (inCharacter != mCharacter) + return true; + + JPH::BodyLockRead lock(mWorld->getPhysicsSystem()->GetBodyLockInterface(), inBodyID2); + if (lock.Succeeded() && lock.GetBody().IsSensor()) + return false; + + return true; +} + void JoltPlayer::OnContactAdded(const JPH::CharacterVirtual* inCharacter, const JPH::BodyID& inBodyID2, const JPH::SubShapeID& inSubShapeID2, JPH::RVec3Arg inContactPosition, JPH::Vec3Arg inContactNormal, JPH::CharacterContactSettings& ioSettings) { - if (inCharacter == mCharacter) - { - JPH::CharacterVirtual::ContactKey c(inBodyID2, inSubShapeID2); - if (std::find(mActiveContacts.begin(), mActiveContacts.end(), c) != mActiveContacts.end()) - AssertFatal(false, "JoltPlayer::OnContactAdded - Received a contact that should have been persisted"); + if (inCharacter != mCharacter) + return; - mActiveContacts.push_back(c); + // Sensors are already rejected by OnContactValidate so this callback will + // never fire for them. No sensor check needed here. + JPH::CharacterVirtual::ContactKey c(inBodyID2, inSubShapeID2); + if (std::find(mActiveContacts.begin(), mActiveContacts.end(), c) != mActiveContacts.end()) + { + Con::warnf("JoltPlayer::OnContactAdded - contact already present, skipping duplicate"); + return; } + mActiveContacts.push_back(c); + + OnContactCommon(inCharacter, inBodyID2, inSubShapeID2, inContactPosition, inContactNormal, ioSettings); } void JoltPlayer::OnContactPersisted(const JPH::CharacterVirtual* inCharacter, const JPH::BodyID& inBodyID2, const JPH::SubShapeID& inSubShapeID2, JPH::RVec3Arg inContactPosition, JPH::Vec3Arg inContactNormal, JPH::CharacterContactSettings& ioSettings) { - if (inCharacter == mCharacter) + if (inCharacter != mCharacter) + return; + + // Sensors are already rejected by OnContactValidate — this callback will + // not fire for them. + JPH::CharacterVirtual::ContactKey c(inBodyID2, inSubShapeID2); + if (std::find(mActiveContacts.begin(), mActiveContacts.end(), c) == mActiveContacts.end()) { - JPH::CharacterVirtual::ContactKey c(inBodyID2, inSubShapeID2); - if (std::find(mActiveContacts.begin(), mActiveContacts.end(), c) == mActiveContacts.end()) - AssertFatal(false, "JoltPlayer::OnContactPersisted - Received a contact that should have been added instead"); + Con::warnf("JoltPlayer::OnContactPersisted - contact not in set, inserting as new"); + mActiveContacts.push_back(c); } + + OnContactCommon(inCharacter, inBodyID2, inSubShapeID2, inContactPosition, inContactNormal, ioSettings); } void JoltPlayer::OnContactRemoved(const JPH::CharacterVirtual* inCharacter, const JPH::BodyID& inBodyID2, const JPH::SubShapeID& inSubShapeID2) @@ -509,8 +530,11 @@ void JoltPlayer::OnContactRemoved(const JPH::CharacterVirtual* inCharacter, cons JPH::CharacterVirtual::ContactKey c(inBodyID2, inSubShapeID2); ContactSet::iterator it = std::find(mActiveContacts.begin(), mActiveContacts.end(), c); if (it == mActiveContacts.end()) - AssertFatal(false, "JoltPlayer::OnContactRemoved - Attempted to remove a contact that was not added"); - + { + // Can occur when the contact was never added due to an earlier ordering anomaly. + Con::warnf("JoltPlayer::OnContactRemoved - contact not found, ignoring"); + return; + } mActiveContacts.erase(it); } } @@ -537,4 +561,3 @@ void JoltPlayer::OnContactSolve(const JPH::CharacterVirtual* inCharacter, const if (!mAllowSliding && inContactVelocity.IsNearZero() && !inCharacter->IsSlopeTooSteep(inContactNormal)) ioNewCharacterVelocity = JPH::Vec3::sZero(); } - diff --git a/Engine/source/T3D/physics/jolt/joltPlayer.h b/Engine/source/T3D/physics/jolt/joltPlayer.h index b95450ea8..6512a21c1 100644 --- a/Engine/source/T3D/physics/jolt/joltPlayer.h +++ b/Engine/source/T3D/physics/jolt/joltPlayer.h @@ -56,11 +56,11 @@ public: // Physics Player. void init(const char* type, - const Point3F& size, - F32 runSurfaceCos, - F32 stepHeight, - SceneObject* obj, - PhysicsWorld* world) override; + const Point3F& size, + F32 runSurfaceCos, + F32 stepHeight, + SceneObject* obj, + PhysicsWorld* world) override; void preUpdate(F32 dt); @@ -88,6 +88,10 @@ public: // contact listener /// Callback to adjust the velocity of a body as seen by the character. Can be adjusted to e.g. implement a conveyor belt or an inertial dampener system of a sci-fi space ship. virtual void OnAdjustBodyVelocity(const JPH::CharacterVirtual* inCharacter, const JPH::Body& inBody2, JPH::Vec3& ioLinearVelocity, JPH::Vec3& ioAngularVelocity) override; + // Called to check if the character can collide with a body at all. Return false to discard the contact + // before it reaches the constraint solver, stair walking, or stick-to-floor algorithms. + virtual bool OnContactValidate(const JPH::CharacterVirtual* inCharacter, const JPH::BodyID& inBodyID2, const JPH::SubShapeID& inSubShapeID2) override; + // Called whenever the character collides with a body. virtual void OnContactAdded(const JPH::CharacterVirtual* inCharacter, const JPH::BodyID& inBodyID2, const JPH::SubShapeID& inSubShapeID2, JPH::RVec3Arg inContactPosition, JPH::Vec3Arg inContactNormal, JPH::CharacterContactSettings& ioSettings) override; diff --git a/Engine/source/T3D/physics/jolt/joltWorld.cpp b/Engine/source/T3D/physics/jolt/joltWorld.cpp index e9adb1335..099cf8548 100644 --- a/Engine/source/T3D/physics/jolt/joltWorld.cpp +++ b/Engine/source/T3D/physics/jolt/joltWorld.cpp @@ -123,6 +123,15 @@ void JoltWorld::destroyWorld() mDestroyPending.store(true, std::memory_order_release); } +class SensorExcludeFilter final : public JPH::BodyFilter +{ +public: + bool ShouldCollideLocked(const JPH::Body& inBody) const override + { + return !inBody.IsSensor(); + } +}; + bool JoltWorld::castRay(const Point3F& startPnt, const Point3F& endPnt, RayInfo* ri, const Point3F& impulse) { JPH::Vec3 startV(startPnt.x, startPnt.y, startPnt.z); @@ -134,10 +143,12 @@ bool JoltWorld::castRay(const Point3F& startPnt, const Point3F& endPnt, RayInfo* return false; JPH::RRayCast ray(startV, dir); - JPH::RayCastResult result; - if (!mPhysicsSystem.GetNarrowPhaseQuery().CastRay(ray, result)) + // Exclude sensor bodies: they are non-solid trigger volumes and must not + // appear as solid hits to projectiles or other physics ray casts. + SensorExcludeFilter sensorFilter; + if (!mPhysicsSystem.GetNarrowPhaseQuery().CastRay(ray, result, {}, {}, sensorFilter)) return false; JPH::BodyLockRead lock( @@ -154,21 +165,23 @@ bool JoltWorld::castRay(const Point3F& startPnt, const Point3F& endPnt, RayInfo* JPH::RVec3 hitPos = ray.GetPointOnRay(result.mFraction); - JPH::Vec3 normal = body.GetWorldSpaceSurfaceNormal( result.mSubShapeID2, hitPos ); - - if (!body.GetUserData()) - return false; + JPH::Vec3 normal = body.GetWorldSpaceSurfaceNormal(result.mSubShapeID2, hitPos); if (ri) { ri->point.set(joltCast(hitPos)); ri->normal.set(joltCast(normal)); - ri->distance = result.mFraction * rayLength; - // SceneObject mapping - PhysicsUserData* userData = PhysicsUserData::cast((void*)body.GetUserData()); - ri->object = userData->getObject(); - ri->material = NULL; + + // A body with no UserData means no associated SceneObject (e.g. internal + // collision geometry). Return the hit but leave ri->object as null rather + // than hiding the hit entirely. + PhysicsUserData* userData = body.GetUserData() + ? PhysicsUserData::cast((void*)body.GetUserData()) + : nullptr; + + ri->object = userData ? userData->getObject() : nullptr; + ri->material = nullptr; ri->face = 0; ri->faceDot = 0; } @@ -236,8 +249,9 @@ void JoltWorld::explosion(const Point3F& pos, F32 radius, F32 forceMagnitude) JPH::Body& body = lock.GetBody(); - // Ignore static/kinematic bodies - if (!body.IsDynamic()) + // Skip sensors and non-dynamic bodies — sensors are trigger volumes and + // static/kinematic bodies don't respond to impulses. + if (body.IsSensor() || !body.IsDynamic()) continue; JPH::Vec3 bodyPos = body.GetCenterOfMassPosition();