xiaozongpeng 发表于 2023-1-29 12:13

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("_Spheres", _sphereBuffer);
      SetComputeBuffer("_MeshObjects", ObjectTracingManager._meshObjectBuffer);
      SetComputeBuffer("_Vertices", ObjectTracingManager._vertexBuffer);
      SetComputeBuffer("_Indices", 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]
查看完整版本: Unity实现GPU光追——Part3 三角形与网格