Skip to content

Instantly share code, notes, and snippets.

@paulhoux
Created July 16, 2015 04:11
Show Gist options
  • Save paulhoux/668ffb8074d1799bc605 to your computer and use it in GitHub Desktop.
Save paulhoux/668ffb8074d1799bc605 to your computer and use it in GitHub Desktop.
#version 410
// From our C++ code, we will tell the
// shader how many seconds have elapsed.
uniform float uTime = 0.0;
// Adjust for different aspect ratios.
uniform float uAspectRatio = 1.0;
// The GPU rasterizer has interpolated the
// texture coordinates, which now have the
// correct values for this pixel (fragment).
in vec2 vertTexCoord0;
// The output is an RGBA color, even if we
// only want grayscale.
out vec4 fragColor;
void main( void )
{
// Start with black.
fragColor = vec4( 0, 0, 0, 1 );
// Be creative :)
vec2 uv = vertTexCoord0 * 2.0 - 1.0;
uv.x *= uAspectRatio;
float circular = dot( uv, uv );
float polar = atan( uv.y, uv.x );
fragColor.r += 0.5 + 0.5 * cos( 4.0 * circular + 0.2 * uTime );
fragColor.r *= 0.5 + 0.5 * cos( 5.0 * polar - 0.5 * uTime );
}
#version 410
// Automatically provided by Cinder.
uniform mat4 ciModelViewProjection;
// Input attributes automatically provided by Cinder.
in vec4 ciPosition;
in vec2 ciTexCoord0;
// Output attributes which we will have to provide ourselves.
out vec2 vertTexCoord0;
void main( void )
{
// Output the vertex coordinate. The GPU will
// interpolate it for us, so that it has the
// correct value for every pixel.
vertTexCoord0 = ciTexCoord0;
// OpenGL requires us to transform every vertex
// to so-called normalized device coordinates.
// This sounds harder than it is:
gl_Position = ciModelViewProjection * ciPosition;
}
#version 410
// The output is an RGBA color.
out vec4 fragColor;
void main(void)
{
// Make the particles green.
fragColor.rgb = vec3( 0, 0.25, 0.1 );
// With a little trick, we can make them round.
vec2 uv = gl_PointCoord.xy * 2.0 - 1.0;
float r = dot( uv, uv );
fragColor.a = 1.0 - smoothstep( 0.45, 0.55, r );
}
#version 410
// Automatically provided by Cinder.
uniform mat4 ciModelViewProjection;
layout(location = 0) in vec4 iPositionVelocity; // xy = position, zw = velocity
void main(void)
{
// Just output the position and a size.
gl_PointSize = 8.0;
gl_Position = ciModelViewProjection * vec4( iPositionVelocity.xy, 0.0, 1.0 );
}
#version 410
// We'd like to know the size of the window.
uniform vec2 uWindowSize;
// We'd like to sample the dynamic background.
uniform sampler2D uTexBackground;
// Automatically provided by Cinder.
uniform mat4 ciModelViewProjection;
// Input attributes: our position and velocity.
layout(location = 0) in vec4 iPositionVelocity;
// Output attributes: should be the same.
layout(location = 0) out vec4 oPositionVelocity;
void main( void )
{
// Start by copying the current data.
oPositionVelocity = iPositionVelocity;
// Convert particle position to a normalized texture coordinate,
// so that we can sample the background texture. This is simple
// in our case, because the texture has the same size as the window.
vec2 coord = oPositionVelocity.xy / uWindowSize;
// Sample the background image four times and
// determine the slope.
float top = textureOffset( uTexBackground, coord, ivec2(0, -1) ).r;
float left = textureOffset( uTexBackground, coord, ivec2(-1, 0) ).r;
float bottom = textureOffset( uTexBackground, coord, ivec2(0, 1) ).r;
float right = textureOffset( uTexBackground, coord, ivec2(1, 0) ).r;
vec2 slope = vec2( right - left, bottom - top );
// Update velocity. Particle will slow down when going uphill.
oPositionVelocity.zw = 0.998 * oPositionVelocity.zw - 1.0 * slope;
// Update position.
oPositionVelocity.xy += oPositionVelocity.zw;
// Make sure the particle does not leave the window.
if( oPositionVelocity.x < 0 )
oPositionVelocity.x += uWindowSize.x;
else if( oPositionVelocity.x >= uWindowSize.x )
oPositionVelocity.x -= uWindowSize.x;
if( oPositionVelocity.y < 0 )
oPositionVelocity.y += uWindowSize.y;
else if( oPositionVelocity.y >= uWindowSize.y )
oPositionVelocity.y -= uWindowSize.y;
}
#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"
#include "cinder/Rand.h"
using namespace ci;
using namespace ci::app;
using namespace std;
class VivekSampleApp : public App {
public:
// Allows us to override default window size, among other things.
static void prepare( Settings *settings );
void setup() override;
void update() override;
void draw() override;
//! Called when the window is resized.
void resize() override;
void keyDown( KeyEvent event ) override { setupShaders(); }
private:
void setupShaders();
void updateBackground();
void renderBackground();
void setupParticles();
void updateParticles();
void renderParticles();
private:
gl::FboRef mBackgroundFbo;
gl::GlslProgRef mBackgroundShader;
gl::GlslProgRef mParticleTransformShader;
gl::GlslProgRef mParticleShader;
typedef struct {
ci::gl::VaoRef mVao;
ci::gl::VboRef mVbo;
ci::gl::TransformFeedbackObjRef mTransformFeedback;
} ParticleBuffer;
// For our particle system, we need a set of two buffers:
// one buffer to read from, one buffer to write to.
std::array<ParticleBuffer, 2> mBuffers;
// We will alternate these buffers, also called ping-ponging.
uint8_t mBufferReadIndex = 0;
uint8_t mBufferWriteIndex = 1;
// Define the number of particles.
const uint32_t kNumParticles = 16384;
};
void VivekSampleApp::prepare( Settings * settings )
{
settings->setWindowSize( 1024, 768 );
}
void VivekSampleApp::setup()
{
// Load the shaders.
setupShaders();
// Initialize our particles.
setupParticles();
// Allow the application to run at the same
// frame rate as your monitor.
gl::enableVerticalSync( true );
disableFrameRate();
}
void VivekSampleApp::update()
{
// Use a fixed time step for a steady 60 updates per second,
// regardless of our frame rate. Feel free to change this.
static const double timestep = 1.0 / 60.0;
// Keep track of time.
static double time = getElapsedSeconds();
static double accumulator = 0.0;
// Calculate elapsed time since last frame.
double elapsed = getElapsedSeconds() - time;
time += elapsed;
// Update stuff.
accumulator += math<double>::min( elapsed, 0.1 ); // prevents 'spiral of death'
while( accumulator >= timestep ) {
// Update our background.
updateBackground();
// Update our particles.
updateParticles();
accumulator -= timestep;
}
}
void VivekSampleApp::draw()
{
// Clear the main buffer (our window).
gl::clear();
// Render the dynamic background.
renderBackground();
// Render the particles.
renderParticles();
}
void VivekSampleApp::resize()
{
// Tell OpenGL we only want to use a single channel (GL_RED),
// because it's meant to be a grayscale background.
// The swizzleMask tells OpenGL that the green and blue channels
// use the information in the red channel.
auto tfmt = gl::Texture2d::Format().internalFormat( GL_RED ).swizzleMask( GL_RED, GL_RED, GL_RED, GL_ONE );
auto fmt = gl::Fbo::Format().colorTexture( tfmt );
// Create a frame buffer object for our dynamic background.
// It will have the same size as the window.
mBackgroundFbo = gl::Fbo::create( getWindowWidth(), getWindowHeight(), fmt );
}
void VivekSampleApp::setupShaders()
{
// Load the background shader, which creates the dynamic
// height map. Always use a try-catch block, so we know
// if something went wrong.
try {
auto vertexFile = loadAsset( "background.vert" );
auto fragmentFile = loadAsset( "background.frag" );
mBackgroundShader = gl::GlslProg::create( vertexFile, fragmentFile );
}
catch( const std::exception &exc ) {
console() << "Failed to load background shader: " << exc.what() << std::endl;
}
// Load the particle transform shader, which takes care of
// animating the particles on the GPU.
try {
auto vertexFile = loadAsset( "transform.vert" );
auto fmt = gl::GlslProg::Format()
.vertex( vertexFile )
.attribLocation( "iPositionVelocity", 0 )
.feedbackVaryings( { "oPositionVelocity" } )
.feedbackFormat( GL_INTERLEAVED_ATTRIBS );
mParticleTransformShader = gl::GlslProg::create( fmt );
}
catch( const std::exception &exc ) {
console() << "Failed to load transform shader: " << exc.what() << std::endl;
}
// Load the particle shader, which draws the particles as
// point sprites.
try {
auto vertexFile = loadAsset( "particles.vert" );
auto fragmentFile = loadAsset( "particles.frag" );
mParticleShader = gl::GlslProg::create( vertexFile, fragmentFile );
}
catch( const std::exception &exc ) {
console() << "Failed to load particle shader: " << exc.what() << std::endl;
}
}
void VivekSampleApp::updateBackground()
{
// First, we tell OpenGL that we'd like to render to the frame buffer,
// instead of to our window. This is called 'binding the frame buffer'.
// We use a helper that will automatically unbind the buffer when we
// exit this function.
gl::ScopedFramebuffer fbo( mBackgroundFbo );
// Next, we will have to make sure that our viewport has the same
// size as the frame buffer.
gl::ScopedViewport viewport( ivec2( 0 ), mBackgroundFbo->getSize() );
// Next, we tell OpenGL we want to draw in 2D, so we disable the
// depth buffer and make sure our view and projection matrices
// are setup for 2D drawing. Again, we use helpers that restore
// the previous settings when we exit this function.
gl::ScopedDepth depth( false, false );
gl::ScopedMatrices matrices;
gl::setMatricesWindow( mBackgroundFbo->getSize(), true );
// Next, we activate the shader. For every pixel, it will
// evaluate its position and the current time to render a
// dynamic height map. So we need to tell it what the current
// time is, by passing it as a uniform variable.
gl::ScopedGlslProg shader( mBackgroundShader );
mBackgroundShader->uniform( "uTime", float( getElapsedSeconds() ) );
// We will also adjust for the window's aspect ratio, so our
// circles are indeed circles and not ellipses.
mBackgroundShader->uniform( "uAspectRatio", getWindowAspectRatio() );
// Finally, we simply run the shader for every pixel in the
// frame buffer. We do this by drawing a rectangle the size
// of the full buffer.
gl::drawSolidRect( mBackgroundFbo->getBounds(), vec2( 0 ), vec2( 1 ) );
// Thanks to the gl::Scoped* helpers, we don't have to reset
// anything manually, the OpenGL state will be restored to
// how it was before we called this function.
}
void VivekSampleApp::renderBackground()
{
// Draw the contents of the frame buffer.
// First, activate a default shader that simply samples a texture.
gl::ScopedGlslProg shader( gl::getStockShader( gl::ShaderDef().texture() ) );
// Bind the texture and render a full screen rectangle to run the shader
// for every pixel.
gl::ScopedTextureBind tex0( mBackgroundFbo->getColorTexture(), 0 );
gl::drawSolidRect( getWindowBounds(), vec2( 0 ), vec2( 1 ) );
}
void VivekSampleApp::setupParticles()
{
// Our particle system will use transform feedback. Every frame, we will
// read the current particle data from the read buffer, transform it in a
// vertex shader and then write it to the write buffer.
// Each particle will have a 2D position and a 2D velocity.
// Since shaders prefer to use data in groups of 4 floats, we'll
// pack the information into a single vec4, where xy = position
// and zw = velocity.
// Create the initial data.
std::vector<vec4> initialData( kNumParticles );
for( size_t i = 0; i < kNumParticles; ++i ) {
vec2 position = vec2( Rand::randFloat() * getWindowWidth(), Rand::randFloat() * getWindowHeight() );
vec2 velocity = vec2( 0 );
initialData[i] = vec4( position, velocity );
}
// Create the two buffers.
for( size_t i = 0; i < mBuffers.size(); i++ ) {
// Create a vertex array object and bind it. We use a helper to
// automatically unbind it at the end of the for-loop.
mBuffers[i].mVao = gl::Vao::create();
gl::ScopedVao vao( mBuffers[i].mVao );
// Store our initial data in a vertex buffer object. We use GL_STATIC_DRAW, because we don't
// need to access or change the data from our CPU.
mBuffers[i].mVbo = gl::Vbo::create( GL_ARRAY_BUFFER, initialData.size() * sizeof( vec4 ), initialData.data(), GL_STATIC_DRAW );
mBuffers[i].mVbo->bind();
// We only use a single attribute. It has a size of 4 floats.
gl::vertexAttribPointer( 0, 4, GL_FLOAT, GL_FALSE, 0, (const GLvoid *)0 );
gl::enableVertexAttribArray( 0 );
// Initialize the transform feedback buffer.
mBuffers[i].mTransformFeedback = gl::TransformFeedbackObj::create();
mBuffers[i].mTransformFeedback->bind();
gl::bindBufferBase( GL_TRANSFORM_FEEDBACK_BUFFER, 0, mBuffers[i].mVbo );
mBuffers[i].mTransformFeedback->unbind();
}
}
void VivekSampleApp::updateParticles()
{
// Activate the shader that animates the particles.
gl::ScopedGlslProg shader( mParticleTransformShader );
// Tell the shader the size of the window. The background texture can be
// found in texture unit 0.
mParticleTransformShader->uniform( "uWindowSize", vec2( getWindowSize() ) );
mParticleTransformShader->uniform( "uTexBackground", 0 );
// Bind the background texture.
gl::ScopedTextureBind tex0( mBackgroundFbo->getColorTexture(), 0 );
// We're going to write to the write buffer.
gl::ScopedVao vao( mBuffers[mBufferWriteIndex].mVao.get() );
// We don't need the rasterizer, because we only use a vertex shader.
gl::ScopedState state( GL_RASTERIZER_DISCARD, true );
// Let Cinder set all default uniforms and attributes for us.
gl::setDefaultShaderVars();
// Use the contents of the read buffer as input.
mBuffers[mBufferReadIndex].mTransformFeedback->bind();
// Run the shader for all particles.
gl::beginTransformFeedback( GL_POINTS );
gl::drawArrays( GL_POINTS, 0, (GLsizei)kNumParticles );
gl::endTransformFeedback();
// The write buffer now becomes the read buffer and vice versa.
swap( mBufferReadIndex, mBufferWriteIndex );
}
void VivekSampleApp::renderParticles()
{
// Use additive blending, because it looks so cool.
gl::ScopedBlendAdditive blend;
// Read from the right vertex array object.
gl::ScopedVao vao( mBuffers[mBufferReadIndex].mVao.get() );
// Allow the shader to set the size of the point sprites.
gl::ScopedState state( GL_PROGRAM_POINT_SIZE, true );
// Bind the shader.
gl::ScopedGlslProg shader( mParticleShader );
// Let Cinder set all default uniforms and attributes for us.
gl::setDefaultShaderVars();
// Draw the particles.
gl::drawArrays( GL_POINTS, 0, (GLsizei)kNumParticles );
}
CINDER_APP( VivekSampleApp, RendererGl( RendererGl::Options().msaa( 8 ) ), &VivekSampleApp::prepare )
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment