查看原文
其他

写给设计师的OF编程指南(8) - ​自定义函数与分形递归

2016-07-21 Wenzy InsLab


这节将会介绍如何在程序中自定义函数,同时用递归函数创作分形图形。

自定义函数

编程中的一个重要概念就是复用。而定义函数,最能体现复用思想。不夸张地说,只有灵活地掌握它,才算真正走进编程的大门。因为你会开始去思考,如何对一些序列化操作进行抽象,以此提高编程效率。

像 OF 中自带的绘图函数 ofDrawCicle,ofDrawRectangle 其实就是经过设计者的抽象后才产生的,它们的底层由一些更“原始”的代码组成,我们使用的时候无需知道里面有什么,只要清楚函数名以及它对应的功能即可。一旦我们掌握了定义函数的方法,也就能用类似的方式对代码块进行打包,自行定制组件。

  • “返回值类型”代表的是函数返回值的类型,后面会展开。

  • 而“函数名”代表的就是你给函数取的名称。以后我们可以直接通过名称,来调用这个函数。与变量名的命名规则类似,函数名只能使用字母,数字或是下划线,并且第一个字母必须是字母或是下划线。

  • 两个大括号之间,需要写我们希望函数执行的语句,这个部分也被称为“函数体”

基本语法:

   返回值类型 ofApp::函数名(){        语句;    }
  • 在 OF 中,这是一个标准写法。它表示我们定义的函数,会被包含在 ofApp 的主程序中。仔细观察会发现,像 setup , update 和 draw 函数,他们都无一例外地在前面写着 ofApp:: 。现阶段我们只要遵循这一规范即可。(特定场景下有特定的写法)

  • “返回值类型”代表的是函数返回值的类型,后面会展开。

  • 而“函数名”代表的就是你给函数取的名称。以后我们可以直接通过名称,来调用这个函数。与变量名的命名规则类似,函数名只能使用字母,数字或是下划线,并且第一个字母必须是字母或是下划线。

  • 两个大括号之间,需要写我们希望函数执行的语句,这个部分也被称为“函数体”

先来看一个最基础的例子

代码示例(8-1):

--- ofApp.h 内    #include "ofMain.h"    class ofApp : public ofBaseApp{        public:            void setup();            void update();            void draw();            ...            void myFunction();    }; --- ofApp.cpp 内    void ofApp::setup(){        myFunction();    }    void ofApp::update(){    }    void ofApp::draw(){    }    void ofApp::myFunction(){        cout << "1" << endl;        cout << "2" << endl;        cout << "3" << endl;        cout << "4" << endl;    }

输出结果:


  • 与声明一个变量类似,在 OF 中要想自定义一个函数。需要先在 ofApp.h 中声明。“ void ”代表的就是函数没有返回值,意思是此函数只会执行函数体中的代码,而不会向外返回数据。后面的 “ myFunction ” 代表函数的名称。

  • 至于自定义函数里有什么东西,就需要在 ofApp.cpp 中定义。这里写的“ void ” 需要呼应 ofApp.h 的 “ void ”,以保持前后类型的一致。

  • 调用函数时,只要写上函数名并且后接一个中括号即可。在示例的 setup 函数中,就调用了一次 myFunction 函数。因此它会执行我们定义好的函数内容,依次在控制台上输出数字 1,2,3,4。

上面的例子只是在控制台输出字符数据。接下来可以放在图形的语境中去理解自定义函数。

代码示例(8-2):

--- ofApp.h 内    #include "ofMain.h"    class ofApp : public ofBaseApp{        public:            void setup();            void update();            void draw();            ...            void myShape();    }; --- ofApp.cpp 内    void ofApp::setup(){         ofSetWindowShape(400,400);    }    void ofApp::update(){    }    void ofApp::draw(){        ofBackground(255);        myShape();    }    void ofApp::myShape(){         ofSetColor(0,0,255,70);        ofDrawCircle(ofGetWidth()/2,ofGetHeight()/2,50);        ofSetColor(0,255,0,70);        ofDrawRectangle(ofGetWidth()/2, ofGetHeight()/2, 50,50);        ofDrawRectangle(ofGetWidth()/2 - 50, ofGetHeight()/2 - 50, 50,50);       }


上例中的 myShape 函数就是绘制一个复合图形。其中包含一个圆和两个矩形。
myShape 每调用一次,这组元素就会绘制一次。

可以试着在 draw 函数里写上两个 myShape 函数。由于原始图形的色彩是带透明度的,所以两组图形重叠到一起就会变深。


传入参数

自定义函数很方便。但会发现上面的方式有些局限,无法修改函数中的参数。导致图形元素的位置都是固定的。我们可以学习另外一种写法,往函数中传入东西。

基本语法:

   返回值类型 函数名(参数类型 参数名){        语句;    }

只要在函数名后的中括号中写上参数类型与参数名即可。假如我们希望往 myFunction 函数中传入一个浮点变量。就可以这么写

   void ofApp::myFunction(float x){        语句;    }

这时我们在函数体内,就可以调用这个参数 x 了。参数的变量名可以任意取,只要遵循变量名的命名规范。当希望传入更多的参数,只要在后面加上逗号,同时写上参数类型,参数名即可。

如:

   void ofApp::myFunction(float x,float y){        语句;    }

当掌握了这种写法,我们就可以通过传入参数改变图形元件的位置了。

代码示例(8-3):

—- ofApp.h 内

   #include "ofMain.h"    class ofApp : public ofBaseApp{        public:            void setup();            void update();            void draw();            ...            void myShape(float x,float y);    };

—- ofApp.cpp 内

   void ofApp::setup(){        ofSetWindowShape(400,400);    }    void ofApp::update(){    }    void ofApp::draw(){        ofBackground(255);        myShape(ofGetWidth()/3, ofGetHeight()/2);        myShape(ofGetWidth()/3 * 2, ofGetHeight()/2);        myShape(mouseX,mouseY);    }    void ofApp::myShape(float x,float y){        ofSetColor(0,0,255,70);        ofDrawCircle(x,y,50);        ofSetColor(0,255,0,70);        ofDrawRectangle(x,y, 50,50);        ofSetColor(0,255,0,70);        ofDrawRectangle(x - 50, y - 50, 50,50);    }


传入多种类型

传入的参数可以不仅仅是 float。像 bool,int,string 这些数据类型都可以传入。

代码示例(8-4):

—- ofApp.h 内

   #include "ofMain.h"    class ofApp : public ofBaseApp{        public:            void setup();            void update();            void draw();            ...            void myShape(bool type,float x,float y);    };

—- ofApp.cpp 内

   void ofApp::setup(){        ofSetWindowShape(400,400);    }    void ofApp::update(){    }    void ofApp::draw(){        ofBackground(255);        ofSetColor(255);        myShape(true,mouseX,mouseY);        myShape(false,ofGetWidth() - mouseX,ofGetHeight() - mouseY);    }    void ofApp::myShape(bool type,float x,float y){        ofSetColor(0);        ofDrawCircle(x,y,50);        if(type){            ofSetColor(0,0,255);        }else{            ofSetColor(255,0,0);        }        ofDrawRectangle(x,y, 50,50);        ofDrawRectangle(x - 50, y - 50, 50,50);    }


这个例子就是利用 bool 值的不同,结合 if 语句来决定图形的颜色。下节我们会学到一种新的数据类型, ofColor。它能存储色彩的数值,若是使用此类型作为传入参数,控制起来就会更灵活。


传出参数

有输入,就有输出。有些时候我们希望函数会返回一些结果。这时我们就可以考虑在函数体中使用 return 关键字。假如我们想创建一些数学公式,例如计算圆的面积,在程序中就可以这么写

代码示例(8-5):

—- ofApp.h 内

   #include "ofMain.h"    class ofApp : public ofBaseApp{        public:            void setup();            void update();            void draw();            ...            float circleArea(float r);    };

—- ofApp.cpp 内

   void ofApp::setup(){          cout << circleArea(2) << endl;    }    float ofApp::circleArea(float r){        float area = PI * r * r;        return area;    }

输出结果:


  • 在函数中创建的 area 相当于是一个局部变量,可用来储存计算后的面积。最后通过将它写在 return 关键字后,就能在调用函数时输出 area 中储存的数据

  • 因为输出的数据类型为 float ,所以在定义函数的开头,返回值的类型就必须写 float,以此取代没有返回值时的 void。


函数重载

OF 中的函数是可以允许重载的,重载的意思是。你可以定义多个不同参数的函数,并且使用同一个函数名。在调用的时候,程序会先判断参数的个数,然后再决定相应的函数。

是否记得前面提到的 ofSetColor 函数?它允许输入的参数个数可以为 1,3,4。根据参数的不同,函数的作用便会有所区别。

下面的例子会设计一个名为 calculate 的函数,当参数个数为 2,便会将两者相乘(相当于求面积)。当参数个数为 3,则将三者相乘。

代码示例(8-6):

—- ofApp.h 内

   #include "ofMain.h"    class ofApp : public ofBaseApp{        public:            void setup();            void update();            void draw();            ...            float calculate(float a,float b);            float calculate(float a,float b,float c);    };

—- ofApp.cpp 内

   void ofApp::setup(){          cout << calculate(2,3) << endl;          cout << calculate(2,3,4) << endl;    }    float ofApp::calculate(float a,float b){        float result = a * b;        return result;    }    float ofApp::calculate(float a,float b,float c){        float result = a * b * c;        return result;    }


递归与分形

递归

当你掌握了自定义函数的方法。就可以写递归函数了。而利用递归函数,你可以很方便地绘制出分形图形。

递归一词听上去很高深,其实就是指函数调用自身。我们以前肯定有听说过这样一个经典的故事,先用它来举例:

从前有座山,山里有个庙,庙里有个老和尚,给小和尚讲故事。故事讲的是:从前有座山,山里有个庙,庙里有个老和尚,给小和尚讲故事。故事讲的是:从前有座山,山里有个庙……

如果继续讲下去,这个故事永远不会终止。它就像递归的结构。只是在程序中,不能让这个流程永远地执行下去,我们需要设置一个条件,来决定是继续还是跳出。否则就会陷入死循环。

下面是一个基本的递归示例:

代码示例(8-7):

—- ofApp.h 内

   #include "ofMain.h"    class ofApp : public ofBaseApp{        public:            void setup();            void update();            void draw();            ...            void recursion(float l);    };

—- ofApp.cpp 内

   void ofApp::setup(){        ofSetWindowShape(400,400);      }    void ofApp::update(){    }    void ofApp::draw(){        ofBackground(255);        ofTranslate(ofGetWidth()/2,ofGetHeight()/2);        recursion(350);    }    void ofApp::recursion(float l){        if(l > 30){            l = l * 0.92;            ofNoFill();            ofSetColor(0);            ofSetRectMode(OF_RECTMODE_CENTER);            ofDrawRectangle(0,0,l, l);            recursion(l);        }    }


  • 可以看到,从语法结构上,和一般的自定义函数没有多大区别。唯一的不同,就是 recursion 函数中还调用了一次 recursion,同时里面有一个 if 语句作为包裹。

  • 其中的 l ,作为输入参数,它是矩形的边长,同时也是递归函数是否执行的条件。只有当矩形的长度 l 大于 30 时,递归才会进行下去。反过来看,当 l <= 30 时,就是递归的终止条件了。所以你会看到图形的中心是空白的。

  • 仅仅有判断条件是不行的,还需要考虑它能否出现不满足的情况。if 条件语句的第一行代码 l = l * 0.92; 就会在每执行一次函数时,都缩小 l 的数值。接着才把它作为输入参数,传递到下一个 recursion 函数中。如此反复,l 迟早会达到终止条件。

  • 假如我们将乘的 0.92,修改成一个大于 1 的数值,程序是会发生崩溃的。因为 l 值始终在增大,就达不到小于 30 的条件了。但不代表此值小于 1 就完全没有问题了,当你写成 0.999999,程序也还是会崩溃。因为变化越微小,它执行的次数就越多,程序在一帧当中需要绘制的矩形也越多。当这个数量超出了计算机的处理能力,便会崩溃。

  • 递归结构有点像电影《盗梦空间》中的梦中梦。梦境之间是互相嵌套。需要有一个条件来“唤醒”,否则人就永远陷进去了。

分形

现在你可以通过递归函数探索另一个魅力无穷的世界 - 分形。


分形(Fractal),又称碎形、残形,通常被定义为“一个粗糙或零碎的几何形状,可以分成数个部分,且每一部分都(至少近似地)是整体缩小后的形状”,即具有自相似的性质。

自然界中就能看到许多分形。


图片来源:


现在,相信你对分形已经有些直观的印象了。在程序中设计分形图形相当简单。通过递归你可以用极简的法则创造出丰富的细节。参看下面例子

代码示例(8-8):

—- ofApp.h 内

   #include "ofMain.h"    class ofApp : public ofBaseApp{        public:            void setup();            void update();            void draw();            ...            void recursion(float r,int num);    };

—- ofApp.cpp 内

   void ofApp::setup(){        ofSetWindowShape(400,400);      }    void ofApp::update(){    }    void ofApp::draw(){        ofBackground(255);        ofTranslate(ofGetWidth()/2,ofGetHeight()/2);        recursion(200,0);    }    void ofApp::recursion(float r,int num){        if(r > 10){            if(r > 30){                ofNoFill();                ofSetColor(0);            }else{                ofFill();                ofSetColor(0);            }            float ratio = mouseX/(float)ofGetWidth();            if(ratio > 0.6){                ratio = 0.6;            }            r = r * ratio;            num ++;            // 绘制中心矩形            ofSetRectMode(OF_RECTMODE_CENTER);            ofDrawRectangle(0,0, r * 2, r * 2);            // 绘制四周矩形            ofPushMatrix();            ofRotate(ofGetElapsedTimef() * num * 10);            ofTranslate(-r,-r);            recursion(r,num);            ofPopMatrix();            ofPushMatrix();            ofRotate(ofGetElapsedTimef() * num * 10);            ofTranslate(-r,r);            recursion(r,num);            ofPopMatrix();            ofPushMatrix();            ofRotate(ofGetElapsedTimef() * num * 10);            ofTranslate(r,-r);            recursion(r,num);            ofPopMatrix();            ofPushMatrix();            ofRotate(ofGetElapsedTimef() * num * 10);            ofTranslate(r,r);            recursion(r,num);            ofPopMatrix();        }    }


  • 在一个递归函数的函数中,函数是可以调用多次的。上面的示例就调用了四次,从而创造了多重分支

  • 图形的繁复程度之所以是动态的。是因为用鼠标的横坐标控制了 ratio 的比例。“ ratio = mouseX/(float)width ”。ratio 的值越大,r 值的递减速度就越慢,所以递归的次数会增多,图形数量也会增多

  • ratio 之后添加的一个判断,if(ratio > 0.6){ratio = 0.6;} 是为了让 ratio 的值不超出 0.6。避免程序因为绘制的图形过多而发生崩溃。

  • ofRotate 可以用注释符隐藏,能更清晰地看见整体的枝干结构。变量 num 的作用是记录当前递归的次数。最终通过 num 影响旋转的速度。可以看到递归的次数越多,方块的旋转速度越快


在分形图形里,有一个有名的图形叫谢尔宾斯基地毯。这与上面的代码是很类似的,只要略作修改,你也可以用程序绘制出来。



  • 谢尔宾斯基地毯

分形相关的练习


分形是图形创作的黑魔法。用极少的代码,极简的结构,就可以创造丰富的细节。上面的练习都基于分形,可以很明显地看到自相似的特征。现在你也可以动手尝试,用代码去设计一些充满魅力的分形图形。

END

最后推荐两部分形相关的纪录片, 相信你会为之着迷 ~~


  • PBS 寻找隐秘的维度 Hunting the Hidden Dimension

  • BBC 神秘的混沌理论 The Secret Life of Chaos


Openframeworks 系列文章

[1]创意编程工具安装向导-Processing与Openframeworks

[2]创造第一个OF程序

[3]图形的运动(上)

[4]图形的运动(下)

[5]程序的流程控制-循环语句

[6]程序的流程控制-条件语句(上)

[7]程序的流程控制-条件语句(下)

资源索引

CreativeCoding学习资源索引

Twitter资源索引

经验分享

设计师如何自学创意编程



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存