本文主要参考NVIDIA Vulkan Ray Tracing Tutorial教程,环境配置与程序均可参照此文档执行(个人水平有限,如有错误请参照原文)。
一、场景构建
首先,我们创建一个具有:两个反射长方体和一个模型的场景。
在main函数中将之前加载模型的地方更改为:
//创建示例
helloVk.loadModel(nvh::findFile( " media/scenes/cube.obj " , defaultSearchPaths, true ),
nvmath::translation_mat4 (nvmath::vec3f(- 2 , 0 , 0 ))
* nvmath::scale_mat4(nvmath::vec3f(. 1f , 5 .f, 5 .f)));
helloVk.loadModel(nvh::findFile( " media/scenes/cube.obj " , defaultSearchPaths, true ),
nvmath::translation_mat4 (nvmath::vec3f( 2 , 0 , 0 ))
* nvmath::scale_mat4(nvmath::vec3f(. 1f , 5 .f, 5 .f)));
helloVk.loadModel(nvh::findFile( " media/scenes/cube_multi.obj " , defaultSearchPaths, true ));
helloVk.loadModel(nvh::findFile( " media/scenes/plane.obj " , defaultSearchPaths, true ),
nvmath::translation_mat4 (nvmath::vec3f( 0 , - 1 , 0 )));
然后将cube.mtl中材质信息修改为具有95%的反射特性的:
newmtl cube_instance_material
illum 3
d 1
Ns 32
Ni 0
Ka 0 0 0
Kd .1 .8 .3
Ks 0.95 0.95 0.95
二、递归反射
Vulkan的光线追踪功能允许对 traceRayEXT 进行递归调用,最多由VkPhysicalDeviceRayTracingPropertiesKHR结构体中的maxRecursionDepth确定。
在createRtPipeline() 中,将最大递归深度提高到 10,并确保不超过物理设备的最大递归限制:
rayPipelineInfo.maxPipelineRayRecursionDepth = std::max(10u, m_rtProperties.maxRecursionDepth); // Ray depth
2.1 修改raycommon.glsl
我们需要确定光线追踪的深度和衰减。所以对raycommon.glsl中的hitPayload 结构体我们添加以下内容:
int depth;
vec3 attenuation;
2.2 修改raytrace.rgen
在光线生成着色器中,我们将在调用traceRayEXT
prd.depth = 0 ;
prd.hitValue = vec3 ( 0 );
prd.attenuation = vec3( 1 .f, 1 .f, 1 .f);
2.3 修改raytrace.rchit
在最近命中着色器的末尾,且在设置prd.hitValue之前,如果材质是反射的,我们需要发射光线:
// 反射
if(mat.illum == 3 && prd.depth < 10)
{
vec3 origin = worldPos;
vec3 rayDir = reflect(gl_WorldRayDirectionEXT, normal);
prd.attenuation *= mat.specular;
prd.depth++;
traceRayEXT(topLevelAS, // acceleration structure
gl_RayFlagsNoneEXT, // rayFlags
0xFF, // cullMask
0, // sbtRecordOffset
0, // sbtRecordStride
0, // missIndex
origin, // ray origin
0.1, // ray min range
rayDir, // ray direction
100000.0, // ray max range
0 // payload (location = 0)
);
prd.depth--;
}
由于反射的原因,每次递归计算出来的hitValue都需要累加,因为payload对于raygen光追的整个执行都是全局的,所以在main()中需要把最后一行改为:
prd.hitValue += vec3(attenuation * lightIntensity * (diffuse + specular)) * prd.attenuation;
2.4 修改raytrace.rmiss
最后,在未命中着色器中光线能量衰减也会产生对应的影响:
prd.hitValue = clearColor.xyz * 0.8 * prd.attenuation;
三、迭代反射
上述代码可以完成反射功能,但它受限于 GPU 可以执行的递归次数,并且还可能影响性能。而且设置超过递归的限制的数据将最终会产生vulkan运行错误。
如果需要的话,我们将返回有效光路载荷中的信息来发发出新光线,而不是从最近命中着色器发出新光线。
- 修改raycommon.glsl
在结构体中新增以下信息来为光追做准备
int done;
vec3 rayOrigin;
vec3 rayDir;
- 修改raytrace.rgen
在光线生产着色器中进行初始化有效光路载荷值:
prd.done = 1;
prd.rayOrigin = origin.xyz;
prd.rayDir = direction.xyz;
之后我们不止调用一次 traceRayEXT,而是在循环中调用它,直到完成为止。
我们在raytrace.rgen中这样进行调用:
vec3 hitValue = vec3(0);
for(;;)
{
traceRayEXT( /*.. */);
hitValue += prd.hitValue * prd.attenuation;
prd.depth++;
if(prd.done == 1 || prd.depth >= 10)
break;
origin.xyz = prd.rayOrigin;
direction.xyz = prd.rayDir;
prd.done = 1; // Will stop if a reflective material isn't hit
}
并在最后确保写入正确的值:
imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(hitValue, 1.0));
- 修改raytrace.rchit
我们不再需要从最近命中着色器发射光线,所以我们可以用:
if(mat.illum == 3)
{
vec3 origin = worldPos;
vec3 rayDir = reflect(gl_WorldRayDirectionEXT, normal);
prd.attenuation *= mat.specular;
prd.done = 0;
prd.rayOrigin = origin;
prd.rayDir = rayDir;
}
hitValue 值的计算也不再需要累加,且需要考虑衰减:
prd.hitValue = vec3(attenuation * lightIntensity * (diffuse + specular));
- 修改raytrace.rmiss
由于光线生成着色器现在处理了衰减,因此我们不再需要衰减未命中着色器中返回的值:
prd.hitValue = clearColor.xyz * 0.8 ;
- 最大递归
最后,我们不再需要在createRtPipeline中设置深度递归,只需要设置深度为 2:一个用于初始光线生成,另一个用于阴影光线。
rayPipelineInfo.maxPipelineRayRecursionDepth = 2 ; //光线深度
在raytrace.rgen中,我们现在可以增大最大光线深度而不会导致vulkan设备运行报错。
- 控制深度
在PushConstantRay结构体中,我们可以添加一个新maxDepth成员来传递给着色器。
struct PushConstantRay
{
vec4 clearColor;
vec3 lightPosition;
float lightIntensity;
int lightType;
int maxDepth;
};
我们可以将默认值设置为 10
PushConstantRay m_pcRay{{}, {}, 0, 0, 10};
在raytrace.rgen着色器中,我们添加何时停止的条件:
if(prd.done == 1 || prd.depth >= pushC.maxDepth)
break;
之后运行可见如下效果:
递归深度为1:
递归深度为2:
递归深度为3:
递归深度为4:
递归深度为5:
递归深度为10: