Unity实现GPU光追——Part3 三角形与网格
本文的主要源于http://three-eyed-games.com/2019/03/18/gpu-path-tracing-in-unity-part-3/旨在对于该文章的学习解读,下面正篇开始
一、三角形
三角形是目前计算机中表示模型最常见的单元,它的定义很简单:三个相连顶点的列表,每个顶点都存储了一些信息,如位置、法线、uv等等。三角形的顺序决定了它是正面还是背面,当我们从视角出发,挨个从列表中读取三角形顶点,当它是逆时针缠绕时,则认为是正面。
对于三角形相交测试,我们最重要的当然是判断是否击中三角形,同时我们还想要知道命中点在三角形上的位置,这对我们采样贴图很重要。
相交测试我们依然可以用连列方程的形式,具体可以看这篇文章,我们先来看看射线和三角形的如何表达的
射线
O是射线出发点,t是射线走过的距离,D是射线射向的方向
O +tD
三角形
我们设定三个点分别是V0,V1和V2,uv是V1,V2对三角形上一点的权重,1-u-v是V0的权重,三个权重均在0~1中。我们可以理解为从V0出发,向V1移动了u的距离,向V2移动了v的距离。因此三角形的方程为
(1-u-v)V_0+uV1+vV2
相交测试数学原理
因此求射线与三角形相交就成了求解射线与三角形连列的方程,因为是在三维空间,所以我们有三个方程,可以求解三个未知数t,u,v
O +tD = (1-u-v)V_0+uV1+vV2
我们进行移项,整理为矩阵形式
\begin{bmatrix}-D&V_1-V_0&V_2-V_0\end{bmatrix} \begin{bmatrix}t\\u\\v\end{bmatrix} = O-V_0
令V1-V0 = E1,V2-V0 = E2,O-V0 = T,可得
\begin{bmatrix}-D&E_1&E_2 \end{bmatrix} \begin{bmatrix}t\\u\\v\end{bmatrix} = T
根据克莱姆法则(具体过程可以看此视频)我们可得
\begin{bmatrix}t\\u\\v\end{bmatrix} = \frac{1}{\begin{vmatrix}-D&E_1&E_2\end{vmatrix}} \begin{vmatrix} T&E_1&E_2\\ -D&T&E_2\\ -D&E_1&T\end{vmatrix}
我们可以将行列式改写为混合积的形式,同时通过调转行列式中的位置去除负号,可得
\begin{bmatrix}t\\u\\v\end{bmatrix} = \frac{1}{\begin{bmatrix}D\times E_2\cdot E_1\end{bmatrix}} \begin{vmatrix} T\times E_1\cdot E_2\\ D\times E_2\cdot T \\ T\times E_1\cdot D\end{vmatrix}
为了简化算法,我们令P=D x E2,Q=T x E1,得到最终公式
\begin{bmatrix}t\\u\\v\end{bmatrix} = \frac{1}{\begin{bmatrix}P\cdot E_1\end{bmatrix}} \begin{vmatrix} Q\cdot E_2\\ P\cdot T \\ Q\cdot D\end{vmatrix}
当满足 t>0 ,1>u,v,1-uv>0 时发生相交
相关代码
对于上述数学原理,我们可以将其移植到shader之中
//三角形测试方法
bool IntersectTriganle_MT97(Ray ray,float3 vert0,float3 vert1,float3 vert2,
inout float t,inout float u,inout float v)
{
//从v0指向v1,v2的两条向量
float3 edge1 = vert1 - vert0;
float3 edge2 = vert2 - vert0;
//获得P向量
float3 P_vec = cross(ray.direction,edge2);
//计算方程参数行列式
float det = dot(edge1,P_vec);
//进行背面剔除
if(det < EPSILON)
return false;
float inv_det = 1.0f / det;
float3 T_vec = ray.origin - vert0;
//计算u值参数并测试
u = dot(T_vec,P_vec) * inv_det;
if(u < 0.0 || u > 1.0f)
return false;
float3 Q_vec = cross(T_vec,edge1);
//计算v值参数并测试
v = dot(ray.direction, Q_vec) * inv_det;
if(v < 0.0 || u+v > 1.0f)
returnfalse;
t = dot(edge2,Q_vec) * inv_det;
return true;
}之后,在Trace函数中创建一个三角形并追踪
//跟踪单个三角形
float3 v0 = float3 ( - 150 , 0 , - 150 ) ;
float3 v1 = float3 ( 150 , 0 , - 150 ) ;
float3 v2 = float3 ( 0 , 150 * sqrt ( 2 ) , - 150 ) ;
float t,u,v;
u=0;
v=0;
if(IntersectTriganle_MT97(ray,v0,v1,v2,t,u,v))
{
if(t > 0 && t < bestHit.distance)
{
bestHit.distance = t;
bestHit.position = ray.origin + t * ray.direction;
bestHit.normal = normalize ( cross ( v1 - v0, v2 - v0 ) ) ;
bestHit.albedo = 0.00f;
bestHit.specular = 0.65f * float3 ( 1 , 0.4f, 0.2f ) ;
bestHit.smoothness = 0.9f;
bestHit.emission = 0.0f;
}
}我们可以看到如下效果
二、引入游戏对象
添加对象
首先我们需要确认所有我们需要进入光追渲染的对象,因此我们创建一个新的脚本RayTracingObject
同时,为了管理这些对象,我们创建ObjectTracingManager
在RayTracingObject中,在启用和关闭脚本时,分别进行注册和注销
public class RayTracingObject : MonoBehaviour
{
private void OnEnable()
{
ObjectTracingManager.RegisterObject(this);
}
private void OnDisable()
{
ObjectTracingManager.UnregisterObject(this);
}
}
在Manager脚本中,每次出现新注册的对象,就将其加入列表
public class ObjectTracingManager : MonoBehaviour
{
private static bool _meshObjectsNeedRebuilding = false;
private static List<RayTracingObject> _rayTracingObjects = new List<RayTracingObject>();
public static void RegisterObject(RayTracingObject obj)
{
_rayTracingObjects.Add(obj);
_meshObjectsNeedRebuilding = true;
}
public static void UnregisterObject(RayTracingObject obj)
{
_rayTracingObjects.Remove(obj);
_meshObjectsNeedRebuilding = true;
}
}目前为止,我们已经可以得到需要追踪的对象。现在来到了关键的部分,我们还需要获得这些网格的其他数据,比如矩阵、顶点和索引缓冲区。将他们放入我们自己的数据结构中,并传递给GPU,一边着色器可以使用他们。让我们在C#端的数据结构和缓冲区定义开始,在Manager中
struct MeshObject
{
public Matrix4x4 localToWorldMatrix;
public int indices_offset;
public int indices_count;
}
private static List<MeshObject> _meshObjects = new List<MeshObject>();
private static List<Vector3> _vertices = new List<Vector3>();
private static List<int> _indices = new List<int>();
private ComputeBuffer _meshObjectBuffer;
private ComputeBuffer _vertexBuffer;
private ComputeBuffer _indexBuffer;同样,我们在着色器中也需要定义它们
struct MeshObject
{
float4x4 localToWorldMatrix;
int indices_offset;
int indices_count;
};
StructuredBuffer<MeshObject> _MeshObjects;
StructuredBuffer<float3> _Vertices;
StructuredBuffer<int> _Indices;现在数据结构已经到位,我们可以用实际数据填充它们,我们将网格的所有顶点收集到一个三维向量列表中,将索引收集到整型列表中。同时,我们需要调整索引,以便索引值可以指向后添加的顶点。
比如我们已经添加了1000个顶点的对象,现在我们需要添加一个简单的立方体网格。我们的第一个三角包含的索引可能为,但由于顶点列表已经有了1000个顶点,我们需要移动索引,因此实际的索引值为,我们将其写在Manager中。
别忘了将RayTracingMaster中的_currentSample变为静态公共变量
public static void RebuildMeshObjectBuffer()
{
if (!_meshObjectsNeedRebuilding)
{
return;
}
_meshObjectsNeedRebuilding = false;
RayTracingMaster._currentSample = 0;
//清空List
_meshObjects.Clear();
_vertices.Clear();
_indices.Clear();
foreach (RayTracingObject obj in _rayTracingObjects)
{
Mesh mesh = obj.GetComponent<MeshFilter>().sharedMesh;
//添加顶点数据
int firstVertex = _vertices.Count;
_vertices.AddRange(mesh.vertices);
//生成AABB包围盒
Vector3 AABBmax = mesh.vertices;
Vector3 AABBmin = mesh.vertices;
for (int i = 0; i < mesh.vertices.Length; i++)
{
UpdateAABB(ref AABBmax,ref AABBmin,mesh.vertices);
}
//添加索引数据,如果不是第一个网格,需要进行偏移
int firstIndex = _indices.Count;
var indices = mesh.GetIndices(0);
//根据当前mesh在顶点buffer的位置进行偏移
_indices.AddRange(indices.Select(index => index + firstVertex));
//添加网格数据
_meshObjects.Add(new MeshObject()
{
localToWorldMatrix = obj.transform.localToWorldMatrix,
indices_count = indices.Length,
indices_offset = firstIndex,
AABBmax = AABBmax,
AABBmin = AABBmin,
albedo = obj.albedo,
specular = obj.specular,
smoothness = obj.smoothness,
emission = obj.emission,
opactiy = obj.opacity,
refractivity = obj.refractivity
});
CreateComputeBuffer(ref _meshObjectBuffer, _meshObjects, 144);
CreateComputeBuffer(ref _vertexBuffer, _vertices, 12);
CreateComputeBuffer(ref _indexBuffer, _indices, 4);
}
}
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
SetShaderParameters();
Render(dest);
ObjectTracingManager.RebuildMeshObjectBuffer();
}在光追的Master类的OnRenderImage中调用RebuildMeshObjectBuffers,同时在OnDisable中释放新缓冲区,以下是两个工具函数,使得对缓冲区的处理更容易
private static void CreateComputeBuffer<T>(ref ComputeBuffer buffer, List<T> data, int stride)
where T : struct
{
//如果已有buffer
if (buffer != null)
{
//如果buffer和data不一致,则释放data
if (data.Count == 0 || buffer.count != data.Count || buffer.stride != stride)
{
buffer.Release();
buffer = null;
}
}
if (data.Count != 0)
{
//如果buffer被释放了就初始化
if (buffer == null)
{
buffer = new ComputeBuffer(data.Count, stride);
}
//设置数据
buffer.SetData(data);
}
}
//判断null并传递给GPU
private void SetComputeBuffer(string name, ComputeBuffer buffer)
{
if (buffer != null)
{
RayTracingShader.SetBuffer(0, name, buffer);
}
}在我们有了缓冲区,并且填装了数据,我们只需将其交给着色器,在SetShaderParameters中,添加以下代码
//设置Mesh
SetComputeBuffer(&#34;_Spheres&#34;, _sphereBuffer);
SetComputeBuffer(&#34;_MeshObjects&#34;, ObjectTracingManager._meshObjectBuffer);
SetComputeBuffer(&#34;_Vertices&#34;, ObjectTracingManager._vertexBuffer);
SetComputeBuffer(&#34;_Indices&#34;, ObjectTracingManager._indexBuffer);追踪网格
Shader中已经有了单个三角形的代码,而网格本质就是一堆新的三角形。这里唯一新的内容就是我们需要使用物体-世界矩阵将顶点变换到世界空间,由于我们处理的是顶点而非向量,因此我们将第四个分量设置为1
void IntersectMeshObject(Ray ray, inout RayHit bestHit, MeshObject meshObject)
{
uint offset = meshObject.indices_offset;
uint count = offset + meshObject.indices_count;
for (uint i = offset; i < count; i += 3)
{
float3 v0 = (mul(meshObject.localToWorldMatrix, float4(_Vertices], 1))).xyz;
float3 v1 = (mul(meshObject.localToWorldMatrix, float4(_Vertices], 1))).xyz;
float3 v2 = (mul(meshObject.localToWorldMatrix, float4(_Vertices], 1))).xyz;
float t, u, v;
if (IntersectTriangle_MT97(ray, v0, v1, v2, t, u, v))
{
if (t > 0 && t < bestHit.distance)
{
bestHit.distance = t;
bestHit.position = ray.origin + t * ray.direction;
bestHit.normal = normalize(cross(v1 - v0, v2 - v0));
bestHit.albedo = 0.0f;
bestHit.specular = 0.65f;
bestHit.smoothness = 0.99f;
bestHit.emission = 0.0f;
}
}
}
}让我们再重构下Trace函数即可完成
//追踪的框架
RayHit Trace(Ray ray)
{
RayHit bestHit = CreateRayHit();
IntersectGroundPlane(ray,bestHit);
uint count,stride,i;
// _Spheres.GetDimensions(count,stride);
// for(i = 0 ; i < count ; i++)
// {
// IntersectSphere(ray,bestHit,_Spheres);
// }
_MeshObjects.GetDimensions(count, stride);
for (i = 0; i < count; i++)
{
IntersectMeshObject(ray, bestHit, _MeshObjects);
}
return bestHit;
}现在我们引入一个Mesh,给它挂上RayTracingObject组件,即可渲染这个物体,不过记得不太使用太多的三角面,因为我们的系统还没进行过优化
三、后续的一些修改
我们可以参照之前球体的内容,将材质属性一并放入Mesh中,首先,我们在两边都修改Mesh的结构体,同时别忘了重新设置Buffer的步长
struct MeshObject
{
public Matrix4x4 localToWorldMatrix;
public int indices_offset;
public int indices_count;
public Vector3 AABBmax;
public Vector3 AABBmin;
public Vector3 albedo;
public Vector3 specular;
public floatsmoothness;
public Vector3 emission;
}
之后在Shader的IntersectMeshObject中,将传递给命中点的数据改为Mesh的数据
bestHit.distance = t;
bestHit.position = ray.origin + t * ray.direction;
bestHit.normal = normalize(cross(v1 - v0, v2 - v0));
bestHit.albedo = meshobject.albedo;
bestHit.specular = meshobject.specular;
bestHit.smoothness = meshobject.smoothness;
bestHit.emission = meshobject.emission;当挂载了RayTracingObject的物体移动时,我们也需要重新渲染
//RayTracingObject
private void Update()
{
if (this.transform.hasChanged)
{
ObjectTracingManager.RefreshObjects();
this.transform.hasChanged = false;
}
}
//RayTracingMaster
public static void RefreshObjects()
{
_meshObjectsNeedRebuilding = true;
}
页:
[1]