基于BSP、L-System的树木模拟
——《基于分形、DEM、DOM和LOD自适应渲染
策略的三维虚拟现实系统.net》项目研发报告
华南理工大学微软俱乐部虚拟现实开发团队 梁成 刘青 张嘉华 罗文杰
摘要:在虚拟现实系统中,要实现各种自然场景的生成,其中树木、花草等自然景物的生成和模拟,是整套虚拟现实系统的重要组成部分,本文介绍了二叉树、L-System两种算法,对两种算进行改进、合并,用以生成树木,并给出了基于Visual Studio.net、FrameWork1.1和GDI+的关键代码。
关键词:二叉树、L-System、递归、字符串替换、GDI+
1. 引言
在虚拟现实系统中,用户可选择“树木编辑”让计算机在生成的场景中加入树木。因此,系统要求用较为简单的方法生成树木,并且尽量节省时间和空间。我们研究了最基本的二叉树算法及比较流行的L-System算法,并对两种算法进行了改进和合并,生成了几种较为逼真的树木。
2. 二叉树随机植物生成
二叉树(BSP)是迭代和递归中最基本的算法之一。简单描述如下:
void tree(iter)
{
画树干;
左转;
tree(iter-1); //画左子树
右转;
tree(iter-1); //画右子树
}
n=0 n=1 n=2 n=3 n=4
其中画子树的过程即递归的过程。iter表示递归的深度(从n递减到0)。
将二叉树进行一些修改,也能生成简单的二维树。
首先,生成的枝干更加复杂(如图)。其中侧枝较多的分枝出现在树干的左边还是右边是随机决定的。
其次,分枝的偏转角得一定的随机幅度;当iter较大,即处于较为底层的状态时,分枝的偏转角的随机幅度也应较小,使树木看起来长势集中些。
再次,应考虑树木生长时的顶端优势。当分枝与最初主干之间的夹角较小时,可以在这个分枝上画两个子树,使分枝看起来更长。
最后,考虑树干的粗细及叶子。随着iter的递减,树干逐渐变细;当iter小于1时,画叶子。
图1
图2
在末端画上叶子,还可以有其他效果。
3. L-System随机植物生成
L-System是指美国生物学家Lindenmayer提出的植物生长的数学模型。它描述了树枝的生长过程:从一条树枝(种子)开始,发出新的芽枝,而发过芽枝的枝干又都发新芽枝……
L-System的核心思想是“字符串的替换”。它通过产生一系列字符串来描述分形树木图形特征。我们以一种灌木的L-System为例:
该类灌木的L-System的原表达式为
L=〈G,W,P〉
其中G是字符集,这里G={F,+,-,[,]};W是起始符号元,用以确定字符串的初始状态,且W∈G, 此处W=“F”;P为该类植物的生成规则集,即替换法则,这里
P={F→FF+[+F-F-F]-[-F+F+F],+→+,-→-,[→[;]→]}
以”F”为种子,经过多次迭代,可以生成描述一颗树的字符串:
深度 L-System产生的字符串
1 F
2 FF+[+F-F-F]-[-F+F+F]
3 FF+[+F-F-F]-[-F+F+F]FF+[+F-F-F]-[-F+F+F]+[+FF+[+F-F-F]-[-F+F+
F]-FF+[+F-F-F]-[-F+F+F]-FF+[+F-F-F]-[-F+F+F]]-[-FF+[+F-F-F]-[-F
+F+F]+FF+[+F-F-F]-[-F+F+F]+FF+[+F-F-F]-[-F+F+F]]
… …
下面为每个字符定义命令:
F——当前方向向前走一步;
[——将当前状态压栈保存;
]——将栈中状态弹出,即恢复原来状态;
+——由当前方向顺时针转δ角;
-——由当前方向逆时针转δ角.
有了字符串,我们可以用简单的递归把树画出来,算法简述如下:
void tree(iter)
{
read a character to ch;
while(ch!=')'} and (ch!=NULL) do
{
switch (ch)
case 'F':画线;
case '+':顺时针转;
case '-':逆时针转;
case '[':turtle (iter-1);
}
}
其中的iter表示深度,在该类灌木中与迭代次数相同。
n=0 n=1 n=2 n=3
可是这样生成的树效果并不理想,因为它的树枝和树叶没有区别(也可以说只有树枝没有树叶),生成的树也千篇一律。其它论文中一般的解决办法是:为每次迭代生成字符赋上不同的参数,但那样要耗去大量的时间和空间,不符合要求。
为此,我们对以上的算法做了一些小修改。
首先,在字符串“FF+[+F-F-F]-[-F+F+F]”中,我们把前两个“F”看成树干,用“G”表示,字符串就成了“GG+[+F-F-F]-[-F+F+F]”,“F”和“G”的替换相同。在接下来画树的过程中,把“F”画成树叶,把“G”画成树干,并且让树干随iter的递减而变细。
在样式方面,我们准备了三种替换方案,在字符串替换时随机选择方案。
这样,生成的树就生动多了。
图3
4. 两种算法的合并
仔细研究,不难发现:L-System算法实际上就是二叉树算法的推广,L-System的树也可以用二叉树式的递归生成。
把两种算法的思想合在一起,可以用以生成一些规律性较强且较复杂的树,例如一颗类似圣诞树的树。
算法简述如下:
void tree(iter)
{
画树干;
将画笔位置移到树干顶部;
for i=1 to layer
{
左转;
tree(iter-1); //画左子树
右转;
tree(iter-1); //画右子树
将画笔位置沿树干下移;
}
}
其中的iter表示深度,layer表示树的层数。
同样,我们也可做些改进:
首先,树干随着iter的递减逐渐变细;当iter小于1时画叶子。
其次,在左子树与右子树这间多添加几个子树,即tree(iter-1)。
还有,当i较小即正在画上层较小的树时,少递归一层,写为tree(iter-2),因为上层较小,可以画得简单些。
图4
5. 基于GDI+的树木绘制
我们采用Visual Studio.net作为集成环境,采用GDI+作为图形接口,FrameWork1.1把GDI+封装为Windows.Drawing命名空间。在我们的实验代码中,我们的算法是基于直角坐标系的,在具体的画线过程中,由于屏幕坐标系相对与我们生成的树木是倒立的,所以我们需要自己编写一个基于正立坐标系的DrawLine函数,在函数体内调用GDI+的内置DrawLine函数之前,根据屏幕大小,把树木图形翻转。具体的DrawLine函数代码如下:
Private Sub DrawLine(ByVal width As Short, ByVal x1 As Short, ByVal y1 As Short, ByVal x2 As Short, ByVal y2 As Short, ByVal col As Color)
y1 = Height - y1
y2 = Height - y2
Dim pen As New Pen(col)
pen.Width = width
gdi.DrawLine(pen, x1, y1, x2, y2)
End Sub
6. 具体的实现代码
以上方法已经应用在我们的实践项目之中,语言为visual basic.net,需要framework1.1支持,下面给出代码的关键部分:
Dim gdi As Graphics
Const pi = 3.1415926
Private Function Rand(ByVal min As Single, ByVal max As Single) As Single
'返回min至max之间的随机数
'On Error resume next
Rand = Rnd() * (max - min) + min
End Function
Private Sub DrawLine(ByVal width As Short, ByVal x1 As Short, ByVal y1 As Short, ByVal x2 As Short, ByVal y2 As Short, ByVal col As Color)
y1 = Height - y1
y2 = Height - y2
Dim pen As New Pen(col)
pen.Width = width
gdi.DrawLine(pen, x1, y1, x2, y2)
End Sub
Private Sub leaf(ByVal x As Short, ByVal y As Short, ByVal angle As Single, ByVal xstep As Single)
Dim i As Short
Dim x1 As Single, y1 As Single
Dim col1 As Color
angle = angle / 180 * pi
Dim t As Single = -10 / 180 * pi
For i = 1 To 3
col1 = Color.FromArgb(255, 90 - i * 15, 80 + i * 30, 130 - i * 30)
x1 = x + Math.Cos(t + angle) * xstep
y1 = y + Math.Sin(t + angle) * xstep
DrawLine(1, x, y, x1, y1, col1)
x1 = x + Math.Cos(-t + angle) * xstep
y1 = y + Math.Sin(-t + angle) * xstep
DrawLine(1, x, y, x1, y1, col1)
t = t - 20 / 180 * pi
Next
End Sub
'------------------------------------------------------------------------------
'用二叉树算法递归生成如图1的树,其中树叶用线段代替
'------------------------------------------------------------------------------
Friend Sub GenerateTree(ByVal iter As Short, ByVal angle As Single, ByVal xstep As Single, ByVal x As Single, ByVal y As Single)
Dim col1 As Color
'区别树干和树叶的不同颜色
If iter <= 2 Then
If iter = 1 Then
col1 = Color.FromArgb(200, 60, 50, 70)
Else
col1 = Color.FromArgb(255, 30, 170, 50)
End If
Else
col1 = Color.FromArgb(200, 200, 80, 50)
'若树干角度过大或过小,则剪枝
If (angle > 180) Or (angle < 0) Then
Exit Sub
End If
End If
Dim rt As Single, x1 As Single, y1 As Single, t As Single
'画树叶
If iter = 0 Then
t = angle / 180 * pi
x1 = x + Math.Cos(t) * xstep
y1 = y + Math.Sin(t) * xstep
'画两种粗细不同的树叶
If xstep > 2 Then
DrawLine(4, x, y, x1, y1, col1)
Else
DrawLine(1, x, y, x1, y1, col1)
End If
Exit Sub
End If
'画主干
t = angle / 180 * pi
x1 = x + Math.Cos(t) * xstep
y1 = y + Math.Sin(t) * xstep
DrawLine(iter, x, y, x1, y1, col1)
x = x1
y = y1
Dim style As Short = 0
'随机决定较多侧枝杈在主干的左边或右边
Dim rstyle As Single
rstyle = Rand(0, 1)
If rstyle < 0.5 Then
style = 1
Else
style = -1
End If
'Do While style = 0
'style = Int(Rnd() * 2 - 1)
'Loop
'最开始时枝杈的偏转角随机度较小
'画较少侧枝的枝干
If iter >= 5 Then
GenerateTree(iter - 1, angle + (30 * style) + Rand(-15, 15), xstep * 0.6, x1, y1)
Else
GenerateTree(iter - 1, angle + (30 * style) + Rand(-30, 30), xstep * 0.6, x1, y1)
End If
If (angle > 80) And (angle < 100) And (iter > 4) Then
'有顶端优势
GenerateTree(iter - 1, angle, xstep * 0.7, x1, y1)
t = angle / 180 * pi
x1 = x + Math.Cos(t) * xstep * 0.8
y1 = y + Math.Sin(t) * xstep * 0.8
DrawLine(iter, x, y, x1, y1, col1)
x = x1
y = y1
GenerateTree(iter - 1, angle, xstep * 0.7, x1, y1)
Else
GenerateTree(iter - 1, angle, xstep * 0.7, x1, y1)
End If
'侧枝较小
xstep = xstep * 0.75
'最开始时枝杈的偏转角随机度较小
If iter >= 5 Then
rt = angle - 30 * style + Rand(-15, 15)
Else
rt = angle - 30 * style + Rand(-30, 30)
End If
'画较多侧枝的枝干
t = rt / 180 * pi
x1 = x + Math.Cos(t) * xstep
y1 = y + Math.Sin(t) * xstep
DrawLine(iter, x, y, x1, y1, col1)
GenerateTree(iter - 1, rt, xstep * 0.5, x, y)
GenerateTree(iter - 1, rt, xstep * 0.7, x1, y1)
GenerateTree(iter - 1, rt - 30 * style + Rand(-15, 15), xstep * 0.7, x1, y1)
End Sub
'------------------------------------------------------------------------------
'用二叉树算法递归生成如图2的树,其中树叶leaf函数生成针叶状
'------------------------------------------------------------------------------
Friend Sub GenerateTree1(ByVal iter As Short, ByVal angle As Single, ByVal xstep As Single, ByVal x As Single, ByVal y As Single)
'on error resume next
Dim col1 As Color = Color.FromArgb(200, 200, 80, 50)
If iter <= 1 Then
'画树叶
leaf(x, y, 90 + (angle - 90) / 2, 8 + xstep / 10)
Exit Sub
Else
If (angle > 180) Or (angle < 0) Then
Exit Sub
End If
End If
Dim rt As Single, x1 As Single, y1 As Single, t As Single
t = angle / 180 * pi
x1 = x + Math.Cos(t) * xstep
y1 = y + Math.Sin(t) * xstep
DrawLine(iter, x, y, x1, y1, col1)
x = x1
y = y1
Dim style As Short = 0
Dim rstyle As Single
rstyle = Rand(0, 1)
If rstyle < 0.5 Then
style = 1
Else
style = -1
End If
'Do While style = 0
'style = Int(Rnd() * 2 - 1)
'Loop
If iter >= 4 Then
GenerateTree1(iter - 1, angle + (40 * style) + Rand(-15, 15), xstep * 0.6, x1, y1)
Else
GenerateTree1(iter - 1, angle + (30 * style) + Rand(-30, 30), xstep * 0.6, x1, y1)
End If
If (angle > 80) And (angle < 100) And (iter > 3) Then
GenerateTree1(iter - 1, angle, xstep * 0.7, x1, y1)
t = angle / 180 * pi
x1 = x + Math.Cos(t) * xstep * 0.8
y1 = y + Math.Sin(t) * xstep * 0.8
DrawLine(iter, x, y, x1, y1, col1)
x = x1
y = y1
GenerateTree1(iter - 1, angle, xstep * 0.7, x1, y1)
Else
GenerateTree1(iter - 1, angle, xstep * 0.7, x1, y1)
End If
xstep = xstep * 0.75
If iter >= 4 Then
rt = angle - 40 * style + Rand(-15, 15)
Else
rt = angle - 30 * style + Rand(-30, 30)
End If
If (rt > 180) Or (rt < 0) Then
Exit Sub
End If
t = rt / 180 * pi
x1 = x + Math.Cos(t) * xstep
y1 = y + Math.Sin(t) * xstep
DrawLine(iter, x, y, x1, y1, col1)
GenerateTree1(iter - 1, rt, xstep * 0.5, x, y)
GenerateTree1(iter - 1, rt, xstep * 0.5, x1, y1)
GenerateTree1(iter - 1, rt - 30 * style + Rand(-15, 15), xstep * 0.5, x1, y1)
End Sub
'------------------------------------------------------------------------------
'用二叉树和L-system算法递归生成如图4的树
'------------------------------------------------------------------------------
Friend Sub GenerateTree2(ByVal iter As Short, ByVal angle As Single, ByVal xstep As Single, ByVal x As Single, ByVal y As Single)
'on error resume next
Dim col1 As Color
Dim rt As Single, x1 As Single, y1 As Single, t As Single, left As Single, right As Single
Dim i As Short
'若角度过大或过小则剪枝
If (angle > 170) Or (angle < 10) Then
Exit Sub
End If
If iter <= 1 Then
'控制叶子的长度
xstep = xstep * 0.8
If xstep > 13 Then xstep = xstep * 0.8
col1 = Color.FromArgb(200, 30, 150, 80)
t = angle / 180 * pi
x1 = x + Math.Cos(t) * xstep
y1 = y + Math.Sin(t) * xstep
DrawLine(3, x, y, x1, y1, col1)
Exit Sub
Else
col1 = Color.FromArgb(200, 200, 80, 50)
End If
Dim pp As Single = 0.75 '每层缩小的比例
'计算缩小的总比例
Dim prop As Single = 1
For i = 1 To 5
prop = prop * pp + pp
Next
prop = prop + 0.5
'根据步长和比例求出第一层的长度
Dim brach As Single = xstep / prop
'画主干
For i = 1 To 5
t = angle / 180 * pi
'最底层较短
If i = 1 Then
x1 = x + Math.Cos(t) * brach * 0.4
y1 = y + Math.Sin(t) * brach * 0.4
Else
x1 = x + Math.Cos(t) * brach
y1 = y + Math.Sin(t) * brach
End If
DrawLine(iter - i + 3, x, y, x1, y1, col1)
x = x1
y = y1
brach = brach * pp
Next
'最顶的分枝
GenerateTree2(iter - 2, angle, brach * 1.5, x, y)
For i = 1 To 4
brach = brach / pp
t = angle / 180 * pi
x = x - Math.Cos(t) * brach
y = y - Math.Sin(t) * brach
left = angle + 60 '最左枝的角度
right = angle - 60 '最右枝的角度
'从左往右画
While left > right
'较顶的分枝较简,较短
If i <= 2 Then
GenerateTree2(iter - 2, left, brach * 1.7, x, y)
Else
GenerateTree2(iter - 1, left, brach * 2, x, y)
End If
left = left - 40 + Rand(-15, 15)
End While
'画最右枝
If i <= 2 Then
GenerateTree2(iter - 2, right, brach * 1.7, x, y)
Else
GenerateTree2(iter - 1, right, brach * 2, x, y)
End If
Next
End Sub
Private strtree As String
Private str_len As Long
Private piont As Long = -1
'------------------------------------------------------------------------------
'用L-system算法递归生成如图4的树
'------------------------------------------------------------------------------
Private Sub GenerateLSystemTree()
gdi = Me.PictureBox1.CreateGraphics
Dim pen As New Pen(Color.Black)
Dim depth As Integer = 4
Dim xStep As Single
Dim Width As Single
Dim Height As Single
Dim Visable As Boolean
Dim pic As Bitmap
'生成字符串
'三种替换方案,其中G为树干,F,为树叶
Const re11 As String = "GG+[+F-F-F]-[-F+F+F]"
Const re21 As String = "GG-[-F+F+]+[+F-F-F]"
Const re31 As String = "GG-[-F+F+F]+[+F-F-F]"
strtree = re31 '种子"F",想限定树的生长偏向同一侧则可规定第一次替换的方案
Dim style As Single '生成随机样式
Dim i As Byte, j As Long, ch As Char, change As Char
For i = 1 To depth
str_len = strtree.Length
For j = 0 To str_len - 1
ch = strtree.Chars(j) '每次读一个字符
If (ch = "F") Or (ch = "G") Then
'先用ABC代替替换方案
style = Rand(0, 3)
Select Case style
Case 0 To 1
change = "A"
Case 1 To 2
change = "B"
Case 2 To 3
change = "C"
End Select
strtree = strtree.Insert(j, change)
strtree = strtree.Remove(j + 1, 1)
End If
Next
'字符替换
strtree = strtree.Replace("A", re11)
strtree = strtree.Replace("B", re21)
strtree = strtree.Replace("C", re31)
'strtree = strtree.Replace("G", "GG")
Next
piont = -1 '字符串指针初始化
'画树干
Dim x As Single = 200
Dim y As Single = 50
Dim x1 As Single, y1 As Single
Dim col1 As Color = Color.FromArgb(255, 200, 80, 60)
x1 = x + Math.Cos(pi / 2) * 60
y1 = y + Math.Sin(pi / 2) * 60
DrawLine(6, x, y, x1, y1, col1)
x = x1
y = y1
Lsystemtree(depth - 1, 90, x, y - 20)
End Sub
Friend Sub Lsystemtree(ByVal iter As Short, ByVal angle As Single, ByVal x As Single, ByVal y As Single)
'on error resume next
Dim col1 As Color
Dim x1 As Single, y1 As Single, t As Single
Dim ch As Char
While (ch <> "]") And (piont < str_len - 1) '推栈或字符串已读完
'读一个字符
piont = piont + 1
ch = strtree.Chars(piont)
Select Case ch
Case "F" '画树叶
col1 = Color.FromArgb(255, 30, 150, 90)
t = angle / 180 * pi
x1 = x + Math.Cos(t) * 4
y1 = y + Math.Sin(t) * 4
DrawLine(3, x, y, x1, y1, col1)
x = x1
y = y1
Case "G" '画树干
col1 = Color.FromArgb(255, 200, 80, 60)
t = angle / 180 * pi
x1 = x + Math.Cos(t) * 4
y1 = y + Math.Sin(t) * 4
DrawLine(iter, x, y, x1, y1, col1)
x = x1
y = y1
Case "+"
angle = angle + 30
Case "-"
angle = angle - 30
Case "[" '压栈
Lsystemtree(iter - 1, angle, x, y)
End Select
End While
End Sub
'------------------------------------------------------------------------------
'各函数的调用,括号内为建议实参
'------------------------------------------------------------------------------
GenerateTree(6, 90, 60, 200, 50) '生成如图1的树
GenerateTree1(5, 90, 60, 200, 50) '生成如图2的树
GenerateTree2(4, 90, 250, 200, 50) '生成如图4的树
GenerateLSystemTree() '生成如图3的树