I’ve successfully created a simple particle system that uses a vector field. Now I want to perform post-processing on it, I believe it should work like this:
- Compute Shader: Move particles
- Draw Particle Shader: Draw particles into a FBO as a texture(off-screen rendering)
- Post-Processing Shader: Read the texture and output to the window buffer.
My first goal is simply to render my scene as normal but as a texture on a quad (a.k.a no post-processing effects).
Currently, the screen is black and sometimes flicker. I’ve added all the code below for context but I expect my mistake is in the ‘Main Program’.
Particle Compute:
#version 430 core
///////////////////
// RANDOMNESS
////////////////////
// Implementation found on StackOverflow:
// https://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl#4275343
// A single iteration of Bob Jenkins' One-At-A-Time hashing algorithm.
uint hash( uint x ) {
x += ( x << 10u );
x ^= ( x >> 6u );
x += ( x << 3u );
x ^= ( x >> 11u );
x += ( x << 15u );
return x;
}
// Compound versions of the hashing algorithm I whipped together.
uint hash( uvec2 v ) { return hash( v.x ^ hash(v.y) ); }
uint hash( uvec3 v ) { return hash( v.x ^ hash(v.y) ^ hash(v.z) ); }
uint hash( uvec4 v ) { return hash( v.x ^ hash(v.y) ^ hash(v.z) ^ hash(v.w) ); }
// Construct a float with half-open range [0:1] using low 23 bits.
// All zeroes yields 0.0, all ones yields the next smallest representable value below 1.0.
float floatConstruct( uint m ) {
const uint ieeeMantissa = 0x007FFFFFu; // binary32 mantissa bitmask
const uint ieeeOne = 0x3F800000u; // 1.0 in IEEE binary32
m &= ieeeMantissa; // Keep only mantissa bits (fractional part)
m |= ieeeOne; // Add fractional part to 1.0
float f = uintBitsToFloat( m ); // Range [1:2]
return f - 1.0; // Range [0:1]
}
// Pseudo-random value in half-open range [0:1].
float random( float x ) { return floatConstruct(hash(floatBitsToUint(x))); }
float random( vec2 v ) { return floatConstruct(hash(floatBitsToUint(v))); }
float random( vec3 v ) { return floatConstruct(hash(floatBitsToUint(v))); }
float random( vec4 v ) { return floatConstruct(hash(floatBitsToUint(v))); }
///////////////////
// DATA
////////////////////
uniform int u_width;
uniform int u_height;
uniform float u_time;
struct Particle{
vec2 pos;
};
layout(std430, binding = 1) buffer particleBuffer
{
Particle particles[];
};
layout(std430, binding = 2) buffer vectorFieldBuffer
{
vec2 vectorField[];
};
///////////////////
// Helper Functions
////////////////////
// Assumes they are laid out in rows...
int calcPosition(int x, int y){
return x + y * u_width;
}
///////////////////
// Main
////////////////////
layout(local_size_x = 1024, local_size_y = 1, local_size_z = 1) in;
void main()
{
uint i = gl_GlobalInvocationID.x;
vec2 pos = particles[i].pos;
// Find the 4 closest vectors
// We have to subtract with 1.0 becaause we need to have vectors all around us.
float f_width = float(u_width) - 1.0;
float f_height = float(u_height) - 1.0;
float x = (pos.x + 1.0) / 2.0 * f_width;
float y = (pos.y + 1.0) / 2.0 * f_height;
vec2 realPos = vec2(x, y);
if (x >= 0.0 && x < f_width && y >= 0.0 && y < f_height){
int x_index = int(x);
int y_index = int(y);
// The inverse of the distance. Lucklily every sqaure is exactly one unit.
vec2 xy_dist = realPos - floor(realPos);
// Interpolate the x-axis
vec2 r1 = vectorField[calcPosition(x_index, y_index)] * (1.0 - xy_dist.x) + vectorField[calcPosition(x_index + 1, y_index)] * xy_dist.x;
vec2 r2 = vectorField[calcPosition(x_index, y_index + 1)] * (1.0 - xy_dist.x) + vectorField[calcPosition(x_index + 1, y_index + 1)] * xy_dist.x;
// Interpolate the y-axis
vec2 velocity = r1 * (1.0 - xy_dist.y) + r2 * xy_dist.y;
particles[i].pos += velocity;
} else {
// Randomly reset the particle somewhere viewable
particles[i].pos = vec2(random(pos.x), random(pos.y)) * 2.0 - 1.0;
}
}
Particle Frag:
#version 430 core
// The color of the line
uniform vec4 u_color;
layout(location = 0) out vec4 color;
void main()
{
color = u_color;
}
Particle Vert:
#version 430 core
layout (location = 0) in vec2 aPos;
uniform mat4 model;
void main()
{
gl_Position = model * vec4(aPos, 0.0, 1.0);
}
Post-Processing Vert:
#version 430 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec2 texCoords;
out vec2 TexCoords;
void main()
{
gl_Position = vec4(position.x, position.y, 0.0, 1.0);
TexCoords = texCoords;
}
Post-Processing Frag:
#version 430 core
in vec2 TexCoords;
out vec4 color;
uniform sampler2D screenTexture;
void main()
{
color = texture(screenTexture, TexCoords);
}
Main Program:
#include <iostream>
#include <cmath>
#include "../include/glad/glad.h"
#include <GLFW/glfw3.h>
#include <stb_image.h>
#include "Shader.hpp"
#include "camera.h"
#include "Arrow.hpp"
#include "GLHelpers.hpp"
#include <array>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/string_cast.hpp>
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
GLenum glCheckError_(const char *file, int line);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 800;
// camera
Camera camera(glm::vec3(0.0f, 0.0f, -3.0f));
// timing
float deltaTime = 0.0f; // time between current frame and last frame
float lastFrame = 0.0f;
//Location Vector
struct Vector {
float x;
float y;
};
int main()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_SAMPLES, 4);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
//std::cout << glGetError() << std::endl; // returns 0 (no error)
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// configure global opengl state
// -----------------------------
glEnable(GL_DEPTH_TEST);
glEnable(GL_MULTISAMPLE); // enabled by default on some drivers, but not all so always enable to make sure
glCheckError();
// Build and compile our shader programs
// ------------------------------------
Shader particleComputeShader("../shaders/particle.comp");
glCheckError();
Shader particleShader("../shaders/particle.vert", "../shaders/particle.frag");
glCheckError();
Shader postprocessingShader( "../shaders/passthrough.vert", "../shaders/simpletexture.frag" );
glCheckError();
// Vector Field
// ------------------------------------
int vectorFieldDim = 9;
float vectorField[vectorFieldDim * vectorFieldDim * 2];
for (int i = 0; i < vectorFieldDim; i++) {
for (int j = 0; j < vectorFieldDim; j++) {
int position = (i * vectorFieldDim + j) * 2;
vectorField[position] = 0.01 * sin(i*M_PI/9.0f);
vectorField[position + 1] = 0.01 * cos(j*M_PI/9.0f);
}
}
particleComputeShader.use();
glCheckError();
particleComputeShader.setInt("u_width", vectorFieldDim);
glCheckError();
particleComputeShader.setInt("u_height", vectorFieldDim);
glCheckError();
GLuint ssbo;
glGenBuffers(1, &ssbo);
glCheckError();
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
glCheckError();
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(vectorField), vectorField, GL_STATIC_DRAW); //sizeof(data) only works for statically sized C/C++ arrays.
glCheckError();
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, ssbo);
glCheckError();
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); // unbind
glCheckError();
// Set model
// ------------------------------------
particleShader.use();
glm::mat4 model = glm::mat4(1.0f);
particleShader.setMat4("model", model);
// Particles
// ------------------------------------
std::vector<float> particles;
int numberOfParticles = 1024;
//float particles[numberOfParticles * 2];
for(int i = 0; i < numberOfParticles * 2; i++){
// TODO: Use propeor good randomness instead...
float pos = static_cast <float> ( 2 * rand()) / static_cast <float> (RAND_MAX);
particles.push_back(pos);
}
unsigned int PARTICLE_VAO, PARTICLE_VBO;
glGenVertexArrays(1, &PARTICLE_VAO);
glGenBuffers(1, &PARTICLE_VBO);
glBindVertexArray(PARTICLE_VAO);
glBindBuffer(GL_ARRAY_BUFFER, PARTICLE_VBO);
glBufferData(GL_ARRAY_BUFFER, particles.size() * 4, particles.data(), GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
// Allow particle.comp to update the particle positions
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, PARTICLE_VBO);
// Particles FBO to be used for post-processing
// -----------------------------------------------
GLuint particleFBO;
glGenFramebuffers(1, &particleFBO);
glBindFramebuffer(GL_FRAMEBUFFER, particleFBO);
// The texture we're going to render to
GLuint particleTexture;
glGenTextures(1, &particleTexture);
// "Bind" the newly created texture : all future texture functions will modify this texture
glBindTexture(GL_TEXTURE_2D, particleTexture);
// Give an empty image to OpenGL ( the last "0" )
glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, SCR_WIDTH, SCR_HEIGHT, 0,GL_RGB, GL_UNSIGNED_BYTE, NULL);
// set the texture wrapping parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// set texture filtering parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, particleTexture, 0);
glBindTexture(GL_TEXTURE_2D, 0); // unbind
// Set the list of draw buffers.
// GLenum DrawBuffers[1] = {GL_COLOR_ATTACHMENT0};
// glDrawBuffers(1, DrawBuffers); // "1" is the size of DrawBuffers
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE){
std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << std::endl;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);//unbind
// Post Processing
// ----------------
static const GLfloat post_processing_quad[] = {
// positions texture coordinates
-1.0f, 1.0f, 0.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 0.0f,
1.0f, -1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f,
1.0f, -1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f
};
unsigned int POST_PROCESSING_VAO, POST_PROCESSING_VBO;
glGenVertexArrays(1, &POST_PROCESSING_VAO);
glGenBuffers(1, &POST_PROCESSING_VBO);
glBindVertexArray(POST_PROCESSING_VAO);
glBindBuffer(GL_ARRAY_BUFFER, POST_PROCESSING_VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(post_processing_quad), post_processing_quad, GL_STATIC_DRAW);
// position attribute
glEnableVertexAttribArray(0);
GLsizei quad_stride = 4 * sizeof(float);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, quad_stride, (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, quad_stride, (void*)(2 * sizeof(float)));
// Shader Configuration
// --------------------
postprocessingShader.use();
postprocessingShader.setInt("screenTexture", 0);
// Global Settings
// ---------------
glPointSize(2.0f);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// per-frame time logic
// --------------------
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// input
// -----
processInput(window);
// Update Particle Positions
// ------
particleComputeShader.use();
particleShader.setFloat("u_time", currentFrame);
glBindVertexArray(PARTICLE_VAO);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, PARTICLE_VBO);
glDispatchCompute(numberOfParticles / 1024, 1, 1);
glMemoryBarrier(GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT);
// Render Particles
// ------
glBindFramebuffer(GL_FRAMEBUFFER, particleFBO);
glEnable(GL_DEPTH_TEST);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
particleShader.use();
particleShader.setVec4f("u_color", 1.0f, 1.0f, 1.0f, 1.0f);
glBindVertexArray(PARTICLE_VAO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, particleTexture);
glDrawArrays(GL_POINTS, 0, numberOfParticles);
// Post-Processing
// ------
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDisable(GL_DEPTH_TEST); // Make sure quad is rendered on top of all other
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
postprocessingShader.use();
glBindVertexArray(POST_PROCESSING_VAO);
glBindTexture(GL_TEXTURE_2D, particleTexture);
glDrawArrays(GL_TRIANGLES, 0, 6);
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &PARTICLE_VAO);
glDeleteBuffers(1, &PARTICLE_VBO);
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}
Shader:
#ifndef SHADER_H
#define SHADER_H
#include "../include/glad/glad.h"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include "GLHelpers.hpp"
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
class Shader
{
public:
unsigned int ID;
// constructor generates the shader on the fly
// ------------------------------------------------------------------------
Shader(const char* vertexPath, const char* fragmentPath)
{
// 1. retrieve the vertex/fragment source code from filePath
std::string vertexCode = loadSourceCode(vertexPath);
unsigned int vertex = compileShaderCode("VERTEX", vertexCode);
std::string fragmentCode = loadSourceCode(fragmentPath);
unsigned int fragment = compileShaderCode("FRAGMENT", fragmentCode);
// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");
// delete the shaders as they're linked into our program now and no longer necessary
glDeleteShader(vertex);
glDeleteShader(fragment);
}
Shader(const char* computePath)
{
std::string computeCode = loadSourceCode(computePath);
glCheckError();
unsigned int compute; compute = compileShaderCode("COMPUTE", computeCode);
glCheckError();
// shader Program
ID = glCreateProgram();
glAttachShader(ID, compute);
glCheckError();
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");
glDeleteShader(compute);
}
// activate the shader
// ------------------------------------------------------------------------
void use()
{
glUseProgram(ID);
}
// utility uniform functions
// ------------------------------------------------------------------------
void setBool(const std::string &name, bool value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
// ------------------------------------------------------------------------
void setInt(const std::string &name, int value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
// ------------------------------------------------------------------------
void setFloat(const std::string &name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
// ------------------------------------------------------------------------
void setVec2f(const std::string &name, float value1, float value2) const
{
glUniform2f(glGetUniformLocation(ID, name.c_str()), value1, value2);
}
// ------------------------------------------------------------------------
void setVec3f(const std::string &name, float value1, float value2, float value3) const
{
glUniform3f(glGetUniformLocation(ID, name.c_str()), value1, value2, value3);
}
// ------------------------------------------------------------------------
void setVec4f(const std::string &name, float value1, float value2, float value3, float value4) const
{
glUniform4f(glGetUniformLocation(ID, name.c_str()), value1, value2, value3, value4);
}
// ------------------------------------------------------------------------
void setMat4(const std::string &name, glm::mat4 value) const
{
glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, glm::value_ptr(value));
}
private:
unsigned int compileShaderCode(std::string type, std::string sourceCode){
const char* shaderCode = sourceCode.c_str();
unsigned int shader;
if (type == "VERTEX"){
shader = glCreateShader(GL_VERTEX_SHADER);
} else if (type == "FRAGMENT"){
shader = glCreateShader(GL_FRAGMENT_SHADER);
} else if (type == "COMPUTE"){
shader = glCreateShader(GL_COMPUTE_SHADER);
} else {
std::cout << "ERROR::UNKNOWN_SHADER_TYPE '" << type << "'" << std::endl;
}
glShaderSource(shader, 1, &shaderCode, NULL);
glCompileShader(shader);
checkCompileErrors(shader, type);
return shader;
}
std::string loadSourceCode(const char* shaderPath) {
std::string shaderCode;
std::ifstream shaderFile;
// ensure ifstream objects can throw exceptions:
shaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
try
{
// open files
shaderFile.open(shaderPath);
std::stringstream shaderStream;
// read file's buffer contents into streams
shaderStream << shaderFile.rdbuf();
// close file handlers
shaderFile.close();
// convert stream into string
return shaderStream.str();
}
catch (std::ifstream::failure& e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ '" << shaderPath <<"'" << std::endl;
return "";
}
}
// utility function for checking shader compilation/linking errors.
// ------------------------------------------------------------------------
void checkCompileErrors(unsigned int shader, std::string type)
{
int success;
char infoLog[1024];
if (type != "PROGRAM")
{
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(shader, 1024, NULL, infoLog);
std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
}
}
else
{
glGetProgramiv(shader, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(shader, 1024, NULL, infoLog);
std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
}
}
}
};
#endif
GLHelpers:
#ifndef GLHELPERS_H
#define GLHELPERS_H
#include "../include/glad/glad.h"
#include <string>
#include <iostream>
GLenum glCheckError_(const char *file, int line)
{
GLenum errorCode;
while ((errorCode = glGetError()) != GL_NO_ERROR)
{
std::string error;
switch (errorCode)
{
case GL_INVALID_ENUM: error = "INVALID_ENUM"; break;
case GL_INVALID_VALUE: error = "INVALID_VALUE"; break;
case GL_INVALID_OPERATION: error = "INVALID_OPERATION"; break;
case GL_STACK_OVERFLOW: error = "STACK_OVERFLOW"; break;
case GL_STACK_UNDERFLOW: error = "STACK_UNDERFLOW"; break;
case GL_OUT_OF_MEMORY: error = "OUT_OF_MEMORY"; break;
case GL_INVALID_FRAMEBUFFER_OPERATION: error = "INVALID_FRAMEBUFFER_OPERATION"; break;
}
std::cout << error << " | " << file << " (" << line << ")" << std::endl;
}
return errorCode;
}
#define glCheckError() glCheckError_(__FILE__, __LINE__)
#endif