Lib Cinder
Lib Cinder
using namespace ci; using namespace ci::app; These using statements are just a shortcut to tell the C++ compiler, if it's ever unclear, I am talking about namespace whatever, but I am not going to keep typing whatever:: everywhere. There is a list of the namespaces inside Cinder here. Now that you understand the basic workings for any Cinder application, feel free to hit Run (or build or whatever button makes it go). You should see a 800x600 pixel window filled with black.
Congratulations. You have just created your new blank canvas: a black expanse filled with potential. It is a single line of code and a perfect place to start. This is how you clear the screen to black in Cinder. gl::clear(); If you are familiar with OpenGL, you will note that this is just a convenience method provided by Cinder. All gl::clear() is doing is wrapping up a few lines of code into one easy to use function. The actual code executed by gl::clear() is shown below. void clear( const ColorA &color, bool clearDepthBuffer ) { glClearColor( color.r, color.g, color.b, color.a );
if( clearDepthBuffer ) { glDepthMask( GL_TRUE ); glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); } else glClear( GL_COLOR_BUFFER_BIT ); } For example, if you wanted to clear the background to red and also clear the depth buffer, you would write gl::clear( Color( 1, 0, 0 ), true ); It is much nicer to just deal with that single line of code instead of needing to write out the full OpenGL syntax to clear the screen. As we continue, we will encounter many other convenience methods. They are entirely optional. If you'd rather write out the whole thing, be our guest. By the way, Color() is just a class provided by Cinder to help describe and manipulate color data. Moving along, perhaps you want the background to cycle between white and black. You could make use of getElapsedSeconds(), which will return a float equal to the number of seconds since the app started. The following gray variable oscillates between 0.0 and 1.0. float gray = sin( getElapsedSeconds() ) * 0.5f + 0.5f; gl::clear( Color( gray, gray, gray ), true ); Animation! Give yourself a pat on the back.
This is where you say that you want your app class to have a gl::Texture object and it is going to be called myImage. This line of code goes in the App class declarations. 3) Load an image into the texture you just declared. myImage = gl::Texture( loadImage( loadResource( "image.jpg" ) ) ); Now that you have declared a new gl::Texture object, you need to put some image data into that gl::Texture. There are myriad ways to do this. In this example we are assuming you've got a resource in your application that is a JPEG file called image.jpg. We can load this resource using loadResource(), and we pass the result of that to loadImage(), and in turn construct our gl::Texture with the image that comes back. This line of code would go into your setup() method. (By the way, this is the Mac OS X way of using resources, and the Windows way is just a bit different, but we won't get into the subtleties here. If you would like to take a break and read about how to use and manage resources, check out Using Resources in Cinder). 4) Draw the Texture into the app window. gl::draw( myImage, getWindowBounds() ); Finally, you place this line in the draw() function and it will draw the gl::Texture so that it fills the app window. This is another Cinder convenience method. Behind the scenes there are OpenGL calls to create a textured GL_TRIANGLE_STRIP. As we mentioned before, you can write out all the OpenGL yourself if you choose. Either way is fine, but for drawing things like images or circles or other simple forms, it is great to have these one-liner solutions. And what does a loaded and drawn image look like? Well, if you use a picture of Paris the kitty, it would look a bit like this.
Notice the second parameter to getOpenFilePath(), which is the result of ImageIo::getLoadExtensions(). This is a quick way to tell the open dialog, "only the let user pick files whose extensions correspond with the types of images I know how to load." The second way of getting images into your application is to load them directly from a Url. This is surprisingly easy. Url url( "http://validurl.com/image.jpg" ); myImage = gl::Texture( loadImage( loadUrl( url ) ) ); Keep in mind that you should not try to draw the texture until after something has been loaded into it. We should check to make sure myImageis a valid gl::Texture before attempting to use it. We can do this with a simple if statement: if( myImage ) gl::draw( myImage, getWindowBounds() );
DRAWING SHAPES
Drawing shapes is just as easy. If you want to draw a circle of a radius of x, you can use gl::drawSolidCircle(). The following line of code will draw a filled circle centered at (15,25) with a radius of 50. gl::drawSolidCircle( Vec2f( 15.0f, 25.0f ), 50.0f ); The circle that is created is actually an OpenGL TRIANGLE_FAN. The number of triangles comprising the fan can be controlled by an optional third parameter. If left blank, the circle will be created with as much detail as is needed based on the circle's radius. For example, the following code will create a filled hexagon. Note that the detail parameter represents the number of vertices to draw. Since we are drawing a triangle fan, we need to include the center point which brings the total vertices to 7, not 6. gl::drawSolidCircle( Vec2f( 15.0f, 25.0f ), 50.0f, 7 ); There are similar methods for drawing all manner of basic geometry, both 2D and 3D. Check the reference for the full list. Not content with a stationary circle? That is easily fixed. float x = cos( getElapsedSeconds() ); float y = sin( getElapsedSeconds() );
gl::drawSolidCircle( Vec2f( x, y ), 50.0f ); Now we have a circle that moves in a 1 pixel radius trajectory around the origin (0,0). A 1 pixel radius around the origin? What good is that? Well, we are breaking this process down step by step so you can see how to evolve a sketch. If you were to just skip ahead to the final code you miss out on how it was derived. First, lets put our circle closer to the center of the app window. Right now, the circle is drawn in the upper left corner of the screen (the origin). We can use getWindowWidth() and getWindowHeight() to retrieve the dimensions of the window and add half their respective values to the x and y variables. float x = cos( getElapsedSeconds() ) + getWindowWidth() / 2; float y = sin( getElapsedSeconds() ) + getWindowHeight() / 2; gl::drawSolidCircle( Vec2f( x, y ), 50.0f ); We can simplify this further by using getWindowSize(), which returns a Vec2i representing the dimensions of the app window. We can add half of the window size to circle and this will also move it to the middle of the screen. float x = cos( getElapsedSeconds() ); float y = sin( getElapsedSeconds() ); gl::drawSolidCircle( Vec2f( x, y ) + getWindowSize() / 2, 50.0f ); Now that we have moved our circle to the center of the screen, lets fix the radius of the sine and cosine offset. Currently, our circle is moving but the range of its movement is 2 pixels so it isn't very lively. If you want your circle to move in a 100 pixel radius circular orbit, just multiply the x and y variables by 100.0. float x = cos( getElapsedSeconds() ) * 100.0f; float y = sin( getElapsedSeconds() ) * 100.0f; gl::drawSolidCircle( Vec2f( x, y ) + getWindowSize() / 2, 50.0f ); Finally we are going to make the circle's radius change in relation to its x position. Since x spends as much time as a negative number as it does a positive number, we will go ahead and use the absolute value of x. gl::drawSolidCircle( Vec2f( x, y ) + getWindowSize() / 2, abs( x ) );
These last few steps, though tiny, are a great example why we should go ahead and make a class for this circle. If we ever wanted to draw two or more circles, each with their own position, speed, and size, it becomes necessary to package up this data into its own class to make it easier to access each circle individually. We could say circle1 has a position of loc1 with a size of radius1, and then do the same with circle2and circle3 and so on. However, when you want to start dealing with thousands of circles, it quickly becomes obvious that we should rethink how we are approaching this problem. First, we will create a controller class. This just makes it easy to segregate Particle-related code. This new class is calledParticleController and as the name suggests, it is in charge of controlling the Particles. It will have its own update() and draw() methods. update() will iterate through all of the Particles and tell each one to run its own personal update() method. After all theParticles are updated, the ParticleController then tells each of the Particles to draw(). The Particle class is based what we did with the circle above. Each Particle has a position in space, a direction of travel, a speed of travel, a size, and whatever else you want to add to give each Particle its own personality. Later on, we will add a few more variables. Here is a summary of the Particle class code (the full source is contained in cinder/tour/Chapter 1/) Particle::Particle( Vec2f loc ) { mLoc = loc; mDir = Rand::randVec2f(); mVel = Rand::randFloat( 5.0f ); mRadius = 5.0f; } void Particle::update() { mLoc += mDir * mVel; } void Particle::draw() { gl::drawSolidCircle( mLoc, mRadius ); } The ParticleController, which we will discuss in a moment, is responsible for creating new Particles. For now, we will also task theParticleController with saying where the new Particle should be created and we pass that location in the constructor. The Particle then determines which direction it is traveling, in this case that direction is a random normalized 2D vector, as well as what speed it is traveling. We'll discuss these Rand functions in
more detail in the next chapter. Note: The variables in the Particle class all begin with the letter 'm'. This is just a naming convention to let me know at a glance which variables are member variables. It is a good habit to get into and comes in very handy when the class grows to hundreds of lines of code. Let's have a peek at ParticleController.h. #pragma once #include "Particle.h" #include <list> class ParticleController { public: ParticleController(); void update(); void draw(); void addParticles( int amt ); void removeParticles( int amt ); std::list<Particle> mParticles; }; Not much to it. The ParticleController::update() method tells all the Particles to update. The ParticleController::draw()method tells all the Particles to draw. And the addParticles() and removeParticles() methods will create or destroy the supplied amount of Particles. All of the Particles are kept in a list. This is a class built-in to C++ which maintains a linked list of objects. If you're new to C++, you should definitely familiarize yourself with these built-in classes (called the STL) - they are extremely fast and powerful. A nice list and discussion of them is available here. If you want to add a new Particle to the end of the list, you use push_back: float x = Rand::randFloat( app::getWindowWidth() ); float y = Rand::randFloat( app::getWindowHeight() ); mParticles.push_back( Particle( Vec2f( x, y ) ) ); And as you might have guessed, to remove a Particle from the end of the list, you use pop_back(). Eventually you are going to want more control over which Particles to remove. For instance, a Particle moves offscreen and you no longer need it around. You cannot rely on pop_back() because it is highly unlikely that the Particle at the end of the list will also be the one that just moved offscreen. We will solve this problem a little later in the tutorial.
In order to tell each of the Particles in our list to update() or draw(), we use an iterator. The iterator is simply a way to access all the items in a list one by one. void ParticleController::update() { for( list<Particle>::iterator p = mParticles.begin(); p != mParticles.end(); ++p ){ p->update(); } } That is just about all we need. All that remains is to add the appropriate ParticleController method calls in the App class and we are done. After we declare our ParticleController, called mParticleController, we add the following line to the setup() method: mParticleController.addParticles( 50 );
And finally, the draw() method: gl::clear( Color( 0, 0, 0 ), true ); mParticleController.draw(); When you build and run the project, you should see 50 white circles appear in random locations and move in random directions.
Since each Particle controls its own variables, we can create remarkably complex (looking) results by simply adding one or two additional instructions for the Particles to execute. For example, if we wanted each Particle to have a random radius, we need only to add a single line of code. That is a deceptively powerful realization. One line of code is all it takes to modify the look of thousands of individuals. Just to clarify, this is not a concept unique to Cinder. This is in no way a new or personal epiphany. As I continue to develop this tutorial, the project is going to become more and more complex. I find it useful to point out key aspects of my process regardless of whether they might be obvious or not. This notion of creating a large number of individual objects, each with its own interpretation of a single set of instructions, is what the majority of my code explorations are based upon. mRadius = Rand::randFloat( 1.0f, 5.0f );
Rand is a class that helps you create random numbers to your specifications. If you just want a random float between 0.0 and 1.0, you write: float randomFloat = Rand::randFloat(); If you prefer to get a random float in a weirder range, you can do this: float randomFloat = Rand::randFloat( 5.0f, 14.0f ); That will give you a random number between 5.0f and 14.0f. This also works for ints. And happily, you can do the same thing for 2D and 3D vectors. If you ask for a randVec2f(), you get a 2D vector that has a length of 1.0. In other words, you get a point located on a circle that has a radius of 1.0. If you use randVec3f(), you will get back a point located on the surface of a unit sphere.
This process is pretty much how I learned trig. I read all about sine equals opposite over hypotenuse in college but I didn't appreciate the nature of trigonometry until I started experimenting with code. My early days of creating generative graphics was about 10% creative thinking and 90% "What if I stick sin(y) here? Hmmm, interesting. What if I stick cos(x) here? Hmmm. How about tan(x)? Oops, nope. How about sin( cos( sin(y*k) + cos(x*k) ) )? Oooh, nice!". Incidentally, sin( cos( sin(y*k) + cos(x*k) ) ) looks something like this: float xyOffset = sin( cos( sin( mLoc.y * 0.3183f ) + cos( mLoc.x * 0.3183f ) ) ) + 1.0f; mRadius = xyOffset * xyOffset * 1.8f;
It is time to give our project some motion. We are going to use the same method that we used to oscillate the background clear color.getElapsedSeconds() and getElapsedFrames() are extremely useful for prototyping some basic movement. float time = app::getElapsedSeconds();
Since we are calling this in our Particle class, we need to tell the Particle class where it can find getElapsedSeconds(). All we do is add this include line to the top of the Particle class. #include "cinder/app/AppBasic.h"
later in the tutorial series. Now that we have the color, we need to make sure OpenGL knows what color to draw our circle. We add this line to render() before we draw the solid circle. gl::color( mColor ); Now, every single one of our Particles has a new set of instructions to follow. Step 1) Find out the color from the Channel which corresponds with my current location. Step 2) Set my color to the returned Channel color. Step 3) Draw myself Each of the 4800 Particles goes through this set of instructions every frame. You might be thinking this is overkill. The Particle only needs to find out its color once. This could happen when each Particle is created and then you never need to make the calculation. This is entirely true. However, in a short while, we will want to animate some of these variables which means we will have to do these calculations every frame anyway. So in general, you should separate your variables and your constants. If a property is not going to change, just define it once and forget about it. However, if you need to animate this property over time, you should do this in the update() method which gets called every frame.
Well, that looks just about like we expected. Nothing special there. How about instead, we adjust each Particle's size and leave the color white. The Particles that should be brighter will be larger than the Particles that should be dark. void Particle::update( const Channel32f &channel ) { mRadius = channel.getValue( mLoc ) * 7.0f; } This code looks familiar enough. Pass in the reference to a Channel, get the grayscale value at the Particle's position, then set the radius to be equal to that value. A quick side-note about the demon that is the magic number. In the code above, I know exactly why I wrote 7.0. Since getData() for aChannel32f returns a value from 0.0 to 1.0, I decided I wanted that range to be larger. I arbitrarily chose 7.0. However, after a couple weeks of being away from the code I wrote, I may not remember why I wrote 7.0 or what that number is even supposed to represent. This doesn't necessarily mean you should replace all numbers with named constants. That would be overkill. Just be aware that when you use constants that are not defined (or at least, described with comments), you are potentially doing something you will regret later, and you are definitely doing something that other coders frown upon. Make an effort to minimize these magic numbers especially if you plan on sharing code with others. Instead of using 7.0, I have created a member variable called mScale which I initialized to 7.0. No more mystery. void Particle::update( const Channel32f &channel ) { mRadius = channel.getValue( mLoc ) * mScale; }
It is time to give the user some control. Chapter 3 will explore some options for user input.
CHAPTER 3: INFLUENCE
USER INTERACTION
It is time for some user interaction. Watching circles move on their own just isn't that satisfying. You want some direct control. There are many ways to accomplish this. You could use webcam input, microphone input, even the serial port. However, for now we are just going to focus on the two simplest ways to allow user interaction: keyboard and mouse.
KEYBOARD INPUT
First up, keyboard input. You might have noticed that a keyDown() method was added to the source code from the last chapter. Much likesetup(), update() and draw(), keyDown() is one of a few special functions (more properly called virtual functions in C++ nomenclature) which we can override to let our app do something based on a particular event. In our case we're not doing anything too crazy, just two boolean toggles to control what should be rendered. If you hit the '1' key, you toggle on or off the rendering of the source image. If you hit the '2' key, you toggle the rendering of the Particles.
void TutorialApp::keyDown( KeyEvent event ) { if( event.getChar() == '1' ){ mRenderImage = ! mRenderImage; } else if( event.getChar() == '2' ){ mRenderParticles = ! mRenderParticles; } } To check for special keys, you use event.getCode() instead of event.getChar(). Special keys include the arrow keys, shift, esc, ctrl, etc. For example, to check for the right arrow, you do this: if( event.getCode() == KeyEvent::KEY_RIGHT ) { console() << "Right Arrow pressed" << std::endl; } Oh, and notice the call to console(). This is a Cinder function which returns a class we can send text to, and it's a handy, cross-platform way to print out notes and debugging information. It behaves just like std::cout, and in fact on the Mac it is std::cout. However on the PC it calls some special code which prints each line to the Output window of Visual C++, or to a system-wide log viewable using the tool DebugView from Microsoft. You can also send many Cinder types directly to it, using something like: Color myColor( 1.0f, 0.5f, 0.25f ); console() << "myColor = " << myColor << std::endl. Moving on, let's imagine as an example you are creating a first-person shooter style camera. You will want to respond to key events by storing the state of a specific key. A good way to do this is to make a few boolean variables like isMovingForward and isJumping. If the 'w' key is pressed ('w' is how you move forward in default FPS controls), set isMovingForward to true. When the 'w' key is released, you setisMovingForward to false. void TutorialApp::keyDown( KeyEvent event ) { if( event.getChar() == 'w' ) { mIsMovingForward = true; } } void TutorialApp::keyUp( KeyEvent event ) { if( event.getChar() == 'w' ) { mIsMovingForward = false; } }
In your camera code, you would use these key states to determine what direction to move the camera. This will give you much better responsiveness than moving the camera only on keyDown() events which are periodic instead of constant.
MOUSE INPUT
Cinder offers five different mouse events which it can listen to. You can check for mouse button press and release, much like with theKeyEvents. You do this by overriding mouseDown() and mouseUp(). Additionally, you can check for left, right, or middle mouse button clicks as well as checking to see if any modifying keys were held down during the click. As an example, here is the code for checking to see if the right mouse button was clicked while the shift key was depressed. void TutorialApp::mouseDown( MouseEvent event ) { if( event.isRight() && event.isShiftDown() ) { console() << "Special thing happened!" << std::endl; } } In addition to button press state, you can also check for move and drag events. If the mouse is in motion, mouseMove() will fire every frame. If you happen to also have a mouse button pressed, mouseDrag() will fire instead. Finally, while we don't make use of it in this tutorial, Cinder supports mousewheel events via the mouseWheel() function. The next thing we are going to add to our tutorial is the ability to influence the Particles based on their proximity to the cursor. The first thing we want to do is use mouseMove() to get and store the cursor position, which we will keep in a new member variable called mMouseLoc. void TutorialApp::mouseMove( MouseEvent event ) { mMouseLoc = event.getPos(); } You will probably notice that while you are dragging the cursor, mouseMove() isn't triggered. This is because you have entered the domain of the mouseDrag() event. But what if you want to keep track of the mouse position even while dragging? Well, you could duplicate the code you have in the mouseMove() function, or simply tell mouseDrag() that it needs to call mouseMove(). void TutorialApp::mouseMove( MouseEvent event ) { mMouseLoc = event.getPos(); }
void TutorialApp::mouseDrag( MouseEvent event ) { mouseMove( event ); } Now that we are keeping track of the cursor position, we need to get that data to the Particles. Well, we can't talk to them without going through ParticleController first, so lets add mMouseLoc as a parameter for ParticleController::update(). Don't forget to make the change in your .h file. If C++ is new to you, this is a common source of compile errors - forgetting to make the required changes to both the .h and .cpp files. void ParticleController::update( const Channel32f &channel, const Vec2i &mouseLoc ) { for( list<Particle>::iterator p = mParticles.begin(); p != mParticles.end(); ++p ){ p->update( channel, mouseLoc ); } } We want to do the same thing to Particle::update(). And while we are poking around in the Particle class code, go ahead and add an additional Vec2f that we will call mDirToCursor. Think of each Particle as having an arrow which always points towards the mouse. This is what mDirToCursor will represent. To find out the mDirToCursor, you take the cursor location and subtract the Particle's location. This will give you a vector that points from theParticle all the way to the mouse. If we draw those vectors, it would look like this:
That is a bit more than we need. Instead we want a normalized vector, which is a vector that has a length of 1.0. We also need to account for the possibility that the mouse location and Particle location might be equal. If we try to normalize() a vector that has a length of zero, the computer will cry. Cinder has a solution to that problem. If you are unable to guarantee that the length will always be greater than zero, you can use safeNormalize() which will do that check for you. void Particle::update( const Channel32f &channel, const Vec2i &mouseLoc ) { mDirToCursor = mouseLoc - mLoc;
mDirToCursor.safeNormalize(); mRadius = channel.getData( mLoc ) * mScale; } If we cinder::Vec2::safeNormalize "safeNormalize()" mDirToCursor and run our project again, it will look like the image below. The length of the arrows is exaggerated to make it easier to see them. Also, you can use gl::drawVector() which asks for the start and end of your line segment and then draws the line and corresponding arrow head. The following code block shows how you would draw the arrows. void Particle::draw() { gl::color( Color( 1.0f, 1.0f, 1.0f ) ); float arrowLength = 15.0f; Vec3f p1( mLoc, 0.0f ); Vec3f p2( mLoc + mDirToCursor * arrowLength, 0.0f ); float headLength = 6.0f; float headRadius = 3.0f; gl::drawVector( p1, p2, headLength, headRadius ); } There are a couple points related to the Vector library we would like to mention. First, gl::drawVector() takes two Vec3f but we have been dealing with Vec2f all this time. The quick solution is to just turn the 2D vector into a 3D one by adding a z component and setting it to 0.0f. The other nice thing about C++ and vector libraries in particular is you have the ability to overload operators. An operator would be something like + or *. In most other programming languages, you can only use these operators with built-in types. However in C++, you canoverload these operators to allow you to use them with objects if you choose. The Cinder vector library allows you to add, subtract, multiply, and divide vectors using the corresponding operator. In the Particle::draw() method shown above, we are taking a Vec2f calledmDirToCursor and multiplying it by the arrowLength. Then we add that amount to mLoc.
It is starting to get really interesting! There are definitely a lot of good tangents to explore here. If you aren't thoroughly excited after reaching this step, then you might be dead inside. This mess of pointy arrows is positively overflowing with potential.
float time = app::getElapsedSeconds() * 4.0f; float dist = mDirToCursor.length() * 0.05f; float sinOffset = sin( dist - time ) * 100.0f; mDirToCursor.normalize(); Vec2f newLoc = mLoc + mDirToCursor * sinOffset; newLoc.x = constrain( newLoc.x, 0.0f, channel.getWidth() - 1.0f );
mDirToCursor
*= sinOffset * 15.0f;
Then, in our Particle::draw() method, we draw the circle at the original mLoc but we add the scaled mDirToCursor. gl::drawSolidCircle( mLoc + mDirToCursor, mRadius );
Congratulations! We have just created an incredibly simple and naive code-based representation of the wave/particle duality of nature and light. Let's continue. Now that we understand how to control our Particles, we can start to fine tune their behavior in Chapter 4.
location. It is that Particle's location. The radius is that Particle's radius. Any data that is specific to this particle should exist inside this Particle and nowhere else. When I think of a particle, I think of a dot in space. This dot is created, it follows the rules it was assigned, it is influenced by outside forces, and eventually it dies. Let's start with the act of creating. In the previous section, our ParticleController made a few thousand Particles right away. All the Particles existed until the user quit the app. That will be our first change. We remove the second ParticleController constructor (the one that made the grid of Particles). From now on, the user will have to use the mouse to make new Particles.
cursor, we will only see 1 particle." We remedy this situation by adding a random vector to the location when we create the new Particle. void ParticleController::addParticles( int amt, const Vec2i &mouseLoc ) { for( int i=0; i<amt; i++ ) { Vec2f randVec = Rand::randVec2f() * 10.0f; mParticles.push_back( Particle( mouseLoc + randVec ) ); } } So we are making a new Particle at the location of the mouse, but we are also offsetting it in a random direction that has a length of 10.0. In other words, our 5 new Particles will all exist on a circle that has a radius of 10.0 whose center is the cursor position.
PARTICLE DEATH
If we allow every Particle to live forever, we will very quickly start dropping frame rate as hundreds of thousands of Particles begin to accumulate. We need to kill off Particles every now and then. Or more accurately, we need to allow Particles to say when they are ready to die. We do this by keeping track of a Particle's age. Every Particle is born with an age of 0. Every frame it is alive, it adds 1 to its age. If the age is ever greater than the life expectancy, then theParticle dies and we get rid of it. First, lets add the appropriate variables to our Particle class. We need an age, a lifespan, and a boolean that is set to true if the age ever exceeds the lifespan. int mAge; int mLifespan; bool mIsDead; Be sure to initialize mAge to 0 and mLifespan to a number that makes sense for your project. We are going to allow every Particle to live until the age of 200. In our Particle's update() method, we increment the age and then compare it to the lifespan. mAge++; if( mAge > mLifespan ) mIsDead = true; Just having a Particle say "Im dead" is not quite enough. We need to also have the ParticleController clean up after the dead and remove them from the list of Particles.
If you look back at the ParticleController update() method, you see we are already iterating through the full list of Particles. We can put our death-check there. for( list<Particle>::iterator p = mParticles.begin(); p != mParticles.end(); ){ if( p->mIsDead ) { p = mParticles.erase( p ); } else { p->update( channel, mouseLoc ); ++p; } } For every Particle in the list, we check to see if its mIsDead is true. If so, then erase() it from the list. Otherwise, go ahead andupdate() the Particle. You might notice this for loop is a little different than you're used to seeing. This is because we don't always want to increment our list iterator p. We only want to increment it if the particle isn't dead. Otherwise we'll set p to be the result of calling erase() (this is standard practice for using the STL's list class). Hurray, you have just made a Particle cursor trail.
PARTICLE VELOCITY
Up until now, our Particles have been stationary. We did do some position perturbations in the last section, but the location of theParticle (mLoc) never changed. It is time to remedy this. We are going to finally make use of velocity. Velocity is the speed that something moves multiplied by the direction that something is moving. You can add velocity to position to get the new position. If velocity never changes, then the Particle will move in a straight line. That will be our first test case with Velocity.
It is incredibly simple, really. All you need is one additional Vec2f in your Particle. We will call it mVel. When you initialize mVel, you set it equal to a random 2D vector. mVel = Rand::randVec2f();
Since we are going to deal with constant velocity, we can just leave it at that. Each Particle, when it is created, is assigned a random velocity. To make the Particle obey that velocity, you add it to the location. mLoc += mVel;
When you run the project, as you click and drag you will create a trail of Particles that move away from their point of creation at a constant speed until they die.
Perhaps you don't want them to move forever. Maybe you just want them to exhibit a burst of velocity at birth but that velocity will trail off until the Particle isn't moving at all. To accomplish that, you simply multiply the velocity with a number less than 1.0. This is referred to as the rate of decay which we will call mDecay. mLoc += mVel; mVel *= mDecay;
As you can see, if we set mDecay to 1.0, the velocity will show no change over time. If we use a number greater than 1.0, the velocity will increase exponentially to infinity. This is why we try to keep the rate of decay less than 1.0. It is far more desirable a feature to have something slow to a stop than to have something speed up to infinity. But this is just a personal choice... feel free to go crazy! I am going to interject here for a moment and fix something that has been annoying me. As it stands,
all the Particles created in any given frame disappear at the same time. It feels rigid and obvious so lets use a little randomness to get us something more organic. mLifespan = Rand::randInt( 50, 250 ); There, all better. Now the Particles die at different rates. Moving on. Another aesthetic trick that is useful with Particles is to pay attention to the ratio of mAge/mLifespan. Or in many cases, 1.0 - mAge/mLifespan. Say, for example, you want to make the Particles shrink out of existence instead of just disappearing. If you have a number from 1.0 to 0.0 that represents how old it is in relation to how old it is allowed to get, you can multiply the radius by that age percentage to make the Particle fade away as it dies. float agePer = 1.0f - ( mAge / (float)mLifespan ); mRadius = 3.0f * agePer;
This is another tiny trick that has a surprisingly effective result. We currently have a scenario where it seems Particles are coming out of the mouse cursor. We can really push this effect by setting the initial velocity of the Particle to be equal to the velocity of the cursor. This will make it seem like Particles are being thrown from the cursor instead of just being passively deposited. Every frame, we are going to subtract the previous location of the cursor from the current location of the cursor in order to find the cursor's velocity. Once we have the cursor velocity, we can pass it to each Particle (like we do with the mouseLoc) and initialize the Particle'smVel with this new mouse-made velocity. If you go ahead and do this, you will probably find the results a little annoying. The Particles appear and move in awkward clumps. There are two things we can do to fix this.
1) We don't actually want the initial velocity to be the same as the mouse. Once tested, the initial movement feels to fast. It looks much better if we multiply it by .25. void ParticleController::addParticles( int amt, const Vec2i &mouseLoc, const Vec2f &mouseVel ) { for( int i=0; i<amt; i++ ) { Vec2f loc = mouseLoc + Rand::randVec2f() * 10.0f; Vec2f vel = mouseVel * 0.25f; mParticles.push_back( Particle( p, v ) ); } } 2) We should add a random vector with a random speed to our cursor velocity in order to make the Particles spread out a little more. Otherwise, every frame our cursor will make a few new Particles and send them all traveling in the same direction. Vec2f velOffset = Rand::randVec2f() * Rand::randFloat( 1.0f, 3.0f ); Vec2f vel = mouseVel * 0.25f + velOffset; What you have just seen is pretty much my entire coding process. Run the code. Find something that doesn't quite feel right. Tweak it. Repeat. An endless cycle of trying to make things slightly better. In keeping with this sentiment, I just noticed that the Particles shouldn't all decay at the same rate. Time to make that randomized as well. mDecay = Rand::randFloat( 0.95f, 0.99f );
Don't forget to #include "cinder/Perlin.h". Now that we have an instance of Perlin, we need
to pass it along to our Particles so they can make use of it. You will do that the same way you passed the Channel to each Particle. Once the Particle has the Perlin reference, you can use the Particle's location as an input and get back a float, or if you choose you can get back a Vec2f or Vec3f but that is a bit more time consuming. We are going to stick with just getting back a single float per Particle. float noise = perlin.fBm( Vec3f( mLoc * 0.005f, app::getElapsedSeconds() * 0.1f ) ); First, what the hell is fBm(), right? That stands for fractional Brownian motion. Google it! But in our case it's just the function we call to get a noise value for a particular location. Second, whats with the weird Vec3f made of only two parameters? Let me break it into a slightly different version to make it easier to see what I am doing. float nX = mLoc.x * 0.005f; float nY = mLoc.y * 0.005f; float nZ = app::getElapsedSeconds() * 0.1f; Vec3f v( nX, nY, nZ ); float noise = perlin.fBm( v ); The reason I am sending a 3D vector to Perlin is that I am interested in getting back the result based on the Particle's position and time. As time passes, even if the Particle is stationary (meaning that the first two parameters in the noise calculation are not changing), the Perlinnoise will continue to animate. So what do we do with that noise? Since we are dealing with Particles that are moving in a 2D space, we could treat the noise like an angle and use sin(angle) and cos(angle) to get an x and y offset for our Particle. Since the noise smoothly changes, our resulting angle will also smoothly change which means our Particles wont end up moving along a jagged path. float angle = noise * 15.0f; mVel += Vec2f( cos( angle ), sin( angle ) ); mLoc += mVel; Perlin fBm() returns a value between -1.0f and 1.0f. We chose to multiply that result by 15.0f to keep the Particle from favoring a specific direction. If that multiplier is too small, you will find that the Particle's will all generally move to the right. We want our Particles to move all over, hence the 15.0f. The math geeks will note that noise * 15.0f will give you a possible range of 30.0, and we all know there are only 2 or 6.28318 radians in a circle, So why not multiply noise by which will
give us a range of 2 ? Even though Perlin results will stay within the -1.0f to 1.0frange, this doesn't guarantee the results will give you an even distribution in that range. Often, you will find the Perlin results stay between-0.25f to 0.25f. If we simply multiply the noise by (creating a range from - to ), we will get randomized movement that appears to favor a specific direction. The way to avoid this is to spread the result out into a greater range. You should play around with these numbers to get a better idea of what I mean. What does it look like? Well, it looks like Perlin noise.
In fact, it looks a little too much like Perlin. This is what we were alluding to earlier in this section when we mentioned that subtlety is key. This effect, though pretty, looks like everyone else's Perlin effect. Don't believe me? Do a Google image search for Perlin noise flow field and you will see plenty of experiments that look just like this. Lets tone it back a bit. mVel += noiseVector * 0.2f * ( 1.0f - agePer );
We also threw in the ( 1.0f - agePer ) because we want the Perlin influence to be nonexistent at the Particle's creation and have it grow stronger as the Particle ages. This creates a nice effect in which the Particles push away from the cursor and as they dwindle in size they dance about more and more until they vanish. Sadly, this is not that exciting as a still image. We need to make a video. You'll notice these lines at the bottom of TutorialApp::draw(). if( mSaveFrames ){ writeImage( getHomeDirectory() + "image_" + toString( getElapsedFrames() ) + ".png", copyWindowSurface() ); } This makes use of the Cinder function writeImage(), which takes a file path as its first parameter, and an image as its second. In our case we'll want to use the built-in function copyWindowSurface(), which returns the window as a Surface. You can also pass writeImage() things like agl::Texture. You'll also notice the use of the function toString(), which is a handy function in Cinder which can take anything you can pass toconsole(), which includes all the C++ default types as well as many of the Cinder
classes, and return it in string form. So this call will send a sequence of images to your home directory, each named image_frame#.png. The resulting sequence of images can be assembled in QuickTime or pretty much any video program. The next chapter is quite exciting. We are going to show how to let the Particles interact with each other. Head on over to Chapter 5.
We are also adding a new variable to represent the Particle's mass which is directly related to the radius. The actual relationship is a matter of personal taste. Once we make use of the mass variable, you might find you like how things behave if your Particles are really massy. I like my Particles a little more floaty.
This formula is not based on anything other than trial and error. I tried setting the mass equal to the radius. Didn't like that. I tried mass equal to radius squared. Didn't like that. I eventually settled on taking a fraction of the radius squared.
Second round, we already handled all of p1's interactions so we move on to p2. Since p2 has already interacted with p1, all that is left is for p2 and p3 to repel each other. That is the logic for the nested iterators. list<Particle>::iterator p2 = p1; for( ++p2; p2 != mParticles.end(); ++p2 ) { Now that we are inside of the second iterator, we are dealing with a single pair of Particles, p1 and p2. We know both of their positions so we can find the vector between them by subtracting p1's position from p2's position. We can then find out how far apart they are by usinglength(). Vec2f dir = p1->mLoc - p2->mLoc; float dist = dir.length(); Here is where we run into our first problem. To do this repulsion force, we don't need the distance between the Particles. We need the distance squared. We could just multiply dist by dist and be done with it, but we have another option. When finding out the length of a vector, you first find out the squared distance, then you take the square root. The code for finding thelength() looks like this: sqrt( x*x + y*y )
You should try to avoid using sqrt() when possible, especially inside of a huge nested loop. It will definitely slow things down as the square root calculation is much more processor-intensive than just adding or multiplying. The good news is there is also a lengthSquared()method we can use. Vec2f dir = p1->mLoc - p2->mLoc; float distSqrd = dir.lengthSquared(); Next, we make sure the distance squared is not equal to zero. One of the next steps is to normalize the direction vector and we already know that normalizing a vector with a length of zero is a bad thing. if( distSqrd > 0.0f ){ Here is the sparkling jewel of our function. First, you go ahead and normalize the direction vector. This leaves you with a vector that has a length of one and can be thought of as an arrow pointing from p2 towards p1. dir.normalize();
The first factor which determines how much push each Particle has on the other is the inverse of the distance squared. float F = 1.0f / distSqrd; Since we already know that force equals mass times acceleration (Newton's 2nd law), we can find p2's contribution to p1's total acceleration by adding the force divided by p1's mass. p1->mAcc += ( F * dir ) / p1->mMass;
To find out p1's contribution to p2's total acceleration, you subtract force divided by p2's mass. p2->mAcc -= ( F * dir ) / p2->mMass;
If this all seems confusing to you, worry not. It still confuses me from time to time. With a little bit of practice, this code will start to feel more familiar. Now we can turn on our repulsion. In our App class, before we tell the ParticleController to update all the Particles, we trigger theParticleController::repulseParticles() method. We can now run our code.
Every single Particle pushes away its neighbors which causes the Particles to spread out in a really natural manner. Here is a short video of the effect in action. Ready for something special? Try this. Turn off the Particle's ability to age. Turn off the Perlin noise influence. And finally, put back theChannel-based variable radius. Once you add a few thousand Particles, you should get back something like this. During the course of this tutorial, we have managed to create a robust stippling algorithm almost by accident. We started with a simple image loading example and some randomly moving circles. After a few minor iterations, we have written a pretty cool program that will dynamically stipple images by simply combining a particle engine with a repulsive force. Hopefully you are inspired and anxious to continue exploring. This is not an end - there is so much
more to do. Try adding a third dimension to this project. Experiment with different types of external and internal forces. Try mixing different flavors of Particles together. Find new ways to control the Particles such as microphone input or webcam. Trace the path the particles travel over time. Draw the connections between neighboring particles. Or maybe don't draw the particles at all and instead only draw their collisions. So many options! So many paths to explore. And it all started from an empty black window. Where to now? Have a peek in the Gallery to see what others are up to with Cinder, or read more about the Features for ideas on what to explore. If you have questions, comments, ideas, or work to share, hop on over to the Cinder forum. On behalf of its whole community, let me say that we're excited you've taken the time to check out Cinder, and we hope you'll come join in.