Implementing displacement decals

In this recipe, we will be creating a displacement decal in order to add dynamic detail to a mesh on the GPU in real-time. This technique combines hardware tessellation and displacement to implement local mesh deformations such as footsteps, bullet holes, and craters.

Getting reading

We are using three new textures that are available in the downloaded content for this recipe, Crater_Diffuse.png, Crater_Displacement.png, and Crater_Normal.png. The completed project can be found in the companion code Ch06_02DisplacementDecals.

How to do it…

We will begin by creating our new ShadersCommonDecal.hlsl HLSL shader file. This will introduce a new constant buffer to hold the necessary information that will be applied to our decal, include the texture references we need, and house the functions to calculate our decal's displacement:

  1. First, create a new HLSL file, ShadersCommonDecal.hlsl; be sure to change the encoding or copy an existing HLSL file and clear the contents.
  2. Within our new shader file, add the following global textures:
    // To support decal displacement mapping
    Texture2D DecalDisplacementMap : register(t2);
    Texture2D DecalDiffuse : register(t3);
    Texture2D DecalNormalMap : register(t4);
    // Assumes that SamplerState Sampler : register(s0); exists
  3. Now, add our new DecalBuffer constant buffer (note that we use the fifth constant buffer slot b4).
    // Controls the decal displacement
    cbuffer DecalBuffer : register(b4) {
        float DecalDisplaceScale; // If 0 no decal applied
        float3 DecalNormal;// If normal is 0 no decal applied
        float3 DecalTangent; // used to determine texcoord
        float3 DecalBitangent;// used to determine texcoord
        float3 DecalPosition; // decal position in local space
        float DecalRadius;    // decal size
    }
  4. We will create three new functions: one for sampling the normal, one for sampling the diffuse, and one for calculating the displacement.
    float3 DecalNormalSample(float2 decalUV)
    {
        return DecalNormalMap.Sample(Sampler, decalUV).rgb;
    }
    
    float4 DecalDiffuseSample(float2 decalUV)
    {
        return DecalDiffuse.Sample(Sampler, decalUV).rgba;
    }
    
    // The float3 result should be added to the vertex position
    float3 DecalDisplacement(float3 worldNormal, float3 worldPosition, out float3 decalUV)
    {
        float3 result = (float3)0;
        
        // Note: if the decalUV.z == 0 the pixel shader will 
        // assume no decal map needs to be queried
        decalUV = (float3)0;
    
        // Skip displacement sampling if 0 multiplier or if the 
        // decal normal is not set
        if (DecalDisplaceScale == 0 || (DecalNormal.x == 0.0 && 
                DecalNormal.y == 0.0 && DecalNormal.z == 0))
            return result;
    
        // Determine decal world position
        float3 decalPosWorld = mul(float4(DecalPosition, 1), 
            World).xyz;
        // Calculate distance from vertex to decal
        float distanceToDecal = distance(worldPosition, 
            decalPosWorld);
    
        // if distance to the decal position is within radius
        // then we need to perform displacement based on decal
        if (distanceToDecal <= DecalRadius)
        {
    ... SNIP see below
        }
        return result;
    }

    Within the if (distanceToDecal <= DecalRadius){…} block of the DecalDisplacement function, if the distance to the decal from the current vertex is within the decal radius we need to do the following:

  5. Determine the current decal UV coordinate based upon the decal's normal/tangent/bitangent and the difference between the vertex position and decal position.
    // Convert vectors to world space
    float3 dT = normalize(mul(DecalTangent, 
        (float3x3)WorldInverseTranspose));
    float3 dB = normalize(mul(DecalBitangent, 
        (float3x3)WorldInverseTranspose));
    float3 dN = normalize(mul(DecalNormal, 
        (float3x3)WorldInverseTranspose));
    float3x3 worldToDecal = float3x3(dT, dB, dN);
            
    decalUV = mul(worldToDecal, worldPosition - decalPosWorld);
    // Remap to range between 0 and 1
    decalUV /= 2 * DecalRadius; // (-0.5,0.5)
    decalUV += 0.5;  // (0,1)
    // z=1 tells pixel shader to sample decal diffuse texture
    decalUV.z = 1.0;
  6. Sample the displacement value and perform the displacement in the same way as we did for regular displacement mapping.
    // Choose the most detailed mipmap level
    const float mipLevel = 1.0f;
    // Sample height map - using R channel
    float height = DecalDisplacementMap.SampleLevel(Sampler, 
        decalUV.xy, mipLevel).r;
        
    // remap height from (0,1) to (-1, 1)
    height = (2 * height) – 1;
    
    // Return offset along DecalNormal. This allows the decal 
    // to be applied at an angle to the surface, e.g. to allow 
    // the direction of a bullet to decide the direction of 
    // deformation. Using the worldNormal instead will result 
    // in uniform decals.
    result = height * DecalDisplaceScale * dN;// worldNormal;
  7. We now have the decal UV property that needs to be forwarded to the pixel shader. For this, we add a new property to the PixelShaderInput structure within Common.hlsl
    float3 DecalUV: TEXCOORD5; // .z==1 means there is a decal

    In the domain shaders that we want to support displacement decals, we can now add the following:

  8. Include CommonDecal.hlsl after the other includes within the domain shader's HLSL file:
    #include "Common.hlsl"
    #include "CommonTess.hlsl"
    #include "CommonDecal.hlsl"
    
  9. After the existing displacement code in the domain shader, add a call to the DecalDisplacement function we just created.
    // Perform displacement
    normal = normalize(normal);
    position += CalculateDisplacement(UV, normal);
    
    // Perform decal displacement
    position += DecalDisplacement(normal, position, 
        result.DecalUV);
    
  10. Next, we need to update each pixel shader that implements texture and normal map sampling (for example, BlinnPhongPS.hlsl, DiffusePS.hlsl, and PhongPS.hlsl).
  11. Then, we include the CommonDecal.hlsl shader file after Common.hlsl.
  12. After the normal and texture sampling, and before the lighting calculations, we can check if the decal has been applied to this fragment:
    // Normal mapping
    ... SNIP
    // Texture sample here (use white if no texture)
    ... SNIP
    
    // Check if we have a decal .z == 1 means we do
    if (pixel.DecalUV.z > 0.5)
    {
        // Decal normal sample using the pixel.DecalUV and 
        // apply to the existing normal. Note that we are 
        // blending the existing normal map sample with the 
        // decal normal sample 
        normal = ApplyNormalMap(normal, pixel.WorldTangent, 
             DecalNormalSample(pixel.DecalUV.xy));
        // Decal texture sample
        float4 decalDiffuse = DecalDiffuseSample(
             pixel.DecalUV.xy);
        // lerp the current sample and the decal diffuse, using 
        // the alpha channel of the decal as 't'.
        sample = lerp(sample, float4(decalDiffuse.rgb, 
             sample.a), decalDiffuse.a);
    }
    // Final color and lighting calculations
    float3 ambient = MaterialAmbient.rgb;
    ...SNIP

    We are nearly done. All we have to do now is create our C# decal constant buffer, assign it to the appropriate shader stages, load some decal textures, and then update the decal constant buffer subresource with the location, size, and normals.

  13. Within ConstantBuffers.cs, add a new structure DecalBuffer with the equivalent properties we used in the HLSL structure. Note that we perform the appropriate padding to align correctly to 16 bytes:
    // The decal constant buffer
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct DecalBuffer
    {
        public float DecalDisplaceScale; // If 0 no decal
        public Vector3 DecalNormal; // If 0 no decal
        public Vector3 DecalTangent;
        public float _padding0;
        public Vector3 DecalBitangent;
        public float _padding1;
        public Vector3 DecalPosition;
        public float DecalRadius;
    }
  14. Within D3DApp.cs, create the new private member fields to hold the decal buffer and textures:
    // A buffer that is used to update the displacement decal
    Buffer decalBuffer; 
    ShaderResourceView decalDiffuse;
    ShaderResourceView decalDisplacement;
    ShaderResourceView decalNormal;
  15. Within D3DApp.CreateDeviceDependentResources, add the obligatory RemoveAndDispose calls for these new properties; and after the existing constant buffers, initialize, the new resources as follows:
    // Create the decal buffer
    decalBuffer = ToDispose(new Buffer(device, Utilities.SizeOf<ConstantBuffers.DecalBuffer>(), ResourceUsage.Default, BindFlags.ConstantBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0));
    
    // Load the decal textures
    decalDiffuse      = ToDispose(ShaderResourceView.FromFile(
                          device, "Crater_Diffuse.png"));
    decalDisplacement = ToDispose(ShaderResourceView.FromFile(
                          device, "Crater_Displacement.png"));
    decalNormal       = ToDispose(ShaderResourceView.FromFile(
                          device, "Crater_Normal.png"));
  16. Next, apply the constant buffer to the shader stages (remember that we used the b4 constant buffer slot in the shader code):
    // Add the decal buffer to the pixel, hull and domain 
    // shaders (it uses the 5th slot 0-indexed)
    context.HullShader.SetConstantBuffer(4, decalBuffer);
    context.DomainShader.SetConstantBuffer(4, decalBuffer);
    context.PixelShader.SetConstantBuffer(4, decalBuffer);
  17. As our DisplacementMeshRenderer class will clear the pixel shader resources, we will make its textureViews property publicly available so that we can assign the decal shader resources to the active mesh:
    public List<ShaderResourceView> TextureViews 
        { get { return textureViews; } }
  18. Now, within the render loop prior to any draw calls, assign the shader resource views to the domain shader and the mesh's new TextureView property.
    context.DomainShader.SetShaderResource(2, 
        decalDisplacement);
    ...
    var m = meshes[meshIndex];
    // Assign decal textures to mesh pixel shader
    // using registers t3 and t4
    m.TextureViews[3] = decalDiffuse;
    m.TextureViews[4] = decalNormal;
  19. And lastly, we will hard code an example decal location within the render loop by updating the decal constant buffer as follows:
    var decal = new ConstantBuffers.DecalBuffer();
    decal.DecalDisplaceScale = 0.10f; // static scale for now
    // Create orthonormalized normal/tangent/bitangent vectors
    var decalVectors = new Vector3[3];
    Vector3.Orthonormalize(decalVectors, new[] { new Vector3(0, 
        0.5f, 0.5f), Vector3.UnitX, -Vector3.UnitY });
    decal.DecalNormal = decalVectors[0];
    decal.DecalTangent = decalVectors[1];   // U-axis of tex
    decal.DecalBitangent = decalVectors[2]; // V-axis of tex
    decal.DecalPosition = new Vector3(0, 1, 1);
    decal.DecalRadius = 0.5f;
    context.UpdateSubresource(ref decal, decalBuffer);
  20. That's it! If we compile and run the project now, we will see that the displacement decal is modifying the existing surface to create a crater as per our textures. Note that the decal position must be located in the correct position so that it can be applied to the surface.
How to do it…

Displacement decal shown on plane (top-left), plane with displacement (top-right), corner of cube (bottom-left), and the tree log (bottom-right).

How it works…

Basically, displacement decals work in the same way as regular displacement mapping. The main differences are that the existence and location of the decal is dependent upon information passed into a constant buffer, and the UV coordinates for the displacement/normal and diffuse sampling are determined based on the difference between the current vertex position and decal position.

Reviewing the tangent space that we covered in Adding surface detail with normal mapping, we can see how the normal, tangent, and bitangent vectors that we assign to the decal constant buffer are controlling the orientation (rotation and angle) of the decal on the surface it is applied.

The example decal position and tangent vectors in the code for this recipe will result in no decal appearing on the plane. This is not only due to the position not meeting the surface, but also because the angle of the decal will look odd. The following code snippet includes values that will appear on the plane in the example scene:

Vector3.Orthonormalize(decalVectors, new[] { 
    new Vector3(0, 0.5f, 0.2f), Vector3.UnitX, Vector3.UnitZ });
decal.DecalPosition = new Vector3(0, 0, 0);

Our current implementation blends the two displacements together; although in certain circumstances, such as the crater shown previously, you may want to replace the existing displacement. This is easy enough to do; however, it is also necessary to blend the normal correctly, otherwise the two surfaces will appear disjointed. A solution for this is to include another channel in the decal displacement map that controls where and to what extent the decal overrides the existing displacement and normal (functioning like an alpha channel).

There's more…

The constant buffer could be easily extended to support multiple decals by changing it to an array of parameters. Chapter 4, Animating Meshes with Vertex Skinning, has an example of how to use arrays of elements in a constant buffer structure for the bones.

Currently we are just setting the decal properties directly in code; instead, we could have this event based (for example, mouse click) and determine the correct location from there. The decal constant buffer only needs to be updated when a decal is changed. If a significant number of decals need to be supported, then it may be a better option to store the decal properties within a regular buffer or texture.

Another interesting effect can be easily created by decreasing the decal displacement scale over time to simulate a fading decal (for example, a fading footstep). However, you will want to apply the same decay on the diffuse/normal texture samples.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.94.153