本文主要参考NVIDIA Vulkan Ray Tracing Tutorial教程,环境配置与程序均可参照此文档执行(个人水平有限,如有错误请参照原文)。
一、场景优化
1.1 场景数据
之前OBJ文件加载后被分别存储在四个缓冲中:
-
顶点缓冲区:位置、法线、UV、颜色数组
-
索引缓冲区:顶点索引,每三个组成一个三角形
-
材质缓冲区:wavefront 材质结构数据
-
材质索引缓冲区:每个三角形的材质索引。
因为我们可以加载多个 OBJ文件,所以我们可能会有多组上述结构的缓冲区数组造成缓冲区冗余。
为了兼容 GLTF 场景数据,我们不再将顶点,位置,法线和其他属性放在单独的缓冲区。 对于场景中的所有几何图形,都放在一个位置缓冲区,对于索引和其他属性也是如此。 所以我们需要记录下对于每个几何图形的元素数量和偏移量等信息。
从源教程中,我们不需要以下内容,因此将其删除:
//delete
std::vector<ObjModel> m_objModel; // Model on host
std::vector<ObjDesc> m_objDesc; // Model description for device access
std::vector<ObjInstance> m_instances; // Scene model instances
在host_device.h中我们将添加结构体:PrimMeshInfo,SceneDesc和GltfShadeMaterial。
// 用于检索最近命中着色器中基元信息的结构体
struct PrimMeshInfo
{
uint indexOffset;
uint vertexOffset;
int materialIndex;
};
// 各缓冲区地址
struct SceneDesc
{
uint64_t vertexAddress; //顶点缓冲区地址
uint64_t normalAddress; //普通缓冲区地址
uint64_t uvAddress; //纹理坐标缓冲区的地址
uint64_t indexAddress; //三角形索引缓冲区的地址
uint64_t materialAddress; //材质缓冲区的地址 (GltfShadeMaterial)
uint64_t primInfoAddress; //网格图元缓冲区的地址 (PrimMeshInfo)
};
此外,我们的 GLTF中用于着色的材质信息如下:
struct GltfShadeMaterial
{
vec4 pbrBaseColorFactor;
vec3 emissiveFactor;
int pbrBaseColorTexture;
};
为了保存创建的用于表示场景的所有缓冲区,我们将它们存储在下面列表中:
nvh::GltfScene m_gltfScene;
nvvk::Buffer m_vertexBuffer;
nvvk::Buffer m_normalBuffer;
nvvk::Buffer m_uvBuffer;
nvvk::Buffer m_indexBuffer;
nvvk::Buffer m_materialBuffer;
nvvk::Buffer m_primInfo;
nvvk::Buffer m_sceneDesc;
1.2 加载 GLTF
为了加载场景,我们将使用TinyGLTF来解析文件。。
我们将加载一个场景而不是加载模型,因此我们将替换loadModel()函数为loadScene()。
在源文件中,加载场景loadScene()将首先使用 TinyGLTF 导入 glTF。
tinygltf::Model tmodel;
tinygltf::TinyGLTF tcontext;
std::string warn, error;
if(!tcontext.LoadASCIIFromFile(&tmodel, &error, &warn, filename))
assert(!"Error while loading scene");
然后我们使用 gltfScene 帮助程序获取我们需要的信息。
m_gltfScene.importMaterials(tmodel);
m_gltfScene.importDrawableNodes(tmodel,
nvh::GltfAttributes::Normal | nvh::GltfAttributes::Texcoord_0);
下一部分是分配缓冲区来保存信息,例如位置、法线、纹理坐标等。
//在设备上创建缓冲区并复制顶点、索引和材质
nvvk::CommandPool cmdBufGet (m_device, m_graphicsQueueIndex);
VkCommandBuffer cmdBuf = cmdBufGet.createCommandBuffer();
m_vertexBuffer = m_alloc.createBuffer(cmdBuf, m_gltfScene.m_positions,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
| VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR);
m_indexBuffer = m_alloc.createBuffer(cmdBuf, m_gltfScene.m_indices,
VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
| VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR);
m_normalBuffer = m_alloc.createBuffer(cmdBuf, m_gltfScene.m_normals,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT
| VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
m_uvBuffer = m_alloc.createBuffer(cmdBuf, m_gltfScene.m_texcoords0,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT
| VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
我们只从 glTF 材质中提取几个如下的数据。
//只复制我们需要的元素所有材质
std::vector<GltfShadeMaterial> shadeMaterials;
for(auto& m : m_gltfScene.m_materials)
{
shadeMaterials.emplace_back(GltfShadeMaterial{m.baseColorFactor, m.emissiveFactor, m.baseColorTexture});
}
m_materialBuffer = m_alloc.createBuffer(cmdBuf, shadeMaterials,
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
为了在最近命中着色器中找到三角形命中的位置以及其他属性,我们将存储该几何体的偏移信息。
//以下用于查找CHIT中的原始mesh信息
std::vector<PrimMeshInfo> primLookup;
for(auto& primMesh : m_gltfScene.m_primMeshes)
{
primLookup.push_back({primMesh.firstIndex, primMesh.vertexOffset, primMesh.materialIndex});
}
m_rtPrimLookup =
m_alloc.createBuffer(cmdBuf, primLookup, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
最后,我们创建一个缓冲区,保存所有缓冲区的地址
SceneDesc sceneDesc;
sceneDesc.vertexAddress = nvvk::getBufferDeviceAddress(m_device, m_vertexBuffer.buffer);
sceneDesc.indexAddress = nvvk::getBufferDeviceAddress(m_device, m_indexBuffer.buffer);
sceneDesc.normalAddress = nvvk::getBufferDeviceAddress(m_device, m_normalBuffer.buffer);
sceneDesc.uvAddress = nvvk::getBufferDeviceAddress(m_device, m_uvBuffer.buffer);
sceneDesc.materialAddress = nvvk::getBufferDeviceAddress(m_device, m_materialBuffer.buffer);
sceneDesc.primInfoAddress = nvvk::getBufferDeviceAddress(m_device, m_primInfo.buffer);
m_sceneDesc = m_alloc.createBuffer(cmdBuf, sizeof(SceneDesc), &sceneDesc,
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
在关闭函数之前,我们将创建纹理(默认场景中没有)并提交命令缓冲区。在所有数据复制到 GPU后释放数据。
//创建所有找到的纹理
createTextureImages (cmdBuf, tmodel);
cmdBufGet.submitAndWait(cmdBuf);
m_alloc.finalizeAndReleaseStaging();
NAME_VK (m_vertexBuffer.buffer);
NAME_VK (m_indexBuffer.buffer);
NAME_VK (m_normalBuffer.buffer);
NAME_VK (m_uvBuffer.buffer);
NAME_VK (m_materialBuffer.buffer);
NAME_VK (m_primInfo.buffer);
NAME_VK (m_sceneDesc.buffer);
}
⚠️注意:宏NAME_VK可以方便地命名 Vulkan 对象,以便在 Nsight Graphics 中轻松识别它们并了解它的创建位置。
1.3 创建 BLAS
我们将使用primitiveToVkGeometry(const nvh::GltfPrimMesh& prim)取代objectToVkGeometryKHR(),两者功能类似,只是输入不同,此外对于结构体VkAccelerationStructureBuildRangeInfoKHR我们还包括偏移量数据。
// 在创建BLAS时,将GLTF图元转换成光追需要的几何数据
//
auto HelloVulkan::primitiveToGeometry(const nvh::GltfPrimMesh& prim)
{
// 缓冲区原始地址.
VkDeviceAddress vertexAddress = nvvk::getBufferDeviceAddress(m_device, m_vertexBuffer.buffer);
VkDeviceAddress indexAddress = nvvk::getBufferDeviceAddress(m_device, m_indexBuffer.buffer);
uint32_t maxPrimitiveCount = prim.indexCount / 3;
VkAccelerationStructureGeometryTrianglesDataKHR triangles{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR};
triangles.vertexFormat = VK_FORMAT_R32G32B32_SFLOAT; // vec3 vertex position data.
triangles.vertexData.deviceAddress = vertexAddress;
triangles.vertexStride = sizeof(nvmath::vec3f);
// Describe index data (32-bit unsigned int)
triangles.indexType = VK_INDEX_TYPE_UINT32;
triangles.indexData.deviceAddress = indexAddress;
//通过将transformData 设置为空设备指针来处理转换。
triangles.maxVertex = prim.vertexCount;
//将上述数据标识为包含不透明三角形。
VkAccelerationStructureGeometryKHR asGeom{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR};
asGeom.geometryType = VK_GEOMETRY_TYPE_TRIANGLES_KHR;
asGeom.flags = VK_GEOMETRY_NO_DUPLICATE_ANY_HIT_INVOCATION_BIT_KHR; // For AnyHit
asGeom.geometry.triangles = triangles;
VkAccelerationStructureBuildRangeInfoKHR offset;
offset.firstVertex = prim.vertexOffset;
offset.primitiveCount = prim.indexCount / 3;
offset.primitiveOffset = prim.firstIndex * sizeof(uint32_t);
offset.transformOffset = 0;
//我们的 blas 仅由一种几何图形组成,但可以由多种几何图形组成
nvvk::RaytracingBuilderKHR::BlasInput input;
input.asGeometry.emplace_back(asGeom);
input.asBuildOffsetInfo.emplace_back(offset);
return input;
}
1.4 创建 TLAS
除了几何的索引存储在primMesh中,与之前创建TLAS几乎没有什么不同、
for(auto& node : m_gltfScene.m_nodes)
{
VkAccelerationStructureInstanceKHR rayInst;
rayInst.transform = nvvk::toTransformMatrixKHR(node.worldMatrix);
rayInst.instanceCustomIndex = node.primMesh; // gl_InstanceCustomIndexEXT: to find which primitive
rayInst.accelerationStructureReference = m_rtBuilder.getBlasDeviceAddress(node.primMesh);
rayInst.flags = VK_GEOMETRY_INSTANCE_TRIANGLE_FACING_CULL_DISABLE_BIT_KHR;
rayInst.mask = 0xFF;
rayInst.instanceShaderBindingTableRecordOffset = 0; // We will use the same hit group for all objects
tlas.emplace_back(rayInst);
}
1.5 光栅化更改
光栅渲染很简单。着色器已更改为使用顶点、法线和纹理坐标。对于每个节点,我们将推送该图元正在使用的材质 ID。由于我们已经展平了场景图,我们可以遍历所有可绘制节点。
std::vector<VkBuffer> vertexBuffers = {m_vertexBuffer.buffer, m_normalBuffer.buffer, m_uvBuffer.buffer};
vkCmdBindVertexBuffers(cmdBuf, 0, static_cast<uint32_t>(vertexBuffers.size()), vertexBuffers.data(), offsets.data());
vkCmdBindIndexBuffer(cmdBuf, m_indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT32);
uint32_t idxNode = 0;
for(auto& node : m_gltfScene.m_nodes)
{
auto& primitive = m_gltfScene.m_primMeshes[node.primMesh];
m_pcRaster.modelMatrix = node.worldMatrix;
m_pcRaster.objIndex = node.primMesh;
m_pcRaster.materialId = primitive.materialIndex;
vkCmdPushConstants(cmdBuf, m_pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0,
sizeof(PushConstantRaster), &m_pcRaster);
vkCmdDrawIndexed(cmdBuf, primitive.indexCount, 1, primitive.firstIndex, primitive.vertexOffset, 0);
}
1.6 光线追踪更改
在createRtDescriptorSet()中,我们将添加的唯一更改是原始信息缓冲区,用于在击中三角形时检索数据。
m_rtDescSetLayoutBind.addBinding(ePrimLookup, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1,
VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR | VK_SHADER_STAGE_ANY_HIT_BIT_KHR); // Primitive info
// ...
VkDescriptorBufferInfo primitiveInfoDesc{m_rtPrimLookup.buffer, 0, VK_WHOLE_SIZE};
// ...
writes.emplace_back(m_rtDescSetLayoutBind.makeWrite(m_rtDescSet, ePrimLookup, &primitiveInfoDesc));
1.7 描述符与渲染管线结构体更改
由于我们使用了不同的缓冲区,并且顶点不再是一个结构体,而是使用了 3 个不同的缓冲区作为位置、法线和纹理坐标。在方法createDescriptorSetLayout(),updateDescriptorSet()和createGraphicsPipeline() 中将相应更改。
具体参照源码
1.8 Shader更改
着色与之前是相同的,并没有使用 glTF PBR 着色模型,但着色器仍需要更改以适应新的传入格式。
光栅化:
//顶点着色器
#version 450
#extension GL_ARB_separate_shader_objects : enable
#extension GL_EXT_scalar_block_layout : enable
#extension GL_GOOGLE_include_directive : enable
#extension GL_EXT_shader_explicit_arithmetic_types_int64 : require
#include "gltf.glsl"
#include "host_device.h"
layout(binding = 0) uniform _GlobalUniforms
{
GlobalUniforms uni;
};
layout(push_constant) uniform _PushConstantRaster
{
PushConstantRaster pcRaster;
};
layout(location = 0) in vec3 i_position;
layout(location = 1) in vec3 i_normal;
layout(location = 2) in vec2 i_texCoord;
layout(location = 1) out vec3 o_worldPos;
layout(location = 2) out vec3 o_worldNrm;
layout(location = 3) out vec3 o_viewDir;
layout(location = 4) out vec2 o_texCoord;
out gl_PerVertex
{
vec4 gl_Position;
};
void main()
{
vec3 origin = vec3(uni.viewInverse * vec4(0, 0, 0, 1));
o_worldPos = vec3(pcRaster.modelMatrix * vec4(i_position, 1.0));
o_viewDir = vec3(o_worldPos - origin);
o_texCoord = i_texCoord;
o_worldNrm = mat3(pcRaster.modelMatrix) * i_normal;
gl_Position = uni.viewProj * vec4(o_worldPos, 1.0);
}
//片段着色器
#version 450
#extension GL_ARB_separate_shader_objects : enable
#extension GL_EXT_nonuniform_qualifier : enable
#extension GL_GOOGLE_include_directive : enable
#extension GL_EXT_scalar_block_layout : enable
#extension GL_EXT_shader_explicit_arithmetic_types_int64 : require
#extension GL_EXT_buffer_reference2 : require
#include "gltf.glsl"
#include "host_device.h"
layout(push_constant) uniform _PushConstantRaster
{
PushConstantRaster pcRaster;
};
// clang-format off
// Incoming
layout(location = 1) in vec3 i_worldPos;
layout(location = 2) in vec3 i_worldNrm;
layout(location = 3) in vec3 i_viewDir;
layout(location = 4) in vec2 i_texCoord;
// Outgoing
layout(location = 0) out vec4 o_color;
// Buffers
layout(buffer_reference, scalar) buffer GltfMaterial { GltfShadeMaterial m[]; };
layout(set = 0, binding = eSceneDesc ) readonly buffer SceneDesc_ { SceneDesc sceneDesc; } ;
layout(set = 0, binding = eTextures) uniform sampler2D[] textureSamplers;
// clang-format on
void main()
{
// Material of the object
GltfMaterial gltfMat = GltfMaterial(sceneDesc.materialAddress);
GltfShadeMaterial mat = gltfMat.m[pcRaster.materialId];
vec3 N = normalize(i_worldNrm);
// Vector toward light
vec3 L;
float lightIntensity = pcRaster.lightIntensity;
if(pcRaster.lightType == 0)
{
vec3 lDir = pcRaster.lightPosition - i_worldPos;
float d = length(lDir);
lightIntensity = pcRaster.lightIntensity / (d * d);
L = normalize(lDir);
}
else
{
L = normalize(pcRaster.lightPosition);
}
// Diffuse
vec3 diffuse = computeDiffuse(mat, L, N);
if(mat.pbrBaseColorTexture > -1)
{
uint txtId = mat.pbrBaseColorTexture;
vec3 diffuseTxt = texture(textureSamplers[nonuniformEXT(txtId)], i_texCoord).xyz;
diffuse *= diffuseTxt;
}
// Specular
vec3 specular = computeSpecular(mat, i_viewDir, L, N);
// Result
o_color = vec4(lightIntensity * (diffuse + specular), 1);
}
光线追踪:
//光线生成着色器
#version 460
#extension GL_EXT_ray_tracing : require
#extension GL_GOOGLE_include_directive : enable
#extension GL_EXT_shader_explicit_arithmetic_types_int64 : require
#include "raycommon.glsl"
#include "sampling.glsl"
#include "host_device.h"
// clang-format off
layout(location = 0) rayPayloadEXT hitPayload prd;
layout(set = 0, binding = 0) uniform accelerationStructureEXT topLevelAS;
layout(set = 0, binding = 1, rgba32f) uniform image2D image;
layout(set = 1, binding = 0) uniform _GlobalUniforms { GlobalUniforms uni; };
layout(push_constant) uniform _PushConstantRay { PushConstantRay pcRay; };
// clang-format on
void main()
{
const vec2 pixelCenter = vec2(gl_LaunchIDEXT.xy) + vec2(0.5);
const vec2 inUV = pixelCenter / vec2(gl_LaunchSizeEXT.xy);
vec2 d = inUV * 2.0 - 1.0;
vec4 origin = uni.viewInverse * vec4(0, 0, 0, 1);
vec4 target = uni.projInverse * vec4(d.x, d.y, 1, 1);
vec4 direction = uni.viewInverse * vec4(normalize(target.xyz), 0);
uint rayFlags = gl_RayFlagsOpaqueEXT;
float tMin = 0.001;
float tMax = 10000.0;
traceRayEXT(topLevelAS, // acceleration structure
rayFlags, // rayFlags
0xFF, // cullMask
0, // sbtRecordOffset
0, // sbtRecordStride
0, // missIndex
origin.xyz, // ray origin
tMin, // ray min range
direction.xyz, // ray direction
tMax, // ray max range
0 // payload (location = 0)
);
imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(prd.hitValue, 1.0));
}
//最近命中着色器
#version 460
#extension GL_EXT_ray_tracing : require
#extension GL_EXT_nonuniform_qualifier : enable
#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 "gltf.glsl"
#include "raycommon.glsl"
#include "host_device.h"
hitAttributeEXT vec2 attribs;
// clang-format off
layout(location = 0) rayPayloadInEXT hitPayload prd;
layout(location = 1) rayPayloadEXT bool isShadowed;
layout(set = 0, binding = 0 ) uniform accelerationStructureEXT topLevelAS;
layout(set = 0, binding = 2) readonly buffer _InstanceInfo {PrimMeshInfo primInfo[];};
//layout(set = 1, binding = B_MATERIALS) readonly buffer _MaterialBuffer {GltfShadeMaterial materials[];};
layout(buffer_reference, scalar) readonly buffer Vertices { vec3 v[]; };
layout(buffer_reference, scalar) readonly buffer Indices { uint i[]; };
layout(buffer_reference, scalar) readonly buffer Normals { vec3 n[]; };
layout(buffer_reference, scalar) readonly buffer TexCoords { vec2 t[]; };
layout(buffer_reference, scalar) readonly buffer Materials { GltfShadeMaterial m[]; };
layout(set = 1, binding = eSceneDesc ) readonly buffer SceneDesc_ { SceneDesc sceneDesc; };
layout(set = 1, binding = eTextures) uniform sampler2D texturesMap[]; // all textures
// clang-format on
layout(push_constant) uniform Constants
{
vec4 clearColor;
vec3 lightPosition;
float lightIntensity;
int lightType;
}
pushC;
void main()
{
// Retrieve the Primitive mesh buffer information
PrimMeshInfo pinfo = primInfo[gl_InstanceCustomIndexEXT];
// Getting the 'first index' for this mesh (offset of the mesh + offset of the triangle)
uint indexOffset = pinfo.indexOffset + (3 * gl_PrimitiveID);
uint vertexOffset = pinfo.vertexOffset; // Vertex offset as defined in glTF
uint matIndex = max(0, pinfo.materialIndex); // material of primitive mesh
Materials gltfMat = Materials(sceneDesc.materialAddress);
Vertices vertices = Vertices(sceneDesc.vertexAddress);
Indices indices = Indices(sceneDesc.indexAddress);
Normals normals = Normals(sceneDesc.normalAddress);
TexCoords texCoords = TexCoords(sceneDesc.uvAddress);
Materials materials = Materials(sceneDesc.materialAddress);
// Getting the 3 indices of the triangle (local)
ivec3 triangleIndex = ivec3(indices.i[indexOffset + 0], indices.i[indexOffset + 1], indices.i[indexOffset + 2]);
triangleIndex += ivec3(vertexOffset); // (global)
const vec3 barycentrics = vec3(1.0 - attribs.x - attribs.y, attribs.x, attribs.y);
// Vertex of the triangle
const vec3 pos0 = vertices.v[triangleIndex.x];
const vec3 pos1 = vertices.v[triangleIndex.y];
const vec3 pos2 = vertices.v[triangleIndex.z];
const vec3 position = pos0 * barycentrics.x + pos1 * barycentrics.y + pos2 * barycentrics.z;
const vec3 world_position = vec3(gl_ObjectToWorldEXT * vec4(position, 1.0));
// Normal
const vec3 nrm0 = normals.n[triangleIndex.x];
const vec3 nrm1 = normals.n[triangleIndex.y];
const vec3 nrm2 = normals.n[triangleIndex.z];
vec3 normal = normalize(nrm0 * barycentrics.x + nrm1 * barycentrics.y + nrm2 * barycentrics.z);
const vec3 world_normal = normalize(vec3(normal * gl_WorldToObjectEXT));
const vec3 geom_normal = normalize(cross(pos1 - pos0, pos2 - pos0));
// TexCoord
const vec2 uv0 = texCoords.t[triangleIndex.x];
const vec2 uv1 = texCoords.t[triangleIndex.y];
const vec2 uv2 = texCoords.t[triangleIndex.z];
const vec2 texcoord0 = uv0 * barycentrics.x + uv1 * barycentrics.y + uv2 * barycentrics.z;
// Vector toward the light
vec3 L;
float lightIntensity = pushC.lightIntensity;
float lightDistance = 100000.0;
// Point light
if(pushC.lightType == 0)
{
vec3 lDir = pushC.lightPosition - world_position;
lightDistance = length(lDir);
lightIntensity = pushC.lightIntensity / (lightDistance * lightDistance);
L = normalize(lDir);
}
else // Directional light
{
L = normalize(pushC.lightPosition - vec3(0));
}
// Material of the object
GltfShadeMaterial mat = materials.m[matIndex];
// Diffuse
vec3 diffuse = computeDiffuse(mat, L, world_normal);
if(mat.pbrBaseColorTexture > -1)
{
uint txtId = mat.pbrBaseColorTexture;
diffuse *= texture(texturesMap[nonuniformEXT(txtId)], texcoord0).xyz;
}
vec3 specular = vec3(0);
float attenuation = 1;
// Tracing shadow ray only if the light is visible from the surface
if(dot(world_normal, L) > 0)
{
float tMin = 0.001;
float tMax = lightDistance;
vec3 origin = gl_WorldRayOriginEXT + gl_WorldRayDirectionEXT * gl_HitTEXT;
vec3 rayDir = L;
uint flags = gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsOpaqueEXT | gl_RayFlagsSkipClosestHitShaderEXT;
isShadowed = true;
traceRayEXT(topLevelAS, // acceleration structure
flags, // rayFlags
0xFF, // cullMask
0, // sbtRecordOffset
0, // sbtRecordStride
1, // missIndex
origin, // ray origin
tMin, // ray min range
rayDir, // ray direction
tMax, // ray max range
1 // payload (location = 1)
);
if(isShadowed)
{
attenuation = 0.3;
}
else
{
// Specular
specular = computeSpecular(mat, gl_WorldRayDirectionEXT, L, world_normal);
}
}
prd.hitValue = vec3(lightIntensity * attenuation * (diffuse + specular));
}
1.8 其他更改
相机位置
CameraManip.setLookat (nvmath :: vec3f ( 0 , 0 , 15 ), nvmath :: vec3f ( 0 , 0 , 0 ), nvmath :: vec3f ( 0 , 1 , 0 ));
场景模型
helloVk.loadScene(nvh::findFile( " media/scenes/cornellBox.gltf " , defaultSearchPaths, true ));
灯光位置
nvmath::vec3f lightPosition{ 0 . f , 4。5f , 0。f };
二、简单路径追踪
要将这个示例转换为简单的路径跟踪(可科学上网Wikipedia Path Tracing),我们需要更改RayGen和ClosestHit着色器。在此之前,我们需要修改应用程序以发送保存当前渲染帧,从而允许像素叠加。
在hello_vulkan.cpp中添加以下两个函数:
void updateFrame();
void resetFrame();
// ------------------------------------------------ --------------------------------------------------
//如果相机矩阵发生变化,则重置帧。
//否则,增加帧。
//
void HelloVulkan::updateFrame()
{
static nvmath::mat4f refCamMatrix;
static float refFov{CameraManip.getFov()};
const auto& m = CameraManip.getMatrix();
const auto fov = CameraManip.getFov();
if(memcmp(&refCamMatrix.a00, &m.a00, sizeof(nvmath::mat4f)) != 0 || refFov != fov)
{
resetFrame();
refCamMatrix = m;
refFov = fov;
}
m_pcRay.frame++;
}
void HelloVulkan::resetFrame()
{
m_pcRay.frame = -1;
}
并在raytrace()函数的开头调用updateFrame()。
并在RtPushConstant结构体末尾添加一个新成员。
2.1 光线生成着色器更改
在光线生成着色器中需要进行一些修改:首先,它将使用时钟作为其随机种子数。
这需要通过添加GL_ARB_shader_clock扩展来完成的。
#extension GL_ARB_shader_clock : enable
随机数的生成在sampling.glsl中,并#include这个文件中。
//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);
}
在main()中,我们将像这样初始化随机数:(
//初始化随机数
uint seed = tea(gl_LaunchIDEXT.y * gl_LaunchSizeEXT.x + gl_LaunchIDEXT.x, int (clockARB()));
为了累积帧样本,我们不仅会写入图像,还会使用上一帧数据。
//随着时间的推移进行帧迭代
if(pcRay.frame > 0)
{
float a = 1.0f / float(pcRay.frame + 1);
vec3 old_color = imageLoad(image, ivec2(gl_LaunchIDEXT.xy)).xyz;
imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(mix(old_color, hitValue, a), 1.f));
}
else
{
//第一帧中,取代在缓冲器中的值
imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(hitValue, 1.f));
}
有效光路载荷hitPayload结构体也需要进行如下修改:
struct hitPayload
{
vec3 hitValue;
uint seed;
uint depth;
};
2.2 最近命中着色器更改
此修改将递归光线的深度depth硬编码为10或命中发光元素为止。
我们从着色器中保留的信息仅需要后续用于计算的命中点状态信息:位置、法线。代码修改如下:
// https://en.wikipedia.org/wiki/Path_tracing
// 命中点材质
GltfMaterial mat = materials[nonuniformEXT(matIndex)];
vec3 emittance = mat.emissiveFactor;
// 随机选一个方向,然后继续执行光追.
vec3 tangent, bitangent;
createCoordinateSystem(world_normal, tangent, bitangent);
vec3 rayOrigin = world_position;
vec3 rayDirection = samplingHemisphere(prd.seed, tangent, bitangent, world_normal);
// 新光线的概率(余弦分布)
const float p = 1 / M_PI;
// 计算这条射线的BRDF(假设Lambertian反射)
float cos_theta = dot(rayDirection, world_normal);
vec3 BRDF = mat.pbrBaseColorFactor.xyz / M_PI;
// 递归追踪反射光源
if(prd.depth < 10)
{
prd.depth++;
float tMin = 0.001;
float tMax = 100000000.0;
uint flags = gl_RayFlagsOpaqueEXT;
traceRayEXT(topLevelAS, // acceleration structure
flags, // rayFlags
0xFF, // cullMask
0, // sbtRecordOffset
0, // sbtRecordStride
0, // missIndex
rayOrigin, // ray origin
tMin, // ray min range
rayDirection, // ray direction
tMax, // ray max range
0 // payload (location = 0)
);
}
vec3 incoming = prd.hitValue;
// 应用渲染方程
prd.hitValue = emittance + (BRDF * incoming * cos_theta / p);
⚠️ 注意:我们不像光栅化器那样实现点光源。因此,只有自发光几何体会发射能量来照亮场景。
2.3 未命中着色器更改
为了避免来自环境的能量贡献。我们修改未命中着色器
void main()
{
if(prd.depth == 0)
prd.hitValue = clearColor.xyz * 0.8;
else
prd.hitValue = vec3(0.01); // 来自环境的微小贡献
prd.depth = 100; // Ending trace
}
三、优化路径追踪
上面的实现是在最近命中着色器中递归进行路径追踪,这不是最优的。如 反射 教程中所述,最好是在光线生成着色器中打破递归性.
因此可进行以下更改提高帧率(可使渲染速度提高 3 倍)。
为了能够做到这一点,我们需要扩展射线payload结构以将数据从Closest Hit传输到RayGen中,即射线原点和方向以及 BRDF 权重三个数据。因此修改hitPayload结构体:
struct hitPayload
{
vec3 hitValue;
uint seed;
uint depth;
vec3 rayOrigin;
vec3 rayDirection;
vec3 weight;
};
3.1 最近命中着色器更改
我们不再需要追踪光线,所以在追踪一条新光线之前,我们可以将信息存储payload值,并在递归代码之前返回。
prd.rayOrigin = rayOrigin;
prd.rayDirection = rayDirection;
prd.hitValue = emittance;
prd.weight = BRDF * cos_theta / p;
return;
3.1 光线生成着色器更改
光线生成将执行光线追踪循环。
首先,初始化payload和局部变量来计算光线循环叠加。
prd.rayOrigin = origin.xyz;
prd.rayDirection = direction.xyz;
prd.weight = vec3(0);
vec3 curWeight = vec3(1);
vec3 hitValue = vec3(0);
现在光线追踪函数的循环将如下所示。
⚠️ 注意:递归深度现在是硬编码,也可以通过push constant传递.
for(; prd.depth < 10; prd.depth++)
{
traceRayEXT(topLevelAS, // acceleration structure
rayFlags, // rayFlags
0xFF, // cullMask
0, // sbtRecordOffset
0, // sbtRecordStride
0, // missIndex
prd.rayOrigin, // ray origin
tMin, // ray min range
prd.rayDirection, // ray direction
tMax, // ray max range
0 // payload (location = 0)
);
hitValue += prd.hitValue * curWeight;
curWeight *= prd.weight;
}
最后不要忘记在imageStore中使用hitValue数据,完整的光线生成着色器如下:
#version 460
#extension GL_EXT_ray_tracing : require
#extension GL_GOOGLE_include_directive : enable
#extension GL_ARB_shader_clock : enable
#extension GL_EXT_shader_explicit_arithmetic_types_int64 : require
#include "raycommon.glsl"
#include "sampling.glsl"
#include "host_device.h"
// clang-format off
layout(location = 0) rayPayloadEXT hitPayload prd;
layout(set = 0, binding = 0) uniform accelerationStructureEXT topLevelAS;
layout(set = 0, binding = 1, rgba32f) uniform image2D image;
layout(set = 1, binding = 0) uniform _GlobalUniforms { GlobalUniforms uni; };
layout(push_constant) uniform _PushConstantRay { PushConstantRay pcRay; };
// clang-format on
void main()
{
// Initialize the random number
uint seed = tea(gl_LaunchIDEXT.y * gl_LaunchSizeEXT.x + gl_LaunchIDEXT.x, int(clockARB()));
const vec2 pixelCenter = vec2(gl_LaunchIDEXT.xy) + vec2(0.5);
const vec2 inUV = pixelCenter / vec2(gl_LaunchSizeEXT.xy);
vec2 d = inUV * 2.0 - 1.0;
vec4 origin = uni.viewInverse * vec4(0, 0, 0, 1);
vec4 target = uni.projInverse * vec4(d.x, d.y, 1, 1);
vec4 direction = uni.viewInverse * vec4(normalize(target.xyz), 0);
uint rayFlags = gl_RayFlagsOpaqueEXT;
float tMin = 0.001;
float tMax = 10000.0;
prd.hitValue = vec3(0);
prd.seed = seed;
prd.depth = 0;
prd.rayOrigin = origin.xyz;
prd.rayDirection = direction.xyz;
prd.weight = vec3(0);
vec3 curWeight = vec3(1);
vec3 hitValue = vec3(0);
for(; prd.depth < 10; prd.depth++)
{
traceRayEXT(topLevelAS, // acceleration structure
rayFlags, // rayFlags
0xFF, // cullMask
0, // sbtRecordOffset
0, // sbtRecordStride
0, // missIndex
prd.rayOrigin, // ray origin
tMin, // ray min range
prd.rayDirection, // ray direction
tMax, // ray max range
0 // payload (location = 0)
);
hitValue += prd.hitValue * curWeight;
curWeight *= prd.weight;
}
// Do accumulation over time
if(pcRay.frame > 0)
{
float a = 1.0f / float(pcRay.frame + 1);
vec3 old_color = imageLoad(image, ivec2(gl_LaunchIDEXT.xy)).xyz;
imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(mix(old_color, hitValue, a), 1.f));
}
else
{
// First frame, replace the value in the buffer
imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(hitValue, 1.f));
}
}
运行结果如下图: