**DirectX Raytracing, Tutorial 4** **Creating a ray traced G-buffer** Our first three [tutorials](../../dxr_tutors.md.html) focused on getting up to speed with our simple tutorial infrastructure and making simple DirectX rasterization calls (to create a basic [G-Buffer](https://en.wikipedia.org/wiki/Deferred_shading)). This tutorial finally moves on to actually spawning some ray tracing with DirectX. To focus on how ray tracing and rasterization are different, this tutorial builds a G-Buffer identical to the one created in [Tutorial 3](../Tutor3/tutorial03.md.html). Our Basic Ray Tracing Render Pipeline ================================================================================= If you open up *`Tutor04-RayTracedGBuffer.cpp`*, you will find the only change is we swaped out *`SimpleGBufferPass`* for the new *`RayTracedGBufferPass`* we'll build below. We reuse the same *`CopyToOutputPass`* we created in [Tutorial 3](../Tutor3/tutorial03.md.html). ~~~~~~~~~~~~~~~~~~~~ C // Create our rendering pipeline RenderingPipeline *pipeline = new RenderingPipeline(); pipeline->setPass(0, RayTracedGBufferPass::create()); pipeline->setPass(1, CopyToOutputPass::create()); ~~~~~~~~~~~~~~~~~~~~ For those who skipped the first three tutorials, this code can be read as "create a rendering pipeline with two components: a pass that generates a ray-traced G-buffer followed by a pass that copies a selectable buffer onscreen." Creating and Launching DirectX Raytracing Work ================================================================================= Start by looking in *`RayTracedGBufferPass.h`*. This should look familiar, as the boilerplate is nearly identical to that from the *`RenderPasses`* from prior tutorials. The major difference is in our pass' member variables: ~~~~~~~~~~~~~~~~~~~~ C RtScene::SharedPtr mpScene; // Falcor scene abstraction RayLaunch::SharedPtr mpRays; // Encapsulates DirectX Raytracing pass ~~~~~~~~~~~~~~~~~~~~ The *`RtScene`* class encapsultes Falcor's scene representation, with additions for ray tracing. In the rest of our tutorials, we will use the *`RtScene`* class (which derives from the *`Scene`* class from [Tutorial 3](../Tutor3/tutorial03.md.html) but adds functionality required for ray tracing). The *`RayLaunch`* is similar to the *`RasterLaunch`* class from [Tutorial 3](../Tutor3/tutorial03.md.html), except it exposes methods for setting up all the new HLSL ray tracing shader types and setting their variables. Initializing our Ray Traced G-Buffer Pass ------------------------------ Our *`RayTracedGBufferPass::initialize()`* looks quite similar to the one for *`SimpleGBufferPass`*: ~~~~~~~~~~~~~~~~~~~~ C bool RayTracedGBufferPass::initialize(RenderContext::SharedPtr pRenderContext, ResourceManager::SharedPtr pResManager) { // Stash a copy of our resource manager; tell it what shared texture // resources we need for this pass. mpResManager = pResManager; mpResManager->requestTextureResources({"WorldPosition", "WorldNormal", "MaterialDiffuse", "MaterialSpecRough", "MaterialExtraParams"}); // Create our wrapper for our DirectX Raytracing launch. mpRays = RayLaunch::create("rtGBuffer.hlsl", "GBufferRayGen"); mpRays->addMissShader("rtGBuffer.hlsl", "PrimaryMiss"); mpRays->addHitShader("rtGBuffer.hlsl", "PrimaryClosestHit", "PrimaryAnyHit"); // Compile our shaders and pass in our scene mpRays->compileRayProgram(); mpRays->setScene(mpScene); return true; } ~~~~~~~~~~~~~~~~~~~~ A couple important notes: * The method *`requestTextureResources`* is identical to calling *`requiresTextureResource`* multiple times, with each of the specified names, simplifying the code somewhat. * We don't request a Z-buffer, because ray tracing does not generate one by default. (Of course, if desired you can output a Z-buffer manually.) We then create our ray tracing wrapper *`mpRays`* by pointing it to our shader file *`rtGBuffer.hlsl`* and specifying the function where our _ray generation shader_ starts, in this case the function *`GBufferRayGen()`* in our HLSL file. Because we plan to launch rays from this shader, we also have to specify the _miss shader_ to use, named *`PrimaryMiss()`*, and our _closest-hit_ and _any hit shaders_, named *`PrimaryClosestHit()`* and *`PrimaryAnyHit()`*. When ray tracing, we can have multiple ray types. This means we can call *`addMissShader`* multiple times. The first time it is called, we specify miss shader #0. The second time, we specify miss shader #1. The next, miss shader #2. Et cetera. Similarly, when called multiple times, *`addHitShader`* specifies hit group #0, 1, 2, etc. This integer identifier is important, as calling *`TraceRay()`* in HLSL requires you specify the correct IDs. Launching Ray Tracing and Sending Data to HLSL ------------------------------ Now that we initialized our rendering resources, we can create our ray traced G-buffer. The *`RayTracedGBufferPass::execute`* pass is somewhat complex, so let's split it into multiple pieces: ~~~~~~~~~~~~~~~~~~~~ C void RayTracedGBufferPass::execute(RenderContext::SharedPtr pRenderContext) { // Color used to clear our G-buffer vec4 black = vec4(0, 0, 0, 0); // Load our textures; ask the resource manager to clear them first // Note: 'auto' type used for brevity; actually Texture::SharedPtr auto wsPos = mpResManager->getClearedTexture("WorldPosition", black); auto wsNorm = mpResManager->getClearedTexture("WorldNormal", black); auto matDif = mpResManager->getClearedTexture("MaterialDiffuse", black); auto matSpec = mpResManager->getClearedTexture("MaterialSpecRough", black); auto matExtra = mpResManager->getClearedTexture("MaterialExtraParams", black); auto matEmit = mpResManager->getClearedTexture("Emissive", black); ~~~~~~~~~~~~~~~~~~~~ First, we need access the textures we'll use to write out our G-buffer. Unlike in [Tutorials 2](../Tutor2/tutorial02.md.html) [and 3](../Tutor3/tutorial03.md.html), we do not need to create a framebuffer object. Instead for ray tracing, we'll write directly into them ([using UAVs](https://docs.microsoft.com/en-us/windows/desktop/direct3dhlsl/sm5-object-rwtexture2d)). We also ask our resource manager to clear them to black prior to returning the textures. ~~~~~~~~~~~~~~~~~~~~C // Pass our background color down to miss shader #0 auto missVars = mpRays->getMissVars(0); missVars["MissShaderCB"]["gBgColor"] = mBgColor; // Color for background missVars["gMatDif"] = matDif; // Where to store bg color ~~~~~~~~~~~~~~~~~~~~ Next, we set the HLSL variables for our _miss shader #0_. These variables are scoped so they are _only_ usable while executing this specific miss shader. This uses the same syntax we used for raster shaders, except we need to specifically ask (*`getMissVars(0)`*) for variables for miss shader #0. In this case, if our rays miss we'll store the specified background color in the *`"MaterialDiffuse"`* texture. ~~~~~~~~~~~~~~~~~~~~C // Pass down variables for our hit group #0 for (auto pVars : mpRays->getHitVars(0)) { pVars["gWsPos"] = wsPos; pVars["gWsNorm"] = wsNorm; pVars["gMatDif"] = matDif; pVars["gMatSpec"] = matSpec; pVars["gMatExtra"] = matExtra; } ~~~~~~~~~~~~~~~~~~~~ In this snippet, we are setting the HLSL variables for hit group #0 (i.e, the _closest-hit_ and _any-hit_ shaders). This code is more complex than for miss shaders, since your variables may change for each geometry instance in your scene. This is the cause of the *`for`* loop, as we loop over all the instances. In this case, the variables specifed are scoped so they are only visibile by the closest-hit and any-hit shaders executed while intersecting the specified geometry instance. Above, we are specifying the output textures *`gWsPos`*, *`gWsNorm`*, *`gMatDif`*, *`gMatSpec`*, and *`gMatExtra`*. We will store our G=Buffer output in our _closest-hit_ shader, which can thought of as similar to a pixel shader. ~~~~~~~~~~~~~~~~~~~~C // Launch our ray tracing mpRays->execute( pRenderContext, mpResManager->getScreenSize() ); } ~~~~~~~~~~~~~~~~~~~~ Finally, we launch our ray tracing pass. *`execute()`* requres the DirectX context and the number of rays to launch (often dependant on screen resolution). The DirectX Raytracing HLSL for Our G-Buffer =============================================================================== Just like when starting to read a C++ program it often makes sense to start reading at *`main()`*, you should probably start reading HLSL ray tracing code at the ray generation shader. As specified above during our pass initialization, our shader's ray generation shader is named *`GBufferRayGen()`*: ~~~~~~~~~~~~~~~~~~~~~C [shader("raygeneration")] void GBufferRayGen() { // Convert our ray index into a ray direction in world space. float2 currenPixelLocation = DispatchRaysIndex() + float2(0.5f, 0.5f); float2 pixelCenter = currenPixelLocation / DispatchRaysDimensions(); float2 ndc = float2(2, -2) * pixelCenter + float2(-1, 1); float3 rayDir = normalize( ndc.x * gCamera.cameraU + ndc.y * gCamera.cameraV + gCamera.cameraW ); // Initialize a ray structure for our ray tracer RayDesc ray = { gCamera.posW, 0.0f, rayDir, 1e+38f }; // Initialize our ray payload (a per-ray, user-definable structure). SimplePayload payload = { false }; // Trace our ray TraceRay(gRtScene, RAY_FLAG_NONE, 0xFF, 0, 1, 0, ray, payload ); } ~~~~~~~~~~~~~~~~~~~~~ The first few lines lookup the current pixel location. The number of rays launched (i.e., the parmaeter passed to *`mpRays->execute`*) is accessible via HLSL's *`DispatchRayDimensions()`* intrinsic, and the current pixel being processed is given by *`DispatchRaysIndex()`*. After converting the pixel location into a value in [-1...1], we use the current camera parameters to generate a world-space vector that describes the ray from the camera through this pixel. The camera variable *`gCamera`* is automatically passed down by our framework. We then create a ray using the DirectX data type *`RayDesc`*. The first entry in the structure (named *`Origin`*) specifies where the ray starts. The second entry (named *`TMin`*) specifies the minimum distance we need to travel along our ray before reporting a valid hit. The third entry (named *`Direction`*) specifies the ray directions. The fourth entry (named *`TMax`*) specifies the maximum distance along our ray to return a valid hit. When tracing rays in DirectX, there is a user-definable _payload_ structure that accompanies each ray and allows you to stash temporary data during ray tracing. In this example shader this payload is unused, so is not particularly important. Finally, we call *`TraceRay()`* to launch our ray with the following parameters: * The first parameter, *`gRtScene`*, is the ray acceleration structure. Our framework populates this HLSL variable for you automatically. * Second, we specify flags to control ray behavior. Here, we use no special flags. * Third, we specify an instance mask. This allows you to skip some instances during traversal. A value *`0xFF`* tests all geometry in the scene (skipping none). * Fourth, we specify which hit group to use. In the DXR spec, this is a somewhat complex computation. For most simple usage, this corresponds directly to the ID of your hit group, as determined in *`RayTracedGBufferPass::initialize`*. (In our case, there is only one hit group so this takes a value of 0). * Fifth, we specify how many hit groups there are. In our case this is 1. (Our framework computes this and stores it in the varaible *`hitProgramCount`*, if you wish to avoid hard coding this value.) * Sixth, we specify which miss shader to use. This directly corresponds to the miss shader ID determined in *`RayTracedGBufferPass::initialize`*. * Seventh, we specify the ray to trace. * Finally, we specify the payload data structure to use while tracing this ray. After calling *`TraceRay()`*, some processing happens internally. The next shader executed depends on the geometry, ray direction, and internal data structures. _Any-hit shaders_ get launched for some potential hits, _closest-hit shaders_ get launched when you have identified the single closest hit along the ray, and _miss shaders_ get launched if no valid hit occurs. ~~~~~~~~~~~~~~~~~~~~~C // A dummy payload for this simple ray; never used struct SimplePayload { bool dummyValue; }; // Our miss shader's variables cbuffer MissShaderCB { float3 gBgColor; }; // The output textures. See bindings in C++ code. // -> gMatDif is visible in the miss shader // -> All textures are visible in the hit shaders RWTexture2D gWsPos, gWsNorm, gMatDif, gMatSpec, gMatExtra; [shader("miss")] void PrimaryMiss(inout SimplePayload) { gMatDif[DispatchRaysIndex()] = float4( gBgColor, 1.0f ); } ~~~~~~~~~~~~~~~~~~~~~ This miss shader is fairly straightforward. When a ray misses all geometry, write out the background color into the specified texture. Where in the texture do we write? That depends on the current pixel location we are working on (using the same *`DispatchRaysIndex()`* intrinsic we used in our ray generation shader). ~~~~~~~~~~~~~~~~~~~~~C [shader("anyhit")] void PrimaryAnyHit(inout SimpleRayPayload, BuiltinIntersectionAttribs attribs) { if (alphaTestFails(attribs)) IgnoreHit(); } ~~~~~~~~~~~~~~~~~~~~~ Our any hit shader is also straightforward. In most cases, when ray traversal detects a hit point, it is a valid intersection. However, if the geometry uses alpha-testing and has a textured alpha mask, we need to query it before confirming the intersection. Here we call a utility function *`alphaTestFails()`* (see tutorial code for specifics) that loads the alpha texture from Falcor's internal scene structure and determines if we hit a transparent texel. If so, this hit is ignored via the *`IgnoreHit()`* intrinsic, otherwise the hit is accepted. ~~~~~~~~~~~~~~~~~~~~~C [shader("closesthit")] void PrimaryClosestHit(inout SimpleRayPayload, BuiltinIntersectionAttribs attribs) { // Which pixel spawned our ray? uint2 idx = DispatchRaysIndex(); // Run helper function to compute important data at the current hit point ShadingData shadeData = getShadingData( PrimitiveIndex(), attribs ); // Save out our G-buffer values to the specified output textures gWsPos[idx] = float4(shadeData.posW, 1.f); gWsNorm[idx] = float4(shadeData.N, length(shadeData.posW - gCamera.posW)); gMatDif[idx] = float4(shadeData.diffuse, shadeData.opacity); gMatSpec[idx] = float4(shadeData.specular, shadeData.linearRoughness); gMatExtra[idx] = float4(shadeData.IoR, shadeData.doubleSidedMaterial ? 1.f : 0.f, 0.f, 0.f); } ~~~~~~~~~~~~~~~~~~~~~ Our closest hit shader looks remarkably like ou pixel shader in [Tutorial 3](../Tutor3/tutorial03.md.html). We first find which pixel onscreen this ray belongs to (using the *`DispatchRaysIndex()`* intrinsic), then we get our *`ShadingData`* using Falcor utility functions, finally we output our G-buffer data to the corresponding output buffers. As in [Tutorial 3](../Tutor3/tutorial03.md.html), this G-buffer is extremely verbose, and you could likely compress the same data into many fewer bits. This format is used for clarity. What Does it Look Like? =============================================================================== That covers the important points of this tutorial. When running, you get basically the same result as in Tutorial 3: ![Result of running Tutorial 4, after loading the scene "pink_room.fscene"](Tutor04-Output.png) Hopefully, this tutorial demonstrated: * How to launch DirectX Raytracing work. * Write basic ray tracing HLSL shaders. * Use intrinsic structures and functions like *`RayDesc`*, *`DispatchRaysIndex`* and *`TraceRay`*. When you are ready, continue on to [Tutorial 5](../Tutor5/tutorial05.md.html), where we build on our existing G-buffers to render ray traced ambient occlusion.