This paper mainly refers to NVIDIA Vulkan Ray Tracing Tutorial Tutorials, environment configurations and programs can be implemented by referring to this document (personal level is limited, please refer to the original text if there are errors).
This section mainly shows how to render different elements with intersection shaders and different materials.
First, we need to know when to use this optional shader. For details, please refer to the description of light tracing rendering pipeline.
1, Usage scenario
This part is designed and used
- Add 2000000 axis aligned bounding boxes to BLAS
- Add 2 materials to the scene
- Create a sphere or cube every other intersecting object, and use one of the above two materials.
Therefore, we need to:
- Add intersection shader (. rint)
- Add a new recent hit shader (. chit)
- Create vkaccelerationstructureometrykhr from vkaccelerationstructureometryaabbsdatkhr.
2, Create data
At host_ In device. H, add the structure we need. The first is to define the structure of the sphere. And it can also be used to define the AABB box. This information retrieves and returns intersection point information in the intersection shader.
struct Sphere { vec3 center; float radius; };
Then we need to save the AABB structure of all spheres, which is also used to create Blas (vk_geometry_type_aabbs_khr).
struct Aabb { vec3 minimum; vec3 maximum; };
Then add the following macro definition to distinguish between sphere and cube
#define KIND_SPHERE 0 #define KIND_CUBE 1
All the above information needs to be saved in the buffer and can be accessed by the shader.
std::vector<Sphere> m_spheres; //All spheres nvvkBuffer m_spheresBuffer; //Save buffer for sphere nvvkBuffer m_spheresAabbBuffer; //Buffer for all AABS nvvkBuffer m_spheresMatColorBuffer; //Multiple materials nvvkBuffer m_spheresMatIndexBuffer; //Define which sphere uses which material
Finally, define two functions: one to create a sphere and the other to create intermediate data of BLAS, similar to the previous function objecttovkgeometriykhr().
void createSpheres (); auto sphereToVkGeometryKHR ();
After that, 2000000 spheres are created at random positions and radii. The Aabb bounding box is created according to the sphere definition, and the two materials will be assigned to each object alternately. And all the information created above will be moved to the Vulkan buffer so that the intersection and recent hit shaders can be accessed.
void HelloVulkan::createSpheres(uint32_t nbSpheres) { std::random_device rd{}; std::mt19937 gen{rd()}; std::normal_distribution<float> xzd{0.f, 5.f}; std::normal_distribution<float> yd{6.f, 3.f}; std::uniform_real_distribution<float> radd{.05f, .2f}; // Ball data m_spheres.resize(nbSpheres); for(uint32_t i = 0; i < nbSpheres; i++) { Sphere s; s.center = nvmath::vec3f(xzd(gen), yd(gen), xzd(gen)); s.radius = radd(gen); m_spheres[i] = std::move(s); } // Align the bounding box with the axis of each sphere std::vector<Aabb> aabbs; aabbs.reserve(nbSpheres); for(const auto& s : m_spheres) { Aabb aabb; aabb.minimum = s.center - nvmath::vec3f(s.radius); aabb.maximum = s.center + nvmath::vec3f(s.radius); aabbs.emplace_back(aabb); } // Two materials MaterialObj mat; mat.diffuse = nvmath::vec3f(0.2, 1, 0.2); std::vector<MaterialObj> materials; std::vector<int> matIdx(nbSpheres); materials.emplace_back(mat); mat.diffuse = nvmath::vec3f(1, 0.5, 0); materials.emplace_back(mat); // The material assigned to each sphere (the type is determined by i%2 in the shader) for(size_t i = 0; i < m_spheres.size(); i++) { matIdx[i] = i % 2; } // Create all buffers using vkBU = VkBufferUsageFlagBits; nvvk::CommandPool genCmdBuf(m_device, m_graphicsQueueIndex); auto cmdBuf = genCmdBuf.createCommandBuffer(); m_spheresBuffer = m_alloc.createBuffer(cmdBuf, m_spheres, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); m_spheresAabbBuffer = m_alloc.createBuffer(cmdBuf, aabbs, VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR); m_spheresMatIndexBuffer = m_alloc.createBuffer(cmdBuf, matIdx, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); m_spheresMatColorBuffer = m_alloc.createBuffer(cmdBuf, materials, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); genCmdBuf.submitAndWait(cmdBuf); // Nsight debug data m_debug.setObjectName(m_spheresBuffer.buffer, "spheres"); m_debug.setObjectName(m_spheresAabbBuffer.buffer, "spheresAabb"); m_debug.setObjectName(m_spheresMatColorBuffer.buffer, "spheresMat"); m_debug.setObjectName(m_spheresMatIndexBuffer.buffer, "spheresMatIdx"); // Add an additional instance to access the material buffer ObjDesc objDesc{}; objDesc.materialAddress = nvvk::getBufferDeviceAddress(m_device, m_spheresMatColorBuffer.buffer); objDesc.materialIndexAddress = nvvk::getBufferDeviceAddress(m_device, m_spheresMatIndexBuffer.buffer); m_objDesc.emplace_back(objDesc); ObjInstance instance{}; instance.objIndex = static_cast<uint32_t>(m_objModel.size()); m_instances.emplace_back(instance); }
After that, don't forget to destroy the buffer in destroryresources()
m_alloc.destroy(m_spheresBuffer); m_alloc.destroy(m_spheresAabbBuffer); m_alloc.destroy(m_spheresMatColorBuffer); m_alloc.destroy(m_spheresMatIndexBuffer);
We need a new underlying acceleration structure (BLAS) to store the above sphere or cube data. In order to improve efficiency and because all elements are static, we add them to a single BLAS.
Compared with the default triangle entity in the original pipeline, what we change in the intersection shader are the Aabb data (see Aabb structure) and geometry type (VK_GEOMETRY_TYPE_AABBS_KHR) of the basic entity.
//-------------------------------------------------------------------------------------------------- // Returns the BLAS raytrace geometry data used to create, including all spheres // auto HelloVulkan::sphereToVkGeometryKHR() { VkDeviceAddress dataAddress = nvvk::getBufferDeviceAddress(m_device, m_spheresAabbBuffer.buffer); VkAccelerationStructureGeometryAabbsDataKHR aabbs{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_AABBS_DATA_KHR}; aabbs.data.deviceAddress = dataAddress; aabbs.stride = sizeof(Aabb); // Build information needed to set up the acceleration structure VkAccelerationStructureGeometryKHR asGeom{VK_STRUCTURE_TYPE_ACCELERATION_STRUCTURE_GEOMETRY_KHR}; asGeom.geometryType = VK_GEOMETRY_TYPE_AABBS_KHR; asGeom.flags = VK_GEOMETRY_OPAQUE_BIT_KHR; asGeom.geometry.aabbs = aabbs; VkAccelerationStructureBuildRangeInfoKHR offset{}; offset.firstVertex = 0; offset.primitiveCount = (uint32_t)m_spheres.size(); offset.primitiveOffset = 0; offset.transformOffset = 0; nvvk::RaytracingBuilderKHR::BlasInput input; input.asGeometry.emplace_back(asGeom); input.asBuildOffsetInfo.emplace_back(offset); return input; }
Finally, in main.cpp, we load the OBJ model, which we can use
//Create sample helloVk.loadModel(nvh::findFile( " media/scenes/plane.obj " , defaultSearchPaths, true )); helloVk.createSpheres( 2000000 );
⚠️ Note: there may be more OBJ models, but due to the way we build TLAS, we need to add spheres after all these models.
The scene becomes larger, so you need to set up the camera
CameraManip.setLookat (nvmath :: vec3f ( 20 , 20 , 20 ), nvmath :: vec3f ( 0 , 1 , 0 ), nvmath :: vec3f ( 0 , 1 , 0 ));
3, Create acceleration structure
3.1 BLAS
The function createbotomlevelas() creates a BLAS for each OBJ. The following modifications will add a new BLAS, which contains the Aabb bounding box information of all spheres.
void HelloVulkan::createBottomLevelAS() { // BLAS - stores each entity in geometry std::vector<nvvk::RaytracingBuilderKHR::BlasInput> allBlas; allBlas.reserve(m_objModel.size()); for(const auto& obj : m_objModel) { auto blas = objectToVkGeometryKHR(obj); // We can add multiple geometry in each BLAS. At present, we only add one allBlas.emplace_back(blas); } // Spheres { auto blas = sphereToVkGeometryKHR(); allBlas.emplace_back(blas); } m_rtBuilder.buildBlas(allBlas, VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR); }
3.2 TLAS
Also in createTopLevelAS(), the top acceleration structure needs to add a reference to the sphere BLAS. We set instanceCustomId and blasId as the last element, which is why sphere BLAS must be added after all other elements.
Then set hitGroupId to 1. We need to add a new hit group for these primitives, because we need to calculate the element properties like other geometry, and these custom elements are not automatically provided by the pipeline like the default triangle element.
Because we added an additional instance when creating the custom object, there is one less element in the loop. So the loop should now look like this:
auto nbObj = static_cast<uint32_t>(m_instances.size()) - 1; tlas.reserve(nbObj); for(uint32_t i = 0; i < nbObj; i++) { const auto& inst = m_instances[i]; ... }
After the loop and before building TLAS, we need to add the following.
//Add all custom objects in BLAS { VkAccelerationStructureInstanceKHR rayInst{}; rayInst.transform = nvvk::toTransformMatrixKHR(nvmath::mat4f(1)); // Position of the instance (identity) rayInst.instanceCustomIndex = nbObj; // nbObj == last object == implicit rayInst.accelerationStructureReference = m_rtBuilder.getBlasDeviceAddress(static_cast<uint32_t>(m_objModel.size())); rayInst.instanceShaderBindingTableRecordOffset = 1; // We will use the same hit group for all objects rayInst.flags = VK_GEOMETRY_INSTANCE_TRIANGLE_FACING_CULL_DISABLE_BIT_KHR; rayInst.mask = 0xFF; // Only be hit if rayMask & instance.mask != 0 tlas.emplace_back(rayInst); }
The instanceCustomIndex gives us the last element m_instances and will be able to access the materials assigned to custom objects in the shader.
4, Descriptor
To access the newly created buffer containing all spheres, you need to make some changes to the descriptor.
Add a new enumeration to the Binding
eImplicit = 3 , //All custom objects
The descriptor needs to add a buffer bound to the custom object.
//Storage sphere (binding = 3) m_descSetLayoutBind.addBinding(eImplicit, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1 , VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR | VK_SHADER_STAGE_INTERSECTION_BIT_KHR);
The function that updateDescriptorSet() writes the buffer value also needs to be modified. The sphere's buffer is then written after the texture array
VkDescriptorBufferInfo dbiSpheres{m_spheresBuffer. Buffer, 0, VK_WHOLE_SIZE}; writes.emplace_back(m_descSetLayoutBind.makeWrite(m_descSet, eImplicit, &dbiSpheres));
5, New shader
The intersection shader is added to the hit group VK_ RAY_ TRACING_ SHADER_ GROUP_ TYPE_ PROCEDURAL_ HIT_ GROUP_ In KHR. In the example, we already have a hit group of triangles and an associated recent hit shader. We will add a new hit group and will become Hit Group ID (1).
This is the creation of a two hit group:
enum StageIndices { eRaygen, eMiss, eMiss2, eClosestHit, eClosestHit2, eIntersection, eShaderGroupCount }; // Closest hit stage.module = nvvk::createShaderModule(m_device, nvh::loadFile("spv/raytrace2.rchit.spv", true, defaultSearchPaths, true)); stage.stage = VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR; stages[eClosestHit2] = stage; // Intersection stage.module = nvvk::createShaderModule(m_device, nvh::loadFile("spv/raytrace.rint.spv", true, defaultSearchPaths, true)); stage.stage = VK_SHADER_STAGE_INTERSECTION_BIT_KHR; stages[eIntersection] = stage;
//Closest hit shader + intersection (hit group 2) group.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_PROCEDURAL_HIT_GROUP_KHR; group.closestHitShader = eClosestHit2; group.intersectionShader = eIntersection; m_rtShaderGroups.push_back(group);
5.1 intersection shader
This shader is called every time a ray hits the scene's Aabb. Note, however, that Aabb information cannot be retrieved in the intersection shader. It is also impossible to obtain the hit point value calculated by the ray tracker on the GPU.
The only information we can get is: using gl_PrimitiveID gets which element is hit in Aabb. Then, using the information stored in the buffer, we can retrieve the geometric information of the sphere.
In the shader, we first declare the extension and include the public file.
#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 "raycommon.glsl" #include "wavefront.glsl"
The following is the topology of all spheres. We can use GL_ The primitiveid gets the specific.
layout(binding = 3, set = eImplicit, scalar) buffer allSpheres_ { Sphere allSpheres[]; };
We will implement two intersection methods for incident light.
struct Ray { vec3 origin; vec3 direction; };
Sphere intersection
// Ray and sphere intersection algorithm // http://viclw17.github.io/2018/07/16/raytracing-ray-sphere-intersection/ float hitSphere(const Sphere s, const Ray r) { vec3 oc = r.origin - s.center; float a = dot(r.direction, r.direction); float b = 2.0 * dot(oc, r.direction); float c = dot(oc, oc) - s.radius * s.radius; float discriminant = b * b - 4 * a * c; if(discriminant < 0) { return -1.0; } else { return (-b - sqrt(discriminant)) / (2.0 * a); } }
Intersects the axis aligned bounding box
//Ray AABB intersection
float hitAabb ( const Aabb aabb, const Ray r)
{
vec3 invDir = 1.0 / r. direction;
vec3 tbot = invDir * (aabb. minimum - r. origin );
VEC3 TTOP = invDir * (AABB max. - origin of R);
vec3 tmin = min (ttop, tbot);
vec3 tmax = maximum (ttop, tbot);
float t0 = max (tmin. x , max (tmin. y , tmin.Ž));
float t1 = min (tmax. x , min (tmax. y , tmax. z ));
Return T1 > max (T0, 0.0)? t0 : - 1.0 ;
}
// Ray AABB bounding box intersection float hitAabb(const Aabb aabb, const Ray r) { vec3 invDir = 1.0 / r.direction; vec3 tbot = invDir * (aabb.minimum - r.origin); vec3 ttop = invDir * (aabb.maximum - r.origin); vec3 tmin = min(ttop, tbot); vec3 tmax = max(ttop, tbot); float t0 = max(tmin.x, max(tmin.y, tmin.z)); float t1 = min(tmax.x, min(tmax.y, tmax.z)); return t1 > max(t0, 0.0) ? t0 : -1.0; }
If there is no hit, both return - 1, otherwise return the distance of the ray to the origin.
void main() { Ray ray; ray.origin = gl_WorldRayOriginEXT; ray.direction = gl_WorldRayDirectionEXT;
And information about the geometry contained in Aabb can be obtained like this.
//Sphere data Sphere sphere = allSpheres.i[gl_PrimitiveID];
Now we just need to know whether the light hits the sphere or the cube.
float tHit = -1; int hitKind = gl_PrimitiveID % 2 == 0 ? KIND_SPHERE : KIND_CUBE; if(hitKind == KIND_SPHERE) { // Sphere intersection tHit = hitSphere(sphere, ray); } else { // AABB intersection Aabb aabb; aabb.minimum = sphere.center - vec3(sphere.radius); aabb.maximum = sphere.center + vec3(sphere.radius); tHit = hitAabb(aabb, ray); }
The reportIntersectionEXT function can be used to obtain the intersection information obtained in the intersection shader, including the distance between the intersection and the origin and the second parameter (hitKind) that can be used to distinguish the original type.
// Report hit point if(tHit > 0) reportIntersectionEXT(tHit, hitKind); }
The overall shader code is as follows:
#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 "raycommon.glsl" #include "wavefront.glsl" layout(set = 1, binding = eImplicit, scalar) buffer allSpheres_ { Sphere allSpheres[]; }; struct Ray { vec3 origin; vec3 direction; }; // Ray-Sphere intersection // http://viclw17.github.io/2018/07/16/raytracing-ray-sphere-intersection/ float hitSphere(const Sphere s, const Ray r) { vec3 oc = r.origin - s.center; float a = dot(r.direction, r.direction); float b = 2.0 * dot(oc, r.direction); float c = dot(oc, oc) - s.radius * s.radius; float discriminant = b * b - 4 * a * c; if(discriminant < 0) { return -1.0; } else { return (-b - sqrt(discriminant)) / (2.0 * a); } } // Ray-AABB intersection float hitAabb(const Aabb aabb, const Ray r) { vec3 invDir = 1.0 / r.direction; vec3 tbot = invDir * (aabb.minimum - r.origin); vec3 ttop = invDir * (aabb.maximum - r.origin); vec3 tmin = min(ttop, tbot); vec3 tmax = max(ttop, tbot); float t0 = max(tmin.x, max(tmin.y, tmin.z)); float t1 = min(tmax.x, min(tmax.y, tmax.z)); return t1 > max(t0, 0.0) ? t0 : -1.0; } void main() { Ray ray; ray.origin = gl_WorldRayOriginEXT; ray.direction = gl_WorldRayDirectionEXT; // Sphere data Sphere sphere = allSpheres[gl_PrimitiveID]; float tHit = -1; int hitKind = gl_PrimitiveID % 2 == 0 ? KIND_SPHERE : KIND_CUBE; if(hitKind == KIND_SPHERE) { // Sphere intersection tHit = hitSphere(sphere, ray); } else { // AABB intersection Aabb aabb; aabb.minimum = sphere.center - vec3(sphere.radius); aabb.maximum = sphere.center + vec3(sphere.radius); tHit = hitAabb(aabb, ray); } // Report hit point if(tHit > 0) reportIntersectionEXT(tHit, hitKind); }
5.2 recent hit shader
New recently hit shaders are as follows:
#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 "raycommon.glsl" #include "wavefront.glsl" hitAttributeEXT vec2 attribs; // clang-format off layout(location = 0) rayPayloadInEXT hitPayload prd; layout(location = 1) rayPayloadEXT bool isShadowed; 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 = 0, binding = eTlas) uniform accelerationStructureEXT topLevelAS; layout(set = 1, binding = eObjDescs, scalar) buffer ObjDesc_ { ObjDesc i[]; } objDesc; layout(set = 1, binding = eTextures) uniform sampler2D textureSamplers[]; layout(set = 1, binding = eImplicit, scalar) buffer allSpheres_ {Sphere i[];} allSpheres; layout(push_constant) uniform _PushConstantRay { PushConstantRay pcRay; }; // clang-format on void main() { // Object data ObjDesc objResource = objDesc.i[gl_InstanceCustomIndexEXT]; MatIndices matIndices = MatIndices(objResource.materialIndexAddress); Materials materials = Materials(objResource.materialAddress); vec3 worldPos = gl_WorldRayOriginEXT + gl_WorldRayDirectionEXT * gl_HitTEXT; Sphere instance = allSpheres.i[gl_PrimitiveID]; // Computing the normal at hit position vec3 worldNrm = normalize(worldPos - instance.center); // Computing the normal for a cube if(gl_HitKindEXT == KIND_CUBE) // Aabb { vec3 absN = abs(worldNrm); float maxC = max(max(absN.x, absN.y), absN.z); worldNrm = (maxC == absN.x) ? vec3(sign(worldNrm.x), 0, 0) : (maxC == absN.y) ? vec3(0, sign(worldNrm.y), 0) : vec3(0, 0, sign(worldNrm.z)); } // Vector toward the light vec3 L; float lightIntensity = pcRay.lightIntensity; float lightDistance = 100000.0; // Point light if(pcRay.lightType == 0) { vec3 lDir = pcRay.lightPosition - worldPos; lightDistance = length(lDir); lightIntensity = pcRay.lightIntensity / (lightDistance * lightDistance); L = normalize(lDir); } else // Directional light { L = normalize(pcRay.lightPosition); } // Material of the object int matIdx = matIndices.i[gl_PrimitiveID]; WaveFrontMaterial mat = materials.m[matIdx]; // Diffuse vec3 diffuse = computeDiffuse(mat, L, worldNrm); vec3 specular = vec3(0); float attenuation = 0.3; // Tracing shadow ray only if the light is visible from the surface if(dot(worldNrm, 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 { attenuation = 1; // Specular specular = computeSpecular(mat, gl_WorldRayDirectionEXT, L, worldNrm); } } prd.hitValue = vec3(lightIntensity * attenuation * (diffuse + specular)); }
The newly added raytrace 2.rchit shader in the hit group is almost the same as the previous raytrace.rchit shader, but since the primitive is custom, we only need to calculate the normal of the hit primitive.
We retrieve the world position from the light and use the gl_HitTEXT is set in the intersection shader.
vec3 worldPos = gl_WorldRayOriginEXT + gl_WorldRayDirectionEXT * gl_HitTEXT;
Sphere information is retrieved in the same way as in the raytrace.rint shader.
Sphere instance = allSpheres.i[gl_PrimitiveID];
Then we calculate the normals like a sphere.
//Calculates the normal of the hit location vec3 normal = normalize(worldPos - instance.center);
To determine whether we intersect a cube rather than a sphere, we need to use gl_HitKindEXT data (set in the second parameter of the reportIntersectionEXT function).
So when this is a cube, we set the normal to the principal axis.
// If the hit intersection returns a value of 1, the normal of the cube is calculated if(gl_HitKindEXT == KIND_CUBE) // Aabb { vec3 absN = abs(normal); float maxC = max(max(absN.x, absN.y), absN.z); normal = (maxC == absN.x) ? vec3(sign(normal.x), 0, 0) : (maxC == absN.y) ? vec3(0, sign(normal.y), 0) : vec3(0, 0, sign(normal.z)); }