Monday, August 05, 2013 Eric Richards

Alpha-Blending Demo

Last time, we covered some of the theory that underlies blending and distance fog.  This time, we’ll go over the implementation of our demo that uses these effects, the BlendDemo.  This will be based off of our previous demo, the Textured Hills Demo, with an added box mesh and a wire fence texture applied to demonstrate pixel clipping using an alpha map.  We’ll need to update our Basic.fx shader code to add support for blending and clipping, as well as the fog effect, and we’ll need to define some new render states to define our blending operations.  You can find the full code for this example at https://github.com/ericrrichards/dx11.git under the BlendDemo project.

blendDemo2

Basic.fx

We will need to make a few changes to our shader effect to support alpha clipping and the fog effect.  First, we will need to define some new values for our per-frame constant buffer for the fog effect.  These will control the final fog color, and the zone in which the fog will have effect.

cbuffer cbPerFrame
{
    // previous constants omitted...

    float  gFogStart;
    float  gFogRange;
    float4 gFogColor;
};

Next, we’ll need to alter our pixel shader function to support alpha clipping, based on the alpha channel of the diffuse map texture, and add the calculation of the fog color.  We calculate the clip value prior to performing any lighting calculations, so that we can return early and save ourselves the cycles on the GPU if the pixel is going to be transparent.

float4 PS(VertexOut pin, uniform int gLightCount, uniform bool gUseTexure, uniform bool gAlphaClip, uniform bool gFogEnabled) : SV_Target
{
    // eye calculation omitted
    
    // Default to multiplicative identity.
    float4 texColor = float4(1, 1, 1, 1);
    if(gUseTexure)
    {
        // Sample texture.
        texColor = gDiffuseMap.Sample( samAnisotropic, pin.Tex );

        if(gAlphaClip)
        {
            // Discard pixel if texture alpha < 0.1.  Note that we do this
            // test as soon as possible so that we can potentially exit the shader 
            // early, thereby skipping the rest of the shader code.
            clip(texColor.a - 0.1f);
        }
    }
     
    // Lighting omitted...
        
    // Fogging
    if( gFogEnabled )
    {
        float fogLerp = saturate( (distToEye - gFogStart) / gFogRange ); 

        // Blend the fog color and the lit color.
        litColor = lerp(litColor, gFogColor, fogLerp);
    }

    // Common to take alpha from diffuse material and texture.
    litColor.a = gMaterial.Diffuse.a * texColor.a;

    return litColor;
}

We linearly interpolate between the lit, textured color and the fog color, based on the distance into the “fog zone” the pixel would be drawn in the scene.  Thus, a pixel closer than gFogStart to the camera will be shown with its normal color, while a pixel >= gFogStart + gFogRange would be shown as gFogColor.  A pixel in the middle of the fog zone would be shown with 50% original color and 50% fog color.

We will also add another batch of techniques, so that we have lit, lit & textured and lit, textured & alpha-clipped techniques with and without fog.

BasicEffect Class

In our C# wrapper class, we will define the necessary members to reference our new techniques and shader constants, along with some simple setter functions for the constants.

// new techinques
public readonly EffectTechnique Light1FogTech;
public readonly EffectTechnique Light2FogTech;
public readonly EffectTechnique Light3FogTech;

public readonly EffectTechnique Light0TexFogTech;
public readonly EffectTechnique Light1TexFogTech;
public readonly EffectTechnique Light2TexFogTech;
public readonly EffectTechnique Light3TexFogTech;

public readonly EffectTechnique Light0TexAlphaClipFogTech;
public readonly EffectTechnique Light1TexAlphaClipFogTech;
public readonly EffectTechnique Light2TexAlphaClipFogTech;
public readonly EffectTechnique Light3TexAlphaClipFogTech;

// new constants
private readonly EffectVectorVariable _fogColor;
private readonly EffectScalarVariable _fogStart;
private readonly EffectScalarVariable _fogRange;

// Initialization
public BasicEffect(Device device, string filename) : base(device, filename) {
    ///...
    Light1FogTech = FX.GetTechniqueByName("Light1Fog");
    Light2FogTech = FX.GetTechniqueByName("Light2Fog");
    Light3FogTech = FX.GetTechniqueByName("Light3Fog");

    Light0TexFogTech = FX.GetTechniqueByName("Light0TexFog");
    Light1TexFogTech = FX.GetTechniqueByName("Light1TexFog");
    Light2TexFogTech = FX.GetTechniqueByName("Light2TexFog");
    Light3TexFogTech = FX.GetTechniqueByName("Light3TexFog");

    Light0TexAlphaClipFogTech = FX.GetTechniqueByName("Light0TexAlphaClipFog");
    Light1TexAlphaClipFogTech = FX.GetTechniqueByName("Light1TexAlphaClipFog");
    Light2TexAlphaClipFogTech = FX.GetTechniqueByName("Light2TexAlphaClipFog");
    Light3TexAlphaClipFogTech = FX.GetTechniqueByName("Light3TexAlphaClipFog");

    _fogColor = FX.GetVariableByName("gFogColor").AsVector();
    _fogStart = FX.GetVariableByName("gFogStart").AsScalar();
    _fogRange = FX.GetVariableByName("gFogRange").AsScalar();
}

RenderStates Class

We are going to be using the same render states time and again in the ensuing series of demos, so we’ll build a static class to manage them for us, rather than creating them for each demo.  We’ll be including all of the Rasterizer, Blend and DepthStencil states that we develop as we go along, but for now, we only need a single RasterizerState, NoCullRS, and a single BlendState, TransparentBS. 

  • NoCullRS – This is a state that we will use for our transparent wire-fence textured “cage” mesh at the center of our scene.  It is very similar to the default render state, except that we have disabled front-face culling, so that we will see the back side of the far triangles through the transparent portions of the box.
  • TransparentBS – This blend state will blending the source and destination pixel based on their alphas, so that we can draw our water mesh partially transparent and see the ground mesh beneath it.

We will follow the same template with out RenderStates class as we have previously followed with our Effects and InputLayouts static classes.  We will implement an InitAll function, which will create the render states, which we will call after initializing Direct3D, and a DestroyAll function, which will release the COM pointers for the render states when we dispose of our application class.

public static class RenderStates {
    public static void InitAll(Device device) {
        Debug.Assert(device != null);
        var noCullDesc = new RasterizerStateDescription {
            FillMode = FillMode.Solid,
            CullMode = CullMode.None,
            IsFrontCounterclockwise = false,
            IsDepthClipEnabled = true
        };
        NoCullRS = RasterizerState.FromDescription(device, noCullDesc);

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

        TransparentBS = BlendState.FromDescription(device, transDesc);
    }
    public static void DestroyAll() {
        Util.ReleaseCom(ref NoCullRS);
        Util.ReleaseCom(ref TransparentBS);
    }
    public static RasterizerState NoCullRS;
    public static BlendState TransparentBS;
}

Demo Application

Our demo application will draw the scene from the screen shot above.  We will need to add the cage geometry to the scene, but that is done in exactly the same fashion as in the CrateDemo, so I will not go over it. We will modify our water mesh material so that it is partially transparent by setting the alpha component of its diffuse color to a value less than 1.0f.  After some tweaking, I found that I liked the effect with an alpha value of 0.6f, but go ahead and experiment with other values.

public BlendDemo(IntPtr hInstance){
    //...
    _wavesMat = new Material {
        Ambient = new Color4(0.5f, 0.5f, 0.5f),
        Diffuse = new Color4(0.6f, 1.0f, 1.0f, 1.0f),
        Specular = new Color4(32.0f, 0.8f, 0.8f, 0.8f)
    };
}

We will also add the option for the user to toggle between render modes using the keyboard number keys.  We will provide three options: 1, for rendering the scene with just lighting, 2, for rendering the scene with lights and textures, and 3, for rendering with lights, textures, and fog.  We do this by hooking our main application form’s KeyDown event, using a standard Windows.Forms event handler.  Note that we have to add this event handler in our Init() function, after we have called the base D3DApp Init() function to create the window and initialize Direct3D.

public override bool Init() {
    Window.KeyDown += SwitchRenderState;
}

private void SwitchRenderState(object sender, KeyEventArgs e) {
    switch (e.KeyCode) {
        case Keys.D1:
            _renderOptions = RenderOptions.Lighting;
            break;
        case Keys.D2:
            _renderOptions = RenderOptions.Textures;
            break;
        case Keys.D3:
            _renderOptions = RenderOptions.TexturesAndFog;
            break;
    }
}

Lastly, in our DrawScene function, we will need to select the appropriate effect technique for drawing our terrain and our box, render the box using the NoCullRS rasterizer state, render the land mesh, and then render the water mesh with the TransparentBS blend state.  Note that we have to draw our blended wave mesh last; generally, we will always need to render our non-blended objects first, and then render our blended objects in back-to-front order for the proper effect.  Note also that we can render our “cage” first, without worrying about draw order; this is because it is not actually blended, the pixels are either opaque or fully transparent, and we are using the alpha-clipping technique to simply not render those pixels that should be transparent.

public override void DrawScene() {
    // snip...

    var blendFactor = new Color4(0,0,0,0);

    var viewProj = _view * _proj;

    Effects.BasicFX.SetDirLights(_dirLights);
    Effects.BasicFX.SetEyePosW(_eyePosW);
    // set fog parameters
    Effects.BasicFX.SetFogColor(Color.Silver);
    Effects.BasicFX.SetFogStart(15.0f);
    Effects.BasicFX.SetFogRange(175.0f);
    // select appropriate rendering techniques
    EffectTechnique landAndWavesTech;
    EffectTechnique boxTech;
    switch (_renderOptions) {
        case RenderOptions.Lighting:
            boxTech = Effects.BasicFX.Light3Tech;
            landAndWavesTech = Effects.BasicFX.Light3Tech;
            break;
        case RenderOptions.Textures:
            boxTech = Effects.BasicFX.Light3TexAlphaClipTech;
            landAndWavesTech = Effects.BasicFX.Light3TexTech;
            break;
        case RenderOptions.TexturesAndFog:
            boxTech = Effects.BasicFX.Light3TexAlphaClipFogTech;
            landAndWavesTech = Effects.BasicFX.Light3TexFogTech;
            break;
        default:
            throw new ArgumentOutOfRangeException();
    }
    // draw the cage mesh
    for (int p = 0; p < boxTech.Description.PassCount; p++) {
        ImmediateContext.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(_boxVB, Basic32.Stride, 0));
        ImmediateContext.InputAssembler.SetIndexBuffer(_boxIB, Format.R32_UInt, 0);

        var world = _boxWorld;
        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(_boxMat);
        Effects.BasicFX.SetDiffuseMap(_boxMapSRV);

        ImmediateContext.Rasterizer.State = RenderStates.NoCullRS;
        boxTech.GetPassByIndex(p).Apply(ImmediateContext);
        ImmediateContext.DrawIndexed(36, 0, 0);

        ImmediateContext.Rasterizer.State = null;
    }


    for (int p = 0; p < landAndWavesTech.Description.PassCount; p++) {
        // Draw the land mesh
        ImmediateContext.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(_landVB, Basic32.Stride, 0));
        ImmediateContext.InputAssembler.SetIndexBuffer(_landIB, Format.R32_UInt, 0);

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

        Effects.BasicFX.SetWorld(world);
        Effects.BasicFX.SetWorldInvTranspose(wit);
        Effects.BasicFX.SetWorldViewProj(wvp);
        Effects.BasicFX.SetTexTransform(_grassTexTransform);
        Effects.BasicFX.SetMaterial(_landMat);
        Effects.BasicFX.SetDiffuseMap(_grassMapSRV);

        var pass = landAndWavesTech.GetPassByIndex(p);
        pass.Apply(ImmediateContext);
        ImmediateContext.DrawIndexed(_landIndexCount, 0, 0);
        // Draw the water mesh
        ImmediateContext.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(_waveVB, Basic32.Stride, 0));
        ImmediateContext.InputAssembler.SetIndexBuffer(_waveIB, Format.R32_UInt, 0);

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

        Effects.BasicFX.SetWorld(world);
        Effects.BasicFX.SetWorldInvTranspose(wit);
        Effects.BasicFX.SetWorldViewProj(wvp);
        Effects.BasicFX.SetTexTransform(_waterTexTransform);
        Effects.BasicFX.SetMaterial(_wavesMat);
        Effects.BasicFX.SetDiffuseMap(_wavesMapSRV);

        // enable alpha-blending
        ImmediateContext.OutputMerger.BlendState = RenderStates.TransparentBS;
        ImmediateContext.OutputMerger.BlendFactor = blendFactor;
        ImmediateContext.OutputMerger.BlendSampleMask = ~0;
        pass.Apply(ImmediateContext);
        ImmediateContext.DrawIndexed(3 * _waves.TriangleCount, 0, 0);
        // reset the blend state
        ImmediateContext.OutputMerger.BlendState = null;
    }
    SwapChain.Present(0, PresentFlags.None);
}

Result

blend-light-only blend-tex-nofog blend-tex-fog
Rendering with only lighting Rendering with textures and lighting Rendering with fog enabled

Next Time…

Next time around, we will be exploring Chapter 10 and the stencil buffer.  We’ll talk about some theory around the stencil buffer and then move onto implementing planar reflections and shadows.