NoesisGUI

Extending with Shaders

There are two ways where custom shaders can be used in NoesisGUI. Effects apply a post-processing shader to any object including children. On the other hand, Brushes can be customized to define how elements are filled and stroked. The following sections describe both approaches in more detail.

Shader Effects

NoesisGUI has two special visual effects built in that can be applied to any Visual. These effects are DropShadowEffect and BlurEffect, which both derive from the abstract Effect class.

To apply an effect to a relevant object, you simply sets its Effect property to an instance of one of the Effect-derived classes. For example:

<Grid
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Image Width="300" Height="300" Source="Images/image.jpg">
        <Image.Effect>
          <BlurEffect Radius="20"/>
        </Image.Effect>
    </Image>
</Grid>

The exciting part is not necessarily the two built-in ones but a third Effect subclass called ShaderEffect that enables you to easily inject your own custom effects. By deriving from the abstract ShaderEffect class, you can apply any pixel shader to any UIElement of the tree.

There are many steps necessary to create a working shader effect class. To ease the development of custom shaders, the Application Framework provides the following effects, we recommend inspecting the source code of these effects while reading this tutorial:

ShadersTutorialImg1.jpg

Creating Custom Effects

ShaderEffect is the abstract class that serves as a base for your custom effect class. It is a dependency object, so you can populate it with dependency properties to define the parameters of your effect. It works in conjunction with a pixel shader that must be compatible with the active RenderDevice. For D3D11 and D3D12 renderers the shader must be written in HLSL. As part of the application framework, RenderContext exposes a function called CreatePixelShader to create a pixel shader.

For example, the PixelateEffect uses the following HLSL shader:

#include "HLSLHelpers.hlsli"

cbuffer Constants: register(b1)
{
    float size;
}

float4 main(in In i) : SV_TARGET
{
    float2 pos = GetImagePosition(i);

    if (floor(pos.y / size) % 2.0 >= 1.0)
    {
        pos.x += size / 2.0f;
    }

    float2 brickNum = floor(pos / size);
    float2 centerOfBrick = brickNum * size + size / 2;
    return SampleInputAtPosition(i, centerOfBrick) * GetOpacity(i);
}

The following inputs are exposed to the pixel shader in form of functions by using the HLSLHelpers.hlsli include:

  • GetInputCoordinate(), returns the current input coordinate. As the effect may be generated inside an texture atlas, shaders shouldn't take any dependencies on how this value is calculated. It should use it only to the pixel shader's input. For rest of cases GetNormalizedInputCoordinate is recommended.
  • GetNormalizedInputCoordinate(), returns the current normalized input coordinates in the range 0 to 1.
  • GetImagePosition(), returns the current image position in pixels.
  • GetOpacity(), returns the current opacity.
  • GetInput(), returns the color at the current input coordinates.
  • SampleInput(), samples input at position uv.
  • SampleInputAtOffset(), samples input at an offset in pixels from the input coordinate.
  • SampleInputAtPosition(), samples input at an absolute scene position in pixels.

Once the pixel shader is loaded it must be stored in the ShaderEffect instance calling SetPixelShader. Note that only one shader must be loaded for all instances of the same effect. Parametrized effects use a constant buffer to send parameters to the pixel shader. The constant buffer is set in the constructor of the effect by calling SetConstantBuffer and every time a parameter changes the constant buffer must be updated with the function InvalidateConstantBuffer.

C++
PixelateEffect::PixelateEffect()
{
    if (Shader == nullptr)
    {
        ShaderSource shader = { "Noesis_Pixelate", Shader::Custom_Effect };
        shader.hlsl = Pixelate_PS;
        Shader = RenderContext::Current()->CreatePixelShader(shader);
    }

    SetPixelShader(Shader);
    SetConstantBuffer(&mConstants, sizeof(mConstants));
}
C++
NS_IMPLEMENT_REFLECTION(PixelateEffect, "NoesisGUIExtensions.PixelateEffect")
{
    auto OnBrickSizeChanged = [](DependencyObject* o, const DependencyPropertyChangedEventArgs& args)
    {
        PixelateEffect* this_ = (PixelateEffect*)o;
        this_->mConstants.size = args.NewValue<float>();
        this_->InvalidateConstantBuffer();
    };

    UIElementData* data = NsMeta<UIElementData>(TypeOf<SelfClass>());
    data->RegisterProperty<float>(SizeProperty, "Size", UIPropertyMetadata::Create(
        5.0f, PropertyChangedCallback(OnBrickSizeChanged)));
}

Normally, an effect is applied to an element's actual render size. Therefore an effect for a 200×200 Image will modify pixels in a 200×200 region. Certain effects, like the drop shadow, need additional space outside the normal render area. Use the ShaderEffect SetPadding function to increase the size required in the pixel shader.

Shader Brushes

Apart from Effects, NoesisGUI provides functionality that allows ImageBrush to be extended by using pixel shaders. By deriving from the abstract BrushShader class, you can generate procedural pattern brushes driven by a pixel shader.

In comparison with ShaderEffect, a brush shader is more efficient because it doesn't need an extra render pass to an offscreen. But on the other hand, it cannot be applied to a tree of elements.

To apply a shader brush to an ImageBrush, you set its Brush.Shader property. Note that this is an Extension to WPF provided by NoesisGUI.

<Grid
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:noesis="clr-namespace:NoesisGUIExtensions;assembly=Noesis.GUI.Extensions"
  xmlns:local="clr-namespace:CustomBrushes">

  <Rectangle Width="300" Height="300">
    <Rectangle.Fill>
      <ImageBrush ImageSource="Images/image.jpg">
        <noesis:Brush.Shader>
          <local:WaveBrushShader Frequency="0.7" />
        </noesis:Brush.Shader>
      </ImageBrush>
    </Rectangle.Fill>
  </Rectangle>
</Grid>

The Application Framework provides the following shader brushes as examples:

ShadersTutorialImg2.jpg

Creating Shader Brushes

BrushShader is the abstract class that serves as a base for your custom brush. It is a dependency object, so you can populate it with dependency properties to configure your shader. Same as in ShaderEffect, this class works in conjunction with a pixel shader that must be compatible with the active RenderDevice.

For example, the MonochromeBrushShader uses the following HLSL shader:

#define PAINT_PATTERN 1
#define CUSTOM_PATTERN 1
#include "../../../Render/D3D11RenderDevice/Src/ShaderPS.hlsl"

cbuffer Constants: register(b1)
{
    float4 color;
}

half4 GetCustomPattern(in In i)
{
    float4 c = pattern.Sample(patternSampler, i.uv0);
    float l = c.r * 0.30 + c.g * 0.59 + c.b * 0.11;
    return float4(color.r * l, color.g * l, color.b * l, color.a);
}

In contrast with ShaderEffect, the only inputs for the pixel shader are the texture corresponding to the ImageBrush and its UV coordinates. Note that the texture may be null. Also note that you need to compile a shader variant for each destination where the brush is being applied. The target is indicated in the SetPixelShader function. For example, different shader variants are needed if the brush is applied to a text or a path.

Once the pixel shader is loaded it must be stored in the BrushShader instance calling SetPixelShader. Note that only one shader must be loaded for all instances of the same effect. Parametrized effects use a constant buffer to send parameters to the pixel shader. The constant buffer is set in the constructor of the shader brush by calling SetConstantBuffer and every time a parameter changes the constant buffer must be updated with the function InvalidateConstantBuffer.

C++
MonochromeBrushShader::MonochromeBrushShader()
{
    if (Shaders[0] == nullptr)
    {
        ShaderSource shaders[] =
        {
            { "Noesis_Monochrome_Path", Shader::Path_Pattern },
            { "Noesis_Monochrome_Path_AA", Shader::Path_AA_Pattern },
            { "Noesis_Monochrome_SDF", Shader::SDF_Pattern },
            { "Noesis_Monochrome_SDF_LCD", Shader::SDF_LCD_Pattern },
            { "Noesis_Monochrome_Opacity", Shader::Opacity_Pattern }
        };

        shaders[0].hlsl = Monochrome_Path_PS;
        shaders[1].hlsl = Monochrome_PathAA_PS;
        shaders[2].hlsl = Monochrome_SDF_PS;
        shaders[3].hlsl = Monochrome_SDF_LCD_PS;
        shaders[4].hlsl = Monochrome_Opacity_PS;

        for (uint32_t i = 0; i < NS_COUNTOF(Shaders); i++)
        {
            Shaders[i] = RenderContext::Current()->CreatePixelShader(shaders[i]);
        }
    }

    mConstants.color = Color::White();
    SetConstantBuffer(&mConstants, sizeof(mConstants));

    for (uint32_t i = 0; i < NS_COUNTOF(Shaders); i++)
    {
        SetPixelShader(Shaders[i], (BrushShader::Target)i);
    }
}
C++
NS_IMPLEMENT_REFLECTION(MonochromeBrushShader, "NoesisGUIExtensions.MonochromeBrushShader")
{
    auto OnColorChanged = [](DependencyObject* o, const DependencyPropertyChangedEventArgs& args)
    {
        MonochromeBrushShader* this_ = (MonochromeBrushShader*)o;
        this_->mConstants.color = args.NewValue<Color>();
        this_->InvalidateConstantBuffer();
    };

    UIElementData* data = NsMeta<UIElementData>(TypeOf<SelfClass>());
    data->RegisterProperty<Color>(ColorProperty, "Color",
        UIPropertyMetadata::Create(Color::White(), PropertyChangedCallback(OnColorChanged)));
}
© 2017 Noesis Technologies