# DXR Tutorial Extra : Simple Lighting Welcome to the next section of the tutorial. If you miss the first tutorial, it is [here](/rtx/raytracing/dxr/DX12-Raytracing-tutorial-Part-1) The bases of this tutorial starts at the end of the Getting Further section of the previous one. !!! Note: Tutorial Extra ([Download](/rtx/raytracing/dxr/tutorial/Files/DXRTutorial_Extra.zip)) Download the files we start from, which include an animated geometry and shadow computation At the end of the first rounds of extras, the rendering is still very simplistic: shadow rays are traced, but the effect of the light on the objects is not computed, giving a very flat appearance. ![](/sites/default/files/pictures/2018/dx12_rtx_tutorial/Extra/extra2_start.png) In this tutorial we will add very simple diffuse lighting to that scene. Since the input geometry does not provide normals, we will compute them from the vertex positions upon hitting the surface. Since the tetrahedron is animated, we also have to transform the normal from object space to world space. To do this, we need to pass another matrix to the per-instance properties. In the header file, change the `InstanceProperties` structure to: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ struct InstanceProperties { XMMATRIX objectToWorld; //# DXR Extra - Simple Lighting XMMATRIX objectToWorldNormal; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## UpdateInstancePropertiesBuffer To transform a normal vector to world space, we cannot simply use the inverse of the existing object-to-world matrix, but have to use the [inverse of the transpose of the upper 3x3 part of that matrix](http://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/transforming-normals). We compute this on the CPU when updating the per-instance properties buffer: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ for (const auto &inst : m_instances) { current->objectToWorld = inst.second; //# DXR Extra - Simple Lighting XMMATRIX upper3x3 = inst.second; // Remove the translation and lower vector of the matrix upper3x3.r[0].m128_f32[3] = 0.f; upper3x3.r[1].m128_f32[3] = 0.f; upper3x3.r[2].m128_f32[3] = 0.f; upper3x3.r[3].m128_f32[0] = 0.f; upper3x3.r[3].m128_f32[1] = 0.f; upper3x3.r[3].m128_f32[2] = 0.f; upper3x3.r[3].m128_f32[3] = 1.f; XMVECTOR det; current->objectToWorldNormal = XMMatrixTranspose(XMMatrixInverse(&det, upper3x3)); current++; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## CreateShaderResourceHeap To access the per-instance properties buffer in the raytracing path, we need to add that buffer to the heap. It will now contain 4 elements: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Create a SRV/UAV/CBV descriptor heap. We need 4 entries - 1 SRV for the TLAS, 1 UAV for the // raytracing output, 1 CBV for the camera matrices, 1 SRV for the // per-instance data (# DXR Extra - Simple Lighting) m_srvUavHeap = nv_helpers_dx12::CreateDescriptorHeap( m_device.Get(), 4, D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, true); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ At the end of the method, we add a view on that buffer in a way similar to what is done for the raster heap: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //# DXR Extra - Simple Lighting srvHandle.ptr += m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; srvDesc.Format = DXGI_FORMAT_UNKNOWN; srvDesc.ViewDimension = D3D12_SRV_DIMENSION_BUFFER; srvDesc.Buffer.FirstElement = 0; srvDesc.Buffer.NumElements = static_cast(m_instances.size()); srvDesc.Buffer.StructureByteStride = sizeof(InstanceProperties); srvDesc.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_NONE; // Write the per-instance properties buffer view in the heap m_device->CreateShaderResourceView(m_instanceProperties.Get(), &srvDesc, srvHandle); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## CreateHitSignature To access the per-instance properties from the hit program we need to change its root signature to add another Shader Resource View (SRV) to the set of buffers from the heap. Instead of just having the top-level acceleration structure and the scene information, we now have our buffer in `register(t3)` by replacing the existing call to `AddHeapRangesParameter` by: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ rsc.AddHeapRangesParameter( {{2 /*t2*/, 1, 0, D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1 /*2nd slot of the heap*/}, {0 /*b0*/, 1, 0, D3D12_DESCRIPTOR_RANGE_TYPE_CBV /*Scene data*/, 2}, // # DXR Extra - Simple Lighting {3 /*t3*/, 1, 0, D3D12_DESCRIPTOR_RANGE_TYPE_SRV /*Per-instance data*/, 3} }); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## CreateShaderBindingTable The shader of the plane will need to access its vertex information to compute its normal, which we do by passing the pointer to the vertex buffer of the plane while building the Shader Binding Table: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ C m_sbtHelper.AddHitGroup( L"PlaneHitGroup", {(void *)m_planeBuffer->GetGPUVirtualAddress(), nullptr, (void *)(m_globalConstantBuffer->GetGPUVirtualAddress()), heapPointer}); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## shaders.hlsl An adaption on the rasterization side is required to keep the it working: since the shader also accesses the matrices, the structure needs to be updated: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ struct InstanceProperties { float4x4 objectToWorld; // # DXR Extra - Simple Lighting float4x4 objectToWorldNormal; }; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This matrix is not used here, and is only declared for alignment. It would be possible to compute the normals from the geometry in a geometry shader, and compute the unshadowed lighting. This is left as an exercise for the reader. In practical cases, the normals would be provided with the geometry. ## Hit.hlsl The hit shader can now take advantage of this new buffer: add the declaration of the structure and the buffer itself to the hit shader file: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // # DXR Extra - Simple Lighting struct InstanceProperties { float4x4 objectToWorld; float4x4 objectToWorldNormal; }; StructuredBuffer instanceProps : register(t3); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### ClosestHit Computing the lighting requires knowing the surface normals at the hit points. In most practical cases such normals are provided with the vertices, but our current geometry only has the vertex positions. Since the hit program can access the vertices of the hit triangle, we can deduce the normal. Note that we use the inverse transpose matrix computed above to transform the normal vector to world space. Add the following right after computing the value of `hitColor`: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // # DXR Extra - Simple Lighting float3 e1 = BTriVertex[indices[vertId + 1]].vertex - BTriVertex[indices[vertId + 0]].vertex; float3 e2 = BTriVertex[indices[vertId + 2]].vertex - BTriVertex[indices[vertId + 0]].vertex; float3 normal = normalize(cross(e2, e1)); normal = mul(instanceProps[InstanceID()].objectToWorldNormal, float4(normal, 0.f)).xyz; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Diffuse surfaces reflect light similarly for all incoming light directions, so we only need to account for the geometric term which is based on the surface normal and the incoming light direction. This code computes the world space position of the hit point `worldOrigin` and deduces the direction `centerLightDir` from that point to the light source. We modulate the hit color by the dot product between the normal and the lighting direction by adding this code after the normal computation: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // # DXR Extra - Simple Lighting float3 worldOrigin = WorldRayOrigin() + RayTCurrent() * WorldRayDirection(); float3 lightPos = float3(2, 2, -2); float3 centerLightDir = normalize(lightPos - worldOrigin); float nDotL = max(0.f, dot(normal, centerLightDir)); hitColor *= nDotL; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### PlaneClosestHit The closest hit shader of the plane needs to be adapted as well. This one already had the computation of the light direction since it is shooting shadow rays. We can compute the normal direction as well, this time without using indexing since the plane does not use indices: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // # DXR Extra - Simple Lighting uint vertId = 3 * PrimitiveIndex(); float3 e1 = BTriVertex[vertId + 1].vertex - BTriVertex[vertId + 0].vertex; float3 e2 = BTriVertex[vertId + 2].vertex - BTriVertex[vertId + 0].vertex; float3 normal = normalize(cross(e2, e1)); normal = mul(instanceProps[InstanceID()].objectToWorldNormal, float4(normal, 0.f)).xyz; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The plane is double-sided, so we need to check whether we hit the front or back face of the plane. When hitting the back face we simply invert the normal. We then check whether the light source actually lies above the shaded point: otherwise, there is no need to cast a ray as we already know the point lies in the shadow. Add the following after computing the normal: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // # DXR Extra - Simple Lighting bool isBackFacing = dot(normal, WorldRayDirection()) > 0.f; if (isBackFacing) normal = -normal; float3 centerLightDir = normalize(lightPos - worldOrigin); bool isShadowed = dot(normal, centerLightDir) < 0.f; if (!isShadowed) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ After the `TraceRay` call, set the `isShadowed` flag according to the result of the shadow ray: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // # DXR Extra - Simple Lighting isShadowed = shadowPayload.isHit; } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We then adapt the shadowing factor, compute the geometric term of the lighting, and use both to modulate the output: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // # DXR Extra - Simple Lighting float factor = isShadowed ? 0.3 : 1.0; float nDotL = max(0.f, dot(normal, lightDir)); float3 hitColor = float3(0.7, 0.7, 0.7)*nDotL*factor; payload.colorAndDistance = float4(hitColor, 1); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ![](/sites/default/files/pictures/2018/dx12_rtx_tutorial/Extra/simpleLighting.png) As an exercise, the reader is invited to modify the `ClosestHit` shader to compute shadows as well.