6. The Particle Playground — Graphics Edition
This version of the demo stays intentionally small. It adds just enough rendering infrastructure to make the simulation visible, while keeping the focus on data flow, GPU-friendly layout, and the connection between simulation and graphics.
What This Article Adds
1. A Renderer Module
The renderer owns the OpenGL objects needed to draw particles. It manages the vertex array, uploads particle data into buffers, and issues the draw call that turns simulation positions into visible points.
class particle_renderer {
// OpenGL handles for the vertex array object and vertex buffer objects
GLuint vao;
// VBO for particle positions
GLuint vbo;
// VBO for particle colors
GLuint colors_buffer;
public:
particle_renderer() = default;
~particle_renderer();
// Initialize the particle renderer for the given number of particles
// This function will create the necessary OpenGL buffers and shaders,
// and upload the initial particle colors to the GPU
void init(size_t num_particles, btm::fast_vec<btm::glvec3>& colors);
inline void draw_particles(btm::fast_vec<btm::glvec3>& positions);
};
2. A Camera Module
The camera comes from btm_framework and is defined in camera.h. It provides the view and projection matrices that place the particle system in the scene and convert world-space positions into screen-space coordinates.
class gl_camera {
btm::fvec3 location;
btm::fvec3 target;
btm::fvec3 up;
float fov;
float nearPlane;
float farPlane;
float aspect;
int width, height;
int left = 0;
int bottom = 0; ///< Viewport position within the window.
public:
gl_camera() = default;
gl_camera(const btm::fvec3& _location, const btm::fvec3& _target, const btm::fvec3& _up = btm::fvec3(0, 1, 0));
void set_aspect(int _width, int _height);
void setup(const btm::fvec3& _location, const btm::fvec3& _target, const btm::fvec3& _up = btm::fvec3(0, 1, 0));
void set_position(const btm::fvec3& _location);
void set_target(const btm::fvec3& _target);
void set_up(const btm::fvec3& _up);
void set_fov(float fov_in_radians);
void set_viewport(int _left, int _bottom, int _width, int _height);
void set_depth_range(float nearP, float farP);
btm::fmat4 projection_matrix();
btm::fmat4 view_matrix();
btm::fmat4 perspective();
void set_viewport();
void apply(gl_shader* shdr);
};
3. A Shader Program
The shader program also comes from btm_framework, where it is defined in shaders.h. The program loads the vertex and fragment shader files, compiles them, and prepares the GPU program used to render each particle.
g_shader->add_file(GL_VERTEX_SHADER, "resources/shaders/particle_VertexShader.glsl");
g_shader->add_file(GL_FRAGMENT_SHADER, "resources/shaders/particle_FragmentShader.glsl");
g_shader->load();
4. Main Loop Integration
The main loop connects the simulation and renderer. Each frame advances the particle simulation, updates the current particle positions, and renders the result through the camera and shader program.
init_framework();
FrameWindow* pFrame = create_main_window(false, 800, 600, "The Particle Playground - part 2");
// std::cout << "Article 2 - C++ as a Graphics Language" << std::endl;
fast_vec<particle> particles;
initialize_particles(particles, 1000);
particle_renderer the_particle_renderer;
fast_vec<btm::glvec3> colors(particles.size());
for (auto& p : particles) {
// Initialize particle colors based on their initial random color
colors.push_back(btm::glvec3(p.color.x(), p.color.y(), p.color.z()));
}
the_particle_renderer.init(particles.size(), colors);
// Initialize application resources (camera, shader)
/// {
std::unique_ptr<gl_camera> camera;
std::unique_ptr<gl_shader> shader;
camera.reset(new btm::gl_camera(btm::fvec3(0, 0, 20), btm::fvec3(0, 0, 0), btm::fvec3(0, 1, 0)));
camera->set_fov(btm::dtr(10.f));
shader.reset(new gl_shader);
shader->add_file(GL_VERTEX_SHADER, "resources/shaders/particle_VertexShader.glsl");
shader->add_file(GL_FRAGMENT_SHADER, "resources/shaders/particle_FragmentShader.glsl");
shader->load();
/// }
start_timer();
get_elapsed_time();
while (pollEvents()) {
float fElapsed = (float)get_elapsed_time();
step_simulation(particles, fElapsed);
render(camera.get(), shader.get(), the_particle_renderer, particles);
}
the_app.terminate();
stop_timer();
What This Achieves
-
Particles continue to fall under gravity, using the simulation logic from Article 1.
-
Their positions are uploaded to the GPU, so the renderer can draw the latest state.
-
The GPU draws the particles as points, turning numerical positions into visible output.
-
The camera defines the view, either orbiting the scene or remaining fixed.
-
The result is a clean real-time visualization that makes the simulation easier to understand.
At this point, the Playground starts to feel alive: the same data that drove the simulation now becomes something visible, inspectable, and interactive.
Once the demo is rendering, the next concern is keeping that frame loop predictable as the project grows.