Basic N-Body Simulation Misbehaves When Moving to GPU


#1

Hi everyone,

I’m writing a simple N-Body simulation (i.e particles moving in a box with/out gravity, and without interacting with each other)

It worked as expected when I was using gl::drawSphere(), but this obviously won’t really take a big number of particles. Thereby, I’m trying to move rendering to the GPU as describe here:
https://libcinder.org/docs/guides/opengl/part3.html

That actually works well for one particle, but as soon as more particles are added they start to move aggressively out of the box in what seems to be a joint cloud of all particles but the first one.

here’s some code:
CinderApp.cpp:

class BasicApp : public App {
public:

	void keyDown(KeyEvent event) override;
	void update() override;
	void setup() override;
	void draw() override;

private:
	std::vector<Particle*> m_paricles;
	CameraPersp m_camera;
	int m_cameraZoom;
	gl::BatchRef m_sphereRef;
};

void BasicApp::mouseDrag( MouseEvent event )
{
}


void BasicApp::setup()
{
	auto lambert = gl::ShaderDef().lambert().color();
	gl::GlslProgRef shader = gl::getStockShader(lambert);

	auto sphere = geom::Sphere();
	m_sphereRef = gl::Batch::create(sphere, shader);
	
	m_cameraZoom = 50;
	m_camera.lookAt(vec3(m_cameraZoom, 0, 0), vec3(0));

	setFpsSampleInterval(getFpsSampleInterval() / 100);
}

void prepareSettings(BasicApp::Settings* settings)
{
}

void BasicApp::keyDown( KeyEvent event )
{
	if( event.getChar() == 'p' ) {
		m_paricles.push_back(
			new Particle(	Utils::GetRandomVec3(5), 
							Utils::GetRandomVec3(1), 
							Rand::randFloat(1,2),
							1/30)
			);
	}

	if (event.getChar() == '-') {
		++m_cameraZoom;
		m_camera.lookAt(vec3(m_cameraZoom, 0, 0), vec3());
	}

	else if( event.getCode() == KeyEvent::KEY_SPACE ) {
		vec3 earthGravity(9.8, 0, 0);
		for (Particle* particle : m_paricles)
			particle->setGravity(earthGravity);

	}
	else if( event.getCode() == KeyEvent::KEY_ESCAPE ) {
		vec3 earthGravity(0);
		for (Particle* particle : m_paricles)
			particle->setGravity(vec3());
	}
}

void BasicApp::update()
{
	for (Particle* particle : m_paricles)
		particle->makeStep();

}

void BasicApp::draw()
{
	gl::clear();
	gl::enableDepthRead();
	gl::enableDepthWrite();
	
	gl::setMatrices(m_camera);

	for (Particle* particle : m_paricles) {
		gl::color(particle->getColor()); 
		gl::translate(particle->getLocation());
		m_sphereRef->draw();
	}
		
}

// This line tells Cinder to actually create and run the application.
CINDER_APP( BasicApp, RendererGl, prepareSettings )

And here’s Particle definition and implementation:

class Particle {

public:

	Particle(ci::vec3 initialLocation, ci::vec3 initialVelocity, int mass, float dt);

	ci::vec3 getLocation() const { return m_location; }
	ci::vec3 getVelocity() const { return m_velocity; }
	int getMass() const { return m_mass; }
	ci::Color getColor() { return m_color; }

	void makeStep();

	ci::vec3 getGravity() const { return m_gravity; }
	void setGravity(ci::vec3 val) { m_gravity = val; }

private:

	ci::vec3 m_location;
	ci::vec3 m_velocity;
	int m_mass;

	ci::Color m_color;

	const int m_bounds = 10;
	
	ci::vec3 m_gravity;
	float m_dt;
};

Particle::Particle(ci::vec3 initialLocation, ci::vec3 initialVelocity, int mass, float dt)
	: m_location(initialVelocity), m_velocity(initialVelocity), m_mass(mass), m_dt(dt)
{
	ci::Color color(ci::Rand::randFloat(1), 
					ci::Rand::randFloat(1), 
					ci::Rand::randFloat(1));

	m_color = color;
}

void Particle::makeStep()
{
	if(ci::length(m_gravity) > 0){
		
		m_velocity = m_velocity - m_gravity * m_dt;
	}
	
	m_location = m_location + m_velocity;
	
	if (ci::math<float>::abs(m_location.x) > m_bounds) m_velocity.x *= -1;
	if (ci::math<float>::abs(m_location.y) > m_bounds) m_velocity.y *= -1;
	if (ci::math<float>::abs(m_location.z) > m_bounds) m_velocity.z *= -1;
	
}

Thanks.


#2

This is happening because the gl::translate is accumulating and affecting all the particles but the first one. When you use the ci::gl::drawSphere you specify the center and don´t call a transform matrix.

In order your code to work you just need to isolate the translate transform

for (Particle* particle : m_paricles) {
		gl::color(particle->getColor());
		ci::gl::pushMatrices();
		gl::translate(particle->getLocation());
		m_sphereRef->draw();
		ci::gl::popMatrices();
		
	}

Note that this is not the best the way to do and you shoulld take a look at the ParticleSphereCPU/GPU example to see a proper OpenGL way.


#3

Great answer @xumo, thanks!

I’ll check out ParticleSphereCPU/GPU to learn more, as you suggested.


#4

Can I recommend gl::ScopedMatrices ?