[图形学] 简化的bsp树

Posted ZJU_fish1996

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[图形学] 简化的bsp树相关的知识,希望对你有一定的参考价值。


简介

        bsp树是一种空间分割树,它主要用于游戏中的场景管理,尤其是室内场景的管理。

        它的本质是二叉树,也就是用一个面把空间分割成两部分,分割出的空间则继续用面分割,以此类推,直到到达特定递归深度,或者空间中不再有物体或只剩下一个物体(得到凸包/凸多面体)。

        最终,叶结点对应场景中的物体,内部结点存储分割面。物体被“收纳”到各个包围盒中。


应用


        bsp树对应的应用主要有两个方面:

        (1) 确定物体的遮挡关系,可视化处理。

        (2) 碰撞检测的广阶段。


       首先,要明确的一点是,bsp树是在设计地图时自动生成的树,它随之被保存到磁盘,也就是事先进行了预处理。在加载场景的时候,我们直接读入bsp文件,而不是重新生成。

      这也就意味着,作为预处理技术,bsp树只能处理静态的场景。


      bsp树本身的结构非常简单,但是这并不代表着bsp树的编程非常容易。其复杂性主要体现在:

      (1) bsp树不是独立存在的,它需要与前期的地图编辑器和后期的场景剔除/碰撞检测结合在一起。而这两者都是非常复杂的项目。

      (2) 选择合适的分割面,使得树尽可能平衡,并且能在恰当的时候停止分割。


       对于场景剔除而言,重点就是判断物体的前后关系,而这种空间拓扑关系在bsp树中已有了明显的体现。我们从根结点开始,根据摄像机所在位置和分割平面进行对比,很容易就能判断出结点的两个子空间与视点的前后关系。我们认为与视点在同一侧的为前面,在不同侧的为后面。

       对于碰撞检测而言,对所有物体都两两进行碰撞检测十分耗时,我们可以首先对物体进行初步排查,如果不处在同一个叶结点(包围盒),那么一定不会发生碰撞,通过简单的遍历树避免了繁琐的计算。


具体实现


      bsp树的编程比较复杂,在这里对bsp树做了最简化。之后的代码仅作为练习用,目的是更好地掌握bsp树。

      如果希望学习可用于商业引擎的bsp树,可参考quake3的地图编辑器。

      主要参考了《实时渲染》一书中给出的一个BSP结构,如下:

        技术分享

       简化部分 :

      (1) 使用二维,而不是三维。

      (2) 手工输入包围盒,而不是自动生成。包围盒为AABB包围盒(轴向),不支持上图中的凹多边形。

      (3) 叶结点和内部结点共用一个数据结构。其中内部结点存储了方向(水平或竖直)以及分割线;叶结点存储了对应场景为实心还是空心。

      (4) 沿着包围盒的边界进行分割(和图中所示相同),选择分割线的方法比较简单:

             1.分别选出水平和竖直方向的最优分割线。(判断标准:与空间中心最接近的包围盒边界线)

             2.如果最优水平分割线和场景中物体相交,而最优竖直分割线和场景中物体不相交,那么优先选择最优竖直分割线,反之亦然。

                如果都相交或都不相交,那么优先选择距离中心点更近的(按相对比例来算)

                如果选择的分割线与包围盒相交,那么把这个包围盒根据分割线拆成两个包围盒。

     (5) 退出条件(达到之一):

             1.达到最大分割层数,直接生成两个叶结点,返回。

             2.空间里没有物体了,返回空叶结点。

             3.空间里只剩下一个物体了,返回满叶结点。


            详细介绍已在代码注释中体现。


           技术分享

            结点数据结构


样例

        技术分享

           蓝线:第一次分割 ; 紫线:第二次分割 ; 黄线:第三次分割

        包围盒:

        (4,10,4,16)

        (10,24,4,9)

        (7,19,23,27)

        (22,28,13,24)

      

        bsp树

        技术分享


代码

bsp.h

#pragma once

#include<vector>

class bspTree
{
private:
	struct bspNode {
		bspNode* left;
		bspNode* right;
		bool isLeaf;//是否是叶结点
		bool isSolid;//是否实心
		bool isHori;//是否水平
		float data;//分割线
	};//结点数据结构
	struct box_t {
		float xmin;
		float xmax;
		float ymin;
		float ymax;
		box_t();
		void set(float x1, float x2, float y1, float y2);
	};//包围盒数据结构
	bspNode* root;//根节点
	std::vector<box_t>box;//包围盒容器
	float xmin, xmax, ymin, ymax;//整个场景的轴向包围盒
	int boxNum;//包围盒的个数
	int layer;//树的最大深度
	int layer_count;//记录当前层数
	bspNode* createEmptyNode();//生成空叶结点
	bspNode* createSolidNode();//生成非空叶结点
	void split(float& data_x, float& data_y, float& dis_x, float& dis_y,
		float xmin, float xmax, float ymin, float ymax);//寻找最优分割线
	bspNode* genNode(bool isFull_1, bool isFull_2, int layer_count,
		float xmin, float xmax, float ymin, float ymax, float data,bool isHori);//生成新的结点
	bspNode* build(int layer_count, float xmin, float xmax,
		float ymin, float ymax);//创建新的结点
	bool isIntersect(float xmin, float xmax, float ymin, float ymax, float data, bool isHori,std::vector<int>& id,int& num);
	//某一空间中的分割线是否与空间中的一个包围盒相交
	void traversal(bspNode* t);//前序遍历
	bool inBox(float x1, float x2, float y1, float y2, int id);//包围盒id是否完全处在某个空间中
	void checkIsFull(bool& isFull_x_1, bool& isFull_x_2, bool& isFull_y_1, bool& isFull_y_2,
		float xmin, float xmax, float ymin, float ymax, float data_x, float data_y);//检测分割出的两个区域是否为满/空
	bool isIntersect(float x1, float x2, float y1, float y2, int id);//包围盒id是否与某个空间有交集
public:
	bspTree(float x1, float x2, float y1, float y2, int l);//构造
	//前四个参数为场景包围盒,l为最大递归深度
	void add(float x1, float x2, float y1, float y2);//添加包围盒
	void build();//创建bsp树
	void print();//前序遍历输出
	void levelOrder();//层次遍历输出
};


bsp.cpp

#include"bsp.h"
#include<algorithm>
#include<queue>

bspTree::box_t::box_t() { }
void bspTree::box_t::set(float x1, float x2, float y1, float y2)
{
	xmin = x1;
	xmax = x2;
	ymin = y1;
	ymax = y2;
}

bspTree::bspTree(float x1, float x2, float y1, float y2, int l)
{
	xmin = x1;
	xmax = x2;
	ymin = y1;
	ymax = y2;
	layer = l;
	boxNum = 0;
	layer_count = 0;
	root = nullptr;
}

void bspTree::add(float x1, float x2, float y1, float y2)
{
	box_t b;
	boxNum++;
	b.set(x1, x2, y1, y2);
	box.push_back(b);
}
//某一空间中的分割线是否与空间中的一个包围盒相交
bool bspTree::isIntersect(float xmin,float xmax,float ymin,float ymax,float data, bool isHori, std::vector<int>& id,int& num)
{
	bool flag = false;//记录是否存在交
	//分割线是水平的
	if (isHori) {
		//遍历所有包围盒
		for (int i = 0; i < boxNum; i++) {
			//如果包围盒完全处在空间中
			if (inBox(xmin, xmax, ymin, ymax, i)) {
				num++;//记录包围盒个数+1
				if (data > box[i].xmin && data < box[i].xmax) { //存在交
					id.push_back(i);//记录包围盒id
					flag = true;//存在交 为真
				}
			}
		}
	}
	//分割线是竖直的
	else if (!isHori) {
		//遍历所有包围盒
		for (int i = 0; i < boxNum; i++) {
			//如果包围盒完全处在空间中
			if (inBox(xmin, xmax, ymin, ymax, i)) {
				num++;//记录包围盒个数+1
				if(data > box[i].ymin&&data < box[i].ymax) {//存在交
					id.push_back(i);//记录包围盒id
					flag = true;//存在交 为真
		    	}
			}
		}
	}

	return flag;
}

//包围盒id是否完全处在某个空间中
bool bspTree::inBox(float x1, float x2, float y1, float y2,int id)
{
	return box[id].xmin >= x1 && box[id].xmax<=x2 &&
		box[id].ymin>=y1 && box[id].ymax <= y2;
}

//寻找最优分割线
void bspTree::split(float& data_x, float& data_y, float& dis_x, float& dis_y,
	float xmin, float xmax, float ymin, float ymax)
{
	float d = 10000;
	//先计算竖直方向

	//遍历所有包围盒
	for (int i = 0; i < boxNum; i++) {
		//如果包围盒完全处在空间中
		if (inBox(xmin,xmax,ymin,ymax,i)) {
			//计算包围盒边界线到中心的距离
			d = box[i].xmin - ((xmax - xmin) / 2 + xmin);
			if (d < 0)d = -d;
			//如果有更小的距离,更新距离和分割线
			if (d < dis_x) {
				dis_x = d;
				data_x = box[i].xmin;
			}

			//计算包围盒边界线到中心的距离
			d = box[i].xmax - ((xmax - xmin) / 2 + xmin);
			if (d < 0) d = -d;
			//如果有更小的距离,更新距离和分割线
			if (d < dis_x) {
				dis_x = d;
				data_x = box[i].xmax;
			}
		}
	}
	//再计算水平方向
	//遍历所有包围盒
	for (int i = 0; i < boxNum; i++) {
		//如果包围盒完全处在空间中
		if (inBox(xmin, xmax, ymin, ymax, i)) {
			//计算包围盒边界线到中心的距离
			d = box[i].ymin - ((ymax - ymin) / 2 + ymin);
			if (d < 0)d = -d;
			//如果有更小的距离,更新距离和分割线
			if (d < dis_y) {
				dis_y = d;
				data_y = box[i].ymin;
			}
			//计算包围盒边界线到中心的距离
			d = box[i].ymax - ((ymax - ymin) / 2 + ymin);
			if (d < 0)d = -d;
			//如果有更小的距离,更新距离和分割线
			if (d < dis_y) {
				dis_y = d ;
				data_y = box[i].ymax;
			}
		}
	}
	//计算相对距离
	dis_x /= xmax - xmin;
	dis_y /= ymax - ymin;
}

//创建空叶结点
bspTree::bspNode* bspTree::createEmptyNode()
{
	bspNode* node = new bspNode();
	node->left = nullptr;
	node->right = nullptr;
	node->isLeaf = true;
	node->isSolid = false;
	node->data = 0.0f;
	return node;
}

//创建非空叶结点
bspTree::bspNode* bspTree::createSolidNode()
{
	bspNode* node = new bspNode();
	node->left = nullptr;
	node->right = nullptr;
	node->isLeaf = true;
	node->isSolid = true;
	node->data = 0.0f;
	return node;
}

//生成结点
bspTree::bspNode* bspTree::genNode(bool isFull_1,bool isFull_2,int layer_count,
	float xmin,float xmax,float ymin,float ymax,float data,bool isHori)
{
	bspNode* node = new bspNode();//申请
	if (!root) { //指定根
		root = node;
	}
	//如果没有到达最大的深度
	if (layer != layer_count) {
		//如果区域1是满的
		if (isFull_1) {
			//递归创建
			if(isHori)node->left = build(layer_count + 1,xmin, xmax,ymin,data);
			else node->left = build(layer_count + 1, xmin, data, ymin, ymax);
		}
		//如果区域1是空的
		else {
			//直接创建空叶结点,不继续递归
			node->left = createEmptyNode();
		}
		//如果区域2是满的
		if (isFull_2) {
			//递归创建
			if(isHori)node->right = build(layer_count + 1, xmin, xmax,data,ymax);
			else node->right = build(layer_count + 1, data, xmax, ymin, ymax);
		}
		//如果区域2是空的
		else {
			//直接创建空叶结点,不继续递归
			node->right = createEmptyNode();
		}
	}
	//如果达到了最大深度
	else if (layer ==layer_count) {
		//如果区域1是满的
		if (isFull_1) {
			//直接创建满叶结点,不继续递归
			node->left = createSolidNode();
		}
		//如果区域1是空的
		else {
			//直接创建空叶结点,不继续递归
			node->left = createEmptyNode();
		}
		//如果区域2是满的
		if (isFull_2) {
			//直接创建满叶结点,不继续递归
			node->right = createSolidNode();
		}
		//如果区域2是空的
		else {
			//直接创建空叶结点,不继续递归
			node->right = createEmptyNode();
		}
	}
	//设置结点基本信息
	node->isLeaf = false;
	node->isHori = isHori;
	node->data = data;
	return node;
}

//包围盒id是否与某个空间有交集
bool bspTree::isIntersect(float x1, float x2, float y1, float y2, int id)
{
	//两种情况:
	// 1. 水平,竖直方向都各有至少一条边界线落在区域内(不含恰好落在区域边界)
	// 2. 水平方向两条边界线都落在区域边界,或竖直方向两条边界线都落在区域边界
	return ((box[id].xmin > x1 && box[id].xmin<x2 ||
		box[id].xmax>x1 && box[id].xmax<x2||
		box[id].xmin == x1 && box[id].xmax==x2) &&
		(box[id].ymin>y1&&box[id].ymin<y2 ||
		box[id].ymax>y1&&box[id].ymax < y2)||
		box[id].ymin==y1&&box[id].ymax==y2);
}

//检测分割出的两个区域是否为满/空
void bspTree::checkIsFull(bool& isFull_x_1, bool& isFull_x_2, bool& isFull_y_1, bool& isFull_y_2,
	float xmin, float xmax, float ymin, float ymax,float data_x,float data_y)
{
	//遍历所有包围盒,如果有包围盒与该空间存在交集,那么这个空间就是满的
	for (int i = 0; i < boxNum; i++) {
		if (!isFull_x_1 && isIntersect(xmin, data_x, ymin, ymax,i)) {
			isFull_x_1 = true;
		}
		if (!isFull_x_2 && isIntersect(data_x, xmax, ymin, ymax, i)) {
			isFull_x_2 = true;
		}
		if (!isFull_y_1 && isIntersect(xmin, xmax, ymin, data_y, i)) {
			isFull_y_1 = true;
		}
		if (!isFull_y_2 && isIntersect(xmin, xmax, data_y, ymax, i)) {
			isFull_y_2 = true;
		}
	}
	return;
}

//创建bsp树
bspTree::bspNode* bspTree::build(int layer_count, float xmin, float xmax,
	float ymin, float ymax)
{
	//printf("%f %f %f %f\n", xmin, xmax, ymin, ymax);
	//超过递归深度直接返回NULL
	if (layer_count == layer + 1)return nullptr;
	bspNode* node = nullptr;

	//初始化一些变量:距离,分割线,是否相交,子空间空/满状态,相交包围盒的id,空间内包围盒的个数
	float dis_x = 10000;
	float dis_y = 10000;
	float data_x = -1;
	float data_y = -1;
	bool isIntersect_x;
	bool isIntersect_y;
	bool isFull_x_1 = false;
	bool isFull_x_2 = false;
	bool isFull_y_1 = false;
	bool isFull_y_2 = false;
	std::vector<int>id_x;
	std::vector<int>id_y;
	int num_x = 0;
	int num_y = 0;

	split(data_x, data_y, dis_x, dis_y, xmin, xmax, ymin, ymax);//找到预备的最优分裂线
	
	//两者未赋值,说明没有可以选择的包围盒,也就是空间是空的,直接返回空叶节点
	if (data_x == -1 && data_y == -1) {
		return createEmptyNode();
	}

	//判断最优分裂线与包围盒是否相交
	isIntersect_x = isIntersect(xmin,xmax,ymin,ymax,data_x, true,id_x,num_x);
	isIntersect_y = isIntersect(xmin,xmax,ymin,ymax,data_y, false,id_y,num_y);
	//判断分割的子空间为空/满
	checkIsFull(isFull_x_1, isFull_x_2, isFull_y_1, isFull_y_2, xmin, xmax, ymin, ymax, data_x, data_y);
	//空间中只有一个物体,直接返回满叶结点
	if (num_x == 1)return createSolidNode();

	//竖直分割线相交,水平分割线不相交,选择水平分隔线
	if (isIntersect_x && !isIntersect_y) {
		node = genNode(isFull_y_1, isFull_y_2, layer_count, xmin, xmax, ymin, ymax, data_y,true);
	}
	//竖直分割线不相交,水平分割线相交,选择竖直分隔线
	else if (!isIntersect_x && isIntersect_y) {
		node = genNode(isFull_x_1, isFull_x_2, layer_count, xmin, xmax, ymin, ymax, data_x,false);
	}
	//都相交 或都不相交,选择距离中心近的
	else {
		//竖直更近
		if (dis_x < dis_y) {
			//如果存在相交,分裂包围盒
			if (isIntersect_x) {
				for (int i = 0; i < id_x.size(); i++) {
					float x1 = box[id_x[i]].xmin;
					float x2 = box[id_x[i]].xmax;
					float y1 = box[id_x[i]].ymin;
					float y2 = box[id_x[i]].ymax;
					boxNum++;
					box[id_x[i]].set(x1, data_x, y1, y2);
					box_t b;
					b.set(data_x, x2, y1, y2);
					box.push_back(b);
				}
				id_x.clear();
			}
			node = genNode(isFull_x_1, isFull_x_2, layer_count, xmin, xmax, ymin, ymax, data_x,false);
		}
		//水平更近
		else {
			//如果存在相交,分裂包围盒
			if (isIntersect_y) {
				for (int i = 0; i < id_y.size(); i++) {
					float x1 = box[id_y[i]].xmin;
					float x2 = box[id_y[i]].xmax;
					float y1 = box[id_y[i]].ymin;
					float y2 = box[id_y[i]].ymax;
					boxNum++;
					box[id_y[i]].set(x1, x2, y1, data_y);
					box_t b;
					b.set(x1, x2, data_y, y2);
					box.push_back(b);
				}
				id_y.clear();
			}
			node = genNode(isFull_y_1, isFull_y_2, layer_count, xmin, xmax, ymin, ymax, data_y,true);
		}
	}
	return node;
}

//创建入口
void bspTree::build()
{
	build(1, xmin, xmax, ymin, ymax);
}

//前序输出
void bspTree::print()
{
	traversal(root);
}

//前序
void bspTree::traversal(bspNode* t)
{
	if (!t)return;
	if (t->data != 0)printf("%f ", t->data);
	else printf("leaf:%d", t->isSolid);
	if (t->isHori)printf("h\n");
	else printf("v\n");
	traversal(t->left);
	traversal(t->right);
}

//层序
void bspTree::levelOrder()
{
	std::queue<bspNode*>q;
	q.push(root);
	while (!q.empty()) {
		bspNode* t = q.front();
		if(t->data!=0)printf("%f ", t->data);
		else printf("leaf:%d", t->isSolid);
		if (t->isHori)printf("h\n");
		else printf("v\n");
		q.pop();
		if (t->left != nullptr)q.push(t->left);
		if (t->right != nullptr)q.push(t->right);
	}
	return;
}

main.cpp

#include "bsp.h"
#include<stdlib.h>
int main()
{
	bspTree* t = new bspTree(1,33,1,33,3);
	t->add(4, 10, 4, 16);
	t->add(10, 24, 4, 9);
	t->add(7, 19, 23, 27);
	t->add(22, 28, 13, 24);
	t->build();
	t->print();
	printf("\n");
	t->levelOrder();
	system("pause");
}



































以上是关于[图形学] 简化的bsp树的主要内容,如果未能解决你的问题,请参考以下文章

[图形学] 《Real-Time Rendering》碰撞检测

决策树代码《机器学习实战》

Real - time Rendering 实时计算机图形学

BSP开发学习2平台设备驱动

BSP开发学习2平台设备驱动

一步步学Metal图形引擎1-《绘制第一个三角形》