我的新博客:http://ryuzhihao.cc/
本文在我的新博客中的链接:http://ryuzhihao.cc/?p=667
许多设计师们觉得:传统的3D建模软件的学习曲线都太过漫长,比如Maya、Blender等等。对于初学者而言,如果没有阅读长篇大量的教程,是很难使用这些软件创建三维模型的。然后,Sketch-based modeling(基于草图的建模)便随着这样需求诞生了~试想一下,如果设计者只需要用纸和笔画一幅草图,电脑就能够根据这幅草图帮助用户自动建立相应的3D模型,这是一件多么酷的事情~!
对于植物的快速建模,现在已经有了很多的方法:基于点云、基于图像、基于规则、基于草图等等。这其中的基于草图就是本篇博客的核心内容了~ 这篇博客,将介绍我做的一个简单的sketch-based tree modeling的程序,以及其中的核心算法,核心代码在文章末尾。对于树木的建模要用到的多叉树结构这里不做说明,本文主要侧重于如何从草图建立多叉树结构。
一、我的程序效果
图1:我的程序效果图
图2:第一个测试用例
图3:第二个测试用例
二、算法流程
1 输入绘制的草图
在提交到程序处理之前,用户要先绘制一副草图。用户在绘制时,可能会使用不同的画笔进行绘制,如:单像素(Single-pixel)、画刷(Brush)。对这两种画笔绘制出来的笔画(strokes),需要采用不同的方式进行处理。在下文,主要介绍的是基于单像素的画笔的处理流程。
图4:绘制的单像素草图
如何实现单像素草图的画板?其实是一个简单涂鸦的算法。在每次鼠标移动时,记录移动的起点和终点,然后利用Bresenham算法等,在两个点之间画直线线。由于鼠标移动频率较高,同时操作系统要检测到鼠标的两次移动是有间隔时间的,因此可以不断的在移动过程中调用Bresenham算法进行画线,最终得到的就是一副涂鸦。由于该算法较为简单且是图形算法的基础,这里不做详细说明。
2 算法综述和数据结构定义
在介绍具体的处理算法之前,先对将要出现的名词做下定义。
- Branch(枝干):是树中的一段枝干,它没有任何的分支。在 图 5 中,每条彩色的线段就是一个branch。
- Node(结点):每条branch由一组node组成,其中P1是该branch与其父枝条的连接点。
- Unit(枝元):branch的两个相邻结点连接起来,组成一个枝元。这个概念在算法阶段还用不到,主要是为了方便绘制。
图5:branch和node
3. 从草图中划分branch,得到二维骨架
这一步的目的是进行枝干分解,得到如图6所示的二维骨架(2D skeleton)。图5是程序对草图处理后,得到的枝干分解图,不同颜色的线条代表一段branch。
这一步采用的算法来自于:《Easy modeling of realistic trees from freehand sketches》的4.1.1 Local pixel analysis小节和4.1.2 link new skeleton points小节。
对于当前正在搜索的像素O:像素O在第一层的8个相连的邻近像素称为8-Nps,如图7的绿色区域。像素O在第二层的16个邻近像素称为16-Nps,如下图的红色区域。
这一步采用的算法来自于:《Easy modeling of realistic trees from freehand sketches》的4.1.1 Local pixel analysis小节和4.1.2 link new skeleton points小节。
对于当前正在搜索的像素O:像素O在第一层的8个相连的邻近像素称为8-Nps,如图7的绿色区域。像素O在第二层的16个邻近像素称为16-Nps,如下图的红色区域。
图8:像素O的局部分析和连接
2)Link new skeleton points(连接新的骨架点)
在完成上面对单个像素点O的局部分析后,得到了N个新的骨架点pi(i = 1,2,…N)。如上图所示,A/C都是新的骨架点。接下来,按照如下策略将骨架点链接起来。
- 如果N=1,把p1直接连接到当前的branch上,并且下一步以p1中心继续进行上面的局部像素分析。
- 如果N>1,把p1直接连接到当前的branch上,并且下一步仍然以p1为中心,继续进行上面的局部像素分析。对于其他的新骨架点(pi,i=2,3,…N),都将产生一个新的branch,且pi分别每个新branch的第二个点,它们的第一个点当然是像素O了。如上图的图(f),A被直接连接到当前的branch上,C则作为新branch的第二个点。
- 如果N=0,当前branch的搜索结束,换其他还没有搜索过的branch继续进行局部像素分析。
这样,从起点开始,对每个像素进行局部分析与连接。这些所有的像素都将一个又一个的连接到骨架上去。
4. 3D branch construction(三维枝干的生成)
通过前面的步骤,我们已经能够得到由若干个Branch组成的二维骨架(2D skeleton)。接下来,要将2D骨架转变为3D结构。
在前面提到的论文中使用了两幅图像:正面和侧面的草图。并以正面的图像为基准,从侧面的图像中匹配对应的branch,这仍然是一个搜索算法。从两个图像对应的2D骨架的main branch开始,逐步将所有的branch进行匹配,然后进行旋转。
不过,本文所示的demo没有采用这种方法,仅仅用了一副图像。这种处理方式要简单的多,只需要将每个branch围绕他的第一个node进行旋转即可(当然,要保证与其他邻近的枝条不碰撞)。这里的就是简单的碰撞检测了,在此不再赘述。
三、核心算法
在上文中,local pixel analysis和link new skeleton points是文章中最为核心的部分,这里给出这一部分的代码,通过下面的函数,能够从图像image中获取到2D的skeleton。
class Branch
{
public:
List<Point> nodes;
List<Branch*> offspring;
} trunk;
void Sketch::generate2DSkeleton(Image image)
{
// Image是输入的原图,黑色表示笔画。
// mask是一个辅助矩阵(初始为0,表示没有访问过
int mask[image.height()][image.width()];
memset(mask,0,image.height()*image.width()*sizeof(int));
// 初始化:此时只有trunk一个branch,且trunk只有一个点
Point root(startPos);
trunk.nodes.push_back(root);
List<Branch*> branchs; // 队列,branch的探索队列
Point cur; // 队列,当前正在搜索的结点
branchs.push_back(&trunk); // 把主干-trunk放进去
// 以该点为中心center,开始探索
while(branchs.size() != 0)
{
Branch* curbranch = branchs.front(); // 当前枝干
branchs.pop_front(); // 当前枝干出队列
cur = curbranch->nodes.front(); // 当前枝干的第一个结点
while(true)
{
mask[cur.y()][cur.x()] = true; // 设置为已经访问过
List<QPoint> AIP; // AIP
for(int i=0; i<8; i++) // 遍历cur的8个内层邻居
{
Point p = cur + OffsetInside[i];
if(mask[p.y()][p.x()] == false) // 黑色没有访问过
{
AIP.push_back(p); // 加入AIP
mask[p.y()][p.x()] = true; // 已访问过
}
}
QList<QPoint> AOG; // AOG
for(int i=0; i<16; i++) / 遍历cur的16个外层邻居
{
Point p = cur + OffsetInside[i];
if(mask[p.y()][p.x()] == false) // 黑色没有访问过
{
AOG.push_back(p); // 加入AOG
}
}
// 查找完AIP和AOG后,进行连接
if(AOG.size() == 0) // 如果没有可以继续延伸的位置了, 切换枝条
{
break;
}
if(AOG.size() == 1)
{
// 在AIP中随机找一个与AOG[0]邻接的点加入当前枝条
for(int i=0; i<AIP.size(); i++)
{
if((fabs(AIP[i].x()-AOG[0].x())<=1) || fabs(AIP[i].y()-AOG[0].y())<=1)
{
nodes.push_back(AIP[i]); // 加入一个下一个被探测的nodes;
curbranch->nodes.push_back(AIP[i]); // 当前枝条也要新增一个点,然后break;
break;
}
}
}
if(AOG.size() > 1)
{
// 随便找一个方向继续延伸当前枝条
int p = qrand()%AOG.size(); // 选择的p用作当前纸条的延伸
for(int i=0; i<AIP.size(); i++)
{
if((fabs(AIP[i].x()-AOG[p].x())<=1) || fabs(AIP[i].y()-AOG[p].y())<=1)
{
nodes.push_back(AIP[i]);
AIP.removeAt(i);
break;
}
}
AOG.removeAt(p);
// 再把其他的AOG加入为新的枝条
for(int k=0; k<AOG.size(); k++)
{
for(int i=0; i<AIP.size(); i++)
{
if((fabs(AIP[i].x()-AOG[k].x())<=1) || fabs(AIP[i].y()-AOG[k].y())<=1)
{
// 创建新的枝条
Branch* newBranch = new Branch();
newBranch->nodes.push_back(AIP[i]); // 为新的枝条加入一个起始结点
curbranch->offspring.push_back(newBranch); // 把新的枝条加入到当前正在探测的枝干
branchs.push_back(curbranch->offspring.back()); // 新增到枝干队列中
AIP.removeAt(i);
break;
}
}
}
}
}
}
}