Tip:VBH,RayTracing,Unity,Compute shader
全名:Bounding volume hierarchy
在我们写光追的时候,引入Mesh filter一类的不再是单独的模拟球,而涉及到Triangle,三角面的时候就会涉及到光线是否击中物体,也就是光线与物体的每一个三角面求交查看是否击中物体。涉及到这个最简单粗暴的就是遍历了,遍历是世界上最好的算法,没有之一。虽然肯定可以达到你要的效果,但是他会带来另一个致命缺陷开销过大,所以就开始各显神通创造多种算法来加速这一过程eg:
(图片来自games101)
他们都是好用的算法但是也有其缺陷(本文介绍BVH当然要口嗨别的算法是垃圾)
总的来说他们都有这样的缺陷:
这几个划分都会碰见一个问题就是会切割实际的物体或者划分规则比较难实现。
他们都是一空间划分为基础构建的"二分"算法。
下面我们引入一下BVH:
以物体划分为基础构建的近似"二分"算法。
(以上的二分说法可能不够严谨,但是思想起源如此)
示例:
BVH以每一个物体为单位(自己定义,一个三角形或者多个都可以)进行划分。
大概描述一下就是将每一个三角形计算一个值(例如重心)进行排序(例如以重心x大小),进行排序之后将三角形分成两堆。然后在这两堆里面进行同样的操作,直到每一个堆都是可以进行快速查找是否和内部命中的情况进行停止(例如每一个堆只有一个三角形停止)。
好多啊看百科可能更好:
介绍BVH之前还需要做一些准备工作:
其实包围盒思想遍地可见,回想一下我们的各种简化操作:
Unity里面的UI都是一个方形的盒子装着的即使你旋转什么的也是一个矩形装着(2D)
再看实际的物体,cube则是被一个长方体包围着(3D)
类似还有由多个包围盒组成的物体
借此我们对每一个三角形引入包围盒思想。
为了方便计算我们对包围盒进行一定的要求,包围盒为轴对齐包围盒,而不是每个三角面的最小包围盒(针对3d) 。
public class Bound3
{
public Vector3 pMin, pMax;
public Bound3()
{
//这里的1000应该使用float.maxValue,但是因为够用就没有修改
pMax = new Vector3(-1000, -1000, -1000);
pMin = new Vector3(1000, 1000, 1000);
}
}
1:计算包围盒简单
2:计算与射线的相交也很快
只需要找到三角面三个点的xMin,xMax,YMin。。。。zMax即可
Bound3 Union(Bound3 b1, Bound3 b2)
{
Bound3 ret = new Bound3();
ret.pMax = new Vector3();
ret.pMin = new Vector3();
ret.pMax = B3Max(b1.pMax, b2.pMax);
ret.pMin = B3Min(b1.pMin, b2.pMin);
return ret;
}
Bound3 Union(Vector3 b1, Vector3 b2)
{
Bound3 ret = new Bound3();
ret.pMax = new Vector3();
ret.pMin = new Vector3();
ret.pMax = B3Max(b1, b2);
ret.pMin = B3Min(b1, b2);
return ret;
}
Min,max示例:
Vector3 V3Min(Vector3 f1, Vector3 f2)
{
return new Vector3(Mathf.Min(f1.x, f2.x), Mathf.Min(f1.y, f2.y), Mathf.Min(f1.z, f2.z));
}
Vector3 V3Max(Vector3 f1, Vector3 f2)
{
return new Vector3(Mathf.Max(f1.x, f2.x), Mathf.Max(f1.y, f2.y), Mathf.Max(f1.z, f2.z));
}
//下面展示一些类似的
//v3乘法
Vector3 V3Mat(Vector3 a, Vector3 b)
{
return new Vector3(a.x * b.x, a.y * b.y, a.z * b.z);
}
//获取重心
Vector3 Cenroid(Bound3 bound)
{
return 1.0f / 2.0f *V3Add (bound.pMax , bound.pMin);
}
包围盒基本算法展示的差不多了
1:构建BVH树
2:在检测时通过BVH树进行加速
二分规则选择:
每次进行分割选择包围盒轴差值最大的Max(pMax.x-pMin.x ,pMax.y-pMin.y ,pMax.z-pMin.z)
这个在三角面哪里会有所体现
1:选择三角面的排序算法:
A:通过重心排序
//dim表示的是切割是通过那种方式0 = x, 1 = y, 2 = z
switch (dim)
{
case 0:
triangles.Sort((a, b) => Cenroid(a.bound3).x.CompareTo(Cenroid(b.bound3).x));
break;
case 1:
triangles.Sort((a, b) => Cenroid(a.bound3).y.CompareTo(Cenroid(b.bound3).y));
break;
case 2:
triangles.Sort((a, b) => Cenroid(a.bound3).z.CompareTo(Cenroid(b.bound3).z));
break;
}
Vector3 Cenroid(Bound3 bound)
{
return 1.0f / 2.0f *V3Add (bound.pMax , bound.pMin);
}
B:通过三角面的边际排序(和上一个基本差不多)
triangles.Sort((a, b) => Cenroid(a, dim).CompareTo(Cenroid(b, dim)));
float Cenroid(Triangle triangle,int D)
{
if(D == 0)
{
return Mathf.Max(triangle.v0.x, Mathf.Max(triangle.v1.x, triangle.v2.x));
}
if (D == 1)
{
return Mathf.Max(triangle.v0.y, Mathf.Max(triangle.v1.y, triangle.v2.y));
}
//D == 2
return Mathf.Max(triangle.v0.z, Mathf.Max(triangle.v1.z, triangle.v2.z));
}
C:etc。。。
创建三角面的存储结构并构建好包围盒:
public class Triangle
{
public Vector3 v0, v1, v2;
public Bound3 bound3;
public Triangle()
{
v0 = new Vector3();
v1 = new Vector3();
v2 = new Vector3();
bound3 = new Bound3();
}
}
private static List<Triangle> triangles = new List<Triangle>();
for (int i = offset; i < count; i += 3)
{
triangles.Add(new Triangle()
{
v0 = _vertices[_indices[i]],
v1 = _vertices[_indices[i + 1]],
v2 = _vertices[_indices[i + 2]],
bound3 = CreateBounds(_vertices[_indices[i]], _vertices[_indices[i + 1]], _vertices[_indices[i + 2]])
});
}
构建BVH树:
BBNode recursiveBuild(List<Triangle> triangles, int n)
{
BBNode node = new BBNode();
Bound3 bounds = new Bound3();
for (int i = 0; i < triangles.Count; i++)
{
bounds = Union(bounds, triangles[i].bound3);
}
//等于1表示已经划分到了最小节点,存储实际的三角面数据
if (triangles.Count == 1)
{
node.isData = true;
node.bound3 = triangles[0].bound3;
node.triangels = triangles[0];
node.left = null;
node.right = null;
AABB.Add(node.bound3);
//Debug.Log("<<<"+ triangles[0].v0 + " " + triangles[0].v1 + " " + triangles[0].v2 + " ");
//Debug.Log("<<<"+triangles[0].bound3.pMin + " " + triangles[0].bound3.pMax );
//Debug.Log("包围盒到达实际三角形" );
//Debug.Log("两个节点包围盒" + node.bound3.pMin + " " + node.bound3.pMax);
//Debug.Log("两个节点包围盒" + node.right.bound3.pMin + " " + node.right.bound3.pMax);
//Debug.Log("父节点包围盒" + node.bound3.pMin + " " + node.bound3.pMax);
Debug.Log(n);
_bs[n] = GetCCNode(node, n);
return node;
}
//表示只有两个节点即将结束,存储实际的三角面数据
if (triangles.Count == 2)
{
List<Triangle> ll = new List<Triangle>();
ll.Add(triangles[0]);
List<Triangle> rr = new List<Triangle>();
rr.Add(triangles[1]);
node.left = recursiveBuild(ll, n *2);
node.right = recursiveBuild(rr, n*2 + 1);
node.bound3 = Union(node.left.bound3, node.right.bound3);
AABB.Add(node.bound3);
//Debug.Log("包围盒分割22222" + "数量为" + triangles.Count);
//Debug.Log("两个节点包围盒" + node.left.bound3.pMin + " " + node.left.bound3.pMax);
//Debug.Log("两个节点包围盒" + node.right.bound3.pMin + " " + node.right.bound3.pMax);
//Debug.Log("当前节点包围盒" + node.bound3.pMin + " " + node.bound3.pMax);
_bs[n] = GetCCNode(node, n);
return node;
}
//大于三还需要多次划分,只存储包围盒数据
if (triangles.Count > 2)
{
Bound3 centroidBounds = new Bound3();
for (int i = 0; i < triangles.Count; i++)
{
centroidBounds = Union(centroidBounds, Cenroid(triangles[i].bound3));
}
int dim = maxExtent(centroidBounds);
triangles.Sort((a, b) => Cenroid(a, dim).CompareTo(Cenroid(b, dim)));
var begin = triangles.GetRange(0, triangles.Count / 2);
var mid = triangles.GetRange(triangles.Count / 2, triangles.Count - triangles.Count / 2);
List<Triangle> ll = new List<Triangle>();
ll.AddRange(begin);
List<Triangle> rr = new List<Triangle>();
rr.AddRange(mid);
node.left = recursiveBuild(ll, n * 2);
node.right = recursiveBuild(rr, n *2 + 1);
node.bound3 = Union(node.left.bound3, node.right.bound3);
AABB.Add(node.bound3);
//Debug.Log("包围盒分割" + dim+"数量为"+triangles.Count);
//Debug.Log("两个节点包围盒" + node.left.bound3.pMin + " " + node.left.bound3.pMax);
//Debug.Log("两个节点包围盒" + node.right.bound3.pMin + " " + node.right.bound3.pMax);
//Debug.Log("当前节点包围盒"+node.bound3.pMin+" "+node.bound3.pMax);
}
_bs[n] = GetCCNode(node,n);
return node;
}
构建好了树,你就可以进行查询操作
如果放入compute shader还需转入数组(不支持数据结构,所以要进行二叉树树转数组),在我的版本里面我是直接当做完全二叉树来操作的
转化成一维数组
上述方法里面已经通过n进行了一维化,转化成ccnode主要是不支持class
public struct CCNode
{
public int isData;
public int pos;
public Bound33 bound3;
public Triangle3 triangels;
}
CCNode GetCCNode(BBNode bBNode,int i)
{
CCNode cCNode = new CCNode();
cCNode.bound3.pMax = bBNode.bound3.pMax;
cCNode.bound3.pMin = bBNode.bound3.pMin;
if(bBNode.triangels == null)
{
//Debug.Log(bBNode.isData + " 11");
bBNode.triangels = new Triangle();
}
cCNode.triangels.bound3.pMax = bBNode.triangels.bound3.pMax;
cCNode.triangels.bound3.pMin = bBNode.triangels.bound3.pMin;
cCNode.triangels.v0 = bBNode.triangels.v0;
cCNode.triangels.v1 = bBNode.triangels.v1;
cCNode.triangels.v2 = bBNode.triangels.v2;
cCNode.pos = i;
cCNode.isData = bBNode.isData == true ? 1 : 0;
return cCNode;
}
现在我们就已经构建好了bvh树了我们来到对应的compute shader
这里面主要有一个核心算法:
我尝试使用过通过栈模拟,但是GPU会崩溃,因为GPU不适合做这一类型的频繁调转操作:
因为我们使用的是完全二叉树父子关系(下表)会满足:s = 2*f +1的父子关系
所以我们以此来进行二叉树的遍历操作
代码如下:
这里对代码做一个解释:
这里面作图的时候忘记吧矩形换成菱形工作量太大就懒得换了
int GetTT(float3 origin,float3 direction)
{
int head = 1;
while(true)
{
Bound3 bound3 = _Bs[head].bound3;
//pos
bool IsIn = IntersectP(origin,direction,bound3);
if(IsIn == true )//命中当前包围盒
{
if( _Bs[head].isData == 1)//是三角面进行三角面是否相交判断
{
if(MyIntersectTriangle(origin,direction,_Bs[head].tt.v0,_Bs[head].tt.v1,_Bs[head].tt.v2,t,u,v))
{
return head;
}
else//不是则找其他节点
{
while(head %2 == 1 && head != 1)
{
head/=2;
}
if(head == 1)
{
break;
}
head++;
}
}
else
{
head*=2;
}
}
else
{
if(head%2 == 0)
{
head++;
}
else
{
while(head %2 == 1 && head != 1)
{
head/=2;
}
if(head == 1)
{
break;
}
head++;
}
}
}
return 0;
}
后面就是实际的使用的使用了这里在对判断算法做一个补充
与三角面的判断:
struct Ray
{
float3 origin;
float3 direction;
float3 energy;
};
bool IntersectTriangle_MT97(Ray ray, float3 vert0, float3 vert1, float3 vert2,
inout float t, inout float u, inout float v)
{
// find vectors for two edges sharing vert0
float3 edge1 = vert1 - vert0;
float3 edge2 = vert2 - vert0;
// begin calculating determinant - also used to calculate U parameter
float3 pvec = cross(ray.direction, edge2);
// if determinant is near zero, ray lies in plane of triangle
float det = dot(edge1, pvec);
// use backface culling
if (det < EPSILON)
return false;
float inv_det = 1.0f / det;
// calculate distance from vert0 to ray origin
float3 tvec = ray.origin - vert0;
// calculate U parameter and test bounds
u = dot(tvec, pvec) * inv_det;
if (u < 0.0 || u > 1.0f)
return false;
// prepare to test V parameter
float3 qvec = cross(tvec, edge1);
// calculate V parameter and test bounds
v = dot(ray.direction, qvec) * inv_det;
if (v < 0.0 || u + v > 1.0f)
return false;
// calculate t, ray intersects triangle
t = dot(edge2, qvec) * inv_det;
return true;
}
与轴对齐包围盒判断:
bool IntersectP(float3 origin , float3 direction,Bound3 b3)
{
if(b3.pMax.x > -1001&&b3.pMax.x<-999)
{
return false;
}
float3 dir_inv = GetDirection_inv(direction);
float3 ttop = V3Mat((b3.pMax - origin),dir_inv);
float3 tbot = V3Mat((b3.pMin - origin),dir_inv);
float3 tmin = V3Min(ttop,tbot);
float3 tmax = V3Max(ttop,tbot);
float t0 = max(tmin.x , max(tmin.y,tmin.z));
float t1 = min(tmax.x , min(tmax.y,tmax.z));
return t0<=t1 && t1>=0;
//return t1 > 0;
}
两个算的来自看的论文,但是不记得出处了,取自这两个地方
下面那个你可在b站看到,最后讲的很笼统直接贴的完整工程趴,里面还有很多其他代码往谅解:
整个工程是基于三眼游戏的
因篇幅问题不能全部显示,请点此查看更多更全内容