**DirectX Raytracing, Tutorial 5** **Ray traced ambient occlusion using a hybrid raster / ray tracing renderer** In [Tutorial 4](../Tutor4/tutorial04.md.html), we walked through our first example of spawning rays in DirectX and the new HLSL shader types needed to control them. This tutorial combines the rasterized [G-Buffer](https://en.wikipedia.org/wiki/Deferred_shading) we created in [Tutorial 3](../Tutor3/tutorial03.md.html) and then launches [ambient occlusion](https://en.wikipedia.org/wiki/Ambient_occlusion) rays from the locations of pixels in that G-buffer. Our Basic Ambient Occlusion Pipeline ================================================================================= If you open up *`Tutor05-AmbientOcclusion.cpp`*, you will find our new pipeline combines the *`SimpleGBufferPass`* from [Tutorial 3](../Tutor3/tutorial03.md.html) with the new *`AmbientOcclusionPass`* we'll build below. ~~~~~~~~~~~~~~~~~~~~ C // Create our rendering pipeline RenderingPipeline *pipeline = new RenderingPipeline(); pipeline->setPass(0, SimpleGBufferPass::create()); pipeline->setPass(1, AmbientOcclusionPass::create()); ~~~~~~~~~~~~~~~~~~~~ This pipeline is a hybrid rasterization and ray tracing pipeline, as it uses a standard rasterization pass to defer rendering, saving our primary hit points in a G-buffer and shading them in a later pass. In this example, our later pass will shade these rasterized points using ray traced ambient occlusion. Launching DirectX Ambient Occlusion Rays From a G-Buffer ================================================================================= Continue by looking in *`AmbientOcclusionPass.h`*. This should look familiar, as the boilerplate is nearly identical to that from *`RayTracedGBufferPass`* in [Tutorial 4](../Tutor4/tutorial04.md.html). The major difference is our member variables to control the ambient occlusion: ~~~~~~~~~~~~~~~~~~~~ C float mAORadius = 0.0f; // Our ambient occlusion radius uint32_t mFrameCount = 0; // Used for unique random seeds each frame ~~~~~~~~~~~~~~~~~~~~ Ambient occlusion gives an approximation of ambient lighting and shadowing due to nearby geometry at each pixel. Usually, ambient occlusion is limited to nearby geometry by having a radius that defines "nearby geometry." In our rendering, this is controlled by *`mAORadius`*. Ambient occlusion is a [stochastic effect](https://en.wikipedia.org/wiki/Stochastic), so we'll need a random number generator to select our rays. Our shader uses a [pseudo random number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator) to generate random samples, and to avoid generating the same rays every frame we need to update our random seeds each frame. We do this by hashing the pixel location and a frame count to initialize our random generator. Initializing our Ambient Occlusion Pass ------------------------------ Our *`AmbientOcclusionPass::initialize()`* is fairly straightforward: ~~~~~~~~~~~~~~~~~~~~ C bool AmbientOcclusionPass::initialize(RenderContext::SharedPtr pRenderContext, ResourceManager::SharedPtr pResManager) { // Stash a copy of our resource manager; request needed buffer resources mpResManager = pResManager; mpResManager->requestTextureResources( {"WorldPosition", "WorldNormal", ResourceManager::kOutputChannel }); // Create our wrapper for our DirectX Raytracing launch. mpRays = RayLaunch::create("aoTracing.hlsl", "AoRayGen"); mpRays->addMissShader("aoTracing.hlsl", "AoMiss"); mpRays->addHitShader("aoTracing.hlsl", "AoClosestHit", "AoAnyHit"); // Compile our shaders and pass in our scene mpRays->compileRayProgram(); mpRays->setScene(mpScene); return true; } ~~~~~~~~~~~~~~~~~~~~ We first request our rendering resources. We need our G-buffer fields *`"WorldPosition"`* and *`"WorldNormal"`* as inputs and we're outputting a displayable color to *`kOutputChannel`*. Then we create a ray tracing wrapper *`mpRays`* that starts tracing rays at in the ray generation shader *`AoRayGen()`* in the file *`aoTracing.hlsl`* and has one ray type with shaders *`AoMiss`*, *`AoAnyHit`*, and *`AoClosestHit`*. Launching Ambient Occlusion Rays ------------------------------ Now that we initialized our rendering resources, we can shoot our ambient occlusion rays. Let's walk through the *`AmbientOcclusionPass::execute`* pass below: ~~~~~~~~~~~~~~~~~~~~ C void AmbientOcclusionPass::execute(RenderContext::SharedPtr pRenderContext) { // Get our output buffer; clear it to black. auto dstTex = mpResManager->getClearedTexture(ResourceManager::kOutputChannel, vec4(0.0f, 0.0f, 0.0f, 0.0f)); // Set our shader variables for our ambient occlusion shader auto rayGenVars = mpRays->getRayGenVars(); rayGenVars["RayGenCB"]["gFrameCount"] = mFrameCount++; rayGenVars["RayGenCB"]["gAORadius"] = mAORadius; rayGenVars["RayGenCB"]["gMinT"] = mpResManager->getMinTDist(); rayGenVars["gPos"] = mpResManager->getTexture("WorldPosition"); rayGenVars["gNorm"] = mpResManager->getTexture("WorldNormal"); rayGenVars["gOutput"] = dstTex; // Shoot our AO rays mpRays->execute( pRenderContext, mpResManager->getScreenSize() ); } ~~~~~~~~~~~~~~~~~~~~ First, we grab our output buffer and clear it to black. Then we pass our G-buffer inputs, output texture, and user-controllable variables down to the ray generation shader. Unlike [Tutorial 4](../Tutor4/tutorial04.md.html), we do not send variables to either the miss shader or closest hit shader. Instead, our variables will only be visibile in the ray generation shader. We pass down our *`mFrameCount`* to seed our per-pixel random number generator, our user-controllable *`mAORadius`* to change the amount of occlusion seen, and a minimum _t_ value to use when shooting our rays. This *`gMinT`* variable will control how much bias to add at the beginning of our rays to avoid self-intersection on surfaces. We also pass down our input textures *`gPos`* and *`gNorm`* from our prior G-buffer pass and our default pipeline output to *`gOutput`*. DirectX Raytracing for Ambient Occlusion ============================================================================== Let's start by reading our ambient occlusion shader's ray generation shader *`AoRayGen()`*: ~~~~~~~~~~~~~~~~~~~~~C cbuffer RayGenCB { float gAORadius, gMinT; uint gFrameCount; } Texture2D gPos, gNorm; RWTexture2D gOutput; [shader("raygeneration")] void AoRayGen() { // Where is this thread's ray on screen? uint2 pixIdx = DispatchRaysIndex(); uint2 numPix = DispatchRaysDimensions(); // Initialize a random seed, per-pixel and per-frame uint randSeed = initRand( pixIdx.x + pixIdx.y * numPix.x, gFrameCount ); // Load the position and normal from our g-buffer float4 worldPos = gPos[pixIdx]; float3 worldNorm = gNorm[pixIdx].xyz; // Default ambient occlusion value if we hit the background float aoVal = 1.0f; // worldPos.w == 0 for background pixels; only shoot AO rays elsewhere if (worldPos.w != 0.0f) { // Random ray, sampled on cosine-weighted hemisphere around normal float3 worldDir = getCosHemisphereSample(randSeed, worldNorm); // Shoot our ambient occlusion ray and update the final AO value aoVal = shootAmbientOcclusionRay(worldPos.xyz, worldDir, gMinT, gAORadius); } gOutput[pixIdx] = float4(aoVal, aoVal, aoVal, 1.0f); } ~~~~~~~~~~~~~~~~~~~~~ First, we discover what pixel we're at and how big our ray launch is. We use this data to initialize our psuedo random number generator (see tutorial code for details on *`initRand()`*) and to load the surfaces stored in our G-buffer (i.e., in *`gPos`* and *`gNorm`*). Next we need to test if our G-buffer actually stores geometry or it the pixel represents background. (Our G-buffer stores this information in the alpha channel of the position: *`0.0f`* means a background pixel, other values represent valid hit points.) For background pixels, our default AO value will be *`1.0f`* representing fully lit. For pixels containing valid G-buffer hits, *`getCosHemisphereSample()`* computes a random ray (see code for details or [various places online](https://blog.thomaspoulet.fr/uniform-sampling-on-unit-hemisphere/) for the math of cosine-weighted sampling). We trace our ambient occlusion ray by calling our helper function *`shootAmbientOcclusionRay()`*: ~~~~~~~~~~~~~~~~~~~~~C struct AORayPayload { float aoVal; // Stores 0 on a ray hit, 1 on ray miss }; float shootAmbientOcclusionRay( float3 orig, float3 dir, float minT, float maxT ) { // Setup AO payload. By default, assume our AO ray will *hit* (value = 0) AORayPayload rayPayload = { 0.0f }; // Describe the ray we're shooting RayDesc rayAO = { orig, minT, dir, maxT }; // We're going to tell our ray to never run the closest-hit shader and to // stop as soon as we find *any* intersection uint rayFlags = RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH | RAY_FLAG_SKIP_CLOSEST_HIT_SHADER; // Trace our ray. TraceRay(gRtScene, rayFlags, 0xFF, 0, 1, 0, rayAO, rayPayload ); // Return our AO value out of the ray payload. return rayPayload.aoVal; } ~~~~~~~~~~~~~~~~~~~~~ This helper function sets up our *`RayDesc`* with the minimum _t_ to avoid self intersection and the specified maximum _t_ (of *`gAoRadius`*). It creates a ray payload that will toggle between 1.0f (it no intersection occurs) and 0.0f (if we hit a surface), then we trace a ray that never executes any closet hit shaders and stops as soon as it gets any confirmed intersections. We only have one hit group and one miss shader specified, so the 4th and 6th parameters to *`TraceRay()`* must be 0 and the 5th parameter must be 1. Our miss and hit shaders for AO rays are extremely simple: ~~~~~~~~~~~~~~~~~~~~~C [shader("miss")] void AoMiss(inout AORayPayload rayData) { rayData.aoVal = 1.0f; } ~~~~~~~~~~~~~~~~~~~~~ When our ambient occlusion ray misses, we need to toggle our payload value to store *`1.0f`*, which is the value representing illuminated. ~~~~~~~~~~~~~~~~~~~~~C [shader("anyhit")] void AoAnyHit(inout AORayPayload rayData, BuiltinIntersectionAttribs attribs) { if (alphaTestFails(attribs)) IgnoreHit(); } ~~~~~~~~~~~~~~~~~~~~~ When we have a potential hit, make sure it is valid by checking if there is an alpha mask and if the hit fails the alpha test. ~~~~~~~~~~~~~~~~~~~~~C [shader("closesthit")] void AoClosestHit(inout AORayPayload, BuiltinIntersectionAttribs) { } ~~~~~~~~~~~~~~~~~~~~~ Out closest hit shader is never executed, so it can be a trivial no-op. Alternatively, this function could be removed from the shader and a null closest-hit shader specified in *`AmbientOcclusionPass::initialize()`*. What Does it Look Like? =============================================================================== That covers the important points of this tutorial. When running, you get the following result: ![Result of running Tutorial 5, after loading the scene "pink_room.fscene"](Tutor5-Output.png) Hopefully, this tutorial demonstrated: * How to launch DirectX rays from a previously rasterized G-buffer * Write basic ray tracing HLSL shaders to output ambient occlusion When you are ready, continue on to [Tutorial 6](../Tutor6/tutorial06.md.html), which accumulates ambient occlusion samples over time so you can get ground-truth ambient occlusion rather than noisy one sample per pixel results.