Openframeworks 三维模型导出技巧
这篇将分享一个在 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 打印。这个功能非常强大,灵活使用可以做许多有趣的拓展~