1

I'm trying to achieve a simple 3 color Angle Gradient in WPF. Best case scenario you got an XAML-ONLY answer, but i'm pretty sure this is impossible.

This Question & Answer (AngleGradient in WPF) is half of what i want. I tried to play with the code but I clearly don't get the math right.

enter image description here

My question is: how can I do exactly what is asked in the question above, but with 3 colors and a gradient between color 3 and color 1 (In the question above it goes with a gradient from blue to white, but straight back to blue after, i would like a reverse gradient from white to blue too).

Think of it as a Equilateral Triangle with a perfect horizontal base. I want for example Red at the top corner, Green at the left bottom corner and Blue at the bottom right corner, and a perfect angular radiant in between.

enter image description here

I don't mind having to compile into a .ps like in the answer suggested in the other thread :)

Thanks a lot !

Community
  • 1
  • 1
ericmas001
  • 603
  • 6
  • 12

1 Answers1

1

I think that other thread does a good job of explaining what you need to do. You will need to pass all three colors to the shader, and then do the math per-pixel to get the color you want.

Get the 0 - 1 angle of the current pixel the same way as the other answer. Then lerp between the appropriate colors at that angle.

Shader

sampler2D inputSampler : register(S0);
float2 center : register(C0);
float4 firstColor : register(C1);
float4 secondColor : register(C2);
float4 thirdColor : register(C3);

float4 main(float2 uv : TEXCOORD) : COLOR
{
    // Put the three colors into an array.
    float4 colors[3] = { firstColor, secondColor, thirdColor };

    // Figure out where this pixel is in relation to the center point
    float2 pos = center - uv;

    // Compute the angle of this pixel relative to the center (in radians),
    // then divide by 2 pi to normalize the angle into a 0 to 1 range.
    // We are flipping the Y here so that 0 is at the top (instead of the bottom) and we
    // rotate clockwise. Could also flip X if we want to rotate counter-clockwise.
    float value = (atan2(pos.x, -pos.y) + 3.141596) / (2.0 * 3.141596);

    // Scale the angle based on the size of our array and determine which indices
    // we are currently between, wrapping around to 0 at the end.
    float scaledValue = value * 3;
    float4 prevColor = colors[(int)scaledValue];
    float4 nextColor = colors[((int)scaledValue + 1) % 3];

    // Figure out how far between the two colors we are
    float lerpValue = scaledValue - (float)((int)scaledValue);

    // Get the alpha of the incoming pixel from the sampler.
    float alpha = tex2D(inputSampler, uv).a;

    // Lerp between the colors. Multiply each color by its own alpha and the result by the
    // incoming alpha becuse WPF expects shaders to return premultiplied alpha pixel values.
    return float4(
        lerp(prevColor.rgb * prevColor.a, nextColor.rgb * nextColor.a, lerpValue) * alpha,
        lerp(prevColor.a, nextColor.a, lerpValue) * alpha);
}

Effect

class AngleGradientEffect : ShaderEffect
{
    public Brush Input
    {
        get { return (Brush)GetValue(InputProperty); }
        set { SetValue(InputProperty, value); }
    }
    public static readonly DependencyProperty InputProperty = RegisterPixelShaderSamplerProperty("Input", typeof(AngleGradientEffect), 0);

    public Point Center
    {
        get { return (Point)GetValue(CenterProperty); }
        set { SetValue(CenterProperty, value); }
    }
    public static readonly DependencyProperty CenterProperty = DependencyProperty.Register("Center", typeof(Point), typeof(AngleGradientEffect),
        new PropertyMetadata(new Point(0.5, 0.5), PixelShaderConstantCallback(0)));

    public Color FirstColor
    {
        get { return (Color)GetValue(FirstColorProperty); }
        set { SetValue(FirstColorProperty, value); }
    }
    public static readonly DependencyProperty FirstColorProperty = DependencyProperty.Register("FirstColor", typeof(Color), typeof(AngleGradientEffect),
        new PropertyMetadata(Color.FromRgb(255, 0, 0), PixelShaderConstantCallback(1)));

    public Color SecondColor
    {
        get { return (Color)GetValue(SecondColorProperty); }
        set { SetValue(SecondColorProperty, value); }
    }
    public static readonly DependencyProperty SecondColorProperty = DependencyProperty.Register("SecondColor", typeof(Color), typeof(AngleGradientEffect),
        new PropertyMetadata(Color.FromRgb(0, 255, 0), PixelShaderConstantCallback(2)));

    public Color ThirdColor
    {
        get { return (Color)GetValue(ThirdColorProperty); }
        set { SetValue(ThirdColorProperty, value); }
    }
    public static readonly DependencyProperty ThirdColorProperty = DependencyProperty.Register("ThirdColor", typeof(Color), typeof(AngleGradientEffect),
        new PropertyMetadata(Color.FromRgb(0, 0, 255), PixelShaderConstantCallback(3)));

    public AngleGradientEffect()
    {
        // ResourceHelper is my own utility that formats URIs for me. The returned URI
        // string will be something like /AssemblyName;component/Effects/AngleGradient.ps
        PixelShader = new PixelShader() { UriSource = ResourceHelper.GetResourceUri("Effects/AngleGradient.ps", relative: true)};

        UpdateShaderValue(InputProperty);
        UpdateShaderValue(CenterProperty);
        UpdateShaderValue(FirstColorProperty);
        UpdateShaderValue(SecondColorProperty);
        UpdateShaderValue(ThirdColorProperty);
    }
}

Usage

<Ellipse
    Width="200"
    Height="200"
    Fill="White">
    <Ellipse.Effect>
        <effects:AngleGradientEffect
            FirstColor="Red"
            SecondColor="Lime"
            ThirdColor="Blue" />
    </Ellipse.Effect>
</Ellipse>

enter image description here

Keep in mind that doing an interpolation between different hues in RGB space will provide some ugly results in some cases. You might want to to look into converting to HSV and interpolating the hue if that is something you expect to be doing.

Xavier
  • 3,254
  • 15
  • 22
  • I have no knowledge about building the shader whatsoever, and I clearly don't understand the Shader part. But it works ! :) The only thing i'm missing is understanding how to rotate your solution (in the shader) so that I get the first color centerTop – ericmas001 Jan 24 '17 at 00:57
  • That is a simple change. Just invert the y component of the position in the shader when computing the angle to flip vertically. `float value = (atan2(pos.x, -pos.y) + 3.141596) / (2.0 * 3.141596);` You can also invert the x if you want to flip horizontally. – Xavier Jan 27 '17 at 22:11
  • I edited the shader in my answer to flip the Y axis, and also changed the alpha handling so that alpha values in the source are respected as well as in the passed in colors. I also added a bunch of code comments to try to explain what the shader is doing. – Xavier Jan 27 '17 at 23:03
  • Great explanation ! Thanks a lot, this is exactly what I needed, have a great day ! – ericmas001 Jan 28 '17 at 01:15