查看原文
其他

写给设计师的 OF 编程指南11-数据储物箱

2016-09-23 Wenzy InsLab


这节将会介绍一个在  Openframeworks 中非常重要的概念 - Vector。之前储存数据,都会用到 int ,float 这类变量。假设我们有 100 个数据想保存,用老方法就会非常繁琐。下面介绍的 Vector 可以用更便捷的方式存放更多数据。它就有点像一排带有数字编号的箱子,根据编号可以存取需要的信息。

若之前在 Processing 中有接触过数组的概念。可以先把 vector 当作是加强版的数组,只是它的很多特性与数组(array)有所区别。在 C++ 中寫程序,vector 会比数组有更多优点,所以更多会用它儲存陣列,而不是使用数组。

下面先介绍Vector的基本语法

Vector的声明与创建

Vector 在声明时,必须指定一个储存的数据类型。尽管它能同时装多个数据,但数据类型必须一致。

下面是 vector 的声明格式:

vector<数据类型> 变量名;

当你希望声明一个整型的 vector,就可以这么写。

vector<int> data;

声明完后,vector 就能开始使用了。与 Processing 中的数组有所区别,开始时无需指定数组的大小。vector 创建时,默认的容量大小都为零。数据是后期逐个添加的。而添加的方法,就是使用 push_back 。下面是它的使用格式。

变量名.push_back(数据);

假如是 int 型的 vector,就可以这样写

data.push_back(5);

这将会在 vector 中添加了一个整数数据“ 5 ”。之后如果想继续添加更多的数据,就可以重复使用 push_back。例如在“ data.push_back(5);” 之后,再写一句“ data.push_back(10);”。程序便会在第一个元素,“ 5 ”之后,添加一个“ 10 ”。若再加一句 “ data.push_back(20);”,便会在第二个元素, “ 10 ” 之后,添加一个 “ 20 ”。每次 使用 push_back,都会默认在 vector 的末尾插入一个新元素。

新增加的数据,根据添加的次序不同,会有各自的编号。因此就能根据编号来对 vector 中的元素进行读取或者写入。这些编号在程序中被称为“下标”。vector 的下标都是从 0 开始的,所以第一个元素的下标为 0。第二个元素下标为 1。依此类推,最后一个元素的下标则为 vector 元素总个数减 1。下标从 0 开始是约定俗成的,虽然有些违反直觉,但只要多用就会熟悉。

我们可以用 cout 来测试,通过下标读取数据

cout << data[0] << endl; // 输出结果为 5

数据不仅仅可以在 push_back 时进行写入。只要 vector 有使用过 push_back,里面有储存数据。之后都可以通过下标进行重新赋值。

data[0] = 10;

此时如果用 cout 输出,显示的数值便会变成 10。

有关 vector 的基本语法先介绍到这里,下面看一个完整的实例

代码示例(11-1):

—- ofApp.h 内

#include "ofMain.h" class ofApp : public ofBaseApp{    public:        void setup();        void update();        void draw();        ...        vector<int> numbers; };

—- ofApp.cpp 内

void ofApp::setup(){    numbers.push_back(10);    numbers.push_back(20);    numbers.push_back(30);    numbers.push_back(40);    numbers.push_back(50);    cout << numbers[0] << endl;    cout << numbers[1] << endl;    cout << numbers[2] << endl;    cout << numbers[3] << endl;    cout << numbers[4] << endl;    cout << numbers.size() << endl; }


  • 这个例子在 vector 中加入了 5 个元素,并对元素进行了写入和输出。

  • 末尾的“ numbers.size() ” 可以获取 vector 的大小。格式为” 变量名.size() “。

C++ 中的数组 array 和 vector 的最大异同

  • array 声明时必须定义数组的元素个数,而 vector 不用。它的元素数量是可以动态变化的。

  • array 的下标不能越界,不能小于 0 或者大于等于数组的长度,否则就会出现报错提示,所以在某程度上保证了程序的安全性。而 vector 允许越界访问,有时候并不报错。因而使用 vector 时,需要更规范地使用下标。

用 for 循环对Openframeworks赋值

有些时候我们不希望手动地对每个数组元素进行赋值,这时就要用到 for 循环。下面的例子就结合了随机函数,让每个元素在初始化时都赋上随机值。

代码示例(11-2):

—- ofApp.h 内

#include "ofMain.h" class ofApp : public ofBaseApp{    public:        void setup();        void update();        void draw();        ...        vector<float> numbers; };

—- ofApp.cpp 内

void ofApp::setup(){    for(int i = 0;i < numbers.size();i++){        numbers.push_back(ofRandom(100));    }    for(int i = 0;i < numbers.size();i++){        cout << numbers[i] << endl;    } }

输出结果:


代码说明:

  • 在 for 循环的终结条件中,用到了 numbers.size()。这是一个比较常规的写法。虽然也能直接写成 5。但这么做的好处是,即使前面修改了数组的大小,后面都能保证写入或输出所有的数据。而无需重新修改终结条件的数值。

vector储存人物信息

假如我们希望储存不同类型的数据,如人物信息,货物价格。就可以考虑用不同类型的vector 来储存这些数据。

代码示例(11-3):

—- ofApp.h 内

#include "ofMain.h" class ofApp : public ofBaseApp{    public:        void setup();        void update();        void draw();        ...        vector<string> name;        vector<float> height;        vector<int> age;        vector<bool> gender; };

—- ofApp.cpp 内

void ofApp::setup(){    name.push_back("Mike");    gender.push_back(1);    age.push_back(5);    height.push_back(0.98);    name.push_back("Jake");    gender.push_back(1);    age.push_back(10);    height.push_back(1.34);    name.push_back("Kate");    gender.push_back(0);    age.push_back(18);    height.push_back(1.7);    for(int i = 0;i < 3;i++){        cout << "name:" << name[i] << " gender:" << gender[i] << " age:" << age[i] << " height:" << height[i]<< endl;    } }

运行结果:


  • 作为“人”这一个体,会有名字,身高,性别这些与之关联的属性。但由于这些属性都是不同的类型。我们不能把它都放到一个数组里。像名字显然属于字符,会用到 string。性别非男即女,可以考虑用bool。年龄通常是整数,所以用 int。身高一般包含小数,所以用 float。

  • 只要把相应人物的各类属性依序录入。就能通过同一个下标。取出对应人物的属性,获得我们想要的结果。在这个程序中,在同一行内就会分别输出姓名,性别,身高,年龄。

vector绘制随机圆点

下面开始抛开抽象的字符,尝试在图形世界中尽情使用vector。

前面的章节,提到过可以在 randomSeed 在画面上画随机的原点。通过 randomSeed 可以不用创建变量,但这样做显然会有局限。这里学习了vector以后,就能够用简便的方法把每个点的坐标都记录下来。

代码示例(11-4):

—- ofApp.h 内

#include "ofMain.h" class ofApp : public ofBaseApp{    public:        void setup();        void update();        void draw();        ...        vector<float> pointsX,pointsY,radiuses;          int num; };

—- ofApp.cpp 内

void ofApp::setup(){    ofSetWindowShape(700,700);    num = 50;    for(int i = 0;i < num;i++){        pointsX.push_back(ofRandom(ofGetWidth()));        pointsY.push_back(ofRandom(ofGetHeight()));        radiuses.push_back(ofRandom(10,30));    } } void ofApp::update(){ } void ofApp::draw(){    ofBackground(0);    for(int i = 0;i < num;i++){        ofDrawCircle(pointsX[i],pointsY[i],radiuses[i]);    } }

运行效果:


  • 对照前面的例子,可以用vector实现完全相同的结果

  • 这里创建了两个vector - pointsX 和 pointsY。因此能分别保存圆点的横纵坐标。

对随机点进行连线

有人可能会有疑惑,既然 randomSeed 也能实现同样效果。那为什么要独立地储存坐标点?下面的例子就通过vector,调取前后两个点的数据,并进行连线。如果是用 randomSeed 的方式,是无法知道上一个 random 产生的数值是什么。因为每次绘图都是用完即弃,并没有用某个容器把数据装起来。

代码示例(11-5):

—- ofApp.h 内

#include "ofMain.h" class ofApp : public ofBaseApp{    public:        void setup();        void update();        void draw();        ...        vector<float> pointsX,pointsY,radiuses;        int num; };

—- ofApp.cpp 内

void ofApp::setup(){    ofSetWindowShape(700,700);    num = 100;    for(int i = 0;i < num;i++){        pointsX.push_back(ofRandom(ofGetWidth()));        pointsY.push_back(ofRandom(ofGetHeight()));        radiuses.push_back(ofRandom(2,7));    } } void ofApp::update(){ } void ofApp::draw(){    ofBackground(0);    for(int i = 0;i < num;i++){        ofDrawCircle(pointsX[i],pointsY[i],radiuses[i]);        if(i > 0){            ofDrawLine(pointsX[i],pointsY[i],pointsX[i - 1],pointsY[i - 1]);        }    } }

运行效果:


代码说明:

  • 绘制直线可以用 ofDrawLine 函数,前后两组坐标分别表示线的两个端点。假如我们想将vector 中的前后两个坐标相连,第一个坐标下标为 i,那前一个坐标自然就是 i - 1。

  • 用 if 语句并把判断条件写成 i > 0 ,是为了保证下标不越界,程序不出错。因为当 i = 0 时,pointX[ i - 1 ] 的下标就为 - 1 。因而需要多加一个判断,跳过当 i = 0 的情况。

小技巧:

在上例的基础上。我们可以创建一个局部变量 showNum 来影响 for 循环的终结条件。最终产生一个连线的动画效果。setup 函数可保持不变,将 draw 函数替换成如下代码

void ofApp::draw(){    ofBackground(0);    int showNum = ofGetElapsedTimef() * 5;    if(showNum > num){        showNum = num;    }    for(int i = 0;i < showNum;i++){        ofDrawCircle(pointsX[i],pointsY[i],radiuses[i]);        if(i > 0){            ofDrawLine(pointsX[i],pointsY[i],pointsX[i - 1],pointsY[i - 1]);        }    } }

运行效果:



代码说明:

  • 程序之所以产生动画,与 showNum 的数值有关

  • 在创建 showNum 时,它就是一个随着程序运行时间越长,数值不断增大的变量。在这里每过一秒,数值就会增大 5。

  • 最开始 for 循环的终结条件写的是” i < num “,这会将vector 中的点一并画出来。但如果取的是一个变化的数值。就可以从下标为 0 的元素开始,一点一点地展示数据

  • 之后若想制作一个画板,播放绘画轨迹。就能用类似的方式去实现。

用ofBeginShape,ofEndShape 画折线

前面的例子结合了 if 条件语句。逐条绘制了直线。下面将介绍一个方法,会让写法更简洁。先看基本实例

代码示例(11-6):

void ofApp::setup(){    ofSetWindowShape(700,700);   } void ofApp::draw(){    ofBackground(0);    ofBeginShape();    ofVertex(350,100);    ofVertex(100,600);    ofVertex(600,600);    ofEndShape(); }

运行结果:


代码说明:

  • 在绘制图形时,会用到ofBeginShape 和 ofEndShape  进行包裹。ofBeginShape 放开头,ofEndShape 放末尾

  • 其中ofVertex 函数放在两者之间,它表示顶点,其中的两个参数作为点的横纵坐标。

  • 程序中写了三个 ofVertex 函数,因而会把三个顶点依序连起来

由于没有写 ofNoFill 函数,所以绘制的结果会是填充图形,而不是折线。只要在其中加上

-

ofNoFill();

就能只显示轮廓,也就成了折线。


掌握了这种写法,例 11-5 的 draw 函数部分就能写成。

void ofApp::draw(){    ofBackground(0);    ofBeginShape();    for(int i = 0;i < num;i++){        ofDrawCircle(pointsX[i],pointsY[i],radiuses[i]);        ofVertex(pointsX[i],pointsY[i]);    }    ofEndShape(); }

运行结果是一样的:


vector绘制彩色方块

下面将介绍 ofColor型vector 的使用方法,先看示例

代码示例(11-7):

—- ofApp.h 内

#include "ofMain.h" class ofApp : public ofBaseApp{    public:        void setup();        void update();        void draw();        ...        vector<float> rectsW,rectsH;              vector<ofColor> colors;      int num; };

—- ofApp.cpp 内

void ofApp::setup(){    ofSetWindowShape(700,700);    num = 150;    for(int i = 0;i < num;i++){        rectsW.push_back(ofRandom(200));        rectsH.push_back(ofRandom(600));        colors.push_back(ofColor(ofRandom(255),ofRandom(255),ofRandom(255)));    } } void ofApp::update(){ } void ofApp::draw(){    ofBackground(0);    ofSetRectMode(OF_RECTMODE_CENTER);    ofEnableBlendMode(OF_BLENDMODE_ADD);    for(int i = 0;i < num;i++){        float alpha = ofGetMouseX()/(float)ofGetWidth() * 255;        ofSetColor(colors[i],alpha);        float x = ofGetWidth()/(float)num * i;        ofDrawRectangle(x,ofGetHeight()/2,rectsW[i],rectsH[i]);    } }

运行效果:


代码说明:

  • ofColor 在声明时和其他类型并没有差别。但赋值时要注意单位,不能省略掉 ofColor()。它必须是作为 ofColor 型的数据,才能被 push_back 到vector中。

  • 写程序时要考虑哪些数据需要用vector储存。若有固定规律可以不用。在这个例子中,矩形的纵坐标都是固定的,而横坐标可以用 i 推算。因此就不需要用额外的vector来储存。

  • 使用加色模式,可以让图形叠加后,更容易产生辉光效果。这里创建了局部变量 alpha,通过移动鼠标就能改变色彩的透明度

若去掉加色模式,就能看到矩形的原始色彩效果


vector在圆周运动的应用(1)

前面有介绍过如何用三角函数来制作圆周运动。单个物体作圆周运动,就需要创建三个变量来分别保存角度,横坐标与纵坐标。若想创建多个物体,就可以考虑用vector。这样每个属性都能独立控制。

代码示例(11-8):

—- ofApp.h 内

#include "ofMain.h" class ofApp : public ofBaseApp{    public:        void setup();        void update();        void draw();        ...        vector<float> circlesX;        vector<float> circlesY;        vector<float> speed;          vector<float> angle;          int num; };

—- ofApp.cpp 内

void ofApp::setup(){    ofSetWindowShape(700,700);    num = 130;    for(int i = 0;i < num;i++){        speed.push_back(ofRandom(0.002,0.03));        angle.push_back(0);        circlesX.push_back(0);        circlesY.push_back(0);    } } void ofApp::update(){    for(int i = 0;i < num;i++){        float r = 30 + i * 2;        angle[i] += speed[i];        circlesX[i] = r * cos(angle[i]);        circlesY[i] = r * sin(angle[i]);    } } void ofApp::draw(){    ofBackground(0);    ofTranslate(ofGetWidth()/2,ofGetHeight()/2);    for(int i = 0;i < num;i++){        ofDrawCircle(circlesX[i],circlesY[i],4);        if(i > 0){            ofSetColor(255);            ofDrawLine(circlesX[i],circlesY[i],circlesX[i - 1],circlesY[i - 1]);        }    } }

运行效果:


代码说明:

  • 因为希望每个圆的运动速度都有区别,所以创建了 speed 变量来独立储存速度信息

还可以用之前提到的ofBeginShape,ofEndShape 来画折线。结果都是一样的。但对于用ofBeginShape,ofEndShape 画的图形,若开启填充效果,就会有非常特别的效果。试着把上例的 draw 函数按如下方式替换。

void ofApp::draw(){    ofBackground(0);    ofTranslate(ofGetWidth()/2,ofGetHeight()/2);    ofBeginShape();    for(int i = 0;i < num;i++){        ofFill();        ofDrawCircle(circlesX[i],circlesY[i],4);        ofVertex(circlesX[i],circlesY[i]);    }    ofNoFill();    ofEndShape(); }

运行效果:


这样就能将绘制的折线变成多边形。由于程序中会自动把重叠的部分进行反色处理,因而产生了上面的效果。

vector在圆周运动的应用(2)

基于上面的代码结构,并对参数进行一些修改。试着 在 setup 函数中使用  ofSetBackgroundAuto(false) 看看 。

代码示例(11-9):

—- ofApp.h 内

#include "ofMain.h" class ofApp : public ofBaseApp{    public:        void setup();        void update();        void draw();        ...        vector<float> circlesX;          vector<float> circlesY;         vector<float> speed;         vector<float> angle;          int num; };

—- ofApp.cpp 内

void ofApp::setup(){     ofSetWindowShape(700,700);    num = 5;    for(int i = 0;i < num;i++){        speed.push_back(ofRandom(0.004,0.03));        angle.push_back(0);        circlesX.push_back(0);        circlesY.push_back(0);    }    ofSetBackgroundAuto(false);    ofBackground(0); } void ofApp::update(){    for(int i = 0;i < num;i++){        float r = 30 + i * 60;        angle[i] += speed[i];        circlesX[i] = r * cos(angle[i]);        circlesY[i] = r * sin(angle[i]);    } } void ofApp::draw(){    ofSetColor(255,50);    ofNoFill();    ofBeginShape();    for(int i = 0;i < num;i++){        ofDrawCircle(circlesX[i],circlesY[i],4);        ofVertex(circlesX[i],circlesY[i]);    }    ofEndShape(); }

点的运动轨迹:


实际运行效果:



仅有黑白二色难免单调。结合前面提到的色彩模式,可以让图形产生富有层次变化的效果。试着替换着色部分的代码

ofEnableBlendMode(OF_BLENDMODE_ADD); ofSetColor(ofColor::fromHsb(int(ofGetElapsedTimef() * 20) % 255,255,255),4);

运行效果:


关于Vector还有更多高级用法,后面的时间就交给大家尽情探索~


Openframeworks 系列文章

[1][2][3][4][5][6][7][8][9][10]

资源索引

CreativeCoding学习资源索引

Twitter资源索引


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

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