Using Bullet for physics in UE4

· by Steve · Read in about 28 min · (5847 Words)

A Problem of Determinism

I had a particular problem to solve for our next (so far unannounced) game. I needed deterministic physics. That is, given a certain starting state, I needed to know that if I applied the same forces to that simulation, the same results would always occur.

Now, this is extremely hard to do universally, especially across platforms, and even between different binaries on the same platform. So, I limited my definition a little: that given an identical build, on the same platform, the results were deterministic. This more constrained definition is certainly achievable technically.

UE uses PhysX (like Unity), but the way it uses it does not support determinism. In order for a physics engine to be deterministic, absolutely everything must be consistent:

  • You must only ever advance the simulation by fixed time steps
  • All interactions must be resolved in a fixed, repeatable order

PhysX is deterministic if you use fixed time steps, and use the “enhanced determinism” mode which means contacts are resolved in a non-random order. You can turn on the latter in the UE settings, but you cannot ever have fixed simulation steps without making source changes to the engine. Epic decided that by they would run the simulation at frame rate speeds, with the option to sub-step if necessary, but the simulation would always be synced up with the frame; meaning even if you sub-stepped with a fixed rate, the final “slice” of time to sync up to the frame is variable. Very much not deterministic.

My preferred way would be to do that last frame-rate sync slice as a prediction, not an actual simulation step, but UE doesn’t give you that option. You have various hooks into the physics ticks in the engine but you can never fix that problem without UE source changes, which I didn’t want to do.

I quickly came to the conclusion that the only way I could do this cleanly was to run my own physics simulation. While I could have built another PhysX scene to run in parallel, I chose to try doing it in Bullet, for a few reasons:

  • I could be 100% sure that UE wouldn’t be messing with it
  • Bullet has a nice in-built system for interpolating fixed simulation ticks with variable frame rates
  • Bullet has support for sliding and rolling friction, which interested me (that didn’t pan out)

Luckily because UE gives you access to the C++ internals, integrating a 3rd party engine isn’t too difficult. This is that story.

Building Bullet

There are a couple of wrinkles in the way you need to build Bullet for compatibility with UE:

Use the DLL version of the Microsoft C++ Runtime Library (CRT)

The default Premake builds of Bullet use the static CRT, which will cause conflicts if you try to link that with a UE module. I submitted a PR to add that as an option (now merged), however it turned out later that I needed another option which the version of Premake didn’t support. So, I switched to the CMake build instead, which already supports this option.

Use the Release version of the Microsoft C++ Runtime Library (CRT) in debug

UE uses the Release version of the CRT even in debug builds, so if you link a debug Bullet library which doesn’t adhere to this, it complains. Now, you can tell your UE module to use the Debug CRT instead (the option is bDebugBuildsActuallyUseDebugCRT = true in your build.cs file), but really it’s better to stick with the release CRT, you don’t really need to debug into it.

So I added this as an option to the CMake build for Bullet as well so everything matched up.

Build Instructions

  1. Run the CMake UI
  2. It doesn’t matter where you choose for “Where to build the binaries”
  3. Check the USE_MSVC_RUNTIME_LIBRARY_DLL option
  4. Check the USE_MSVC_RELEASE_RUNTIME_ALWAYS option
  5. Pick a location for LIBRARY_OUTPUT_PATH that’s inside your UE project
    • e.g. UEProject/ThirdParty/lib
  6. Press the Configure button
  7. Press Generate
  8. Press Open Project
  9. In VS, Build > Batch Build
    • Check App_HelloWorld in Debug, Release and RelWithDbgInfo configs
    • Hit Build

Once done you should have static libraries in LIBRARY_OUTPUT_PATH/Debug|Release|RelWithDebInfo.

Adding to your UE project build

As with any C++ project you need access to the headers and the libraries. Unfortunately Bullet doesn’t separate its headers and cpp files, so the easiest thing is just to make sure you have the source somewhere accessible. I have Bullet as a submodule inside my project, and have my CMake “where to build” option set outside the tracked project dir for cleanup simplicity.

Then you need to edit your Project.Build.cs to add the header / library folders. Something like this:

public class MyProject : ModuleRules
{
    public MyProject(ReadOnlyTargetRules Target) : base(Target)
    {
        // This stuff is pretty standard, yours may include more things
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
        PrivateDependencyModuleNames.AddRange(new string[] {  });


        AddBullet();
    }

    /// Helper to give us ProjectRoot/ThirdParty
    private string ThirdPartyPath
    {
        get { return Path.GetFullPath( Path.Combine( ModuleDirectory, "../../ThirdParty/" ) ); }
    }

    protected AddBullet() 
    {
        // This is real basic, only for a single platform & config (Win64)
        // If you build for more variants you'll have to do some more work here

        bool bDebug = Target.Configuration == UnrealTargetConfiguration.Debug || Target.Configuration == UnrealTargetConfiguration.DebugGame;
        bool bDevelopment = Target.Configuration == UnrealTargetConfiguration.Development;

        string BuildFolder = bDebug ? "Debug":
            bDevelopment ? "RelWithDebInfo" : "Release";
        string BuildSuffix = bDebug ? "_Debug":
            bDevelopment ? "_RelWithDebugInfo" : "";

        // Library path
        string LibrariesPath = Path.Combine(ThirdPartyPath, "lib", "bullet", BuildFolder);
        PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath, "BulletCollision" + BuildSuffix + ".lib")); 
        PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath, "BulletDynamics" + BuildSuffix + ".lib")); 
        PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath, "LinearMath" + BuildSuffix + ".lib")); 

        // Include path (I'm just using the source here since Bullet has mixed src & headers)
        PublicIncludePaths.Add( Path.Combine( ThirdPartyPath, "bullet3", "src" ) );
        PublicDefinitions.Add("WITH_BULLET_BINDING=1");

    }
}

Including Bullet in your UE source files

One more wrinkle; there are several differences with UE’s build environment that will cause problems if you just #include Bullet things plainly into your UE code.

Whenever you include a Bullet header, you need to surround them with special UE macros like this:

// This is needed to suppress some warnings that UE4 escalates that Bullet doesn't
THIRD_PARTY_INCLUDES_START
// This is needed to fix memory alignment issues
PRAGMA_PUSH_PLATFORM_DEFAULT_PACKING
#include <btBulletDynamicsCommon.h>
PRAGMA_POP_PLATFORM_DEFAULT_PACKING
THIRD_PARTY_INCLUDES_END

The memory alignment thing is particularly insidious because it will ostensibly work without it, but you will get occasional memory errors caused by Bullet and UE packing things differently.

Personally, the way I’ve handled this is to create wrapper headers which do all this for me so I don’t have to remember:

BulletMinimal.h (include in headers)

This header includes the basic value types (vectors etc) and headers for classes we derive from (see later), together with forward-declarations for everything we reference by pointer. So ideal for including in our UE header files which need to talk track Bullet data.

#pragma once
#include "CoreMinimal.h"

// The most minimal include for Bullet as needed by headers for value types / subclasses

// This is needed to suppress some warnings that UE4 escalates that Bullet doesn't
THIRD_PARTY_INCLUDES_START
// This is needed to fix memory alignment issues
PRAGMA_PUSH_PLATFORM_DEFAULT_PACKING

// Value types
#include <LinearMath/btQuaternion.h>
#include <LinearMath/btTransform.h>
#include <LinearMath/btVector3.h>

// Core things we override
#include <LinearMath/btDefaultMotionState.h>
#include <LinearMath/btIDebugDraw.h>

// Forward decl for everything else we use
class btCollisionConfiguration;
class btCollisionDispatcher;
class btBroadphaseInterface;
class btConstraintSolver;
class btDynamicsWorld;
class btCollisionShape;
class btBoxShape;
class btCapsuleShape;
class btConvexHullShape;
class btCompoundShape;
class btSphereShape;
class btRigidBody;
class btCollisionObject;

PRAGMA_POP_PLATFORM_DEFAULT_PACKING
THIRD_PARTY_INCLUDES_END

BulletMain.h (include in source files)

This header is what I include in source files which actually need to call Bullet fully. It’s a shorter header but includes more things (indirectly).

#pragma once
#include "CoreMinimal.h"

// More complete Bullet include 

// This is needed to suppress some warnings that UE4 escalates that Bullet doesn't
THIRD_PARTY_INCLUDES_START
// This is needed to fix memory alignment issues
PRAGMA_PUSH_PLATFORM_DEFAULT_PACKING
#include <btBulletDynamicsCommon.h>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
PRAGMA_POP_PLATFORM_DEFAULT_PACKING
THIRD_PARTY_INCLUDES_END

OK, that’s all the bootstrapping done, let’s actually make use of Bullet.

Usage Principles

I was not trying to replace UE’s collision and physics entirely, to produce a complete drop-in replacement that should work for everything. I like simplicity, so I only implemented precisely what I needed.

In my case, I know precisely ahead of time what objects are in my simulation, and have an existing Actor which “owns” all of them. Therefore, I implemented almost the entire physics integration in that one owning actor, which controlled the lifecycle of everything else.

That Actor also acted as a local centre for the simulation, so that no matter where it was in the world, the Bullet simulation was running locally around this origin, which helps preserve precision (because of this I only needed single precision).

Crucially, although all the UE objects had physics disabled, they still had their PhysX colliders, and I continued to use UE collision for non-physics interactions, like overlap triggers. This let me keep the scope constrained to just physics simulation.

You’re of course free to extend this idea to a more complete solution if you want, but I chose to keep things as simple as possible.

The “Physics World” Actor

As mentioned above, almost all the implementation is in a single actor.

Data Storage

Here’s pretty much all the data we hold about the Bullet world, in one place. Again, I exploit my particular simple requirements here and there are no (direct) Bullet links in any other actors, just here, because this Actor owns all the objects in this part of the world anyway. You’ll see how that all hangs together later.

...
#include "BulletMinimal.h"
...

class MYPROJECT_API APhysicsWorldActor : public AActor
{
    ...

    // Bullet section
    // Global objects
    btCollisionConfiguration* BtCollisionConfig;
    btCollisionDispatcher* BtCollisionDispatcher;
    btBroadphaseInterface* BtBroadphase;
    btConstraintSolver* BtConstraintSolver;
    btDynamicsWorld* BtWorld;
    // Custom debug interface
    btIDebugDraw* BtDebugDraw;
    // Dynamic bodies
    TArray<btRigidBody*> BtRigidBodies;
    // Static colliders
    TArray<btCollisionObject*> BtStaticObjects;
    // Re-usable collision shapes
    TArray<btBoxShape*> BtBoxCollisionShapes;
    TArray<btSphereShape*> BtSphereCollisionShapes;
    TArray<btCapsuleShape*> BtCapsuleCollisionShapes;
    // Structure to hold re-usable ConvexHull shapes based on origin BodySetup / subindex / scale
    struct ConvexHullShapeHolder
    {
        UBodySetup* BodySetup;
        int HullIndex;
        FVector Scale;
        btConvexHullShape* Shape;
    };
    TArray<ConvexHullShapeHolder> BtConvexHullCollisionShapes;
    // These shapes are for *potentially* compound rigid body shapes
    struct CachedDynamicShapeData
    {
        FName ClassName; // class name for cache
        btCollisionShape* Shape;
        bool bIsCompound; // if true, this is a compound shape and so must be deleted
        btScalar Mass;
        btVector3 Inertia; // because we like to precalc this
    };
    TArray<CachedDynamicShapeData> CachedDynamicShapes;

    ...
};    

Don’t worry too much about what everything is yet, it’ll all be referenced in the source excerpts below.

I’ve chosen to store everything in simple arrays. When I need to find something, I just iterate. I know the size of my world is not huge, and that most of the work is done straight after load, so more complex data access patterns are not required. YMMV.

Initialising the world

First things first, let’s get the Bullet world bootstrapped.

    // This is all pretty standard Bullet bootstrap
    BtCollisionConfig = new btDefaultCollisionConfiguration();
    BtCollisionDispatcher = new btCollisionDispatcher (BtCollisionConfig);
    BtBroadphase = new btDbvtBroadphase ();
    BtConstraintSolver = new btSequentialImpulseConstraintSolver();
    BtWorld = new btDiscreteDynamicsWorld (BtCollisionDispatcher, BtBroadphase, BtConstraintSolver, BtCollisionConfig);

    // I mess with a few settings on BtWorld->getSolverInfo() but they're specific to my needs  

    // Gravity vector in our units (1=1cm)
    BtWorld->setGravity(BulletHelpers::ToBtDir(FVector(0, 0, -980)));

That’s all pretty painless. There’s nothing in the world yet, but it’s all set up and know what direction gravity is in.

But wait, what’s that BulletHelpers class?

That’s just providing a central place to convert UE units to Bullet units (and back) Bullet uses 1=1m, wheras UE uses 1=1cm. To ensure realistic physical behaviour, we need to use the correct units. As I mentioned earlier, I also run Bullet as a local simulation relative to this Physics World actor so I can keep maximum precision, BulletHelpers does that too. I figure it’s not worth cluttering things here because it’s simple math, but I’ll put it at the end.

Adding Static Colliders

Let’s start adding things to the world. Let’s talk about static colliders first, things that won’t move, but dynamic objects will bounce off.

Again, I’ll stress here that I’m not creating a generic drop-in here so I’m not querying the scene for things that might be part of this physics scene, it’s all specified manually by me. What I do is have a list of actors, on this world actor, and a set of physical properties for them (a bit like a physical material in UE, just directly specified for simplicity). I have a fixed number of “types” of physical material so I just do them each deliberately, like this:

    // This list can be edited in the level, linking to placed static actors
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Bullet Physics|Objects")
    TArray<AActor*> PhysicsStaticActors1;
    // These properties can only be edited in the Blueprint
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Bullet Physics|Objects")
    float PhysicsStatic1Friction = 0.6;
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Bullet Physics|Objects")
    float PhysicsStatic1Restitution = 0.3;
    // I chose not to use spinning / rolling friction in the end since it had issues!

So I place my Physics World actor in the level, along with a bunch of static actors, then I add them to the list for processing at startup.

You can then pass each set of actors with the same friction / restitution options through the code below to turn them into Bullet static colliders. Actors can contain a StaticMeshComponent or proxy collider components, both are processed:

void APhysicsWorldActor::SetupStaticGeometryPhysics(TArray<AActor*> Actors, float Friction, float Restitution)
{
    for (AActor* Actor : Actors)
    {
        // Just in case we remove items from the list & leave blank
        if (Actor == nullptr)
            continue;

        ExtractPhysicsGeometry(Actor,
            [Actor, this, Friction, Restitution](btCollisionShape* Shape, const FTransform& RelTransform)
            {
                // Every sub-collider in the actor is passed to this callback function
                // We're baking this in world space, so apply actor transform to relative
                const FTransform FinalXform = RelTransform * Actor->GetActorTransform();
                AddStaticCollision(Shape, FinalXform, Friction, Restitution, Actor);
            });
    }
}

ExtractPhysicsGeometry is a cascading function which pulls out all the collider components we care about, and sends them to a callback (here implemented as a Lambda, one of the handful of things added to C++ since I last used it 10 years ago that I actually like 😉). The callback is defined like this:

    typedef const std::function<void (btCollisionShape* /*SingleShape*/, const FTransform& /*RelativeXform*/)>& PhysicsGeometryCallback;
    void ExtractPhysicsGeometry(AActor* Actor, PhysicsGeometryCallback CB);

So we get back one shape per collider, together with its transform (relative to the Actor). In this static geometry case all I do is pass this to the AddStaticCollision function which does this:

btCollisionObject* APhysicsWorld::AddStaticCollision(btCollisionShape* Shape, const FTransform& Transform, float Friction,
                                         float Restitution, AActor* Actor)
{
    btTransform Xform = BulletHelpers::ToBt(Transform, GetActorLocation());
    btCollisionObject* Obj = new btCollisionObject();
    Obj->setCollisionShape(Shape);
    Obj->setWorldTransform(Xform);
    Obj->setFriction(Friction);
    Obj->setRestitution(Restitution);
    Obj->setUserPointer(Actor);
    BtWorld->addCollisionObject(Obj);
    BtStaticObjects.Add(Obj);
    return Obj;
}

Because these objects aren’t dynamic the setup is quite simple; just create a Bullet collision object for each shape separately, set its world space transform, friction and restitution, add it to Bullet’s world, then add it to our own arrays so we can clean up later.

We don’t care about creating compound transforms where an Actor has multiple colliders, because for static objects it doesn’t matter. For dynamic objects, this will be something we’ll have to cope with (see later).

setUserPointer with a pointer back to the actor is so that if I want to I can get UE tags on collisions. Remember, for overlap triggers etc I’m just using UE’s own systems still, but hard collisions only originate in Bullet.

The btCollisionObject is returned in case we want to change anything else on it, but in this case we’re happy just with the minimum of friction / restitution.

Detail on extracting colliders

But how is all that collision shape information being extracted? OK, there’s a bunch more code here:

void APhysicsWorldActor::ExtractPhysicsGeometry(AActor* Actor, PhysicsGeometryCallback CB)
{
    TInlineComponentArray<UActorComponent*, 20> Components;
    // Used to easily get a component's transform relative to actor, not parent component
    const FTransform InvActorTransform = Actor->GetActorTransform().Inverse();

    // Collisions from meshes
    Actor->GetComponents(UStaticMeshComponent::StaticClass(), Components);
    for (auto && Comp : Components)
    {
        ExtractPhysicsGeometry(Cast<UStaticMeshComponent>(Comp), InvActorTransform, CB);
    }
    // Collisions from separate collision components
    Actor->GetComponents(UShapeComponent::StaticClass(), Components);
    for (auto && Comp : Components)
    {
        ExtractPhysicsGeometry(Cast<UShapeComponent>(Comp), InvActorTransform, CB);
    }   
}

void APhysicsWorldActor::ExtractPhysicsGeometry(UStaticMeshComponent* SMC, const FTransform& InvActorXform, PhysicsGeometryCallback CB)
{
    UStaticMesh* Mesh = SMC->GetStaticMesh();
    if (!Mesh)
        return;

    // We want the complete transform from actor to this component, not just relative to parent
    FTransform CompFullRelXForm =  SMC->GetComponentTransform() * InvActorXform;
    ExtractPhysicsGeometry(CompFullRelXForm, Mesh->BodySetup, CB);

    // Not supporting complex collision shapes right now
    // If we did, note that Mesh->ComplexCollisionMesh is WITH_EDITORONLY_DATA so not available at runtime
    // See StaticMeshRender.cpp, FStaticMeshSceneProxy::GetDynamicMeshElements
    // Line 1417+, bDrawComplexCollision
    // Looks like we have to access LODForCollision, RenderData->LODResources
    // So they use a mesh LOD for collision for complex shapes, never drawn usually?

}

void APhysicsWorldActor::ExtractPhysicsGeometry(UShapeComponent* Sc, const FTransform& InvActorXform, PhysicsGeometryCallback CB)
{
    // We want the complete transform from actor to this component, not just relative to parent
    FTransform CompFullRelXForm =  Sc->GetComponentTransform() * InvActorXform;
    ExtractPhysicsGeometry(CompFullRelXForm, Sc->ShapeBodySetup, CB);   
}


void APhysicsWorldActor::ExtractPhysicsGeometry(const FTransform& XformSoFar, UBodySetup* BodySetup, PhysicsGeometryCallback CB)
{
    FVector Scale = XformSoFar.GetScale3D();
    btCollisionShape* Shape = nullptr;

    // Iterate over the simple collision shapes
    for (auto && Box : BodySetup->AggGeom.BoxElems)
    {
        // We'll re-use based on just the LxWxH, including actor scale
        // Rotation and centre will be baked in world space
        FVector Dimensions = FVector(Box.X, Box.Y, Box.Z) * Scale;
        Shape = GetBoxCollisionShape(Dimensions);
        FTransform ShapeXform(Box.Rotation, Box.Center);
        // Shape transform adds to any relative transform already here
        FTransform XForm = ShapeXform * XformSoFar;
        CB(Shape, XForm);
    }
    for (auto && Sphere : BodySetup->AggGeom.SphereElems)
    {
        // Only support uniform scale so use X
        Shape = GetSphereCollisionShape(Sphere.Radius * Scale.X);
        FTransform ShapeXform(FRotator::ZeroRotator, Sphere.Center);
        // Shape transform adds to any relative transform already here
        FTransform XForm = ShapeXform * XformSoFar;
        CB(Shape, XForm);
    }
    // Sphyl == Capsule (??)
    for (auto && Capsule : BodySetup->AggGeom.SphylElems)
    {
        // X scales radius, Z scales height
        Shape = GetCapsuleCollisionShape(Capsule.Radius * Scale.X, Capsule.Length * Scale.Z);
        // Capsules are in Z in UE, in Y in Bullet, so roll -90
        FRotator Rot(0, 0, -90);
        // Also apply any local rotation
        Rot += Capsule.Rotation;
        FTransform ShapeXform(Rot, Capsule.Center);
        // Shape transform adds to any relative transform already here
        FTransform XForm = ShapeXform * XformSoFar;
        CB(Shape, XForm);
    }
    for (int i = 0; i < BodySetup->AggGeom.ConvexElems.Num(); ++i)
    {
        Shape = GetConvexHullCollisionShape(BodySetup, i, Scale);
        CB(Shape, XformSoFar);
    }
    
}

OK, that was a bit of a code dump! But hopefully you can see what it’s doing. Simple collision is stored in UE under the UBodySetup class; you can pull this out of a UStaticMeshComponent, a mesh with colliders included in its definition, or from a UShapeComponent, which are proxy colliders added underneath an actor with no related geometry.

Re-using collider shapes

I re-use Bullet collider shapes as encouraged in the Bullet docs, which is what methods like GetBoxCollisionShape do. I use some very basic matching rules, nothing fancy:

btCollisionShape* APhysicsWorldActor::GetBoxCollisionShape(const FVector& Dimensions)
{
    // Simple brute force lookup for now, probably doesn't need anything more clever
    btVector3 HalfSize = BulletHelpers::ToBtSize(Dimensions * 0.5);
    for (auto && S : BoxCollisionShapes)
    {
        btVector3 Sz = S->getHalfExtentsWithMargin();
        if (FMath::IsNearlyEqual(Sz.x(), HalfSize.x()) && 
            FMath::IsNearlyEqual(Sz.y(), HalfSize.y()) &&
            FMath::IsNearlyEqual(Sz.z(), HalfSize.z()))
        {
            return S;
        }
    }

    // Not found, create
    auto S = new btBoxShape(HalfSize);
    // Get rid of margins, just cause issues for me
    S->setMargin(0);
    BoxCollisionShapes.Add(S);

    return S;
    
}

btCollisionShape* APhysicsWorldActor::GetSphereCollisionShape(float Radius)
{
    // Simple brute force lookup for now, probably doesn't need anything more clever
    btScalar Rad = BulletHelpers::ToBtSize(Radius);
    for (auto && S : SphereCollisionShapes)
    {
        // Bullet subtracts a margin from its internal shape, so add back to compare
        if (FMath::IsNearlyEqual(S->getRadius(), Rad))
        {
            return S;
        }
    }

    // Not found, create
    auto S = new btSphereShape(Rad);
    // Get rid of margins, just cause issues for me
    S->setMargin(0);
    SphereCollisionShapes.Add(S);

    return S;
    
}

btCollisionShape* APhysicsWorldActor::GetCapsuleCollisionShape(float Radius, float Height)
{
    // Simple brute force lookup for now, probably doesn't need anything more clever
    btScalar R = BulletHelpers::ToBtSize(Radius);
    btScalar H = BulletHelpers::ToBtSize(Height);
    btScalar HalfH = H * 0.5f;
    
    for (auto && S : CapsuleCollisionShapes)
    {
        // Bullet subtracts a margin from its internal shape, so add back to compare
        if (FMath::IsNearlyEqual(S->getRadius(), R) &&
            FMath::IsNearlyEqual(S->getHalfHeight(), HalfH))
        {
            return S;
        }
    }

    // Not found, create
    auto S = new btCapsuleShape(R, H);
    CapsuleCollisionShapes.Add(S);

    return S;
    
}

btCollisionShape* APhysicsWorldActor::GetConvexHullCollisionShape(UBodySetup* BodySetup, int ConvexIndex, const FVector& Scale)
{
    for (auto && S : ConvexHullCollisionShapes)
    {
        if (S.BodySetup == BodySetup && S.HullIndex == ConvexIndex && S.Scale.Equals(Scale))
        {
            return S.Shape;
        }
    }
    
    const FKConvexElem& Elem = BodySetup->AggGeom.ConvexElems[ConvexIndex];
    auto C = new btConvexHullShape();
    for (auto && P : Elem.VertexData)
    {
        C->addPoint(BulletHelpers::ToBtPos(P, FVector::ZeroVector));
    }
    // Very important! Otherwise there's a gap between 
    C->setMargin(0);
    // Apparently this is good to call?
    C->initializePolyhedralFeatures();

    ConvexHullCollisionShapes.Add({
        BodySetup,
        ConvexIndex,
        Scale,
        C
    });

    return C;
}

OK, that’s all there is to do for static colliders!

Dynamic Rigid Body Objects

We’re using physics because we want some objects to move around, so let’s get on to dynamic objects, in our case rigid bodies (Bullet supports soft bodies too but I’m not getting into that).

In Bullet, btRigidBody is the class which is just a subclass of btCollisionObject which we already used for the static geometry. So in a lot of ways it’s much the same process, except:

  1. We need the objects to have a mass
  2. We may need compound colliders
  3. We need a way for the transform updates from Bullet to update UE transforms

Luckily though, we can re-use all that code from ExtractPhysicsGeometry but just process the results a little differently.

We’re going to cache collision info for our potentially complex rigid bodies by the FName of their class, so we don’t waste time parsing every instance of an Actor to convert it. It could be that an actor’s collision isn’t complex, just a sphere or a box, in which case bIsCompound will be false and this entry is just an alias to one of the simpler types (we’ll use that to make sure we don’t delete the shape twice).

In this case, we’re not just caching the shape, but also its Mass / Inertia. This is because calculating Interia (the distribution of mass around the shape) is potentially expensive to do for every Actor instance, so we might as well only do it once per Blueprint class. We store the mass it was derived from for convenience too.

Whenever we want this data, we call this function:

const APhysicsWorldActor::CachedDynamicShapeData& APhysicsWorldActor::GetCachedDynamicShapeData(AActor* Actor, float Mass)
{
    // We re-use compound shapes based on (leaf) BP class
    const FName ClassName = Actor->GetClass()->GetFName();
    for (auto&& Data: CachedDynamicShapes)
    {
        if (Data.ClassName == ClassName)
            return Data;
    }

    // Because we want to support compound colliders, we need to extract all colliders first before
    // constructing the final body.
    TArray<btCollisionShape*, TInlineAllocator<20>> Shapes;
    TArray<FTransform, TInlineAllocator<20>> ShapeRelXforms;
    ExtractPhysicsGeometry(Actor,
        [&Shapes, &ShapeRelXforms](btCollisionShape* Shape, const FTransform& RelTransform)
        {
            Shapes.Add(Shape);
            ShapeRelXforms.Add(RelTransform);
        });


    CachedDynamicShapeData ShapeData;
    ShapeData.ClassName = ClassName;
    
    // Single shape with no transform is simplest
    if (ShapeRelXforms.Num() == 1 &&
        ShapeRelXforms[0].EqualsNoScale(FTransform::Identity))
    {
        ShapeData.Shape = Shapes[0];
        // just to make sure we don't think we have to clean it up; simple shapes are already stored
        ShapeData.bIsCompound = false; 
    }
    else
    {
        // Compound or offset single shape; we will cache these by blueprint type
        btCompoundShape* CS = new btCompoundShape();
        for (int i = 0; i < Shapes.Num(); ++i)
        {
            // We don't use the actor origin when converting transform in this case since object space
            // Note that btCompoundShape doesn't free child shapes, which is fine since they're tracked separately
            CS->addChildShape(BulletHelpers::ToBt(ShapeRelXforms[i], FVector::ZeroVector), Shapes[i]);
        }

        ShapeData.Shape = CS;
        ShapeData.bIsCompound = true;
    }

    // Calculate Inertia
    ShapeData.Mass = Mass;
    ShapeData.Shape->calculateLocalInertia(Mass, ShapeData.Inertia);

    // Cache for future use
    CachedDynamicShapes.Add(ShapeData);

    return CachedDynamicShapes.Last();
    
}

You can see that if we don’t find an existing entry, we’re using the same ExtractPhysicsGeometry routine, but instead of immediately building colliders for each, we’re collecting them up in order to potentially make a compound collider out of them if needed. We cache this by the FName of the class for simple re-use.

Yes, we could have used a TMap<FName, ...> for this and other lookups. But I use plain arrays whenever I can, because they’re usually more efficient (thank you, cache) unless you have much larger collections than I have. If your collections are bigger, feel free to change it.

We then take the opportunity to calculate the inertia from the new shape. So that’s the shape done. Now we need to create a btRigidBody. This is the core of it:

btRigidBody* APhysicsWorldActor::AddRigidBody(AActor* Actor, const APhysicsWorldActor::CachedDynamicShapeData& ShapeData, float Friction, float Restitution)
{
    return AddRigidBody(Actor, ShapeData.Shape, ShapeData.Inertia, ShapeData.Mass, Friction, Restitution);
}
btRigidBody* APhysicsWorldActor::AddRigidBody(AActor* Actor, btCollisionShape* CollisionShape, btVector3 Inertia, float Mass, float Friction, float Restitution)
{
    
    auto Origin = GetActorLocation();
    auto MotionState = new BulletCustomMotionState(Actor, Origin);
    const btRigidBody::btRigidBodyConstructionInfo rbInfo(Mass, MotionState, CollisionShape, Inertia);
    btRigidBody* Body = new btRigidBody(rbInfo);
    Body->setUserPointer(Actor);
    BtWorld->addRigidBody(Body);
    BtRigidBodies.Add(Body);

    return Body;
    
}

It’s quite similar to AddStaticGeometry, but with the addition of a couple of things:

  • Inertia / Mass data, which we already calculated
  • A BulletCustomMotionState instance

Obviously, a dynamic physical object is going to move. The BulletCustomMotionState class is in charge of keeping the transforms in sync between Bullet and UE.

I’ll talk about that below when we talk about ticking the simulation.

Cleaning Up

We’ve created a whole bunch of things that we have to clean up at the end of the simulation, so let’s make sure we do that. Bullet doesn’t delete anything itself so we’re in charge of cleaning that up.

    for (int i = BtWorld->getNumCollisionObjects() - 1; i >= 0; i--)
    {
        btCollisionObject* obj = BtWorld->getCollisionObjectArray()[i];
        btRigidBody* body = btRigidBody::upcast(obj);
        if (body && body->getMotionState())
        {
            delete body->getMotionState();
        }
        BtWorld->removeCollisionObject(obj);
        delete obj;
    }
    
    // delete collision shapes
    for (int i = 0; i < BoxCollisionShapes.Num(); i++)
        delete BoxCollisionShapes[i];
    BoxCollisionShapes.Empty();
    for (int i = 0; i < SphereCollisionShapes.Num(); i++)
        delete SphereCollisionShapes[i];
    SphereCollisionShapes.Empty();
    for (int i = 0; i < CapsuleCollisionShapes.Num(); i++)
        delete CapsuleCollisionShapes[i];
    CapsuleCollisionShapes.Empty();
    for (int i = 0; i < ConvexHullCollisionShapes.Num(); i++)
        delete ConvexHullCollisionShapes[i].Shape;
    ConvexHullCollisionShapes.Empty();
    for (int i = 0; i < CachedDynamicShapes.Num(); i++)
    {
        // Only delete if this is a compound shape, otherwise it's an alias to other simple arrays
        if (CachedDynamicShapes[i].bIsCompound)
            delete CachedDynamicShapes[i].Shape;
    }
    CachedDynamicShapes.Empty();

    delete BtWorld;
    delete BtConstraintSolver;
    delete BtBroadphase;
    delete BtCollisionDispatcher;
    delete BtCollisionConfig;
    delete BtDebugDraw; // I haven't talked about this yet, later

    BtWorld = nullptr;
    BtConstraintSolver = nullptr;
    BtBroadphase = nullptr;
    BtCollisionDispatcher = nullptr;
    BtCollisionConfig = nullptr;
    BtDebugDraw = nullptr;

    // Clear our type-specific arrays (duplicate refs)
    BtStaticObjects.Empty();
    BtRigidBodies.Empty();

You’ll notice that I don’t delete the contents of BtStaticObjects or BtRigidBodies because they’re duplicates of the pointers in BtWorld->getCollisionObjectArray(). You might wonder why I have them; it’s actually so that I can delete the dynamic objects from the scene without rebuilding the static elements, effectively a reset. I’m not going to go over that here since it’s a long post already, but it’s a useful distinction to make in the lifecycle.

Making it move

Right now, we’ve created the world but we haven’t actually simulated anything. For our world to move, we have to ‘step’ the physics system, and then sync the updated transforms with our Unreal objects.

Stepping the world

First, let’s step the world. We’ll call this from the Tick method on our world actor. The simplest implementation is this:

void APhysicsWorldActor::StepPhysics(float DeltaSeconds)
{
    BtWorld->stepSimulation(DeltaSeconds, BtMaxSubSteps, 1./BtPhysicsFrequency);
}

This causes Bullet to advance the simulation by DeltaSeconds, at a fixed frequency of BtPhysicsFrequency. It will stop if the number of substeps exceeds BtMaxSubSteps, which just makes sure the simulation doesn’t run away with itself at low frame rates (the physics will just run in slow motion in this case, they’ll still be deterministic).

But what about the fragments of DeltaSeconds which don’t divide nicely into BtPhysicsFrequency? Well, unlike UE4, Bullet stores these fragments up for next time, and also uses them to predict motion for the fragment of the next step, which it passes to the MotionState for visual updates. This means that while the physics is always executing at a fixed rate, the visuals remain smooth. UE actually tells PhysX to step for this last variable fragment, which is a simpler way of keeping everything smooth, but completely breaks determinism. I much prefer Bullet’s way of doing things.

In my game, I actually take a slightly different approach and only let Bullet perform 1 step at a time, doing the sub-stepping myself around the call to stepSimulation. This is so that I can vary the physics frequency depending on changing requirements, while still staying deterministic. This is an extra complication that I’ll skip right now.

Updating transforms via MotionState

You’ll remember that when creating a btRigidbody instance, I also created an instance of BulletCustomMotionState to go with it. This little “glue” class is entirely responsible for updating the UE transforms based on Bullet simulation.

As mentioned above, Bullet is already smart enough to incorporate prediction into the transform updates it gives the MotionState class, so we don’t have to worry about doing anything but converting values. Also, the MotionState is only called when the rigid body is “awake”, meaning that sleeping objects that have come to rest have no overhead.

Here’s my implementation:

#pragma once

#include "BulletMinimal.h"
#include "BulletHelpers.h"
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"

/**
 * Customised MotionState which propagates motion to linked Actor & tracks when sleeping
 */
class MYPROJECT_API BulletCustomMotionState : public btMotionState
{
protected:
    TWeakObjectPtr<AActor> Parent;
    // Bullet is made local so that all sims are close to origin
    // This world origin must be in *UE dimensions*
    FVector WorldOrigin; 
    btTransform CenterOfMassTransform;


public:
    BulletCustomMotionState()
    {
        
    }
    BulletCustomMotionState(AActor* ParentActor, const FVector& WorldCentre, const btTransform& CenterOfMassOffset = btTransform::getIdentity())
        : Parent(ParentActor), WorldOrigin(WorldCentre), CenterOfMassTransform(CenterOfMassOffset)

    {
    }

    ///synchronizes world transform from UE to physics (typically only called at start)
    void getWorldTransform(btTransform& OutCenterOfMassWorldTrans) const override
    {
        if (Parent.IsValid())
        {
            auto&& Xform = Parent->GetActorTransform();
            OutCenterOfMassWorldTrans = BulletHelpers::ToBt(Parent->GetActorTransform(), WorldOrigin) * CenterOfMassTransform.inverse();
        }
    
    }

    ///synchronizes world transform from physics to UE
    void setWorldTransform(const btTransform& CenterOfMassWorldTrans) override
        // send this to actor
        if (Parent.IsValid(false))
        {
            btTransform GraphicTrans = CenterOfMassWorldTrans * CenterOfMassTransform;
            Parent->SetActorTransform(BulletHelpers::ToUE(GraphicTrans, WorldOrigin));
        }
    }
};

This is based on Bullet’s own template for custom MotionStates which includes a correction for the centre of mass, and includes our WorldOrigin offset which makes the sim local to the APhysicsWorldActor, meaning you can have the sim running far from the origin with the same precision as if it was located at (0,0,0). Other than that it’s just using the BulletHelpers to do simple scale conversion. (BulletHelpers are listed at the end of this post).

And that’s basically it! You now have a Bullet simulation running inside UE and moving UE objects around. It’s using all the colliders you define as normal so mesh / actor workflow is mostly the same. You just have to track your static and dynamic objects a bit more specifically, rather than just expecting them to work just by adding them to the scene (it’s up to you how you do this, you could reference them specifically like I do, or perform scene queries for tagged objects).

My simulation has the advantage of being tightly controlled, with persistent objects (I’m not spawning / destroying things much beyond the initial setup), you might have to do more work if your sim has a lot of object churn.

Debug Renderering

It’s very useful for debugging to see what Bullet thinks the world looks like. Luckily, Bullet provides a simple interface you can override to make this happen, here’s my UE version:

#pragma once
#include "CoreMinimal.h"
#include "BulletMinimal.h"

class BulletDebugDraw : public btIDebugDraw
{
protected:
    UWorld* World;
    FVector WorldOrigin;
    int DebugMode;
public:
    BulletDebugDraw(UWorld* world, const FVector& worldOrigin);
    
    void drawLine(const btVector3& from, const btVector3& to, const btVector3& color) override;

    void drawContactPoint(const btVector3& PointOnB, const btVector3& normalOnB, btScalar distance, int lifeTime,
        const btVector3& color) override;
    void reportErrorWarning(const char* warningString) override;
    void draw3dText(const btVector3& location, const char* textString) override;
    void setDebugMode(int debugMode) override;
    int getDebugMode() const override;
};
#include "BulletDebugDraw.h"
#include "BulletHelpers.h"
#include "DrawDebugHelpers.h"

BulletDebugDraw::BulletDebugDraw(UWorld* world, const FVector& worldOrigin)
    : World(world), WorldOrigin(worldOrigin), DebugMode(btIDebugDraw::DBG_DrawWireframe)
{
}

void BulletDebugDraw::drawLine(const btVector3& from, const btVector3& to, const btVector3& color)
{
    DrawDebugLine(World, 
        BulletHelpers::ToUEPos(from, WorldOrigin),
        BulletHelpers::ToUEPos(to, WorldOrigin),
        BulletHelpers::ToUEColour(color));
}

void BulletDebugDraw::drawContactPoint(const btVector3& PointOnB, const btVector3& normalOnB, btScalar distance,
    int lifeTime, const btVector3& color)
{
    drawLine(PointOnB, PointOnB + normalOnB * distance, color);
    btVector3 ncolor(1, 0, 0);
    drawLine(PointOnB, PointOnB + normalOnB * 0.01, ncolor);
    
}

void BulletDebugDraw::reportErrorWarning(const char* warningString)
{
    UE_LOG(LogTemp, Warning, TEXT("BulletDebugDraw: %hs"), warningString);
}

void BulletDebugDraw::draw3dText(const btVector3& location, const char* textString)
{
}

void BulletDebugDraw::setDebugMode(int debugMode)
{
    DebugMode = debugMode;
}

int BulletDebugDraw::getDebugMode() const
{
    return DebugMode;
}

To use this class, you need to tell the Bullet world instance about it during the initial setup:

    // set up debug rendering
    BtDebugDraw = new BulletDebugDraw(GetWorld(), GetActorLocation());
    BtWorld->setDebugDrawer(BtDebugDraw);

Then, add something like this to your StepPhysics implementation (called by Tick):

#if WITH_EDITORONLY_DATA
    if (bPhysicsShowDebug)
        BtWorld->debugDrawWorld();
#endif

I just have that bPhysicsShowDebug flag exposed as a UPROPERTY so I can flip it on/off to display Bullet debug information. Very handy!

Turning off UE physics

A quick aside to note that you’re going to need to remember to make sure the “Simulate Physics” option on objects controlled by Bullet is disabled. You don’t want PhysX and Bullet to fight!

It’s still useful to have PhysX colliders on your objects, because it means that things like overlapping triggers, and traces, continue to work as before even though none of that is going through Bullet. It’s slightly wasteful to have 2 systems operating in parallel, but it keeps things far simpler.

The End (Almost)

I hope someone finds this useful - I realise it’s a bit of a “code dump” and maybe you would have preferred a nicely maintained open source plugin, but honestly I don’t really have time for that, sorry.

And anyway, as soon as something’s a library people expect it to be general purpose, which this is not. It’s a specific solution to a specific problem, and requires some understanding of the situations it’s applicable to. So the self-assembly presentation is intentional 😉

If you like this post or have a question, you can hit me up on Twitter.

Appendix: BulletHelpers class

#pragma once
#include "BulletMinimal.h"
// Bullet scale is 1=1m, UE is 1=1cm
// So x100
#define BULLET_TO_WORLD_SCALE 100.f
#define WORLD_TO_BULLET_SCALE (1.f/BULLET_TO_WORLD_SCALE)

/**
 * 
 */
class MYPROJECT_API BulletHelpers
{
    
public:
    static float ToUESize(btScalar Sz)
    {
        return Sz * BULLET_TO_WORLD_SCALE;
    }
    static btScalar ToBtSize(float Sz)
    {
        return Sz * WORLD_TO_BULLET_SCALE;
    }
    static btVector3 ToBtSize(FVector Sv)
    {
        // For clarity; this is for box sizes so no offset
        return ToBtDir(Sv);
    }
    static FVector ToUEPos(const btVector3& V, const FVector& WorldOrigin)
    {
        return FVector(V.x(), V.y(), V.z()) * BULLET_TO_WORLD_SCALE + WorldOrigin;
    }
    static btVector3 ToBtPos(const FVector& V, const FVector& WorldOrigin)
    {
        return btVector3(V.X - WorldOrigin.X, V.Y - WorldOrigin.Y, V.Z - WorldOrigin.Z) * WORLD_TO_BULLET_SCALE;
    }
    static FVector ToUEDir(const btVector3& V, bool AdjustScale = true)
    {
        if (AdjustScale)
            return FVector(V.x(), V.y(), V.z()) * BULLET_TO_WORLD_SCALE;
        else
            return FVector(V.x(), V.y(), V.z());
    }
    static btVector3 ToBtDir(const FVector& V, bool AdjustScale = true)
    {
        if (AdjustScale)
            return btVector3(V.X, V.Y, V.Z) * WORLD_TO_BULLET_SCALE;
        else
            return btVector3(V.X, V.Y, V.Z);
    }
    static FQuat ToUE(const btQuaternion& Q)
    {
        return FQuat(Q.x(), Q.y(), Q.z(), Q.w());
    }
    static btQuaternion ToBt(const FQuat& Q)
    {
        return btQuaternion(Q.X, Q.Y, Q.Z, Q.W);
    }
    static btQuaternion ToBt(const FRotator& r)
    {
        return ToBt(r.Quaternion());
    }
    static FColor ToUEColour(const btVector3& C)
    {
        return FLinearColor(C.x(), C.y(), C.z()).ToFColor(true);
    }

    static FTransform ToUE(const btTransform& T, const FVector& WorldOrigin)
    {
        const FQuat Rot = ToUE(T.getRotation());
        const FVector Pos = ToUEPos(T.getOrigin(), WorldOrigin);
        return FTransform(Rot, Pos);
    }
    static btTransform ToBt(const FTransform& T, const FVector& WorldOrigin)
    {
        return btTransform(
            ToBt(T.GetRotation()), 
            ToBtPos(T.GetLocation(), WorldOrigin));
    }
};