Bullet Physics
|
A great way to understand bullet physics is through it's examples, I highly recommend building the project directly and running the example browser as you'll be able to look at the source code and then see what it produces.
The "Basic Example"'s source code is located at /examples/BasicDemo/BasicExample.cpp, let's list the contents here for simplicity
There are two main layers of understanding that need to happen here, firstly we can see that this example has specific code for it's own use like when it creates it's own box collision shape, but also there seems to be many helper functions being used here to make the creation of new examples quite simple.
You'll notice that in most of the example code you'll be able to learn about how to set up ceratin situations directly relating to bullet.
The example browser is the best way to see how bullet works and what it's capable of, to run the example browser you should be familiar with building bullet directly which you can read about in the building section.
To operate the example browser simply select the example you're interested in in the left panel.
While reading the examples directly will teach you a lot about how bullet works in isolation, learning about how bullet is integrated into real code is another big takeaway from these examples. So we'll start by looking at this.
Before we get started, crack out the source code and be ready to follow along. I find the best way to learn a complex system is to start by running the code ourselves (as if we were running it) and seeing what this execution path interacts with along the way. By picking a good start point you should be able to get a full end to end view of the system.
First off, you'll probably have noticed m_guiHelper
hanging around a bit, it's used at the start to generate the axis, it sets up the toggleable debug drawer in the example browser, and finally auto generates graphics objects before the as a part of the physics initialization method.
You'll be able to locate most of these helpers through the examples/CommonInterfaces directory in the bullet library, and specificially in that directory you can find CommonGUIHelperInterface.h
which provides an interface for a gui helper.
If you hadn't recognized yet, the example browser uses opengl to render the examples, and since we've seen that this gui helper has the ability to auto generate graphics objects, it would make sense if it the implementation of the gui helper would be in some opengl related folder, and of course it is. You can locate it at /ExampleBrowser/OpenGLGuiHelper.cpp
.
Reading through the implementation of autogenerateGraphicsObjects
we find that it iterates through a list of objects calling createCollisionObjectGraphicsObject
so we go to it's implementation and see that a few checks are made and then eventually a pretty big call is made to registerGraphicsInstance
passing along the id of this shape, it's initial position, rotation, color and scaling. The true call is actually quite lengthy and looks something like this:
When first seeing this call, we can tell that we've hit something important because it seems to go through many objects, and talk to the renderer. We'll first start with what m_glApp
is.
We can find out that it's type is CommonGraphicsApp
, and one of the implementations of it is in OpenGLWindow/SimpleOpenGL3App.cpp
, here we can see that when it is instantiated m_renderer
is set to a new GLInstancingRenderer
object, which is defined in OpenGLWindow/GLInstancingRenderer.cpp
.
At this point we can see the definition of registerGraphicsInstance
which was our goal. Looking in it's definition we can see that it simply stores the data passed in into m_data
which has the type InternalDataRenderer
, this internal data has individual pointers for the positional, rotational, color and scale information so that opengl can use it in the VAO.
This file is actually quite important when it comes to rendering, if we go to the top of the file you can see the vertex and fragment shader source code, additionally it provides various drawing functionalities which take in information and draw it with opengl. Usually this ends up looking like binding any texture samplers using the shader program, binding any matrix uniforms into the shader as well as colors attributes, then the vertex array object is bound and then glDrawArrays
is used to finally draw the information, after all that is done the vertex array object is unbound.
At this point we've dug through a few files, and we are getting a fuller understanding of how this system works. One thing that isn't quite clear is that in our original BasicExample.cpp
file, we saw nothing to do with the rendering, it simply set up the data and never did anything with it. That tells us that the rendering is being abstracted away somewhere, we do know that our rendering uses opengl, and that that is an iterative animation system so that there will most likely be a main loop calling a function, which eventually updates the graphics.
Following this thread we can take a look at ExampleBrowser/main.cpp
which we can assume is the main entry point into the example browser. Inside of this file, we can see that an instance of the class OpenGLExampleBrowser
is instantiated, then a bit later we find a do while loop that calls update on this example browser instance. This seems promising so we can take a look inside of ExampleBrowser/OpenGLExampleBrowser.cpp
.
Inside of this file we can see the definition of the update method which is a bit of a monster, and could do with a refactor for sure. The main logic is that it verifies what kind of simulation is being run and then based on that it updates the graphics accordingly.
So long as we have loaded up a demo, there are a few options, which are if the simulation is currently paused, if it is a single step simulation (meaning that we only step the simulation for one iteration), if the simulation used a fixed or varying time step, all in all this stage determines how the simulation should be stepped.
Then if we are going to be rendering visual geometry we then call renderScene
on the current demo, this rendering component should be what we are after.
The variable sCurrentDemo
is initialized through this line
This line resides in the function void openFileDemo(const char* filename)
, which is called through a callback when a user selects a demo through the gui, and this function iterates the index i until:
Becomes true, and then calls the previous line I mentioned. The call to m_createFunc
is imporant, looking at any example and specifically BasicExample.cpp
we can see that this call simply creates and returns a new object BasicExample
that inherits from the CommonExampleInterface
. It can be thought of as a constructor method.
This completes the aside.
Back to rendering, we know that the following call is made: sCurrentDemo->updateGraphics();
, but doing a recursive search ththrough the code base only told that the PhysicsServerExample
has an implementation. This means that most likely the instancing renderer is automatically doing the rendering itself.
Before we continue with the rendering it's about time we actually crack down on what an instancing renderer is as it keeps coming up, I highly recommend you read learnopengl.com's highly detailed explanation on the topic here: https://learnopengl.com/Advanced-OpenGL/Instancing.
To boil it down quickly, when we render stuff with opengl for a small collection of object we usually just iterate through the objects, each time binding their respective VAO's textures and uniforms before calling an opengl draw method. Even if all the object were the same (perhaps a basic cube), we would still need to do this for each one, this will eventually case a performance bottleneck because of how many draw calls there are as the number of objects increases.
So long as we wish to draw the same object, but with different transformations, we can use glDrawArraysInstanced, which allows us to use the object once, along with a instance count and we can then use this as an index to change the positions of the objects in the shader, this makes it so that we only need one draw call and makes the rendering much faster.
With that out of the way, recall that the instancing renderer is located at OpenGLWindow/GLInstancingRenderer.cpp
, here we can find renderScene
and renderSceneInternal
the latter of which does all the heavy lifting. It can roughly be broken down into the following sections
glBindVertexArray(gfxObj->m_cube_vao);
4.4. Set up vertex attribute pointers for vertex positions (of the collision shape), position of each instance (or just 1 if one instance), rotations for each instance, uvs, normals, colors of each instance, scale of each instance. 4.5. Enable vertex attribute arrays and bind the indices for the current object's vertices into the VAOglDrawElementsInstanced(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, indexOffset, gfxObj->m_numGraphicsInstances);
to draw all the instances of this object at once.Note that there is no model matrix required because when we use instancing we draw the same object in different locations by passing an buffer of positions to opengl.
The way the example browser works is that it defines a common interface for all examples, it has a renderer interface and by defualt it uses the opengl renderer with instanced rendering. The examples create bullet physics objects and then call glDrawElementsInstanced(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, indexOffset, gfxObj->m_numGraphicsInstances);
which takes all the collision shapes in the current bullet physics simulation and for each one it converts it into a form that is ready for opengl to use.
Then in the main function of the example browser there is a loop that calls update on the example browser which eventually renders everything that was created through the call to autogenerateGraphicsObjects
.
The main methods you should check out would be
void OpenGLGuiHelper::autogenerateGraphicsObjects(btDiscreteDynamicsWorld* rbWorld)
void OpenGLGuiHelper::createCollisionShapeGraphicsObject(btCollisionShape* collisionShape)
int OpenGLGuiHelper::registerGraphicsShape(const float* vertices, int numvertices, const int* indices, int numIndices, int primitiveType, int textureId)
int GLInstancingRenderer::registerShape(const float* vertices, int numvertices, const int* indices, int numIndices, int primitiveType, int textureId)
line 1162 in examples/OpenGLWindow/GLInstancingRenderer.cppIn the call to void OpenGLGuiHelper::createCollisionShapeGraphicsObject(btCollisionShape* collisionShape)
we can see that if the passed in object is a cube that a standard cube is loaded in through cube_vertices_textured
, using a recursive grep search we can see it's defined here /OpenGLWindow/ShapeData.h
on line 475, you'll find that these shapes are stored as 1d arrays, but follow a certain ordering based on what extra information is stored in the array such as normals and texture coordinates. From this comment we can see that the objects data is not pulled directly from the collision shape created in bullet but is simply using vertices from a constants file.