0% found this document useful (0 votes)
270 views

Lib Cinder

1. Cinder projects have three main functions: setup(), update(), and draw(). Setup initializes variables, update changes variables, and draw displays content. 2. Additional functions like prepareSettings() can modify window properties. Namespaces like ci:: are used to organize Cinder code. 3. Loading an image involves including headers, declaring a texture, loading the image into the texture using loadImage(), and drawing the texture. Images can also be loaded from URLs or by prompting the user to select a file. 4. Basic shapes like circles can be easily drawn using functions like gl::drawSolidCircle() which creates an OpenGL triangle fan. Parameters control properties like position, radius, and detail.

Uploaded by

rashed44
Copyright
© Attribution Non-Commercial (BY-NC)
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
270 views

Lib Cinder

1. Cinder projects have three main functions: setup(), update(), and draw(). Setup initializes variables, update changes variables, and draw displays content. 2. Additional functions like prepareSettings() can modify window properties. Namespaces like ci:: are used to organize Cinder code. 3. Loading an image involves including headers, declaring a texture, loading the image into the texture using loadImage(), and drawing the texture. Images can also be loaded from URLs or by prompting the user to select a file. 4. Basic shapes like circles can be easily drawn using functions like gl::drawSolidCircle() which creates an OpenGL triangle fan. Parameters control properties like position, radius, and detail.

Uploaded by

rashed44
Copyright
© Attribution Non-Commercial (BY-NC)
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 38

CHAPTER 1: GETTING STARTED

BUILDING A NEW PROJECT


Getting a new project up and running in Cinder is simple. Take a peek at the Mac or Windows guides to creating a new project to see for yourself. However, for this tutorial you can simply follow along from the project source code contained in the cinder/tour folder. When you create a new Cinder project, you will notice there are a few functions declared for you. Every Cinder project is made up of three main functions. You initialize your variables in the setup() method which is called once when your program begins. You make changes to those variables in the update() method. And finally, you draw() content in your program window. Update and draw are the heartbeat of any Cinder project. Setup, then update and draw, update and draw, update and draw, on and on until you quit the application. void setup(); void update(); void draw(); Additionally, you can modify some of the settings using the prepareSettings() method. It is entirely optional and if you choose to leave it out, Cinder will use a default window size of 640x480 with a frame rate of 30. For this tutorial, we want our window to be 800x600 with a frame rate of 60 so we would say: void TutorialApp::prepareSettings( Settings *settings ){ settings->setWindowSize( 800, 600 ); settings->setFrameRate( 60.0f ); } Another thing to notice up front is that Cinder uses C++ namespaces. Depending on what programming languages you've worked with, you may have already encountered namespaces before. They're nothing fancy - just a way of grouping functions and classes together under a common name. Everything in Cinder is inside the cinder:: namespace. So to reference something inside it, like say, the Timer class, we refer tocinder::Timer. C++ namespaces also support hierarchies, which is a very nice feature that Cinder takes advantage of. So for example, the OpenGL texture class has the full name of cinder::gl::Texture. However this can get a little long-winded sometimes, so Cinder provides a couple of shortcuts. The first is that whenever you would refer to cinder:: you can also refer to its synonym, ci::. These are completely interchangeable, but ci:: is a little easier to type, so we recommend it. Secondly, you'll generally see in the samples the following two lines toward the top:

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.

LOADING AND DISPLAYING IMAGES


Loading images in C++ can be a chore. Luckily, Cinder does most of the hard work for you. The process for loading and displaying an image can be broken up into only a few lines of code. 1) Tell the compiler we're interested in Cinder's image input/output code and gl Texture code. #include "cinder/ImageIo.h" #include "cinder/gl/Texture.h" You put these lines at the top of the project with the other includes. 2) Declare a new texture in the App class. gl::Texture myImage;

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.

OTHER OPTIONS FOR DEALING WITH IMAGES


As we mentioned before, including images directly in your app as resources is one option, but Cinder makes it easy to load images from many different sources. I'm going to show you two additional ways you can use images in your application without needing to have them stored locally. The first way is to prompt the user to open a file. The following code will attempt to create an image from a file selected by means of a standard open dialog box. Once you select a file, assuming the file is a valid image, a texture is created. Otherwise, an exception is thrown and we print an error message (we'll discuss console()more in a bit). try { std::string p = getOpenFilePath( "", ImageIo::getLoadExtensions() ); if( ! p.empty() ) { // an empty string means the user canceled myImage = gl::Texture( loadImage( p ) ); } } catch( ... ) { console() << "Unable to load the image." << std::endl; }

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 ) );

CREATING A BASIC PARTICLE ENGINE

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 );

The update() method will look like this: mParticleController.update();

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.

50? Boring. How about 50,000?

Up next, we are going to add some personality to our Particles. On to Chapter 2.

CHAPTER 2: PERSONALITY AND DIVERSITY


LINING UP THE PARTICLES
As we discussed in the previous chapter, our Particles have a location, direction, speed, and radius. However, instead of spawning theParticles in a random location, we are going to create an evenly spaced grid of Particles. Our project window is currently set to 800x600 so we will create a grid of 80x60 Particles. This will give us 10 pixels between eachParticle for a total count of 4800. Previously, we used ParticleController::addParticles( int amt ) to populate our list. We are going to make a similarly named method that will lay the Particles out in a grid. void ParticleController::addParticle( int xi, int yi ) { float x = ( xi + 0.5f ) * 10.0f; float y = ( yi + 0.5f ) * 10.0f; mParticles.push_back( Particle( Vec2f( x, y ) ) ); } The addition of 0.5f is just a way to make sure the grid of Particles is centered on screen. Try removing the 0.5f to see the difference. Inside the ParticleController constructor, we add a nested for-loop which will call addParticle( x, y ) 4800 times. for( int y=0; y<mYRes; y++ ){ for( int x=0; x<mXRes; x++ ){ addParticle( x, y ); } }

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.

mRadius = cos( mLoc.y * 0.1f ) + sin( mLoc.x * 0.1f ) + 2.0f;

mRadius = ( sin( mLoc.y * mLoc.x ) + 1.0f ) * 2.0f;

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"

RECREATING THE IMAGE WITH PARTICLES


In the last section, we learned three different ways to load and display an image. In this section, we are going to combine the image andParticle engine to hopefully create something greater than the sum of the parts. We will start by replacing our gl::Texture with a new class, the Channel. This is a class which can be used for storing a grayscale image. Its name comes from the fact that its older brother, the Surface represents a color image made up of individual color channels: red, green, blue and sometimes alpha. However we don't need all that data for our purposes - we just want a gray image, so using a Surface is overkill. Also, although gl::Textures can be grayscale, we don't want to draw this image anymore - we want to get at its data. So instead of putting it on the graphics card, we want to store it in memory, which a Channel is ideal for. Loading an image into a Channel is practically the same as loading into a gl::Texture. Url url( "http://www.flight404.com/_images/paris.jpg" ); mChannel = Channel32f( loadImage( loadUrl( url ) ) ); It is the same as before except we create a Channel32f instead of the gl::Texture. The 32f simply means the Channel is made up of 32 bit floating point numbers. In this scheme, 1.0 represents white and 0 represents black. This next step is just begging to happen. Each Particle is going to reference the Channel to see what color gray exists at that Particle's location. It will then set its color to this grayscale value. void Particle::update( const Channel32f &channel ) { float gray = channel.getValue( mLoc ); mColor = Color( gray, gray, gray ); } We pass in a reference to the Channel and use getValue() to get the value of the Channel at a specific location. We simplified things a bit by making the image the same size as the project window. Otherwise we would have to do some extra work to make sure the image fills the entire app window and that we don't try to access outside of the Channel's dimension. This is something we will address

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.

ITERATION 1: MOUSE DISTORTION


We start by changing the resolution of the Particle grid. We double the number of Particle's along each axis to end up with 4x the amount we were using prior. This brings us to 19200 Particles which is perfectly fine for realtime performance. For the accompanying images, we are actually using 480,000 Particles and not surprisingly, the frame rate will suffer. To help keep the frame rate zippy, we are going to switch to rectangles instead of circles because there are fewer vertices to draw. We'll use Cinder's built-in rectangle class, and we'll use the version that takes floats called Rectf. There are a few different ways to construct a Rectf. We are going to use 2 pairs of variables. The first pair represents the x and y coordinate of the rectangle's upper left corner. The second pair of variables will represent the lower right corner. Rectf rect( mLoc.x, mLoc.y, mLoc.x + mRadius, mLoc.y + mRadius ); gl::drawSolidRect( rect ); I want to apologize for using the word radius to describe the size of this rectangle. If it helps, you can think of it as a circle but with a triangle fan resolution of 5. Now we introduce a local Vec2f called newLoc which is based on the current location but has an offset added to it. Our offset will be the unit vector representing the direction to the cursor. We multiply it by 100.0 because an offset of 0.0 to 1.0 is not that noticeable. Vec2f newLoc = mLoc + mDirToCursor * 100.0f; newLoc.x = constrain( newLoc.x, 0.0f, channel.getWidth() - 1.0f ); newLoc.y = constrain( newLoc.y, 0.0f, channel.getHeight() - 1.0f ); We add those constrain() calls because we want to make sure the new location isn't outside the bounds of the Channel. Now, instead of usingmLoc to get the corresponding Channel value, we use newLoc which will give us an offset value. We are left with a strange bulgey lens effect centered on our cursor. Poor kitty!

ITERATION 2: WAVEY PIXELS


Another baby step. We are going to put back some of the sin() and time based code we had used earlier. The time variable is just a scaled version of getElapsedSeconds(). The dist variable is a scaled version of the length of mDirToCursor vector before we normalize it (because if we wait until after we normalize it, it will have a length of one). Finally, sinOffset takes the sine of time plus dist and scales it up 100x. The time is there to oscillate our wave and the dist is there so we can create concentric oscillations emanating from the cursor position. Below is the Particle's entire update() method. mDirToCursor = mouseLoc - mLoc;

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 );

newLoc.y ); float gray mColor mRadius

= constrain( newLoc.y, 0.0f, channel.getHeight() - 1.0f

= channel.getValue( newLoc ); = Color( gray, gray, gray ); = mRadiusScale;

ITERATION 3: WAVEY PARTICLES


Time to go back to the Particle circles. We have commented out the color and are now back to drawing the Particles as white circles of variable radius. Instead of using the newLoc to retrieve the corresponding Channel value, we are going to switch back to using mLoc. The one main change for this iteration is we are going to use the sinOffset to warp our mDirToCursor vector. float time = app::getElapsedSeconds() * 4.0f; float dist = distToCursor * 0.05f; float sinOffset = sin( dist - time ); mRadius = channel.getValue( mLoc ) * mRadiusScale;

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.

CHAPTER 4: FINE TUNING


MOVING THE PARTICLES OFF THE GRID
I don't know about you, but I am getting tired of this grid format. Particles weren't designed to be stationary. Our particles want to roam. It is time to cut them free from their grid tethers. As I mentioned early on, the Particle is just a holder for data. Not just any data. The Particle class holds the data that describes the particle itself. The location is not just any

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.

CREATING PARTICLES WITH MOUSE EVENTS


We are going to need to beef up our mouse related code. First up, we will add mouseDown() and mouseUp() methods to our project. We will also make a boolean that will keep track of whether a mouse button is pressed. void mouseDown( MouseEvent event ); void mouseUp( MouseEvent event ); bool mIsPressed; If any mouse button is pressed, mouseDown() will fire. Inside that function, all we do is set mIsPressed to true. If mouseUp() is called,mIsPressed will be set to false. Easy enough. void TutorialApp::mouseDown( MouseEvent event ) { mIsPressed = true; } void TutorialApp::mouseUp( MouseEvent event ) { mIsPressed = false; } Finally, in our App class update() method, we add an if statement that checks to see if mIsPressed is true. If it is, then have theParticleController make some new Particles. if( mIsPressed ) mParticleController.addParticles( 5, mMouseLoc ); We have gone ahead and changed the addParticles() method in ParticleController to take both the number of Particles we want as well as the location where we want to initially put them. You might be thinking, "Hey, wait. If we make 5 particles and place all of them at the location of the

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 );

ENTER PERLIN NOISE


Oh man, how I LOVE Perlin noise. When used sparingly, Perlin noise can add some magic to your Particle systems. But be aware, it is very easy to overuse and abuse Perlin noise. Subtlety is key. What is Perlin noise? Wikipedia can give you a very thorough answer to that question. In short, Perlin noise is a smoothly interpolated, easily controllable random number generator. One of the cool things about it is that Perlin noise can be defined in 1D, 2D, 3D or 4D, and it will always give us back a consistent value for a particular location. Cinder has a built-in implementation, and here it is: mPerlin = Perlin();

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.

CHAPTER 5: EXTERNAL FORCES


PERSONAL SPACE
At the end of the last section, we had a nice Particle emitter cursor trail. As you drag the cursor around, you leave a trail of hundreds of moving Particles. Every one of those Particles is responding to its initial starting velocity combined with a hint of Perlin noise. EachParticle does its thing and is oblivious to what any of the Particles are doing. Until now. We are going to implement a very basic repulsive force to each Particle. Every single Particle will push away every other Particle. We will do this by giving each Particle an acceleration vector called mAcc. Here is how it will work. From the ParticleController, we will iterate through all the Particles. For each Particle, we check it against all other Particles. If those two Particles are close, they will push each other away more strongly than if the two Particles are on opposite sides of the app window. We add that repulsion to the respective Particles' mAcc vectors. Once we have iterated through all the Particles, we add the acceleration to the velocity. Then we add the velocity to the position. We decay the velocity. We reset the acceleration. And we repeat. The abridged version of what each Particle will go through looks like this: mVel += mAcc; mLoc += mVel; mVel *= mDecay; mAcc.set( 0, 0 );

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.

mMass = mRadius * mRadius * 0.005f;

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.

BASIC REPULSIVE FORCE


void ParticleController::repulseParticles() { for( list<Particle>::iterator p1 = mParticles.begin(); p1 != mParticles.end(); ++p1 ) { list<Particle>::iterator p2 = p1; for( ++p2; p2 != mParticles.end(); ++p2 ) { Vec2f dir = p1->mLoc - p2->mLoc; float distSqrd = dir.lengthSquared(); if( distSqrd > 0.0f ){ dir.normalize(); float F = 1.0f/distSqrd; p1->mAcc += dir * ( F / p1->mMass ); p2->mAcc -= dir * ( F / p2->mMass ); } } } } Lets go through this step by step. First, you set up a for-loop that uses the list iterator to go through all the Particles, one by one, in the order they are sorted in the list. for( list<Particle>::iterator p1 = mParticles.begin(); p1 != mParticles.end(); ++p1 ){ Next, we create a second iterator that also loops through the Particles, but it starts at one Particle ahead of the first iterator's position. Put another way, if we are on Particle 15 in the first iterator, the second iterator will loop through Particles 16 and higher.Particle 16 will iterate through Particle 17 and higher, etc. Put yet another way, imagine that we have 3 Particles. Each Particle wants to repel every other Particle. First round, p1 and p2 repel each other, and then p1 and p3 repel each other.

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.

You might also like