Search code examples
c#xamlprotectedderived-class

How to derived from the Effect class


Textblock does not implement the stroke property, and is a sealed class. The most common work-around for this is to create your own textblock class from FrameworkElement. However, I've recently stumbled across the DropShadowEffect, and wondered if it was possible to use a custom effect to achieve the same outlined text result without the work of implementing the entire outlined text block. (I want a crisper outline that DropShadow will give me.)

To that end, I tried creating a class inheriting from Effect, but immediately ran into problems:

namespace MyNamespace;

public class OutlineEffect : Effect
{
    internal override Channel GetChannelCore(int index)
    {
        //How am I supposed to override an internal class in a Microsoft namespace?
    }

    //...
}

It does say in the documentation:

Derive from the Effect class to implement a custom bitmap effect. In most cases, you will derive from ShaderEffect

so I would assume this is possible. So, How do you derive from Effect?


Solution

  • You have to inherit from ShaderEffect instead of Effect. Here's an example for outlining text use an edge-detection filter effect:

    Combining this Shader tutorial and the Prewitt Edge Detection Filter I managed to get a decent outline effect around text. There are other ways to get a similar effect, like creating your own outline text block, but using an effect has the advantage of rendering using the GPU, and applying generically to any UIElement. However, I had to play a lot with the EdgeResponse property to get a nice outline.

    The end result in XAML:

    <Grid>
        <Grid.Resources>
            <local:EdgeDetectionEffect x:Key="OutlineEffect"
                x:Shared="false"
                EdgeResponse="4.0"
                ActualHeight="{Binding RelativeSource={RelativeSource AncestorType=TextBlock}, Path=ActualHeight}"
                ActualWidth="{Binding RelativeSource={RelativeSource AncestorType=TextBlock}, Path=ActualWidth}"/>
        </Grid.Resources>
        <TextBlock Text="The Crazy Brown Fox Jumped Over the Lazy Dog."
            FontWeight="Bold"
            Foreground="Yellow"
            Effect="{StaticResource OutlineEffect}"/>
    </Grid>
    

    To create the effect, I first created the EdgeDetectionColorEffect.fx (hdld) file - this is the code the GPU uses to filter the image. I compiled it in Visual Studio Command Prompt with the command:

    fxc /T ps_2_0 /E main /Focc.ps EdgeDetectionColorEffect.fx

    sampler2D Input : register(s0);
    float ActualWidth : register(c0);
    float ActualHeight : register(c1);
    float4 OutlineColor : register(c2);
    float EdgeDetectionResponse : register(c3);
    
    float4 GetNeighborPixel(float2 pixelPoint, float xOffset, float yOffset)
    {
        float2 NeighborPoint = {pixelPoint.x + xOffset, pixelPoint.y + yOffset};
        return tex2D(Input, NeighborPoint);
    }
    
    // pixel locations:
    // 00 01 02
    // 10 11 12
    // 20 21 22
    float main(float2 pixelPoint : TEXCOORD) : COLOR
    {
    
         float wo = 1 / ActualWidth; //WidthOffset
         float ho = 1 / ActualHeight; //HeightOffset
    
        float4 c00 = GetNeighborPixel(pixelPoint, -wo, -ho); // color of the pixel up and to the left of me.
        float4 c01 = GetNeighborPixel(pixelPoint,  00, -ho);        
        float4 c02 = GetNeighborPixel(pixelPoint,  wo, -ho);
        float4 c10 = GetNeighborPixel(pixelPoint, -wo,   0);
        float4 c11 = tex2D(Input, pixelPoint); // this is the current pixel
        float4 c12 = GetNeighborPixel(pixelPoint,  wo,   0);
        float4 c20 = GetNeighborPixel(pixelPoint, -wo,  ho);
        float4 c21 = GetNeighborPixel(pixelPoint,   0,  ho);
        float4 c22 = GetNeighborPixel(pixelPoint,  wo,  ho);
    
        float t00 = c00.r + c00.g + c00.b; //total of color channels
        float t01 = c01.r + c01.g + c01.b;
        float t02 = c02.r + c02.g + c02.b;
        float t10 = c10.r + c10.g + c10.b;
        float t11 = c11.r + c11.g + c11.b;
        float t12 = c12.r + c12.g + c12.b;
        float t20 = c20.r + c20.g + c20.b;
        float t21 = c21.r + c21.g + c21.b;
        float t22 = c22.r + c22.g + c22.b;
    
        //Prewitt - convolve the 9 pixels with:
        //       01 01 01        01 00 -1
        // Gy =  00 00 00   Gx = 01 00 -1
        //       -1 -1 -1        01 00 -1
    
        float gy = 0.0;  float gx = 0.0;
        gy += t00;       gx += t00;
        gy += t01;       gx += t10;
        gy += t02;       gx += t20;
        gy -= t20;       gx -= t02;
        gy -= t21;       gx -= t12;
        gy -= t22;       gx -= t22;
    
        if((gy*gy + gx*gx) > EdgeDetectionResponse)
        {
            return OutlineColor;
        }
    
        return c11;
    }
    

    Here's the wpf effect class:

    public class EdgeDetectionEffect : ShaderEffect
    {
        private static PixelShader _shader = new PixelShader { UriSource = new Uri("path to your compiled shader probably called cc.ps", UriKind.Absolute) };
    
    public EdgeDetectionEffect()
    {
        PixelShader = _shader;
        UpdateShaderValue(InputProperty);
        UpdateShaderValue(ActualHeightProperty);
        UpdateShaderValue(ActualWidthProperty);
        UpdateShaderValue(OutlineColorProperty);
        UpdateShaderValue(EdgeResponseProperty);
    }
    
    public Brush Input
    {
         get => (Brush)GetValue(InputProperty);
         set => SetValue(InputProperty, value);
    }
    public static readonly DependencyProperty InputProperty = 
        ShaderEffect.RegisterPixelShaderSamplerProperty(nameof(Input), 
        typeof(EdgeDetectionEffect), 0);
    
    public double ActualWidth
    {
         get => (double)GetValue(ActualWidthProperty);
         set => SetValue(ActualWidthProperty, value);
    }
    public static readonly DependencyProperty ActualWidthProperty =
        DependencyProperty.Register(nameof(ActualWidth), typeof(double), typeof(EdgeDetectionEffect),
        new UIPropertyMetadata(1.0, PixelShaderConstantCallback(0)));
    
    //notice the PixelShaderConstantCallback(#) - this tells it which GPU register to use (compare the number here to the first few lines of the EdgeDetectionColorEffect.fx file above.
    
    public double ActualHeight
    {
         get => (double)GetValue(ActualHeightProperty);
         set => SetValue(ActualHeightProperty, value);
    }
    public static readonly DependencyProperty ActualHeightProperty =
        DependencyProperty.Register(nameof(ActualHeight), typeof(double), typeof(EdgeDetectionEffect),
        new UIPropertyMetadata(1.0, PixelShaderConstantCallback(1)));
    
    public Color OutlineColor
    {
         get => (Color)GetValue(OutlineColorProperty);
         set => SetValue(OutlineColorProperty, value);
    }
    public static readonly DependencyProperty OutlineColorProperty=
        DependencyProperty.Register(nameof(OutlineColor), typeof(Color), typeof(EdgeDetectionEffect),
        new UIPropertyMetadata(Colors.Black, PixelShaderConstantCallback(2)));
    
    public double EdgeResponse
    {
         get => (double)GetValue(EdgeResponseProperty);
         set => SetValue(EdgeResponseProperty, value);
    }
    public static readonly DependencyProperty EdgeResponseProperty =
        DependencyProperty.Register(nameof(EdgeResponse), typeof(double), typeof(EdgeDetectionEffect),
        new UIPropertyMetadata(4.0, PixelShaderConstantCallback(3)));