1
\$\begingroup\$

I've been working on a game using Bullet Physics for C++ as well as SDL for the windowing (with OpenGL)

I've recently threaded the game, so that a separate while loop runs the Physics Simulation, whilst the rendering runs independently (Right now I've got a Car driving around a Heightfield, which worked perfectly without jitter, prior to thread it)

The reason was, because I wanted to de-coupled physics simulation with rendering (So low FPS could still have the same Physics simulation speed, which would inevitably cause choppiness)

Problem: at FPS greater, or lower than 60 FPS (pretty much if it isn't synced with the 60 Hz Physics Timestep) there will be jittering of the vehicle, and physics objects in general...

The btTransform = vehiclePhysics.GetTransform() is the Vehicle's motion state. Right now it's a btDefaultMotionState, so this would deal with some bit of interpolation, but below 50 fps, it's still very jittery.

When I activate the lerpVec3 transformation on my Camera's lookAt, the vehicle at low FPS jitters less, but the entire scene starts to jitter. And when I don't the scene is LERP'd and camera movement is smooth, but the vehicle jitters, and the Debug Drawing of the vehicle also jitters, so it's clearly the transform of the vehicle or in general, physics objects that are wonky due to the threading...

Render Thread Code (essentials)

This is the code within my rendering loop (main thread) that will get the transforms from the shared Resource, and get it ready for rendering (As well as Debug Drawing code for BulletDebugDrawer)

  vehiclePhysicsInfo vI = sharedPhysicsRessource.GetVehiclePhyInfo();
    btTransform vehicleTransform = vI.transform;

    if (isFirstUpdate) {
        lastVehicleTransform = vehicleTransform;
        isFirstUpdate = false;
    }
    
    btVector3 vehiclePosition = vehicleTransform.getOrigin();
    btVector3 lastVehiclePosition = lastVehicleTransform.getOrigin();
    
//... Redacted trials at interpolation (On or off, I still get the problem, so it's useless anyways)

//Tried manual LERP & SLERP interpolations for positional and rotational data, and it's togglable with the If stmt below, still doesn't change much at all..

    if(interpolated_transforms){
      vehiclePosition = interpolatedPosition;
      vehicleRotation = interpolatedRotation;
    }

    glm::quat glmVehicleRotation = glm::quat(vehicleRotation.w(), vehicleRotation.x(), vehicleRotation.y(), vehicleRotation.z());
    if(interpolated_transforms){
        glmVehicleRotation = glm::quat(interpolatedRotation.w(), interpolatedRotation.x(), interpolatedRotation.y(), interpolatedRotation.z());
    }


    //Position Translation
    glm::mat4 translation = glm::translate(glm::mat4(1.0f), glm::vec3(vehiclePosition.x(), vehiclePosition.y(), vehiclePosition.z()));

    glm::mat4 rotation = glm::mat4_cast(glmVehicleRotation);
    glm::mat4 rotate90DEG_Adjustment = glm::rotate(glm::mat4(1.0f), glm::radians(-90.0f), glm::vec3(0.0f, 1.0f, 0.0f));

    vehicleModelMatrix = translation * rotation * rotate90DEG_Adjustment * glm::scale(glm::vec3(0.75f));
    // WheelMatrix = translation * glm::scale(glm::vec3(0.25f));

    //! PROTOTYPING: VEHICLE RENDERING CODE

    // Specify the color of the background
    glClearColor(0.07f, 0.13f, 0.17f, 1.0f);
    
    // Clean the back buffer and depth buffer
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    // Tell OpenGL which Shader Program we want to use
    shaderProgram.Activate();

    // Assigns a value to the uniform; NOTE: Must always be done after activating the Shader Program
    // brickTex.Bind();
    glUniform1i(useTextureLocation, GL_FALSE); 
    //debugDrawer->setDebugMode(btIDebugDraw::DBG_DrawWireframe);
    
    if(show_debug_draw) {

        physicsWorldPTR->debugDrawWorld();
        debugDrawer->flushLines();


    }
    glUniform1i(useTextureLocation, GL_TRUE); 

  //* ###### CAMERA #######

    camera.DEBUG = false;

    if(!camera.DEBUG) {
    
      auto targetVec = glm::vec3(vehiclePosition.x() + 1.0f, vehiclePosition.y() + 3.0f, vehiclePosition.z() - 5.0f); //* Camera Offset
      auto dirVec = targetVec - camera.Position;
      
      if (glm::distance2(targetVec, camera.Position) > 0.02f)
        camera.Position += dirVec * 0.03f;

      glm::vec3 lookAtPosition = glm::vec3(vehiclePosition.x(), vehiclePosition.y(), vehiclePosition.z());
      
      //camera.LookAt = lookAtPosition; //Naive Approach

      camera.LookAt = lerpVec3(camera.LookAt, lookAtPosition, 0.35f);
    
    } else { camera.Inputs(Window); }

    camera.Matrix(45.0f, 0.1f, 1000.0f, shaderProgram, "camMatrix"); //! IMPORTANT

  //*############## OpenGL - Draw Calls ################

This is the Physics Thread Code (essentials)

auto lastTime = std::chrono::high_resolution_clock::now();
    double accumulator = 0.0;
    const double physicsTimestep = 1.00 / 60.0;

    if (debugDrawer) {
            physicsWorld->dynamicsWorld->setDebugDrawer(debugDrawer);
            sharedRSRC->UpdatePhysicsWorld(physicsWorld->dynamicsWorld);
    }

    while (running) {
        
        auto currentTime = std::chrono::high_resolution_clock::now();
        double frameTime = std::chrono::duration_cast<std::chrono::duration<double>>(currentTime - lastTime).count();
        lastTime = currentTime;
        accumulator += frameTime;

        uint8_t input;
        while (playerInputQueue.TryPop(input)) {
            //... input handing, works fine (Thread Safe Queue)
          
            // Process and print the input command
        }

        while (accumulator >= physicsTimestep) {

            // Step the physics simulation
            physicsWorld->dynamicsWorld->stepSimulation(physicsTimestep, 2, physicsTimestep);

            // Get the latest vehicle transform and update shared resource
            btTransform vehicleTransform = vehiclePhysics.GetTransform();
            if (sharedRSRC) {
                
                vehiclePhysicsInfo vInfo;
                
                vInfo.transform = vehicleTransform;
                
                for (int i = 0; i < vehiclePhysics.vehicle->getNumWheels(); i++)
                {
                btWheelInfo wheelinfo = vehiclePhysics.vehicle->getWheelInfo(i);
                vInfo.wheelVec.push_back(wheelinfo);
                }

                sharedRSRC->UpdateVehiclePhyInfo(vInfo);
                sharedRSRC->SwapBuffers();
            }

            // Decrease the accumulated time
            accumulator -= physicsTimestep;
        }

        btVector3 vehiclePosition = vehiclePhysics.GetTransform().getOrigin();
        
        //player positions (vehicle)
        
        btScalar pXpos = vehiclePosition.getX();
        btScalar pYpos = vehiclePosition.getY();
        btScalar pZpos = vehiclePosition.getZ();

        terrainChunkManager.update(pXpos, pZpos);
        
        
        // std::cout << "Thread is running..." << std::endl;
        // Sleep or wait logic can be added here to control update rate
    }

And here is the shared resource, used to communicate between threads, basic header file, and these functions have Thread_guards that lock and update mutex

class SharedPhysicsResource {

public:
    void UpdateVehiclePhyInfo(const vehiclePhysicsInfo& data);
    vehiclePhysicsInfo GetVehiclePhyInfo();

    void SwapBuffers();

    void UpdatePhysicsWorld(btDiscreteDynamicsWorld* physicsWorld);
    btDiscreteDynamicsWorld* getPhysicsWorld();

private:
    vehiclePhysicsInfo playerVehicleBuffer[2];
    int rI = 0; //Read 
    int wI = 1; //Write

    std::mutex mutex_;
    btDiscreteDynamicsWorld* physicsWorld_;
};

I've tried:

  • Manual linear interpolation (and spherical as well for rotations)
  • Using btMotionState instead of getting the RigidBody transforms (This helps a lot for when the FPS is a constant 60)
  • Double buffering the shared resource that contains the btTransform for the rendering of Vehicle
  • Unsuccessfully tried to build my own btMotionState, but I don't think this is the problem, as at 60 FPS, the default motion state works great

I've briefly looked at some other Bullet setups and they use different different constraint solvers & generally other components that seem inherently multi-threaded.

For instance, here's my dynamics world setup (that I'm running in an independent thread)

PhysicsWorldSingleton::PhysicsWorldSingleton() {
    
    broadphase = new btDbvtBroadphase(); //uses QuadTree Data Structure to minimize expensive collision checking

    collisionConfiguration = new btDefaultCollisionConfiguration();
    dispatcher = new btCollisionDispatcher(collisionConfiguration);

    solver = new btSequentialImpulseConstraintSolver();
    
    dynamicsWorld = new btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration);
    dynamicsWorld->setGravity(btVector3(0.0f, -13.81f , 0.0f));

};

I'd be very grateful to hear any advise, wisdom, or things I might potentially be doing wrong with my setup, as I know games that fluctuate between 30-144 FPS, with smooth physics simulation...? Also please let me know if you need clarification on anything, or additional code snippets

\$\endgroup\$

2 Answers 2

0
+100
\$\begingroup\$

"The final touch" segment from this blogpost seems to address this.

You've tried interpolation, but since I don't see any reference to the "accumulator" variable in the rendering code or in the shared resources, I gather you didn't interpolate using that specific variable - which is required for the accurate desirable interpolation.

From the blog post:

Any remainder in the accumulator is effectively a measure of just how much more time is required before another whole physics step can be taken. For example, a remainder of dt/2 means that we are currently halfway between the current physics step and the next. A remainder of dt*0.1 means that the update is 1/10th of the way between the current and the next state. We can use this remainder value to get a blending factor between the previous and current physics state simply by dividing by dt. This gives an alpha value in the range [0,1] which is used to perform a linear interpolation between the two physics states to get the current state to render.

So, my suggestion is:

  • Add the accumulator to the shared resources.
  • Read it in the render thread.
  • Divide it by the physics timestep.
  • Use the result as the interpolation value between the previous physics state and the current one.

Hope this helped!

\$\endgroup\$
0
\$\begingroup\$

consider this as a naive answer from a non bullet specialist,just some thoughts about the process:

Physics

Firstly, physical simulation should not be impacted by multi threading, if it worked before, no reasons it doesn't anymore. If it is not the case, perhaps consider rendering physics in your physic thread again to debug it.

Thread of the the physical simulation should work on its own. It should do the job, then copy(or swap if possible) its last simulated state to share : so we get:

LastSimulatedState at LastSimulated_t with lastSimulatedMotion as speed vector

available for the render thread.

Rendering at time t = now()

Obviously, Rendering should not lock the Simulation thread

Rendering world is in the state of the last rendering, say LastRenderState The new wanted rendering state should be the LastSimulatedState extrapolated by physical motion to (t - LastSimulated_t)

ExtrapolatedRenderState = LastSimulatedState + (t - LastSimulated_t) * lastSimulatedMotion

Render it like, would probably cause jittering, (extrapolation problem)

So you'd have to interpolate from your lastRenderState to ExtrapolatedRenderState by a ratio:

NewRenderState = LERP(LastRenderState,ExtrapolatedRenderState,ratio)

You would never render the physical world but would constantly try to approach it gently.

Hope this helps, if it is not just, trash it ;)

\$\endgroup\$
1
  • \$\begingroup\$ I cannot comment the accepted answer but, I think using accumulator as interpolation ratio for the rendering would cause some unwanted effects. If Physics take some time, accumulator would not change between two render loops, You'll get a several render frames identical. In my opinion, that uses the last physical loop time as rendering time (t) which, in some situations, could be quite differents. Try to put a sleep in your physical loop to simulate computation... like 100ms to check if your render isn't stuck at 10fps \$\endgroup\$
    – Daes
    Feb 7 at 13:41

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .