让图形跑起来(上篇)

在开始看这节之前,希望你已经熟悉了基本的函数绘图方法。

前面的有关绘图部分,我们的代码都只写在 draw 函数中,这节会开始介绍 setup 函数和 update 函数。

在开始做运动图形之前,我们需要了解“动画”是如何产生的。

上图相当吸引人,而且非常直观地揭示了动画的实现原理。

动画是魔法,是关于视觉欺骗的魔法。只是在这个信息爆炸,视频满天飞的年代,我们已经对它习以为常了。也很少有人会去惊叹,能看到动画,本身就是一件神奇的事。

在程序中制作动画,原理是相通的。我们只要考虑怎么在每页上画上不同的图形,程序就会自动去翻页,而大脑会将它脑补成动画。

下面会聊聊如何实现基本的动画运动,在此之前需要我们了解一些变量的基础知识。

变量

变量是数据的容器,它可以在程序中反复使用。

代码示例(2-1):(窗口大小为 500 X 500)

void ofApp::draw(){
   ofDrawCircle(100, 250, 25);
   ofDrawCircle(200, 250, 25);
   ofDrawCircle(300, 250, 25);
   ofDrawCircle(400, 250, 25); 
}

这段代码没有用到变量,在屏幕上画了四个圆。仔细观察,会发现这四个圆的宽高都是一样的。既然一样,为了减少数值的重复输入,我们其实可以定义一个符号来表示,这个符号就是变量。

使用变量后的代码:

代码示例(2-2):(窗口大小为 500 X 500)

void ofApp::draw(){
    int a = 25;
   ofDrawCircle(100, 250, 25);
   ofDrawCircle(200, 250, 25);
   ofDrawCircle(300, 250, 25);
   ofDrawCircle(400, 250, 25); 
}

绘图的结果与原来是完全一样的。

定义了变量 a ,我们就可以很方便地改变数值。如果将 a = 25,改成a = 50。那所有圆的半径都会统一变成 50,这样就无需再逐一修改数值了。变量是个相当好的发明~

变量的创建

变量使用前需要先声明,并且需要指定它的数据类型。

int i;    
i = 50;

第一句代码声明了一个变量 i。int 是一个专门用来声明变量的符号。声明时,会在电脑内存中开辟一个空间,相当于生成了一个“箱子”,专门用来存放整数数据。

第二句代表把 50 赋值给变量 i。执行这句以后,数据就会稳稳地存放在了 i 这个变量上了。

你也可以用更便捷的方法,两步并作一步,声明的同时完成赋值。

int i = 50;

变量的命名规则

变量名的命名比较自由,但也有讲究的地方。

  • 必须是字母和数字或下划线的组合。可以用一个字符,也可以用一个单词,
  • 区分大小写,name 和 Name 代表不同的变量
  • 名字尽量简洁易懂,首字符只能以字母开头,不要用数字和特殊字符。
  • 不能用已有的关键字作为变量名,比如 int,float

以下,都是错误的声明方式

int $a;
int 89b;

以下是合法的声明

int r;
int super_24;
int openTheDoor;

变量的类型

除了可以声明为整数数据,还可以声明为小数数据(也叫浮点数据),用关键字float。

float b = 0.5

这里要牢记声明数据时,用了变量类型。若是用了关键字 int ,后面赋值时就不能写 i = 0.5 之类,这样程序会出错。但反过来是可以的,比如 float i = 5 是正确的语法,程序仍会将它识别为小数。

运算符

Openframeworks 中的常用运算符有几种:

+

-

*

/

取模,得出余数

  • 加减乘除大家都不陌生,% 可能是第一次看到,它得出的数是余数。9 % 3的结果是 0 。而 9 % 5 的结果会是 4。

  • 运算符可以在数值以及变量间使用。

代码示例(2-3):

void ofApp::draw(){
    int a = 1;         //声明整数变量a,赋值为1
    int b = 2;         //声明了整数变量b,赋值为2
    int c;                //声明了整数变量c
    c = a + b;         //将两变量相加,并赋值给c
    cout << c << endl;          //输出变量c
}

输出的结果不会显示在窗口,而是在下方的控制台。

  • 第四行的写法看上去很特别。这是计算机赋值操作的常用格式。等号的左边要写最终赋值的变量,等号右边写上运算的过程。若写成 a + b = c; 则是无效的
  • 第五行的 cout << ? << endl 可以先理解为一个固定写法,? 中可以填写数值,变量又或是表达式,通过它就可以在控制台输出相应的结果,这同时是检查程序有否出现异常的常用方法。
  • 实时运行程序时,下面的数字 3 则为输出的计算结果。你会发现控制台上的数字会不断往下刷新。这是由于我们将 cout 句式写在 draw 函数中导致的。后面会提到具体原因。

运算规则

Openframeworks 中进行运算,需要搞清变量的类型。其中特别要留意的,是浮点数与整数类型之间的转换。

代码示例(2-4):

 cout << 9 / 5 << endl;     //结果 1

在程序中,整数与整数进行运算只会得到整数,9 除以 5 的实际结果是 1.8 ,但程序中的输出结果却会是 1 。这是由于程序对结果不进行四舍五入处理,而是直接舍去小数点之后的数。

代码示例(2-5):

cout << 9.0 / 5.0 << endl ;   //结果 1.8

浮点数与浮点数之间运算会得出浮点数,实际结果是 1.8 ,程序输出结果也会是 1.8。

代码示例(2-6):

cout << 9 / 5.0 << endl ;     //结果 1.8
cout << 9.0 / 5 << endl ;     //结果 1.8

最后是整数和浮点数混用,最终输出的结果会是浮点数 1.8。

  • 其实只需记住一点,规则这样设计是为了不损失数据的精度,所以只要有一方是浮点数,结果都会是浮点数。

Setup函数,Update 函数 与 Draw函数

铺垫完基础的知识,离制作动画越来越接近了。

setup,update 以及 draw函数,相当于是 Openframeworks 里面的主函数。这三个函数非常特殊,它们是程序的基本框架,会控制程序的流程。

基本格式:

void ofApp::setup(){

}

void ofApp::update(){

}

void ofApp::draw(){

}

程序看似能灵活地响应多个任务,实现丰富复杂的功能。但它内部是其实有一个非常严谨的运行流程的。通过前面提到的 cout 命令,我们可以通过控制台的输出直观地了解过程。

示例代码:

void ofApp::setup(){
   cout << 1 << endl;
}

void ofApp::update(){
    cout << 2 << endl;
}

void ofApp::draw(){
    cout << 3 << endl;
}

我们先试着在三个函数中输入以上代码。点击三角符运行一段时间再关闭程序。以下是运行结果:

控制台输出的第一个结果为数字1,后面则是 2 3 之间来回交替。

cout 命令每执行一遍,就会输出一个结果。setup 函数在程序中是第一个执行的,同时只会运行一次。所以你会发现“1”只出现了一次。而 update 函数和 draw 函数则是循环交替执行的,并且先运行 update 函数再运行 draw函数。所以你会看到先输出的是数字“2”,后面出现的才是数字“3”。

由于这种特性,setup函数往往用于初始化环境属性,如设置屏幕宽高,背景颜色,进行变量的赋值等等。而涉及大量数据运算的,则放在 update 函数内。而 draw函数 ,则用于放绘图函数,以此产生不断变化的图像。

update与draw的运行频率

相信现在你能明白,实例(2-3)中为会不断地输出 3了。因为我们将代码写在 draw 函数内,所以 draw 函数内的代码都会从上至下,不断循环执行,重复输出结果。

由于 update 函数和 draw 函数在 OF 中的运行频率默认为每秒 60 次(60 FPS)。所以在一秒之内,draw 函数的每一行代码都执行了 60 次。如果你仅仅是为了获得计算结果,多余的计算是没有意义的。所以更好的办法应该是直接放在 setup 函数里面。代码同样会被执行,并且只运行一次。

代码示例(2-7):

void ofApp::setup(){
    int a = 1;         //声明整数变量a,赋值为1
    int b = 2;         //声明了整数变量b,赋值为2
    int c;                //声明了整数变量c
    c = a + b;         //将两变量相加,并赋值给c
    cout << c << endl;          //输出变量c
}    

与帧率相关的函数:

设置帧率

OF程序默认每秒 60 帧的频率是可以被改变的,通过 ofSetFrameRate() 函数就能进行设置

格式:

ofSetFrameRate(x)

x 代表设置后的帧速率

--代码示例(2-9):

void ofApp::setup(){
    ofSetFrameRate(1);
}

void ofApp::update(){
    cout <<  1 << endl;
}

程序运行后,现在就变成每隔一秒,才从控制台输出一个数字1

获取帧率

尽管设置了程序的帧率,在程序中未必就是按预置的帧率来运行的。特别对于运算量大的程序,往往会有卡帧的情况出现。我们可以通过 ofGetFrameRate() 获取程序当前的实时帧数。

--代码示例(2-10):

void ofApp::setup(){
    ofSetFrameRate(10);
}

void ofApp::update(){
    cout <<  ofGetFrameRate() << endl;
}

通过这种方法,你可以了解你的程序是否运行流畅。

平移的圆

为了加深这三个函数的理解,最好的办法莫过于做一个动画。

Openframeworks 中写动画效果的方式是“很笨拙”的。没有我们想象中的智能。不存在某种现成的指令,比如指定某图形做曲线运动,再设置速度,移动距离等产生,程序便自动生成动画。

既然你是程序世界的造物主,你就需要亲力亲为,去定义这些细节。电脑需要非常明确的指令,每帧要画怎样的图形。

我们先从一个平移运动的动画开始去了解。

代码示例(2-11):

— ofApp.cpp内 —

void ofApp::setup(){
    ofSetWindowShape(300,300);
    x = 0;
    y = ofGetHeight()/2;
}
void ofApp::update(){
    x = x+1;
}
void ofApp::draw(){
    ofBackground(234, 113, 107);
    ofDrawCircle(x, y, 25, 25);
}

前面都只在 ofApp.cpp 中写代码,到这里我们还需要在 ofApp.h 中写上

— ofApp.h内 —

int x;
int y;

运行效果:

一段纯用代码实现的动画就出来了

代码解释

我们先来看最奇怪的部分。现在是第一次在 ofApp.h 里面写代码。写了两个声明整形变量 x 与 y 的语句。细心的人会发现,前面的例子 2-2 中,变量的声明是直接写在 draw 函数中的,这与在 ofApp.h 里有何区别?

在 ofApp.h 中声明的变量,为全局变量,它的好处是只要声明过一次,这个变量在 setup ,update 或是 draw 函数中都能使用。 而如果仅在某个函数中声明的变量,则为局部变量。它只能在这个函数中使用。

所以若是在程序中这么写,是无法运行的,会出现报错提示。

理解 ofApp.h 部分后,我们再来看 ofApp.cpp 中的代码。前面声明了两个变量x,y。用于储存圆的坐标位置。变量的初值设定,在setup函数中也实现了,这代表的就是圆的起始坐标。

setup 里面有一句

ofSetWindowShape(300,300);

它的作用是设置窗口的大小,与通过前面的 main.cpp 中的 ofSetupOpenGL() 语句去设置,效果是一样的。

现在关键代码其实是 update 函数中的这个:

x = x + 1;

我们不要将它看成数学等式,否则会很奇怪。这里的“=”号是一个赋值符号,代表把右边的数值放到左边的变量里。假设 x 为 50,代码运行后。等号右边就等于 50 + 1,即 51。最终这个结果会赋到变量 x 里,于是x的值就变更为 51 了。

顺着程序的流程走,update 函数每运行一次,x的数值就会增加1。于是每次绘图,圆形都会比上一帧多向右平移一个像素,图形因此就动起来了。

如果你之前有使用过 Processing,就会发觉 Openframeworks 中多了一个 update 函数,那 update 函数与 draw 函数具体有何区别?什么情况代码应该写在 update 中,什么时候代码应该写在 draw 里?

可以记住两个原则,与图形运动相关的数据运算,尽量写在 update 函数中,而实现绘图部分的代码,则写在 draw 函数内。

当你的程序较小型的时,将代码写在 update 或是 draw 里头是没有太大差别的。但当做一个涉及绘制大量图形的程序,情况就有所不同了。假如用 OF 做的是一个赛车游戏,你将计算汽车位置坐标相关的代码写在 draw 里头了,一旦遇到复杂的场景,画面开始掉帧的时候。你看到的不仅仅是画面变得不流畅,而是场景中的时间也随之变慢了。这样会出现不协调的感觉。因此最理想的做法是将数据更新部分写在 update 中。

控制运动方向

图形往哪个方向运动,取决于你怎么变化你的坐标。如果改成 y=y+1,圆就会往下运动,如果 x,y 都同时增加 1,圆就会往右下方运动。写成减号会朝相反的方向。

--代码示例(2-12):

— ofApp.cpp内 —

void ofApp::setup(){
    ofSetWindowShape(300,300);
    x = 0;
    y = 0;
}
void ofApp::update(){
    x = x + 1;
    y = y + 1;
}
void ofApp::draw(){
    ofBackground(234, 113, 107);
    ofDrawCircle(x, y, 25, 25);
}

— ofApp.h内 —

int x;
int y;

控制运动速率

由于 update 函数和 draw 函数默认都是每秒运行60帧的。因此按这个速率去推算,上面的圆,每秒就会向右移动60像素。

想改变图形的速率,有两个办法。一个就是增大每次 x 的变化值。

x=x+10;

这样速度就会比原来提升10倍!

另一个就是改变画布的帧速率,用前面提到的 ofSetFrameRate()。假如你在setup函数内写上ofSetFrameRate(10),就会将播放速度设置成 10 帧每秒。这样较原来默认的 60 帧每秒就变慢了6倍。

但第二种方法不推荐,因为会降低画面的流畅度。

OF 中的背景

前面的例子,都是将 background 函数写在draw里的,有没有想过,写在setup里,会有何不同?现在在平移运动的例子上作些改动。

--代码示例(2-13):

void ofApp::setup(){
    ofSetWindowShape(300,300);
    ofBackground(234, 113, 107);
    x = 0;
    y = ofGetHeight()/2;
}
void ofApp::update(){
    x = x+1;
}
void ofApp::draw(){    
    ofDrawCircle(x, y, 25, 25);
}

从结果上看其实是没有区别的。这点与 Processing 完全不同。这是因为 OF 中会默认开启自动刷新背景的功能。我们需要关闭它才能看到效果。

在 setup 中加入一行代码。

ofSetBackgroundAuto(false);

--代码示例(2-14):

void ofApp::setup(){
    ofSetWindowShape(300,300);
    ofSetBackgroundAuto(false);
    ofBackground(234, 113, 107);
    x = 0;
    y = ofGetHeight()/2;
}
void ofApp::update(){
    x = x+1;
}
void ofApp::draw(){    
    ofDrawCircle(x, y, 25, 25);
}

这时会发现动画变得有所不同了,画面上运动的圆形似乎没有被清除。若是将自动刷新的功能关闭,draw 函数里面的图形都会绘制到同一个画布上。

实现拖尾效果

通过命令 ofSetBackgroundAuto(false) ,OF 的画布就有了不清除的特性。我们可以结合已知的绘图函数实现一些有趣的效果。

--代码示例(2-15):

void ofApp::setup(){
    ofSetWindowShape(300,300);
    ofSetBackgroundAuto(false);
    ofBackground(234, 113, 107);
    x = 0;
    y = ofGetHeight()/2;
}
void ofApp::update(){
    x = x+1;
}
void ofApp::draw(){
    ofSetColor(234, 113, 107,15);
    ofDrawRectangle(0,0,ofGetWidth(),ofGetHeight());
    ofSetColor(255);
    ofDrawCircle(x, y, 25, 25);
}

通过巧妙地使用 ofDrawRectangle(),在每次绘制前覆盖一层和背景色相同且透明的矩形,就会使得前一帧的图形都变淡。也就相当于实现了近似拖尾的效果。当我们改变 ofDrawRectangle() 前的 ofSetColor 的透明度,就会影响效果的强弱。透明度越小则遮盖得越少,残影也就越强。

其他注意事项-代码格式

代码书写是有一定规范的。 * 为了代码有更好的可读性,大括号里面的每行代码前面应该留出一定的空间,并且尽量对齐。键入 TAB 或者若干空格可以进行缩进。 * 程序中空格符与换行符不会影响程序本身,多打一个少打一个往往没有关系。

End

通过熟悉这些命令,你已经可以简单地指挥图形的运动了。再结合上节内容,充分发挥你的想象力,就可以做出很多有意思的动效。

下一节将会看到更多丰富的实例,同时也会用上数学函数,将它和图形的运动结合起来。