其他
[ 源码分享 ] Test251- 有关树的生成
分享一个先前练习的源码,使用的框架为 Openframeworks。下图是用该程序生成 3d 模型文件,再放到 KeyShot 中渲染的效果
程序中的生成过程
(打开程序后,枝干会自动开始生成,如希望产生其他形态,可按快捷键 ‘ C ’ 重置)
(待曲线生成完毕,按快捷键‘ R ’可将曲线转换为 3d 模型文件,并储存到 data 文件夹中)
具体代码
ofApp.h 部分
#include "ofMain.h"
#include "WenzyGrowingCurve.h"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
......
ofPoint crossProduct(ofPoint a,ofPoint b);
void generate();
void drawMeshLine(ofPolyline myLine,int divideNum,int circleNum,float Rmin,float Rmax);
ofMesh mesh;
bool addingMesh;
int startStepNum; // 起始步长
int bloomNum; // 每个节点生长的枝干数量
int maxRecursionNum;
ofEasyCam cam;
vector<WenzyGrowingCurve> curve; // 储存所有曲线
};
ofApp.cpp 部分
void ofApp::setup(){
cam.setDistance(2000);
startStepNum = 200;
maxRecursionNum = 3; // 递归次数,决定枝干复杂度
bloomNum = 5;
generate();
}
void ofApp::generate(){
// 样式一:
for(int i = 0;i < 1;i++){
WenzyGrowingCurve temp;
temp.setup(ofPoint(0,-300,0),ofPoint(0,1,0),0,startStepNum,0.01);
curve.push_back(temp);
}
// 样式二
// for(int i = 0;i < 6;i++){
// WenzyGrowingCurve temp;
// temp.setup(ofPoint(0,0,0),ofPoint(0,0,0),0,startStepNum,0.02);
// curve.push_back(temp);
// }
mesh.setMode(OF_PRIMITIVE_TRIANGLES);
}
//--------------------------------------------------------------
void ofApp::update(){
for(int i = 0;i < curve.size();i++){
curve[i].update();
// 长出新枝干
if(curve[i].generation < maxRecursionNum && curve[i].moving == false && !curve[i].hasBloomed){
curve[i].hasBloomed = true;
for(int j = 0;j < bloomNum;j++){
WenzyGrowingCurve temp;
int nextStepNum;
if(curve[i].generation < maxRecursionNum - 1){
float ratio = 0.72; // 决定每个枝干缩小的比例
nextStepNum = pow(ratio,curve[i].generation) * startStepNum * ofRandom(0.8,1.1);
}else{
// 末端枝干更短
nextStepNum = startStepNum * 0.2 * ofRandom(0.5,1.5);
}
temp.setup(curve[i].endPos,curve[i].curveLineDir,curve[i].generation + 1,nextStepNum,0.06);
curve.push_back(temp);
}
}
}
ofSetWindowTitle(ofToString(ofGetFrameRate()));
}
// 求叉积
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();
ofRotateY(ofGetFrameNum() * 0.5);
ofSetLineWidth(2.5);
ofSetColor(255,200);
for(int i = 0;i < curve.size();i++){
curve[i].draw();
}
if(addingMesh){
for(int i = 0;i < curve.size();i++){
float minR = 50 * pow(0.5,curve[i].generation + 1);
float maxR = 50 * pow(0.5,curve[i].generation);
if(curve[i].generation == 0){
drawMeshLine(curve[i].curveLine,20,20,maxR,minR);
}else if(curve[i].generation == maxRecursionNum){
// 末端枝干用更少的面数可节省资源
drawMeshLine(curve[i].curveLine,6,10,maxR,minR);
}else{
drawMeshLine(curve[i].curveLine,20,20,maxR,minR);
}
}
addingMesh = false;
// 保存文件
mesh.save("1.ply");
}
if(!addingMesh){
ofSetLineWidth(1);
ofSetColor(255,100);
mesh.drawWireframe();
}
cam.end();
}
void ofApp::drawMeshLine(ofPolyline myLine,int divideNum,int circleNum,float startR,float 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);
//ofDrawSphere(C,3);
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;
}
// 重新生成
if(key == 'c'){
curve.clear();
mesh.clear();
generate();
}
}
自定义类 WenzyGrowingCurve.h
class WenzyGrowingCurve{
public:
ofPolyline curveLine; // 表示每条基础曲线
ofVec3f curveLineDir; // 线条方向
ofVec3f curvePos; // 用于加入的点,方便累加
ofVec3f noiseSeed;
ofVec3f startPos,endPos; // 每条曲线的起始结束点
// 动画相关
bool moving; // 是否开始运动(添加点)
int curStep; // 当前已运行步数
int stepNum; // 运行总步数
float stepLength; // 每次步进的长度
int generation; // 代数,方便外部初始化
float dirRange; // 扩散角度,数值越大扭曲程度越高
bool hasBloomed; // 是否已经生长过
void setup(ofVec3f startPos_,ofVec3f startDir_,int generation_,int stepNum_,float dirRange_){
noiseSeed = ofVec3f(ofRandom(100),ofRandom(100),ofRandom(100));
stepLength = 2;
moving = true;
curStep = 0;
stepNum = stepNum_;
generation = generation_;
curveLineDir = startDir_; // 继承上条曲线的方向
startPos = startPos_;
curvePos = startPos;
hasBloomed = false;
dirRange = dirRange_;
curveLine.curveTo(startPos);
curveLine.curveTo(startPos);
}
void update(){
if(moving && curStep < stepNum){
float noiseRange = 0.01; // 控制 noise 的输入
curveLineDir += ofVec3f(ofSignedNoise(noiseSeed.x + ofGetFrameNum() * noiseRange),
ofSignedNoise(noiseSeed.y + ofGetFrameNum() * noiseRange),
ofSignedNoise(noiseSeed.z + ofGetFrameNum() * noiseRange))
* dirRange;
curveLineDir.normalize();
curvePos += curveLineDir * stepLength;
if(curStep % 3 == 0){
curveLine.curveTo(curvePos);
}
curStep++;
if(curStep == stepNum){
endPos = curvePos;
curveLine.curveTo(curvePos);
curveLine.curveTo(curvePos);
}
&nb
44 26413 44 11866 0 0 3790 0 0:00:06 0:00:03 0:00:03 3791
44 26413 44 11866 0 0 2872 0 0:00:09 0:00:04 0:00:05 2873
44 26413 44 11866 0 0 2312 0 0:00:11 0:00:05 0:00:06 2312
44 26413 44 11866 0 0 1934 0 0:00:13 0:00:06 0:00:07 2405
44 26413 44 11866 0 0 1663 0 0:00:15 0:00:07 0:00:08 2301sp; }else{
moving = false;
}
}
void draw(){
curveLine.draw();
}
};
简要说明:
枝干的生成主要通过 WenzyGrowingCurve.h 类实现。代码不足 100 行,尽管结构简单,但生成的效果还是比较贴近植物的自然形态。通过该类生成的对象对应每根独立的枝干。调整相关参数,可以产生截然不同的形态。
从曲线转化为立体,主要通过自定义函数 drawMeshLine 实现。之前某篇文章有介绍过具体原理。
程序导出的 3d 文件格式为 ply,可以使用 Meshlab 等软件转换为常用的 obj 格式放到 keyshot 中渲染
End
以上代码仅仅是一个基础版本,还有很多内容可以继续深挖。例如使用 shader 渲染,添加树叶,让它产生更丰富自然的光影效果。
(使用 Shader 实时渲染)
完整工程文件下载:
(链接: https://pan.baidu.com/s/1sldpISP 密码: xs2v)