In this recipe, we will implement dynamic cubic environment mapping or cube mapping and explore how threading impacts the rendering performance. A cube map is commonly used for skyboxes with the camera located inside the cube. In this recipe, we will be using cube maps to implement reflections for objects. We will also be rendering directly to the cube map resource in order to implement dynamic reflections. In Direct3D, a cube map is implemented using a
texture cube; this is a special texture array with six slices where each slice represents a face of the cube along an axis. The TextureCube
HLSL shader resource provides built-in sampling support. The following figure shows a static cube map laid out flat, and the texture array indices are matched to the appropriate axis:
Note that the cube map is defined using a left-handed coordinate system (that is, +Z is forward, +X is to the right, and +Y is upward). Our recipe will, therefore require axis scaling and vertex winding changes when generating dynamic cube maps.
The Collosseum cube map is by Emil Persson, who has a number of static cube map textures released under a Creative Commons Attribution 3.0 license available at http://www.humus.name/index.php?page=Textures.
For this recipe, we will start with the project from the previous recipe, Benchmarking multithreaded rendering. The scene file named Scene.fbx
, used in the example screenshots, is available from the companion source download for this book. The completed project can also be found in the companion source named Ch09_02DynamicCubeMapping
.
We will begin by creating the necessary HLSL code to generate and consume our cube maps. Generating the cube map will involve creating a new vertex shader (VS_CubeMap
), geometry shader (GS_CubeMap
), and pixel shader (PS_CubeMap
); to consume cube maps, we will update the PerMaterial
constant buffer and each pixel shader.
PerMaterial
constant buffer in ShadersCommon.hlsl
to include a flag indicating whether a material is reflective and how reflective it is. This will be used by the pixel shaders.cbuffer PerMaterial : register (b2) {... bool IsReflective; float ReflectionAmount; ...};
ShadersCubeMap.hlsl
, and ensure that it has the correct encoding as described in Chapter 2, Rendering with Direct3D. Add the following include
directive to use our existing constant buffers, vertex, and pixel structures:#include "Common.hlsl"
// Cube map ViewProjections for each face cbuffer PerEnvironmentMap : register(b4) { float4x4 CubeFaceViewProj[6]; }; // Use the PixelShaderInput as GeometryShaderInput #define GeometryShaderInput PixelShaderInput
PixelShaderInput
structure within ShadersCommon.hlsl
, except that we have added one new property to control the render target (that is, the face of the cube map) used.// Pixel Shader input structure (from Geometry Shader) struct GS_CubeMapOutput { float4 Position : SV_Position; ...SNIP (existing PixelShaderInput structure properties) // Allows writing to multiple render targets uint RTIndex : SV_RenderTargetArrayIndex; };
ShadersVS.hlsl
; however, we need to apply the World
matrix transform on the Position
property.// Vertex shader cubemap function GeometryShaderInput VS_CubeMap(VertexShaderInput vertex) { GeometryShaderInput result = (GeometryShaderInput)0; ...SNIP // Only apply world transform (not WorldViewProjection) result.Position = mul(vertex.Position, World); ...SNIP }
instance
attribute of Shader Model 5 to execute the shader six times per input primitive (that is, six times per triangle).[maxvertexcount(3)] // Outgoing vertex count (1 triangle) [instance(6)] // Number of times to execute for each input void GS_CubeMap(triangle GeometryShaderInput input[3], uint instanceId: SV_GSInstanceID, inout TriangleStream<GS_CubeMapOutput> stream) { // Output the input triangle using the View/Projection // of the cube face identified by instanceId float4x4 viewProj = CubeFaceViewProj[instanceId]; GS_CubeMapOutput output; // Assign the render target instance // i.e. 0 = +X-face, 1 = -X-face and so on output.RTIndex = instanceId; // In order to render correctly into a TextureCube we // must either: // 1) use a left-handed view/projection; OR // 2) use a right-handed view/projection with -1 X- // axis scale // Our meshes assume a right-handed coordinate system // therefore both cases above require vertex winding // to be switched. uint3 idx = uint3(0,2,1); [unroll] for (int v = 0; v < 3; v++) { // Apply cube face view/projection output.Position = mul(input[idx[v]].Position, viewProj); // Copy other vertex properties as is output.WorldPosition = input[idx[v]].WorldPosition; ...SNIP // Append to the stream stream.Append(output); } stream.RestartStrip(); }
// Globals for texture sampling Texture2D Texture0 : register(t0); TextureCube Reflection : register(t1); SamplerState Sampler : register(s0); float4 PS_CubeMap(GS_CubeMapOutput pixel) : SV_Target { // Normalize our vectors float3 normal = normalize(pixel.WorldNormal); float3 toEye = normalize(pixel.ToCamera); ...SNIP // Calculate reflection (if any) if (IsReflective) { float3 reflection = reflect(-toEye, normal); color = lerp(color, Reflection.Sample(Sampler, reflection).rgb, ReflectionAmount); } // Calculate final alpha value and return float alpha = pixel.Diffuse.a * sample.a; return float4(color, alpha); }
BlinnPhongPS.hlsl
) that will implement reflections, add the highlighted changes (as shown in the preceding code snippet), except for the function signature. This includes the TextureCube
shader resource and blending of the reflection sample.PerMaterial
structure, create our DynamicCubeMap
renderer class, and update our MeshRenderer
class to use the cube map for reflections.ConstantBuffers.cs
file, add the IsReflective
and ReflectionAmount
properties to the PerMaterial
structure.[StructLayout(LayoutKind.Sequential, Pack = 1)] public struct PerMaterial { ... public uint IsReflective; //reflective (0 false, 1 true) public float ReflectionAmount; // how reflective? 0-1 ... }
Common.RendererBase
called DynamicCubeMap
.using SharpDX; using SharpDX.DXGI; using SharpDX.Direct3D11; using Common; using Buffer = SharpDX.Direct3D11.Buffer; // Represents a dynamic cubic environment map (cube map) public class DynamicCubeMap: Common.RendererBase { ... }
// Represents the camera for a cube face // Note: the View matrix includes the current position // Matrix.Transpose(Matrix.Invert(View)).Column4==Position public struct CubeFaceCamera { public Matrix View; public Matrix Projection; }
DynamicCubeMap
class:// The cubic environment map texture array (6 slices) Texture2D EnvMap; // The RTV for all cube map faces (for single pass) RenderTargetView EnvMapRTV; // The DSV for all cube map faces (for single pass) DepthStencilView EnvMapDSV; // The TextureCube SRV for use by the mesh/renderer public ShaderResourceView EnvMapSRV; // The 'per cube map buffer' to be assigned to the geometry // shader stage when rendering the cubemap. This will // contain the 6 ViewProjection matrices for the cube map. public Buffer PerEnvMapBuffer; // The viewport based on Size x Size ViewportF Viewport; // The renderer instance using the cube map reflection public RendererBase Reflector { get; set; } // The cameras for each face of the cube public CubeFaceCamera[] Cameras = new CubeFaceCamera[6]; // The cube map texture size (e.g. 256x256) public int Size { get; private set; } public DynamicCubeMap(int size = 256) : base() { // Set the cube map resolution (e.g. 256 x 256) Size = size; }
DynamicCubeMap.CreateDeviceDependentResources
with the following code to reset the resources and to retrieve the device reference:RemoveAndDispose(ref EnvMap); RemoveAndDispose(ref EnvMapSRV); RemoveAndDispose(ref EnvMapRTV); RemoveAndDispose(ref EnvMapDSV); RemoveAndDispose(ref PerEnvMapBuffer); var device = this.DeviceManager.Direct3DDevice;
texture
array resource. The two important properties that define a resource compatible with the TextureCube
HLSL shader resource are highlighted in the following code snippet:// Create the cube map TextureCube (array of 6 textures) var textureDesc = new Texture2DDescription() { Format = Format.R8G8B8A8_UNorm, Height = this.Size, Width = this.Size, ArraySize = 6, // 6-sides of the cube BindFlags = BindFlags.ShaderResource | BindFlags.RenderTarget, OptionFlags = ResourceOptionFlags.GenerateMipMaps | ResourceOptionFlags.TextureCube, SampleDescription = new SampleDescription(1, 0), MipLevels = 0, Usage = ResourceUsage.Default, CpuAccessFlags = CpuAccessFlags.None, }; EnvMap = ToDispose(new Texture2D(device, textureDesc));
// Create the SRV for the texture cube
var descSRV = new ShaderResourceViewDescription();
descSRV.Format = textureDesc.Format;
descSRV.Dimension = SharpDX.Direct3D.ShaderResourceViewDimension.TextureCube;
descSRV.TextureCube.MostDetailedMip = 0;
descSRV.TextureCube.MipLevels = -1;
EnvMapSRV = ToDispose(new ShaderResourceView(device, EnvMap, descSRV));
// Create the RTVs var descRTV = new RenderTargetViewDescription(); descRTV.Format = textureDesc.Format; descRTV.Dimension = RenderTargetViewDimension.Texture2DArray; descRTV.Texture2DArray.MipSlice = 0; // Single RTV array for single pass rendering descRTV.Texture2DArray.FirstArraySlice = 0; descRTV.Texture2DArray.ArraySize = 6; EnvMapRTV = ToDispose(new RenderTargetView(device, EnvMap, descRTV));
// Create DSVs using (var depth = new Texture2D(device, new Texture2DDescription { Format = Format.D32_Float, BindFlags = BindFlags.DepthStencil, Height = Size, Width = Size, Usage = ResourceUsage.Default, SampleDescription = new SampleDescription(1, 0), CpuAccessFlags = CpuAccessFlags.None, MipLevels = 1, OptionFlags = ResourceOptionFlags.TextureCube, ArraySize = 6 // 6-sides of the cube })) { var descDSV = new DepthStencilViewDescription(); descDSV.Format = depth.Description.Format; descDSV.Dimension = DepthStencilViewDimension.Texture2DArray; descDSV.Flags = DepthStencilViewFlags.None; descDSV.Texture2DArray.MipSlice = 0; // Single DSV array for single pass rendering descDSV.Texture2DArray.FirstArraySlice = 0; descDSV.Texture2DArray.ArraySize = 6; EnvMapDSV = ToDispose(new DepthStencilView(device, depth, descDSV)); }
// Create the viewport Viewport = new Viewport(0, 0, Size, Size); // Create the per environment map buffer (to store the 6 // ViewProjection matrices) PerEnvMapBuffer = ToDispose(new Buffer(device, Utilities.SizeOf<Matrix>() * 6, ResourceUsage.Default, BindFlags.ConstantBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0));
This completes the initialization of our cube map's Direct3D resources.
DynamicCubeMap
class, we will create a new public method for updating the current camera positions.// Update camera position for cube faces
public void SetViewPoint(Vector3 camera)
{ // The LookAt targets for view matrices
var targets = new[] {
camera + Vector3.UnitX, // +X
camera - Vector3.UnitX, // -X
camera + Vector3.UnitY, // +Y
camera - Vector3.UnitY, // -Y
camera + Vector3.UnitZ, // +Z
camera - Vector3.UnitZ // -Z
};
// The "up" vector for view matrices
var upVectors = new[] {
Vector3.UnitY, // +X
Vector3.UnitY, // -X
-Vector3.UnitZ,// +Y
+Vector3.UnitZ,// -Y
Vector3.UnitY, // +Z
Vector3.UnitY, // -Z
};
// Create view and projection matrix for each face
for (int i = 0; i < 6; i++)
{
Cameras[i].View = Matrix.LookAtRH(camera,
targets[i],
upVectors[i]) * Matrix.Scaling(-1, 1, 1);
Cameras[i].Projection = Matrix.PerspectiveFovRH(
(float)Math.PI * 0.5f, 1.0f, 0.1f, 100.0f);
}
}
To remain consistent, we have used a right-handed coordinate system for the view. However, the TextureCube
resource will be a little backwards unless we also scale -1
along the x axis. We will also need to reverse the vertex winding order (as we have done in the geometry shader GS_CubeMap
) or switch the culling from back face to front face (or use no culling).
delegate
that will perform the rendering logic. Therefore, we will not use the DynamicCubeMap.DoRender
override and instead create a new public function named UpdateSinglePass
.protected override void DoRender() { throw new NotImplementedException("Use UpdateSinglePass instead."); } // Update the 6-sides of the cube map using a single pass // via Geometry shader instancing with the provided context // renderScene: The method that will render the scene public void UpdateSinglePass( DeviceContext context, Action<DeviceContext, Matrix, Matrix, RenderTargetView, DepthStencilView, DynamicCubeMap> renderScene) { // Don't render the reflector itself if (Reflector != null) Reflector.Show = false; // Prepare pipeline context.OutputMerger.SetRenderTargets(EnvMapDSV, EnvMapRTV); context.Rasterizer.SetViewport(Viewport); // Prepare the view projections Matrix[] viewProjections = new Matrix[6]; for (var i = 0; i < 6; i++) viewProjections[i] = Matrix.Transpose( Cameras[i].View * Cameras[i].Projection); // Update perEnvMapBuffer with the ViewProjections context.UpdateSubresource(viewProjections, PerEnvMapBuffer); // Assign perEnvMapBuffer to the GS stage slot 4 context.GeometryShader .SetConstantBuffer(4, PerEnvMapBuffer); // Render scene using the view, projection, RTV and DSV renderScene(context, Cameras[0].View, Cameras[0].Projection, EnvMapRTV, EnvMapDSV, this); // Unbind the RTV and DSV context.OutputMerger.ResetTargets(); // Prepare the SRV mip levels context.GenerateMips(EnvMapSRV); // Re-enable the Reflector renderer if (Reflector != null) Reflector.Show = true; }
This completes our DynamicCubeMap
renderer class. Next, we need to update the MeshRenderer
class to consume DyanamicCubeMap.EnvMapSRV
.
MeshRenderer
, add a new public property for assigning a cube map.public DynamicCubeMap EnvironmentMap { get; set; }
MeshRenderer.DoRender
method, where the material constant buffer is prepared, add the following code to assign the cube map SRV:...SNIP // If this mesh has a cube map assigned set // the material buffer accordingly if (this.EnvironmentMap != null) { material.IsReflective = 1; material.ReflectionAmount = 0.4f; context.PixelShader.SetShaderResource(1, this.EnvironmentMap.EnvMapSRV); } // Update material buffer context.UpdateSubresource(ref material, PerMaterialBuffer); ...SNIP
For our final changes, we'll move over to D3DApp.cs
where we will compile the *_CubeMap
shaders, move our rendering logic into a reusable action, and implement threading.
CubeMap.hlsl
within D3DApp.CreateDeviceDependentResources
as we have done in previous chapters. Compile the shader functions VS_CubeMap
, GS_CubeMap
, and PS_CubeMap
using the vs_5_0
, gs_5_0
, and ps_5_0
shader profiles respectively.DeviceContext
initialization within a D3DApp.InitializationContext
function. However, we will be calling this for each context before each rendering pass and need to preserve the existing render targets under certain circumstances. In addition, the vertex, pixel, and geometry shaders change depending on whether we are rendering the cube map or the final scene.VertexShader activeVertexShader = null; GeometryShader activeGeometryShader = null; PixelShader activePixelShader = null; protected void InitializeContext(DeviceContext context, bool updateRenderTarget) { ...SNIP // Set the default vertex shader to run context.VertexShader.Set(activeVertexShader); // Set the constant buffer for geometry shader stage context.GeometryShader.SetConstantBuffer(0, perObjectBuffer); context.GeometryShader.SetConstantBuffer(1, perFrameBuffer); // Set the geometry shader context.GeometryShader.Set(activeGeometryShader); ...SNIP // Set the pixel shader to run context.PixelShader.Set(activePixelShader); ...SNIP // When rendering cube maps don't change the render target if (updateRenderTarget) { // Set viewport context.Rasterizer.SetViewport(this.Viewport); // Set render targets context.OutputMerger.SetTargets(this.DepthStencilView, this.RenderTargetView); } }
D3DApp.Run
, add a new local List<DynamicCubeMap>
instance to keep track of the available cube maps.// Keep track of list of cube maps List<DynamicCubeMap> envMaps = new List<DynamicCubeMap>();
DynamicCubeMap
instance and initialize.// If MeshRenderer instance is reflective:
var mesh = ...some reflective MeshRenderer instance
var envMap = ToDispose(new DynamicCubeMap(256));
envMap.Reflector = mesh;
envMap.Initialize(this);
m.EnvironmentMap = envMap;
// Add to list of cube maps
envMaps.Add(envMap);
// Action for rendering the entire scene Action<DeviceContext, Matrix, Matrix, RenderTargetView, DepthStencilView, DynamicCubeMap> renderScene = (context, view, projection, rtv, dsv, envMap) => { // We must initialize the context every time we render // the scene as we are changing the state depending on // whether we are rendering a cube map or final scene InitializeContext(context, false); // We always need the immediate context var immediateContext = this.DeviceManager.Direct3DDevice .ImmediateContext; // Clear depth stencil view context.ClearDepthStencilView(dsv, DepthStencilClearFlags.Depth | DepthStencilClearFlags.Stencil, 1.0f, 0); // Clear render target view context.ClearRenderTargetView(rtv, background); // Create viewProjection matrix var viewProjection = Matrix.Multiply(view, projection); // Extract camera position from view var camPosition = Matrix.Transpose(Matrix.Invert(view)).Column4; cameraPosition = new Vector3(camPosition.X, camPosition.Y, camPosition.Z); ...SNIP perform all rendering actions and multithreading }
renderScene
is rendering the environment map, it is necessary to assign the per environment map constant buffer to the geometry shader stage for each deferred context.// If multithreaded ... // If environment map is being rendered if (envMap != null) renderContext.GeometryShader.SetConstantBuffer(4, envMap.PerEnvMapBuffer);
RenderLoop.Run(Window, () => { ... })
, to first render each cube map, then render the final scene, as shown in the following code:// Retrieve immediate context var context = DeviceManager.Direct3DContext; #region Update environment maps // Assign the environment map rendering shaders activeVertexShader = envMapVSShader; activeGeometryShader = envMapGSShader; activePixelShader = envMapPSShader; // Render each environment map foreach (var envMap in envMaps) { var mesh = envMap.Reflector as MeshRenderer; if (mesh != null) { // Calculate view point for reflector var center = Vector3.Transform( mesh.Mesh.Extent.Center, mesh.World * worldMatrix); envMap.SetViewPoint(new Vector3(center.X, center.Y, center.Z)); // Render envmap in single full render pass using // geometry shader instancing. envMap.UpdateSinglePass(context, renderScene); } } #endregion #region Render final scene // Reset the vertex, geometry and pixel shader activeVertexShader = vertexShader; activeGeometryShader = null; activePixelShader = blinnPhongShader; // Initialize context (also resetting the render targets) InitializeContext(context, true); // Render the final scene renderScene(context, viewMatrix, projectionMatrix, RenderTargetView, DepthStencilView, null); #endregion
The following screenshot shows the dynamic cube map from this recipe used with seven reflective surfaces. The 100 cubes in the sky are rotating around the y axis, and the cube maps are updated dynamically. The close up of the spheres illustrates how reflections include the reflections on other surfaces:
Rather than rendering the entire scene six times per cube map, we use multiple render targets and the instance
attribute of the geometry shader to do this in a single render pass in a fraction of the time (approximately three to four times faster). For each triangle output from the vertex shader, the Direct3D pipeline calls the geometry shader six times as per the instance
attribute. The SV_GSInstanceID
input semantic contains the zero-based instance ID; this ID is used to index the view/projections that we calculated for each cube face. We indicate which render target to send the fragment to by setting the SV_RenderTargetArrayIndex
input semantic (GS_CubeMapOutput.RTIndex
in our example HLSL) to the value of the geometry shader's instance ID.
The following diagram outlines the process within the pipeline:
To calculate the reflection in our pixel shader, we use the intrinsic HLSL reflect
function; this takes the camera direction and surface normal to compute a reflection vector. This reflection vector is used to sample the cube from its SRV as shown in the following diagram:
The view/projections are calculated for each face by taking the object's center point and creating "look at" view matrices for all the six faces. Because we are using a right-handed coordinate system, it is necessary for us to flip the x axis of the cube map view matrix by scaling the x axis by -1
and reversing the vertex winding order in the geometry shader. By implementing multithreaded deferred contexts, we can improve the performance when there is an increased CPU load or larger numbers of draw calls (for example, more reflective surfaces).
The following graph shows the performance impact of multithreading:
The worst case scenario indicates a situation where there are two dynamic cube maps, no CPU load, and only 100 cubes reflecting in the sky. The best case scenario is of three dynamic cube maps with 2,000 matrix operations per mesh and 300 cubes in the sky. The impact of multithreading did not hit a positive result when there was no additional CPU load for 100 or 200 cubes in the sky but did for 300 cubes. It is clear that once there is enough CPU load, multithreaded rendering produces significant performance benefits; however, there are certain cases where it can have a detrimental effect.
It is important to note that our implementation does not implement frustum culling or object occlusion. We also do not take into consideration whether or not a face of the cube map will be visible; however, this gets more complicated when you consider the reflections of reflections.
Within the completed companion project for this recipe, any mesh with a name containing reflector
will have a cube map associated, any mesh name containing rotate
will be rotated around the y axis, and adding a mesh name containing replicate
will cause the object to be duplicated and arranged in a grid pattern. The same mesh can contain any or all of the three of these key words. There is also an implementation of the six-pass cube map for performance comparison.
3.12.162.65