Improving performance for a blood rendering

Hi

In my project I was trying to improve my blood splatter system to add pooling. It’s still very simple, but I need to improve it’s performance.

Code:


package warlord.ingame.effects;

import java.util.ArrayList;
import warlord.ingame.world.Block;
import warlord.ingame.world.Tile;
import warlord.opengl.Game;
import warlord.opengl.Logger;
import warlord.opengl.MathHelper;

import static org.lwjgl.opengl.GL11.*;

public class BloodSplatter {
	
	// Each particle is 4x4
	public static float SIZE = 4.0f;
	public static int particlesPerTile = Math.round(Tile.WIDTH / SIZE);
	
	private ArrayList<float[]> particles; // Stores the x, y, alpha, and life
	
	
	public BloodSplatter(Block block){
		particles = new ArrayList<float[]>();
	}
	
	public void update(){
		for(int index = 0; index < particles.size(); index++){
			float[] particle = particles.get(index);
			if(particle.length < 4){
				Logger.print("Particle doesn't have a life. Skipping index " + index);
				continue;
			}
			if(Game.getFloat() < 0.999f){
				continue; // .1% chance of spreading
			}
			setLife(index, getLife(index) - 1);
			addParticle(particle[0] - SIZE, particle[1], getLife(index));
			addParticle(particle[0] + SIZE, particle[1], getLife(index));
			addParticle(particle[0], particle[1] - SIZE, getLife(index));
			addParticle(particle[0], particle[1] + SIZE, getLife(index));
		}
		trimLevels();
	}
	
	public void sprayDeath(float cx, float cy){
		sprayDeath(Math.round(cx), Math.round(cy), 5);
	}
	public void sprayDeath(int cx, int cy, int radius){
		for(int _radius = 0; _radius <= radius; _radius++){
			for(int degree = 0; degree <= 360; degree++){
				float dx = (MathHelper.cos((float)Math.toRadians(degree)));
				float dy = (MathHelper.sin((float)Math.toRadians(degree)));
				addParticle(cx + dx, cy + dy);
			}
		}
	}
	
	public void addParticle(float x, float y){
		particles.add(new float[]{x, y, 1.0f, 30.0f});
	}
	public void addParticle(float x, float y, float life){
		particles.add(new float[]{x, y, 1.0f, life});
	}
	
	public void setLife(int index, float life){
		float[] old = particles.get(index);
		old[3] = life;
		particles.set(index, old);
	}
	public float getLife(int index){
		return particles.get(index)[3];
	}
	
	public void trimLevels(){
		for(int index = 0; index < particles.size(); index++){
			float[] particle = particles.get(index);
			if(particle[3] <= 0.0f){
				// the particle is dead
				particles.remove(index);
			}
		}
	}
	
	public void render(){
		update();
		glBegin(GL_QUADS);
		ArrayList<Float> x = new ArrayList<Float>();
		ArrayList<Float> y = new ArrayList<Float>();
		for(int index = 0; index < particles.size(); index++){
			float[] particle = particles.get(index);
			if(x.contains(particle[0])){
				if(y.contains(particle[1])){
					continue;
				}
			}
			x.add(particle[0]);
			y.add(particle[1]);
			glColor4f(1.0f, 0.0f, 0.0f, 30.0f / particle[3]);
			glVertex2f(particle[0], particle[1]);
			glVertex2f(particle[0] + SIZE, particle[1]);
			glVertex2f(particle[0] + SIZE, particle[1] + SIZE);
			glVertex2f(particle[0], particle[1] + SIZE);
		}
		glEnd();
		glColor3f(1.0f, 1.0f, 1.0f);
	}

}

The blood seems to pool alright and looks okay so far, but when the blood starts to spread and overlap it begins to lag. I will eventually change the rendering but for now in development I’m using immediate mode. Any techniques or ideas to improve performance would be appreciated :slight_smile:

CopyableCougar4

here is my guess …

once you get alot particles going on …

if(x.contains(particle[0])){
  if(y.contains(particle[1])){
    continue;
  }
}

ArrayList.contains() is pretty expensive. try using a HashSet instead.
i would also create the two sets as fields and use clear() every render.

then i would go back and replace the particles array. for unique items, as you seem to be aiming for, i would create a particle class with custom equals() and hashcode() methods and store them in a hashmap. the hashmap would only accept unique particles so the check would become obsolete.

you like some code example into that direction ?

2nd, ofc it would speed up things when you replace the actual rendering part. do you run any shaders yet ? if, you could keep immediate mode and draw all particles in gl_points mode, just the center of each particle - and use point-sprites, which require a gl_pointSize line in a vertex-shader. would create exact the same output as you get now but with just 1/4 draw-calls.

Hi

So I was digging around in old ditched projects and I found some old blood code. So I spent the last 15 minutes refactoring and testing it, and I think I am part of the way to my solution. Here is the latest code:


package warlord.ingame.effects;

import java.util.ArrayList;
import java.util.HashSet;

import warlord.ingame.world.Collisions;
import warlord.opengl.Game;
import warlord.opengl.MathHelper;

import static org.lwjgl.opengl.GL11.*;

public class Blood {
	
	private ArrayList<Particle> blood;
	private Emitter emitter;
	
	public Blood(){
		blood = new ArrayList<Particle>();
		emitter = new Emitter(10.0f);
	}
	
	public void addParticle(float x, float y, float alpha, float radius, long ticks){
		blood.add(new Particle(x, y, alpha, ticks));
	}
	
	public void render(){
		HashSet<Float> _x = new HashSet<Float>();
		HashSet<Float> _y = new HashSet<Float>();
		for(int i = 0; i < blood.size(); i++){
			Particle particle = blood.get(i);
			if(_x.contains(particle.x)){
				if(_y.contains(particle.y)){
					continue;
				}
			}
			_x.add(particle.x);
			_y.add(particle.y);
			if(Game.getFloat() > (59.0f / 60.0f)){
				particle.spread(this, particle); // Only spread 1x per second
			}
			particle.render();
		}
	}
	
	public void sprayDeath(float x, float y, int angle){
		emitter.spray(x, y, this, angle - 60, 120);
	}
	public void sprayDeath(float x, float y){
		emitter.spray(x, y, this, 0, 360);
	}
	
	/*
	 * INNER UTILITY CLASSES
	 */
	
	private static class Particle {

		public float x, y, alpha;
		private long ticks;
		private boolean gone;
		private static float size = 4.0f;
		
		public Particle(float x, float y, float alpha, long ticks){
			this.x = x;
			this.y = y;
			this.alpha = alpha;
			this.ticks = ticks;
			gone = false;
			if(Collisions.isCollision(x, y)){
				gone = true;
			}
		}
		
		public void spread(Blood blood, Particle old){
			if(gone || old.ticks <= 0){
				return;
			}
			if(Game.getFloat() > 0.5f) blood.addParticle(x - size, y, 1.0f, Game.getFloat() * 80.0f, old.ticks - 1);
			if(Game.getFloat() > 0.5f) blood.addParticle(x + size, y, 1.0f, Game.getFloat() * 80.0f, old.ticks - 1);
			if(Game.getFloat() > 0.5f) blood.addParticle(x, y - size, 1.0f, Game.getFloat() * 80.0f, old.ticks - 1);
			if(Game.getFloat() > 0.5f) blood.addParticle(x, y + size, 1.0f, Game.getFloat() * 80.0f, old.ticks - 1);
			if(Game.getFloat() > 0.5f) blood.addParticle(x - size, y - size, 1.0f, Game.getFloat() * 80.0f, old.ticks - 1);
			if(Game.getFloat() > 0.5f) blood.addParticle(x - size, y + size, 1.0f, Game.getFloat() * 80.0f, old.ticks - 1);
			if(Game.getFloat() > 0.5f) blood.addParticle(x + size, y - size, 1.0f, Game.getFloat() * 80.0f, old.ticks - 1);
			if(Game.getFloat() > 0.5f) blood.addParticle(x + size, y + size, 1.0f, Game.getFloat() * 80.0f, old.ticks - 1);
		}
		
		public void render(){
			if(gone){
				return;
			}
			// All of the particles should destroy themselves in a few seconds
			if(ticks > 0L){
				ticks--;
			}
			if(ticks <= 0 && Game.getFloat() > (59.9f / 60.0f)){
				gone = true;
			}
			alpha = Math.max(0.0f, alpha - 0.0001f);
			glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
			glBegin(GL_QUADS);
				glVertex2f(x, y);
				glVertex2f(x + size, y);
				glVertex2f(x + size, y + size);
				glVertex2f(x, y + size);
			glEnd();
			glColor3f(1.0f, 1.0f, 1.0f);
		}
		
	}
	
	private static class Emitter {
		
		private float radius;
		
		public Emitter(float radius){
			this.radius = radius;
		}
		
		public void spray(float cx, float cy, Blood blood, int degree_, int range){
			for(float _radius = 1.0f; _radius <= radius; _radius += 1.0f){
				for(int degree = degree_; degree <= degree_ + range; degree++){
				//	if(Game.getFloat() > (1.0f - percent)){
						// Add a particle
						float dx = _radius * (float)MathHelper.cos((float) Math.toRadians(degree));
						float dy = _radius * (float)MathHelper.sin((float) Math.toRadians(degree));
						if(Game.getFloat() > 0.75f) blood.addParticle(dx + cx, dy + cy, 1.0f, _radius, 500 - Math.round(_radius));
				//	}
				}
			}
		}
		
	}

}

CopyableCougar4

One of the easiest ways to increase performance is to stop using immediate mode :wink:

Pretty sure you can call glBegin/End outside of the particle render loop instead of for every particle, so try that first, but yeah, no real reason to stick around with that crap.

Could you describe in more high-level terms (pseudocode perhaps, general algorithm description) what it is you’re actually trying to do? I’m having trouble determining it but it looks slower than it needs to be.

My attempt at a description of what I’m going for:

When blood needs to be added:


for(distance from 1 to the given radius){
  for(angle from 0 to 360){
    dx = cos(angle in radians)
    dy = sin(angle in radians)
    if(random chance){
       add a particle at the given position
    }
  }
}

Updating the blood


for(Particle in particle arraylist){
  if the spread distance is less than 0 it "dies"
  spread the particle to the 8 surrounding locations, by cloning and reducing the life / spread distance
}

Rendering the blood


for(Particle in particle){
  draw colored quad
}

When a particle is added there is also a check in the constructor to see if that particle collides with a wall. I hope I was descriptive enough :slight_smile:

CopyableCougar4