[OpenGL]基于草图进行树木建模 – Sketch Based Modeling

我的新博客: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

图6:分解后的草图

这一步采用的算法来自于:《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,如下图的红色区域。



图6:分解后的草图

这一步采用的算法来自于:《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都是新的骨架点。接下来,按照如下策略将骨架点链接起来。

  1. 如果N=1,把p1直接连接到当前的branch上,并且下一步以p1中心继续进行上面的局部像素分析。
  2. 如果N>1,把p1直接连接到当前的branch上,并且下一步仍然以p1为中心,继续进行上面的局部像素分析。对于其他的新骨架点(pi,i=2,3,…N),都将产生一个新的branch,且pi分别每个新branch的第二个点,它们的第一个点当然是像素O了。如上图的图(f),A被直接连接到当前的branch上,C则作为新branch的第二个点。
  3. 如果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;
                        }
                    }
                }
            }
        }
    }
}

猜你喜欢

转载自blog.csdn.net/Mahabharata_/article/details/79511778