I am trying to do a particle system with OpenGL 3 using point sprite.
I use a VBO with GL_STREAM_DRAW where I put the coordinate of every particle. During each frame, I update the VBO with the new particle coordinate. The particle are simply rendered with GL_POINTS, using GL_VERTEX_PROGRAM_POINT_SIZE.
I noticed that some particles where overlayed by other despite the fact that they were supposed to be closer to the camera.
The point sprite are actually drawn by order of draw call and not by depth, which create situation like this :
Here the farthest particle is drawn first, the closes particle is drawn second. As expected, the closet particles completely cover the one behind it.
Here, the draw order is reversed resulting in the farthest particle being visible.
I try using OpenGL depth testing using
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);
glDepthFunc(GL_LEQUAL);
glDepthRange(0.f, 1.f);
glEnable(GL_DEPTH_CLAMP);
But it just result in nothing being drawn.
As I understand it, one way to resolve this problem is to reorder the particles by depth but this solution would be very costly on the CPU for many particle so is there a way to have proper depth testing for point sprite on the GPU ?
The vertex shader use for drawing the particle is the following :
#version 330
layout(location = 0) in vec4 position;
uniform float time;
uniform mat4 camera;
smooth out float dist;
void main()
{
vec4 cameraPos = position + vec4(0.0, 0.0, -1.0, 0.0);
gl_Position = camera * cameraPos;
dist = sqrt(dot(camera * cameraPos, position));
gl_PointSize = 15.0/dist;
}
The fragment shader :
#version 330
out vec4 colour;
uniform float time;
smooth in float dist;
float map(float value, float inMin, float inMax, float outMin, float outMax) {
return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
}
void main()
{
// colour = vec4(pos.x, pos.y, 1.0, 1.0);
if(dot(gl_PointCoord-0.5,gl_PointCoord-0.5)>0.25)
discard;
else {
float g = (dot(gl_PointCoord-0.5,gl_PointCoord-0.5) > 0.22 ? 0.6 : map(dot(gl_PointCoord-0.5,gl_PointCoord-0.5), 0.0, 0.21, 0.0, 0.6));
colour = vec4(g, g*sin(time)*sin(time)*cos(time), sin(dist), 1.0);
}
}
The full code (minus some boilerplate code):
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/vec4.hpp>
#include <glm/mat4x4.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/trigonometric.hpp>
#include <algorithm>
#include <string>
#include <iostream>
#include <vector>
#include <random>
#include <cmath>
#include "tools.h"
#include "shader.h"
#include "data.h"
#define BENCHMARK 230000
#define MAX_POINT 2
#define TTL 100
void init_program(GLuint* program)
{
std::vector<GLuint> shaders;
shaders.push_back(create_shader(GL_VERTEX_SHADER, read_file("data/particle.vs")));
shaders.push_back(create_shader(GL_FRAGMENT_SHADER, read_file("data/particle.fs")));
*program = create_program(shaders);
std::for_each(shaders.begin(), shaders.end(), glDeleteShader);
}
bool first=true;
void create_new_point(Point* p)
{
// Testing draw order
if(first)
p->pos = glm::vec4(0.f, 0.f, 0.f, 1.f);
else
p->pos = glm::vec4(0.f, 0.f, 0.8, 1.f);
p->dir = glm::vec4(0.f, 0.f, 0.f, 0.f);
p->ttl = TTL+(TTL*(distrib(gen)/2.0));
first = false;
}
void update_point(Point* p, double dt)
{
if((p->ttl - dt) <= 0)
create_new_point(p);
else
{
glm::vec4 speed(dt/2.0);
p->pos += (p->dir*speed);
p->ttl = p->ttl - dt;
}
}
void vbo_point(std::vector<Point>& points, float* data, GLuint* vbo, bool update)
{
for(size_t n=0; n<points.size(); ++n)
{
if(update)
{
data[n*4] = points[n].pos.x;
data[n*4+1] = points[n].pos.y;
data[n*4+2] = points[n].pos.z;
data[n*4+3] = points[n].pos.w;
}
else
{
data[n*4] = 0;
data[n*4+1] = 0;
data[n*4+2] = 0;
data[n*4+3] = 0;
}
}
glBindBuffer(GL_ARRAY_BUFFER, *vbo);
if(update)
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(float)*4*points.size(), data);
else
glBufferData(GL_ARRAY_BUFFER, sizeof(float)*4*points.size(), data, GL_STREAM_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
int main(void)
{
GLFWwindow* window;
if (!glfwInit())
return -1;
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
window = glfwCreateWindow(1280, 768, "Hello World", NULL, NULL);
if (!window)
{
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
gladLoadGLLoader((GLADloadproc) glfwGetProcAddress);
// Init data
GLuint vbo, vao, program;
glGenBuffers(1, &vbo);
init_program(&program);
// VAO
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);
/*
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);
glDepthFunc(GL_LEQUAL);
glDepthRange(0.f, 1.f);
glEnable(GL_DEPTH_CLAMP);
glEnable(GL_BLEND) ;
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
*/
// Time data
double prev = 0.0;
double curr = 0.0;
double frameTime = 0.0;
// Init Points
std::vector<Point> points;
for(size_t n=0; n<MAX_POINT; ++n)
{
Point tmp = {glm::vec4(0), glm::vec4(0), 0};
points.push_back(tmp);
}
float* data = new float[4*points.size()];
for(size_t n=0; n<points.size(); ++n)
update_point(&points[n], 0);
vbo_point(points, data, &vbo, false);
glfwSwapInterval(1);
GLint time = glGetUniformLocation(program, "time");
GLint camera_location = glGetUniformLocation(program, "camera");
glm::mat4 camera_matrix = glm::perspective(glm::radians(45.f), 1.33f, 0.1f, 10.f);
/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
curr = glfwGetTime();
/* Render here */
glClear(GL_COLOR_BUFFER_BIT);
glClearColor(1.f, 1.f, 1.f, 0.f);
glUseProgram(program);
glUniform1f(time, glfwGetTime());
glUniformMatrix4fv(camera_location, 1, GL_FALSE, glm::value_ptr(camera_matrix));
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
glDrawArrays(GL_POINTS, 0, points.size());
glDisableVertexAttribArray(0);
glUseProgram(0);
/* Swap front and back buffers */
glfwSwapBuffers(window);
/* Poll for and process events */
glfwPollEvents();
for(size_t n=0; n<points.size(); ++n)
update_point(&points[n], frameTime);
vbo_point(points, data, &vbo, true);
std::cout << std::fixed;
std::cout.precision(8);
std::cout << "\rfps: " << 1.f/frameTime << " | Point drawed :" << points.size()
<< " | TTL1: " << points[0].ttl;
prev = glfwGetTime();
frameTime = prev-curr;
}
delete[] data;
glfwTerminate();
return 0;
}