开始使用 Diligent Engine 进行路径跟踪
使用 Diligent Engine 实现基本路径跟踪器的示例
免责声明:本文使用了 Diligent Engine 网站上发布的内容。
引言
路径追踪是一种渲染方法,它能自然地模拟复杂的光现象,如软阴影、环境光遮蔽、相互反射、焦散等,以创建逼真的图像。多年来,它一直被用于产品级渲染,但随着近期 GPU 技术的进步,现在它也开始进入实时渲染领域。它使用蒙特卡洛积分法来求解渲染方程。在本文中,我们将介绍如何使用Diligent Engine(一个现代跨平台底层图形库和渲染框架)来实现一个基本的路径追踪器。
背景
本文并非路径追踪的入门介绍,因为网络上已有大量关于该主题的优秀资源可以轻松找到。《Ray Tracing in One Weekend》以及维也纳工业大学的渲染入门课程都是不错的起点。在本文中,我们将提供一个关于如何开始使用路径追踪的实用分步指南,并假设读者已对基础知识有所了解。这篇文章介绍了 Diligent Engine。
路径追踪
我们实现了一个带有下一事件估计(也称为光源采样)的基本路径追踪算法。渲染过程包括以下三个阶段:
- G-buffer 生成
- 路径追踪和辐射度累积
- 解析
在第一阶段,场景被渲染到一个 G-buffer 中,该 G-buffer 包含以下渲染目标:
- 反照率 (Albedo) (
RGBA8_UNORM
) - 法线 (Normal) (
RGBA8_UNORM
) - 自发光 (Emissive) (
R11G11B10_FLOAT
) - 深度 (Depth) (
R32_FLOAT
)
在第二阶段,会执行一个计算着色器,它为 G-buffer 的每个像素重建其世界空间位置,追踪一条穿过场景的光路,并将其贡献累加到辐射度累积缓冲区。每一帧,都会追踪一定数量的新路径,并累积它们的贡献。如果相机移动或光源属性改变,累积缓冲区将被清除,过程重新开始。
最后,在第三阶段,通过对所有光路贡献进行平均,来解析光累积缓冲区中的辐射度。
场景表示
我们将使用一些解析几何体(长方体)来定义场景,并通过计算与每个长方体的交点并为每条光线找到最近的交点来追踪光线。实际应用可能会使用 DXR/Vulkan 光线追踪(请参阅
教程 21 - 光线追踪和教程 22 - 混合渲染)。
一个长方体由其中心、尺寸、颜色和类型定义
struct BoxInfo
{
float3 Center;
float3 Size;
float3 Color;
int Type;
};
类型允许两个值:朗伯漫反射表面和光源。一条光线由其原点和归一化方向定义
struct RayInfo
{
float3 Origin;
float3 Dir;
};
一个命中点包含有关交点处的颜色、表面法线、从光线原点到命中点的距离以及命中类型(朗伯表面、漫反射光源或无)的信息
struct HitInfo
{
float3 Color; // Albedo or radiance for light
float3 Normal;
float Distance;
int Type;
};
光线与长方体的交点由 IntersectAABB
函数计算
bool IntersectAABB(in RayInfo Ray,
in BoxInfo Box,
inout HitInfo Hit)
该函数接收光线信息、长方体属性以及当前的命中点信息。如果新的命中点比 Hit
定义的当前点更近,则该结构体将更新为新的颜色、法线、距离和命中类型。
在场景中投射光线,就是将光线与每个长方体进行求交
float RoomSize = 10.0;
float WallThick = 0.05;
BoxInfo Box;
Box.Type = HIT_TYPE_LAMBERTIAN;
float3 Green = float3(0.1, 0.6, 0.1);
float3 Red = float3(0.6, 0.1, 0.1);
float3 Grey = float3(0.5, 0.5, 0.5);
// Right wall
Box.Center = float3(RoomSize * 0.5 + WallThick * 0.5, 0.0, 0.0);
Box.Size = float3(WallThick, RoomSize * 0.5, RoomSize * 0.5);
Box.Color = Green;
IntersectAABB(Ray, Box, Hit);
// Left wall
Box.Center = float3(-RoomSize * 0.5 - WallThick * 0.5, 0.0, 0.0);
Box.Size = float3(WallThick, RoomSize * 0.5, RoomSize * 0.5);
Box.Color = Red;
IntersectAABB(Ray, Box, Hit);
// Ceiling
// ...
G-buffer 渲染
G-buffer 是通过一个全屏渲染通道来渲染的,其中像素着色器在场景中执行光线投射。它首先使用逆视图-投影矩阵计算光线的起点和终点
float3 f3RayStart =
ScreenToWorld(PSIn.Pos.xy, 0.0, f2ScreenSize, g_Constants.ViewProjInvMat);
float3 f3RayEnd =
ScreenToWorld(PSIn.Pos.xy, 1.0, f2ScreenSize, g_Constants.ViewProjInvMat);
RayInfo Ray;
Ray.Origin = f3RayStart;
Ray.Dir = normalize(f3RayEnd - f3RayStart);
接下来,着色器在场景中投射一条光线,并将命中点的属性写入 G-buffer
目标
PSOutput PSOut;
HitInfo Hit = IntersectScene(Ray, g_Constants.f2LightPosXZ, g_Constants.f2LightSizeXZ);
if (Hit.Type == HIT_TYPE_LAMBERTIAN)
{
PSOut.Albedo.rgb = Hit.Color;
}
else if (Hit.Type == HIT_TYPE_DIFFUSE_LIGHT)
{
PSOut.Emissive.rgb = g_Constants.f4LightIntensity.rgb;
}
PSOut.Normal = float4(saturate(Hit.Normal * 0.5 + 0.5), 0.0);
在本例中,我们只有一个光源,所以我们使用常量缓冲区中的光源属性。实际应用将从命中点检索光源属性。
最后,我们通过使用视图-投影矩阵变换命中点来计算深度
float3 HitWorldPos = Ray.Origin + Ray.Dir * Hit.Distance;
float4 HitClipPos = mul(float4(HitWorldPos, 1.0), g_Constants.ViewProjMat);
PSOut.Depth = min(HitClipPos.z / HitClipPos.w, 1.0);
路径追踪
路径追踪是渲染过程的核心部分,由一个计算着色器实现,该着色器为每个屏幕像素运行一个线程。它从 G-buffer 定义的位置开始,在场景中追踪给定数量的光路,每条路径执行给定次数的反弹。
对于每次反弹,着色器会向光源追踪一条光线并计算其贡献。然后,它使用余弦加权的半球分布在着色点选择一个随机方向,沿该方向投射一条光线,并在新位置重复此过程。
着色器首先从 G-buffer 读取样本属性,并使用逆视图-投影矩阵重建当前样本的世界空间位置
float fDepth = g_Depth.Load(int3(ThreadId.xy, 0)).x;
float3 f3SamplePos0 = ScreenToWorld(float2(ThreadId.xy) + float2(0.5, 0.5),
fDepth, f2ScreenSize,
g_Constants.ViewProjInvMat);
float3 f3Albedo0 = g_Albedo.Load(int3(ThreadId.xy, 0)).xyz;
float3 f3Normal0 = normalize(g_Normal.Load(int3(ThreadId.xy, 0)).xyz * 2.0 - 1.0);
float3 f3Emissive = g_Emissive.Load(int3(ThreadId.xy, 0)).xyz;
其中 ThreadId
是二维网格中计算着色器的线程索引,与屏幕坐标相匹配。
然后,着色器准备一个二维哈希种子
uint2 Seed = ThreadId.xy * uint2(11417, 7801) +
uint2(g_Constants.uFrameSeed1, g_Constants.uFrameSeed2);
这是一个非常重要的时刻:种子对于每一帧、每个样本和每一次反弹都必须是唯一的,以产生一个好的随机序列。糟糕的序列会导致收敛变慢或结果有偏差。g_Constants.uFrameSeed1
和 g_Constants.uFrameSeed2
是 CPU 为每一帧计算的随机种子。
然后,着色器追踪给定数量的光路并累积它们的贡献。每条路径都从同一个 G-buffer 样本开始
float3 f3SamplePos = f3SamplePos0;
float3 f3Albedo = f3Albedo0;
float3 f3Normal = f3Normal0;
// Total contribution of this path
float3 f3PathContrib = float3(0.0, 0.0, 0.0);
// Bounce attenuation
float3 f3Attenuation = float3(1.0, 1.0, 1.0);
for (int j = 0; j < g_Constants.iNumBounces; ++j)
{
// ...
}
在循环中,着色器首先在光源表面上采样一个随机点
float2 rnd2 = hash22(Seed);
float3 f3LightSample = SampleLight(g_Constants.f2LightPosXZ,
g_Constants.f2LightSizeXZ, rnd2);
float3 f3DirToLight = f3LightSample - f3SamplePos;
float fDistToLightSqr = dot(f3DirToLight, f3DirToLight);
f3DirToLight /= sqrt(fDistToLightSqr);
hash22
函数接受一个种子并产生两个在 [0, 1] 范围内的伪随机值。然后 SampleLight
函数使用这些值在光源长方体的角点之间进行插值
float3 SampleLight(float2 Pos, float2 Size, float2 uv)
{
BoxInfo Box = GetLight(Pos, Size);
float3 Corner0 = Box.Center - Box.Size;
float3 Corner1 = Box.Center + Box.Size;
float3 Sample;
Sample.xz = lerp(Corner0.xz, Corner1.xz, uv);
Sample.y = Corner0.y;
return Sample;
}
在本例中,光源是一个位于 XZ 平面、沿 Y 轴向下的矩形区域光。其属性是恒定的
float fLightArea = g_Constants.f2LightSizeXZ.x *
g_Constants.f2LightSizeXZ.y *
2.0;
float3 f3LightIntensity = g_Constants.f4LightIntensity.rgb *
g_Constants.f4LightIntensity.a;
float3 f3LightNormal = float3(0.0, -1.0, 0.0);
在光源上获取随机点后,会向该点投射一条阴影光线以测试
可见性
RayInfo ShadowRay;
ShadowRay.Origin = f3SamplePos;
ShadowRay.Dir = f3DirToLight;
float fLightVisibility = TestShadow(ShadowRay);
TestShadow
函数在场景中投射一条光线,如果它没有击中任何物体,则返回 1,否则返回 0。
然后路径辐射度更新如下
f3PathContrib +=
f3Attenuation
* f3BRDF
* max(dot(f3DirToLight, f3Normal), 0.0)
* fLightVisibility
* f3LightIntensity
* fLightProjectedArea;
其中
f3Attenuation
是路径衰减。最初,它是float3(1, 1, 1)
,并在每次反弹时乘以表面反照率f3BRDF
是朗伯 BRDF(理想漫反射表面),等于f3Albedo / PI
max(dot(f3DirToLight, f3Normal), 0.0)
是N dot L
项fLightVisibility
是通过投射阴影光线计算出的光源可见性f3LightIntensity
是光照强度常量fLightProjectedArea
是投影到半球上的光源表面积
在蒙特卡洛积分中,我们假定每个样本都代表了整个光源表面,所以我们将整个光源表面积投影到着色点周围的半球上,看看它覆盖了多大的立体角。这个值由 fLightProjectedArea
给出。
在添加了光源贡献后,我们通过使用余弦加权的半球分布选择一个随机方向来进入下一个样本
RayInfo Ray;
Ray.Origin = f3SamplePos;
Ray.Dir = SampleDirectionCosineHemisphere(f3Normal, rnd2);
// Trace the scene in the selected direction
HitInfo Hit = IntersectScene(Ray, g_Constants.f2LightPosXZ,
g_Constants.f2LightSizeXZ);
余弦加权分布会使更多的随机样本靠近法线方向,而在地平线方向的样本较少,因为由于 N dot L
项的存在,后者的贡献较小。
此时,如果我们击中了光源,就停止循环——光源的贡献是单独计算的
if (Hit.Type != HIT_TYPE_LAMBERTIAN)
break;
最后,我们将衰减与当前表面反照率相乘,并更新样本属性
f3Attenuation *= f3Albedo;
// Update current sample properties
f3SamplePos = Ray.Origin + Ray.Dir * Hit.Distance;
f3Albedo = Hit.Color;
f3Normal = Hit.Normal;
然后,该过程在下一个表面样本位置重复。
在路径中的所有反弹都追踪完毕后,总的路径贡献与来自 G-buffer 的自发光项相结合,并被添加到总辐射度中
f3Radiance += f3PathContrib + f3Emissive;
在所有样本追踪完成后,着色器将总辐射度添加到累积缓冲区
if (g_Constants.fLastSampleCount > 0)
f3Radiance += g_Radiance[ThreadId.xy].rgb;
g_Radiance[ThreadId.xy] = float4(f3Radiance, 0.0);
fLastSampleCount
指示到目前为止已经累积了多少样本。值为零表示着色器是第一次执行,在这种情况下,它会覆盖之前的值。
解析
辐射度累积缓冲区的解析是在另一个全屏渲染通道中完成的,并且非常直接:它只是简单地对所有累积的路径进行平均
void main(in PSInput PSIn,
out PSOutput PSOut)
{
PSOut.Color = g_Radiance.Load(int3(PSIn.Pos.xy, 0)) / g_Constants.fCurrSampleCount;
}
实现
本示例的完整源代码可在 GitHib 上获取。
Diligent Engine 允许在运行时从源代码编译着色器,并使用它们来创建管线状态。虽然这种方法简单方便,但也有缺点:
- 在运行时编译着色器开销很大。一个使用大量着色器的应用程序可能会遇到显著的加载时间问题。
- 在运行时编译着色器需要将编译器与引擎捆绑在一起。对于 Vulkan,这包括 glslang、SPIRV-Tools 和其他组件,这会显著增加二进制文件的大小。
- 着色器编译后,Diligent 还需要对它们进行修补,以使其与管线资源布局或资源签名兼容。
管线状态处理可以完全离线进行,使用 Render state packager 工具。该工具使用 Diligent Render State Notation,一种基于 JSON 的渲染状态描述语言。打包器生成的一个归档文件可以包含多个管线状态以及着色器和管线资源签名,这些都可以在运行时加载并立即使用,无需任何开销。每个对象还可以包含不同后端(例如 DX12 和 Vulkan)的数据,因此一个归档文件可以在不同平台上使用。
离线归档管线有许多好处:
- 运行时加载开销降至最低,因为不执行着色器编译或字节码修补。
- 可以从二进制文件中移除着色器编译工具链,以减小其大小。
- 渲染状态与可执行代码分离,改善了代码结构。
- 着色器和管线状态可以更新,而无需重新构建应用程序。
渲染过程的三个阶段由三个管线状态实现。这些管线使用 Diligent Render State Notation 定义。它们通过渲染状态打包器离线创建,并打包成一个单一的归档文件。在运行时,该归档文件被加载,并使用 IDearchiver
对象解包管线。
Diligent Render State Notation
Diligent Render State Notation (DRSN) 是一种基于 JSON 的渲染状态描述语言。DRSN 文件包含以下部分:
Imports
部分定义了其他 DRSN 文件,这些文件的对象应被导入到当前文件中,其工作方式与#include
指令非常相似。Defaults
部分定义了文件中定义的对象的默认值。Shaders
部分包含着色器描述。RenderPasses
部分定义了管线使用的渲染通道。ResourceSignatures
部分定义了管线资源签名。Pipelines
部分包含管线状态。
DRSN 文件中的所有对象都通过名称引用,每个对象类别的名称必须是唯一的。这些名称在运行时用于解包状态。
DRSN 以 JSON 格式反映了引擎的核心结构。例如,G-buffer PSO 在 DRSN 中定义如下:
"Pipelines": [
{
"PSODesc": {
"Name": "G-Buffer PSO",
"ResourceLayout": {
"Variables": [
{
"Name": "cbConstants",
"ShaderStages": "PIXEL",
"Type": "STATIC"
}
]
}
},
"GraphicsPipeline": {
"PrimitiveTopology": "TRIANGLE_LIST",
"RasterizerDesc": {
"CullMode": "NONE"
},
"DepthStencilDesc": {
"DepthEnable": false
}
},
"pVS": {
"Desc": {
"Name": "Screen Triangle VS"
},
"FilePath": "screen_tri.vsh",
"EntryPoint": "main"
},
"pPS": {
"Desc": {
"Name": "G-Buffer PS"
},
"FilePath": "g_buffer.psh",
"EntryPoint": "main"
}
}
]
请参阅此页面了解有关 Diligent Render State Notation 的更多信息。
打包器命令行
本示例使用以下命令行来创建归档文件:
Diligent-RenderStatePackager.exe -i RenderStates.json -r assets -s assets
-o assets/StateArchive.bin --dx11 --dx12 --opengl --vulkan --metal_macos
--metal_ios --print_contents
该命令行使用以下选项:
-i
- 输入的 DRSN 文件-r
- 渲染状态表示法文件搜索目录-s
- 着色器搜索目录-o
- 输出的归档文件--dx11
,--dx12
,--opengl
,--vulkan
,--metal_macos
,--metal_ios
- 为其生成管线数据的设备标志。请注意,平台上不支持的设备(例如在 Windows 上的--metal_macos
,或在 Linux 上的--dx11
)将被忽略。--print_contents
- 将归档内容打印到日志中
要查看完整的命令行选项列表,请使用 -h
或 --help
选项运行打包器。
如果您使用 CMake
,您可以定义一个自定义命令,在构建步骤中创建归档文件:
set(PSO_ARCHIVE ${CMAKE_CURRENT_SOURCE_DIR}/assets/StateArchive.bin)
set_source_files_properties(${PSO_ARCHIVE} PROPERTIES GENERATED TRUE)
set(DEVICE_FLAGS --dx11 --dx12 --opengl --vulkan --metal_macos --metal_ios)
add_custom_command(OUTPUT ${PSO_ARCHIVE} # We must use full path here!
COMMAND $<TARGET_FILE:Diligent-RenderStatePackager>
-i RenderStates.json -r assets -s assets
-o "${PSO_ARCHIVE}" ${DEVICE_FLAGS} --print_contents
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
MAIN_DEPENDENCY "${CMAKE_CURRENT_SOURCE_DIR}/assets/RenderStates.json"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/assets/screen_tri.vsh"
"${CMAKE_CURRENT_SOURCE_DIR}/assets/g_buffer.psh"
"${CMAKE_CURRENT_SOURCE_DIR}/assets/resolve.psh"
"${CMAKE_CURRENT_SOURCE_DIR}/assets/path_trace.csh"
"${CMAKE_CURRENT_SOURCE_DIR}/assets/structures.fxh"
"${CMAKE_CURRENT_SOURCE_DIR}/assets/scene.fxh"
"${CMAKE_CURRENT_SOURCE_DIR}/assets/hash.fxh"
"$<TARGET_FILE:Diligent-RenderStatePackager>"
COMMENT "Creating render state archive..."
VERBATIM
)
解包管线状态
要从归档文件中解包管线状态,首先使用引擎工厂创建一个 dearchiver
对象
RefCntAutoPtr<IDearchiver> pDearchiver;
DearchiverCreateInfo DearchiverCI{};
m_pEngineFactory->CreateDearchiver(DearchiverCI, &pDearchiver);
然后从文件中读取归档数据并将其加载到 dearchiver 中
FileWrapper pArchive{"StateArchive.bin"};
auto pArchiveData = DataBlobImpl::Create();
pArchive->Read(pArchiveData);
pDearchiver->LoadArchive(pArchiveData);
要从归档文件中解包管线状态,需要填充一个 PipelineStateUnpackInfo
结构体实例,其中包含管线类型和名称。同时,提供一个指向渲染设备的指针
PipelineStateUnpackInfo UnpackInfo;
UnpackInfo.pDevice = m_pDevice;
UnpackInfo.PipelineType = PIPELINE_TYPE_GRAPHICS;
UnpackInfo.Name = "Resolve PSO";
虽然大多数管线状态参数可以在构建时定义,但有些只能在运行时指定。例如,交换链的格式可能在归档打包时是未知的。dearchiver 通过一个特殊的回调函数,让应用程序有机会修改一些管线状态的创建属性。可以修改的属性包括渲染目标和深度缓冲区的格式、深度-模板状态、混合状态、光栅化状态。请注意,定义资源布局的属性不能修改。另请注意,修改属性不会影响加载速度,因为不需要重新编译或修补着色器。
我们使用 MakeCallback
辅助函数定义一个回调,该回调用于设置渲染目标的格式
auto ModifyResolvePSODesc = MakeCallback(
[this](PipelineStateCreateInfo& PSODesc) {
auto& GraphicsPSOCI = static_cast<GraphicsPipelineStateCreateInfo&>(PSODesc);
auto& GraphicsPipeline = GraphicsPSOCI.GraphicsPipeline;
GraphicsPipeline.NumRenderTargets = 1;
GraphicsPipeline.RTVFormats[0] = m_pSwapChain->GetDesc().ColorBufferFormat;
GraphicsPipeline.DSVFormat = m_pSwapChain->GetDesc().DepthBufferFormat;
});
UnpackInfo.ModifyPipelineStateCreateInfo = ModifyResolvePSODesc;
UnpackInfo.pUserData = ModifyResolvePSODesc;
最后,我们调用 UnpackPipelineState
方法从归档文件中创建 PSO
pDearchiver->UnpackPipelineState(UnpackInfo, &m_pResolvePSO);
渲染
从归档文件中解包出管线状态后,它们就可以像往常一样使用了。我们需要创建着色器资源绑定对象并初始化变量。
为了渲染场景,我们运行上面描述的三个阶段中的每一个。首先,填充 G-buffer
ITextureView* ppRTVs[] = {
m_GBuffer.pAlbedo->GetDefaultView(TEXTURE_VIEW_RENDER_TARGET),
m_GBuffer.pNormal->GetDefaultView(TEXTURE_VIEW_RENDER_TARGET),
m_GBuffer.pEmissive->GetDefaultView(TEXTURE_VIEW_RENDER_TARGET),
m_GBuffer.pDepth->GetDefaultView(TEXTURE_VIEW_RENDER_TARGET)
};
m_pImmediateContext->SetRenderTargets(
_countof(ppRTVs), ppRTVs, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
m_pImmediateContext->CommitShaderResources(
m_pGBufferSRB, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
m_pImmediateContext->SetPipelineState(m_pGBufferPSO);
m_pImmediateContext->Draw({3, DRAW_FLAG_VERIFY_ALL});
接下来,使用计算通道执行路径追踪
static constexpr Uint32 ThreadGroupSize = 8;
m_pImmediateContext->SetPipelineState(m_pPathTracePSO);
m_pImmediateContext->CommitShaderResources(
m_pPathTraceSRB, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
DispatchComputeAttribs DispatchArgs{
(SCDesc.Width + ThreadGroupSize - 1) / ThreadGroupSize,
(SCDesc.Height + ThreadGroupSize - 1) / ThreadGroupSize
};
m_pImmediateContext->DispatchCompute(DispatchArgs);
最后,解析辐射度
```cpp
ITextureView* ppRTVs[] = {m_pSwapChain->GetCurrentBackBufferRTV()};
m_pImmediateContext->SetRenderTargets(
_countof(ppRTVs), ppRTVs, m_pSwapChain->GetDepthBufferDSV(),
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
m_pImmediateContext->SetPipelineState(m_pResolvePSO);
m_pImmediateContext->CommitShaderResources(
m_pResolveSRB, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
m_pImmediateContext->Draw({3, DRAW_FLAG_VERIFY_ALL});
```
控制应用程序
- 移动相机:鼠标左键 + WSADQE
- 移动光源:鼠标右键
您还可以更改以下参数:
- Num bounces - 每条路径的反弹次数
- Show only last bounce - 仅渲染路径中的最后一次反弹
- Samples per frame - 每一帧为每个像素采集的光路数量
- Light intensity, width, height and color - 不同的光源参数
结果
下图是由本文描述的示例应用程序渲染的图像
请注意来自区域光的软阴影、漫反射全局光照(盒子上的彩色反弹)和环境光遮蔽(角落处的变暗)。
观察单个光线反弹是很有趣的。第一次反弹(仅直接光照):
注意软阴影、黑色的天花板以及盒子上没有彩色的反弹。
第二次反弹
注意盒子是如何被墙壁反射的二次光照亮的。
第三次反弹
第四次反弹几乎看不见
由于第四次及以后的反弹几乎没有贡献,我们默认使用 3 次反弹。
下表展示了在一台运行 Windows 10、搭载 NVidia GeForce RTX 2070 Super 的笔记本电脑上,以 1920 x 1057 分辨率、每像素每帧 8 个样本收集的性能结果:
反弹次数 | 渲染时间 (ms) |
1 | 2.8 |
2 | 2.8 |
3 | 4.2 |
4 | 6.0 |
5 | 7.5 |
6 | 9.0 |
资源
- P.Shirley 的《Ray Tracing in One Weekend》
- M.Pharr、W.Jakob 和 G.Humphreys 的《Physically Based Rendering: From Theory to Implementation》
- B.Kerbl 和 A.Celarek 的维也纳工业大学渲染入门课程
历史
- 2022年9月6日:初始版本