Object and Particle Collision

Hi All,

I am fairly new to OF (and coding in general) so please excuse the noob questions. I am working on a small interactive particle system project and have some questions.

Any help would be greatly appreciated!

I am trying to do some collision (both between particles in the same particle system and between those particles and string elements) and I am running into some issues:

  1. The particles read and react correctly when they collide into each other, but they also seem to be running the collision function on themselves. I have nested for loops and run the collision function only when the particles[i].pos != particles[j].pos, but it doesn’t seem to be making a difference.

  2. I am using drawStringsAsShapes for my string variables, but how do I do collision between them and the particles? I have a letterCollide function in my ParticleSystemTop class, but I think I need to be more specific with the location of the string. Eventually I want the particles to bounce off the letter (when the appropriate key is pressed). Also, I’ll have the particles coming from all directions (right now they’re just coming from the top).

  3. I am running into memory issues. I think this is related to my first question, as the computer is running too many calculations at once. After a little while, the movement of the particles gets really choppy and eventually the project freezes. What am I doing wrong?

Code: https://github.com/Nedelstein/edeln591_dtOF_2018/tree/master/interactiveLettersMidterm

Thanks in advance for your help!

1 Like

CORRECTION: For my first question, I actually think it’s an issue of the starting position. Still unsure of how to solve it, but the particles seem to get stuck at the top of the screen. Any ideas?

Hi,

The first problem I see is

void ParticleSystemTop::update(){
    for (int i=0; i <numParticles; i++){
        glm::vec2 vel = glm::vec2(ofRandom(-1,1), ofRandom(-1,1));
        float mass = ofRandom(0.05, 5);
        pos.x = ofRandom(ofGetWidth());
        pos.y = ofRandom(0,5);
        Particle particle = Particle(pos, vel, mass);
        particles.push_back(particle);

Each update you create numParticles particles. Do you really want that ? At 60 fps, you will have 600 000 particles after 10 seconds.

Here’s a solution to create them once:

ParticleSystemTop::ParticleSystemTop(){
	init();
}

ParticleSystemTop::ParticleSystemTop(glm::vec2 _pos){
    pos = _pos;
	init();
}

void ParticleSystemTop::init()
{
	for (int i=0; i < 1000; i++){
		glm::vec2 vel = glm::vec2(ofRandom(-1,1), ofRandom(-1,1));
		float mass = ofRandom(0.05, 5);
		pos.x = ofRandom(ofGetWidth());
		pos.y = ofRandom(0,5);
		particles.emplace_back(pos, vel, mass);
	}
}

And yes,

Particle particle = Particle(pos, vel, mass);
particles.push_back(particle);

can be write

Particle particle(pos, vel, mass);
particles.push_back(particle);

or better

particles.emplace_back(pos, vel, mass);

This avoid to create a particle, copy it in the vector and then destroy it.

Another tip: a modern way to iterate over a container is to use the range-based for loop, like this:

void ParticleSystemTop::draw(){
	for (auto & p : particles){
		p.draw();
    }
}

I avoid to use variables like numParticles. It’s error prone because you have to think to update it each time you modify the vector. I prefer:

void ParticleSystemTop::update(){
	int num = particles.size();
	for (int i=0; i < num; i++){
        particles[i].update();

You can avoid to test the collision twice. If you do…

for (int i=0; i <numParticles; i++){
    for (int j=0; j< particles.size(); j++){
        if (particles[i].pos != particles[j].pos){
            particles[i].collide(particles[j]);
        }
    }    
}

… you test the same collision twice, for exemple when i=1, j=2 then when i=2, j=1.
A solution is:

void ParticleSystemTop::update(){
	int num = particles.size() ;
	for( int i = 0 ; i < num ; i++ ){
		particles[ i ].update() ;
		for( int j = i + 1 ; j < num ; j++ ){
			particles[ i ].collide( particles[ j ] ) ;
		}
	}
}

Or the same with iterators:

void ParticleSystemTop::update(){
	auto end = particles.end() ;
	for( auto it1 = particles.begin() ; it1 < end ; it1++ ){
		it1->update() ;
		for( auto it2 = it1 + 1 ; it2 < end ; it2++ ){
			it1->collide( *it2 ) ;
		}
	}
}

Of course you have to adapt the collide function:

void Particle::collide(Particle& p){
    float d = ofDist(pos.x, pos.y, p.pos.x, p.pos.y);
    if (d < radius + p.radius){
        vel*=-1;
        p.vel *=-1; // make both particles bounce
    }
}

For the interaction with the letters, a simple solution is to test the collision with the bounding box that contains the string. You may be more precise by testing the collision against the outline of each letter.

1 Like

Hi lilive,

Thanks so much for this! It’s super helpful.

However, it’s still running choppy on my machine. Any idea why this would be?

I’ve rearranged the particles so that they aren’t on top of each other at the start. And i’ve also added a delay to the collide function so that it doesn’t run for the first 10 seconds, but this doesn’t seem to be helping much.

Hi,
can you run it with less particles?
I guess that you are coding it your self just to learn about particle systems. Otherwise, there are a lot of examples around.
Just a small correction to your code.
I would change the following

for (int i=0; i <numParticles; i++){
    for (int j=0; j< particles.size(); j++){
        if (particles[i].pos != particles[j].pos){
            particles[i].collide(particles[j]);
        }
    }    
}

for

for (int i=0; i <particles.size() ; i++){
    for (int j=0; j< particles.size(); j++){
        if(i != j){
            if (particles[i].pos != particles[j].pos){
                particles[i].collide(particles[j]);
            }
        }
    }    
}

Dont use the i < numParticles check in the first for, as this can lead into bugs.
Then, checking that if(i != j){ is crucial to avoid a particle running the collision detection against itself.
Then you are checking the collision twice, so in order to avoid it add a boolean flag to the particles, for example put bool bCollisionChecked in the particle class declaration. Then set it to false for all the particles before running the nested for loops.
like

for(auto& p:particles){
    p.bCollisionChecked = false;
}

then in the particle::collision function check that this flag is false before performing the collision test, which should look like


void Particle::collide(Particle& p){
if( ! bCollisionChecked){
   bCollisionChecked = true;
   bool collision;
    float d = ofDist(pos.x, pos.y, p.pos.x, p.pos.y);
    if (d < radius + p.radius){
        collision = true;
    }
    else{
        collision = false;
    }
    
    if (collision == true){
        if (pos != p.pos){
        vel*=-1;
//        p.vel *=-1;
        }
    }
}
}
1 Like

I agree with that !
You can find some examples in OF.

Problem: draw each particle with ofDrawCircle is not very fast.
Possible solution: examples\gl\billboardExample

Problem: the movements computations are too heavy for the CPU
Possible solution: do them with the GPU
examples\gl\computeShaderParticlesExample
examples\gl\gpuParticleSystemExample

If you want to nicely continue to learn how to code a particle system without diving into the GPU part (which can be too much to learn for now), here’s a common optimization:
When possible, don’t compute the real distance between two particles, but the square of this distance.

ofDist is a costly operation, because it compute a square root:

float ofDist(float x1, float y1, float x2, float y2) {
	return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

You can do the same as…

void Particle::collide(Particle& p){
    float d = ofDist(pos.x, pos.y, p.pos.x, p.pos.y);
    if (d < radius + p.radius){
        vel*=-1;
        p.vel *=-1;
    }
}

…with:

void Particle::collide(Particle& p){
	glm::vec2 delta = pos - p.pos;
	float d = delta.x * delta.x + delta.y * delta.y; // this is the square of the distance
	float r = radius + p.radius;
	if (d < r * r ){ // compare it with the square of the collision distance
		vel *= -1;
		p.vel *= -1;
	}
}

With this optimization, the program run 3 times faster in my computer.

You can use…

void ofApp::setup(){
	// ...
	ofSetVerticalSync( false );
}
void ofApp::draw()
{
	// ...
	ofDrawBitmapStringHighlight( ofToString( ofGetFrameRate() ), 10, 30 ) ;
}

… to see the result of your optimization tries.

Thank you both this is incredible helpful! The particle system is running now without any lag!

1 Like

Hi All, me again.

So I’ve been trying to use the getStringAsPoints function but I think it’s a bit out of my league for now. I have switched to OfPath to draw the letters, but am still having trouble getting them to interact with my particle system. I understand that I’ll have to use the getOutline() function, but am a bit confused how to implement it. I started writing another collision function, but got stuck. Code below.

Also, here’s the git link: https://github.com/Nedelstein/edeln591_dtOF_2018/tree/master/interactiveLettersMidterm

h:

#pragma once

#include "ofMain.h"
#include "ofxCenteredTrueTypeFont.h"
#include "Particle.hpp"

class Letters{
public:

    void setup();
    void update();
    void draw(string _str);
    void collide();
    
    
ofTrueTypeFont letterFont;
    
    string str = "";
    glm::vec2 pos;
    float mass;
    typedef ofPath ofTTFCharacter;
    
   ofPath letterN;
    float nStartX = ofGetWidth()/2 - 100;
    float nStartY = ofGetHeight() *0.75;
    bool nDraw;
   vector<Particle> particles;
};

cpp:

#include "Letters.hpp"

void Letters::setup() {
    letterFont.loadFont("Courier New.ttf", 700, true,true,true);
    pos.x = ofGetWidth()/4;
    pos.y = ofGetHeight() - ofGetHeight()/5;
    
    
    letterN.moveTo(nStartX, nStartY);
    letterN.lineTo(nStartX +10, nStartY);
    letterN.lineTo(nStartX +10, ofGetHeight()/4);
    letterN.lineTo(nStartX, ofGetHeight()/4);
    letterN.close();


    letterN.moveTo(nStartX +10, ofGetHeight()/4);
    letterN.lineTo(nStartX, ofGetHeight()/4);
    letterN.lineTo(nStartX +200, nStartY);
    letterN.lineTo(nStartX +210, nStartY);
    letterN.close();

    letterN.moveTo(nStartX+200, nStartY);
    letterN.lineTo(nStartX+210, nStartY);
    letterN.lineTo(nStartX+210, ofGetHeight()/4);
    letterN.lineTo(nStartX+200, ofGetHeight()/4);
    letterN.close();

    letterN.setStrokeColor(255);
    letterN.setFilled(true);
    letterN.setFillColor(255);
    letterN.setStrokeWidth(10);
    
}



void Letters::update(){

}

void Letters::collide(){
    for(int i=0; i<particles.size(); i ++){
        for (int j = 0; j<letterN.getOutline().size(); j++){
            
        }
            
        }
        
    }



void Letters::draw(string _str){

    if(nDraw){
    letterN.draw();
    }
    
    
    
    str = _str;
    
    ofPoint startPos;
    startPos.x =  ofGetWidth() *.5;
    startPos.y =  ofGetHeight() * .5;

}




An approach is to test the collision with any line segment of the letter outline.

How to detect the collision between a moving circle and a line segment ? How to compute the bounce ? I don’t exactly know, and I have no link to give you, sorry. But I’m sure you will find the answer somewhere, this problem has been solved many times.

If you want to continue to code your own particle system instead of using an existing one (have you seen the particles system addons ?), you can simplify the problem and ignore the particle radius. With this simplification, if a particle move from pos1 to pos2, you only have to test the intersection between the line segment [pos1,pos2] and to compute the new position. You can easily find the code for the intersection somewhere, and compute the bounce shouldn’t be too hard.

hey,
the ofPath::getOutline() function returns a vector<ofPolyline> where each polyline is one of the contours that form the letter. Usually the first element of the vector is the outside of the letter. then, the ofPolyline has a function bool inside(glm::vec3) which will return true if the point you pass to it is inside the polyline.

void Letters::collide(){
    for(int i=0; i<particles.size(); i ++){
        auto& outline = letterN.getOutline();
        for (int j = 0; j < outline.size(); j++){
            if(outline[j].inside(particles[i])){
                // particle is inside and has collided. do whwatever you think it is useful.
               
            }
        }   
    }
}

openFrameworks has a function to find out the intersection between line segments,


/// \brief Determine the intersection between two lines.
/// \param line1Start Starting point for first line.
/// \param line1End End point for first line.
/// \param line2Start Starting point for second line.
/// \param line2End End point for second line.
/// \param intersection glm::vec3 reference in which to store the computed intersection point.
/// \returns True if the lines intersect.
template<class vectype>
bool ofLineSegmentIntersection(const vectype& line1Start, const vectype& line1End, const vectype& line2Start, const vectype& line2End, vectype& intersection)

hope this helps
cheers

Awesome! Thank you all! I think I’ll do just that - ignore the radius and just check the intersections.

Hi All,

Me again. Sorry to keep bugging you with this. I am so close! I figured out how to convert the characters to points so that they draw as circles on the screen with a keypressed command. I am trying to use them as an attractor for the particle system I have floating on the top of my screen. I have a getForce function in my Letter class and then am calling it in ofApp, but I can’t get it to work. Essentially, I am trying to get the particles to attract to the character (just have it for ‘N’ for now) and then stop moving once they reach it. Any help would be greatly appreciated!!

Github code: https://github.com/Nedelstein/edeln591_dtOF_2018/tree/master/interactiveLettersMidterm

ofApp.cpp:

#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
    ofBackground(0);
    ofSetVerticalSync(false);
    ofSetCircleResolution(100);
    
    noah['n'] = Letters();
    noah['n'].setup('N', center);
    noah['o'] = Letters();
    noah['o'].setup('O', center);
    noah['a'] = Letters();
    noah['a'].setup('A', center);
    noah['h'] = Letters();
    noah['h'].setup('H', center);
    
    
    glm::vec2 posTop = glm::vec2(ofRandom(ofGetWidth()), ofRandom(0,5));
    ParticleSystemTop _particleSystemTop = ParticleSystemTop(posTop);
    particleSystemTop.push_back(_particleSystemTop);
    
    elasticForceOn = true;
    
    
}

//--------------------------------------------------------------
void ofApp::update(){
    
    
    
    for (int i=0; i<particleSystemTop.size(); i++){
        glm::vec2 force = noah[currentChar].getForce(particleSystemTop[i].pos);
        particleSystemTop[i].applyForce(force);
        particleSystemTop[i].applyDampingForce(0.2);
        particleSystemTop[i].update();
        
        if (elasticForceOn){
            particleSystemTop[i].applyElasticForce(0.3);
            
            
        }
    
        
        //particle force from top to center/////
        glm::vec2 dirTop = center - particleSystemTop[i].pos;
        float distanceTop = glm::length(dirTop);
        if(distanceTop >0){
            glm::vec2 normalizedDirTop = dirTop / distanceTop;

            attractionTop = normalizedDirTop;
        }



    }
    
}

//--------------------------------------------------------------
void ofApp::draw(){

    for (int i=0; i<particleSystemTop.size(); i++){
        particleSystemTop[i].draw();
    }
    if (currentChar != ' '){
    noah[currentChar].draw();
    }
    
    


}

//--------------------------------------------------------------
void ofApp::keyPressed(int key){
    
    
    if (key == 'f'){
        ofToggleFullscreen();
        
    }
    
    if (key == 'n' || key == 'o' || key == 'a' || key == 'h'){
        currentChar = key;
        for (int i=0; i<particleSystemTop.size(); i++){
        particleSystemTop[i].applyForce(attractionTop);
            particleSystemTop[i].collision = false;
        elasticForceOn = false;
        }
    }
    
//    if (key == 'n'){
//        for (int i=0; i<particleSystemTop.size(); i++){
//
////            letter.nDraw = true;
//
//        }
    
    
    if (key == 'o'){
        for(int i=0; i<particleSystemTop.size(); i++){
            particleSystemTop[i].applyForce(attractionTop);
            letter.oDraw = true;
            particleSystemTop[i].oForce = true;
        }
    }

    if (key == 'a'){

    }
    if (key == 'h'){

    }
}

//--------------------------------------------------------------
void ofApp::keyReleased(int key){
    if (key == 'n' || key == 'o' || key == 'a' || key == 'h'){
        currentChar = ' ';
        for (int i=0; i<particleSystemTop.size(); i++){
            particleSystemTop[i].applyElasticForce(0.3);
            particleSystemTop[i].oForce = false;
            particleSystemTop[i].collision = false;
    }
        letter.nDraw = false;
        letter.oDraw = false;
    }
}

//--------------------------------------------------------------
void ofApp::mouseMoved(int x, int y ){

}

//--------------------------------------------------------------
void ofApp::mouseDragged(int x, int y, int button){

}

//--------------------------------------------------------------
void ofApp::mousePressed(int x, int y, int button){

}

//--------------------------------------------------------------
void ofApp::mouseReleased(int x, int y, int button){

}

//--------------------------------------------------------------
void ofApp::mouseEntered(int x, int y){

}

//--------------------------------------------------------------
void ofApp::mouseExited(int x, int y){

}

//--------------------------------------------------------------
void ofApp::windowResized(int w, int h){

}

//--------------------------------------------------------------
void ofApp::gotMessage(ofMessage msg){

}

//--------------------------------------------------------------
void ofApp::dragEvent(ofDragInfo dragInfo){ 

}

ofApp.h:

#pragma once

#include "ofMain.h"
#include "ofxCenteredTrueTypeFont.h"
#include "Letters.hpp"
#include "ParticleSystemTop.hpp"

class ofApp : public ofBaseApp{

	public:
		void setup();
		void update();
		void draw();

		void keyPressed(int key);
		void keyReleased(int key);
		void mouseMoved(int x, int y );
		void mouseDragged(int x, int y, int button);
		void mousePressed(int x, int y, int button);
		void mouseReleased(int x, int y, int button);
		void mouseEntered(int x, int y);
		void mouseExited(int x, int y);
		void windowResized(int w, int h);
		void dragEvent(ofDragInfo dragInfo);
		void gotMessage(ofMessage msg);
    
    ofxCenteredTrueTypeFont letterFont;
    

    Letters n;
    Letters o;
    Letters a;
    Letters h;
    
    Letters letter;
    vector<ParticleSystemTop> particleSystemTop;
    
    string _str;
    glm::vec2 center = glm::vec2(ofGetWidth()/2, ofGetHeight()/2);
    glm::vec2 attractionTop;
    bool elasticForceOn;
    
    map< char, Letters> noah;
    char currentChar;
};

Particle System cpp:

#include "ParticleSystemTop.hpp"

ParticleSystemTop::ParticleSystemTop(){
    pos = glm::vec2(0,0);
//    init();
}

ParticleSystemTop::ParticleSystemTop(glm::vec2 _pos){
    pos = _pos;
    
    init();
    
}

void ParticleSystemTop::applyForce(glm::vec2 force){
    for (int i=0; i<particles.size(); i++){
        particles[i].applyForce(force);
    }
}
void ParticleSystemTop::applyDampingForce(float strength){
    for (int i = 0; i>particles.size(); i++){
        particles[i].applyDampingForce(strength);
    }
}

void ParticleSystemTop::applyElasticForce(float strength){
    for (int i=0; i<particles.size(); i++){
        particles[i].applyElasticForce(strength);
    }
}

void ParticleSystemTop::update(){

    for (int i=0; i <particles.size(); i++){
        particles[i].update();
        particles[i].checkEdges();
        
        if (collision){
        for (int j=0; j< particles.size(); j++){
            if(i !=j){
                if (particles[i].pos != particles[j].pos){

                particles[i].collide(particles[j]);
                }
            }
            }
        }
        }
}

void ParticleSystemTop::init(){
    for (int i=0; i <numParticles; i++){
        glm::vec2 vel = glm::vec2(0, 0);
        float mass = ofRandom(0.5, 2);
        pos.x = ofRandom(ofGetWidth());
        pos.y = ofRandom(1,30);
        particles.emplace_back(pos, vel, mass);
//        Particle particle = Particle(pos, vel, mass);
//        particles.push_back(particle);
}
}
void ParticleSystemTop::draw(){
    for(int i=0; i<particles.size(); i++){
        particles[i].draw();
    }
}


void ParticleSystemTop::oCollide(){
    if (oForce){
        for(int i=0; i<particles.size(); i++){
            glm::vec2 diff = letter.oPos - particles[i].pos;
            particles[i].applyForce(diff * 0.03);
            particles[i].applyDampingForce(0.1);
            float distance = ofDist(particles[i].pos.x, particles[i].pos.y, letter.oPos.x, letter.oPos.y);
            if (distance < letter.oRad + particles[i].radius){
                particles[i].vel = glm::vec2(0,0);
            }
        }
    }
}

//void ParticleSystemTop::nCollide(){
//    for(int i=0; i<particles.size(); i ++){
//        vector<ofPolyline> outline = letter.letterN.getOutline();
//        for (int j = 0; j < outline.size(); j++){
//            if(outline[j].inside(particles[i])){
//                particles[i].vel = glm::vec2(0,0);
//
//            }
//        }
//    } 
//}



//void ParticleSystemTop::letterCollide(){
//    for(int i=0; i<particles.size(); i++){
//        float distance = ofDist(particles[i].pos.x, particles[i].pos.y, letter.pos.x, letter.pos.y);
//        if (distance < 0){
//            particles[i].vel *=-1;
//        }
//
//    }
//}



Particle System h:

#pragma once

#include "ofMain.h"
#include "Particle.hpp"
#include "Letters.hpp"

class ParticleSystemTop{
public:
    
    ParticleSystemTop();
    ParticleSystemTop(glm::vec2 _pos);
    
    void setup();
    void update();
    void applyForce(glm::vec2 force);
    void draw();
    void applyElasticForce(float strength);
    void applyDampingForce(float strength);
    void letterCollide();
    void oCollide();
    void nCollide();
    void init();
    
    
    glm::vec2 pos;
    
    glm::vec2 center = glm::vec2(ofGetWidth()/2, ofGetHeight()/2);
    
    vector<Particle> particles;
    
    int numParticles = 500;
    
    Letters letter;
    bool oForce;
    bool collision;
    
};

Letter cpp:

#include "Letters.hpp"

void Letters::setup(char c, glm::vec2 _pos) {
    pos = _pos;
    letterFont.loadFont("Courier New.ttf", 600, false,false,true);
    
    ofTTFCharacter nLetter = letterFont.getCharacterAsPoints(c, true, false);
    
    float width   = letterFont.stringWidth(ofToString(c));  // char width and height
    float height  = letterFont.stringHeight(ofToString(c));
    glm::vec2 adjustedPos = _pos + glm::vec2(-width*0.5, height*0.5);
    
    vector<ofPolyline> nPolylines = nLetter.getOutline();
    
    for (int j = 0; j<nPolylines.size(); j++){
        ofPolyline outline = nPolylines[j].getResampledBySpacing(4);
        outline.translate(adjustedPos);
        for (int k = 0; k < outline.size(); k++){
            
            attractors.push_back(outline[k]);
            }
        }
    
    
    oPos.x = ofGetWidth() *0.5;
    oPos.y = ofGetHeight() *0.5;
    oRad = ofGetWidth()/4;
    
    
}

glm::vec2 Letters::getForce(glm::vec2 particlePos){
    glm::vec2 force = glm::vec2(0,0);
    for (int i=0; i<attractors.size(); i++){
        glm::vec2 dir = attractors[i] - particlePos;
        float distance = glm::length(dir);
        if (distance !=0 ){
            glm::vec2 norm = dir/distance;
            force = norm * 0.001;
        }
        else if (distance !=0 && distance < 10){
            force*=-1;
            }
        }
    
    
//   clamp the total force strength like so:
    float totalStrength = glm::length(force);
    if (totalStrength > 0){
        force = force / totalStrength; // normalize
        totalStrength = ofClamp(totalStrength, 0, 0.5);
        force *= totalStrength; // reapply clamped strength
    }
    return force;
}

void Letters::update(){

}




void Letters::draw(){
    
    for(int i=0; i<attractors.size(); i++){
        ofDrawCircle(attractors[i], 2);
    }
}



Letter h:

#pragma once

#include "ofMain.h"
#include "Particle.hpp"

class Letters{
public:

    void setup(char c, glm::vec2 _pos);
    void update();
    void draw();
    void Ncollide();
    void Ocollide();
    glm::vec2 getForce(glm::vec2 pos);
    
    vector<glm::vec2> attractors;
    
    
ofTrueTypeFont letterFont;
    
    glm::vec2 pos;
    float mass;
    typedef ofPath ofTTFCharacter;
    
   ofPath letterN;
    float nStartX = ofGetWidth()/2 - 100;
    float nStartY = ofGetHeight() *0.75;
    bool nDraw;
   vector<Particle> particles;
    
    glm::vec2 oPos;
    float oRad;
    bool oDraw;
    
    
};