libCinder

42
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

description

creative library for c++.

Transcript of libCinder

Page 1: libCinder

CHAPTER 1: GETTING STARTED

BUILDING A NEW PROJECTGetting 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

Page 2: libCinder

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

Page 3: libCinder

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 IMAGESLoading 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. 

Page 4: libCinder

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.

Page 5: libCinder

OTHER OPTIONS FOR DEALING WITH IMAGESAs 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;

Page 6: libCinder

}

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 SHAPESDrawing 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.

Page 7: libCinder

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

Page 8: libCinder

CREATING A BASIC PARTICLE ENGINEThese 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 );

}

Page 9: libCinder

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

Page 10: libCinder

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.

Page 11: libCinder

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 DIVERSITYLINING 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++ ){

Page 12: libCinder

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:

Page 13: libCinder

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;

Page 14: libCinder

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 PARTICLESIn 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.

Page 15: libCinder

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. 

Page 16: libCinder

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

Page 17: libCinder

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 INTERACTIONIt 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.

Page 18: libCinder

KEYBOARD INPUTFirst 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

Page 19: libCinder

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 INPUTCinder 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.

Page 20: libCinder

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.

Page 21: libCinder

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

}

Page 22: libCinder

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

Page 23: libCinder

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! 

Page 24: libCinder

ITERATION 2: WAVEY PIXELSAnother 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 = constrain( newLoc.y, 0.0f, channel.getHeight() -

1.0f );

float gray = channel.getValue( newLoc );

mColor = Color( gray, gray, gray );

mRadius = mRadiusScale;

Page 25: libCinder

ITERATION 3: WAVEY PARTICLESTime 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 );

Page 26: libCinder

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 GRIDI 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

Page 27: libCinder

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 EVENTSWe 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.

Page 28: libCinder

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

Page 29: libCinder

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 VELOCITYUp until now, our Particles have been stationary. We did do some position

perturbations in the last section, but the location of theParticle (mLoc) never

Page 30: libCinder

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.

Page 31: libCinder

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

Page 32: libCinder

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 NOISEOh 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.

Page 33: libCinder

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.

Page 34: libCinder

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.

Page 35: libCinder

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 SPACEAt 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.

Page 36: libCinder

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

Page 37: libCinder

}

}

}

}

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:

Page 38: libCinder

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

Page 39: libCinder

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.