12 min read

(For more resources related to this topic, see here.)

What is creative coding

This is a really short introduction about what creative coding is, and I’m sure that it is possible to find out much more about this topic on the Internet. Nevertheless, I will try to explain how it looks from my perspective.

Creative coding is a relatively new term for a field that combines coding and design. The central part of this term might be the “coding” one—to become a creative coder, you need to know how to write code and some other things about programming in general. Another part—the “creative” one—contains design and all the other things that can be combined with coding.

Being skilled in coding and design at the same time lets you explain your ideas as working prototypes for interface designs, art installations, phone applications, and other fields. It can save time and effort that you would give in explaining your ideas to someone else so that he/she could help you. The creative coding approach may not work so well in large projects, unless there are more than one creative codes involved.

A lot of new tools that make programming more accessible have emerged during the last few years. All of them are easy to use, but usually the less complicated a tool is, the less powerful it is, and vice versa.

A few words about Cinder

So we are up to some Cinder coding! Cinder is one of the most professional and powerful creative coding frameworks that you can get for free on the Internet. It can help you if you are creating some really complicated interactive real-time audio-visual piece, because it uses one of the most popular and powerful low-level programming languages out there—C++—and relies on minimum third-party code libraries. The creators of Cinder also try to use all the newest C++ language features, even those that are not standardized yet (but soon will be) by using the so called Boost libraries.

This book is not intended as an A-to-Z guide about Cinder nor the C++ programming language, nor areas of mathematics involved. This is a short introduction for us who have been working with similar frameworks or tools and know some programming already. As Cinder relies on C++, the more we know about it the better. Knowledge of ActionScript, Java, or even JavaScript will help you understand what is going on here.

Introducing the 3D space

To use Cinder with 3D we need to understand a bit about 3D computer graphics. First thing that we need to know is that 3D graphics are created in a three-dimensional space that exists somewhere in the computer and is transformed into a two-dimensional image that can be displayed on our computer screens afterwards.

Usually there is a projection (frustrum) that has different properties which are similar to the properties of cameras we have in the real world. Frustrum takes care of rendering all the 3D objects that are visible in frustrum. It is responsible for creating the 2D image that we see on the screen.

As you can see in the preceding figure, all objects inside the frustrum are being rendered on the screen. Objects outside the view frustrum are being ignored.

OpenGL (that is being used for drawing in Cinder) relies on the so called rendering pipeline to map the 3D coordinates of the objects to the 2D screen coordinates. Three kind of matrices are used for this process: the model, view, and projection matrices. The model matrix maps the 3D object’s local coordinates to the world (or global) space, the view matrix maps it to the camera space, and finally the projection matrix takes care of the mapping to the 2D screen space. Older versions of OpenGL combine the model and view matrices into one—the modelview matrix.

The coordinate system in Cinder starts from the top-left corner of the screen. Any object placed there has the coordinates 0, 0, 0 (these are values of x, y, and z respectively). The x axis extends to the right, y to the bottom, but z extends towards the viewer (us), as shown in the following figure:

Drawing in 3D

Let’s try to draw something by taking into account that there is a third dimension.

Create another project by using TinderBox and name it Basic3D. Open the project file ( xcode/Basic3D.xcodeproj on Mac or vc10\Basic3D.sln on Windows). Open Basic3DApp.cpp in the editor and navigate to the draw() method implementation.

Just after the gl::clear() method add the following line to draw a cube:

gl::drawCube( Vec3f(0,0,0), Vec3f(100,100,100) );


The first parameter defines the position of the center of the cube, the second defines its size. Note that we use the Vec3f() variables to de fine position and size within three (x, y and z) dimensions.

Compile and run the project. This will draw a solid cube at the top-left corner of the screen. We are able to see just one quarter of it because the center of the cube is the reference point. Let’s move it to the center of the screen by transforming the previous line as follows:

gl::drawCube( Vec3f(getWindowWidth()/2,getWindowHeight()/2,0), Vec3f(100,100,100) );


Now we are positioning the cube in the middle of the screen no matter what the window’s width or height is, because we pass half of the window’s width (getWindowWidth()/2 ) and half of the window’s height ( getWindowHeight()/2) as values for the x and y coordinates of the cube’s location. Compile and run the project to see the result. Play around with the size parameters to understand the logic behind it.

We may want to rotate the cube a bit. There is a built-in rotate() function that we can use. One of the things that we have to remember, though, is that we have to use it before drawing the object. So add the following line before gl::drawCube():

gl::rotate( Vec3f(0,1,0) );


Compile and run the project. You should see a strange rotation animation around the y axis. The problem here is that the rotate() function rotates the whole 3D world of our application including the object in it and it does so by taking into account the scene coordinates. As the center of the 3D world (the place where all axes cross and are zero) is in the top-left corner, all rotation is happening around this point.

To change that we have to use the translate() function. It is used to move the scene (or canvas) before we rotate() or drawCube(). To make our cube rotate around the center of the screen, we have to perform the following steps:

  1. Use the translate() function to translate the 3D world to the center of the screen.
  2. Use the rotate() function to rotate the 3D world.
  3. Draw the object (drawCube()).
  4. Use the translate()function to translate the scene back.

We have to use the translate() function to translate the scene back to the location, because each time we call translate() values are added instead of being replaced.

In code it should look similar to the following:

gl::translate( Vec3f(getWindowWidth()/2,getWindowHeight()/2,0) ); gl::rotate( Vec3f::yAxis()*1 ); gl::drawCube( Vec3f::zero(), Vec3f(100,100,100) ); gl::translate( Vec3f(-getWindowWidth()/2,-getWindowHeight()/2,0) );


So now we get a smooth rotation of the cube around the y axis. The rotation angle around y axis is increased in each frame by 1 degree as we pass the Vec3f::yAxis()*1 value to the rotate() function. Experiment with the rotation values to understand this a bit more.

What if we want the cube to be in a constant rotated position? We have to remember that the rotate() function works similar to the translate function. It adds values to the rotation of the scene instead of replacing them. Instead of rotating the object back, we will use the pushMatrices() and popMatrices() functions. Rotation and translation are transformations. Every time you call translate() or rotate() , you are modifying the modelview matrix. If something is done, it is sometimes not so easy to undo it. Every time you transform something, changes are being made based on all previous transformations in the current state. So what is this state? Each state contains a copy of the current transformation matrices. By calling pushModelView() we enter a fresh state by making a copy of the current modelview matrix and storing it into the stack. We will make some crazy transformations now without worrying about how we will undo them. To go back, we call popModelView() that pops (or deletes) the current modelview matrix from the stack, and returns us to the state with the previous modelview matrix.

So let’s try this out by adding the following code after the gl::clear() call:

gl::pushModelView(); gl::translate( Vec3f(getWindowWidth()/2,getWindowHeight()/2,0) ); gl::rotate( Vec3f(35,20,0) ); gl::drawCube( Vec3f::zero(), Vec3f(100,100,100) ); gl::popModelView();


Compile and run our program now, you should see something similar to the following screenshot:

As we can see, before doing anything, we create a copy of the current state with pushModelView(). Then we do the same as before, translate our scene to the middle of the screen, rotate it (this time 35 degrees around x axis and 20 degrees around y axis), and finally draw the cube! To reset the stage to the state it was before, we have to use just one line of code, popModelView().

Using built-in eases

Now, say we want to make use of the easing algorithms that we saw in the EaseGallery sample. To do that, we have to change the code by following certain steps.

To use the easing functions, we have to include the Easing.h header file:#include “cinder/Easing.h”

First we are going to add two more variables, startPostition and circleTimeBase:

Vec2f startPosition[CIRCLE_COUNT]; Vec2f currentPosition[CIRCLE_COUNT]; Vec2f targetPosition[CIRCLE_COUNT]; float circleRadius[CIRCLE_COUNT]; float circleTimeBase[CIRCLE_COUNT];


Then, in the setup() method implementation, we have to change the currentPosition parts to startPosition and add an initial value to the circleTimeBase array members:

startPosition[i].x = Rand::randFloat(0, getWindowWidth()); startPosition[i].y = Rand::randFloat(0, getWindowHeight()); circleTimeBase[i] = 0;


Next, we have to change the update() method so that it can be used along with the easing functions. They are based on time and they return a floating point value between 0 and 1 that defines the playhead position on an abstract 0 to 1 timeline:

void BasicAnimationApp::update() { Vec2f difference; for (int i=0; i<CIRCLE_COUNT; i++) { difference = targetPosition[i] – startPosition[i]; currentPosition[i] = easeOutExpo( getElapsedSeconds()-circleTimeBase[i]) * difference + startPosition[i]; if ( currentPosition[i].distance(targetPosition[i]) < 1.0f ) { targetPosition[i].x = Rand::randFloat(0, getWindowWidth()); targetPosition[i].y = Rand::randFloat(0, getWindowHeight()); startPosition[i] = currentPosition[i]; circleTimeBase[i] = getElapsedSeconds(); } } }


The highlighted parts in the preceding code snippet are those that have been changed. The most important part of it is the currentPosition[i] calculation part. We take the distance between the start and end points of the timeline and multiply it with the position floating point number that is being returned by our easing function, which in this case is easeOutExpo() . Again, it returns a floating point variable between 0 and 1 that represents the position on an abstract 0 to 1 timeline. If we multiply any number with, say, 0.33f, we get one-third of that number, 0.5f, we get one-half of that number, and so on. So, we add this distance to the circle’s starting position and we get it’s current position!

Compile and run our application now. You should see something as follows:

Almost like a snow storm! We will add a small modification to the code though. I will add a TWEEN_SPEED definition at the top of the code and multiply the time parameter passed to the ease function with it, so we can control the speed of the circles:

#define TWEEN_SPEED 0.2


Change the following line in the update() method implementation:

currentPosition[i] = easeOutExpo( (getElapsedSeconds()-circleTimeBase[i])*TWEEN_SPEED) * difference + startPosition[i];


I did this because the default time base for each tween is 1 second. That means that each transition is happening exactly for 1 second and that’s a bit too fast for our current situation. We want it to be slower, so we multiply the time we pass to the easing function with a floating point number that is less than 1.0f and greater than 0.0f. By doing that we ensure that the time is scaled down and instead of 1 second we get 5 seconds for our transition.

So try to compile and run this, and see for yourself! Here is the full source code of our circle-creation:

#include “cinder/app/AppBasic.h” #include “cinder/gl/gl.h” #include “cinder/Rand.h” #include “cinder/Easing.h” #define CIRCLE_COUNT 100 #define TWEEN_SPEED 0.2 using namespace ci; using namespace ci::app; using namespace std; class BasicAnimationApp : public AppBasic { public: void setup(); void update(); void draw(); void prepareSettings( Settings *settings ); Vec2f startPosition[CIRCLE_COUNT]; Vec2f currentPosition[CIRCLE_COUNT]; Vec2f targetPosition[CIRCLE_COUNT]; float circleRadius[CIRCLE_COUNT]; float circleTimeBase[CIRCLE_COUNT]; }; void BasicAnimationApp::prepareSettings( Settings *settings ) { settings->setWindowSize(800,600); settings->setFrameRate(60); } void BasicAnimationApp::setup() { for(int i=0; i<CIRCLE_COUNT; i++) { currentPosition[i].x=Rand::randFloat(0, getWindowWidth()); currentPosition[i].y=Rand::randFloat(0, getWindowHeight()); targetPosition[i].x=Rand::randFloat(0, getWindowWidth()); targetPosition[i].y=Rand::randFloat(0, getWindowHeight()); circleRadius[i] = Rand::randFloat(1, 10); startPosition[i].x = Rand::randFloat(0, getWindowWidth()); startPosition[i].y = Rand::randFloat(0, getWindowHeight()); circleTimeBase[i] = 0; } } void BasicAnimationApp::update() { Vec2f difference; for (int i=0; i<CIRCLE_COUNT; i++) { difference = targetPosition[i] – startPosition[i]; currentPosition[i] = easeOutExpo( (getElapsedSeconds()-circleTimeBase[i]) * TWEEN_SPEED) * difference + startPosition[i]; if ( currentPosition[i].distance( targetPosition[i]) < 1.0f ) { targetPosition[i].x = Rand::randFloat(0, getWindowWidth()); targetPosition[i].y = Rand::randFloat(0, getWindowHeight()); startPosition[i] = currentPosition[i]; circleTimeBase[i] = getElapsedSeconds(); } } } void BasicAnimationApp::draw() { gl::clear( Color( 0, 0, 0 ) ); for (int i=0; i<CIRCLE_COUNT; i++) { gl::drawSolidCircle( currentPosition[i], circleRadius[i] ); } } CINDER_APP_BASIC( BasicAnimationApp, RendererGl )


Experiment with the properties and try to change the eases. Not all of them will work with this example, but at least you will understand how to use them to create smooth animations with Cinder.

Summary

This article explains what is Cinder, introduces the 3D space, how to draw in 3D, and also explains in short about using built-in eases.

Resources for Article :


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here