项目中要用到3D建模,对比了网上的各种3D建模软件,最终sweethome3d被选中作为原型. sweethome3d有以下几个优点:
- 采用Java实现
- 拥有客户端和web端
- 插件易于开发
- 可以集成各种模型组件
- 开源
需求
在sweethome3d建模软件上实现一个插件,可以根据房屋单层模型自动构建出一栋楼,由于不需要查看楼内结构,只需要保存楼层外壳即可,这样可以简化模型结构,加快模型渲染.
与sweethome3d官方沟通,得知软件已支持多楼层复制,但不支持单层模型约简,故核心难点是如何将包含内部结构的单层模型约简成单层外壳,将其内部结构剔除.
需要实现的效果如下
原始模型
约简后的模型
算法设计
算法原理
可以使用泛洪填充算法(Flood Fill Algorithm)做模型约简.
算法实现的核心思想是,将模型包裹在一个最小矩形内,在矩形边界上找一个点,向矩形内部逐步扩散,当触碰到模型边界时即表明该边界属于外壳,边界可以保留,该扩散点停止扩散.如此可以实现一种类似"水漫金山"的方式,将模型外壳识别出来
算法分析
递归泛洪算法
一个点做扩散时可以按四领域或八领域的方式扩散
四领域
四邻域的泛洪填充算法,寻找点p(x, y)的上下左右四个临近点,如果没有
被填充,则填充它们,并且继续寻找它们的四邻域点,直到封闭区域完全被填充
八领域
八邻域的填充算法,则是在四邻域的基础上增加了左上,左下,右上,右下四个相邻点。
并递归寻找它们的八邻域点填充,直到区域完全被填充。
这两种扩散方式本质上是一样的,这样的递归有两个问题:
- 栈溢出:算法基于递归实现,容易出现导致栈溢出,在实际测试中,对25m*20m的模型就已经栈溢出
- 运行缓慢: 25m*20m面积的模型测试时间长度超过5mins
线性泛洪算法
采用Queue-Linear Flood Fill算法可避免递归导致的栈溢出问题,算法复杂度为O(n),对同一25m*20m的模型,算法运行时间缩短至10%
线性泛洪算法是非递归方法,以横坐标为基准,逐渐上下扩展,扩展时按行来走,每次都是计算行的范围,把每个行内的点入队,对行内所有点,逐一出队,再对出队的点上下扩展,将没走过的点入队,当所有点遍历完,队列为空,结束算法
构建起始线
上下扩展
算法实现思路
采用线性泛洪算法可以识别出最外围的边界线,但模型内部的线还需要剔除.有些线在模型内部,直接剔除即可,但有些边界线是从模型外部延伸到内部,此时需要保留其外部部分,剔除其处于内部部分.
可以将所有线段按交点切割,再判断每条线段是否处于外边界即可
如下,不同颜色标识出切割后的每一条线段
优化
- 范围缩放
默认Sweethome3d的距离单位是1cm,如果每一步扩散时都是按单位长度扩散,由于房屋范围过大,每次的步长为1cm就太浪费了,只要保证步长不超过墙壁厚度即可,这里把每次的扩散步长设为墙厚度,故而模型另起一个二维矩阵int[][] pointMatrix,对模型做了映射,矩阵里的[0][1],和[0][2]实际相隔距离不是1cm而是墙的厚度 - 缓存接触点
缓存接触点,当使用线性泛洪算法找到接触外围的边界点时,缓存该点,在剔除内部线段时,只需要使用缓存点去判断是否接触该线段,与缓存点接触的则可判为外围边界线,否则为内部线,就不用再遍历所有的点了,只遍历和外部有关的点
如下图,所有的接触点(红色点)将形成一层膜包裹住整个模型
核心算法
/**
* 非递归方法,以横坐标为基准,上下扩展 按行来走,每次都是计算行的范围,把每个行内的点入队 对行内所有点,出队,上下扩展,再入队
* 当所有点遍历完,队列为空,结束
*
* @see https
* ://stackoverflow.com/questions/8070401/android-flood-fill-algorithm
* 算法复杂度 O(n)
* @param x
* @param y
*/
public void floodFill(int x, int y) {
// 起始点,左右横向走
LinearFill(x, y);
FloodFillRange range;
int downY, upY;
while (ranges.size() > 0) {
range = ranges.remove(); // 出队
downY = range.Y - 1; // 上下扩展
upY = range.Y + 1;
for (int i = range.startX; i <= range.endX; i++) {
// 边界内,没碰到墙,就记下这一行,入队
if (upY < (pointMatrix[0].length) && (pointMatrix[i][upY] == 0)
&& !isWallTouch(i, upY))
LinearFill(i, upY);
if (downY >= 0 && (pointMatrix[i][downY] == 0)
&& !isWallTouch(i, downY))
LinearFill(i, downY);
}
}
}
/**
* 左右横向走,入队
*/
protected void LinearFill(int x, int y) {
// 寻找左边界点
int leftX = x;
while (true) {
pointMatrix[leftX][y] = 1; // 标记已经走过
leftX--;
// 边界检查 是否已经走过 是否是目标点,比如是否已经触及墙壁
if (leftX < 0 || (pointMatrix[leftX][y] == 1)
|| isWallTouch(leftX, y)) {
break;
}
}
leftX++;
// 寻找右边界点
int rightX = x;
while (true) {
pointMatrix[rightX][y] = 1;
rightX++;
if (rightX >= pointMatrix.length || (pointMatrix[rightX][y] == 1)
|| isWallTouch(rightX, y)) {
break;
}
}
rightX--;
// 把左右点加入队列
FloodFillRange r = new FloodFillRange(leftX, rightX, y);
ranges.offer(r);
}
源码地址:FloodFill