几年前写过一套随机树木的生成算法,其中使用了分形和放样建模。那时候还不知道有speedtree这款软件,写的比较粗糙,最近看了speedtree的演示把原算法改进了一下,算是一个speedtree的简化版本。
重构主要是把原先使用递归函数实现的分形过程改为了非递归的,这样结构更清晰一些,尽量把树木生成分拆为若干独立步骤,减少每个步骤之间的耦合。使用骨骼动画模拟树木生长及随风舞动。
搜集的一些关于树木形态的数学规律:
分形:每条分枝就像一颗经过旋转的独立树。
达芬奇公式:当树木分岔时,树干的粗度等于同一高度树枝的总粗度,工程学的角度发现这样的结构最能抵抗强风。
一般树冠分为六种形态:长型、球型、椭圆型、瓶型,金字塔型和垂枝型。这跟顶端优势是否明显有关系,如果顶端优势明显,向两边生长就会抑制,形成高大的树,如松树;如果顶端优势不明显,树就会倾向于横向生长,如橡树。树冠的形态和生长地域息息相关,寒冷地区的针叶树,树冠比温带地区要小,而且呈金字塔状,这样的结构可以防止在下雪的时候侧枝积累过多的重量而折断。
分叉很多时候符合斐波纳契数列(Fibonacci Sequence),形如[1、1、2、3、5、8、13、21],F0=0,F1=1,Fn=F(n-1)+F(n-2)。越往后面,相邻两个斐波纳契数的比(1/2, 2/3, 3/5, 5/8, 8/13)越近似等于0.618黄金分割。在发育成树枝的叶腋的排列中,记任意一个点为0然后往上数,直到另一个相同位置出现叶腋,其中的周数/叶腋数(1/2, 1/3, 2/5, 3/8, 8/13)很多都是斐波那契数的相邻两个。这种排列方式可以保证在空间上错落开,充分利用阳光。分叉黄金角为137.51度。
可调整分支参数:
BranchType branchType; //分支类型
BranchMeshType meshType; //模型类型
RectF texRect ; //纹理
int branchNum ; //当前层枝条数
float length ; //枝干长度
float radiusBottom ; //枝干底部半径
float radiusTop ; //枝干顶部半径
float sizeVariation ; // 大小异性
float bend ; //枝干弯曲度
float gravityW ; //全局重力(world)
float gravityL ; //局部重力(local 平行父枝干方向)
float gravityC ; //局部向心力(center垂直指向父枝干方向) 适用于花瓣内收
int pathStepNum ; //长度段数
int shapeStepNum ; //切面段数
bool axisXAlign ; //是否将x轴放到父枝的垂直面上,树叶(花瓣)的放样才正确
float pitchAng ; //当前层枝条与父枝的夹角 pitch
float pitchAngVariation; //pitchAng差异范围
float rotAng ; //当前层枝条与父枝的旋转递增夹角 rot 一般为黄金角137.51,君子兰对称为180 十字花90
//float rotAngVariation ;
float minOffset ; //当前层枝条在父枝上分叉点高度偏移
float maxOffset ; //.......最大
float offsetRelax ; //分叉均匀度,0度时-分叉点集中到每节的起始处
//叶子参数
float leafConvex ; //叶子横向凹凸 (放样形状)
String leafMesh ;
//风力动画
float windWeight ; //风动系数
//生长动画
float matureTime ; //成熟时间[0~1],成熟后大小不变,成熟前线性长大并且被父枝的生长大小叠加缩放
//生长期纹理融合 in treedef
参数调整技巧:
垂柳: 设置全局重力参数gravityW
花 : 花瓣可以使用多层树叶代替,顶层和底层花瓣向心力不同、局部重力不同,模拟出花瓣的不同弧度。
球形树:设置分枝pitchAng=0、pitchAngVariation=180,在分枝的末端添加树叶pitchAng=90、pitchAngVariation=0。
草丛: 可以使用一个微小的假主干,或者将主干数目设置为大于1。
树叶不一定是末端,也可以再分出次级树枝、树叶或花。
算法大部分代码集中在分枝路径的计算和骨骼动画模型的生成,大体的步骤:
1、计算枝条个数(因为不想用动态数组,所以先计算个数分配空间)
2、构造枝条结构
3、计算骨骼个数
4、构造骨骼结构
5、计算枝条曲线、放样形状。这一步代码较多,路径受弯曲度影响分形叠加曲线,路径受重力、局部重力、向心力等影响。放样形状可以是闭合曲线(枝条)或弯曲线(面片,十字面片)。
6、计算骨骼矩阵
7、生成骨骼动画驱动的树
其中只有枝条计算和骨骼计算有一些穿插耦合,因为枝条曲线的计算需要用到父骨骼的矩阵,骨骼矩阵又需要根据自身枝条曲线计算,不好完全独立开。
数据结构:
enum BranchType
{
BT_Trunk , //树干
BT_Branch, //树枝
BT_Leaves, //树叶
BT_Root , //树根
BT_Flower, //花、果实
};
const char* BranchTypeToString(int enumeration);
bool StringToBranchType(const char* str,int& type);
enum BranchMeshType //模型类型
{
MT_Strip , //线放样
MT_Cross , //十字放样
MT_Cylinder , //圆放样
MT_CylinderCap , //圆放样封顶
MT_Billboard , //
MT_Sphere , //
MT_Mesh , //
};
const char* BranchMeshTypeToString(int enumeration);
bool StringToBranchMeshType(const char* str,int& type);
//随机树配置
class TreeDef
{
public:
#define MaxDepth 4
#define MaxShapeStep 16
#define MaxPathStep 8
#define MaxSubBranch 50
#define MaxSubBranchDef 3
#define MaxBranchDef 32 //128
#define MaxGrowTex 4
enum Method
{
TT_StaticAndUV, //静态树 纹理动画
TT_AnimBone , //绑定骨骼动画
};
//每一级分枝
class BranchDef
{
public:
char name[64] ;
int ID ;
int parentID ;
int depth ; //层深度
//int seed ; //单独seed
BranchType branchType; //分支类型
BranchMeshType meshType;
RectF texRect ; //纹理
int branchNum ; //当前层枝条数
//todo枝长曲线 lengthBottom lengthCen lengthTop
float length ; //枝干长度
float radiusBottom ; //枝干底部半径
float radiusTop ; //枝干顶部半径
float sizeVariation ; //
float bend ; //枝干弯曲度(1-笔直度) 弯曲度和俯仰角效果有重叠? 点在和路径垂直的横截面上偏移 起点无偏移 末端在横截面的圆环上
float gravityW ; //全局重力(world) 路径受重力的影响
float gravityL ; //局部重力(local平行父枝干方向)
float gravityC ; //局部向心力(center垂直指向父枝干方向) 适用于花瓣内收
int pathStepNum ; //长度段数
int shapeStepNum ; //切面段数
bool axisXAlign ; //将x轴放到父枝的垂直面上,树叶(花瓣)的放样才正确
//
float pitchAng ; //当前层枝条与父枝的夹角 pitch
float pitchAngVariation; //pitchAng差异范围
float rotAng ; //当前层枝条与父枝的旋转递增夹角 rot 一般为黄金角137.51,君子兰对称为180 十字花90
//float rotAngVariation ;
float minOffset ; //当前层枝条在父枝上分叉点高度偏移
float maxOffset ;
float offsetRelax ; //分叉均匀度,0度时-分叉点集中到每节的起始处
//叶子参数
float leafConvex ; //叶子横向凹凸 (放样形状)
String leafMesh ;
//风力动画
float windWeight ; //风动系数
float windWeightSq ; //修正风力权重呈平方增长,且避免段数越大弯曲累积越多
//生长动画
float matureTime ; //成熟时间[0~1],成熟后大小不变,成熟前线性长大并且被父枝的生长大小叠加缩放
//生长期纹理融合 in treedef
BranchDef* parent;
int subDefNum;
BranchDef* subDef[MaxSubBranchDef];
int numW;
RectF uiRect; //for diagram edit
};
TreeDef();
void RandGen();
void RandChange();
void Load(const char* fileName);
void Save(const char* fileName);
BranchDef* GetBranchDef(int ID);
void GenTopology();
void UpdateAllItems(ListItemUpdater& updater);
public:
String m_fileName;
String m_texture ;
//生长期纹理融合
int m_growTexNum;
String m_growTexture[MaxGrowTex];
float m_growTexTime[MaxGrowTex]; //在此生长时间点开始改变材质
Method m_method ;
int m_seed ;
BranchDef* m_trunkDef; //主干,规定只有一个trunkdef,其它未连接parentID的分枝不被构造。
int m_branchDefNum;
BranchDef m_branchDef[MaxBranchDef];
//mesh
//RendSys::MovieClip* m_movieLib;
};
//随机树模型生成及动画更新器
class TreeProducer
{
public:
class TBone;
class Branch;
TreeProducer();
~TreeProducer();
void Free();
void EditRender();
void GenRandTree(Mesh* mesh,Skeleton* skeleton,TreeDef* def,int randSeed=-1);
//private:
//生成静态树:树叶和树枝纹理drawcall分开,树叶可以单独uv动画
void GenRandTreeStaticAndUV(Mesh* mesh,TreeDef* def,int randSeed=-1);
//生成骨骼动画驱动的树:骨骼动画为了加速后期可以塌陷到morph,当skeleton为空时生成静态树
void GenRandTreeAnimBone (Mesh* mesh,Skeleton* skeleton,TreeDef* def,int randSeed=-1);
void CalTBoneMatrix (Branch* branch);
void GenRendMeshSkeleton (Mesh* mesh,Skeleton* skeleton);
void RefreshRendSkeleton (Skeleton* skeleton,int frame,int offset);
void RefreshRendMixSkeleton(MixSkeleton* mixSkeleton,int frame,int offset);
//生长 0~1
void RefreshGrow(float life);
//风的角度和风力,如果叠加生长动画,则RefreshGrow调用要在RefreshWind前面
void RefreshWind(TreeWind* wind);
class Branch
{
public :
Branch();
~Branch();
public:
int indexG; //在全局分枝中的索引
int indexL; //在父枝分枝中的索引
int indexLT; //在父枝同类型分枝中的索引
TreeDef::BranchDef* def; //
TBone* bones; //arr[path step num+1]
float lengthScale ; //枝干长度半径缩放系数
float stemOffset ; //在父枝干上的分叉位置[0~parentDef->pathStepNum]
float stemRot ; //分支方向
float stemAngle ; //pitch
//枝成熟期原始造型
vec3 shapePos [MaxShapeStep+1] ; //放样形状坐标
vec3 shapeNormal[MaxShapeStep+1] ; //放样形状法线
vec3 pathPos [MaxPathStep+1 ] ; //放样路径坐标
int subBranchNum; //multi type
Branch** subBranchs;
Branch* parent;
TexturePtr m_texture;
};
class TBone
{
public :
TBone();
~TBone();
public:
int index ; //indexW
Branch* branch ; //
TBone* parent ;
int subBoneNum ;
TBone** subBones ;
mat4 matL ; //原始局部矩阵
mat4 matLGrow ; //经过生长动画模拟后的局部矩阵,不叠加风力,若未执行生长模拟则等于matL
mat4 matLWind ; //经过生长动画模拟+风力后的局部矩阵,使用matLGrow作为原始局部矩阵叠加风力,若未执行生长模拟则等于matLGrow
mat4 matW ; //世界矩阵
vec3 axisYW ;
风动
//vec3 windRotSpeed; //角速度 此处使用简化的快速模拟,真实的物理模拟对每根骨骼创建刚体速度太慢
//生长
float matureTime ; //成熟时间[0~1],大小随时间呈ln曲线变化,成熟时达到0.8f,成熟后大小微变,成熟前线性长大并且被父枝的生长大小叠加缩放
float scaleW ;
};
//private:
public :
int m_seed;
Branch* m_branchs ; //主干chunk可以有多个
int m_branchNum;
TBone* m_bones;
int m_boneNum;
};
生长动画算法:
很短也许不是最好的,但是比较快速,主要是逐级改变骨骼缩放。
void TreeProducer::RefreshGrow(float life)
{
Clamp(life,0.0f,1.0f);
mat4 matScale;
float scaleL;
float scaleL2; //y轴非线性缩放
float matureTime;
TBone* bone = m_bones;
for(int i = 0; i < m_boneNum; ++i,++bone)
{
if(bone->parent)
{
matureTime = bone->branch->def->matureTime;
//scaleW-time 为过三点的折线
if(matureTime==1)
bone->scaleW = 0.8f * life / matureTime;
else if(life >= matureTime || matureTime==0)
bone->scaleW = 0.8f + 0.2f*(life-matureTime)/(1-matureTime);
else
bone->scaleW = 0.8f * life / matureTime;
if (bone->parent->scaleW<_EPSILON)
{
scaleL = 0;
scaleL2 = 0;
}
else
{
scaleL = bone->scaleW/bone->parent->scaleW;
scaleL2 = bone->scaleW/sqrt(bone->parent->scaleW);
}
//优化展开
matScale.FromScale(scaleL,scaleL2,scaleL);
bone->matLGrow = matScale * bone->matL;
bone->matW = bone->parent->matW * bone->matLGrow;
}
else
{
bone->scaleW = 1;
}
}
}
随风舞动动画模拟:
此处使用简化的快速模拟。真实的物理模拟需要对每根骨骼创建刚体,模拟风的作用力,速度太慢。如果同时开启生长模拟和风力模拟,则风力模拟需要放在生长模拟之后计算。
//简化的快速风动模拟
class TreeWind
{
public:
TreeWind();
void Random();
void Update();
const vec3& GetWindDir();
float GetWindForce();
//参数
float strength ; //强度
float turbulence; //湍流强度
float frequency ; //湍流频率 周期=TWOPI/frequency
private:
//输出
vec3 m_tree3DWindDir;
float m_tree3DWindForce;
vec3 m_tree3DWindAngSpeed;
};
void TreeWind::Update()
{
float acumTime = G_Timer->GetAccumTime();
float time = G_Timer->GetStepTimeLimited();
m_tree3DWindForce = (
sin(acumTime*frequency)*0.7f
+sin(acumTime*frequency*2.337f+0.123f)*0.2f
//+sin(acumTime*frequency*5.537f+0.823f)*0.1f
)*strength; //周期=TWOPI/frequency
vec3 angAcc;
RandDir(angAcc);
m_tree3DWindAngSpeed += angAcc*0.2f*time;
m_tree3DWindAngSpeed.Normalize();
mat4 mat;
mat.FromAxisAngle(m_tree3DWindAngSpeed,RandRange(60.0f,100.0f)*turbulence*DEG2RAD*time);
m_tree3DWindDir = mat*m_tree3DWindDir;
}
void TreeProducer::RefreshWind(TreeWind* wind)
{
const vec3& windDir = wind->GetWindDir();
float windForce = wind->GetWindForce();
float windWeight;
vec3 axisY(0,1,0);
mat4 rot;
vec3 axis;
TBone* bone = m_bones;
TreeDef::BranchDef* branchDef;
for(int i = 0; i < m_boneNum; ++i,++bone)
{
branchDef = bone->branch->def;
if(bone->parent)
{
windWeight = branchDef->windWeightSq;
if (windWeight>_EPSILON)
{
CrossVec3(axis,bone->axisYW,windDir);
float angRad = (windForce+0.4f) * windWeight * LengthVec3(axis); //LengthVec3(axis)减少抖动
rot.FromAxisAngle(axis,angRad);
bone->matLWind = bone->matLGrow * rot;
}
else
{
bone->matLWind = bone->matLGrow;
}
bone->matW = bone->parent->matW * bone->matLWind;
bone->axisYW = axisY;
bone->matW.AffectNormal(bone->axisYW);
}
}
}