Sunday, August 11, 2013 Eric Richards

Planar Reflections and Shadows using the Stencil Buffer in SlimDX and Direct3D 11

In this post, we are going to discuss applications of the Direct3D stencil buffer, by porting the example from Chapter 10 of Frank Luna’s Introduction to 3D Game Programming with Direct3D 11.0 to C# and SlimDX.  We will create a simple scene, consisting of an object (in our case, the skull mesh that we have used previously), and some simple room geometry, including a section which will act as a mirror and reflect the rest of the geometry in our scene.  We will also implement planar shadows, so that our central object will cast shadows on the rest of our geometry when it is blocking our primary directional light.  The full code for this example can be downloaded from my GitHub repository, at https://github.com/ericrrichards/dx11.git, under the MirrorDemo project.

mirror

The Stencil Buffer

The stencil buffer is an off-screen buffer that we can configure to control how objects are drawn.  It gets its name from our ability to use it to mask out certain areas of the screen, as you would use a real-life stencil.  We have actually been using the stencil buffer implicitly all along; our D3DApp class creates the stencil buffer as part of the depth buffer in our OnResize() function.  The depth and stencil buffers are combined as a single texture, with the bits of each texel allocated to the depth or stencil buffer according to the Format enum type specified.  These formats are:

  • Format.D24_UNorm_S8_UInt – This is probably the most common format that you will use.  Each element of the depth/stencil buffer is 32 bits, with 24 bits allocated as a floating-point depth buffer in the range [0, 1], and 8 bits allocated to the stencil for a range of [0-255].
  • Format.D32_UNorm_S8X24_UInt – This format uses a 32 bit floating-point depth buffer, for some additional precision, with the same 8-bit stencil, and 24 bits of padding.  Note that this format requires 64 bits of memory per texel, double that of the D24_UNorm_S8_UInt format, so unless you are experiencing a lot of z-fighting and need the extra precision, you may want to avoid this format.

Additionally, we can specify either a 16 or 32 bit depth-only buffer, using Format.D16_UNorm or Format.D32_Float, however, these formats do not contain a stencil buffer component, so we will not consider them.

We can clear the depth-stencil buffer using the DeviceContext.ClearDepthStencilView() function, which we have been using thus far without explaining in our DrawScene() functions.

ImmediateContext.ClearDepthStencilView(DepthStencilView, DepthStencilClearFlags.Depth | DepthStencilClearFlags.Stencil, 1.0f, 0);

The parameters of this function are:

  1. The depth/stencil resource view which we have created to our depth/stencil buffer.  In our framework, this is stored as the member variable DepthStencilView in the D3DApp class, and is created when we create the depth-stencil buffer in our OnResize function.
  2. A DepthStencilClearFlags enum object, which indicates whether we want to clear the depth, stencil, or both buffers.
  3. A float value to clear the depth buffer to.
  4. A byte value to clear the stencil buffer to.

Direct3D knows how to shift the bits of the values written to the depth-stencil buffer, based on the format the buffer was created with.

Stencil Test

We can use the stencil buffer to mask certain areas of the back-buffer by using the stencil test.  This test is applied in the graphics hardware during the rasterization stage (i.e. after our pixel shader has executed).  The stencil test operates on each pixel as follows:

if ( stencilRef & stencilReadMask (operation) pixel_value & stencilReadMask) {
    accept pixel;
} else {
    reject pixel;
}

Here, stencilRef is a value that we supply as a reference value, pixel_value is the value currently in the stencil buffer for the pixel, and stencilReadMask is a bitmask indicating which bits of the stencil buffer should be compared.  (operation) is a comparison function, defined as one of the Comparison enum members, which are the same as we have previously covered for blending.

We control the stencil test by creating and applying a DepthStencilState state block object.  To create a DepthStencilState object, first we need to fill out a DepthStencilStateDescription structure.  The members of the DepthStencilStateDescription structure are:

  • IsDepthEnabled – This flag allows us to specify whether the depth test is performed.  Typically, we will always set this to true, as we would otherwise need to make sure that we rendered objects front-to-back for proper results.
  • DepthWriteMask – This enables us to control whether the depth buffer is written to.  The default is DepthWriteMask.All, which will update the depth buffer; we can use DepthWriteMask.Zero to disable depth writes.
  • DepthComparison – This member is one of the Comparison enum elements, which controls how Direct3D selects pixels based on their depth value.  Usually, we will use the default value of Comparison.Less, so that the normal depth test is performed.
  • IsStencilEnabled – This value controls whether the stencil test is performed.  The default is false, so that the stencil test is not used, and so we need to specify true to use the stencil test.
  • StencilReadMask – This is the bitmask used in the stencil test, as described above.  We will need to supply a byte with the bits that we wish to read set to 1’s.  To read all bytes (i.e. no masking) we supply 0xff.
  • StencilWriteMask -  When we update the stencil buffer, we can also mask certain bits.  To write all bits (which we will be doing), supply 0xff.
  • FrontFace – a DepthStencilOperationDescription structure indicating how the stencil buffer should consider front-facing triangles.
  • BackFace – a DepthStencilOperationDescription structure indicating how the stencil buffer should consider back-facing triangles.

The DepthStencilOperationDescription structure defines the comparison function used in the stencil test, as well as how the stencil buffer should be updated based on the results of the stencil test.  Note that we are able to define different operations for the front and back-facing triangles.  The members of the DepthStencilOperationDescription structure are:

  • FailOperation – A member of the StencilOperation enum describing how the stencil buffer should be updated when the stencil test fails.
  • DepthFailOperation – A member of the StencilOperation enum describing how the stencil buffer should be updated if the stencil test succeeds, but the depth test fails.
  • PassOperation – A member of the StencilOperation enum describing how the stencil buffer should be updated when the stencil test succeeds.
  • Comparison – One of the aforementioned Comparison enum members, which specifies the operation used in the stencil test.

The StencilOperation enum provides the following options for updating the stencil buffer:

  • StencilOperation.Keep – Keep the current value of the stencil buffer; do not update it.
  • StencilOperation.Zero – Set the stencil buffer to 0.
  • StencilOperation.Replace – Replace the current value of the stencil buffer with the stencil reference value.
  • StencilOperation.IncrementAndClamp – Increment the value of the stencil buffer.  If the value is larger than the maximum stencil value (255), clamp the value to 255.
  • StencilOperation.DecrementAndClamp – Decrement the value of the stencil buffer and clamp the value to >= 0.
  • StencilOperation.Invert -  Invert the bits of the stencil buffer.  I.e. 0 –> 0xff.
  • StencilOperation.Increment – Increment the stencil buffer value.  If we exceed the maximum value (255), wrap around to the minimum value (0).’
  • StencilOperation.Decrement – Decrement the stencil buffer value.  Wrap to the maximum value (255), if the value becomes less than 0.

Once we have done all this, we can create the DepthStencilState by using the static DepthStencilState.FromDescription() factory method.

New RenderStates

Now, it’s time for us to implement the DepthStencilStates that we will need for our mirror/shadowing demo.  We will define three DepthStencilStates:

  • MarkMirrorDSS – This state will be used to mark the position of our mirror on the stencil buffer, without changing the depth buffer.  We will pair this with a new BlendState (See Blending Theory), NoRenderTargetWritesBS, which will disable writing any color information to the backbuffer, so that we the combined effect will be to write only to the stencil.
  • DrawReflectionDSS – This state will be used to draw the geometry that should appear as a reflection in our mirror.  We will set the stencil test up so that we will only render pixels if they have been previously marked as part of the mirror by the MarkMirrorDSS.
  • NoDoubleBlendDSS – This state will be used to draw our shadows.  Because we are drawing our shadows as partially transparent black using alpha-blending, if we were to simply draw the shadow geometry, we would have darker patches where multiple surfaces of the shadow object are projected to the shadow plane, a condition known as shadow-acne.  Instead, we setup the stencil test to check that the current stencil value is equal to the reference value, and increment on passes.  Thus, the first time a projected pixel is drawn, it will pass the stencil test, increment the stencil value, and be rendered.  On subsequent draws, the pixel will then fail the stencil test.

The full implementation of these DepthStencilStates is as follows (from RenderStates.cs):

var noRenderTargetWritesDesc = new BlendStateDescription {
    AlphaToCoverageEnable = false,
    IndependentBlendEnable = false
};
noRenderTargetWritesDesc.RenderTargets[0].BlendEnable = false;
noRenderTargetWritesDesc.RenderTargets[0].SourceBlend = BlendOption.One;
noRenderTargetWritesDesc.RenderTargets[0].DestinationBlend = BlendOption.Zero;
noRenderTargetWritesDesc.RenderTargets[0].BlendOperation = BlendOperation.Add;
noRenderTargetWritesDesc.RenderTargets[0].SourceBlendAlpha = BlendOption.One;
noRenderTargetWritesDesc.RenderTargets[0].DestinationBlendAlpha = BlendOption.Zero;
noRenderTargetWritesDesc.RenderTargets[0].BlendOperationAlpha = BlendOperation.Add;
noRenderTargetWritesDesc.RenderTargets[0].RenderTargetWriteMask = ColorWriteMaskFlags.None;

NoRenderTargetWritesBS = BlendState.FromDescription(device, noRenderTargetWritesDesc);

var mirrorDesc = new DepthStencilStateDescription {
    IsDepthEnabled = true,
    DepthWriteMask = DepthWriteMask.Zero,
    DepthComparison = Comparison.Less,
    IsStencilEnabled = true,
    StencilReadMask = 0xff,
    StencilWriteMask = 0xff, 
    FrontFace = new DepthStencilOperationDescription {
        FailOperation = StencilOperation.Keep,
        DepthFailOperation = StencilOperation.Keep,
        PassOperation = StencilOperation.Replace,
        Comparison = Comparison.Always
    },
    BackFace = new DepthStencilOperationDescription {
        FailOperation = StencilOperation.Keep,
        DepthFailOperation = StencilOperation.Keep,
        PassOperation = StencilOperation.Replace,
        Comparison = Comparison.Always
    }
};

MarkMirrorDSS = DepthStencilState.FromDescription(device, mirrorDesc);

var drawReflectionDesc = new DepthStencilStateDescription {
    IsDepthEnabled = true,
    DepthWriteMask = DepthWriteMask.All,
    DepthComparison = Comparison.Less,
    IsStencilEnabled = true,
    StencilReadMask = 0xff,
    StencilWriteMask = 0xff,
    FrontFace = new DepthStencilOperationDescription {
        FailOperation = StencilOperation.Keep,
        DepthFailOperation = StencilOperation.Keep,
        PassOperation = StencilOperation.Keep,
        Comparison = Comparison.Equal
    },
    BackFace = new DepthStencilOperationDescription {
        FailOperation = StencilOperation.Keep,
        DepthFailOperation = StencilOperation.Keep,
        PassOperation = StencilOperation.Keep,
        Comparison = Comparison.Equal
    }
};
DrawReflectionDSS = DepthStencilState.FromDescription(device, drawReflectionDesc);

var noDoubleBlendDesc = new DepthStencilStateDescription {
    IsDepthEnabled = true,
    DepthWriteMask = DepthWriteMask.All,
    DepthComparison = Comparison.Less,
    IsStencilEnabled = true,
    StencilReadMask = 0xff,
    StencilWriteMask = 0xff,
    FrontFace = new DepthStencilOperationDescription {
        FailOperation = StencilOperation.Keep,
        DepthFailOperation = StencilOperation.Keep,
        PassOperation = StencilOperation.Increment,
        Comparison = Comparison.Equal
    },
    BackFace = new DepthStencilOperationDescription {
        FailOperation = StencilOperation.Keep,
        DepthFailOperation = StencilOperation.Keep,
        PassOperation = StencilOperation.Increment,
        Comparison = Comparison.Equal
    }
};
NoDoubleBlendDSS = DepthStencilState.FromDescription(device, noDoubleBlendDesc);

Implementing the Demo

I’m going to omit the bulk of the code for this example, as it should be easy to follow if you’ve been looking at the previous examples.  Setting up materials and lights, building geometry, and the simple update code for this example should be old hat by now.  We’re still using our BasicEffect shader to render, which has not changed since the Blend Demo.  Most of the interesting stuff happens in the DrawScene() function, which I have split out into helper functions that each handle one component of the rendering.  Leaving out the extraneous details of setting our global shader variables and selecting rendering techniques, this looks like:

public override void DrawScene() {
    // snip...
            
    DrawRoom(activeTech, viewProj);
    DrawSkull(activeSkullTech, viewProj);
    MarkMirrorOnStencil(activeTech, viewProj, blendFactor);
    DrawFloorReflection(activeTech, viewProj);
    DrawSkullReflection(activeSkullTech, viewProj);
            
    DrawSkullShadowReflection(activeSkullTech, viewProj, blendFactor);
    DrawMirror(activeTech, viewProj, blendFactor);

    ImmediateContext.ClearDepthStencilView(DepthStencilView, DepthStencilClearFlags.Stencil, 1.0f, 0);

    DrawSkullShadow(activeSkullTech, viewProj, blendFactor);
    SwapChain.Present(0, PresentFlags.None);

}

We have two rendering techniques here, because our room geometry is textured, while our skull mesh is not.  The first two draw functions are standard stuff to just draw our room and skull geometry, with no special effects.  Below is a screenshot of just our room geometry and our skull; note that we left out the portion of the wall which will be our mirror.

mirror-no-reflections-noshadow

Planar Reflections

To draw our reflection, we will need to reflect our scene geometry across the mirror plane.  This transformation can be represented by what is called a reflection matrix.  Fortunately for us, SlimDX’s Matrix class has a static method Reflection, which will return a proper reflection matrix given the mirror plane.  We can then multiply the world matrix of our object by this reflection matrix and draw our geometry reflected.  However, just reflecting the geometry alone will not get us a proper mirror, as the geometry will be rendered outside of the mirror, as the screenshot shows.

reflection-no-stencil

Instead, we must first mark the pixels of the mirror on the stencil buffer.  We do this by rendering the quad for the mirror using our NoRenderTargetWritesBS and MarkMirrorDSS states prior to rendering the reflected geometry.  As we mentioned previously, this does not render to the depth or back buffers, only the stencil buffer.

private void MarkMirrorOnStencil(EffectTechnique activeTech, Matrix viewProj, Color4 blendFactor) {
    // Draw mirror to stencil
    for (int p = 0; p < activeTech.Description.PassCount; p++) {
        var pass = activeTech.GetPassByIndex(p);

        ImmediateContext.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(_roomVB, Basic32.Stride, 0));

        var world = _roomWorld;
        var wit = MathF.InverseTranspose(world);
        var wvp = world * viewProj;

        Effects.BasicFX.SetWorld(world);
        Effects.BasicFX.SetWorldInvTranspose(wit);
        Effects.BasicFX.SetWorldViewProj(wvp);
        Effects.BasicFX.SetTexTransform(Matrix.Identity);

        ImmediateContext.OutputMerger.BlendState = RenderStates.NoRenderTargetWritesBS;
        ImmediateContext.OutputMerger.BlendFactor = blendFactor;
        ImmediateContext.OutputMerger.BlendSampleMask = -1;

        ImmediateContext.OutputMerger.DepthStencilState = RenderStates.MarkMirrorDSS;
        ImmediateContext.OutputMerger.DepthStencilReference = 1;

        pass.Apply(ImmediateContext);
        ImmediateContext.Draw(6, 24);
        ImmediateContext.OutputMerger.DepthStencilState = null;
        ImmediateContext.OutputMerger.DepthStencilReference = 0;
        ImmediateContext.OutputMerger.BlendState = null;
        ImmediateContext.OutputMerger.BlendFactor = blendFactor;
        ImmediateContext.OutputMerger.BlendSampleMask = -1;
    }
}

reflection-bad-lights

If you look closely at the above image, you’ll notice that the lighting is wrong; the original and reflected skull are lit in the same way, whereas the reflected skull should show the unlit back side of the original skull.  We can fix this by also reflecting the direction vectors of our directional lights when we draw the reflected geometry, using the Vector3.Transform method.  Note that we need to restore the original directions of our lights when we are finished drawing the reflection.  Also note that we must use a RasterizerState which reverses the normal winding order of our polygons, since the reflection transformation does not modify the face normals, and so they become inward facing. The full reflection method for the skull object is as follows:

private void DrawSkullReflection(EffectTechnique activeSkullTech, Matrix viewProj) {
    // Draw skull reflection

    for (int p = 0; p < activeSkullTech.Description.PassCount; p++) {
        var pass = activeSkullTech.GetPassByIndex(p);

        ImmediateContext.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(_skullVB, Basic32.Stride, 0));
        ImmediateContext.InputAssembler.SetIndexBuffer(_skullIB, Format.R32_UInt, 0);

        var mirrorPlane = new Plane(new Vector3(0, 0, 1), 0);
        var r = Matrix.Reflection(mirrorPlane);

        var world = _skullWorld * r;
        var wit = MathF.InverseTranspose(world);
        var wvp = world * viewProj;

        Effects.BasicFX.SetWorld(world);
        Effects.BasicFX.SetWorldInvTranspose(wit);
        Effects.BasicFX.SetWorldViewProj(wvp);
        Effects.BasicFX.SetMaterial(_skullMat);

        var oldLightDirections = _dirLights.Select(l => l.Direction).ToArray();

        for (int i = 0; i < _dirLights.Length; i++) {
            var l = _dirLights[i];
            var lightDir = l.Direction;
            var reflectedLightDir = Vector3.Transform(lightDir, r);
            _dirLights[i].Direction = new Vector3(reflectedLightDir.X, reflectedLightDir.Y, reflectedLightDir.Z);
        }
        Effects.BasicFX.SetDirLights(_dirLights);

        ImmediateContext.Rasterizer.State = RenderStates.CullClockwiseRS;

        ImmediateContext.OutputMerger.DepthStencilState = RenderStates.DrawReflectionDSS;
        ImmediateContext.OutputMerger.DepthStencilReference = 1;
        pass.Apply(ImmediateContext);

        ImmediateContext.DrawIndexed(_skullIndexCount, 0, 0);

        ImmediateContext.Rasterizer.State = null;
        ImmediateContext.OutputMerger.DepthStencilState = null;
        ImmediateContext.OutputMerger.DepthStencilReference = 0;

        for (int i = 0; i < oldLightDirections.Length; i++) {
            _dirLights[i].Direction = oldLightDirections[i];
        }
        Effects.BasicFX.SetDirLights(_dirLights);
    }
}

reflection-no-mirror-noshadows

Planar Shadows

To draw planar shadows, we need to define the plane that the shadow should be projected to, and the vector to our main directional light.  Then, we can use the SlimDX Matrix.Shadow method to create a matrix that will properly project our shadows.  Note that we augment our directional vector to a Vector4; we can use this shadow matrix for both directional and point lights, by setting the W component to either 0, for directional lights, or 1, for point lights.  For point lights, you would use the position of the light as the xyz portion of the Vector4, rather than the direction.  We draw our shadows by multiplying our object’s world matrix by the shadow matrix.  We also add a tiny offset translation, so that the shadow appears on top of the surface it is shadowing, and so hopefully avoid z-fighting.  Then we draw our shadowing object, using our TransparentBS BlendState and our NoDoubleBlendDSS DepthStencilState.  Note that we have to draw the shadow once for each plane that we require shadows on; in our case, we are casting shadows on the floor and the wall, so we have to draw the geometry twice.

private void DrawSkullShadow(EffectTechnique activeSkullTech, Matrix viewProj, Color4 blendFactor) {
    // draw skull shadow on floor
    for (int p = 0; p < activeSkullTech.Description.PassCount; p++) {
        var pass = activeSkullTech.GetPassByIndex(p);

        ImmediateContext.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(_skullVB, Basic32.Stride, 0));
        ImmediateContext.InputAssembler.SetIndexBuffer(_skullIB, Format.R32_UInt, 0);

        var shadowPlane = new Plane(new Vector3(0, 1, 0), 0.0f);
        var toMainLight = -_dirLights[0].Direction;

        var s = Matrix.Shadow(new Vector4(toMainLight, 0), shadowPlane);
        var shadowOffsetY = Matrix.Translation(0, 0.001f, 0);

        var world = _skullWorld * s * shadowOffsetY;
        var wit = MathF.InverseTranspose(world);
        var wvp = world * viewProj;

        Effects.BasicFX.SetWorld(world);
        Effects.BasicFX.SetWorldInvTranspose(wit);
        Effects.BasicFX.SetWorldViewProj(wvp);
        Effects.BasicFX.SetMaterial(_shadowMat);

        ImmediateContext.OutputMerger.BlendState = RenderStates.TransparentBS;
        ImmediateContext.OutputMerger.BlendFactor = blendFactor;
        ImmediateContext.OutputMerger.BlendSampleMask = -1;

        ImmediateContext.OutputMerger.DepthStencilState = RenderStates.NoDoubleBlendDSS;
        ImmediateContext.OutputMerger.DepthStencilReference = 0;
        pass.Apply(ImmediateContext);

        ImmediateContext.DrawIndexed(_skullIndexCount, 0, 0);

        // draw skull shadow on wall
        shadowPlane = new Plane(new Vector3(0, 0, -1), 0.0f);
        toMainLight = -_dirLights[0].Direction;

        s = Matrix.Shadow(new Vector4(toMainLight, 0), shadowPlane);
        shadowOffsetY = Matrix.Translation(0, 0, -0.001f);

        world = _skullWorld * s * shadowOffsetY;
        wit = MathF.InverseTranspose(world);
        wvp = world * viewProj;

        Effects.BasicFX.SetWorld(world);
        Effects.BasicFX.SetWorldInvTranspose(wit);
        Effects.BasicFX.SetWorldViewProj(wvp);
        Effects.BasicFX.SetMaterial(_shadowMat);

        pass.Apply(ImmediateContext);

        ImmediateContext.DrawIndexed(_skullIndexCount, 0, 0);

        ImmediateContext.Rasterizer.State = null;
        ImmediateContext.OutputMerger.DepthStencilState = null;
        ImmediateContext.OutputMerger.DepthStencilReference = 0;

        ImmediateContext.OutputMerger.BlendState = null;
        ImmediateContext.OutputMerger.BlendFactor = blendFactor;
        ImmediateContext.OutputMerger.BlendSampleMask = -1;
    }
}

shadow

Our last effect is to draw the reflection of the shadow in the mirror.  This requires us to combine the two techniques.  Fortunately, this is easier than it sounds, as the main step is simply to make sure that you multiply the matrices in the correct order.  We will want to multiply world * shadow * shadowOffset * reflection

private void DrawSkullShadowReflection(EffectTechnique activeSkullTech, Matrix viewProj, Color4 blendFactor) {
    // draw skull shadow
    for (int p = 0; p < activeSkullTech.Description.PassCount; p++) {
        var pass = activeSkullTech.GetPassByIndex(p);

        ImmediateContext.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(_skullVB, Basic32.Stride, 0));
        ImmediateContext.InputAssembler.SetIndexBuffer(_skullIB, Format.R32_UInt, 0);

        var shadowPlane = new Plane(new Vector3(0, 1, 0), 0.0f);
        var toMainLight = -_dirLights[0].Direction;

        var s = Matrix.Shadow(new Vector4(toMainLight, 0), shadowPlane);
        var shadowOffsetY = Matrix.Translation(0, 0.001f, 0);

        var mirrorPlane = new Plane(new Vector3(0, 0, 1), 0);
        var r = Matrix.Reflection(mirrorPlane);

        var world = _skullWorld * s * shadowOffsetY * r;
        var wit = MathF.InverseTranspose(world);
        var wvp = world * viewProj;

        Effects.BasicFX.SetWorld(world);
        Effects.BasicFX.SetWorldInvTranspose(wit);
        Effects.BasicFX.SetWorldViewProj(wvp);
        Effects.BasicFX.SetMaterial(_shadowMat);

        ImmediateContext.OutputMerger.BlendState = RenderStates.TransparentBS;
        ImmediateContext.OutputMerger.BlendFactor = blendFactor;
        ImmediateContext.OutputMerger.BlendSampleMask = -1;

        var oldLightDirections = _dirLights.Select(l => l.Direction).ToArray();

        for (int i = 0; i < _dirLights.Length; i++) {
            var l = _dirLights[i];
            var lightDir = l.Direction;
            var reflectedLightDir = Vector3.Transform(lightDir, r);
            _dirLights[i].Direction = new Vector3(reflectedLightDir.X, reflectedLightDir.Y, reflectedLightDir.Z);
        }
        Effects.BasicFX.SetDirLights(_dirLights);

        ImmediateContext.Rasterizer.State = RenderStates.CullClockwiseRS;

        ImmediateContext.OutputMerger.DepthStencilState = RenderStates.NoDoubleBlendDSS;
        ImmediateContext.OutputMerger.DepthStencilReference = 1;
        pass.Apply(ImmediateContext);

        ImmediateContext.DrawIndexed(_skullIndexCount, 0, 0);

        ImmediateContext.Rasterizer.State = null;
        ImmediateContext.OutputMerger.DepthStencilState = null;
        ImmediateContext.OutputMerger.DepthStencilReference = 0;

        ImmediateContext.OutputMerger.BlendState = null;
        ImmediateContext.OutputMerger.BlendFactor = blendFactor;
        ImmediateContext.OutputMerger.BlendSampleMask = -1;

        for (int i = 0; i < oldLightDirections.Length; i++) {
            _dirLights[i].Direction = oldLightDirections[i];
        }
        Effects.BasicFX.SetDirLights(_dirLights);
    }
}

reflect-shadows

Finally, we draw the mirror again, this time using alpha-blending and a texture to give a slightly more irregular look to the mirror.

private void DrawMirror(EffectTechnique activeTech, Matrix viewProj, Color4 blendFactor) {
    // draw mirror with transparency
    for (int p = 0; p < activeTech.Description.PassCount; p++) {
        var pass = activeTech.GetPassByIndex(p);

        ImmediateContext.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(_roomVB, Basic32.Stride, 0));

        var world = _roomWorld;
        var wit = MathF.InverseTranspose(world);
        var wvp = world * viewProj;

        Effects.BasicFX.SetWorld(world);
        Effects.BasicFX.SetWorldInvTranspose(wit);
        Effects.BasicFX.SetWorldViewProj(wvp);
        Effects.BasicFX.SetTexTransform(Matrix.Identity);
        Effects.BasicFX.SetMaterial(_mirrorMat);
        Effects.BasicFX.SetDiffuseMap(_mirrorDiffuseMapSRV);

        ImmediateContext.OutputMerger.BlendState = RenderStates.TransparentBS;
        ImmediateContext.OutputMerger.BlendFactor = blendFactor;
        ImmediateContext.OutputMerger.BlendSampleMask = -1;

        pass.Apply(ImmediateContext);
        ImmediateContext.Draw(6, 24);
    }
}

One last thing to note is that we clear the stencil buffer between rendering the mirror and rendering the skull shadows.  This is done so that we can draw shadows on the mirror; without the clear, the mirror portion of the wall has a stencil value of 1, from the MarkMirror function, and so our NoDoubleBlendDSS considers it to already be in shadow.

mirror-final

Next Time…

If you’re still with me, congratulations!  This was something of a doozy.  Unfortunately, the examples are going to continue to be very involved from here on out…  When I get around to writing up the next section, we’ll take a look at the Geometry Shader, and learn how we can use it to make billboards.


Bookshelf

Hi, I'm Eric, and I'm a biblioholic. Here is a selection of my favorites. All proceeds go to feed my addiction...