💡 Extending with Shaders
There are two ways to use custom shaders in NoesisGUI. Effects apply a post-processing shader to any object, including its children. On the other hand, Brushes can be customized with a shader to define how elements are filled and stroked. The following sections describe both approaches in more detail.
In both cases, a pixel shader compatible with the active RenderDevice is required. Our Application Framework provides a ShaderCompiler tool that is used in C++ and Unity. For Unreal, the native Shader Graph editor must be used.
Shader Effects
NoesisGUI has two special built-in visual effects 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 set 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 effects, 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 in 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:
- ChromaticAberrationEffect
- DirectionalBlurEffect
- NoiseEffect
- PinchEffect
- PixelateEffect
- SaturationEffect
- TintEffect
- VignetteEffect
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. As described above, it works in conjunction with a pixel shader.
For example, the PixelateEffect uses the following shader:
#include "EffectHelpers.h"
uniforms
{
float _size;
};
fixed4 main_effect()
{
float2 pos = GetImagePosition();
if (mod(floor(pos.y / _size), 2.0) >= 1.0)
{
pos.x += _size / 2.0f;
}
float2 brickNum = floor(pos / _size);
float2 centerOfBrick = brickNum * _size + _size / 2.0;
return SampleInputAtPosition(centerOfBrick);
}
The following inputs are exposed to the pixel shader in the form of functions by using the EffectHelpers.h include:
- GetInputCoordinate(), returns the current input coordinate. As the effect may be generated inside a texture atlas, shaders shouldn't take any dependencies on how this value is calculated. It should only be used to sample the pixel shader's input. For all other cases, GetNormalizedInputCoordinate is recommended.
- GetNormalizedInputCoordinate(), returns the current normalized input coordinates in the range of 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 the input at position uv.
- SampleInputAtOffset(), samples the input at an offset in pixels from the input coordinate.
- SampleInputAtPosition(), samples the input at an absolute scene position in pixels.
Once the pixel shader is loaded, it must be stored in the ShaderEffect instance by calling SetPixelShader. Because the shader is shared by all instances of the same effect, it only needs to be loaded once. Parameterized 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.
PixelateEffect::PixelateEffect()
{
RenderContext::EnsureShaders(Shaders, "Pixelate", Pixelate_bin);
SetPixelShader(Shaders.shaders[0]);
SetConstantBuffer(&mConstants, sizeof(mConstants));
}
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)));
}
An effect is normally 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 an 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.
Compared to a ShaderEffect, a brush shader is more efficient because it doesn't need an extra render pass to an offscreen surface. On the other hand, it cannot be applied to an entire 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:WaveBrush Frequency="0.7" />
</noesis:Brush.Shader>
</ImageBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
The Application Framework provides the following shader brushes as examples:
Creating Custom 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. As with ShaderEffect, this class works in conjunction with a pixel shader.
For example, the TintBrush uses the following shader:
#include "BrushHelpers.h"
uniforms
{
float _color_r, _color_g, _color_b, _color_a;
float _intensity;
};
fixed4 main_brush(float2 uv)
{
fixed4 color = make_fixed4(_color_r, _color_g, _color_b, _color_a);
fixed intensity = make_fixed(_intensity);
fixed4 c = SampleImage(uv);
return lerp(c, c * color, intensity);
}
Unlike ShaderEffect, here we use 'BrushHelpers.h' to access a different set of helper functions. For example, SampleImage() is used to sample the texture corresponding to the ImageBrush.
Once the pixel shader is loaded, it must be stored in the BrushShader instance by calling SetPixelShader for each target. For example, different shader variants are needed if the brush is applied to text or a path. Because the shader is shared by all instances of the same brush, it only needs to be loaded once. Parameterized brushes 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.
TintBrush::TintBrush()
{
RenderContext::EnsureShaders(Shaders, "Tint", Tint_noesisbrush);
SetConstantBuffer(&mConstants, sizeof(mConstants));
for (uint32_t i = 0; i < NS_COUNTOF(Shaders.shaders); i++)
{
SetPixelShader(Shaders.shaders[i], (BrushShader::Target)i);
}
}
NS_IMPLEMENT_REFLECTION(TintBrush, "NoesisGUIExtensions.TintBrush")
{
auto OnColorChanged = [](DependencyObject* o, const DependencyPropertyChangedEventArgs& args)
{
TintBrush* this_ = DynamicCast<TintBrush*>(o);
if (this_ != nullptr)
{
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)));
}