本文主要参考NVIDIA Vulkan Ray Tracing Tutorial教程,环境配置与程序均可参照此文档执行(个人水平有限,如有错误请参照原文)。
与最近命中着色器一样,任何命中着色器都在光线和几何体之间的交点上运行。但是,任何命中着色器都将在沿射线与几何体的所有命中点上执行。然后将在离相机最近的且被设置的相交点上调用最近命中着色器。
任何命中着色器可用于丢弃相交点,但也可用于简单的透明度。在此示例中,我们将展示添加此着色器并创建透明效果。
一、任何命中着色器(Any Hit Shader)
创建一个新的着色器文件raytrace.rahit。
此着色器以 raytrace.chit 开头,但使用的信息较少。
#version 460
#extension GL_EXT_ray_tracing : require
#extension GL_EXT_scalar_block_layout : enable
#extension GL_GOOGLE_include_directive : enable
#extension GL_EXT_shader_explicit_arithmetic_types_int64 : require
#extension GL_EXT_buffer_reference2 : require
#include "random.glsl"
#include "raycommon.glsl"
#include "wavefront.glsl"
// clang-format off
layout(location = 0) rayPayloadInEXT hitPayload prd;
layout(buffer_reference, scalar) buffer Vertices {Vertex v[]; }; // Positions of an object
layout(buffer_reference, scalar) buffer Indices {uint i[]; }; // Triangle indices
layout(buffer_reference, scalar) buffer Materials {WaveFrontMaterial m[]; }; // Array of all materials on an object
layout(buffer_reference, scalar) buffer MatIndices {int i[]; }; // Material ID for each triangle
layout(set = 1, binding = eObjDescs, scalar) buffer ObjDesc_ { ObjDesc i[]; } objDesc;
// clang-format on
⚠️注意: 您也可以在Vulkan_Ray Tracing 10_简单路径追踪找到。
//sampling.glsl
//使用使用16对的微小加密算法由两个unsigned int值生成一个随机的unsigned int。
//Zafar, Olano, and Curtis, "GPU Random Numbers via the Tiny Encryption Algorithm"
uint tea(uint val0, uint val1)
{
uint v0 = val0;
uint v1 = val1;
uint s0 = 0;
for(uint n = 0; n < 16; n++)
{
s0 += 0x9e3779b9;
v0 += ((v1 << 4) + 0xa341316c) ^ (v1 + s0) ^ ((v1 >> 5) + 0xc8013ea4);
v1 += ((v0 << 4) + 0xad90777d) ^ (v0 + s0) ^ ((v0 >> 5) + 0x7e95761e);
}
return v0;
}
//生成一个随机的无符号整数,在[0,2 ^24)给定之前的RNG状态
//使用Numerical Recipes线性同余生成器
uint lcg(inout uint prev)
{
uint LCG_A = 1664525u;
uint LCG_C = 1013904223u;
prev = (LCG_A * prev + LCG_C);
return prev & 0x00FFFFFF;
}
//生成一个随机浮点数在[0,1)给定之前的RNG状态 float rnd(inout uint prev)
{
return (float(lcg(prev)) / float(0x01000000));
}
//-------------------------------------------------------------------------------------------------
// Sampling
//-------------------------------------------------------------------------------------------------
// 在+Z方向附近随机采样
vec3 samplingHemisphere(inout uint seed, in vec3 x, in vec3 y, in vec3 z)
{
#define M_PI 3.141592
float r1 = rnd(seed);
float r2 = rnd(seed);
float sq = sqrt(1.0 - r2);
vec3 direction = vec3(cos(2 * M_PI * r1) * sq, sin(2 * M_PI * r1) * sq, sqrt(r2));
direction = direction.x * x + direction.y * y + direction.z * z;
return direction;
}
//从传入法线返回正切和副法线
void createCoordinateSystem(in vec3 N, out vec3 Nt, out vec3 Nb)
{
if(abs(N.x) > abs(N.y))
Nt = vec3(N.z, 0, -N.x) / sqrt(N.x * N.x + N.z * N.z);
else
Nt = vec3(0, -N.z, N.y) / sqrt(N.y * N.y + N.z * N.z);
Nb = cross(N, Nt);
}
对于 any hit 着色器,我们需要知道我们击中的是哪种材质,以及该材质是否支持透明度。如果它是不透明的,我们就简单地返回,这意味着命中将被接受。
void main()
{
// 对象数据
ObjDesc objResource = objDesc.i[gl_InstanceCustomIndexEXT];
MatIndices matIndices = MatIndices(objResource.materialIndexAddress);
Materials materials = Materials(objResource.materialAddress);
// 对象的材质
int matIdx = matIndices.i[gl_PrimitiveID];
WaveFrontMaterial mat = materials.m[matIdx];
if (mat.illum != 4)
return;
现在我们将应用透明度:
if (mat.dissolve == 0.0 )
ignoreIntersectionEXT ();
else if (rnd(prd.seed) > mat.dissolve)
ignoreIntersectionEXT ();
}
如上所述,我们使用随机数生成器来确定光线是击中还是忽略了对象。如果我们累加了足够多的光线,最终的结果就会收敛到我们想要的样子。
有效光路载荷
随机数seed也需要在射线有效光路载荷中传递。
所以在 raycommon.glsl 中,添加随机数:
struct hitPayload
{
vec3 hitValue;
uint seed;
};
二、添加任何命中着色器
任何命中着色器将成为命中着色器组的一部分。目前,命中着色器组仅包含最近命中着色器。
在createRtPipeline()中,在加载后最近命中着色器后,加载任何命中着色器
enum StageIndices
{
...
eAnyHit,
eShaderGroupCount
};
// Hit Group - Any Hit
stage.module = nvvk::createShaderModule(m_device, nvh::loadFile("spv/raytrace.rahit.spv", true, defaultSearchPaths, true));
stage.stage = VK_SHADER_STAGE_ANY_HIT_BIT_KHR;
stages[eAnyHit] = stage;
Any Hit 与 Closest Hit 位于相同的 Hit 组中,因此我们需要添加 Any Hit 索引并将其添加进命中组中。
// 最近的命中着色器
// Payload 0
group.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_TRIANGLES_HIT_GROUP_KHR;
group.generalShader = VK_SHADER_UNUSED_KHR;
group.closestHitShader = eClosestHit;
group.anyHitShader = eAnyHit;
m_rtShaderGroups.push_back(group);
2.1 将缓冲区的访问权限授予 Any Hit 着色器
在 createDescriptorSetLayout() 中,我们需要允许 Any Hit 着色器访问场景描述缓冲区
// Obj descriptions
m_descSetLayoutBind.addBinding(eObjDescs, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT
| VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR | VK_SHADER_STAGE_ANY_HIT_BIT_KHR);
2.2 不透明标识
在示例中,在创建VkAccelerationStructureGeometryKHR对象时,我们将它们的标志设置为VK_GEOMETRY_OPAQUE_BIT_KHR。然而,这避免了调用任何命中着色器。
我们可以删除所有标志,但可能会出现另一个问题:同一个三角形可能多次调用 any hit 着色器。要让任何命中着色器处理每个三角形只有一次命中,需要设置VK_GEOMETRY_NO_DUPLICATE_ANY_HIT_INVOCATION_BIT_KHR标志:
asGeom.flags = VK_GEOMETRY_NO_DUPLICATE_ANY_HIT_INVOCATION_BIT_KHR; //避免多次击中;
三、其他着色器修改
3.1 光线生成着色器
首先,seed需要在任何命中着色器中可用,这也是我们将其添加到 hitPayload 结构体中的原因。
prd.seed = tea(gl_LaunchIDEXT.y * gl_LaunchSizeEXT.x + gl_LaunchIDEXT.x, pushC.frame);
为了优化,TraceRayEXT调用使用了gl_RayFlagsOpaqueEXT标志。但这将跳过任何命中着色器,因此将其更改为
uint rayFlags = gl_RayFlagsNoneEXT;
3.2 最近命中着色器
同样,在最近命中着色器中,将标志更改为gl_RayFlagsSkipClosestHitShaderEXT,因为我们要启用任何命中和未命中着色器,但我们仍然不处理阴影光线的最近命中着色器。此操作也将启用透明阴影。
uint flags = gl_RayFlagsSkipClosestHitShaderEXT;
3.3 场景和模型更新
场景
main()中改为以下场景:
helloVk.loadModel(nvh::findFile( " media/scenes/wuson.obj " , defaultSearchPaths, true ));
helloVk.loadModel(nvh::findFile( " media/scenes/sphere.obj " , defaultSearchPaths, true ),
nvmath::scale_mat4 (nvmath::vec3f( 1 . 5f ))
* nvmath::translation_mat4(nvmath::vec3f( 0 . 0f , 1 . 0f , 0 . 0f )));
helloVk.loadModel(nvh::findFile( " media/scenes/plane.obj " , defaultSearchPaths, true ));
OBJ材质
默认情况下,所有对象都是不透明的,您需要更改材质描述。
编辑材质文件media/scenes/wuson.mtl和media/scenes/sphere.mtl的前几行,并使用新的照明模型(4)的0.5的透明值:
newmtl default
illum 4
d 0.5
...
帧累加见之前章节的设置详述。
四、光追管线优化
上面的代码可运行,但有潜在BUG。原因是:阴影射线在最近命中着色器中执行traceRayEXT的调用时使用有效载荷 1,并且当与对象相交时,任何命中着色器将使用有效载荷 0 执行。此处,程序添加了填充并且没有任何问题,但不应该这样处理。
每次traceRayEXT调用时都应该具有与光线跟踪调用不同有效光路一样多的命中组。对于其他示例,没问题,因为我们使用了gl_RayFlagsSkipClosestHitShaderNV标志,并且不会调用最近的命中着色器(有效负载 0),并且命中组中没有任何命中或相交着色器。但在本例中,将跳过最近命中着色器,但不会跳过任何命中着色器。
为了解决这个问题,我们需要添加另一个命中组。
这是当前 SBT绑定表 。
并且我们需要将以下内容添加到光线追踪管线中,即之前 Hit Group 以及使用适当负载的新 AnyHit。
4.1 新着色器
创建两个新文件raytrace_0.ahit和raytrace_1.ahit,并重命名raytrace.ahit为raytrace_ahit.glsl
在raytrace_0.ahit添加以下代码
#version 460
#extension GL_GOOGLE_include_directive : enable
#define PAYLOAD_0
#include "raytrace_rahit.glsl"
并在raytrace_1.ahit中,替换PAYLOAD_0为PAYLOAD_1
然后在raytrace_ahit.glsl删除#version 460 并添加以下代码,以便我们有正确的布局。
#ifdef PAYLOAD_0
layout(location = 0) rayPayloadInNV hitPayload prd;
#elif defined(PAYLOAD_1)
layout(location = 1) rayPayloadInNV shadowPayload prd;
#endif
4.2 新的有效光路载荷
我们不能简单地为阴影射线有效光路载荷设置一个布尔值。我们还需要为随机函数添加seed种子 。
在raycommon.glsl文件中,添加以下结构
struct shadowPayload
{
bool isHit;
uint seed;
};
阴影有效光路荷载的作用是在最近命中和阴影未命中着色器中。首先,让我们修改raytraceShadow.rmiss:
#version 460
#extension GL_NV_ray_tracing : require
#extension GL_GOOGLE_include_directive : enable
#include "raycommon.glsl"
layout(location = 1) rayPayloadInNV shadowPayload prd;
void main()
{
prd.isHit = false;
}
最近命中着色器raytrace.rchit需要改变payload的使用,还要调用traceRayEXT
将有效光路载荷替换为
layout(location = 1) rayPayloadNV shadowPayload prdShadow;
然后就在调用之前traceRayEXT,将值初始化为
prdShadow.isHit = true ;
prdShadow.seed = prd.seed;
在光线追踪之后,将种子值设置回主要光路荷载中
prd.seed = prdShadow.seed;
并检查阴影射线是否击中了物体
if(prdShadow.isHit)
4.3 执行traceRayEXT
当我们调用 时traceRayEXT,由于我们使用的是有效载荷 1(最后一个参数),因此我们还需要跟踪来命中替代命中组,即使用有效载荷 1 的组。为此,我们需要将 sbtRecordOffset 设置为 1
traceRayEXT(topLevelAS, // acceleration structure
flags, // rayFlags
0xFF, // cullMask
1, // sbtRecordOffset
0, // sbtRecordStride
1, // missIndex
origin, // ray origin
tMin, // ray min range
rayDir, // ray direction
tMax, // ray max range
1 // payload (location = 1)
);
4.4 光线追踪管线
最后一步是添加新的 Hit Group。在createRtPipeline()中我们需要加载新的 any hit 着色器并创建一个新的 Hit Group。
换"shaders/raytrace.rahit.spv"为"shaders/raytrace_0.rahit.spv"
加载新的着色器模块,代码如下:
enum StageIndices
{
eRaygen,
eMiss,
eMiss2,
eClosestHit,
eAnyHit,
eAnyHit2,
eShaderGroupCount
};
// Hit Group - Any Hit
stage.module = nvvk::createShaderModule(m_device, nvh::loadFile("spv/raytrace_0.rahit.spv", true, defaultSearchPaths, true));
stage.stage = VK_SHADER_STAGE_ANY_HIT_BIT_KHR;
stages[eAnyHit] = stage;
//
stage.module = nvvk::createShaderModule(m_device, nvh::loadFile("spv/raytrace_1.rahit.spv", true, defaultSearchPaths, true));
stage.stage = VK_SHADER_STAGE_ANY_HIT_BIT_KHR;
stages[eAnyHit2] = stage;
在创建第一个 Hit Group 后,创建一个新的 Hit Group,其中仅添加使用 payload 1 的 any hit。因为我们在光追调用中需要跳过最近命中着色器,所以我们可以在命中组中忽略它。
// Payload 1
group.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_TRIANGLES_HIT_GROUP_KHR;
group.generalShader = VK_SHADER_UNUSED_KHR;
group.closestHitShader = VK_SHADER_UNUSED_KHR;
group.anyHitShader = eAnyHit2;
m_rtShaderGroups.push_back(group);