本文主要参考NVIDIA Vulkan Ray Tracing Tutorial教程,环境配置与程序均可参照此文档执行(个人水平有限,如有错误请参照原文)。
与最近命中着色器一样,任何命中着色器都在光线和几何体之间的交点上运行。但是,任何命中着色器都将在沿射线与几何体的所有命中点上执行。然后将在离相机最近的且被设置的相交点上调用最近命中着色器。
任何命中着色器可用于丢弃相交点,但也可用于简单的透明度。在此示例中,我们将展示添加此着色器并创建透明效果。
创建一个新的着色器文件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);
在 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);
在示例中,在创建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; //避免多次击中;
首先,seed需要在任何命中着色器中可用,这也是我们将其添加到 hitPayload 结构体中的原因。
prd.seed = tea(gl_LaunchIDEXT.y * gl_LaunchSizeEXT.x + gl_LaunchIDEXT.x, pushC.frame);
为了优化,TraceRayEXT调用使用了gl_RayFlagsOpaqueEXT标志。但这将跳过任何命中着色器,因此将其更改为
uint rayFlags = gl_RayFlagsNoneEXT;
同样,在最近命中着色器中,将标志更改为gl_RayFlagsSkipClosestHitShaderEXT,因为我们要启用任何命中和未命中着色器,但我们仍然不处理阴影光线的最近命中着色器。此操作也将启用透明阴影。
uint flags = gl_RayFlagsSkipClosestHitShaderEXT;
场景
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。
创建两个新文件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
我们不能简单地为阴影射线有效光路载荷设置一个布尔值。我们还需要为随机函数添加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)
当我们调用 时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) );
最后一步是添加新的 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);