L-系统语法规则构建三维分形树

L-系统语法规则构建三维分形树

  • 前言
  • L-系统原理介绍
  • L-系统建模实践
  • 实现效果演示

前言

继粒子系统篇结束,本次我将带来L系统构建分形树的实现。树一直都是虚拟室外景观中非常重要的一部分。目前模拟树的方法有很多,其中最为廉价的就是公告牌了,用一个2D的平面去仿造三维立体的效果,在以前显卡还不强力的时候这是个不错的选择,即使是现在也有用武之地。还有通过三维建模工具建立的三维模型,这些三维模型非常逼真,但是有个非常大的缺点,就是顶点多了很多,所以需要进一步的处理,如LOD算法来降低显卡的负担。实际上也可以和公告牌结合起来,例如远处就没有必要用模型去渲染了,直接用公告牌,如此折中也不错。还有一种方法就是通过程序来构建树的结构,在这里我将介绍通过L-系统语法规则进行植物建模。采用的图形API为OpenGL3.3+,可编程管线。

L-系统原理介绍

L-系统的定义

Lindenmayer 系统(简称为L-系统),是由生物学家Aristid Lindenmayer于1968年引进的一种字符串迭代重写机制,被认为是植物生长的数学理论。其核心概念是重写,也成为迭代重写。一般情况下,重写是通过应用一个重写规则或产生式的集合,对简单的初始目标中的部分(可以是一部分也可以是多个部分)进行连续置换来定义复杂目标的技术。在L-系统中,产生式是并行地、同时替换所给字符串中的所有字符,这种区别反映了L-系统的生物学动机。如下图所示,是利用迭代重写规则生成的雪花曲线。
这里写图片描述

开始由初始图形和生成规则,生成规则是由一条定向的折线形成的、边长为r的N次等边图形,我们称之为规则模型,这样每一步都利用规则模型替换前一个图形的每一条直线边,使得置换的生成规则与被置换的区间具有相同的端点。迭代四次后生成的图形类似雪花图形。这里说得有点抽象,下面就具体举个粒子。

L-系统的形式化语言是基于字符表示。假设初始字符串为A,生成规则(或产生式规则)为:

A>aB
B>bA

则经过一次推导,第一个生成规则符合第一个条件,则由aB代替A,其结果为aB。第二次推导,根据第二个生成规则,字符B由bA代替,其余的没有生成规则的字符不变,也就是复制过来,其结果为abA,以此类推,abA产生abaB,继续下去依次得到ababA、ababaB等等。这个迭代重写机制不难理解。

L-系统的几何解释

经过语法规则迭代重写得到的仅仅是一串字符串,我们还需要对这些字符进行解析。Prusinkiewicz提出解析L-系统的海龟解释方法。海龟解释的基本思想如下:平面上海龟的当前状态由一个三原数组(x,y,a)来定义,其中笛卡儿坐标(x,y)表示海龟的位置,角度a为海龟的前进方向,可以解释为海龟面对的方向。给出步长d和角度的增量sita,海龟根据以下符号所代表的命令做出反应。
F:海龟向前移动一个步长d,海龟的位置变为(x1,y1,a),其中,x1 = x + d*cos(a),y1 = y + d*sin(a),在点(x,y)和(x1,y1)之间画一条线段。
f:向前移动一个步长d,不画线段。
+:向左转角度sita,海龟的下一个状态为(x1,y1,a+sita),角的正方向为逆时针方向。
-:向右转角度sita,海龟的下一个状态为(x1,y1,a-sita)。
[:将海龟的当前状态压如堆栈。
]:从堆栈中弹出当前海龟的状态。

假设有字符串:F[-F[-F -F]FF -F - FF]FF -F -FF
另海龟的初始方向角为90度,旋转角增量为90度,前进步长为d(单个方格的长度),则海龟的爬行路径如图所示:
这里写图片描述

这只是个简单的例子,而且是二维平面上的。现在我们把它扩展到三维空间,海龟在空间的当前位置是由三个向量[x,y,z]表示的,他们分别表示海龟前进的方向、向左的方向和向上的方向,三个方向向量是相互垂直的单位向量。海龟的旋转可由以下方程完成:

[x1,y1,z1]=[x,y,z]R

其中R是一个3×3的矩阵,当旋转角为alpha的时候,绕向量x、y、z的旋转矩阵表示如下:
这里写图片描述

海龟在空间中旋转的方向如图所示:
这里写图片描述
+:向左旋转角度sita,使用旋转矩阵Rz(sita);
-:向右旋转角度sita,使用旋转矩阵Rz(-sita);
&:向下旋转角度sita,使用旋转矩阵Ry(sita);
^:向上旋转角度sita,使用旋转矩阵Ry(-sita);
\:向左滚动角度sita,使用旋转矩阵Rx(sita);
/:向右滚动角度sita,使用旋转矩阵Rx(-sita);

其中Rx(sita)、Ry(sita)、Rz(sita)分别表示绕x、y、z轴旋转sita角度。通过以上的方式控制海龟的旋转方向,就可以利用L-系统绘制三维空间上的图形。

好了,经过上面的长篇大论,相信都已经对L-系统有了一个清晰的认识,接下来我们就进行实践。

L-系统建模实践

随机L-系统

为了构建更加真实的植物,现在引入随机L-系统。相比确定性L-系统,随机L-系统加入了一个随机的概率,这个概率表现在产生式上。举个简单的粒子:

w:F
F>F[+F]F[F]FF[+F]FF[F]F
这就是一个简单随机L-系统实例,注意到F的替换规则有三个,分别是 F[+F]F[-F]FF[+F]FF[-F]F,那么替换的时候选哪个?这个时候就是靠概率来决定了,令三个产生式被选中的概率都是三分之一,替换的时候根据概率选取用哪个来进行替换。这样产生的植物局部是随机的,但是整体上维持了该种植物的一般性特征。

语法解析器

现在我们来构建一个语法解析器。这个语法解析器根据初始字符串、需要迭代的层级、产生式进行字符串的平行重写迭代。两个for循环就搞定了,外层循环是迭代的层级,内聪循环则是对当前的字符串进行替换。废话不多少,核心代码如下:

void Grammar::Iteration(int level)
{//字符串迭代重写,level为迭代次数
    srand(time(NULL));
    result = start;
    for(int i = 0;i < level;i ++){
        string tmpstr = "";
        for(int j = 0;j < result.size();j ++){
            tmpstr += search(result[j]);
        }
        result = tmpstr;
    }
}

string Grammar::search(char target)
{
    int index = find(target);
    if(index == -1){
        //在产生式中没有它的替换规则,那么直接就是复制它自身
        string ret;
        ret.push_back(target);
        return ret;
    }
    //generations里面存储了产生式,用pair<char,vector<string> >的数据结构
    int num = generations[index].second.size();

    int index = rand()%num;//在多个产生式中等概率选取一个

    return generations[index].second[index];
}

int Grammar::find(char target)
{
    for(int i = 0;i < generations.size();i ++){
        if(generations[i].first == target)
            return i;
    }        
    return -1;//未找到替换产生式
}

字符串几何解释器

现在我们就一个具体实例实践:
起始字符串:

{S}

产生式:

S -> F[^$X][*%X][&%X]
X -> F[^%D][&$D][/$D][*%D]、F[&%D][*$D][/$D][^%D]
D -> F[^$X][*%FX][&%X]

迭代层级:

8层

经过以上的规则和层级,我们就会得到一大串一大串的字符,这些字符都存储了植物的分形信息。由于太过冗长,我就不贴出来了。得到后的字符串还仅仅是字符串,接下来我就对这串字符串进行几何解释。

struct State
{
    glm::vec3 pos;
    glm::vec3 dir;
    float length;
    float radius;
    int level;
};

void LSystemTree::generateFractal()
{
    //trunks存储树干,包含了树干的起点和终点
    trunks.clear();
    //leafs存树树叶的位置
    leafs.clear();
    //curState记录当前的状态,包括位置和方向,还有长度和半径
    //长度和半径用于树干的构造
    curState.pos = glm::vec3(0,0,0);
    curState.dir = glm::vec3(0,1,0);
    curState.length = length;
    curState.level = 1;
    curState.radius = radius;

    //状态栈
    std::stack<State>stacks;

    for(int i = 0;i < grammar.getResult().size();i ++){
        //从字符串中选取一个字符串进行操作
        char ch = grammar.getResult()[i];
        Trunk tmp;
        switch(ch){
            //遇到字符F,则前进一段距离,获得一段树干
            case 'F':{
                tmp.start = curState.pos;
                curState.pos += curState.dir*(float)curState.length;
                tmp.end = curState.pos;
                tmp.radius = curState.radius;
                tmp.level = curState.level;
                trunks.push_back(tmp);
                break;
            }
            //遇到$字符,则绕Y轴旋转dy角度,dy = 30.0f
            case '$':{
                curState.dir = Geometry::RotateY(curState.dir,dy);
                break;
            }
            //遇到%字符,绕Y轴旋转-dy角度,dy = 30.0f
            case '%':{
                curState.dir = Geometry::RotateY(curState.dir,-dy);
                break;
            }
            //遇到^字符,绕X轴旋转dx角度,dx = 35.0f
            case '^':{
                curState.dir = Geometry::RotateX(curState.dir,dx);
                break;
            }
            //遇到&字符,绕X轴旋转-dx角度,dx = 35.0f
            case '&':{
                curState.dir = Geometry::RotateX(curState.dir,-dx);
                break;
            }
            //遇到*字符,绕Z轴旋转dz角度,dz = 35.0f
            case '*':{
                curState.dir = Geometry::RotateZ(curState.dir,dz);
                break;
            }
            //遇到/字符,绕Z轴旋转dz角度,dz = 35.0f
            case '/':{
                curState.dir = Geometry::RotateZ(curState.dir,-dz);
                break;
            }
            //遇到[字符,将当前状态压入栈中保存,然后将当前状体中的长度和半径以一个比例
            //消减,其实是进入了子枝干,长度和半径需要消减
            //这里lengthFactor = 0.75,radiusFactor = 0.72。
            case '[':{
                stacks.push(curState);
                curState.length *= lengthFactor;
                curState.radius *= radiusFactor;
                curState.level += 1;
                break;
            }
            //遇到]字符,将栈顶状态释放
            case ']':{
                //若当前层级是最高层,说明当前已经走到了叶子,将其记录到leafs中
                if(curState.level == grammar.getLevel()){
                    Trunk tm = trunks[trunks.size()-1];
                    Leaf rs;
                    rs.dir = tm.end - tm.start;
                    rs.pos = tm.end;
                    leafs.push_back(rs);
                }
                curState = stacks.top();
                stacks.pop();
                break;
            }
            default:
                break;
        }
    }
}

经过以上的几何解释过程,我们就得到了两个信息,trunks和leafs,这两个vector分别存储了树干和树叶。每个树干包含起点和终点,以及长度和半径信息。树叶中主要是位置以及方向向量。有了这些信息,我们下一步就是进行构造植物模型。

分形树的构建

在上一步中我们已经得到了树干和树叶的信息,那该怎么通过OpenGL画出来呢?最简单的就是树干直接用线,设定一定的线宽。但是这种方法太过粗糙,线宽的设定也有上限,最多只能为10,在调试阶段或许还有用。比较好的方法就是通过圆柱来表示树干,这个圆柱不需要要太过精细。但是现在我们遇到两个问题。第一,现代OpenGL太底层了,没有提供画圆柱API,这需要我们自己去弄,不难;第二,如何在空间中的任意两点之间画一个圆柱,这个可以先在原点画一个圆柱,然后求出旋转和平移矩阵,将其平移到目标位置。

根据圆的方程可以构建圆柱如下,我们在原点处向着z轴的正方向构建,所以圆柱起点为(0,0,0),而终点为(0,0,len),len为圆柱的长度。同时圆柱和不需要太过精细,5片矩形实际上够了。

void CylinderMesh(float len,float radius,vector<glm::vec3>&vertice)
 {
        unsigned int slice = 6;
        float delta = 360.0f/(float)(slice-1.0);
        for(unsigned int x = 0;x < slice-1;x ++){
            float angle = delta*x;
            float rc1 = radius*cos(glm::radians(angle));
            float rs1 = radius*sin(glm::radians(angle));
            float rc2 = radius*cos(glm::radians(angle+delta));
            float rs2 = radius*sin(glm::radians(angle+delta));

            glm::vec3 point1 = glm::vec3(rc1,rs1,len);
            glm::vec3 point2 = glm::vec3(rc1,rs1,0.0);
            glm::vec3 point3 = glm::vec3(rc2,rs2,len);
            glm::vec3 point4 = glm::vec3(rc2,rs2,0.0);

            vertice.push_back(point1);
            vertice.push_back(point2);
            vertice.push_back(point3);
            vertice.push_back(point3);
            vertice.push_back(point2);
            vertice.push_back(point4);
       }
}

现在我们需要求出旋转平移矩阵,将原点处的圆柱平移到目标位置。废话少说,

glm::mat4 GetTranMat(glm::vec3 start,glm::vec3 end,glm::mat4 &nmats)
{
        glm::mat4 trans(1.0f);
        //求出平移矩阵,这个简单,就是目标位置的起始点
        trans = glm::translate(trans,start);
        glm::vec3 z = glm::vec3(0,0,-1);
        glm::vec3 p = start - end;
        //叉乘获取一个垂直于z轴和目标向量的向量,实际上就是旋转轴!
        glm::vec3 t = glm::cross(z,p);
        float Pi = 3.1415926;
        //点乘,然后单位化,然后通过acos获取他们的夹角!
        float angle = acos(glm::dot(z,p)/glm::length(p));
        glm::mat4 rotas(1.0f);
        //根据旋转轴旋转angle角度
        rotas = glm::rotate(rotas,angle,t);
        nmats = rotas;
        //顺序是先旋转后平移
        return trans*rotas;
}

通过以上的计算就求出了旋转平移矩阵,那么多的树干,只需要求一个圆柱的网格顶点,然后计算出相应的缩放矩阵和旋转平移矩阵就能获取目标网格顶点了!

实现效果演示

加上纹理映射和光照阴影,效果如下:
树
其中的粒子效果是前介绍过的了。比较大颗的树是迭代了7层,小颗的则是迭代了4层。

参考资料:《基于L-系统的虚拟植物形态建模方法研究》李纪永
参考博客:http://blog.csdn.net/Mahabharata_/article/details/66967837?locationNum=10&fps=1

  • 5
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值