Vulkan_Ray Tracing 12_AnyHit Shader

本文主要参考NVIDIA Vulkan Ray Tracing Tutorial教程,环境配置与程序均可参照此文档执行(个人水平有限,如有错误请参照原文)。

与最近命中着色器一样,任何命中着色器都在光线和几何体之间的交点上运行。但是,任何命中着色器都将在沿射线与几何体的所有命中点上执行。然后将在离相机最近的且被设置的相交点上调用最近命中着色器。

任何命中着色器可用于丢弃相交点,但也可用于简单的透明度。在此示例中,我们将展示添加此着色器并创建透明效果。

Vulkan_Ray Tracing 12_AnyHit Shader

一、任何命中着色器(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绑定表 。
Vulkan_Ray Tracing 12_AnyHit Shader

并且我们需要将以下内容添加到光线追踪管线中,即之前 Hit Group 以及使用适当负载的新 AnyHit。

Vulkan_Ray Tracing 12_AnyHit Shader

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);

上一篇:Synchronized死锁


下一篇:手把手教你如何自己设计实现一个深度学习框架(附代码实现)