Behavior:
With the following texture
I get a weird behavior when I try to rotate my particles in the direction they are moving. Essentially, the quad seems to rotate, but just barely so that when they bounce on the ground or when they spawn we can clearly see that they are not aligned upward when the particle is going upward.
Shader code and process explanation:
particle_billboard.geom:
//This geometry shader takes points as a
//primitive input and return quads built with
//triangle strips
#version 430
layout (points) in;
layout (triangle_strip, max_vertices = 4) out;
layout (std140, binding = 0) uniform CameraInfo {
mat4 ProjectionView; // Projection * View
vec3 eye; // camera position
};
uniform float particle_size;
in vec4 ex_Color[];
in vec3 ex_Direction[]; // Only one direction per particle so index is always direction[0]
out vec2 TexCoord;
out vec4 FragColor;
float angle_between_vector(vec3 v1, vec3 v2)
{
return acos(
dot(v1, v2)/ length(v1)*length(v2)
);
}
vec3 project_vector_on_plane(vec3 v, vec3 normalized_plane_normal)
{
return v - dot(v, normalized_plane_normal) * normalized_plane_normal;
}
vec4 quat_from_axis_angle(vec3 axis, float rad_angle)
{
vec4 qr;
float half_angle = 0.5*rad_angle;
qr.x = axis.x * sin(half_angle);
qr.y = axis.y * sin(half_angle);
qr.z = axis.z * sin(half_angle);
qr.w = cos(half_angle);
return qr;
}
vec3 rotate_vertex_position(vec3 position, vec3 axis, float rad_angle)
{
vec4 q = quat_from_axis_angle(axis, rad_angle);
vec3 v = position.xyz;
return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v);
}
void main(void) {
// Built-in variable "gl_in" of the geometry shader
// gives us the index in the primitive. Points only
// have one index, so gl_in[0] it is.
vec3 wPos = gl_in[0].gl_Position.xyz;
vec3 wRotatedPos = wPos;
vec3 originalPos = wPos;
vec3 wPosToCamera = normalize(eye - wPos);
vec3 wUp = vec3(0.0, 1.0, 0.0);
vec3 wRight = cross(wPosToCamera, wUp);
FragColor = ex_Color[0]; // Points only have one vertex
// 0.1 - Project ex_Direction on
// 1- Compute angle between right and direction
float wDirectionAngle = angle_between_vector(project_vector_on_plane(ex_Direction[0], wPosToCamera), wRight);
// Lower left corner
wPos -= (wRight * particle_size/2);
wPos.y -= particle_size/2;
wRotatedPos = rotate_vertex_position(wPos - originalPos, wPosToCamera, wDirectionAngle) + originalPos;
gl_Position = ProjectionView * vec4(wRotatedPos, 1.0);
TexCoord = vec2(0.0, 0.0);
EmitVertex();
// Upper left corner
wPos.y += particle_size;
wRotatedPos = rotate_vertex_position(wPos - originalPos, wPosToCamera, wDirectionAngle) + originalPos;
gl_Position = ProjectionView * vec4(wRotatedPos, 1.0);
TexCoord = vec2(0.0, 1.0);
EmitVertex();
// lower right corner
wPos.y -= particle_size;
wPos += wRight * particle_size;
wRotatedPos = rotate_vertex_position(wPos - originalPos, wPosToCamera, wDirectionAngle) + originalPos;
gl_Position = ProjectionView * vec4(wRotatedPos, 1.0);
TexCoord = vec2(1.0, 0.0);
EmitVertex();
// Upper right corner
wPos.y += particle_size;
wRotatedPos = rotate_vertex_position(wPos - originalPos, wPosToCamera, wDirectionAngle) + originalPos;
gl_Position = ProjectionView * vec4(wRotatedPos, 1.0);
TexCoord = vec2(1.0, 1.0);
EmitVertex();
EndPrimitive();
}
That is pretty much it for the code, I don't really do much else in the vertex and frag shaders... But I can provide them if necessary.
First of all, I draw the particles with quads that I build in the geometry shader from a single point. The quads are built with a right
and up
vector a little bit like this tutorial does, mainly for making the quads always face the camera.
Now, I visualize the texture as being aligned with the right
vector, so I try to compute the angle (wDirectionAngle
) between this vector and the direction vector of the particle projected on the particle quad. The particle quad being the plane with the normal in the same direction as the particle_position-to-camera-vector "wPosToCamera
". Finally, I use the wPosToCamera
as a quaternion vector to rotate my generated quads coordinate around the center of the quad. The rotation around the quaternion and the parameters I use are also heavily inspired on this tutorial.
I've debugged the direction angle and it appears to be correct, although I'm not 100% sure. What could possibly cause this to happen, is there a flaw in the logical process? I couldn't really get any inspiration of similar posts, since they are all about 2D.
EDIT:
tldr;
ErnieDingo's answer works wonderfully IMO. In my case, I had to swap the parameters for the cross product in order to see the quads. I also used as suggested the up vector coming from the camera and multiplied the vectors by a particle size factor registered as an uniform.
detailed edit
Some things I want to say for anyone coming accross this post.
1- Don't use vec3 in an uniform block which can only have std140 definition which rounds up its alignment to vec4 as mentioned here https://www.khronos.org/opengl/wiki/Interface_Block_(GLSL)
Warning: Implementations sometimes get the std140 layout wrong for vec3 components. You are advised to manually pad your structures/arrays out and avoid using vec3 at all. std430: This layout works like std140, except with a few optimizations in the alignment and strides for arrays and structs of scalars and vector elements. Specifically, they are no longer rounded up to a multiple of 16 bytes. So an array of
float
s will match with a C++ array offloat
s. Note that this layout can only be used with shader storage blocks, not uniform blocks.
With a workaround I was able to use the proper upvector in my shader.
2- My workflow was overcomplicated, in fact as mentioned by ErnieDingo the crossproduct of the particle direction and the camera direction is good enough for the quads to always face the camera.
3- Resulting gifs and code
//This geometry shader takes points as a
//primitive input and return quads built with
//of triangle strips
#version 430
layout (points) in;
layout (triangle_strip, max_vertices = 4) out;
layout (std140, binding = 0) uniform CameraInfo {
mat4 ProjectionView; // Projection * View
vec3 eye; // camera position
vec3 target;
vec3 up_vector;
};
uniform float particle_size;
in vec4 ex_Color[];
in vec3 ex_Direction[]; // Only one direction per particle so index is always direction[0]
out vec2 TexCoord;
out vec4 FragColor;
void main(void) {
vec3 wPos = gl_in[0].gl_Position.xyz;
vec3 wDirection = ex_Direction[0].xyz;
vec3 wCameraPlaneNormal = normalize(eye - target);
vec3 wUp = up_vector;
float dot = dot(wDirection, wCameraPlaneNormal);
if (abs(dot) < 1.0f)
{
wUp = cross(wCameraPlaneNormal, wDirection);
}
wUp = normalize(wUp) * particle_size;
wDirection *= particle_size;
FragColor = ex_Color[0]; // Points only have one vertex
// Lower left corner
gl_Position = ProjectionView * vec4(wPos + wUp - wDirection, 1.0);
TexCoord = vec2(0.0, 0.0);
EmitVertex();
// Upper left corner
gl_Position = ProjectionView * vec4(wPos - wUp - wDirection, 1.0);
TexCoord = vec2(0.0, 1.0);
EmitVertex();
// lower right corner
gl_Position = ProjectionView * vec4(wPos + wUp + wDirection, 1.0);
TexCoord = vec2(1.0, 0.0);
EmitVertex();
// Upper right corner
gl_Position = ProjectionView * vec4(wPos - wUp + wDirection, 1.0);
TexCoord = vec2(1.0, 1.0);
EmitVertex();
EndPrimitive();
}