// OpenGL + Bullet integration for a pendulum simulation using GLFW and GLAD
// Requires: Bullet (static libs in bullet_dev_install), GLFW, GLAD, GLM
#include "./../libs/include/glad/glad.h"
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <btBulletDynamicsCommon.h>
#include <iostream>
#include <vector>
#include <fstream>
#include <sstream>
// Global Bullet variables
btDiscreteDynamicsWorld* dynamicsWorld;
btRigidBody* pendulumBob;
btHingeConstraint* hinge;
// OpenGL sphere
GLuint sphereVAO = 0, sphereVBO, sphereEBO;
int indexCount = 0;
GLuint shaderProgram;
// === Shader Loading ===
std::string readFile(const char* path) {
std::ifstream file(path);
std::stringstream ss;
ss << file.rdbuf();
return ss.str();
}
GLuint loadShaders(const char* vPath, const char* fPath) {
std::string vertCode = readFile(vPath);
std::string fragCode = readFile(fPath);
const char* vShaderCode = vertCode.c_str();
const char* fShaderCode = fragCode.c_str();
GLuint vShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vShader, 1, &vShaderCode, NULL);
glCompileShader(vShader);
GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fShader, 1, &fShaderCode, NULL);
glCompileShader(fShader);
GLuint prog = glCreateProgram();
glAttachShader(prog, vShader);
glAttachShader(prog, fShader);
glLinkProgram(prog);
glDeleteShader(vShader);
glDeleteShader(fShader);
return prog;
}
// === Sphere Mesh ===
void createSphereMesh(int stacks = 18, int sectors = 36) {
std::vector<float> vertices;
std::vector<unsigned int> indices;
for (int i = 0; i <= stacks; ++i) {
float stackAngle = glm::pi<float>() / 2 - i * glm::pi<float>() / stacks;
float xy = cos(stackAngle);
float z = sin(stackAngle);
for (int j = 0; j <= sectors; ++j) {
float sectorAngle = j * 2 * glm::pi<float>() / sectors;
float x = xy * cos(sectorAngle);
float y = xy * sin(sectorAngle);
vertices.insert(vertices.end(), { x, y, z });
}
}
for (int i = 0; i < stacks; ++i) {
int k1 = i * (sectors + 1);
int k2 = k1 + sectors + 1;
for (int j = 0; j < sectors; ++j, ++k1, ++k2) {
if (i != 0)
indices.insert(indices.end(), { k1, k2, k1 + 1 }); // ✅ fixed
if (i != (stacks - 1))
indices.insert(indices.end(), { k1 + 1, k2, k2 + 1 }); // ✅ fixed
}
}
indexCount = indices.size();
glGenVertexArrays(1, &sphereVAO);
glGenBuffers(1, &sphereVBO);
glGenBuffers(1, &sphereEBO);
glBindVertexArray(sphereVAO);
glBindBuffer(GL_ARRAY_BUFFER, sphereVBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, sphereEBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), indices.data(), GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
}
// === Render Function ===
void renderSphere(glm::vec3 position,float scale) {
glm::mat4 model = glm::translate(glm::mat4(1.0f), position);
model = glm::scale(model, glm::vec3(scale));
glUseProgram(shaderProgram);
glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "model"), 1, GL_FALSE, &model[0][0]);
glBindVertexArray(sphereVAO);
glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0);
}
// === Bullet Initialization ===
void initBullet() {
auto* broadphase = new btDbvtBroadphase();
auto* config = new btDefaultCollisionConfiguration();
auto* dispatcher = new btCollisionDispatcher(config);
auto* solver = new btSequentialImpulseConstraintSolver();
dynamicsWorld = new btDiscreteDynamicsWorld(dispatcher, broadphase, solver, config);
dynamicsWorld->setGravity(btVector3(0, -10, 0));
auto* shape = new btSphereShape(1.0);
btTransform start;
start.setIdentity();
start.setOrigin(btVector3(5, -5, 0)); // give offset from hinge point
btVector3 inertia(0, 0, 0);
shape->calculateLocalInertia(1.0, inertia);
auto* motionState = new btDefaultMotionState(start);
btRigidBody::btRigidBodyConstructionInfo info(1.0, motionState, shape, inertia);
pendulumBob = new btRigidBody(info);
dynamicsWorld->addRigidBody(pendulumBob);
btVector3 pivot(0, 0, 0);
btVector3 pivotInA = pendulumBob->getCenterOfMassTransform().inverse() * pivot;
btVector3 axisInA(0, 0, 1);
hinge = new btHingeConstraint(*pendulumBob, pivotInA, axisInA);
dynamicsWorld->addConstraint(hinge);
}
void drawLine(glm::vec3 a, glm::vec3 b) {
GLfloat vertices[] = {
b.x, b.y, b.z,
a.x, a.y, a.z
};
GLuint VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_DYNAMIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
glEnableVertexAttribArray(0);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_LINES, 0, 2);
glDeleteBuffers(1, &VBO);
glDeleteVertexArrays(1, &VAO);
}
// === Main ===
int main() {
// GLFW Init
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
GLFWwindow* window = glfwCreateWindow(800, 600, "Bullet Pendulum", NULL, NULL);
glfwMakeContextCurrent(window);
gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f); // dark background
glEnable(GL_DEPTH_TEST);
shaderProgram = loadShaders("shaders/vertex.glsl", "shaders/fragment.glsl");
createSphereMesh();
initBullet();
//glm::mat4 projection = glm::perspective(glm::radians(45.0f), 800.0f/600.0f, 0.1f, 100.0f);
//glm::mat4 view = glm::lookAt(glm::vec3(0, 0, 20), glm::vec3(0, 0, 0), glm::vec3(0, 1, 0));
glm::mat4 view = glm::lookAt(glm::vec3(0, 5, 20), glm::vec3(0, 0, 0), glm::vec3(0, 1, 0));
glm::mat4 projection = glm::perspective(glm::radians(50.0f), 800.0f/600.0f, 0.1f, 100.0f);
while (!glfwWindowShouldClose(window)) {
dynamicsWorld->stepSimulation(1.f/60.f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(shaderProgram);
glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "view"), 1, GL_FALSE, &view[0][0]);
glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "projection"), 1, GL_FALSE, &projection[0][0]);
btTransform trans;
pendulumBob->getMotionState()->getWorldTransform(trans);
btVector3 pos = trans.getOrigin();
glm::vec3 bobPos(pos.getX(), pos.getY(), pos.getZ());
// draw the pendulum bob
renderSphere(bobPos,1.f);
// draw rope from hinge (0,0,0) to bob
drawLine(glm::vec3(0.0f, 0.0f, 0.0f), -bobPos); // hinge to bob
renderSphere(glm::vec3(0, 0, 0),0.2f); // draw a tiny sphere at the hinge
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
The Vertex Shader code (Vertex.glsl)
#version 330 core
layout(location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
The Fragment Shader code (fragment.glsl)
#version 330 core
out vec4 FragColor;
void main() {
FragColor = vec4(1.0, 0.8, 0.2, 1.0); // Yellowish sphere
}
And this is how it looks like...