创造你的第一个程序

相当一部分人开始学习 OF 是先从 P5 开始的。所以教程会同时对比 P5 与 OF。若是没有 P5 基础,也不妨碍你去理解 OF。

通过浏览 OF 自带的 example。相信你已经了解程序大致能干些什么了。下面我们来正式开始学习 Openframeworks~

OF 与 C++

OF 本身只是个框架,它是基于C++。所以我们学的本质上就是 C++ 的语法。我们也是用 C++ 来写 OF 程序。

对设计师而言,C++ 听上去就有点不明觉厉的感觉。但我们其实不需要像程序员一样去了解 C++ 的方方面面。OF 本身是个框架,已经对编程的流程做了许多简化,使你无需了解许多底层的知识即能上手开发。

OF 的文件结构

第一篇提到,我们通过 ProjectGenerator 或是直接复制一个文件,就能创建空白的文档。下面我们来详细看看 OF 的文件结构。只要展开 src 文件夹,我们能看到以下三个文件

  • main.cpp
  • ofApp.cpp
  • ofApp.h

通过点击这几个文件,你会发现里面并不是完全空白的,而是直接就包含了一些代码。这些代码是 ProjectGenerator 帮我们自动生成的。作用是在 C++ 程序中载入 OF 框架,并将 OF 的常用结构都提供好。这样我们以后只需要直接往里面填东西,而不需要重复写同样的代码。

  • main.cpp

main.cpp 是存放程序的主函数的地方,任何的 C++ 程序,无论简单和复杂,都需要有一个 main 函数。在初期,你只需要关心一行代码。

ofSetupOpenGL(1024,768, OF_WINDOW);	

这行代码是用于设置窗口的运行大小,第一第二个参数分别代表窗口的长宽。而第三个参数用来设置窗口的类型。其中 OF_WINDOW 代表的是初始的大小按前两个参数来设置,你也可以将它替换成 OF_FULLSCREEN,这样程序运行后则会变成全屏模式。(全屏模式下只能通过 Esc 键来退出程序)

  • ofApp.cpp 与 ofApp.h

这两个文件是互相对应的,后缀.cpp 代表源文件,后缀.h 代表头文件。我们的 OF 代码,主要就是写在这两个文件内。ofApp.h 可以先理解为是用来声明程序中所用元素的类型以及名称,而 ofApp.cpp 则用来详细描述这些元素的具体实现内容。

设计师的 “HelloWorld”

了解完 OF 程序的结构后,就可以正式开始了。许多编程语言的经典入门都是从输出 “HelloWorld” 开始讲起。但属于设计师的 “HelloWorld”,不应该是字符。咱们从画图开始!

从“点”开始

点线面是平面空间的基础元素。接下来我们会在程序中表现这些基础元素。在 OF 里写程序,刚开始我们只需要关注 ofApp.cpp 里面的 draw 函数。下面的提到代码,你都只需要写在draw函数内。

我们先尝试画一个点,在 draw 函数的大括号内敲下这行代码。

void ofApp::draw(){
    ofDrawCircle(512,384,30);
}

接着点击左上角的运行按钮,会弹出如下界面

通过简单的一句代码,一个处于窗口中间的点就出现了。我们来仔细看看这句代码的含义。

开始的一行 ofDrawCircle ,是关于画点的命令。这在程序中被称作函数。以后如果看到这样的格式:一串英文字符,后面再跟一对括号,那在程序中就被称为函数。

格式:

函数名()

它与数学函数的定义不一样。程序中的函数,其实是一些代码块的封装。我们可以无需搞清函数内部的构造和原理,只需要知道它能做什么。就像开汽车不需要懂引擎的运作原理。

而括号里面的数字,就代表运行这个函数所需要填的参数,它们之间会用逗号来作为分隔。这里的第一个参数就代表点的横坐标,第二个参数代表纵坐标,第三个代表点的半径大小。

这里的数值大小,都是以像素为单位的。你想在屏幕上画一个圆点,就需要这三个变量去确定。由于屏幕的宽高默认是 1024 * 768。所以点坐标(512,384)就会恰好处在屏幕中央。

最后,当你写完这个表达式,请千万记得在指令后面加一个;号来表示结束。这是初学者最容易犯的错误之一,请保持这个习惯,否则程序会出BUG(错误)。

[其他注意事项]在 Xcode 里,必须在英文输入法状态下打入代码。否则在编译文件时程序会报错。一个中文字符的 ; 与英文字符的 ; 对程序而言是完全不一样的。

现在,具有历史意义的第一行代码就被我们敲出来了。后面介绍的绘图函数也会同样简单。函数相当神奇,使用它可以大大提高效率,它生来就是为懒人服务的。只要懂得正确地组装,就能写出功能丰富的程序。

坐标系统

如果你想画一个处于左上角的点应该怎么做?在此之前,你需要先了解 OF 的坐标系统。坐标系统就像地图一样。有了它,所有绘制的图形都有了立足之地。它与我们中学阶段接触到的笛卡尔坐标系很不一样,它的 Y 轴方向是反的。假如我们定义了一个 150 X 100 的画布,那左上角的第一个像素坐标就为(0,0),右下角的最后一个像素坐标则为(149,99)。

觉得难记可以这么去理解,坐标数其实代表了偏移量。起始坐标点序号为 0,与它相邻的坐标就距离初始坐标一个单位,因此序号为 1 。第 150 个坐标距离与第 1 个坐标相比,偏移了 149 个单位,所以序号就为 149。

但为什么要从 0 计算到 149 而不是从 1 计算到 150 呢?看上去多麻烦。这是程序中约定俗成的,很多概念都是从0开始计数。

所以,如果我们希望在左上角画一个点,需要这么去写。

ofDrawCircle(0,0,30);

如果你希望点画在左下角,则需要这样写。

ofDrawCircle(0,768,30);

这里的 768 是默认的窗口高度值。当然,其实是有更便捷的形式的,所有能简化编程流程的方法,总会有前人帮我们想好。我们无需去记住屏幕宽高的具体数值,而是可以用一串英文字符去代表。

例如可以通过 ofGetWidth() 获取窗口的宽,ofGetHeight() 获取程序的高

因此,如果你想在左下角画一个点,可以这样去写

ofDrawCircle(0,ofGetHeight(),30);

在中心画点则变成这样

ofDrawCircle(ofGetWidth()/2,ofGetHeight()/2,30);

“/”在程序中代表除号。这样写程序可以自动计算结果,而无需给出具体的坐标。窗口宽高的一半,就代表中心点的坐标了。这么写的好处,就是点的位置可以随着窗口的变化而变化。OF 的程序运行窗口,是可以随时拉伸的。如果你用绝对值去写,点的位置相对原点是不会改变的。利用 ofGetWidth() 和 ofGetHeight() 则不一样,它获取的是窗口的实时大小,数值是能随时改变的。

除此之外,你也可以在里面写一个超复杂的表达式进去

ofDrawCircle(1 + 2 + 3 + 4 + 5, 100 - 50 - 20 - 10,30);	

当然,没事可不要这么去写,除非是想研究什么古怪的数列规律~~

线与面

当你熟悉了点的画法,画线画面的方法也是类似的。下面列举的是 OF 里面较常用的绘图函数。

画直线

ofDrawLine(x1,y1,x2,y2);

x1, y1代表线条的一个端点横纵坐标,x2,y2则代表另一个。函数会将两坐标相连,成为一条直线。

画贝塞尔曲线

ofDrawBezier(x1,y2,x2,y2,x3,y3,x4,y4);

在程序里画曲线,有多种方式。贝塞尔是其中一种。如果你在 PS 中使用过贝塞尔曲线,你就会了解,一条贝塞尔曲线包含两个端点以及两个控制点。参数的填写顺序是按端点1,控制点1,控制点2,端点2的方式去排序的。

假如我们在程序中写下这段代码

ofDrawBezier(119, 228, 134, 150, 341, 118, 425, 186);

会发现出现并不是一条曲线,而是成了一个封闭的图形。这是因为显示模式的原因所导致的,后面就会提到如何解决这个问题。

画圆(画点)

ofDrawCircle(x,y,r);

x,y 代表圆的横坐标,r 代表圆的半径。

在 OF 程序中,画圆所用的函数和画点的所用函数是一样的,区别只是半径的大小。这与 P5 有所区别。P5 中会分成 point 与 ellipse 函数。

画椭圆

ofDrawEllipse(x,y,w,h);

x,y 代表椭圆的中心坐标,w,h 分别代表椭圆的长与宽。

画三角形

ofDrawTriangle(x1,y1,x2,y2,x3,y3);

这里会用到三对坐标参数,分别代表三角形的 3 个顶点坐标。

画矩形

ofDrawRectangle(x,y,w,h);

前两个参数代表方形左上方的顶点坐标,w,h 分别代表方形的长和宽。

画圆角矩形

ofDrawRectRounded(x,y,w,h,r);

前四个参数与 ofDrawRectangle 一致,后一个参数代表半径。

后面还会提到更多的绘图函数,程序中提供的绘图函数往往很基础,也很有限。当你想绘制一些更复杂的图形时,就需要你去用这些基础元件去组装。

上色

只有形状,不免无趣。我们还要学会给图形上色。和我们预想的上色方式不一样,不是每个形状都会有一个独立的色彩属性,来让你去定义它的颜色。比如ofDrawEllipse(200,200,20,20,?)后面再一个参数。

程序中上色要使用 ofSetColor() 函数,这是一种通用的上色方法,它会设置在这个命令之后的所有图形元素的颜色。

ofSetColor() 的输入参数很特别,它允许多种输入方式。这在编程中被称为“重载”。这类函数允许你用同一个函数命令,但随着传入参数的类型和数量不同,就会产生不同的效果。

写法1:

ofSetColor(x);

x代表灰度值,最小为0(代表黑色),最大为255(代表白色)

代码示例:(为方便演示,宽高的分辨率都设置为500)

void ofApp::draw(){
	ofSetColor(0);
	ofDrawCircle(ofGetWidth()/2,ofGetHeight()/2,50);
}

写法2:

ofSetColor(x,a);

x代表灰度值,a代表透明度(最小值0,最大值255)

代码示例:

void ofApp::draw(){
	ofSetColor(0,100);
	ofDrawCircle(ofGetWidth()/2,ofGetHeight()/2,50);
}

写法3:

ofSetColor(r,g,b);

r代表red(红),g代表green(绿),b代表blue(蓝)。设计师对RGB色彩模式必定会相当熟悉,这分别代表三原色。

代码示例:

void ofApp::draw(){
    ofSetColor(0,200,0);
    ofDrawCircle(ofGetWidth()/2,ofGetHeight()/2,50);
}

写法4:

ofSetColor(r,g,b,a);
```	

代码示例:

```	
void ofApp::draw(){
	ofSetColor(0,200,0,80);
	ofDrawCircle(ofGetWidth()/2,ofGetHeight()/2,50);
}

rgb代表三原色,a代表alpha(透明度)

综合实例

前面的图形,是可以同时绘制到画布上的。只要这些函数命令都写在 draw 函数内

void ofApp::draw(){
     ofSetColor(0);
     ofDrawCircle(100, 250, 50);
     ofSetColor(0,100);
     ofDrawCircle(200, 250, 50);
     ofSetColor(0, 200, 0);
     ofDrawCircle(300, 250, 50);
     ofSetColor(0, 200, 0, 80);
     ofDrawCircle(400, 250, 50);
}

若是我们只写一个 ofSetColor ,则会这样

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

由于代码是由上至下执行的,ofSetColor()之后绘制的图像,都会自动填充同一个颜色。只要你没有写其他的 ofSetColor()。

描边模式与填充模式

这里需要提到两个函数,ofFill() 与 ofNoFill()。

前面你会发现,当我们用bezier命令去画曲线的时候,默认生成的是一个填充图形。这是因为程序默认启用的是 ofFill() 模式来进行绘图。如果我们仅仅希望显示边线,需要在前面加上 noFill() 函数

代码示例:

void ofApp::draw(){
    ofNoFill();
    ofDrawBezier(119, 228, 134, 150, 341, 118, 425, 186);
}

它就会设置成只显示线框的模式

同理,你也可以将它应用在 ofDrawCircle,ofDrawRectangle 等的绘图函数上。

代码示例:

void ofApp::draw(){
     ofNoFill();
     ofDrawCircle(ofGetWidth()/2,ofGetHeight()/2,50);
}

边线宽度与边线颜色

前面我们已经掌握了如何设置填充图形的颜色,以及如何切换不同的显示模式。那如何设置边线的颜色以及宽度?是否也需要独立的函数命令?

对设置颜色而言,无论你在哪个显示模式下,ofSetColor()函数都是通用的,都用于设置图形元素的颜色。因此,你只要多加一行。

代码示例:

void ofApp::draw(){
    ofNoFill();
    ofSetColor(255,0,0);
    ofDrawCircle(ofGetWidth()/2,ofGetHeight()/2,50);
}

若想设置边线的宽度,则可以用 ofSetLineWidth() 函数,它只对边线显示模式下的图形起作用。

代码示例:

void ofApp::draw(){
    ofNoFill();
    ofSetLineWidth(5);
    ofSetColor(255,0,0);
    ofDrawCircle(ofGetWidth()/2,ofGetHeight()/2,50);
}

ofSetLineWidth() 里填写的参数,在程序中是有大小限制的。线条最粗默认只能到 10 像素。超过这个数值,线条的像素宽仍然会是 10 像素。

带描边的填充图形

调用一次绘图函数,要么是画边线,要么是画填充图形。所以如果你想画一个带描边形状的填充图形时,就需要将同一个绘图函数写两次,每个部分再独立设置不同的属性。

代码示例:

void ofApp::draw(){	    
    // 填充部分
    ofFill();
    ofSetColor(255);
    ofDrawCircle(ofGetWidth()/2,ofGetHeight()/2,50);
	    
    // 描边部分
    ofNoFill();
    ofSetLineWidth(5);
    ofSetColor(0);
    ofDrawCircle(ofGetWidth()/2,ofGetHeight()/2,50);
}

  • 这与Processing的绘图机制十分不一样。需要格外注意。
  • 上面的 “//” 号,代表的是注释符。它一般用作提示或是解释程序的意义,是给写程序的人看的,可以出现在程序中的任何位置。程序编译的时候,计算机不会对 “//” 之后的字符进行处理。

背景颜色设置

你会发觉现在绘图的背景一直是灰色的,因为我们没有进行过设置。所以背景默认为灰色。

设置背景颜色的函数是 ofBackground() ,它可以用来填充背景,有点像油漆桶。括号中填的是色彩参数,和前面的 ofSetColor() 一样,也有四种不同的参数输入方式。

ofBackground(x) 

x代表灰度值,最小为0(代表黑色),最大为255(代表白色)

ofBackground(x,a) 

x代表灰度值,a代表透明度(最小值0,最大值255)

ofBackground(r,g,b) 	

r代表red(红),g代表green(绿),b代表blue(蓝)

ofBackground(r,g,b,a)

r代表red(红),g代表green(绿),b代表blue(蓝),a代表alpha(透明度)

代码示例:

void ofApp::draw(){
    ofBackground(0);
    ofSetColor(255,0,0);
    ofDrawCircle(ofGetWidth()/2,ofGetHeight()/2,50);
}

通过这种便捷的办法就可以画一个背景。当然,我们开动一下脑筋,用已知的绘图函数,也是能够做到的。例如用 ofDrawRectangle。

代码示例:

void ofApp::draw(){
	ofSetColor(0);
	ofDrawRectangle(0,0,ofGetWidth(),ofGetHeight());   	ofSetColor(255,0,0);
	ofDrawCircle(ofGetWidth()/2,ofGetHeight()/2,50);
}

这种画背景的方式显然没有直接用 ofBackground()函数简洁。但到后面会有一种很有意思的玩法。在后面谈到动画相关章节时,会给大家揭晓。

综合运用

上面提到的知识点,已经足够动手去做一些作品了。下面给出一个综合示例

代码示例:

	void ofApp::draw(){
	    //背景色设置与线条宽度设置
	    ofBackground(200, 0, 0);
	    ofSetLineWidth(8);
	    
	    // 耳
	    ofFill();
	    ofSetColor(255);
	    ofDrawCircle(175, 220, 91);
	    ofDrawCircle(ofGetWidth() - 175, 220, 91);
	    ofNoFill();
	    ofSetColor(0);
	    ofDrawCircle(175, 220, 91);
	    ofDrawCircle(ofGetWidth() - 175, 220, 91);
	    
	    // 脸
	    ofFill();
	    ofSetColor(255);
	    ofDrawRectangle(100, 100, 300, 300);
	    ofNoFill();
	    ofSetColor(0);
	    ofDrawRectangle(100, 100, 300, 300);
	    
	    // 眉毛
	    ofDrawLine(150, 160, 220, 240);
	    ofDrawLine(ofGetWidth() - 150, 160, ofGetWidth() - 220, 240);
	    
	    // 左眼
	    ofFill();
	    ofSetColor(255);
	    ofDrawCircle(175, 220, 30);
	    ofNoFill();
	    ofSetColor(0);
	    ofDrawCircle(175, 220, 30);
	    
	    // 右眼
	    ofFill();
	    ofSetColor(255);
	    ofDrawCircle(ofGetWidth() - 175, 220, 30);
	    ofNoFill();
	    ofSetColor(0);
	    ofDrawCircle(ofGetWidth() - 175, 220, 30);
	    
	    // 嘴
	    ofFill();
	    ofSetColor(255);
	    ofDrawTriangle(170, 300, ofGetWidth() - 170, 300, 250, 350);
	    ofNoFill();
	    ofSetColor(0);
	    ofDrawTriangle(170, 300, ofGetWidth() - 170, 300, 250, 350);
	    
	    // 鼻
	    ofFill();
	    ofSetColor(0);
	    ofDrawCircle(ofGetWidth()/2, ofGetHeight()/2,4);
	 
	}

这段代码有另一个 Processing 版本,你可以对比两者的异同。现在你也可以开始用类似的方式,用基础绘图函数去创作自己的作品了。

END

学程序不要只去背知识点,而是要将它们灵活运用起来。除了一些特别常用的函数要记住以外,其余的在用的时候再去查也是没有问题的。

在 OF 官网中可以查到 OF 的所有相关函数。(http://openframeworks.cc/documentation/)

在浏览器中通过查找命令(Ctrl + F)就能在此页面下直接搜索函数关键字

点击词条就有对应的用法与解释

但不是所有函数都有解释和对应的范例的,需要你去碰运气了。在这点上 Processing 的文档就做得比较好,几乎每个命令都有解释。

  • 另一个小秘诀,如果在 Xcode 里写程序的时候忘记怎么用某个函数了。只要你开头的几个字母书写正确,隔一段时间后下面就会直接弹出相关函数的提示,这时直接按回车则能自动补全函数。你也可以通过上下方向键来切换并查看其他函数。

通过这节的介绍。是否觉得代码没有以前那么神秘了?

在程序中画图,远没有用画笔或者ps工具那么形象和自由。里面的任何一个图形,都需要用数据来描述它的位置,大小或角度。而对于复杂的图形,是很难纯靠想象,直接写出代码的。一般是先绘制好草图,再用ps的信息面板查看关键点的坐标,最后选择合适的函数去表现。

  • 就像先前的例子

看到这里,有怀疑精神的设计师估计会跳出来。我在PS里面随便拉几下就能画出来的图形,为什么非要在用程序里画,过程还这么麻烦?这完全体现不出程序的优势?

说得很有道理,请保持好质疑的心态,答案在后面揭晓。之后还会聊到三个“大头”函数,setup函数,update函数以及draw函数。只要灵活运用,就能让图片“动”起来。而我们在学校里曾经学习的那些有关数学函数的知识,也开始有了用武之地,那些看似枯燥的数学公式,将会成为我们控制图形的利器。