什么是光线追踪?
光线追踪(Raytracing)是通过模拟现实世界中光线的传播过程并生成更加真实的效果的一种图形渲染技术。
早期在电影,动画,设计等领域已经使用软件摸拟光线追踪来渲染更加真实的图像。一般的做法是从相机位置向每个像素点发射光线,经过与物体表面发生一定次数的反射和折射,最终得到每个像素点的颜色并形成图像。
软件摸拟光线追踪依赖于CPU的计算,由于计算过程较为复杂,并且需要大量的计算,通常会花费很长的时间。因此高通公司设计了专门用于GPU硬件加速的计算单元,能够显著提高光线追踪的计算速度,让实时光线追踪成为可能。
实时光线追踪在高通平台的效果展示
左图是开启光线追踪的阴影效果,右图是使用传统管线的阴影效果
左图是开启光线追踪的反射效果,右图是使用SSR实现的反射效果
移动平台的硬件光线追踪
移动平台使用Vulkan Api来实现硬件光线追踪的功能。Vulkan支持两种类型的光线追踪Ray Query和Ray Pipeline。
Ray Query主要用于生成光线,与场景求交并获得交点的几何信息。它通常使用在Compute Shader或Fragment Shader中。
Ray Pipeline可以实现完整的光线追踪管线,不依赖于传统的渲染管线。它包含了Ray Generation Shader,Miss Shader和Closest hit shader以及可选的Intersection Shader和Any hit shader。
目前高通平台只支持Ray Query,将在后续的平台实现对Ray Pipeline的支持。
为了提高渲染时的效率,光线追踪需要将几何图形组织到加速结构(Acceleration Structure
)中,用于减少渲染过程中光线与三角形相交测试的次数。
在Vulkan中,硬件主要分成了两个层次的加速结构:顶层加速结构(top-level acceleration structure)和底层加速结构(bottom-level acceleration structures)。
顶层加速结构用于引用任意数量的底层加速结构,并且可以设置矩阵用于表示底层加速结构的位置。
底层加速结构对应场景中独立的三维模型,用于储存三维模型的顶点以及索引数据。
当调用Vulkan Api构建好顶层加速结构,驱动会构建好相应的BVH(Bounding Volume Hierarchy)。这时我们在Shader中使用Ray Query进行光线求交时,速度可以得到很大的提升。
如何使用硬件光线追踪技术?
使用硬件光线追踪技术首先需要使用第二代骁龙8或更高的移动平台,并且Vulkan版本需要不低于1.2.162.
接下来重点说一下在Vulkan中启用Ray Query的注意要点。
在vkCreateDevice时需要添加几个扩展
- VK_KHR_acceleration_structure – 此扩展用于创建和管理加速结构
通过查询VkPhysicalDeviceAccelerationStructureFeaturesKHR结构体,可以得到该设备支持哪些扩展功能。
同时把需要开启的扩展功能设置成VK_TRUE,并把该结构体传入pEnabledFeatures。
- VK_KHR_buffer_device_address – 此扩展允许应用程序查询buffer的64位GPU地址
通过查询VkPhysicalDeviceBufferDeviceAddressFeaturesKHR结构体,可以得到该设备支持哪些扩展功能。
同时把需要开启的扩展功能设置成VK_TRUE,并把该结构体传入pEnabledFeatures。
- VK_KHR_ray_query – 此扩展为所有的着色器阶段提供光线查询相关的函数
通过查询VkPhysicalDeviceRayQueryFeaturesKHR结构体,可以得到该设备支持哪些扩展功能。
同时把需要开启的扩展功能设置成VK_TRUE,并把该结构体传入pEnabledFeatures。
为了提高光线跟踪的性能,接下来需要创建加速结构。
创建底层加速结构需要把对应的模型数据写入VkAccelerationStructureGeometryKHR。需要注意的是,创建模型数据时的VertexBuffer和IndexBuffer需要添加VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,以便使用vkGetBufferDeviceAddressKHR来获取Buffer的设备地址。
VkAccelerationStructureGeometryKHR ASGeometry = {};
ASGeometry.flags = VK_GEOMETRY_OPAQUE_BIT_KHR;
ASGeometry.geometryType = VK_GEOMETRY_TYPE_TRIANGLES_KHR;
ASGeometry.geometry.triangles.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_TRIANGLES_DATA_KHR;
ASGeometry.geometry.triangles.vertexFormat = VK_FORMAT_R32G32B32_SFLOAT;
ASGeometry.geometry.triangles.vertexData = VertexBufferDeviceAddress;
ASGeometry.geometry.triangles.maxVertex = VerticeCount;
ASGeometry.geometry.triangles.vertexStride = sizeof(Vertex);
ASGeometry.geometry.triangles.indexType = VK_INDEX_TYPE_UINT32;
ASGeometry.geometry.triangles.indexData = IndexBufferDeviceAddress;
ASGeometry.geometry.triangles.transformData.deviceAddress = 0;
ASGeometry.geometry.triangles.transformData.hostAddress = nullptr;
接下来需要使用vkGetAccelerationStructureBuildSizesKHR来获取需要为加速结构分配的内存大小。
VkAccelerationStructureBuildGeometryInfoKHR ASGeometryInfo = {};
ASGeometryInfo.type = VK_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL_KHR;
ASGeometryInfo.flags = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR;
ASGeometryInfo.geometryCount = 1;
ASGeometryInfo.pGeometries = &ASGeometry;
VkAccelerationStructureBuildSizesInfoKHR ASBuildSizesInfo = {};
vkGetAccelerationStructureBuildSizesKHR(
device,
VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR,
&ASGeometryInfo,
&numTriangles,
&ASBuildSizesInfo);
创建VkAccelerationStructureKHR前需要先创建与之对应的VkBuffer,此VkBuffer的usage需要设置成VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT。用vkGetAccelerationStructureBuildSizesKHR获取的大小来申请内存,最后使用vkCreateAccelerationStructureKHR来创建VkAccelerationStructureKHR。最终调用vkCmdBuildAccelerationStructuresKHR来构建加速结构,生成对应的BVH。
至此一个底层加速结构已经构建完成,后面可以根据场景的模型数量继续构建其它的底层加速结构。
接下来创建顶层加速结构。底层加速结构需要的是几何信息,顶层加速结构需要的是实例(Instance)信息。第一个实例对应一个底层加速结构,创建的代码如下所示:
VkAccelerationStructureInstanceKHR instance{};
instance.transform = IdentityMatrix;
instance.instanceCustomIndex = 0;
instance.mask = 0xFF;
instance.instanceShaderBindingTableRecordOffset = 0;
instance.flags = VK_GEOMETRY_INSTANCE_TRIANGLE_FACING_CULL_DISABLE_BIT_KHR;
instance.accelerationStructureReference = BLASDeviceAddress;
定义好了所有的Instance,需要创建一个Instance Buffer把数据存放在GPU中。接下来获取需要为加速结构分配的内存大小。
VkAccelerationStructureGeometryKHR ASGeometry = {};
ASGeometry.geometryType = VK_GEOMETRY_TYPE_INSTANCES_KHR;
ASGeometry.flags = VK_GEOMETRY_OPAQUE_BIT_KHR;
ASGeometry.geometry.instances.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_INSTANCES_DATA_KHR;
ASGeometry.geometry.instances.arrayOfPointers = VK_FALSE;
ASGeometry.geometry.instances.data = InstanceBufferDeviceAddress;
VkAccelerationStructureBuildGeometryInfoKHR ASBuildGeometryInfo = vks::initializers::accelerationStructureBuildGeometryInfoKHR();
ASBuildGeometryInfo.type = VK_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL_KHR;
ASBuildGeometryInfo.flags = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR;
ASBuildGeometryInfo.geometryCount = 1;
ASBuildGeometryInfo.pGeometries = &ASGeometry;
VkAccelerationStructureBuildSizesInfoKHR ASBuildSizesInfo = {};
vkGetAccelerationStructureBuildSizesKHR(
device,
VK_ACCELERATION_STRUCTURE_BUILD_TYPE_DEVICE_KHR,
&ASBuildGeometryInfo,
&primitive_count,
&ASBuildSizesInfo);
获取到加速结构的大小后,与创建底层加速结构一样,先创建一个VkBuffer,然后再创建对应的顶层加速结构。
创建顶层加速结构后,需要把它绑定到对应的Descriptor Set,这样我们就可以在shader中使用加速结构了。
在编写GLSL时需要使用OpenGL GLSL 4.60(#version 460)或者更高的版本。使用下面的代码启用ray query扩展以及定义加速结构。
#extension GL_EXT_ray_query : enable
layout (binding = 1, set = 0) uniform accelerationStructureEXT TLAS;
使用ray query与场景求交的代码如下所示:
rayQueryEXT rq;
rayQueryInitializeEXT(rq, TLAS, gl_RayFlagsTerminateOnFirstHitEXT, cullMask, origin, tMin, direction, tMax);
rayQueryProceedEXT(rq);
if (rayQueryGetIntersectionTypeEXT(rq, true) == gl_RayQueryCommittedIntersectionNoneEXT) {
// 这里编写求交成功后的代码
}
光线追踪的最佳实践
创建加速结构时,如果设置成fast build,可以减少构建加速结构的时间。如果设置成fast trace,可以减少在使用ray query求交的时间。
对于静态物体,可以在场景预加载时生成对应的BLAS,同时设置成fast trace。这样我们在渲染场景时,就不需要再构建加速结构,并且可以得到最佳的ray query性能。
对于动态物体,尽量使用refit的方式去构建BLAS。
对于顶层加速结构,建议设置成fast trace,并且使用rebuild的方式每帧更新。
尽可能的减少ray query shader的复杂度。最好是只在shader里做ray query的相关操作,把结果保存到Texture或者Buffer中,然后使用得到的结果去实现不同的效果。
Ray query默认会求交多次并找出最近的交点。当只需要查询是否有交点的情况下,建议在shader中加上gl_RayFlagsTerminateOnFirstHitEXT标志,这样ray query只要找到交点就会停止继续求交,可以减少不必要的操作。
作者:
刘煜彤,高通工程师