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.
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
.
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:
ShadersCommonDecal.hlsl
; be sure to change the encoding or copy an existing HLSL file and clear the contents.// To support decal displacement mapping Texture2D DecalDisplacementMap : register(t2); Texture2D DecalDiffuse : register(t3); Texture2D DecalNormalMap : register(t4); // Assumes that SamplerState Sampler : register(s0); exists
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 }
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:
// 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;
// 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;
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:
CommonDecal.hlsl
after the other includes within the domain shader's HLSL file:#include "Common.hlsl"
#include "CommonTess.hlsl"
#include "CommonDecal.hlsl"
DecalDisplacement
function we just created.// Perform displacement normal = normalize(normal); position += CalculateDisplacement(UV, normal); // Perform decal displacement position += DecalDisplacement(normal, position, result.DecalUV);
BlinnPhongPS.hlsl
, DiffusePS.hlsl
, and PhongPS.hlsl
).CommonDecal.hlsl
shader file after Common.hlsl
.// 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.
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; }
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;
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"));
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);
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; } }
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;
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);
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).
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.
18.222.94.153