本篇中描述在Unity中创建RayTracing渲染路径。

参考:
HDRP中的HDRaytracingRecursiveRenderer.cs中的RaytracingRecursiveRender方法
RaytracingRenderer.raytrace
知乎-洛城:一篇光线追踪的入门
Unity中国官方:Unity实时光线追踪技术介绍
GPU Ray Tracing in One Weekend

简述RayTracing


从相机位置射向屏幕上一颗像素,与GameObject相交并反射至光源;
光会衰减、折射;可能需要反弹多次才会遇到光源,要决定最大碰撞次数。


BVH算法(Bounding volume hierarchy)
场景中每个GameObject都有自己的包围盒(bounding box);
将多个物体编成一组,得到更大的包围盒;
先使用最外层的包围盒进行碰撞检测,通过测试再进行下一级测试;
在示例图片中,兔子模型被继续细分为多个包围盒;
Ray在前进方向上可能穿透物体、与多个物体相交,取深度值最低的结果;
在逐级测试的最后阶段,Ray与mesh表面的一个三角形相交,获得表面参数。

整个渲染流程分多个步骤完成。
1.判断一次相交
Ray彼此不知道对方,但知道整个场景的信息。
Ray与场景相交于一三角形,获得表面参数。
2.根据剩余的碰撞次数继续进行测试
Ray通过反射、折射,继续与场景进行碰撞检测;
如果撞到了光源,光源强度可作为光照的输入;
如果撞到了其他物体,对亮度没有直接增益;
3.着色阶段
逐屏幕像素根据累积的表面参数计算最终颜色。

GPGPU
Compute Shader用于在GPU中执行大规模并行运算;
这些运算和光栅化shader一样,可以对RT进行读写、图形数学计算;
使用Compute Shader定制的着色方案可以称作计算管线(Compute Pipeline)。

设备限制
本篇中讲到的HDRP-RayTracing,利用到了dx12的实验性api,也就是DXR;
需要显卡支持;没有显卡的话,无法执行shader,但是可以看看计算过程。

基于GPGPU的追踪计算

Unity在HDRP中为RayTracing提供了新的api:
cmd.DispatchRays(rayTracingShader, rayGenName, width, height, depth);
rayTracingShader:负责追踪计算用的Compute Shader程序
rayGenName:在rayTracingShader中,用于生成Ray向量、碰撞检测、记录结果的方法;
width/height:用视口的像素宽x高作为并行kernel数;
depth:眼睛数量,针对VR的双眼渲染;
以这个api为入口,我们可以挖掘出整个Ray Tracing的轮廓。

在参考代码方面,虽然可以直接看HDRP,但是那边的代码太复杂了;
新人学习一步步制作效果的话,建议看GPU Ray Tracing in One Weekend。

RayTracingAccelerationStructure(加速体)
在提交RayTracing的Draw call之前,需要先将场景中的几何体传递给GPU。
RayTracingAccelerationStructure用于描述场景的一帧的状态,核心思想是以最快的速度去决定场景中哪些物体会与特定的光线相交,并以此来避免计算那些不必要进行求交的测试。
该类型的api:
自构:new RayTracingAccelerationStructure();
AddInstance:向加速体中添加renderer组件;
Build:在GPU上构建加速体;

RayTracingShader
一种新的shader类型,资源文件名以.raytrace结尾;
在新shader中,可使用专用的api写逻辑;
rayGenName指定的方法有[shader("raygeneration")]属性。

输出颜色

对RT的读写,相当于渲染界的Hello World,对RayTracing管线也是一样:

uint2 dispatchIdx = DispatchRaysIndex().xy; //视口的像素位置 左下角为(0,0)
uint2 dispatchDim = DispatchRaysDimensions().xy; //视口的像素宽度和像素高度

_OutputTarget[dispatchIdx] = float4((float)dispatchIdx.x / dispatchDim.x, (float)dispatchIdx.y / dispatchDim.y, 0.0f, 1.0f);

本例中没有光照计算,直接将固定的颜色值写入RT。

生成Ray向量

步骤一:像素的NDC坐标
float2 xy = DispatchRaysIndex().xy + 0.5f;
float2 ndcPos = xy / DispatchRaysDimensions().xy; //positionNDC
ndcPos = ndcPos * 2.0f - 1.0f; //从屏幕空间转换为NDC空间
步骤二:使用NDC坐标+深度 重建世界坐标

//DX平台Z轴的远平面Z值为0
float4 world = mul(_InvCameraViewProj, float4(ndcPos, 0, 1));
world.xyz /= world.w; //参考Common.hlsl中的ComputeWorldSpacePosition方法(仿射变换)

步骤三:获得Ray向量(朝向远平面一点的世界空间位置)
float3 direction = normalize(world.xyz - _WorldSpaceCameraPos.xyz);

计算中用到的变量需要在C#中赋值:

Shader.SetGlobalVector(CameraShaderParams._WorldSpaceCameraPos, camera.transform.position);
var projMatrix = GL.GetGPUProjectionMatrix(camera.projectionMatrix, false); //不反转Y
var viewMatrix = camera.worldToCameraMatrix;
var viewProjMatrix = projMatrix * viewMatrix;
var invViewProjMatrix = Matrix4x4.Inverse(viewProjMatrix);
Shader.SetGlobalMatrix(CameraShaderParams._InvCameraViewProj, invViewProjMatrix);

TraceRay

的到了Ray向量之后,就需要尝试与场景碰撞,这里使用DXR的api,TraceRay方法:
void TraceRay(RaytracingAccelerationStructure AccelerationStructure,
uint RayFlags,
uint InstanceInclusionMask,
uint RayContributionToHitGroupIndex,
uint MultiplierForGeometryContributionToHitGroupIndex,
uint MissShaderIndex,
RayDesc Ray,
inout payload_t Payload);
下面是参数解释:
1.AccelerationStructure:加速体,在一帧开始前要构建好的场景数据;
2.RayFlags:类似于CullMode,默认的剔除背面选项是RAY_FLAG_CULL_BACK_FACING_TRIANGLES;
注:在碰撞测试的最后阶段,Ray可能与三角形的正面/背面相交;
3.InstanceInclusionMask:用于屏蔽部分mesh实例,~0表示渲染所有mesh实例
4.RayContributionToHitGroupIndex:Ray命中三角形时,调用的物体shader中的方法的索引;
5.MultiplierForGeometryContributionToHitGroupIndex:?
6.MissShaderIndex:没命中时,Miss方法索引;
7.Ray:对Ray的初始描述,起点、向量、最小长度、最大长度;
8.Payload:用户自定义数据,用于在RayTracing过程中传递数据,SV_RayPayload;
根据不同的碰撞结果,对Payload传递数据;多次碰撞时递归调用TraceRay方法;
在TraceRay方法返回时,也意味着Ray的完整追踪过程已经结束,可从Playload访问结果。


TraceRay方法包揽了主要的业务逻辑:
遍历加速体,逐级循环碰撞测试;
未触发碰撞时,调用全局的Miss方法;
触发了任何碰撞时,需要判断是否继续搜索,调用全局的Any Hit方法;
一轮碰撞测试成功时,需要向Payload传递数据,调用逐mesh的shader的Closest Hit方法;

Miss方法 [shader("anyhit")]
可自定义着色方案,比如采样天空盒子;

Any Hit方法 [shader("anyhit")] 不是必要的
AcceptHitAndEndSearch():停止遍历加速体,使用已有的碰撞结果;

逐mesh的Closest Hit shader写法注意点:
使用独单独的SubShader用于管理RayTracing shader;
Pass中的LightMode设置,与SetRayTracingShaderPass命令一致时才生效;
#pragma raytracing xxx :标记Pass为RayTracing用;
在shader中可以访问SV_RayPayload和SV_IntersectionAttributes(三角形的重心坐标);
有且只能有一个Closest Hit shader;
可以在closesthit方法中根据剩余反弹次数继续调用TraceRay方法;

SRP中指定用于RayTracing的shader的lightmode标签:
cmd.SetRayTracingShaderPass(_shader, "RayTracing");

获取三角形相关数据

对于逐物体的材质来说,进行光照计算是基础功能;
也就是说,要得到Ray与三角形碰撞处的变量,用于光照模型中的计算;
三角形有3个点,对应3组逐顶点数据,碰撞点根据3个点的权重混合属性;

//获取三角形的三个顶点的序列值
uint3 triangleIndices = UnityRayTracingFetchTriangleIndices(PrimitiveIndex());

//定义三组顶点数据 结构体中声明要取用的参数
IntersectionVertex v0, v1, v2;

//向结构体中填充顶点数据
FetchIntersectionVertex(triangleIndices.x, v0);
FetchIntersectionVertex(triangleIndices.y, v1);
FetchIntersectionVertex(triangleIndices.y, v2);

//计算碰撞点分别占3个点的权重
float3 barycentricCoordinates = float3(1.0 - attributeData.barycentrics.x - attributeData.barycentrics.y, attributeData.barycentrics.x, attributeData.barycentrics.y);

//以法线属性为例 计算碰撞点的法线
float3 normalOS = INTERPOLATE_RAYTRACING_ATTRIBUTE(v0.normalOS, v1.normalOS, v2.normalOS, barycentricCoordinates);

//使用逐物体的o2w矩阵(DXR builtin) 将法线转换到世界空间
float3x3 objectToWorld = (float3x3)ObjectToWorld3x4();
float3 normalWS = normalize(mul(objectToWorld, normalOS));

抗锯齿

上面的计算中,我们使用一个像素的中心的采样结果作为最终渲染结果,锯齿很严重;
整个像素(宽度为1的正方形)可以生成无数个Ray,可以适量的,通过多次采样混合结果;
但是要保证采样点均匀分布,可以通过指定位置数组、随机均匀分布算法等实现。

光照计算

如果Ray命中了物体,就可以通过逐物体的shader,访问shader中的光照模型;
但是无法直接得到最终着色,因为我们还缺少光源;
想要得到光源(或者Sky Box),就需要在closesthit方法中继续执行TraceRay方法;
也就是,在SV_RayPayload结构中添加剩余反弹次数限制。

float3 origin = WorldRayOrigin(); //获取Ray的起点
float3 direction = WorldRayDirection(); //获取Ray的向量
float distance = RayTCurrent()://返回当前碰撞点与出发点直接的距离;
float3 positionWS = origin + direction * distance; //计算新的起点
//漫反射物体的表面随机反射
//float3 reflectDir = normalize(normalWS +GetRandomOnUnitSphere(rayIntersection.PRNGStates));
//镜面物体的表面镜面反射
float3 reflectDir = reflect(direction, normalWS); //计算反射方向
//根据漫反射程度混合反射方向
reflectDir = reflectDir + _Fuzz * GetRandomOnUnitSphere(rayIntersection.PRNGStates)
//折射方向。。。

光源

平行光/环境光这种大自然中广范围存在的光,通过Miss shader实现;
电灯泡、电杆等体积光源使用mesh,用closesthit,手动计算衰减:
rayIntersection.color = float4(lightColor.rgb * _Intensity, 1.0f);

目前来说,DXR技术还不成熟,只是简单了解一下,等以后再开发。


关注成长,注重因果。