Search code examples
xamluwpwin-universal-apppixel-shaderwin2d

Support more than one color input in my Pixel Shader (UWP, Win2D)


I've been working on an app that can provide Color Replacement, and had a lot of help from @Jet Chopper on a solution. He's provided me the following code which essentially uses a ControlSpectrum control for Source and Target colors. The idea is you specify a Source Color which then gets replaced by a Target color. Here's the current working code:

This is my original post that contains the original solution with a GIF. Original Post

XAML:

<Grid>
    <xaml:CanvasAnimatedControl x:Name="AnimatedControl"
                            CreateResources="AnimatedControl_OnCreateResources"
                            Draw="AnimatedControl_OnDraw"/>

    <StackPanel HorizontalAlignment="Left" VerticalAlignment="Bottom">
        <TextBlock Text="Source Color" FontSize="32" Foreground="White" TextAlignment="Center"/>

        <ColorSpectrum Width="256" Height="256" ColorChanged="SourceColorSpectrum_OnColorChanged"/>
    </StackPanel>

    <StackPanel HorizontalAlignment="Right" VerticalAlignment="Bottom">
        <TextBlock Text="Replace Color" FontSize="32" Foreground="White" TextAlignment="Center"/>

        <ColorSpectrum Width="256" Height="256" ColorChanged="TargetColorSpectrum_OnColorChanged"/>
    </StackPanel>

    <Slider Width="512" ValueChanged="RangeBase_OnValueChanged" VerticalAlignment="Center"/>
</Grid>

CODE:

private PixelShaderEffect _textureShader;
private GaussianBlurEffect _blur;
private BlendEffect _blend;

private void AnimatedControl_OnCreateResources(CanvasAnimatedControl sender, CanvasCreateResourcesEventArgs args)
{
    args.TrackAsyncAction(CreateResourcesAsync(sender).AsAsyncAction());
}

private async Task CreateResourcesAsync(CanvasAnimatedControl sender)
{
    var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Shaders/TextureTest.bin"));
    var buffer = await FileIO.ReadBufferAsync(file);

    var sourceImage = await CanvasBitmap.LoadAsync(sender, new Uri("ms-appx:///Assets/image.jpg"));

    _textureShader = new PixelShaderEffect(buffer.ToArray())
    {
        Source1 = sourceImage
    };

    _blur = new GaussianBlurEffect
    {
        BlurAmount = 4,
        Source = _textureShader
    };

    _blend = new BlendEffect
    {
        Foreground = _blur,
        Background = sourceImage,
        Mode = BlendEffectMode.Color
    };
}

private void AnimatedControl_OnDraw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args)
{
    args.DrawingSession.DrawImage(_blend);
}

private void RangeBase_OnValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
    _textureShader.Properties["threshold"] = (float)e.NewValue / 100;
}

private void SourceColorSpectrum_OnColorChanged(ColorSpectrum sender, ColorChangedEventArgs args)
{
    _textureShader.Properties["sourceColor"] = new Vector3((float)args.NewColor.R / 255, (float)args.NewColor.G / 255, (float)args.NewColor.B / 255);
}

private void TargetColorSpectrum_OnColorChanged(ColorSpectrum sender, ColorChangedEventArgs args)
{
    _textureShader.Properties["replaceColor"] = new Vector3((float)args.NewColor.R / 255, (float)args.NewColor.G / 255, (float)args.NewColor.B / 255);
}

PIXEL SHADER:

#define D2D_INPUT_COUNT 1
#define D2D_INPUT0_SIMPLE

#include "d2d1effecthelpers.hlsli"

float3 sourceColor;
float3 replaceColor;
float threshold;

D2D_PS_ENTRY(main)
{
    float3 color = D2DGetInput(0).rgb;

    if (abs(color.r - sourceColor.r) < threshold && abs(color.g - sourceColor.g) < threshold && abs(color.b - sourceColor.b) < threshold) 
    {
        float3 newColor = color - sourceColor + replaceColor;
        return float4(newColor.r, newColor.g, newColor.b, 1);
    }
    else 
    {
        return float4(0, 0, 0, 0);
    }
}

So my next step was to take this solution one step forward and introduce more that one color replacement simulataneously. So I changed everything to support 2 colors as such:

MY CHANGES

XAML:

<Grid>
    <xaml:CanvasAnimatedControl x:Name="AnimatedControl"
                            CreateResources="AnimatedControl_OnCreateResources"
                            Draw="AnimatedControl_OnDraw"/>

    <StackPanel HorizontalAlignment="Left" VerticalAlignment="Bottom">
        <TextBlock Text="Source Color" FontSize="32" Foreground="White" TextAlignment="Center"/>

        <ColorSpectrum Width="256" Height="256" ColorChanged="SourceColorSpectrum_OnColorChanged"/>
        <ColorSpectrum Width="256" Height="256" ColorChanged="SourceColorSpectrum_OnColorChanged2"/>
    </StackPanel>

    <StackPanel HorizontalAlignment="Right" VerticalAlignment="Bottom">
        <TextBlock Text="Replace Color" FontSize="32" Foreground="White" TextAlignment="Center"/>

        <ColorSpectrum Width="256" Height="256" ColorChanged="TargetColorSpectrum_OnColorChanged"/>
        <ColorSpectrum Width="256" Height="256" ColorChanged="TargetColorSpectrum_OnColorChanged2"/>
    </StackPanel>

    <Slider Width="512" ValueChanged="RangeBase_OnValueChanged" VerticalAlignment="Center"/>
</Grid>

CODE:

private PixelShaderEffect _textureShader;
private GaussianBlurEffect _blur;
private BlendEffect _blend;

private void AnimatedControl_OnCreateResources(CanvasAnimatedControl sender, CanvasCreateResourcesEventArgs args)
{
    args.TrackAsyncAction(CreateResourcesAsync(sender).AsAsyncAction());
}

private async Task CreateResourcesAsync(CanvasAnimatedControl sender)
{
    var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Shaders/TextureTest.bin"));
    var buffer = await FileIO.ReadBufferAsync(file);

    var sourceImage = await CanvasBitmap.LoadAsync(sender, new Uri("ms-appx:///Assets/image.jpg"));

    _textureShader = new PixelShaderEffect(buffer.ToArray())
    {
        Source1 = sourceImage,
        Source2 = sourceImage
    };

    _blur = new GaussianBlurEffect
    {
        BlurAmount = 4,
        Source = _textureShader
    };

    _blend = new BlendEffect
    {
        Foreground = _blur,
        Background = sourceImage,
        Mode = BlendEffectMode.Color
    };
}

private void AnimatedControl_OnDraw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args)
{
    args.DrawingSession.DrawImage(_blend);
}

private void RangeBase_OnValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
    _textureShader.Properties["threshold"] = (float)e.NewValue / 100;
}

private void SourceColorSpectrum_OnColorChanged(ColorSpectrum sender, ColorChangedEventArgs args)
{
    _textureShader.Properties["sourceColor"] = new Vector3((float)args.NewColor.R / 255, (float)args.NewColor.G / 255, (float)args.NewColor.B / 255);
}

private void TargetColorSpectrum_OnColorChanged(ColorSpectrum sender, ColorChangedEventArgs args)
{
    _textureShader.Properties["replaceColor"] = new Vector3((float)args.NewColor.R / 255, (float)args.NewColor.G / 255, (float)args.NewColor.B / 255);
}

private void SourceColorSpectrum_OnColorChanged2(ColorSpectrum sender, ColorChangedEventArgs args)
{
    _textureShader.Properties["sourceColor2"] = new Vector3((float)args.NewColor.R / 255, (float)args.NewColor.G / 255, (float)args.NewColor.B / 255);
}

private void TargetColorSpectrum_OnColorChanged2(ColorSpectrum sender, ColorChangedEventArgs args)
{
    _textureShader.Properties["replaceColor2"] = new Vector3((float)args.NewColor.R / 255, (float)args.NewColor.G / 255, (float)args.NewColor.B / 255);
}

PIXEL SHADER:

#define D2D_INPUT_COUNT 2
#define D2D_INPUT0_SIMPLE
#define D2D_INPUT1_SIMPLE

#include "d2d1effecthelpers.hlsli"

float3 sourceColor;
float3 replaceColor;
float3 sourceColor2;
float3 replaceColor2;
float threshold;

D2D_PS_ENTRY(main)
{
    float3 color1 = D2DGetInput(0).rgb;
    float3 color2 = D2DGetInput(1).rgb;

    float4 result1;
    float4 result2;

    if (abs(color1.r - sourceColor.r) < threshold &&
        abs(color1.g - sourceColor.g) < threshold &&
        abs(color1.b - sourceColor.b) < threshold) 
    {
        float3 newColor = color1 - sourceColor + replaceColor;
        result1 = float4(newColor.r, newColor.g, newColor.b, 1);
    }
    else 
    {
        result1 = float4(0, 0, 0, 0);
    }

    if (abs(color2.r - sourceColor2.r) < threshold &&
        abs(color2.g - sourceColor2.g) < threshold &&
        abs(color2.b - sourceColor2.b) < threshold)
    {
        float3 newColor = color2 - sourceColor2 + replaceColor2;
        result2 = float4(newColor.r, newColor.g, newColor.b, 1);
    }
    else
    {
        result2 = float4(0, 0, 0, 0);
    }

    return result1 * result2;
}

So essentially I just doubled everything, in the XAML, the code, and the Pixel Shader. But for the Pixel Shader, I'm not sure if my return value is correct by multiplying both results. Am I on the right track for the ability to replace more than one color at once ?


Solution

  • Ok here's your sample with 2 input colors, 2 replace colors and 2 thresholds.

    XAML:

    <Grid>
        <xaml:CanvasAnimatedControl x:Name="AnimatedControl"
                                CreateResources="AnimatedControl_OnCreateResources"
                                Draw="AnimatedControl_OnDraw"/>
    
        <StackPanel HorizontalAlignment="Left" VerticalAlignment="Bottom">
            <TextBlock Text="Source Color 2" FontSize="16" Foreground="White" TextAlignment="Center"/>
    
            <ColorSpectrum Width="128" Height="128" ColorChanged="ColorSpectrum_OnColorChanged2"/>
        </StackPanel>
    
        <StackPanel HorizontalAlignment="Right" VerticalAlignment="Bottom">
            <TextBlock Text="Replace Color 2" FontSize="16" Foreground="White" TextAlignment="Center"/>
    
            <ColorSpectrum Width="128" Height="128" ColorChanged="ColorSpectrum_OnColorChanged3"/>
        </StackPanel>
    
        <StackPanel HorizontalAlignment="Left" VerticalAlignment="Top">
            <TextBlock Text="Source Color 1" FontSize="16" Foreground="White" TextAlignment="Center"/>
    
            <ColorSpectrum Width="128" Height="128" ColorChanged="ColorSpectrum_OnColorChanged"/>
        </StackPanel>
    
        <StackPanel HorizontalAlignment="Right" VerticalAlignment="Top">
            <TextBlock Text="Replace Color 1" FontSize="16" Foreground="White" TextAlignment="Center"/>
    
            <ColorSpectrum Width="128" Height="128" ColorChanged="ColorSpectrum_OnColorChanged1"/>
        </StackPanel>
    
        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <Slider Width="512" ValueChanged="RangeBase_OnValueChanged" VerticalAlignment="Center"/>
        <Slider Width="512" ValueChanged="RangeBase1_OnValueChanged" VerticalAlignment="Center"/>
        </StackPanel>
    </Grid>
    

    Code behind:

        private PixelShaderEffect _textureShader;
        private GaussianBlurEffect _blur;
        private BlendEffect _blend;
    
        public Ripple()
        {
            InitializeComponent();
        }
    
        private void AnimatedControl_OnCreateResources(CanvasAnimatedControl sender, CanvasCreateResourcesEventArgs args)
        {
            args.TrackAsyncAction(CreateResourcesAsync(sender).AsAsyncAction());
        }
    
        private async Task CreateResourcesAsync(CanvasAnimatedControl sender)
        {
            var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Shaders/TextureTest.bin"));
            var buffer = await FileIO.ReadBufferAsync(file);
    
            var sourceImage = await CanvasBitmap.LoadAsync(sender, new Uri("ms-appx:///Assets/image.jpg"));
    
            _textureShader = new PixelShaderEffect(buffer.ToArray())
            {
                Source1 = sourceImage
            };
    
            _blur = new GaussianBlurEffect
            {
                BlurAmount = 4,
                Source = _textureShader
            };
    
            _blend = new BlendEffect
            {
                Foreground = _blur,
                Background = sourceImage,
                Mode = BlendEffectMode.Color
            };
        }
    
        private void AnimatedControl_OnDraw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args)
        {
            args.DrawingSession.DrawImage(_blend);
        }
    
        private void RangeBase_OnValueChanged(object sender, RangeBaseValueChangedEventArgs e)
        {
            _textureShader.Properties["threshold"] = (float)e.NewValue / 100;
        }
    
        private void ColorSpectrum_OnColorChanged(ColorSpectrum sender, ColorChangedEventArgs args)
        {
            _textureShader.Properties["sourceColor"] = new Vector3((float)args.NewColor.R / 255, (float)args.NewColor.G / 255, (float)args.NewColor.B / 255);
        }
    
        private void ColorSpectrum_OnColorChanged1(ColorSpectrum sender, ColorChangedEventArgs args)
        {
            _textureShader.Properties["replaceColor"] = new Vector3((float)args.NewColor.R / 255, (float)args.NewColor.G / 255, (float)args.NewColor.B / 255);
        }
    
        private void RangeBase1_OnValueChanged(object sender, RangeBaseValueChangedEventArgs e)
        {
            _textureShader.Properties["threshold2"] = (float)e.NewValue / 100;
        }
    
        private void ColorSpectrum_OnColorChanged2(ColorSpectrum sender, ColorChangedEventArgs args)
        {
            _textureShader.Properties["sourceColor2"] = new Vector3((float)args.NewColor.R / 255, (float)args.NewColor.G / 255, (float)args.NewColor.B / 255);
        }
    
        private void ColorSpectrum_OnColorChanged3(ColorSpectrum sender, ColorChangedEventArgs args)
        {
            _textureShader.Properties["replaceColor2"] = new Vector3((float)args.NewColor.R / 255, (float)args.NewColor.G / 255, (float)args.NewColor.B / 255);
        }
    

    HLSL shader:

    #define D2D_INPUT_COUNT 1
    #define D2D_INPUT0_SIMPLE
    
    #include "d2d1effecthelpers.hlsli"
    
    float3 sourceColor;
    float3 replaceColor;
    float threshold;
    
    float3 sourceColor2;
    float3 replaceColor2;
    float threshold2;
    
    D2D_PS_ENTRY(main)
    {
        float3 color = D2DGetInput(0).rgb;
    
        if (abs(color.r - sourceColor.r) < threshold && abs(color.g - sourceColor.g) < threshold && abs(color.b - sourceColor.b) < threshold) 
        {
            float3 newColor = color - sourceColor + replaceColor;
            return float4(newColor.r, newColor.g, newColor.b, 1);
        }
        else if (abs(color.r - sourceColor2.r) < threshold2 && abs(color.g - sourceColor2.g) < threshold2 && abs(color.b - sourceColor2.b) < threshold2)
        {
            float3 newColor = color - sourceColor2 + replaceColor2;
            return float4(newColor.r, newColor.g, newColor.b, 1);
        }
        else 
        {
            return float4(0, 0, 0, 0);
        }
    }
    

    enter image description here

    Enjoy!