查看原文
其他

Openframeworks 三维模型导出技巧

2017-02-03 Wenzy InsLab

这篇将分享一个在 Openframeworks 中导出三维模型的方法。

无论是 Processing 还是 Openframeworks。默认的灯光系统效果往往比较粗糙。灯光不属于全局光照,物体之间不会相互影响,没有反射折射以及投影。

要做出更有质感的3D效果,除了进一步学习 shader 以外。我们还可以从程序中导出模型,再借助其他三维工具进行渲染。下面的实例可以在 Openframeworks 中导出格式为 ply 的三维文件。

导出实例01

(源代码)

ofApp.h 内—

class ofApp : public ofBaseApp{    public:        void setup();        void update();        void draw();        ...        ofEasyCam cam;        ofLight light;        ofMesh mesh;   };

ofApp.cpp 内—

void ofApp::setup(){    light.enable();    light.setPosition(50, 400, 50);    mesh.clear();    mesh.setMode(OF_PRIMITIVE_TRIANGLES);    for(int i = 0;i < 100;i++){        ofCylinderPrimitive cyl;        // 设置 Mesh 模式        cyl.setMode(OF_PRIMITIVE_TRIANGLES);        // 设置模型精度        cyl.setResolution(6,1);        cyl.set(ofRandom(5,50),ofRandom(10,200));        ofPoint move;        float range = 200;        move.set(ofRandom(-1,1) * range,0,ofRandom(-1,1) * range);        for(int j = 0;j < cyl.getMesh().getVertices().size();j++){            cyl.getMesh().getVertices()[j] += move;        }        mesh.append(cyl.getMesh());    } } void ofApp::draw(){    ofEnableDepthTest();    ofBackground(0);    cam.begin();    mesh.draw();    cam.end(); } void ofApp::keyPressed(int key){    if(key == 's'){        ofSaveScreen(ofToString(ofGetFrameNum()) + ".png");        mesh.save("mesh.ply");    }    if(key == 'c'){        setup();    } }

程序运行效果:

按‘c’键随机生成模型。按‘s’键保存。默认导出的三维格式为 ply。若希望在其他软件中渲染,可先使用 meshlab 等软件将 ply 格式转成 obj 格式。

(在 keyshot 中的渲染效果)

代码浅析:

  • 若想将二维图形导出矢量 PDF 文件,在 OF 中只要使用命令ofBeginSaveScreenAsPDF 和 ofEndSaveScreenAsPDF 对绘图函数进行包裹,就能直接导出。但对于三维图形,不存在类似的命令。我们需要借助 OF 本身提供的一个类 - ofMesh (官网介绍:)

  • 使用 ofMesh 导出模型步骤很简单,只要使用 .save() 即可。稍微麻烦的地方在于如何用 ofMesh 来描述一个多面体。一种通用的办法是使用 .addVertex 从底层构建。开始时需要先考虑如何用三角面拼成立体造型,获得各个顶点的空间位置,再逐个添加。另一种相对便捷,只要借助 ofCylinderPrimitive 等函数做转换中介。本实例就用到 ofCylinderPrimitive 来表示圆柱体,而不是 ofDrawCylinder。原因是 ofCylinderPrimitive 内部提供了一个函数 getMesh() ,可以直接获取到圆柱体的 mesh 面。最后再用命令 .append ,就能把当前的顶点数据都添加到同一个 mesh 对象上。

  • ofMesh 是用顶点的形式来描述多边形。之前我们使用 ofRotate,ofTranslate 等函数虽然可以控制三维形体的位置。但却无法影响 mesh 的实际坐标点,所以需要手动去对 mesh 的每个顶点进行换算。例子中创建的局部变量 move,作用就是对圆柱体进行平移。

  • 与 ofCylinderPrimitive 类似的,还有 ofConePrimitive(圆锥), ofBoxPrimitive(方体),ofSpherePrimitive(圆形)

  • 导出的 ply 格式若在某些三维软件中无法打开。可以考虑用软件 meshLab 转换成 obj 格式。

关于导出与渲染

对于不常使用三维软件的朋友,若想简单快捷出效果,推荐使用 keyshot。它对新手十分友好,掌握基本操作只需五分钟。把模型导入到场景后,左侧可以选择材质以及对应的环境光照,只要按住拖动到模型上即可。

默认光照

使用其他环境贴图

进阶-导出实例02

第一个例子中提到的导出方法,虽然比较便捷。但产生的形态十分有限,只能是基本的方体,圆柱体等等。若想更灵活地生成造型,还是需要使用 .addVertex。

以下例子实现了一个函数,可以将空间曲线转换成带粗细变化的柱状多面体。

源代码:

ofApp.h 内—

class ofApp : public ofBaseApp{    public:        void setup();        void update();        void draw();        ...        ofPoint crossProduct(ofPoint a,ofPoint b);        void drawMeshLine(ofPolyline myLine,int divideNum,int circleNum,float Rmin,float Rmax);
       ofEasyCam cam;        ofPolyline curveLine;        int curveNum;        ofMesh mesh;        bool addingMesh; };

ofApp.cpp 内—

void ofApp::setup(){    // 生成基础空间曲线    curveNum = 4;    for(int i = 0;i < curveNum;i++){        float range = 150;        ofPoint temp;        temp = ofPoint(ofRandom(-1,1) * range,200 * i,ofRandom(-1,1) * range);        if(i == 0 || i == curveNum - 1){            curveLine.curveTo(temp);            curveLine.curveTo(temp);        }else{            curveLine.curveTo(temp);        }    }    mesh.setMode(OF_PRIMITIVE_TRIANGLES); } void ofApp::update(){ } // 求叉积 ofPoint ofApp::crossProduct(ofPoint a, ofPoint b){    ofPoint c;    c.x = a.y * b.z - a.z * b.y;    c.y = a.z * b.x - a.x * b.z;    c.z = a.x * b.y - a.y * b.x;    return c; } void ofApp::draw(){    ofBackground(0);    cam.begin();    // 绘制基础空间曲线    ofSetColor(0,0,255);    ofSetLineWidth(8);    curveLine.draw();    if(addingMesh){        drawMeshLine(curveLine, 20, 10, 100, 0);        addingMesh = false;        mesh.save("1.ply");    }else{        ofSetColor(255,200);        ofSetLineWidth(1);        mesh.drawWireframe();    }    cam.end(); } // 转换函数,将 ofPolyline 转成多面体 void ofApp::drawMeshLine(ofPolyline myLine,int divideNum,int circleNum,float startR,float endR){    // myLine:曲线,divideNum:曲线细分数量,circleNum:截面细分数量,startR:起始半径,endR:末端半径    vector<vector<ofPoint>> myPos;    // 从 -1 开始是为了能绘制底面    for(int i = -1;i < divideNum;i++){        float ratio1,ratio2;        ofPoint A,B; // 根据 A,B 求圆环        if(i != -1){            ratio1 = i/(float)divideNum;            A = myLine.getPointAtPercent(ratio1);            ratio2 = (i + 1)/(float)divideNum;            B = myLine.getPointAtPercent(ratio2);        }else{            ratio1 = 0;            A = myLine.getPointAtPercent(ratio1);            ratio2 = 0.00001;            B = myLine.getPointAtPercent(ratio2);        }        // dir 为基向量        ofPoint ab;        ab = B - A;        ab.normalize();        // ab 与 X 轴的叉积        ofPoint M;        M = crossProduct(ab,ofPoint(1,0,0));        // ab 与 M 的叉积,N        ofPoint N;        N = crossProduct(ab,M);        // 求基向量        ofPoint n,m;        n = N.normalize();        m = M.normalize();        // 设 theta        float newRatio = (i + 1)/(float)(divideNum + 1);        float R = ofLerp(startR,endR,newRatio);        vector<ofPoint> tempPos;        for(int i = 0;i < circleNum;i++){            float theta = 2 * PI /circleNum * i;            float ratio = 1;            ofPoint C;            C = B + ratio * R * (m * cos(theta) + n * sin(theta));            ofSetColor(255,0,0);            tempPos.push_back(C);        }        myPos.push_back(tempPos);    }    ofSetColor(255,150);    for(int i = 0;i < myPos.size()-1;i++){        int indexA = i;        int indexB = i + 1;        for(int j = 0;j < circleNum;j++){            if(addingMesh){                mesh.addVertex(myPos[indexA][j]);                mesh.addVertex(myPos[indexA][(j + 1) % circleNum]);                mesh.addVertex(myPos[indexB][j]);                mesh.addVertex(myPos[indexA][(j + 1) % circleNum]);                mesh.addVertex(myPos[indexB][(j + 1) % circleNum]);                mesh.addVertex(myPos[indexB][j]);                if(i == 0){                    ofPoint center;                    center.set(0,0,0);                    for(int k = 0;k < circleNum;k++){                        center += myPos[indexA][k];                    }                    center /= circleNum;                    for(int k = 0;k < circleNum;k++){                        mesh.addVertex(center);                        mesh.addVertex(myPos[indexA][(k+1) % circleNum]);                        mesh.addVertex(myPos[indexA][k]);                    }                }                if(i == myPos.size() - 2){                    ofPoint center;                    center.set(0,0,0);                    for(int k = 0;k < circleNum;k++){                        center += myPos[indexB][k];                    }                    center /= circleNum;                    for(int k = 0;k < circleNum;k++){                        mesh.addVertex(myPos[indexB][k]);                        mesh.addVertex(myPos[indexB][(k+1) % circleNum]);                        mesh.addVertex(center);                    }                }            }        }    } } void ofApp::keyPressed(int key){    if(key == 'r'){        addingMesh = !addingMesh;    } }

运行效果(按 R 键生成多面体,并保存模型):

导出模型在 meshLab 中的预览效果

代码浅析:

  • 使用.addVertex之前,首先要先进行一些数学运算,计算每个多面体的顶点。原理如上图

  • 获得了顶点后,要考虑如何将多面体肢解成一个个三角面,再有序地使用 .addVertex

简单应用:

现在有了 drawMeshLine 函数,就可以很方便地任意曲线转化成多面体了。它非常适合用来表现树状结构。

END

渲染时如果希望对模型的不同部分赋予不同材质,可以考虑分层导出,也就是生成多个 ply 文件。另外,导出的模型不仅可以在三维软件中渲染。经过转换和后处理,还可以拿去 3d 打印。这个功能非常强大,灵活使用可以做许多有趣的拓展~


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

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