![582d588cfb39387d6c41cdd8be827dbf.png](https://i-blog.csdnimg.cn/blog_migrate/62e431e195898197ee9945585c3524e2.jpeg)
GPU Ray Tracing in One Weekend by Unity 2019.3
Version 1.0, 2019-Oct-23
Copyright 2019. ZHing. All rights received.
目录:
- 1. 综述
- 1.1. 准备工作环境
- 1.2. 利用SPR调用DXR
- 1.3. 本文所使用SRP框架介绍
- 2. 输出图像
- 2.1. 在Unity中创建RayTraceShader
- 2.2. C#中在SRP管线
- 2.3. 最终输出
- 3. 输出背景
- 3.1. 在Unity中创建RayTraceShader
- 3.2. C#代码
- 3.3. 最终输出
- 4. 渲染Sphere
- 4.1. 在Unity中创建RayTraceShader
- 4.2. 创建球体Shader
- 4.3. C#代码
- 4.4. 最终输出
- 5. 法线输出
- 5.1. 创建球体Shader
- 5.2. 最终输出
- 6. Antialiasing
- 6.1. 在Unity中创建RayTraceShader
- 6.2. C#代码
- 6.3. 最终输出
- 7. Diffuse材质
- 7.1. RayTrace Shader
- 7.2. 创建物体Shader及其Material
- 7.3. 最终输出
- 8. Dielectrics材质
- 8.1. 物体ClosestHitShader
- 8.2. 最终输出
- 9. 失焦模糊
- 9.1. C#代码
- 9.2. RayTrace Shader
- 9.3. 最终输出
- 10. 全部放到一起
1. 综述
本文基于 https://raytracing.github.io 的《Ray Tracing in One Weekend》,介绍如何使用Unity 2019.3、SRP和DXR实现Ray Tracing。 因此在阅读本文之前,需要首先阅读《Ray Tracing in One Weekend》。本文不会对《Ray Tracing in One Weekend》文中已经解释清楚的算法做重复解释。本文中提到的“原文”均指“Ray Tracing in One Weekend”。
本文的重点放在如何在Unity 2019.3中实现同原文一样的Ray Tracing渲染。
由于使用了基于GPU加速的DXR,因此本文的所有案例渲染速度比原文要快得多得多,但前提需要支持DXR的硬件和系统来运行。
此外由于目前Unity 2019.3目前集成的DXR并不支持Intersection Shader,因此无法想原文中一样使用Procedural Geometry,将使用Mesh代替。
1.1. 准备工作环境
本文所述实现,基于Unity 2019.3集成的DXR。因此需要对Unity工程进行基本的设置。
![2da50427df243dbed7a85b64755fcd6d.png](https://i-blog.csdnimg.cn/blog_migrate/558c743150347472e4e9134669f71c96.png)
设置Graphics API为Direct3D12为第一项
1.2. 利用SPR调用DXR
因为使用了SPR和SIMD数学库,因此需要至少导入如下Unity Package。
![cfe53c227ef3f1efc062fb7c73fb1e8f.png](https://i-blog.csdnimg.cn/blog_migrate/03248d5e038b7d2f10be5b9033811e97.png)
![9e39acc903c6145410a58f3f3bbbe14d.png](https://i-blog.csdnimg.cn/blog_migrate/b8cae319edb936c842a201ed3e87af43.png)
1.3. 本文所使用SRP框架介绍
本文所有例程都基于一个最简SRP框架实习,详细请参看工程源代码,这里只做简单介绍。
RayTracingRenderPipelineAsset继承自RenderPipelineAsset,SRP的核心类,用于创建RayTracingRenderPipeline。
RayTracingRenderPipeline继承自RenderPipeline,SRP的核心类。Render函数为整个渲染流程的入口。
RayTracingTutorialAsset为各个例程资源的基类。继承自ScriptableObject,保存了各个例程所需要用到的Shader引用。
RayTracingTutorial为各个例程的基类。由RayTracingRenderPipeline驱动进行各个例程的渲染工作。
2. 输出图像
例程类:OutputColorTutorial
场景文件:1_OutputColorTutorialScene
利用Ray Tracing输出图像,可谓Ray Tracing中的Hello World。
2.1. 在Unity中创建RayTraceShader
![9788e1cec959929a30b7e3738d3ae25b.png](https://i-blog.csdnimg.cn/blog_migrate/8122eea99dfb61e10cbdcc21a9b58aa3.png)
#pragma max_recursion_depth 1
DispatchRaysIndex().xy用于返回当前像素的位置
DispatchRaysDimensions().xy用于返回当前渲染目标的尺寸
以上代码功能为从左到右将像素的R通道从0到1变化,从下到上将像素的G通道从0到1变化,所有像素的B通道保持0.2不变。
2.2. C#中在SRP管线
var
RequireOutputTarget函数根据当前将要渲染的相机设置获取对应的渲染目标。详细实现参看本文所附工程源代码。
cmd.SetRayTracingTextureParam用于将渲染目标设置到以上RayTrace Shader中。_shader为Tracing Shader对象。
参数_outputTargetShaderId通过Shader.PropertyToID("_OutputTarget")方式得到,本文后续都通过此方法获取将不再赘述。
cmd.DispatchRays用于调用之前RayTrace Shader中的raygeneration函数OutputColorRayGenShader进行Ray Tracing。
由于RayTrace Shader只能渲染到RenderTarget上,在屏幕上并看不到渲染结果,因此最后需要进行一次Blit操作将渲染结果绘制到屏幕上。后续渲染都有此步骤将不再赘述。
2.3. 最终输出
![dd8e9b3c2a5e87ac03bb34ee843a0a77.png](https://i-blog.csdnimg.cn/blog_migrate/7c73da6ad482c9e24237eb45783cc598.jpeg)
3. 输出背景
例程类:BackgroundTutorial
场景文件:2_BackgroundTutorialScene
利用Ray Tracing输出渐变背景
3.1. 在Unity中创建RayTraceShader
inline
GenerateCameraRay用于在当前像素位置构建一条光线的Origin和Direction,此处和原文稍有不同。
首先计算出屏幕坐标在Project Space中的位置screenPos。然后利用C#中计算好的_InvCameraViewProj矩阵将其变换到World Space从而获得光线方向direction。origin直接通过C#中传入的_WorldSpaceCameraPos获得。
Color函数和原文一致,用于计算从上到下的渐变。
3.2. C#代码
C#中设置相机参数
Shader
设置_WorldSpaceCameraPos和_InvCameraViewProj。
C#中在SRP管线中的代码如下。
var
此处和之前不同之处仅仅为调用RayTrace Shader不同。
3.3. 最终输出
![d8ff35448bcba7d6904417ce15bfa8f9.png](https://i-blog.csdnimg.cn/blog_migrate/4d8e83aa9d6ed42312b7fbeb2c3202bf.png)
4. 渲染Sphere
例程类:AddASphereTutorial
场景文件:3_AddASphereTutorialScene
利用Ray Tracing绘制一个球体。注意,由于Unity目前集成的DXR并不支持Intersection Shader,因此无法像原文中一样使用Procedural Geometry。改用一个球体Mesh进行绘制,对应FBX文件已放入源代码项目中。
4.1. 在Unity中创建RayTraceShader
struct
在raygeneration shader中调用了TraceRay函数发射光线,此函数的用法请参考Microsoft DXR文档。这里只解释一下RayDesc和RayIntersection结构,RayDesc为DXR中描述一条光线的数据结构,Origin为光线的起点,Direction为光线的方向,TMin为t的最小值,TMax为t的最大值(代码中为相机远裁剪面距离)。Ray Intersection结构为用户自定义的Ray Payload数据结构,用于在Ray Tracing过程中传递数据。此处定义的color用于返回Ray Trace的结果颜色。
此处增加了miss shader。miss shader将在没有任何光线碰撞被检测到时执行,此处调用BackgroundColor返回第3节中的渐变背景色。
4.2. 创建球体Shader
在Unity中创建一个普通的shader文件,在其末尾添加如下代码。
SubShader
{
Pass
{
Name "RayTracing"
Tags { "LightMode" = "RayTracing" }
HLSLPROGRAM
#pragma raytracing test
struct RayIntersection
{
float4 color;
};
CBUFFER_START(UnityPerMaterial)
float4 _Color;
CBUFFER_END
[shader("closesthit")]
void ClosestHitShader(inout RayIntersection rayIntersection : SV_RayPayload, AttributeData attributeData : SV_IntersectionAttributes)
{
rayIntersection.color = _Color;
}
ENDHLSL
}
}
此处定义了另一个SubShader并在其中增加了一个名为RayTracing的Pass。Pass名字可以随意定义,后续C#中将会看到具体指定Pass的代码。
#pragma raytracing test表示这是一个Ray Trace Pass。
closesthit shader ClosestHitShader接收两个参数,第一个参数rayIntersection为之前传递的Ray Payload数据,第二个参数attributeData为发生碰撞的相关数据,本例并未使用,仅仅简单的返回一个颜色数据。注意:一个RayTracing Pass中有且只能有一个closesthit shader,Unity 2019.3目前并不支持多hit group。关于hit group参看Microsoft DXR文档。
创建好Shader后建立相应的Material,并在场景中创建一个球体附上相应的Material。
4.3. C#代码
C#中设置相机参数同第3节内容。
C#中在SRP管线中的代码如下。
var
本例中由于需要对球体进行Ray Trace检测,因此使用到了加速结构。RequestAccelerationStructure用于获取加速结构对象,通过SetRayTracingAccelerationStructure作为参数传递到Ray Trace Shader中。关于Unity中加速结构的使用,参看:Unity Scripting API - RayTracingAccelerationStructure
SetRayTracingShaderPass用于设定当前Ray Tracing所需要执行的Pass,既4.2节中指定的RayTracing Pass。
4.4. 最终输出
![6206023eaf2347c60a399c3dd53945b1.png](https://i-blog.csdnimg.cn/blog_migrate/60e14d15d9a091b13b336937a2cb1083.png)
5. 法线输出
例程类:AddASphereTutorial
场景文件:4_SurfaceNormalTutorialScene
本例在第4节例程的基础上修改而来,仅仅修改了物体本身的Shader。
5.1. 创建球体Shader
#include "UnityRaytracingMeshUtils.cginc"
struct RayIntersection
{
float4 color;
};
struct IntersectionVertex
{
// Object space normal of the vertex
float3 normalOS;
};
void FetchIntersectionVertex(uint vertexIndex, out IntersectionVertex outVertex)
{
outVertex.normalOS = UnityRayTracingFetchVertexAttribute3(vertexIndex, kVertexAttributeNormal);
}
[shader("closesthit")]
void ClosestHitShader(inout RayIntersection rayIntersection : SV_RayPayload, AttributeData attributeData : SV_IntersectionAttributes)
{
// Fetch the indices of the currentr triangle
uint3 triangleIndices = UnityRayTracingFetchTriangleIndices(PrimitiveIndex());
// Fetch the 3 vertices
IntersectionVertex v0, v1, v2;
FetchIntersectionVertex(triangleIndices.x, v0);
FetchIntersectionVertex(triangleIndices.y, v1);
FetchIntersectionVertex(triangleIndices.z, v2);
// Compute the full barycentric coordinates
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);
float3x3 objectToWorld = (float3x3)ObjectToWorld3x4();
float3 normalWS = normalize(mul(objectToWorld, normalOS));
rayIntersection.color = float4(0.5f * (normalWS + 1.0f), 0);
}
UnityRayTracingFetchTriangleIndices为Unity实现的Utils函数,用于根据PrimitiveIndex返回的索引信息获取Ray Tracing碰撞三角形的的索引值。
IntersectionVertex结构定义了我们所关心的Ray Tracing碰撞三角形的顶点信息。
FetchIntersectionVertex用于填充IntersectionVertex数据,其内部调用的UnityRayTracingFetchVertexAttribute3函数为Unity实现的Utils函数,用于获取对应的顶点数据。此例获取了Object Space的顶点Normal数据。
INTERPOLATE_RAYTRACING_ATTRIBUTE用于在碰撞三角形的三个顶点中插值计算出碰撞点的具体信息。
ObjectToWorld3x4为DXR builtin函数,用于将顶点数据从Object Space变换到World Space。
最后将最终得到的World Space中的normalWS输出到颜色。
5.2. 最终输出
![0dfe24216350b710472420bc1021137d.png](https://i-blog.csdnimg.cn/blog_migrate/83a85927b6f002e36efb4ba6ead549f8.png)
6. Antialiasing
例程类:AntialiasingTutorial
场景文件:5_AntialiasingTutorialScene
如果放大第5节中的最终输出图像会发现存在很严重的锯齿问题。
![b21860a6b2f5db82c3d17ca05458bd7f.png](https://i-blog.csdnimg.cn/blog_migrate/59db1d77ad2607de79b28837a82fc269.png)
原文中处理此问题采用在一个像素内多次采样然后取平均值的方法,在DXR中如果使用同样方法将导致绘制一帧的时间大大增加,变得卡顿。因此在此例中使用Accumulate Average Sample的方法。
6.1. 在Unity中创建RayTraceShader
struct
GenerateCameraRayWithOffset用于给该像素产生的光线做一个偏移操作,偏移值由GetRandomValue得到。
GetRandomValue获取随机数的方法采用了《GPU Gems 3》中“Chapter 37. Efficient Random Number Generation and Application Using CUDA”节的方法,具体实现可参看GPU Gems 3和本项目源代码。需要特别指出的是,GPU进行Ray Tracing时是多个像素并行计算的,因此随机数产生器的States需要每个像素相对独立。因此在RayIntersection数据结构中增加了PRNGStates用于保存随机数产生器的状态。C#中在RayTracingRenderPipeline中的RequirePRNGStates函数对其进行初始化,具体过程参看源代码。
_FrameIndex为当前渲染到第几帧的索引值,如果此值大于1则将当前帧的数据和之前的数据进行累加平均操作,否则直接将当前帧数据写入渲染目标中。
6.2. C#代码
var
RequireOutputTargetSize用于获取当前渲染目标的大小。
RequirePRNGStates用于获取随机数生成器状态Buffer。
_frameIndex为当前渲染到第几帧的索引值,累积到1000后停止渲染。
6.3. 最终输出
![ef2c0a535a2d517afb1a1ac263519f6c.png](https://i-blog.csdnimg.cn/blog_migrate/b83495ac8b859678c1da904a8d997b74.png)
7. Diffuse材质
例程类:AntialiasingTutorial
场景文件:6_DiffuseTutorialScene
关于Diffuse的实现参考原文。
7.1. RayTrace Shader
代码基本同前一节相同,区别在于Ray Payload增加了remainingDepth字段,标识当前光线还剩余几次递归可用,最大递归次数由MAX_DEPTH设定。注意:MAX_DEPTH值需要小于max_recursion_depth定义的值减1。
RayIntersection
7.2. 创建物体Shader及其Material
ClosestHitShader代码如下
[
计算normalWS的方法和前一节相同,不再赘述。
当rayIntersection.remainingDepth大于0时将使用原文中的方法进行Diffuse计算,再次调用TraceRay进行Ray Tracing递归计算。
GetRandomOnUnitSphere返回单位球体上均匀分布的随机向量。
7.3. 最终输出
![3b4d92f9cf52781abee446099e18687d.png](https://i-blog.csdnimg.cn/blog_migrate/053f7d3b936d3c0a944337e1917d399b.png)
8. Dielectrics材质
例程类:AntialiasingTutorial
场景文件:8_DielectricsTutorialScene
原文中在使用负数半径的方法来达到玻璃泡的效果,由于Unity中我们无法使用Intersection Shader,因此里添加了一个新的物体Shader DielectricsInv来反转Normal达到相同的效果。
8.1. 物体ClosestHitShader
inline
_IOR为材质的折射率。
和Diffuse的区别在于反射光线和折射光线的计算,具体算法参考原文,这里不再赘述。
此处调用TraceRay时将第二个参数改为了RAY_FLAG_NONE,因为光线射入物体内部后的折射光线需要与三角形反面做相交计算,故不再使用RAY_FLAG_CULL_BACK_FACING_TRIANGLES。
8.2. 最终输出
![43d32b166b810a7b40808f804b2df6cf.png](https://i-blog.csdnimg.cn/blog_migrate/4e48c7b6ee9c5f57bd9f4fa81fc20c50.jpeg)
9. 失焦模糊
例程类:CameraTutorial
场景文件:9_CameraTutorialScene
算法原理参看原文,这里不再赘述。仅对DXR实现进行说明。
9.1. C#代码
FocusCamera类用于扩展Unity的Camera,为其增加了focusDistance和aperture参数。
thisCamera
leftBottomCorner为Camera的Film Plane的左下角的World Space坐标。
size为Camera的Film Plane在World Space中的大小。
cmd
_FocusCameraLeftBottomCorner将Camera的Film Plane的左下角的World Space坐标传送到RayTrace Shader。
_FocusCameraRight, _FocusCameraUp将Camera在World Space中的右矢量和上矢量传送到RayTrace Shader。
_FocusCameraSize将Camera的Film Plane在World Space中的大小传送到RayTrace Shader。
_FocusCameraHalfAperture将Aperture的一半传送到RayTrace Shader。
9.2. RayTrace Shader
RayTrace Shader基本同之前小节的内容,仅仅将GenerateCameraRayWithOffset替换为了GenerateFocusCameraRayWithOffset函数。
inline
算法原理同原文所述,只是部分计算移动到了C#阶段计算,参看9.1节代码。
apertureOffset由GetRandomInUnitDisk产生。
GetRandomInUnitDisk用于在单位圆上随机产生均匀分布的矢量。
9.3. 最终输出
![36a9f966d4f423006362cf8dfe652b3c.png](https://i-blog.csdnimg.cn/blog_migrate/e093892cdc6f6367597cc3533041268e.png)
10. 全部放到一起
源代码地址:https://github.com/zhing2006/GPU-Ray-Tracing-in-One-Weekend-by-Unity-2019.3
![482505b296e03b72d68eb9ec23715460.png](https://i-blog.csdnimg.cn/blog_migrate/9193e0e59bf8bd02acc48a3b64bef566.jpeg)
![0cf9fd0e38a8c6ee6dd6105a5657601e.png](https://i-blog.csdnimg.cn/blog_migrate/b372a8742cf6115fe239ce258202a945.jpeg)