Particle/Billboard rotation issues

OK, working on a particle engine and I’m having trouble getting the particles to face the camera properly. Basically what happens is since I’m using the projection matrix to rotate them they rotate in place based on the camera rotation which leads to distortion when looking any direction other than directly down +z and -z. Any tips?

FYI: I’ve been looking at other examples and they all use the modelview identity matrix then apply a translation to achieve this effect, but my modelview matrix is always at the identity unless I render something, then I pop the stack…so I’m confused to say the least.

here’s the relevant code:

Particle draw

	public void draw(GL2 gl)
	{
		gl.glMatrixMode(GL2.GL_PROJECTION);
		gl.glPushMatrix();
		gl.glTranslatef(m_position.m_x, m_position.m_y, m_position.m_z);
		gl.glGetFloatv(GL2.GL_PROJECTION_MATRIX, m_proj, 0);

		//make sure the upper left hand 3x3 is identity
		m_proj[0] = 1.0f;
		m_proj[1] = 0.0f;
		m_proj[2] = 0.0f;
		m_proj[4] = 0.0f;
		m_proj[5] = 1.0f;
		m_proj[6] = 0.0f;
		m_proj[8] = 0.0f;
		m_proj[9] = 0.0f;
		m_proj[10] = 1.0f;

		m_buffer_data.put(m_proj);
		m_buffer_data.flip();
		gl.glLoadMatrixf(m_buffer_data);

		//draw the particle for testing
		gl.glBegin(GL2.GL_TRIANGLE_STRIP);
			gl.glTexCoord2f(1, 1);	gl.glVertex3f(m_size, m_size, 0);
			gl.glTexCoord2f(0, 1);	gl.glVertex3f(-m_size, m_size, 0);
			gl.glTexCoord2f(1, 0);	gl.glVertex3f(m_size, -m_size, 0);
			gl.glTexCoord2f(0, 0);	gl.glVertex3f(-m_size, -m_size, 0);
		gl.glEnd();

		gl.glPopMatrix();
	}

Emitter draw


        gl.glEnable(GL.GL_BLEND);
		gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
		for(int i = 0; i < m_particles.size(); i++)
			m_particles.get(i).draw(gl);
		gl.glDisable(GL.GL_BLEND);

Current Camera init:


        gl.glMatrixMode(GLMatrixFunc.GL_PROJECTION);
		gl.glLoadIdentity();
		glu.gluPerspective(m_fov, (float)(window_width) / (float)(window_height), m_z_near, m_z_far);
		
		gl.glRotatef(-m_pitch, 1.0f, 0.0f, 0.0f);
		gl.glRotatef(-m_yaw, 0.0f, 1.0f, 0.0f);
		gl.glTranslatef(-m_transform.m_translate.m_x, -m_transform.m_translate.m_y, -m_transform.m_translate.m_z);

So think about it. (Following ignored translation just to be simpler) Your camera rotates your projection matrix by -pitch and -yaw, which simulates a camera rotated by pitch and yaw. Then you have the (rotation part of) the model matrix set to the identity ie not doing anything. So essentially your particle will be rotated by -pitch and -yaw, which obviously works when pitch and yaw = 0 or 180 degrees (180 assuming back face culling isn’t enabled) but no other time.

So if you want to get a nice billboard effect, you have to have the model matrix to be rotated by pitch and yaw as well, (or don’t have the projection matrix rotated). Now there are better ways to do this computationally speaking but it really isn’t necessary.

in case you really really want to do billboarding by hand, ignore this.

… but point-sprites do exactly that for you. much easier and faster. the “problem” usualy is, we want the billboards/particles to be drawn in the correct size - according to the distance to the camera.

the “java part” could look like this:


// init block :
GL14.glPointParameteri(GL20.GL_POINT_SPRITE_COORD_ORIGIN,GL20.GL_LOWER_LEFT); // lower left or any other corner you prefer.

// render loop :

GL11.glEnable(GL32.GL_PROGRAM_POINT_SIZE); // tell GL you define pointsize in a shader
GL11.glEnable(GL20.GL_POINT_SPRITE);

// use shader
GL20.glUseProgram(program_id);

GL11.glBegin(GL11.GL_POINTS);

  // draw lots of particles
  for(int i = 0; i < num_particles; i++) GL11.glVertex3f([...]);

GL11.glEnd();

// end shader
GL20.glUseProgram(0);

GL11.glDisable(GL32.GL_PROGRAM_POINT_SIZE); // disable pointsprites, allow "normal" point-rendering
GL11.glDisable(GL20.GL_POINT_SPRITE);

the vertex shader could look like this : (the key is we write gl_PointSize)

#version 120

[...]

varying vec3 normal;  // screen space normal

uniform float tan_yfovr_05 = 0.6370703; // = 65 deg yfov
uniform vec2  screen_size = vec2(800.0,600.0);
uniform float pointsize = 0.01;

void main()
{
  [...]
  normal      = vec3(0.0,0.0,1.0); // for point-sprites, this is always "facing the camera"
  vec4 MVV    = gl_ModelViewMatrix * gl_Vertex;
  vec4 VP     = gl_ProjectionMatrix * MVV;
  
  gl_Position = VP;
  
  // pointsprites :
  float pt = (pointsize / (tan_yfovr_05 * -MVV.z)) * (screen_size.y); // sphere screen radius projection
  gl_PointSize = pt;
  // or maybe somthing like
  // gl_PointSize = max(pt, 2.0);
}

this shader requires 3 inputs (uniforms) from outside :

[icode]pointsize[/icode] is the radius of your particles. play with different values until you get what you like.
[icode]screen_screen[/icode] is your screen/frame size.

[icode]tan_yfovr_05[/icode] is a precalculated value which is used to project the “circle” into a value which matches your current projection :

first, we need the field-of-view, Y value, in radians :

to convert deg to radians and radians to deg you can use something like this:

public static final float PI = 3.14159265358979323846f;
public static final float DEG2RAD = PI / 180;
public static final float RAD2DEG = 180 / PI;

wherever you calculate your projection, also calculate this :

float yfov = 65.0f; // y-field-of-view deg. whatever value.
float yfovr = yfov*DEG2RAD;

float tan_yfovr_05 = (float)StrictMath.tan(yfovr*0.5); // pass this value to the shader

for the sake of completness, this is how you can compute the x-fov from y-fov :

float aspect       = (float)width/height; // screen size
float xfovr        = (float)( 2*StrictMath.atan(tan_yfovr_05*aspect) );
float xfov         = xfovr*RAD2DEG;

anyway, now we should have nice rectangles drawn around the center of each point-vertex, matching the perspective projection.

a fragment-shader could look like this (key is, we use gl_PointCoord instead of gl_MultiTexCoord0 or something) :

#version 120

[...]

const vec2 center = vec2(0.5);

// nice round point-sprite
void main()
{
  float alpha = smoothstep(0.5, 0.4, length(vec2(0.5) - gl_PointCoord));
  gl_FragColor = vec4(1.0,1.0,1.0,alpha);
}

// another approach could be :
void main()
{
  float dd = length(center - gl_PointCoord);
  float alpha = 1.0 - step(0.5, dd);

  gl_FragColor = vec4(1.0,1.0,1.0,alpha);
}

// proper way to draw nice antialiased round pointsprites could be:
void main()
{
  float dd      = length(center - gl_PointCoord);
  vec2  edge    = fwidth(gl_PointCoord) * 0.5;
  float edgeMax = max(edge.x, edge.y);
  float alpha   = 1.0 - smoothstep(0.5 - edgeMax, 0.5 + edgeMax, dd);

  gl_FragColor = vec4(1.0,1.0,1.0,alpha);
}

now if you want to draw billboards with trees (or something like that) on them, this stuff will not help you much since it always creates rectangles with equal sides. :slight_smile:

o/

@quew8: I get how it is supposed to work, but here’s the thing. I rotate the projection matrix so when I set it to the identity all the particles look to be rotated around themselves in place based on what the camera is rotated to, so it’s not that I can’t see the particles, it’s that they become distorted because they rotate to the angle of the camera’s rotation.

Now, thing is if I rotate the modelview by the pitch and yaw it doesn’t yield any better results, still distorted.

@basil_: Thanks! This would work, but I’m looking to do different widths and heights later on, maybe I’ll use this method for now, but I’d still like to know what I’m doing “wrong”.

aye, good to get it rolling anway. :slight_smile: point-sprites are more ment to be a replacement for point-rendering than a way to handle … something like mesh simplification-LOD-in-the-background.