This commit is contained in:
marauder2k7 2026-06-18 22:43:31 +00:00 committed by GitHub
commit 9028b355e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1138 changed files with 216458 additions and 155 deletions

View file

@ -79,7 +79,7 @@ torqueAddSourceDirectories("platform/nativeDialogs")
torqueAddSourceDirectories( "T3D" "T3D/AI" "T3D/assets" "T3D/decal" "T3D/examples" "T3D/fps" "T3D/fx"
"T3D/gameBase" "T3D/gameBase/std"
"T3D/lighting"
"T3D/physics"
"T3D/physics" "T3D/physics/jolt"
# "T3D/components" "T3D/sceneComponent" "T3D/systems" "T3D/gameOBjects"
"T3D/sfx" "T3D/turret" "T3D/vehicles")

View file

@ -0,0 +1,384 @@
#include "platform/platform.h"
#include "T3D/physics/jolt/joltBody.h"
#include "math/mBox.h"
#include "console/console.h"
#include "scene/sceneObject.h"
JoltBody::JoltBody()
:mBody(NULL),
mWorld(NULL),
mMass(0.0f),
mCenterOfMass(NULL),
mInvCenterOfMass(NULL),
mIsDynamic(false),
mIsEnabled(false)
{
}
JoltBody::~JoltBody()
{
SAFE_DELETE(mCenterOfMass);
SAFE_DELETE(mInvCenterOfMass);
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 = nullptr;
}
PhysicsWorld* JoltBody::getWorld()
{
return mWorld;
}
PhysicsCollision* JoltBody::getColShape()
{
return mColShape;
}
bool JoltBody::init(PhysicsCollision* shape, F32 mass, U32 bodyFlags, SceneObject* obj, PhysicsWorld* world)
{
if (!shape || !world)
return false;
mWorld = dynamic_cast<JoltWorld*>(world);
if (!mWorld)
return false;
mColShape = dynamic_cast<JoltCollision*>(shape);
if (!mColShape)
return false;
mIsDynamic = false;
const JPH::Shape* joltShape = mColShape->getJoltShape();
// Determine motion type
JPH::EMotionType motionType = JPH::EMotionType::Static;
JPH::ObjectLayer layer = Layers::NON_MOVING;
if (mass > 0.0f)
{
motionType = JPH::EMotionType::Dynamic;
layer = Layers::MOVING;
}
else if (bodyFlags & BF_KINEMATIC)
{
motionType = JPH::EMotionType::Kinematic;
layer = Layers::MOVING;
}
mMass = mass;
MatrixF objXfm = obj->getTransform();
QuatF angPos(objXfm);
Point3F pos;
objXfm.getColumn(3, &pos);
JPH::BodyCreationSettings settings(
joltShape,
joltCast(pos), // initial position
joltCast(angPos), // rotation
motionType,
layer
);
// Mass override if dynamic
if (motionType == JPH::EMotionType::Dynamic)
{
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.
mIsDynamic = true;
}
JPH::BodyInterface& bi = mWorld->getPhysicsSystem()->GetBodyInterface();
JPH::Body* body = bi.CreateBody(settings);
bi.AddBody(body->GetID(), JPH::EActivation::Activate);
if (bodyFlags & BF_TRIGGER)
{
body->SetIsSensor(true);
}
mBody = body;
getUserData().setObject(obj);
getUserData().setBody(this);
mBody->SetUserData(reinterpret_cast<U64>(&mUserData));
const JPH::Mat44 trans = mBody->GetCenterOfMassTransform();
MatrixF comMat;
QuatF qang = joltCast(trans.GetQuaternion());
qang.setMatrix(&comMat);
comMat.setPosition(joltCast(trans.GetTranslation()));
mCenterOfMass = new MatrixF(comMat);
mInvCenterOfMass = new MatrixF(*mCenterOfMass);
mInvCenterOfMass->inverse();
return true;
}
void JoltBody::setTransform(const MatrixF& xfm)
{
if (!isValid())
return;
Point3F pos = xfm.getPosition();
QuatF q(xfm);
JPH::BodyInterface& bi = mWorld->getPhysicsSystem()->GetBodyInterface();
bi.SetPositionAndRotation(
mBody->GetID(),
joltCast(pos),
joltCast(q),
mIsEnabled ? JPH::EActivation::Activate : JPH::EActivation::DontActivate
);
if (isDynamic())
{
// 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());
}
}
void JoltBody::applyCorrection(const MatrixF& xfm)
{
setTransform(xfm);
}
MatrixF& JoltBody::getTransform(MatrixF* outMatrix)
{
const JPH::Mat44 trans = mBody->GetWorldTransform();
*outMatrix = joltCast(trans);
return *outMatrix;
}
void JoltBody::applyImpulse(const Point3F& origin, const Point3F& force)
{
if (!isValid())
return;
JPH::BodyInterface& bi = mWorld->getPhysicsSystem()->GetBodyInterface();
bi.AddImpulse(mBody->GetID(), joltCast(force), joltCast(origin));
}
void JoltBody::applyTorque(const Point3F& torque)
{
if (!isValid())
return;
JPH::BodyInterface& bi = mWorld->getPhysicsSystem()->GetBodyInterface();
bi.AddTorque(mBody->GetID(), joltCast(torque));
}
void JoltBody::applyForce(const Point3F& force)
{
if (!isValid())
return;
JPH::BodyInterface& bi = mWorld->getPhysicsSystem()->GetBodyInterface();
bi.AddForce(mBody->GetID(), joltCast(force));
}
void JoltBody::moveKinematicTo(const MatrixF& xfm)
{
Point3F pos = xfm.getPosition();
QuatF angPos(xfm);
mWorld->getPhysicsSystem()->GetBodyInterface().MoveKinematic(
mBody->GetID(),
joltCast(pos),
joltCast(angPos),
TickSec
);
}
bool JoltBody::isValid()
{
return mBody != nullptr && mWorld != NULL;
}
Box3F JoltBody::getWorldBounds()
{
JPH::AABox box = mBody->GetWorldSpaceBounds();
return Box3F(
Point3F(joltCast(box.mMin)),
Point3F(joltCast(box.mMax))
);
}
void JoltBody::setSimulationEnabled(bool enabled)
{
if (!isValid())
return;
JPH::BodyInterface& bi = mWorld->getPhysicsSystem()->GetBodyInterface();
if (enabled)
{
if (bi.IsAdded(mBody->GetID()))
{
bi.ActivateBody(mBody->GetID());
}
else
{
bi.AddBody(mBody->GetID(), JPH::EActivation::Activate);
}
}
else
{
bi.DeactivateBody(mBody->GetID());
bi.RemoveBody(mBody->GetID());
}
mIsEnabled = enabled;
}
void JoltBody::setSleepThreshold(F32 linear, F32 angular)
{
if (!mBody->IsDynamic())
return;
JPH::MotionProperties* mp = mBody->GetMotionProperties();
/* mp->SetLinearSleepThreshold(linear);
mp->SetAngularSleepThreshold(angular);*/
}
void JoltBody::setDamping(F32 linear, F32 angular)
{
if (!mBody->IsDynamic())
return;
JPH::MotionProperties* mp = mBody->GetMotionProperties();
mp->SetLinearDamping(linear);
mp->SetAngularDamping(angular);
}
void JoltBody::setSleeping(bool sleeping)
{
if (!isValid())
return;
if (!mIsEnabled)
return;
JPH::BodyInterface& bi = mWorld->getPhysicsSystem()->GetBodyInterface();
if (sleeping)
bi.DeactivateBody(mBody->GetID());
else
bi.ActivateBody(mBody->GetID());
}
void JoltBody::setMaterial(F32 restitution, F32 friction, F32 staticFriction)
{
mBody->SetRestitution(restitution);
mBody->SetFriction(friction);
}
void JoltBody::getState(PhysicsState* outState)
{
if (!outState)
return;
const JPH::RMat44 worldXfm = mBody->GetWorldTransform();
outState->position = joltCast(worldXfm.GetTranslation());
outState->orientation = joltCast(mBody->GetRotation());
outState->linVelocity = getLinVelocity();
outState->angVelocity = getAngVelocity();
if (mBody->IsDynamic())
{
const JPH::MotionProperties* mp = mBody->GetMotionProperties();
// Linear momentum = m * v
F32 mass = mp->GetInverseMass() > 0.0f ? 1.0f / mp->GetInverseMass() : 0.0f;
outState->momentum = outState->linVelocity * mass;
outState->angularMomentum = outState->angVelocity;
}
else
{
outState->momentum.set(0, 0, 0);
outState->angularMomentum.set(0, 0, 0);
}
// Sleeping?
outState->sleeping = !mBody->IsActive();
}
Point3F JoltBody::getCMassPosition() const
{
JPH::RVec3 p = mBody->GetCenterOfMassPosition();
return Point3F(joltCast(p));
}
Point3F JoltBody::getLinVelocity() const
{
return joltCast(mBody->GetLinearVelocity());
}
Point3F JoltBody::getAngVelocity() const
{
return joltCast(mBody->GetAngularVelocity());
}
void JoltBody::setLinVelocity(const Point3F& vel)
{
if (!isValid())
return;
JPH::BodyInterface& bi = mWorld->getPhysicsSystem()->GetBodyInterface();
if (!bi.IsAdded(mBody->GetID()))
return;
bi.SetLinearVelocity(mBody->GetID(), joltCast(vel));
}
void JoltBody::setAngVelocity(const Point3F& vel)
{
if (!isValid())
return;
JPH::BodyInterface& bi = mWorld->getPhysicsSystem()->GetBodyInterface();
if (!bi.IsAdded(mBody->GetID()))
return;
bi.SetAngularVelocity(mBody->GetID(), joltCast(vel));
}
void JoltBody::findContact(SceneObject** contactObject,
VectorF* contactNormal,
Vector<SceneObject*>* outOverlapObjects) const
{
if (contactObject)
*contactObject = nullptr;
if (contactNormal)
*contactNormal = VectorF::Zero;
}

View file

@ -0,0 +1,94 @@
#ifndef _JOLTBODY_H_
#define _JOLTBODY_H_
#ifndef _T3D_PHYSICS_PHYSICSBODY_H_
#include "T3D/physics/physicsBody.h"
#endif
#ifndef _REFBASE_H_
#include "core/util/refBase.h"
#endif
#ifndef _MMATRIX_H_
#include "math/mMatrix.h"
#endif
#ifndef _T3D_JOLT_PLUGIN_H_
#include "T3D/physics/jolt/joltPlugin.h"
#endif
#ifndef _JOLTWORLD_H_
#include "T3D/physics/jolt/joltWorld.h"
#endif
#ifndef _JOLTCOLLISION_H_
#include "T3D/physics/jolt/joltCollision.h"
#endif
class JoltBody : public PhysicsBody
{
protected:
JPH::Body* mBody;
JoltWorld* mWorld;
JoltCollisionRef mColShape;
///
F32 mMass;
///
bool mIsDynamic;
/// Is the body participating in the physics simulation.
bool mIsEnabled;
/// The center of mass offset used if the graphical
/// transform is not at the mass center.
MatrixF* mCenterOfMass;
/// The inverse center of mass offset.
MatrixF* mInvCenterOfMass;
public:
JoltBody();
virtual ~JoltBody();
// PhysicsObject
virtual PhysicsWorld* getWorld();
virtual void setTransform(const MatrixF& xfm);
virtual MatrixF& getTransform(MatrixF* outMatrix);
virtual Box3F getWorldBounds();
virtual void setSimulationEnabled(bool enabled);
virtual bool isSimulationEnabled() { return mIsEnabled; }
// PhysicsBody
virtual bool init(PhysicsCollision* shape,
F32 mass,
U32 bodyFlags,
SceneObject* obj,
PhysicsWorld* world);
bool isDynamic() const override { return mIsDynamic; }
PhysicsCollision* getColShape() override;
void setSleepThreshold(F32 linear, F32 angular) override;
void setDamping(F32 linear, F32 angular) override;
void getState(PhysicsState* outState) override;
F32 getMass() const override { return mMass; }
Point3F getCMassPosition() const override;
void setLinVelocity(const Point3F& vel) override;
void setAngVelocity(const Point3F& vel) override;
Point3F getLinVelocity() const override;
Point3F getAngVelocity() const override;
void setSleeping(bool sleeping) override;
void setMaterial(F32 restitution,
F32 friction,
F32 staticFriction) override;
void applyCorrection(const MatrixF& xfm) override;
void applyImpulse(const Point3F& origin, const Point3F& force) override;
void applyTorque(const Point3F& torque) override;
void applyForce(const Point3F& force) override;
void findContact(SceneObject** contactObject, VectorF* contactNormal, Vector<SceneObject*>* outOverlapObjects) const override;
void moveKinematicTo(const MatrixF& xfm) override;
bool isValid() override;
};
#endif // !_JOLTBODY_H_

View file

@ -0,0 +1,370 @@
#include "T3D/physics/jolt/joltCollision.h"
#include "terrain/terrFile.h"
// Save and undefine the macro if it exists
#ifdef Offset
#pragma push_macro("Offset")
#undef Offset
#endif
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
#include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
#include <Jolt/Physics/Collision/Shape/TriangleShape.h>
#include <Jolt/Physics/Collision/Shape/HeightFieldShape.h>
#include <Jolt/Physics/Collision/Shape/PlaneShape.h>
#include <Jolt/Physics/Collision/Shape/CompoundShape.h>
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
#include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
#ifdef Offset
// Restore the original macro after includes
#pragma pop_macro("Offset")
#endif
JoltCollision::JoltCollision()
{
VECTOR_SET_ASSOCIATION(mChildren);
}
JoltCollision::~JoltCollision()
{
}
void JoltCollision::addPlane(const PlaneF& plane)
{
JPH::Plane joltPlane({ plane.x, plane.y, plane.z }, plane.d);
JPH::PlaneShapeSettings planeSettings(joltPlane);
auto result = planeSettings.Create();
if (result.HasError())
{
Con::errorf("Jolt Error: %s", result.GetError().c_str());
return;
}
ChildShapeEntry entry;
entry.shape = result.Get();
entry.localXfm = JPH::Mat44::sIdentity(); // Plane at origin by default
entry.localPos = JPH::Vec3::sZero();
entry.localRot = JPH::Quat::sIdentity();
mChildren.push_back(entry);
rebuildCompound();
}
void JoltCollision::addBox(const Point3F& halfWidth, const MatrixF& localXfm)
{
JPH::BoxShapeSettings boxSettings(JPH::Vec3(halfWidth.x, halfWidth.y, halfWidth.z));
boxSettings.mConvexRadius = 0.01f;
auto result = boxSettings.Create();
if (result.HasError())
{
Con::errorf("Jolt Error: %s", result.GetError().c_str());
return;
}
auto baseShape = result.Get();
// Convert engine matrix to Jolt position + rotation
JPH::Vec3 localPos;
JPH::Quat localRot;
toJolt(localXfm, localPos, localRot);
// Wrap the base shape with RotatedTranslatedShape
JPH::Ref<JPH::RotatedTranslatedShapeSettings> rtsSettings = new JPH::RotatedTranslatedShapeSettings(localPos, localRot, baseShape);
auto rtsShape = rtsSettings->Create().Get();
// Store in child entry
ChildShapeEntry entry;
entry.shape = rtsShape;
entry.localPos = localPos;
entry.localRot = localRot;
entry.localXfm = joltCast(localXfm);
mChildren.push_back(entry);
rebuildCompound();
}
void JoltCollision::addSphere(F32 radius, const MatrixF& localXfm)
{
JPH::SphereShapeSettings settings(radius);
auto result = settings.Create();
if (result.HasError())
{
Con::errorf("Jolt Error: %s", result.GetError().c_str());
return;
}
auto baseShape = result.Get();
JPH::Vec3 localPos;
JPH::Quat localRot;
toJolt(localXfm, localPos, localRot);
JPH::Ref<JPH::RotatedTranslatedShapeSettings> 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);
mChildren.push_back(entry);
rebuildCompound();
}
void JoltCollision::addCapsule(F32 radius, F32 height, const MatrixF& localXfm)
{
JPH::CapsuleShapeSettings settings(radius, height);
auto result = settings.Create();
if (result.HasError())
{
Con::errorf("Jolt Error: %s", result.GetError().c_str());
return;
}
auto baseShape = result.Get();
JPH::Vec3 localPos;
JPH::Quat localRot;
toJolt(localXfm, localPos, localRot);
JPH::Ref<JPH::RotatedTranslatedShapeSettings> 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);
mChildren.push_back(entry);
rebuildCompound();
}
bool JoltCollision::addConvex(const Point3F* points, U32 count, const MatrixF& localXfm)
{
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<JPH::Vec3> verts;
verts.reserve(count);
for (U32 i = 0; i < count; ++i)
{
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(), (int)verts.size());
auto result = settings.Create();
if (result.HasError())
{
Con::errorf("Jolt addConvex Error: %s", result.GetError().c_str());
return false;
}
ChildShapeEntry entry;
entry.shape = result.Get();
entry.localPos = JPH::Vec3::sZero();
entry.localRot = JPH::Quat::sIdentity();
entry.localXfm = JPH::Mat44::sIdentity();
mChildren.push_back(entry);
rebuildCompound();
return true;
}
bool JoltCollision::addTriangleMesh(const Point3F* vert, U32 vertCount, const U32* index, U32 triCount, const MatrixF& localXfm)
{
if (!vert || !index || vertCount == 0 || triCount == 0)
return false;
// 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)
{
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(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
)
);
}
JPH::MeshShapeSettings settings(triangles);
auto result = settings.Create();
if (result.HasError())
{
Con::errorf("Jolt addTriangleMesh Error: %s", result.GetError().c_str());
return false;
}
// Store at identity — vertices are already in shape-local space.
ChildShapeEntry entry;
entry.shape = result.Get();
entry.localPos = JPH::Vec3::sZero();
entry.localRot = JPH::Quat::sIdentity();
entry.localXfm = JPH::Mat44::sIdentity();
mChildren.push_back(entry);
rebuildCompound();
return true;
}
bool JoltCollision::addHeightfield(
const U16* heightData,
const bool* holes,
U32 blockSize,
F32 metersPerSample,
const MatrixF& localXfm) // We wont use localXfm directly
{
if (!heightData || blockSize == 0)
return false;
// 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;
// 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;
// Pass 1: build a flat (un-flipped) padded grid with edge-clamping.
std::vector<float> unflipped(totalSamples);
for (U32 y = 0; y < paddedSize; ++y)
{
U32 srcY = (y < blockSize) ? y : blockSize - 1;
for (U32 x = 0; x < paddedSize; ++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;
unflipped[y * paddedSize + x] = h;
}
}
// Pass 2: flip Y axis into the final sample array for Jolt's coordinate system.
std::vector<float> 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];
// 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::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 addHeightfield Error (blockSize=%u paddedSize=%u): %s",
blockSize, paddedSize, result.GetError().c_str());
return false;
}
auto baseShape = result.Get();
JPH::Vec3 localPos;
JPH::Quat localRot;
toJolt(localXfm, localPos, localRot);
JPH::Quat rotFix = JPH::Quat::sRotation(JPH::Vec3::sAxisX(), JPH::DegreesToRadians(90.0f));
localRot = rotFix * localRot;
JPH::Ref<JPH::RotatedTranslatedShapeSettings> rtsSettings = new JPH::RotatedTranslatedShapeSettings(localPos, localRot, baseShape);
auto rtsShape = rtsSettings->Create().Get();
ChildShapeEntry entry;
entry.shape = rtsShape;
entry.localPos = localPos;
entry.localRot = localRot;
mChildren.push_back(entry);
rebuildCompound();
return true;
}
void JoltCollision::rebuildCompound()
{
// clear
mCompundShape = nullptr;
if (mChildren.empty())
return;
if (mChildren.size() == 1)
{
// 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;
}
// 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)
{
compoundSettings.AddShape(
JPH::Vec3::sZero(),
JPH::Quat::sIdentity(),
child.shape
);
}
auto result = compoundSettings.Create();
if (result.HasError())
{
Con::errorf("Jolt rebuildCompound Error: %s", result.GetError().c_str());
return;
}
mCompundShape = result.Get();
}

View file

@ -0,0 +1,62 @@
#ifndef _JOLTCOLLISION_H_
#define _JOLTCOLLISION_H_
#ifndef _T3D_PHYSICS_PHYSICSCOLLISION_H_
#include "T3D/physics/physicsCollision.h"
#endif
#ifndef _T3D_JOLT_PLUGIN_H_
#include "T3D/physics/jolt/joltPlugin.h"
#endif
#ifndef _MMATRIX_H_
#include "math/mMatrix.h"
#endif
#ifndef _TVECTOR_H_
#include "core/util/tVector.h"
#endif
struct ChildShapeEntry
{
JPH::Ref<JPH::Shape> shape;
JPH::Mat44 localXfm;
JPH::Vec3 localPos;
JPH::Quat localRot;
};
class JoltCollision : public PhysicsCollision
{
private:
JPH::Ref<JPH::Shape> mCompundShape;
Vector<ChildShapeEntry> mChildren;
public:
JoltCollision();
virtual ~JoltCollision();
// Add basic primitives
void addPlane(const PlaneF& plane) override;
void addBox(const Point3F& halfWidth, const MatrixF& localXfm) override;
void addSphere(F32 radius, const MatrixF& localXfm) override;
void addCapsule(F32 radius, F32 height, const MatrixF& localXfm) override;
bool addConvex(const Point3F* points, U32 count, const MatrixF& localXfm) override;
bool addTriangleMesh(const Point3F* vert, U32 vertCount,
const U32* index, U32 triCount,
const MatrixF& localXfm) override;
bool addHeightfield(const U16* heights, const bool* holes, U32 blockSize, F32 metersPerSample, const MatrixF& localXfm) override;
void rebuildCompound();
JPH::Ref<JPH::Shape> getJoltShape() const { return mCompundShape; }
};
/// A strong pointer to a reference counted JoltCollision.
typedef StrongRefPtr<JoltCollision> JoltCollisionRef;
/// A weak pointer to a reference counted JoltCollision.
typedef WeakRefPtr<JoltCollision> JoltCollisionWeakRef;
#endif // !_JOLTCOLLISION_H_

View file

@ -0,0 +1,371 @@
#include "platform/platform.h"
#include "T3D/physics/jolt/joltPlayer.h"
#include "collision/collision.h"
#include "scene/sceneObject.h"
#include "T3D/player.h"
#ifdef Offset
#pragma push_macro("Offset")
#undef Offset
#endif
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
#include <Jolt/Physics/Collision/CollideShape.h>
#include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
#include <Jolt/Physics/Collision/BroadPhase/BroadPhaseLayer.h>
#ifdef Offset
#pragma pop_macro("Offset")
#endif
JoltPlayer::JoltPlayer()
: mCharacter(NULL),
mObject(NULL),
mWorld(NULL),
mMaxSlopeCos(0.0f),
mStepHeight(0.0f),
mOriginOffset(0.0f),
mAllowSliding(false),
mCollisionEnabled(true),
mIsEnabled(false)
{
mDesiredVelocity = JPH::Vec3::sZero();
}
JoltPlayer::~JoltPlayer()
{
if (mCharacter)
{
mWorld->getPhysicsSystem()->GetBodyInterface().SetUserData(
mCharacter->GetBodyID(), 0);
if (mCollisionEnabled)
disableCollision();
mCharacter = NULL;
}
mIsEnabled = false;
}
void JoltPlayer::setTransform(const MatrixF& xfm)
{
if (!mCharacter)
return;
const Point3F pos = xfm.getPosition();
const QuatF rot(xfm);
// Character::SetPosition / SetRotation go through BodyInterface internally.
mCharacter->SetPosition(joltCast(pos), JPH::EActivation::Activate);
mCharacter->SetRotation(joltCast(rot), JPH::EActivation::Activate);
}
MatrixF& JoltPlayer::getTransform(MatrixF* outMatrix)
{
*outMatrix = joltCast(mCharacter->GetWorldTransform());
return *outMatrix;
}
Box3F JoltPlayer::getWorldBounds()
{
const JPH::AABox box = mCharacter->GetShape()->GetWorldSpaceBounds(
mCharacter->GetWorldTransform(), JPH::Vec3::sOne());
return Box3F(Point3F(joltCast(box.mMin)), Point3F(joltCast(box.mMax)));
}
void JoltPlayer::setSimulationEnabled(bool enabled)
{
if (!mCharacter || !mWorld)
return;
mIsEnabled = enabled;
}
void JoltPlayer::init(const char* type, const Point3F& size, F32 runSurfaceCos,
F32 stepHeight, SceneObject* obj, PhysicsWorld* world)
{
if (!obj || !world)
return;
mObject = obj;
mWorld = dynamic_cast<JoltWorld*>(world);
if (!mWorld)
return;
mStepHeight = stepHeight;
mMaxSlopeCos = runSurfaceCos;
const F32 radius = mMax(size.x, size.y) * 0.5f;
F32 height = mMax(0.01f, size.z);
mOriginOffset = (0.5f * height);
// Capsule centred at mOriginOffset along Z so the bottom of the capsule
// sits at world Z = 0 (feet). rotFix converts Y-axis capsule → Z-up.
const JPH::Quat rotFix = JPH::Quat::sRotation(
JPH::Vec3::sAxisX(), JPH::DegreesToRadians(90.0f));
JPH::Ref<JPH::Shape> shape =
JPH::RotatedTranslatedShapeSettings(
JPH::Vec3(0.0f, 0.0f, mOriginOffset), rotFix,
new JPH::CapsuleShape((0.5f * height) - radius, radius))
.Create().Get();
JPH::Ref<JPH::CharacterSettings> settings = new JPH::CharacterSettings();
settings->mUp = JPH::Vec3::sAxisZ(); // Z-up world
settings->mMaxSlopeAngle = mAcos(runSurfaceCos);
settings->mShape = shape;
settings->mMass = obj->getMass();
settings->mFriction = 0.5f;
settings->mLayer = Layers::CHARACTER;
// Accept contacts that touch the hemisphere at the bottom of the capsule
settings->mSupportingVolume = JPH::Plane(JPH::Vec3::sAxisZ(), -radius);
// Torque's Player already applies gravity via the displacement passed to
// move(). Suppress Jolt's built-in gravity here so it is not applied twice.
settings->mGravityFactor = 0.0f;
Con::printf("JoltPlayer::init - maxSlopeAngle: %f degrees",
mRadToDeg(mAcos(runSurfaceCos)));
// --- Create Character ---
MatrixF objXfm = obj->getTransform();
QuatF angPos(objXfm);
Point3F pos;
objXfm.getColumn(3, &pos);
mCharacter = new JPH::Character(
settings,
joltCast(pos),
joltCast(angPos),
reinterpret_cast<U64>(&mUserData),
mWorld->getPhysicsSystem()
);
// Ensure Z is treated as up (may also be set via CharacterBaseSettings but
// calling SetUp explicitly guarantees it regardless of library version).
mCharacter->SetUp(JPH::Vec3::sAxisZ());
getUserData().setObject(obj);
mCharacter->AddToPhysicsSystem(JPH::EActivation::Activate);
setSimulationEnabled(true);
}
void JoltPlayer::preUpdate(F32 dt)
{
if (!mCharacter || !mIsEnabled)
return;
static const float cCollisionTolerance = 0.05f;
mCharacter->PostSimulation(cCollisionTolerance);
}
Point3F JoltPlayer::move(const VectorF& displacement, CollisionList& outCol)
{
if (!mCharacter || !mWorld)
return Point3F::Zero;
if (!mIsEnabled)
return joltCast(mCharacter->GetPosition());
// If preUpdate() was not called by the engine, call PostSimulation here.
// (In architectures where preUpdate IS called this is a no-op because
// PostSimulation is idempotent between physics steps.)
static const float cCollisionTolerance = 0.05f;
mCharacter->PostSimulation(cCollisionTolerance);
const F32 dt = TickSec;
JPH::Vec3 velocity = joltCast(displacement) / dt;
const JPH::Vec3 up = mCharacter->GetUp();
const JPH::Character::EGroundState groundState = mCharacter->GetGroundState();
const bool isOnGround =
groundState == JPH::Character::EGroundState::OnGround;
if (isOnGround && !mCharacter->IsSlopeTooSteep(mCharacter->GetGroundNormal()))
{
const JPH::Vec3 horizontalVel(velocity.GetX(), velocity.GetY(), 0.0f);
mAllowSliding = horizontalVel.Length() > 0.01f;
// Clamp small upward residuals on flat ground to prevent floating
if (velocity.GetZ() < 0.05f)
velocity.SetZ(0.0f);
// Carry platform / conveyor velocity
const JPH::Vec3 groundVel = mCharacter->GetGroundVelocity();
if (groundVel.LengthSq() > 0.01f)
{
velocity.SetX(velocity.GetX() + groundVel.GetX());
velocity.SetY(velocity.GetY() + groundVel.GetY());
// Only add upward elevator velocity, not static-surface residuals
if (groundVel.GetZ() > 0.05f)
velocity.SetZ(velocity.GetZ() + groundVel.GetZ());
}
}
else
{
mAllowSliding = true;
}
// Hand velocity to Jolt — the physics step (tickPhysics) will move the body.
// Unlike CharacterVirtual there is no ExtendedUpdate here; collision response
// and stair-stepping are resolved by Jolt's constraint solver.
mCharacter->SetLinearVelocity(velocity);
const bool activelyJumping = (velocity.GetZ() > 0.01f);
// -----------------------------------------------------------------
// Fill collision list — ground contact only
// (Non-ground contacts come through the physics ContactListener;
// sensor / trigger overlaps are found in findContact via broadphase)
// -----------------------------------------------------------------
if (isOnGround && !activelyJumping && outCol.getCount() < CollisionList::MaxCollisions)
{
const JPH::BodyID groundBodyID = mCharacter->GetGroundBodyID();
JPH::BodyLockRead lock(
mWorld->getPhysicsSystem()->GetBodyLockInterface(), groundBodyID);
if (lock.Succeeded())
{
const JPH::Body& body = lock.GetBody();
PhysicsUserData* ud = reinterpret_cast<PhysicsUserData*>(body.GetUserData());
Collision& col = outCol.increment();
col.point = joltCast(mCharacter->GetGroundPosition());
col.normal = joltCast(mCharacter->GetGroundNormal());
col.object = ud ? ud->getObject() : NULL;
col.material = NULL;
col.distance = 0.0f;
col.faceDot = mDot(col.normal, VectorF(0.f, 0.f, 1.f));
}
}
// Return the position from the last physics step. tickPhysics() will move
// the body further based on the velocity we just set; that result feeds into
// the next tick via GetPosition() here.
return joltCast(mCharacter->GetPosition());
}
void JoltPlayer::findContact(SceneObject** contactObject, VectorF* contactNormal,
Vector<SceneObject*>* outOverlapObjects) const
{
if (!mCharacter || !mIsEnabled)
return;
const auto& system = mWorld->getPhysicsSystem();
// --- Primary ground contact -----------------------------------------
// PostSimulation() already ran in preUpdate() and produced reliable ground
// data. Pull it directly rather than re-deriving the normal from
// CheckCollision's mPenetrationAxis.
if (mCharacter->GetGroundState() == JPH::Character::EGroundState::OnGround
&& !mCharacter->GetGroundBodyID().IsInvalid())
{
JPH::BodyLockRead lock(system->GetBodyLockInterface(),
mCharacter->GetGroundBodyID());
if (lock.Succeeded())
{
auto* ud = reinterpret_cast<PhysicsUserData*>(lock.GetBody().GetUserData());
if (ud)
{
*contactObject = ud->getObject();
*contactNormal = joltCast(mCharacter->GetGroundNormal());
}
}
}
// --- Overlap harvest for triggers / items ---------------------------
// CheckCollision tests the character's actual capsule (not an expanded
// AABB) and returns every body within the skin distance. We only need
// body references here — no normal math — so the collector is trivial.
// Sensor bodies (triggers) and physical bodies (items) are both included
// so that Player::findContact() can call potentialEnterObject() on them.
struct OverlapCollector : public JPH::CollideShapeCollector
{
OverlapCollector(const JPH::PhysicsSystem* inSystem,
Vector<SceneObject*>& inOverlaps)
: mSystem(inSystem), mOverlaps(inOverlaps) {
}
void AddHit(const JPH::CollideShapeResult& inResult) override
{
JPH::BodyLockRead lock(mSystem->GetBodyLockInterface(), inResult.mBodyID2);
if (!lock.Succeeded())
return;
auto* ud = reinterpret_cast<PhysicsUserData*>(
lock.GetBody().GetUserData());
if (!ud)
return;
if (auto* obj = ud->getObject(); obj && !mOverlaps.contains(obj))
mOverlaps.push_back(obj);
}
const JPH::PhysicsSystem* mSystem;
Vector<SceneObject*>& mOverlaps;
};
const JPH::RVec3 pos = mCharacter->GetPosition();
OverlapCollector collector(system, *outOverlapObjects);
mCharacter->CheckCollision(
pos,
mCharacter->GetRotation(),
JPH::Vec3::sZero(), // no movement hint needed — pure overlap harvest
0.1f, // 10 cm skin
mCharacter->GetShape(),
pos, // base offset for float precision near origin
collector
);
}
bool JoltPlayer::testSpacials(const Point3F& nPos, const Point3F& nSize) const
{
return true;
}
void JoltPlayer::setSpacials(const Point3F& nPos, const Point3F& nSize)
{
if (!mCharacter || !mWorld)
return;
const F32 radius = mMax(nSize.x, nSize.y) * 0.5f;
const F32 height = mMax(0.01f, nSize.z);
mOriginOffset = 0.5f * height;
const JPH::Quat rotFix = JPH::Quat::sRotation(
JPH::Vec3::sAxisX(), JPH::DegreesToRadians(90.0f));
JPH::Ref<JPH::Shape> shape =
JPH::RotatedTranslatedShapeSettings(
JPH::Vec3(0.0f, 0.0f, mOriginOffset), rotFix,
new JPH::CapsuleShape((0.5f * height) - radius, radius))
.Create().Get();
// Character::SetShape is simpler than CharacterVirtual::SetShape —
// no filter objects or TempAllocator required.
const float maxPenetration = 0.05f;
mCharacter->SetShape(shape, maxPenetration);
}
void JoltPlayer::enableCollision()
{
if (!mCharacter || !mWorld || mCollisionEnabled)
return;
mCollisionEnabled = true;
// Character IS a body — adding it back to the physics system re-enables it
mCharacter->AddToPhysicsSystem(JPH::EActivation::Activate);
}
void JoltPlayer::disableCollision()
{
if (!mCharacter || !mWorld || !mCollisionEnabled)
return;
mCollisionEnabled = false;
mCharacter->RemoveFromPhysicsSystem();
}

View file

@ -0,0 +1,69 @@
#ifndef _JOLTPLAYER_H_
#define _JOLTPLAYER_H_
#ifndef _T3D_PHYSICS_PHYSICSPLAYER_H_
#include "T3D/physics/physicsPlayer.h"
#endif
#ifndef _T3D_JOLT_PLUGIN_H_
#include "T3D/physics/jolt/joltPlugin.h"
#endif
#ifndef _JOLTWORLD_H_
#include "T3D/physics/jolt/joltWorld.h"
#endif
#ifdef Offset
#pragma push_macro("Offset")
#undef Offset
#endif
#include <Jolt/Physics/Character/Character.h>
#ifdef Offset
#pragma pop_macro("Offset")
#endif
class JoltPlayer : public PhysicsPlayer
{
protected:
JoltWorld* mWorld;
SceneObject* mObject;
JPH::Ref<JPH::Character> mCharacter;
JPH::Vec3 mDesiredVelocity;
F32 mMaxSlopeCos;
F32 mStepHeight;
F32 mOriginOffset;
bool mIsEnabled;
bool mAllowSliding;
bool mCollisionEnabled;
public:
JoltPlayer();
virtual ~JoltPlayer();
// PhysicsObject
PhysicsWorld* getWorld() override { return mWorld; }
void setTransform(const MatrixF& xfm) override;
MatrixF& getTransform(MatrixF* outMatrix) override;
Box3F getWorldBounds() override;
void setSimulationEnabled(bool enabled) override;
bool isSimulationEnabled() override { return mIsEnabled; }
// PhysicsPlayer
void init(const char* type, const Point3F& size, F32 runSurfaceCos,
F32 stepHeight, SceneObject* obj, PhysicsWorld* world) override;
void preUpdate(F32 dt);
Point3F move(const VectorF& displacement, CollisionList& outCol) override;
void findContact(SceneObject** contactObject, VectorF* contactNormal,
Vector<SceneObject*>* outOverlapObjects) const override;
bool testSpacials(const Point3F& nPos, const Point3F& nSize) const override;
void setSpacials(const Point3F& nPos, const Point3F& nSize) override;
void enableCollision() override;
void disableCollision() override;
JPH::Ref<JPH::Character> getCharacter() { return mCharacter; }
};
#endif // _JOLTPLAYER_H_

View file

@ -0,0 +1,303 @@
#include "platform/platform.h"
#include "T3D/physics/jolt/joltPlugin.h"
#include "T3D/physics/jolt/joltWorld.h"
#include "T3D/physics/jolt/joltCollision.h"
#include "T3D/physics/jolt/joltBody.h"
#include "T3D/physics/jolt/joltPlayer.h"
#include "T3D/gameBase/gameProcess.h"
#include "core/util/tNamedFactory.h"
#include "gfx/sim/debugDraw.h"
AFTER_MODULE_INIT(Sim)
{
NamedFactory<PhysicsPlugin>::add("JoltPhysics", &JoltPlugin::create);
}
JoltPlugin::JoltPlugin()
{
JPH::RegisterDefaultAllocator();
JPH::Factory::sInstance = new JPH::Factory();
JPH::RegisterTypes();
#ifdef JPH_DEBUG_RENDERER
JPH::DebugRenderer::sInstance = new JoltDebugRenderer();
#endif // TORQUE_TOOLS
}
JoltPlugin::~JoltPlugin()
{
delete JPH::Factory::sInstance;
#ifdef JPH_DEBUG_RENDERER
delete JPH::DebugRenderer::sInstance;
JPH::DebugRenderer::sInstance = nullptr;
#endif
}
void JoltPlugin::reset()
{
// First delete all the cleanup objects.
if (getPhysicsCleanup())
getPhysicsCleanup()->deleteAllObjects();
getPhysicsResetSignal().trigger(PhysicsResetEvent_Restore);
// Now let each world reset itself.
Map<StringNoCase, PhysicsWorld*>::Iterator iter = mPhysicsWorldLookup.begin();
for (; iter != mPhysicsWorldLookup.end(); iter++)
iter->value->reset();
}
PhysicsPlugin* JoltPlugin::create()
{
return new JoltPlugin();
}
void JoltPlugin::destroyPlugin()
{
// Cleanup any worlds that are still kicking.
Map<StringNoCase, PhysicsWorld*>::Iterator iter = mPhysicsWorldLookup.begin();
for (; iter != mPhysicsWorldLookup.end(); iter++)
{
iter->value->destroyWorld();
delete iter->value;
}
mPhysicsWorldLookup.clear();
delete this;
}
PhysicsCollision* JoltPlugin::createCollision()
{
return new JoltCollision();
}
PhysicsBody* JoltPlugin::createBody()
{
return new JoltBody();
}
PhysicsPlayer* JoltPlugin::createPlayer()
{
return new JoltPlayer();
}
bool JoltPlugin::isSimulationEnabled() const
{
bool ret = false;
JoltWorld* world = static_cast<JoltWorld*>(getWorld(smClientWorldName));
if (world)
{
ret = world->getEnabled();
return ret;
}
world = static_cast<JoltWorld*>(getWorld(smServerWorldName));
if (world)
{
ret = world->getEnabled();
return ret;
}
return ret;
}
void JoltPlugin::enableSimulation(const String& worldName, bool enable)
{
JoltWorld* world = static_cast<JoltWorld*>(getWorld(worldName));
if (world)
world->setEnabled(enable);
}
void JoltPlugin::setTimeScale(const F32 timeScale)
{
// Grab both the client and
// server worlds and set their time
// scales to the passed value.
JoltWorld* world = static_cast<JoltWorld*>(getWorld(smClientWorldName));
if (world)
world->setEditorTimeScale(timeScale);
world = static_cast<JoltWorld*>(getWorld(smServerWorldName));
if (world)
world->setEditorTimeScale(timeScale);
}
const F32 JoltPlugin::getTimeScale() const
{
// Grab both the client and
// server worlds and call
// setEnabled( true ) on them.
JoltWorld* world = static_cast<JoltWorld*>(getWorld(smClientWorldName));
if (!world)
{
world = static_cast<JoltWorld*>(getWorld(smServerWorldName));
if (!world)
return 0.0f;
}
return world->getEditorTimeScale();
}
bool JoltPlugin::createWorld(const String& worldName)
{
Map<StringNoCase, PhysicsWorld*>::Iterator iter = mPhysicsWorldLookup.find(worldName);
PhysicsWorld* world = NULL;
iter != mPhysicsWorldLookup.end() ? world = (*iter).value : world = NULL;
if (world)
{
Con::errorf("BtPlugin::createWorld - %s world already exists!", worldName.c_str());
return false;
}
world = new JoltWorld();
if (worldName.equal(smClientWorldName, String::NoCase))
world->initWorld(false, ClientProcessList::get());
else
world->initWorld(true, ServerProcessList::get());
mPhysicsWorldLookup.insert(worldName, world);
return world != NULL;
}
void JoltPlugin::destroyWorld(const String& worldName)
{
Map<StringNoCase, PhysicsWorld*>::Iterator iter = mPhysicsWorldLookup.find(worldName);
if (iter == mPhysicsWorldLookup.end())
return;
PhysicsWorld* world = (*iter).value;
world->destroyWorld();
mPhysicsWorldLookup.erase(iter);
}
PhysicsWorld* JoltPlugin::getWorld(const String& worldName) const
{
if (mPhysicsWorldLookup.isEmpty())
return NULL;
Map<StringNoCase, PhysicsWorld*>::ConstIterator iter = mPhysicsWorldLookup.find(worldName);
return iter != mPhysicsWorldLookup.end() ? (*iter).value : NULL;
}
PhysicsWorld* JoltPlugin::getWorld() const
{
if (mPhysicsWorldLookup.size() == 0)
return NULL;
Map<StringNoCase, PhysicsWorld*>::ConstIterator iter = mPhysicsWorldLookup.begin();
return iter->value;
}
U32 JoltPlugin::getWorldCount() const
{
return mPhysicsWorldLookup.size();
}
#ifdef JPH_DEBUG_RENDERER
JoltDebugRenderer::JoltDebugRenderer()
{
JPH::DebugRenderer::Initialize();
}
void JoltDebugRenderer::DrawLine(JPH::RVec3Arg inFrom, JPH::RVec3Arg inTo, JPH::ColorArg inColor)
{
DebugDrawer::get()->drawLine(joltCast(inFrom), joltCast(inTo), fromJolt(inColor));
}
void JoltDebugRenderer::DrawTriangle(JPH::RVec3Arg inV1, JPH::RVec3Arg inV2, JPH::RVec3Arg inV3, JPH::ColorArg inColor, ECastShadow inCastShadow)
{
DebugDrawer::get()->drawTri(joltCast(inV1), joltCast(inV2), joltCast(inV3), fromJolt(inColor));
}
JPH::DebugRenderer::Batch JoltDebugRenderer::CreateTriangleBatch(const Triangle* inTriangles, int inTriangleCount)
{
BatchImpl* batch = new BatchImpl;
if (inTriangles && inTriangleCount)
batch->mTriangles.assign(inTriangles, inTriangles + inTriangleCount);
return batch;
}
JPH::DebugRenderer::Batch JoltDebugRenderer::CreateTriangleBatch(const Vertex* inVertices, int inVertexCount, const JPH::uint32* inIndices, int inIndexCount)
{
BatchImpl* batch = new BatchImpl;
if (inVertices == nullptr || inVertexCount == 0 || inIndices == nullptr || inIndexCount == 0)
return batch;
// Convert indexed triangle list to triangle list
batch->mTriangles.resize(inIndexCount / 3);
for (size_t t = 0; t < batch->mTriangles.size(); ++t)
{
JPH::DebugRenderer::Triangle& triangle = batch->mTriangles[t];
triangle.mV[0] = inVertices[inIndices[t * 3 + 0]];
triangle.mV[1] = inVertices[inIndices[t * 3 + 1]];
triangle.mV[2] = inVertices[inIndices[t * 3 + 2]];
}
return batch;
}
void JoltDebugRenderer::DrawGeometry(JPH::RMat44Arg inModelMatrix, const JPH::AABox& inWorldSpaceBounds, float inLODScaleSq, JPH::ColorArg inModelColor, const GeometryRef& inGeometry, ECullMode inCullMode, ECastShadow inCastShadow, EDrawMode inDrawMode)
{
const auto& lod = inGeometry->GetLOD(
mFrameState.cameraPos,
inWorldSpaceBounds,
inLODScaleSq
);
const BatchImpl* batch = static_cast<const BatchImpl*>(lod.mTriangleBatch.GetPtr());
if (!batch)
return;
const U32 triCount = batch->mTriangles.size();
if (triCount == 0)
return;
if (!mFrameState.frustum.getBounds().isOverlapped(fromJolt(inWorldSpaceBounds)))
return;
// Start one batched primitive build
GFX->setupGenericShaders();
GFXStateBlockDesc desc;
desc.setZReadWrite(false);
GFX->setStateBlockByDesc(desc);
PrimBuild::begin(GFXLineList, triCount * 6);
for (const auto& tri : batch->mTriangles)
{
JPH::Vec3 v0 = inModelMatrix * JPH::Vec3(tri.mV[0].mPosition);
JPH::Vec3 v1 = inModelMatrix * JPH::Vec3(tri.mV[1].mPosition);
JPH::Vec3 v2 = inModelMatrix * JPH::Vec3(tri.mV[2].mPosition);
JPH::Color color = inModelColor * tri.mV[0].mColor;
PrimBuild::color(fromJolt(color));
// Edge 1
PrimBuild::vertex3f(v0.GetX(), v0.GetY(), v0.GetZ());
PrimBuild::vertex3f(v1.GetX(), v1.GetY(), v1.GetZ());
// Edge 2
PrimBuild::vertex3f(v1.GetX(), v1.GetY(), v1.GetZ());
PrimBuild::vertex3f(v2.GetX(), v2.GetY(), v2.GetZ());
// Edge 3
PrimBuild::vertex3f(v2.GetX(), v2.GetY(), v2.GetZ());
PrimBuild::vertex3f(v0.GetX(), v0.GetY(), v0.GetZ());
}
PrimBuild::end(true);
}
#endif

View file

@ -0,0 +1,211 @@
#ifndef _T3D_JOLT_PLUGIN_H_
#define _T3D_JOLT_PLUGIN_H_
#ifndef _T3D_PHYSICS_PHYSICSPLUGIN_H_
#include "T3D/physics/physicsPlugin.h"
#endif
#ifndef _SCENERENDERSTATE_H_
#include "scene/sceneRenderState.h"
#endif
#if defined(TORQUE_TOOLS) && defined(TORQUE_OS_WIN)
#ifndef JPH_DEBUG_RENDERER
#define JPH_DEBUG_RENDERER
#endif
#endif
// Save and undefine the macro if it exists
#ifdef Offset
#pragma push_macro("Offset")
#undef Offset
#endif
#include <Jolt/Jolt.h>
#include <Jolt/Physics/PhysicsSystem.h>
#include <Jolt/RegisterTypes.h>
#include <Jolt/Core/TempAllocator.h>
#include <Jolt/Core/StreamWrapper.h>
#include <Jolt/Physics/Collision/RayCast.h>
#include <Jolt/Physics/Collision/CastResult.h>
#ifdef JPH_DEBUG_RENDERER
#include <Jolt/Renderer/DebugRenderer.h>
#endif
#include <Jolt/Core/JobSystemThreadPool.h>
#include <Jolt/Physics/PhysicsSettings.h>
#include <Jolt/Physics/StateRecorderImpl.h>
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
#include <Jolt/Physics/Constraints/SixDOFConstraint.h>
#include <Jolt/Physics/Body/BodyActivationListener.h>
#ifdef Offset
// Restore the original macro after includes
#pragma pop_macro("Offset")
#endif
inline Point3F joltCast(const JPH::Vec3& vec)
{
return Point3F(vec.GetX(), vec.GetY(), vec.GetZ());
}
inline JPH::Vec3 joltCast(const Point3F& point)
{
return JPH::Vec3(point.x, point.y, point.z);
}
inline QuatF joltCast(const JPH::Quat& quat)
{
/// The Torque quat has the opposite winding order.
return QuatF(-quat.GetX(), -quat.GetY(), -quat.GetZ(), quat.GetW());
}
inline JPH::Quat joltCast(const QuatF& quat)
{
/// The Torque quat has the opposite winding order.
return JPH::Quat(-quat.x, -quat.y, -quat.z, quat.w);
}
inline JPH::RMat44 joltCast(const MatrixF& m)
{
QuatF ang(m);
return JPH::Mat44::sRotationTranslation(joltCast(ang), joltCast(m.getPosition()));
}
inline MatrixF joltCast(const JPH::Mat44& xfm)
{
MatrixF out;
QuatF ang = joltCast(xfm.GetQuaternion());
ang.setMatrix(&out);
out.setPosition(joltCast(xfm.GetTranslation()));
return out;
}
inline void toJolt(const MatrixF& mat, JPH::RVec3& pos, JPH::Quat& rot)
{
Point3F p;
p = mat.getPosition();
pos = joltCast(p);
QuatF q(mat);
rot = joltCast(q);
}
inline JPH::AABox toJolt(const Box3F& box)
{
return JPH::AABox(
joltCast(box.minExtents),
joltCast(box.maxExtents)
);
}
inline Box3F fromJolt(const JPH::AABox& box)
{
return Box3F(
joltCast(box.mMin),
joltCast(box.mMax)
);
}
#ifndef _COLOR_H_
#include "core/color.h"
#endif
inline JPH::Color toJolt(const ColorI& c)
{
return JPH::Color(c.red, c.green, c.blue, c.alpha);
}
inline ColorI fromJolt(const JPH::Color& c)
{
return ColorI(c.r, c.g, c.b, c.a);
}
#ifdef JPH_DEBUG_RENDERER
struct DebugRenderFrameState
{
Frustum frustum;
JPH::Vec3 cameraPos;
};
class JoltDebugRenderer final : public JPH::DebugRenderer
{
public:
JPH_OVERRIDE_NEW_DELETE
explicit JoltDebugRenderer();
virtual void DrawLine(JPH::RVec3Arg inFrom, JPH::RVec3Arg inTo, JPH::ColorArg inColor) override;
virtual void DrawTriangle(JPH::RVec3Arg inV1, JPH::RVec3Arg inV2, JPH::RVec3Arg inV3, JPH::ColorArg inColor, ECastShadow inCastShadow) override;
virtual JPH::DebugRenderer::Batch CreateTriangleBatch(const Triangle* inTriangles, int inTriangleCount) override;
virtual JPH::DebugRenderer::Batch CreateTriangleBatch(const Vertex* inVertices, int inVertexCount, const JPH::uint32* inIndices, int inIndexCount) override;
virtual void DrawGeometry(JPH::RMat44Arg inModelMatrix, const JPH::AABox& inWorldSpaceBounds, float inLODScaleSq, JPH::ColorArg inModelColor, const GeometryRef& inGeometry, ECullMode inCullMode, ECastShadow inCastShadow, EDrawMode inDrawMode) override;
virtual void DrawText3D(JPH::RVec3Arg inPosition, const JPH::string_view& inString, JPH::ColorArg inColor, float inHeight) override {};
void SetFrameState(const DebugRenderFrameState& state)
{
mFrameState = state;
}
/// Implementation specific batch object
class BatchImpl : public JPH::RefTargetVirtual
{
public:
JPH_OVERRIDE_NEW_DELETE
virtual void AddRef() override
{
++mRefCount;
}
virtual void Release() override
{
if (--mRefCount == 0)
delete this;
}
JPH::Array<JPH::DebugRenderer::Triangle> mTriangles;
private:
JPH::atomic<U32> mRefCount = 0;
};
private:
DebugRenderFrameState mFrameState;
};
#endif
class JoltPlugin : public PhysicsPlugin
{
public:
JoltPlugin();
~JoltPlugin();
/// Create function for factory.
static PhysicsPlugin* create();
// PhysicsPlugin
virtual void destroyPlugin();
virtual void reset();
virtual PhysicsCollision* createCollision();
virtual PhysicsBody* createBody();
virtual PhysicsPlayer* createPlayer();
virtual bool isSimulationEnabled() const;
virtual void enableSimulation(const String& worldName, bool enable);
virtual void setTimeScale(const F32 timeScale);
virtual const F32 getTimeScale() const;
virtual bool createWorld(const String& worldName);
virtual void destroyWorld(const String& worldName);
virtual PhysicsWorld* getWorld(const String& worldName) const;
virtual PhysicsWorld* getWorld() const;
virtual U32 getWorldCount() const;
};
#endif // !_T3D_JOLT_PLUGIN_H_

View file

@ -0,0 +1,391 @@
#include "platform/platform.h"
#include "T3D/physics/jolt/joltWorld.h"
#include "T3D/physics/jolt/joltBody.h"
#include "T3D/physics/jolt/joltPlayer.h"
#include "platform/profiler.h"
#include "sim/netConnection.h"
#include "console/console.h"
#include "console/consoleTypes.h"
#include "scene/sceneRenderState.h"
#include "T3D/gameBase/gameProcess.h"
#include "T3D/physics/physicsUserData.h"
// Save and undefine the macro if it exists
#ifdef Offset
#pragma push_macro("Offset")
#undef Offset
#endif
#include <Jolt/Physics/Collision/RayCast.h>
#include <Jolt/Physics/Collision/CastResult.h>
#include <Jolt/Physics/Body/BodyLock.h>
#include <Jolt/Physics/Body/Body.h>
#include <Jolt/Physics/Collision/NarrowPhaseQuery.h>
#include <Jolt/Physics/Collision/CollideShape.h>
#ifdef Offset
// Restore the original macro after includes
#pragma pop_macro("Offset")
#endif
JoltWorld::JoltWorld()
:mIsEnabled(false),
mIsSimulating(false),
mTickCount(0),
mProcessList(NULL),
mEditorTimeScale(1.0f)
{
mTempAllocator = nullptr;
mJobSystem = nullptr;
}
JoltWorld::~JoltWorld()
{
}
bool JoltWorld::initWorld(bool isServer, ProcessList* processList)
{
const U32 cMaxBodies = 65536;
const U32 cNumBodyMutexes = 1024;
const U32 cMaxBodyPairs = 65536;
const U32 cMaxContactConstraints = 10240;
mTempAllocator = new JPH::TempAllocatorImpl(10 * 1024 * 1024);
// thread pool of 2 threads?
mJobSystem = new JPH::JobSystemThreadPool(
JPH::cMaxPhysicsJobs,
JPH::cMaxPhysicsBarriers,
-1
);
mPhysicsSystem.Init(
cMaxBodies,
cNumBodyMutexes,
cMaxBodyPairs,
cMaxContactConstraints,
mBroadPhaseLayerInterface,
mObjectVsBroadPhaseLayerFilter,
mObjectLayerPairFilter
);
mIsEnabled = true;
mPhysicsSystem.SetGravity(joltCast(mGravity));
mProcessList = processList;
mProcessList->preTickSignal().notify(this, &JoltWorld::getPhysicsResults);
mProcessList->postTickSignal().notify(this, &JoltWorld::tickPhysics, 1000.0f);
return true;
}
void JoltWorld::destroyWorldInternal()
{
// Stop ticking
mIsEnabled = false;
// Remove all bodies
JPH::BodyInterface& bi = mPhysicsSystem.GetBodyInterface();
JPH::BodyIDVector bodies;
mPhysicsSystem.GetBodies(bodies);
for (JPH::BodyID id : bodies)
{
if (bi.IsAdded(id))
bi.RemoveBody(id);
}
// Remove tick listeners
if (mProcessList)
{
mProcessList->preTickSignal().remove(this, &JoltWorld::getPhysicsResults);
mProcessList->postTickSignal().remove(this, &JoltWorld::tickPhysics);
mProcessList = nullptr;
}
// Delete job system and temp allocator
delete mJobSystem;
mJobSystem = nullptr;
delete mTempAllocator;
mTempAllocator = nullptr;
delete this;
}
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);
JPH::Vec3 dir = JPH::Vec3(endPnt.x - startPnt.x, endPnt.y - startPnt.y, endPnt.z - startPnt.z);
F32 rayLength = dir.Length();
if (rayLength <= 0.0001f)
return false;
JPH::RRayCast ray(startV, dir);
JPH::RayCastResult 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(
mPhysicsSystem.GetBodyLockInterface(),
result.mBodyID
);
if (!lock.Succeeded())
return false;
dir /= rayLength;
const JPH::Body& body = lock.GetBody();
JPH::RVec3 hitPos = ray.GetPointOnRay(result.mFraction);
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;
// 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;
}
return true;
}
PhysicsBody* JoltWorld::castRay(const Point3F& start, const Point3F& end, U32 bodyTypes)
{
return nullptr;
}
void JoltWorld::explosion(const Point3F& pos, F32 radius, F32 forceMagnitude)
{
JPH::Vec3 center(pos.x, pos.y, pos.z);
float radiusSq = radius * radius;
// Broadphase query: get all bodies in sphere
JPH::SphereShape sphere(radius);
JPH::CollideShapeSettings settings;
settings.mMaxSeparationDistance = 0.0f;
JPH::RMat44 transform = JPH::RMat44::sTranslation(center);
struct ExplosionCollector : public JPH::CollideShapeCollector
{
std::vector<JPH::BodyID> bodies;
void AddHit(const JPH::CollideShapeResult& result) override
{
bodies.push_back(result.mBodyID2);
}
};
ExplosionCollector collector;
mPhysicsSystem.GetNarrowPhaseQuery().CollideShape(
&sphere,
JPH::Vec3::sOne(),
transform,
settings,
JPH::Vec3::sZero(),
collector
);
auto& bodyInterface = mPhysicsSystem.GetBodyInterface();
for (const JPH::BodyID& bodyID : collector.bodies)
{
if (!bodyInterface.IsAdded(bodyID))
continue;
// Wake the body FIRST
bodyInterface.ActivateBody(bodyID);
// Lock body safely
JPH::BodyLockWrite lock(
mPhysicsSystem.GetBodyLockInterface(),
bodyID
);
if (!lock.Succeeded())
continue;
JPH::Body& body = lock.GetBody();
// 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();
JPH::Vec3 offset = bodyPos - center;
float distSq = offset.LengthSq();
if (distSq > radiusSq || distSq <= 0.0001f)
continue;
float dist = sqrtf(distSq);
// Normalize direction
JPH::Vec3 dir = offset / dist;
// Falloff (linear)
float falloff = 1.0f - (dist / radius);
// Final impulse
JPH::Vec3 impulse = dir * (forceMagnitude * falloff);
// Apply impulse at center of mass
body.AddImpulse(impulse);
}
}
void JoltWorld::onDebugDraw(const SceneRenderState* state)
{
#ifdef JPH_DEBUG_RENDERER
if (!state->isDiffusePass()) return;
if (!mIsEnabled)
return;
auto* renderer = static_cast<JoltDebugRenderer*>(JPH::DebugRenderer::sInstance);
if (!renderer)
return;
DebugRenderFrameState frame;
frame.frustum = state->getCullingFrustum();
frame.cameraPos = joltCast(state->getCameraPosition());
renderer->SetFrameState(frame);
JPH::BodyManager::DrawSettings settings;
settings.mDrawShape = true;
settings.mDrawWorldTransform = false;
settings.mDrawBoundingBox = true;
settings.mDrawVelocity = false;
settings.mDrawCenterOfMassTransform = true;
settings.mDrawShapeColor = JPH::BodyManager::EShapeColor::SleepColor;
mPhysicsSystem.DrawBodies(settings, renderer);
#endif
}
void JoltWorld::tickPhysics(U32 elapsedMs)
{
// Perform deferred destruction
if (mDestroyPending.exchange(false))
{
destroyWorldInternal();
return;
}
if (!mIsEnabled)
return;
if (mResetPending.exchange(false))
performReset();
const F32 dt = (F32)elapsedMs * 0.001f;
PROFILE_SCOPE(JoltWorld_TickPhysics);
mPhysicsSystem.Update(
dt * mEditorTimeScale,
smPhysicsMaxSubSteps, // collision steps
mTempAllocator,
mJobSystem
);
mIsSimulating = true;
}
void JoltWorld::getPhysicsResults()
{
if (!mIsSimulating)
return;
PROFILE_SCOPE(JoltWorld_getPhysicsResults);
mIsSimulating = false;
mTickCount++;
}
void JoltWorld::performReset()
{
JPH::BodyInterface& bodyInterface = mPhysicsSystem.GetBodyInterface();
JPH::BodyIDVector bodies;
mPhysicsSystem.GetBodies(bodies);
for (JPH::BodyID id : bodies)
{
if (!bodyInterface.IsAdded(id))
continue;
bodyInterface.ActivateBody(id);
bodyInterface.SetLinearAndAngularVelocity(
id,
JPH::Vec3::sZero(),
JPH::Vec3::sZero()
);
}
mPhysicsSystem.OptimizeBroadPhase();
}
void JoltWorld::reset()
{
if (!mIsEnabled)
return;
// we operate with a threading environment o not reset right away.
mResetPending.store(true, std::memory_order_release);
}
void JoltWorld::setEnabled(bool enabled)
{
mIsEnabled = enabled;
if (!mIsEnabled)
getPhysicsResults();
}

View file

@ -0,0 +1,190 @@
#pragma once
#ifndef _JOLTWORLD_H_
#define _JOLTWORLD_H_
#ifndef _T3D_PHYSICS_PHYSICSWORLD_H_
#include "T3D/physics/physicsWorld.h"
#endif
#ifndef _T3D_JOLT_PLUGIN_H_
#include "T3D/physics/jolt/joltPlugin.h"
#endif
class ProcessList;
class PhysicsBody;
class JoltPlayer;
namespace Layers
{
static constexpr JPH::ObjectLayer NON_MOVING = 0;
static constexpr JPH::ObjectLayer MOVING = 1;
static constexpr JPH::ObjectLayer CHARACTER = 2;
static constexpr JPH::ObjectLayer DEBRIS = 3;
static constexpr JPH::ObjectLayer NUM_LAYERS = 4;
}
namespace BroadPhaseLayers
{
static constexpr JPH::BroadPhaseLayer NON_MOVING(0);
static constexpr JPH::BroadPhaseLayer MOVING(1);
static constexpr U32 NUM_LAYERS = 2;
}
class BPLayerInterfaceImpl final : public JPH::BroadPhaseLayerInterface
{
public:
BPLayerInterfaceImpl()
{
mObjectToBroadPhase[Layers::NON_MOVING] = BroadPhaseLayers::NON_MOVING;
mObjectToBroadPhase[Layers::MOVING] = BroadPhaseLayers::MOVING;
mObjectToBroadPhase[Layers::CHARACTER] = BroadPhaseLayers::MOVING;
mObjectToBroadPhase[Layers::DEBRIS] = BroadPhaseLayers::MOVING;
}
virtual U32 GetNumBroadPhaseLayers() const override
{
return BroadPhaseLayers::NUM_LAYERS;
}
virtual JPH::BroadPhaseLayer GetBroadPhaseLayer(JPH::ObjectLayer layer) const override
{
return mObjectToBroadPhase[layer];
}
#if defined(JPH_EXTERNAL_PROFILE) || defined(JPH_PROFILE_ENABLED)
virtual const char* GetBroadPhaseLayerName(JPH::BroadPhaseLayer layer) const override
{
switch ((JPH::BroadPhaseLayer::Type)layer)
{
case 0: return "NON_MOVING";
case 1: return "MOVING";
default: return "UNKNOWN";
}
}
#endif
private:
JPH::BroadPhaseLayer mObjectToBroadPhase[Layers::NUM_LAYERS];
};
class ObjectLayerPairFilterImpl : public JPH::ObjectLayerPairFilter
{
public:
virtual bool ShouldCollide(
JPH::ObjectLayer layer1,
JPH::ObjectLayer layer2) const override
{
switch (layer1)
{
case Layers::NON_MOVING:
return layer2 == Layers::MOVING
|| layer2 == Layers::CHARACTER
|| layer2 == Layers::DEBRIS;
case Layers::MOVING:
return true;
case Layers::CHARACTER:
return layer2 == Layers::NON_MOVING
|| layer2 == Layers::MOVING
|| layer2 == Layers::CHARACTER;
case Layers::DEBRIS:
return layer2 == Layers::NON_MOVING; // Only Statics
default:
return false;
}
}
};
class ObjectVsBroadPhaseLayerFilterImpl : public JPH::ObjectVsBroadPhaseLayerFilter
{
public:
virtual bool ShouldCollide(
JPH::ObjectLayer layer1,
JPH::BroadPhaseLayer layer2) const override
{
switch (layer1)
{
case Layers::NON_MOVING:
return layer2 == BroadPhaseLayers::MOVING;
case Layers::MOVING:
case Layers::CHARACTER:
return true;
case Layers::DEBRIS:
return layer2 == BroadPhaseLayers::NON_MOVING;
default:
return false;
}
}
};
class JoltWorld : public PhysicsWorld
{
protected:
bool mIsEnabled;
U32 mTickCount;
bool mIsSimulating;
std::atomic<bool> mResetPending{ false };
std::atomic<bool> mDestroyPending{ false };
F32 mEditorTimeScale;
JPH::PhysicsSystem mPhysicsSystem;
JPH::TempAllocatorImpl* mTempAllocator;
JPH::JobSystemThreadPool* mJobSystem;
BPLayerInterfaceImpl mBroadPhaseLayerInterface;
ObjectVsBroadPhaseLayerFilterImpl mObjectVsBroadPhaseLayerFilter;
ObjectLayerPairFilterImpl mObjectLayerPairFilter;
ProcessList* mProcessList;
public:
JoltWorld();
virtual ~JoltWorld();
// PhysicWorld
virtual bool initWorld(bool isServer, ProcessList* processList);
void destroyWorldInternal();
virtual void destroyWorld();
virtual bool castRay(const Point3F& startPnt, const Point3F& endPnt, RayInfo* ri, const Point3F& impulse);
virtual PhysicsBody* castRay(const Point3F& start, const Point3F& end, U32 bodyTypes);
virtual void explosion(const Point3F& pos, F32 radius, F32 forceMagnitude);
virtual void onDebugDraw(const SceneRenderState* state);
virtual void reset();
virtual bool isEnabled() const { return mIsEnabled; }
void tickPhysics(U32 elapsedMs);
void getPhysicsResults();
bool isWritable() const { return !mIsSimulating; }
void performReset();
void setEnabled(bool enabled);
bool getEnabled() const { return mIsEnabled; }
void setEditorTimeScale(F32 timeScale) { mEditorTimeScale = timeScale; }
const F32 getEditorTimeScale() const { return mEditorTimeScale; }
JPH::PhysicsSystem* getPhysicsSystem() { return &mPhysicsSystem; }
const JPH::BroadPhaseLayerInterface& getBroadPhaseLayerInterface() const { return mBroadPhaseLayerInterface; };
const JPH::ObjectLayerPairFilter& getObjectLayerPairFilter() const { return mObjectLayerPairFilter; }
const JPH::ObjectVsBroadPhaseLayerFilter& getObjectVsBroadPhaseLayerFilter() const { return mObjectVsBroadPhaseLayerFilter; }
JPH::TempAllocatorImpl* getTempAllocator() { return mTempAllocator; }
ProcessList* getProcessList() { return mProcessList; }
};
#endif // !_JOLTWORLD_H_

View file

@ -47,8 +47,8 @@ Rigid::Rigid()
friction = 0.5f;
atRest = false;
sleepLinearThreshold = POINT_EPSILON;
sleepAngThreshold = POINT_EPSILON;
sleepLinearThreshold = 0.01f;
sleepAngThreshold = 0.01f;
sleepTimeThreshold = 0.75f;
sleepTimer = 0.0f;
}
@ -76,8 +76,8 @@ void Rigid::integrate(F32 delta)
{
QuatF dq;
F32 sinHalfAngle;
mSinCos(angle * delta * -0.5f, sinHalfAngle, dq.w);
sinHalfAngle *= 1.0f / angle;
mSinCos(angle * -0.5f, sinHalfAngle, dq.w);
sinHalfAngle *= delta / angle;
dq.x = angVelocity.x * sinHalfAngle;
dq.y = angVelocity.y * sinHalfAngle;
dq.z = angVelocity.z * sinHalfAngle;
@ -101,7 +101,7 @@ void Rigid::integrate(F32 delta)
}
// 5. refresh ang velocity
updateAngularVelocity(delta);
updateAngularVelocity();
// 6. CoM update
@ -138,7 +138,7 @@ void Rigid::updateCenterOfMass()
void Rigid::applyImpulse(const Point3F &r, const Point3F &impulse)
{
if ((impulse.lenSquared() - mass) < POINT_EPSILON) return;
if (impulse.lenSquared() < POINT_EPSILON) return;
wake();
// Linear momentum and velocity

View file

@ -105,7 +105,7 @@ public:
//
void setSleepThresholds(F32 linVel2, F32 angVel2, F32 timeToSleep);
void wake();
TORQUE_FORCEINLINE void updateAngularVelocity(F32 delta) { Point3F deltaVel = angVelocity * delta; invWorldInertia.mulV(angMomentum, &deltaVel); }
TORQUE_FORCEINLINE void updateAngularVelocity() { invWorldInertia.mulV(angMomentum, &angVelocity); }
};

View file

@ -49,6 +49,7 @@
#include "console/engineAPI.h"
#include "T3D/physics/physicsPlugin.h"
#include "T3D/physics/physicsCollision.h"
#include "T3D/containerQuery.h"
IMPLEMENT_CO_DATABLOCK_V1(RigidShapeData);
@ -175,6 +176,7 @@ namespace {
// The larger this number the less often the working list
// will be updated due to motion, but any non-static shape
// that moves into the query box will not be noticed.
//
// Client prediction
const S32 sMaxWarpTicks = 3; // Max warp duration in ticks
const S32 sMaxPredictionTicks = 30; // Number of ticks to predict
@ -189,6 +191,8 @@ namespace {
const U32 sServerCollisionMask = sCollisionMoveMask; // ItemObjectType
const U32 sClientCollisionMask = sCollisionMoveMask;
bool smNoCorrections = false;
bool smNoSmoothing = false;
void nonFilter(SceneObject* object,void *key)
{
@ -272,6 +276,7 @@ RigidShapeData::RigidShapeData()
medSplashSoundVel = 2.0;
hardSplashSoundVel = 3.0;
enablePhysicsRep = true;
isDynamic = false;
for (S32 i = 0; i < Sounds::MaxSounds; i++)
INIT_SOUNDASSET_ARRAY(WaterSounds, i);
@ -453,6 +458,7 @@ void RigidShapeData::packData(BitStream* stream)
stream->write(medSplashSoundVel);
stream->write(hardSplashSoundVel);
stream->write(enablePhysicsRep);
stream->write(isDynamic);
// write the water sound profiles
for (U32 i = 0; i < Sounds::MaxSounds; ++i)
@ -517,6 +523,7 @@ void RigidShapeData::unpackData(BitStream* stream)
stream->read(&medSplashSoundVel);
stream->read(&hardSplashSoundVel);
stream->read(&enablePhysicsRep);
stream->read(&isDynamic);
// write the water sound profiles
for (U32 i = 0; i < Sounds::MaxSounds; ++i)
@ -568,6 +575,12 @@ void RigidShapeData::initPersistFields()
addGroup("Physics");
addField("enablePhysicsRep", TypeBool, Offset(enablePhysicsRep, RigidShapeData),
"@brief Creates a representation of the object in the physics plugin.\n");
addField("isDynamic", TypeBool, Offset(isDynamic, RigidShapeData),
"@brief When true and a physics plugin is active, the body is fully dynamic:\n"
"the plugin owns collision detection, constraint solving and sleep.\n"
"When false (default) the body is kinematic — the built-in Rigid integrator\n"
"drives the simulation, which is correct for player-controlled shapes that\n"
"receive Move input each tick.\n");
addField("massCenter", TypePoint3F, Offset(massCenter, RigidShapeData), "Center of mass for rigid body.");
addField("massBox", TypePoint3F, Offset(massBox, RigidShapeData), "Size of inertial box.");
addFieldV("bodyRestitution", TypeRangedF32, Offset(body.restitution, RigidShapeData), &CommonValidators::PositiveFloat, "The percentage of kinetic energy kept by this object in a collision.");
@ -652,7 +665,14 @@ RigidShape::RigidShape()
mWorkingQueryBoxCountDown = sWorkingQueryBoxStaleThreshold;
mPhysicsRep = NULL;
}
// NEW — initialise PhysicsState members used by the dynamic path
mState.position.set(0, 0, 0);
mState.orientation.identity();
mState.linVelocity.set(0, 0, 0);
mState.angVelocity.set(0, 0, 0);
mState.sleeping = false;
mRenderState[0] = mRenderState[1] = mState;
}
RigidShape::~RigidShape()
{
@ -799,16 +819,26 @@ void RigidShape::_createPhysics()
return;
TSShape* shape = mShapeInstance->getShape();
PhysicsCollision* colShape = NULL;
colShape = shape->buildColShape(false, getScale());
PhysicsCollision* colShape = shape->buildColShape(false, getScale());
if (!colShape)
return;
if (colShape)
{
PhysicsWorld* world = PHYSICSMGR->getWorld(isServerObject() ? "server" : "client");
mPhysicsRep = PHYSICSMGR->createBody();
mPhysicsRep->init(colShape, 0, PhysicsBody::BF_KINEMATIC, this, world);
mPhysicsRep->setTransform(getTransform());
}
const bool dynamic = mDataBlock->isDynamic;
PhysicsWorld* world = PHYSICSMGR->getWorld(isServerObject() ? "server" : "client");
mPhysicsRep = PHYSICSMGR->createBody();
mPhysicsRep->init(colShape, dynamic ? mDataBlock->mass : 0,
dynamic ? 0 : PhysicsBody::BF_KINEMATIC,
this, world);
mPhysicsRep->setMaterial(mDataBlock->body.restitution,
mDataBlock->body.friction,
mDataBlock->body.friction);
mPhysicsRep->setDamping(mDataBlock->minDrag, mDataBlock->minDrag);
mPhysicsRep->setTransform(getTransform());
}
//----------------------------------------------------------------------------
@ -836,70 +866,156 @@ void RigidShape::processTick(const Move* move)
mDelta.posVec.x = -mDelta.warpOffset.x;
mDelta.posVec.y = -mDelta.warpOffset.y;
mDelta.posVec.z = -mDelta.warpOffset.z;
return;
}
else
if (!move)
{
if (!move)
if (isGhost())
{
if (isGhost())
{
// If we haven't run out of prediction time,
// predict using the last known move.
if (mPredictionCount-- <= 0)
return;
move = &mDelta.move;
}
else
move = &NullMove;
// If we haven't run out of prediction time,
// predict using the last known move.
if (mPredictionCount-- <= 0)
return;
move = &mDelta.move;
}
else
move = &NullMove;
}
// Process input move
updateMove(move);
// =========================================================================
// DYNAMIC BODY PATH — physics plugin owns all simulation
// =========================================================================
if (mPhysicsRep && mPhysicsRep->isDynamic())
{
// Single-player shortcut: mirror server state directly to avoid a full
// network round-trip (mirrors PhysicsShape::processTick pattern).
if (PHYSICSMGR->isSinglePlayer() && isClientObject() && getServerObject())
{
RigidShape* srv = static_cast<RigidShape*>(getServerObject());
Parent::setTransform(srv->mState.getTransform());
mRenderState[0] = srv->mRenderState[0];
mRenderState[1] = srv->mRenderState[1];
mState = srv->mState;
// Keep mRigid in sync for anything that reads it (camera, sounds, etc.)
mRigid.linVelocity = mState.linVelocity;
mRigid.angVelocity = mState.angVelocity;
mRigid.linPosition = mState.position;
mRigid.angPosition = mState.orientation;
mRigid.atRest = mState.sleeping;
return;
}
// Process input move
updateMove(move);
// Store previous render state for correction smoothing
mRenderState[0] = mRenderState[1];
Point3F errorDelta = mRenderState[1].position - mState.position;
const bool doSmoothing = !errorDelta.isZero() && !smNoSmoothing;
const bool wasSleeping = mState.sleeping;
// Save current rigid state interpolation
mDelta.posVec = mRigid.linPosition;
mDelta.rot[0] = mRigid.angPosition;
// Freeze support: put the body to sleep when movement is disabled
if (mDisableMove)
{
mPhysicsRep->setSleeping(true);
}
else
{
// Pull the newly-integrated state from the physics plugin
mPhysicsRep->getState(&mState);
_updateContainerForces(); // water, zones
}
// Update the physics based on the integration rate
S32 count = mDataBlock->integration;
--mWorkingQueryBoxCountDown;
// Smooth any server correction back into the render state
mRenderState[1] = mState;
if (doSmoothing)
{
F32 blend = mClampF(errorDelta.len() / 20.0f, 0.1f, 0.9f);
mRenderState[1].position.interpolate(mState.position,
mRenderState[0].position, blend);
mRenderState[1].orientation.interpolate(mState.orientation,
mRenderState[0].orientation, blend);
}
if (!mDisableMove)
updateWorkingCollisionSet(getCollisionMask());
for (U32 i = 0; i < count; i++)
updatePos(TickSec / count);
// Keep mRigid in sync for subsystems that read it directly
mRigid.linPosition = mState.position;
mRigid.angPosition = mState.orientation;
mRigid.linVelocity = mState.linVelocity;
mRigid.angVelocity = mState.angVelocity;
mRigid.atRest = mState.sleeping;
// Wrap up interpolation info
mDelta.pos = mRigid.linPosition;
mDelta.posVec -= mRigid.linPosition;
mDelta.rot[1] = mRigid.angPosition;
if (!wasSleeping || !mState.sleeping)
{
// Update engine transform from physics state
Parent::setTransform(mState.getTransform());
// Update container database
setPosition(mRigid.linPosition, mRigid.angPosition);
setMaskBits(PositionMask);
updateContainer();
if (isServerObject() && !smNoCorrections && !PHYSICSMGR->isSinglePlayer())
setMaskBits(PositionMask);
//TODO: Only update when position has actually changed
//no need to check if mDataBlock->enablePhysicsRep is false as mPhysicsRep will be NULL if it is
if (mPhysicsRep)
mPhysicsRep->moveKinematicTo(getTransform());
updateContainer();
}
if (isServerObject())
{
checkTriggers();
notifyCollision();
}
return; // done — do NOT fall through to Rigid path
}
// =========================================================================
// KINEMATIC / RIGID SIMULATION PATH (original behaviour, bug-fixed)
// =========================================================================
mDelta.posVec = mRigid.linPosition;
mDelta.rot[0] = mRigid.angPosition;
S32 count = mDataBlock->integration;
--mWorkingQueryBoxCountDown;
if (!mDisableMove)
updateWorkingCollisionSet(getCollisionMask());
for (U32 i = 0; i < count; i++)
updatePos(TickSec / count);
mDelta.pos = mRigid.linPosition;
mDelta.posVec -= mRigid.linPosition;
mDelta.rot[1] = mRigid.angPosition;
setPosition(mRigid.linPosition, mRigid.angPosition);
setMaskBits(PositionMask);
updateContainer();
// Keep the kinematic physics body in sync so other dynamic actors see us
if (mPhysicsRep)
mPhysicsRep->moveKinematicTo(getTransform());
}
void RigidShape::interpolateTick(F32 dt)
{
Parent::interpolateTick(dt);
if ( isMounted() )
return;
if (isMounted()) return;
if(dt == 0.0f)
if (mPhysicsRep && mPhysicsRep->isDynamic())
{
PhysicsState state;
state.interpolate(mRenderState[1], mRenderState[0], dt);
setRenderTransform(state.getTransform());
mDelta.dt = dt;
return;
}
// Original Rigid path
if (dt == 0.0f)
setRenderPosition(mDelta.pos, mDelta.rot[1]);
else
{
QuatF rot;
rot.interpolate(mDelta.rot[1], mDelta.rot[0], dt);
Point3F pos = mDelta.pos + mDelta.posVec * dt;
setRenderPosition(pos,rot);
setRenderPosition(pos, rot);
}
mDelta.dt = dt;
}
@ -1062,13 +1178,21 @@ void RigidShape::getCameraTransform(F32* pos,MatrixF* mat)
void RigidShape::getVelocity(const Point3F& r, Point3F* v)
{
mRigid.getVelocity(r, v);
if (mPhysicsRep && mPhysicsRep->isDynamic())
*v = mState.linVelocity;
else
mRigid.getVelocity(r, v);
}
void RigidShape::applyImpulse(const Point3F &pos, const Point3F &impulse)
{
if (mPhysicsRep && mPhysicsRep->isDynamic())
{
mPhysicsRep->applyImpulse(pos, impulse);
return;
}
Point3F r;
mRigid.getOriginVector(pos,&r);
mRigid.getOriginVector(pos, &r);
mRigid.applyImpulse(r, impulse);
}
@ -1104,6 +1228,17 @@ void RigidShape::setTransform(const MatrixF& newMat)
Parent::setTransform(newMat);
mRigid.atRest = false;
mContacts.clear();
// For dynamic bodies, also keep mState consistent
if (mPhysicsRep && mPhysicsRep->isDynamic())
{
mState.position = newMat.getPosition();
mState.orientation.set(newMat);
mRenderState[0] = mRenderState[1] = mState;
}
if (mPhysicsRep)
mPhysicsRep->setTransform(newMat); // covers both kinematic and dynamic
}
void RigidShape::forceClientTransform()
@ -1153,8 +1288,8 @@ void RigidShape::updatePos(F32 dt)
if (mCollisionList.getCount())
{
F32 k = mRigid.getKineticEnergy();
F32 G = mNetGravity* dt * TickMs / mDataBlock->integration;
F32 Kg = mRigid.mass * G * G * TickSec;
F32 G = mNetGravity * dt;
F32 Kg = mRigid.mass * G * G;
if (k < sRestTol * Kg && ++restCount > sRestCount)
mRigid.setAtRest();
}
@ -1241,6 +1376,53 @@ void RigidShape::updatePos(F32 dt)
//----------------------------------------------------------------------------
void RigidShape::_updateContainerForces()
{
if (!mPhysicsRep || !mPhysicsRep->isDynamic())
return;
ContainerQueryInfo info;
info.box = getWorldBox();
info.mass = mDataBlock->mass;
getContainer()->findObjects(getWorldBox(), WaterObjectType | PhysicalZoneObjectType,
findRouter, &info);
// Base drag from the datablock's dragForce field
F32 linDrag = mDataBlock->dragForce;
F32 angDrag = mDataBlock->dragForce;
Point3F cmass = mPhysicsRep->getCMassPosition();
if (info.waterCoverage > 0.0f)
{
// Scale drag by water viscosity
F32 waterScale = info.waterViscosity * 2.0f;
F32 pow4 = mPow(info.waterCoverage, 0.25f);
linDrag = mLerp(linDrag, linDrag * waterScale, pow4);
angDrag = mLerp(angDrag, angDrag * waterScale, pow4);
// Buoyancy — uses ShapeBaseData::density (inherited by RigidShapeData)
F32 density = mDataBlock->density;
if (density > 0.0f)
{
F32 buoyancy = (info.waterDensity / density) * mPow(info.waterCoverage, 2.0f);
// mNetGravity is signed (negative = downward in Torque Z-up).
// Buoyancy opposes gravity, so the force is in the +Z direction.
Point3F buoyancyForce(0.0f, 0.0f, buoyancy * -mNetGravity * TickSec * mDataBlock->mass);
mPhysicsRep->applyImpulse(cmass, buoyancyForce);
}
}
mPhysicsRep->setDamping(linDrag, angDrag);
// Physical zone forces (wind, push, etc.)
if (!info.appliedForce.isZero())
mPhysicsRep->applyImpulse(cmass, info.appliedForce);
}
//----------------------------------------------------------------------------
void RigidShape::updateForces(F32 dt)
{
if (mDisableMove)
@ -1542,41 +1724,80 @@ void RigidShape::writePacketData(GameConnection *connection, BitStream *stream)
{
Parent::writePacketData(connection, stream);
mathWrite(*stream, mRigid.linPosition);
if (!stream->writeFlag(mRigid.atRest))
if (mDataBlock->isDynamic)
{
mathWrite(*stream, mRigid.angPosition);
mathWrite(*stream, mRigid.linMomentum);
mathWrite(*stream, mRigid.angMomentum);
mathWrite(*stream, mState.position);
if (!stream->writeFlag(mState.sleeping))
{
stream->writeQuat(mState.orientation, 9);
stream->writeVector(mState.linVelocity, 1000.0f, 16, 9);
stream->writeVector(mState.angVelocity, 10.0f, 10, 9);
}
}
else
{
mathWrite(*stream, mRigid.linPosition);
if (!stream->writeFlag(mRigid.atRest))
{
mathWrite(*stream, mRigid.angPosition);
mathWrite(*stream, mRigid.linMomentum);
mathWrite(*stream, mRigid.angMomentum);
}
stream->writeFlag(mContacts.getCount() == 0);
}
stream->writeFlag(mContacts.getCount() == 0);
stream->writeFlag(mDisableMove);
stream->setCompressionPoint(mRigid.linPosition);
stream->setCompressionPoint(mDataBlock->isDynamic ? mState.position : mRigid.linPosition);
}
void RigidShape::readPacketData(GameConnection *connection, BitStream *stream)
{
Parent::readPacketData(connection, stream);
mathRead(*stream, &mRigid.linPosition);
if (stream->readFlag())
if (mDataBlock->isDynamic)
{
mRigid.setAtRest();
mathRead(*stream, &mState.position);
if (stream->readFlag()) // sleeping
{
mState.sleeping = true;
if (mPhysicsRep) mPhysicsRep->setSleeping(true);
}
else
{
mState.sleeping = false;
stream->readQuat(&mState.orientation, 9);
stream->readVector(&mState.linVelocity, 1000.0f, 16, 9);
stream->readVector(&mState.angVelocity, 10.0f, 10, 9);
if (mPhysicsRep && mPhysicsRep->isDynamic())
{
mPhysicsRep->setTransform(mState.getTransform());
mPhysicsRep->setLinVelocity(mState.linVelocity);
mPhysicsRep->setAngVelocity(mState.angVelocity);
}
}
}
else
{
mathRead(*stream, &mRigid.angPosition);
mathRead(*stream, &mRigid.linMomentum);
mathRead(*stream, &mRigid.angMomentum);
mRigid.updateInertialTensor();
mRigid.updateVelocity();
mathRead(*stream, &mRigid.linPosition);
if (stream->readFlag())
{
mRigid.setAtRest();
}
else
{
mathRead(*stream, &mRigid.angPosition);
mathRead(*stream, &mRigid.linMomentum);
mathRead(*stream, &mRigid.angMomentum);
mRigid.updateInertialTensor();
mRigid.updateVelocity();
}
if (stream->readFlag())
mContacts.clear();
}
if (stream->readFlag())
mContacts.clear();
mDisableMove = stream->readFlag();
stream->setCompressionPoint(mRigid.linPosition);
stream->setCompressionPoint(mDataBlock->isDynamic ? mState.position : mRigid.linPosition);
}
@ -1597,16 +1818,32 @@ U32 RigidShape::packUpdate(NetConnection *con, U32 mask, BitStream *stream)
{
stream->writeFlag(mask & ForceMoveMask);
stream->writeCompressedPoint(mRigid.linPosition);
if (!stream->writeFlag(mRigid.atRest))
if (mDataBlock->isDynamic)
{
mathWrite(*stream, mRigid.angPosition);
mathWrite(*stream, mRigid.linMomentum);
mathWrite(*stream, mRigid.angMomentum);
// PhysicsState packet: position, orientation, sleeping, velocities.
// mState was updated in processTick from mPhysicsRep->getState().
stream->writeCompressedPoint(mState.position);
stream->writeQuat(mState.orientation, 9);
if (!stream->writeFlag(mState.sleeping))
{
stream->writeVector(mState.linVelocity, 1000.0f, 16, 9);
stream->writeVector(mState.angVelocity, 10.0f, 10, 9);
}
}
else
{
// Original Rigid momentum packet
stream->writeCompressedPoint(mRigid.linPosition);
if (!stream->writeFlag(mRigid.atRest))
{
mathWrite(*stream, mRigid.angPosition);
mathWrite(*stream, mRigid.linMomentum);
mathWrite(*stream, mRigid.angMomentum);
}
}
}
if(stream->writeFlag(mask & FreezeMask))
if (stream->writeFlag(mask & FreezeMask))
stream->writeFlag(mDisableMove);
return retMask;
@ -1614,93 +1851,137 @@ U32 RigidShape::packUpdate(NetConnection *con, U32 mask, BitStream *stream)
void RigidShape::unpackUpdate(NetConnection *con, BitStream *stream)
{
Parent::unpackUpdate(con,stream);
Parent::unpackUpdate(con, stream);
if (stream->readFlag())
return;
mDelta.move.unpack(stream);
if (stream->readFlag())
if (stream->readFlag()) // PositionMask
{
// Check if we need to jump to the given transform
// rather than interpolate to it.
bool forceUpdate = stream->readFlag();
mPredictionCount = sMaxPredictionTicks;
F32 speed = mRigid.linVelocity.len();
mDelta.warpRot[0] = mRigid.angPosition;
// Read in new position and momentum values
stream->readCompressedPoint(&mRigid.linPosition);
if (stream->readFlag())
if (mDataBlock->isDynamic)
{
mRigid.setAtRest();
// --- Dynamic path ---
PhysicsState newState;
stream->readCompressedPoint(&newState.position);
stream->readQuat(&newState.orientation, 9);
newState.sleeping = stream->readFlag();
if (!newState.sleeping)
{
stream->readVector(&newState.linVelocity, 1000.0f, 16, 9);
stream->readVector(&newState.angVelocity, 10.0f, 10, 9);
}
if (mPhysicsRep && mPhysicsRep->isDynamic())
{
if (forceUpdate)
{
// Hard snap — no smoothing, straight to the authoritative position
mPhysicsRep->setTransform(newState.getTransform());
}
else if (!smNoCorrections)
{
// Soft correction — the physics body blends toward the new state
mPhysicsRep->applyCorrection(newState.getTransform());
}
mPhysicsRep->setSleeping(newState.sleeping);
if (!newState.sleeping)
{
mPhysicsRep->setLinVelocity(newState.linVelocity);
mPhysicsRep->setAngVelocity(newState.angVelocity);
}
// Re-read so mState reflects what the body is actually doing after
// the correction (applyCorrection may blend rather than snap).
mPhysicsRep->getState(&mState);
}
else
{
// No live physics rep — store state for extrapolation / render
mState = newState;
}
if (forceUpdate || !isProperlyAdded())
{
Parent::setTransform(mState.getTransform());
mRenderState[0] = mRenderState[1] = mState;
}
// Sync mRigid so anything reading it sees sane values
mRigid.linPosition = mState.position;
mRigid.angPosition = mState.orientation;
mRigid.linVelocity = mState.linVelocity;
mRigid.angVelocity = mState.angVelocity;
mRigid.atRest = mState.sleeping;
mRigid.updateCenterOfMass();
}
else
{
mathRead(*stream, &mRigid.angPosition);
mathRead(*stream, &mRigid.linMomentum);
mathRead(*stream, &mRigid.angMomentum);
mRigid.updateVelocity();
}
// --- Original Rigid (kinematic) path ---
F32 speed = mRigid.linVelocity.len();
mDelta.warpRot[0] = mRigid.angPosition;
if (!forceUpdate && isProperlyAdded())
{
// Determine number of ticks to warp based on the average
// of the client and server velocities.
Point3F cp = mDelta.pos + mDelta.posVec * mDelta.dt;
mDelta.warpOffset = mRigid.linPosition - cp;
// Calc the distance covered in one tick as the average of
// the old speed and the new speed from the server.
F32 dt,as = (speed + mRigid.linVelocity.len()) * 0.5 * TickSec;
// Cal how many ticks it will take to cover the warp offset.
// If it's less than what's left in the current tick, we'll just
// warp in the remaining time.
if (!as || (dt = mDelta.warpOffset.len() / as) > sMaxWarpTicks)
dt = mDelta.dt + sMaxWarpTicks;
stream->readCompressedPoint(&mRigid.linPosition);
if (stream->readFlag())
{
mRigid.setAtRest();
}
else
dt = (dt <= mDelta.dt)? mDelta.dt : mCeil(dt - mDelta.dt) + mDelta.dt;
// Adjust current frame interpolation
if (mDelta.dt)
{
mDelta.pos = cp + (mDelta.warpOffset * (mDelta.dt / dt));
mDelta.posVec = (cp - mDelta.pos) / mDelta.dt;
QuatF cr;
cr.interpolate(mDelta.rot[1],mDelta.rot[0],mDelta.dt);
mDelta.rot[1].interpolate(cr,mRigid.angPosition,mDelta.dt / dt);
mDelta.rot[0].extrapolate(mDelta.rot[1],cr,mDelta.dt);
mathRead(*stream, &mRigid.angPosition);
mathRead(*stream, &mRigid.linMomentum);
mathRead(*stream, &mRigid.angMomentum);
mRigid.updateVelocity();
}
// Calculated multi-tick warp
mDelta.warpCount = 0;
mDelta.warpTicks = (S32)(mFloor(dt));
if (mDelta.warpTicks)
if (!forceUpdate && isProperlyAdded())
{
mDelta.warpOffset = mRigid.linPosition - mDelta.pos;
mDelta.warpOffset /= mDelta.warpTicks;
mDelta.warpRot[0] = mDelta.rot[1];
mDelta.warpRot[1] = mRigid.angPosition;
Point3F cp = mDelta.pos + mDelta.posVec * mDelta.dt;
mDelta.warpOffset = mRigid.linPosition - cp;
F32 dt, as = (speed + mRigid.linVelocity.len()) * 0.5f * TickSec;
if (!as || (dt = mDelta.warpOffset.len() / as) > sMaxWarpTicks)
dt = mDelta.dt + sMaxWarpTicks;
else
dt = (dt <= mDelta.dt) ? mDelta.dt : mCeil(dt - mDelta.dt) + mDelta.dt;
if (mDelta.dt)
{
mDelta.pos = cp + (mDelta.warpOffset * (mDelta.dt / dt));
mDelta.posVec = (cp - mDelta.pos) / mDelta.dt;
QuatF cr;
cr.interpolate(mDelta.rot[1], mDelta.rot[0], mDelta.dt);
mDelta.rot[1].interpolate(cr, mRigid.angPosition, mDelta.dt / dt);
mDelta.rot[0].extrapolate(mDelta.rot[1], cr, mDelta.dt);
}
mDelta.warpCount = 0;
mDelta.warpTicks = (S32)(mFloor(dt));
if (mDelta.warpTicks)
{
mDelta.warpOffset = mRigid.linPosition - mDelta.pos;
mDelta.warpOffset /= mDelta.warpTicks;
mDelta.warpRot[0] = mDelta.rot[1];
mDelta.warpRot[1] = mRigid.angPosition;
}
}
else
{
mDelta.dt = 0;
mDelta.pos = mRigid.linPosition;
mDelta.posVec.set(0, 0, 0);
mDelta.rot[1] = mDelta.rot[0] = mRigid.angPosition;
mDelta.warpCount = mDelta.warpTicks = 0;
setPosition(mRigid.linPosition, mRigid.angPosition);
}
mRigid.updateCenterOfMass();
}
else
{
// Set the shape to the server position
mDelta.dt = 0;
mDelta.pos = mRigid.linPosition;
mDelta.posVec.set(0,0,0);
mDelta.rot[1] = mDelta.rot[0] = mRigid.angPosition;
mDelta.warpCount = mDelta.warpTicks = 0;
setPosition(mRigid.linPosition, mRigid.angPosition);
}
mRigid.updateCenterOfMass();
}
if(stream->readFlag())
if (stream->readFlag()) // FreezeMask
mDisableMove = stream->readFlag();
}
@ -1724,6 +2005,14 @@ void RigidShape::consoleInit()
"The larger this number the less often the working list will be updated due to motion, but any non-static shape that "
"moves into the query box will not be noticed.\n\n"
"@ingroup GameObjects\n");
// NEW — mirrors PhysicsShape debug knobs for the dynamic path
Con::addVariable("$RigidShape::noCorrections", TypeBool, &smNoCorrections,
"@brief When true, the server will not send state corrections to the client "
"for dynamic RigidShapes. Debug only.\n\n");
Con::addVariable("$RigidShape::noSmoothing", TypeBool, &smNoSmoothing,
"@brief When true, dynamic RigidShape clients snap to corrected positions "
"instead of smoothly interpolating. Debug only.\n\n");
}
void RigidShape::initPersistFields()
@ -1857,12 +2146,20 @@ void RigidShape::reset()
{
mRigid.clearForces();
mRigid.setAtRest();
if (mPhysicsRep && mPhysicsRep->isDynamic())
{
mPhysicsRep->setLinVelocity(Point3F::Zero);
mPhysicsRep->setAngVelocity(Point3F::Zero);
mPhysicsRep->setSleeping(true);
}
}
void RigidShape::freezeSim(bool frozen)
{
mDisableMove = frozen;
setMaskBits(FreezeMask);
if (mPhysicsRep && mPhysicsRep->isDynamic())
mPhysicsRep->setSleeping(frozen);
}
DefineEngineMethod( RigidShape, reset, void, (),,

View file

@ -119,7 +119,7 @@ class RigidShapeData : public ShapeBaseData
F32 splashVelEpsilon;
bool enablePhysicsRep;
bool isDynamic;
F32 dragForce;
F32 vertFactor;
@ -189,6 +189,10 @@ class RigidShape: public ShapeBase
};
PhysicsBody* mPhysicsRep;
/// Current physics state (used by the dynamic-body path).
PhysicsState mState;
/// Previous and current render states for the dynamic-body path.
PhysicsState mRenderState[2];
StateDelta mDelta;
S32 mPredictionCount; ///< Number of ticks to predict
@ -243,6 +247,8 @@ class RigidShape: public ShapeBase
void updateForces(F32 dt);
void _updateContainerForces();
public:
// Test code...
static ClippedPolyList* sPolyList;

View file

@ -2587,11 +2587,15 @@ void MeshRoad::_generateSegments()
polylist.triangulate();
PhysicsCollision *colShape = PHYSICSMGR->createCollision();
colShape->addTriangleMesh( polylist.mVertexList.address(),
if (!colShape->addTriangleMesh(polylist.mVertexList.address(),
polylist.mVertexList.size(),
polylist.mIndexList.address(),
polylist.mIndexList.size() / 3,
MatrixF::Identity );
getWorldTransform()))
{
// failed to create mesh, get out GET OUT NOWWWW
return;
}
PhysicsWorld *world = PHYSICSMGR->getWorld( isServerObject() ? "server" : "client" );
mPhysicsRep = PHYSICSMGR->createBody();