[图形学] 简化的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树的主要内容,如果未能解决你的问题,请参考以下文章