#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 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::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 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 )