概要
vulkan ray tracingのhello world(hello triangle)のサンプルコードを例としてビルドできた後に、次のステップとして複数オブジェクトを描画する方法を記事にする。
サンプルコード
・Khronos公式
三角形をひとつ描画
https://github.com/KhronosGroup/Vulkan-Samples/tree/master/samples/extensions/raytracing_basic
・NVIDIA Vulkan Ray Tracing Tutorial
Cubeをひとつ描画
https://nvpro-samples.github.io/vk_raytracing_tutorial_KHR/#accelerationstructure/top-levelaccelerationstructure/helperdetails:raytracingbuilder::buildtlas()
複数オブジェクトの描画について
Khronos公式より、Acceleration Structureの階層図は以下のようになっている。
引用:https://www.khronos.org/blog/vulkan-ray-tracing-best-practices-for-hybrid-rendering figure1. Organisation and interaction of the bottom and top acceleration structures with the shader binding table
複数オブジェクトを描画する方法はいくつか考えられるが、この図中の単語を用いれば以下が考えられる。
① Top Level AS, Instance, BLASをひとつだけ用意する。BLASに格納するひとつのVertex Buffer, Index Bufferに、offsetなどを利用して複数のオブジェクトのデータを入れる。
② ひとつのTop Level ASに複数のInstanceを生成する。各InstanceにBLASを一つずつ割り当てる。この場合にはBLAS, Vertex Buffer, Index Bufferはオブジェクトの数だけ準備する。
bufferの準備が大変だが、ray tracingのアーキテクチャー的に最も簡単な方法は①である。しかし、この方法では増え続けるオブジェクトの数には対応しきれないため、②の方法で描画できないかを試してみた。
rchitシェーダ
vulkan ray tracingにおいて、光が当たった頂点についての動作を記述するシェーダがrchitシェーダである。サンプルコードの2例目では、rchitシェーダからvertex/indexにアクセスする方法として、アプリ側でvertex buffer, index bufferをstorage bufferとして用意しておき、それをdescriptor setとしてシェーダに渡す方法が示されている。
以下はそのコードを基にした記述例
#version 460
#extension GL_EXT_ray_tracing : enable
#extension GL_EXT_scalar_block_layout : enable
#extension GL_EXT_nonuniform_qualifier : enable
struct Vertex3D
{
vec3 pos;
vec3 color;
vec3 normal;
};
layout(location = 0) rayPayloadInEXT vec3 hitValue;
hitAttributeEXT vec3 attribs;
layout(binding = 2, set = 0) uniform CameraProperties
{
mat4 viewInverse;
mat4 projInverse;
mat4 modelViewProj;
} cam;
layout(binding = 3, set = 0, scalar) buffer Vertices {Vertex3D v[];} vertices[];
layout(binding = 4, set = 0) buffer Indices {uint i[];} indices[];
layout(push_constant) uniform Constants
{
vec4 clearColor;
vec3 lightPosition;
float lightIntensity;
int lightType;
}pushC;
void main()
{
uint objId = gl_InstanceCustomIndexEXT;
ivec3 ind = ivec3(indices[nonuniformEXT(objId)].i[3 * gl_PrimitiveID + 0], //
indices[nonuniformEXT(objId)].i[3 * gl_PrimitiveID + 1], //
indices[nonuniformEXT(objId)].i[3 * gl_PrimitiveID + 2]); //
Vertex3D v0 = vertices[nonuniformEXT(objId)].v[ind.x];
Vertex3D v1 = vertices[nonuniformEXT(objId)].v[ind.y];
Vertex3D v2 = vertices[nonuniformEXT(objId)].v[ind.z];
const vec3 barycentricCoords = vec3(1.0f - attribs.x - attribs.y, attribs.x, attribs.y);
vec3 normal = v0.normal * barycentricCoords.x + v1.normal * barycentricCoords.y + v2.normal * barycentricCoords.z;
vec4 normalP = normalize(cam.modelViewProj * vec4(normal, 0.0));
vec3 worldPos = v0.pos * barycentricCoords.x + v1.pos * barycentricCoords.y + v2.pos * barycentricCoords.z;
vec4 worldP = cam.modelViewProj * vec4(worldPos, 0.0);
vec3 L;
float lightIntensity = pushC.lightIntensity;
float lightDistance = 10000.0;
vec3 lDir = pushC.lightPosition - worldP.xyz;
lightDistance = length(lDir);
lightIntensity = lightIntensity / (lightDistance * lightDistance);
L = normalize(lDir);
float dotNL = min(lightIntensity * max(dot(normalP.xyz, L), 0.1), 1.0);
hitValue = vec3(dotNL);
}
bindingの位置などは各自の環境に合わせて変更が必要である。重要なのは、以下のようにしてオブジェクトのvertexにアクセスできるということである。
layout(binding = 3, set = 0, scalar) buffer Vertices {Vertex3D v[];} vertices[];
uint objId = gl_InstanceCustomIndexEXT;
Vertex3D v0 = vertices[nonuniformEXT(objId)].v[ind.x];
Top Level ASの変更
rchitシェーダ内で頂点にアクセスできる方法はわかったが、このシェーダ内ではInstanceCustomIndexという値を使用してbufferの位置を特定している。InstanceCustomIndexはTLAS(Top Level AS)内で指定するので、BLASごとにindexを割り当てるのだが、この方法が分かりづらかったので、記事に残しておきたいと思った。
Instance
VkAccelerationStructureInstanceKHRは生成したいinstanceの数だけ記述する。
for(uint32_t i = 0; i < bottomCount; i++)
{
VkAccelerationStructureInstanceKHR instance{};
instance.transform = mTransformMatrix;
instance.instanceCustomIndex = (uint16_t)i;
instance.mask = 0xFF;
instance.instanceShaderBindingTableRecordOffset = 0;
instance.flags = VK_GEOMETRY_INSTANCE_TRIANGLE_FACING_CULL_DISABLE_BIT_KHR;
instance.accelerationStructureReference = bottoms[i]->GetDeviceAddress();
mInstances.push_back(instance);
}
instance bufferの生成
instance bufferはひとつだけ用意する。ひとつのinstance bufferに複数のinstance情報をコピーする。
Geometry
geometryはひとつだけ記述する。後述の通りgeometryがひとつしか書けないために、上記のinstance buferはひとつしか生成しないのかもしれない。
VkAccelerationStructureGeometryKHR geometry = {};
geometry.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR;
geometry.geometryType = VK_GEOMETRY_TYPE_INSTANCES_KHR;
geometry.flags = VK_GEOMETRY_OPAQUE_BIT_KHR;
geometry.geometry.instances.sType = VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_INSTANCES_DATA_KHR;
geometry.geometry.instances.arrayOfPointers = VK_FALSE;
geometry.geometry.instances.data.deviceAddress = instaceBufferAddress
GeometryInfo
geometry infoはひとつだけ用意する。Khronos公式のページにTLASの場合、geometry info内のgeometry countは"must be 1"と書かれてある。
https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkAccelerationStructureBuildGeometryInfoKHR.html
##build range info
VkAccelerationStructureBuildRangeInfoKHRの数は1つだが、メンバのprimitiveCountに生成するinstanceの数を記述する。
VkAccelerationStructureBuildRangeInfoKHR mRangeInfo = {};
mRangeInfo.primitiveCount = mInstances.size();
mRangeInfo.primitiveOffset = 0;
mRangeInfo.firstVertex = 0;
mRangeInfo.transformOffset = 0;
std::vector<VkAccelerationStructureBuildRangeInfoKHR*> rangeInfos{&mRangeInfo};
pfnCmdBuildAccelerationStructuresKHR(commandBuffer, 1, &mBuildCommandGeometryInfo, rangeInfos.data());
#出力例
以下の例ではgroundとしての黒い平面とオブジェクトの黒い立方体の2つを用意している。平面上の一点から光を発生させ、平面と立方体の両方でライティングの計算ができていることがわかる。