WPF WriteableBitmap and effects - wpf

Im trying to look into using the WPF WriteableBitmap class to allow my application to apply an opacity mask to an image.
Basically I have a blue rectangle as an image, and another 100% transparent green rectangle image over the top of the blue one.
When the user moves their mouse over the green (transparent) image, I want to apply the opacity mask (perhaps using a simple ellipse) so that it looks like a green glow is occurring.
Im purposefully not doing this is XAML and standard WPF effects because I really need it to be super performant and I will eventually swap out the ellipse with a more advance blob...
Any thoughts??
Thanks!

I'm sorry, I don't quite understand your intentions. Maybe if I could see the image, I could answer correctly from start, but here is my first-maybe-wrong answer.
If you say super-performant, you probably want to look at pixel shaders. They are processed by GPU, supported by WPF in a form of a custom effect and are easy to implement. Also you can apply shaders to playing video, while it's hard to do with WritableBitmap.
To write a pixel shader, you need to have FX Compiler (fxc.exe) from DirectX SDK and Shazzam tool - WYSIWYG WPF Shaders compiler by Walt Ritscher.
When you get them both, go ahead and try the following HLSL code
float X : register(C0); // Mouse cursor X position
float Y : register(C1); // Mouse cursor Y position
float4 Color : register(C2); // Mask color
float R : register(C3); // Sensitive circle radius.
sampler2D implicitInputSampler : register(S0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
float4 finalColor = tex2D(implicitInputSampler, uv);
if ( (uv.x - X) * (uv.x - X) + (uv.y - Y) * (uv.y - Y) < R*R)
{
finalColor = Color; // Blend/Change/Mask it as you wish here.
}
return finalColor;
}
This gives you the following C# effect:
namespace Shazzam.Shaders {
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;
public class AutoGenShaderEffect : ShaderEffect {
public static DependencyProperty InputProperty = ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(AutoGenShaderEffect), 0);
public static DependencyProperty XProperty = DependencyProperty.Register("X", typeof(double), typeof(AutoGenShaderEffect), new System.Windows.UIPropertyMetadata(new double(), PixelShaderConstantCallback(0)));
public static DependencyProperty YProperty = DependencyProperty.Register("Y", typeof(double), typeof(AutoGenShaderEffect), new System.Windows.UIPropertyMetadata(new double(), PixelShaderConstantCallback(1)));
public static DependencyProperty ColorProperty = DependencyProperty.Register("Color", typeof(System.Windows.Media.Color), typeof(AutoGenShaderEffect), new System.Windows.UIPropertyMetadata(new System.Windows.Media.Color(), PixelShaderConstantCallback(2)));
public static DependencyProperty RProperty = DependencyProperty.Register("R", typeof(double), typeof(AutoGenShaderEffect), new System.Windows.UIPropertyMetadata(new double(), PixelShaderConstantCallback(3)));
public AutoGenShaderEffect(PixelShader shader) {
// Note: for your project you must decide how to use the generated ShaderEffect class (Choose A or B below).
// A: Comment out the following line if you are not passing in the shader and remove the shader parameter from the constructor
PixelShader = shader;
// B: Uncomment the following two lines - which load the *.ps file
// Uri u = new Uri(#"pack://application:,,,/glow.ps");
// PixelShader = new PixelShader() { UriSource = u };
// Must initialize each DependencyProperty that's affliated with a shader register
// Ensures the shader initializes to the proper default value.
this.UpdateShaderValue(InputProperty);
this.UpdateShaderValue(XProperty);
this.UpdateShaderValue(YProperty);
this.UpdateShaderValue(ColorProperty);
this.UpdateShaderValue(RProperty);
}
public virtual System.Windows.Media.Brush Input {
get {
return ((System.Windows.Media.Brush)(GetValue(InputProperty)));
}
set {
SetValue(InputProperty, value);
}
}
public virtual double X {
get {
return ((double)(GetValue(XProperty)));
}
set {
SetValue(XProperty, value);
}
}
public virtual double Y {
get {
return ((double)(GetValue(YProperty)));
}
set {
SetValue(YProperty, value);
}
}
public virtual System.Windows.Media.Color Color {
get {
return ((System.Windows.Media.Color)(GetValue(ColorProperty)));
}
set {
SetValue(ColorProperty, value);
}
}
public virtual double R {
get {
return ((double)(GetValue(RProperty)));
}
set {
SetValue(RProperty, value);
}
}
}
}
Now you can track mouse position, and set corresponding properties of your effect to trigger changes. One thing to note here: X and Y in HLSL code are ranged from 0 to 1. So you'll have to convert actual coordinates to percentages, before passing them to shader.
Things to read more about pixel shaders and WPF:
Reference for HLSL on MSDN.
Writing custom GPU-based Effects for WPF by Greg Schechter.
Hope this helps :)

Related

Current best practice for compiling pixel shaders in WPF?

I'd like to color some icons in a WPF application using a pixel shader. However, when I go to get the DirectX SDK to get fxc, I see that it's deprecated -- I find links to DirectX11 and the new "Effects" system in WPF. However, that seems to be deprecated as well.
I'm wondering: what is the current practice to get a pixel shader into WPF? For context (though this shouldn't matter technically), I'm using a Prism/Unity-based MVVM architecture, so I'd like to eventually handle these colors through XAML binding.
I see alot of people using SlimDX, but I'd really like to avoid introducing yet another library dependency into my application.
The DirectX SDK was merged into the Windows SDK back when Windows 8 was released [source], which is where you can find the newer versions of FXC.
Took me a few hours to get find a solution so i wanted to share it:
Visual Studio:
Add below batch script to Project(right click Project in the Solution Explorer Window)>Properties>BuildEvents>"Pre-build event command line:"-field) and adapt the path.
echo COMPILING SHADERS:
set Compiler=C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x86\fxc.exe
set OutputFile=$(ProjectDir)Resources\SearchColorFilter.ps
set InputFile=$(ProjectDir)Resources\SearchColorFilter.fx
"%Compiler%" /O0 /Fc /Zi /T ps_2_0 /Fo "%OutputFile%" "%InputFile%"
here is an Example of a pixel shader, it replaces magenta-like colors with white, keeps yellow and greys out the rest
SearchColorFilter.ps:
sampler2D implicitInputSampler : register(S0);
float filterControl : register(C0);
float4 main(float2 uv : TEXCOORD) : COLOR{
float4 color = tex2D(implicitInputSampler, uv);
if (filterControl == 1) {
//modify magenta tint into white, Magenta: float4(1,0,1,1)
if (color.r > 0.35 && color.g < 0.2 && color.b > 0.35) return float4(color.r, color.r * 0.5 + color.b * 0.5, color.b, 1);//MAGENTA-->WHITE
//keep yellow, Yellow: float4(1,0,1,1)
if (color.r > 0.35 && color.g > 0.35 && color.b < 0.2) return color;//YELLOW-->YELLOW
//grey out rest
return float4(color.r * 0.5, color.g * 0.5, color.b * 0.5, color.a);//OTHER -> HALF VALUE (= grey out)
}
return color;
}
Don't forget to:
add the created .ps-file to your project via the Solution Explorer and set its Build Action to "Resource"
save the .fx file with correct Encoding: "US-ASCII - Codepage 20127"
BONUS:
In order to apply a shader to an UIElement, this can be done by creating a class
SearchColorFilter.cs:
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;
public class SearchColorFilter : ShaderEffect
{
static SearchColorFilter()
{
var uri = MakePackUri("Resources/SearchColorFilter.ps");
_pixelShader.UriSource = uri;
}
private static PixelShader _pixelShader = new PixelShader();
public SearchColorFilter()
{
this.PixelShader = _pixelShader;
UpdateShaderValue(InputProperty);
UpdateShaderValue(FilterControlProperty);
}
public Brush Input
{
get { return (Brush)GetValue(InputProperty); }
set { SetValue(InputProperty, value); }
}
public static readonly System.Windows.DependencyProperty InputProperty =
ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(SearchColorFilter), 0);
public double FilterControl
{
get { return (double)GetValue(FilterControlProperty); }
set { SetValue(FilterControlProperty, value); }
}
public static System.Uri MakePackUri(string relativeFile)
{
System.Reflection.Assembly a = typeof(SearchColorFilter).Assembly;
string assemblyShortName = a.ToString().Split(',')[0];
string uriString = "pack://application:,,,/" + assemblyShortName + ";component/" + relativeFile;
return new System.Uri(uriString);
}
public static readonly DependencyProperty FilterControlProperty =
DependencyProperty.Register("FilterControl", typeof(double), typeof(SearchColorFilter),
new UIPropertyMetadata(0.0d, PixelShaderConstantCallback(0)));
}
Now you can assign the shader to any UIElement using
SearchColorFilter recolorShader = new SearchColorFilter();
recolorShader.FilterControl = 1;
MyUIElement.Effect = recolorShader;

Custom WPF ShaderEffect alpha channel has unusual behavior

I've got a fairly straightforward pixel shader—set alpha channel to zero and return.
sampler2D tex : register(s0);
float4 PS(float2 uv : TEXCOORD) : COLOR
{
float4 color = tex2D(tex, uv);
color.a = 0;
return color;
}
I'd assume this would cause the image it's applied to to be completely invisible. However that's not what appears to be happening. Instead, the resulting image will become invisible over a white background, but over a black background it'll be unchanged. It appears that this shader is somehow causing an "add" function to be called between the foreground and the background.
For example, the following code loads a foreground and background image, applies the above shader effect to the foreground, renders them to a bitmap, and writes the bitmap to file.
public sealed partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
}
void Button_Click(object sender, RoutedEventArgs e)
{
const int width = 1024;
const int height = 768;
var sz = new Size(width, height);
var background = new Image { Source = new BitmapImage(new Uri(#"c:\background.jpg")) };
background.Measure(sz);
background.Arrange(new Rect(sz));
var foreground = new Image { Source = new BitmapImage(new Uri(#"c:\foreground.jpg")), Effect = new Alpha() };
foreground.Measure(sz);
foreground.Arrange(new Rect(sz));
var target = new RenderTargetBitmap(width, height, 96d, 96d, PixelFormats.Default);
target.Render(background);
target.Render(foreground);
var jpg = new JpegBitmapEncoder();
jpg.Frames.Add(BitmapFrame.Create(target));
using (var fileStream = File.OpenWrite(#"c:\output.jpg"))
{
jpg.Save(fileStream);
}
}
}
// Standard ShaderEffect stuff here, nothing exciting.
public sealed class Alpha : ShaderEffect
{
static readonly PixelShader Shader = new PixelShader{UriSource = new Uri("pack://application:,,,/Alpha.ps", UriKind.RelativeOrAbsolute)};
public static readonly DependencyProperty InputProperty = RegisterPixelShaderSamplerProperty("Input", typeof(Alpha), 0);
public Alpha()
{
PixelShader = Shader;
UpdateShaderValue(InputProperty);
}
public Brush Input
{
get { return (Brush)GetValue(InputProperty); }
set { SetValue(InputProperty, value); }
}
}
This produces the following when applied to two of the Win7 sample pictures:
This is the same behavior I see on the screen when I apply the effect to one Image in XAML, with another Image or anything else behind it.
Note the image is the same if foreground and background are reversed, so if it's not "add", it's at least something commutative. I think it's "add".
Computers are usually right, so I assume this is user error, but why is setting alpha to zero not giving me a transparent image? And how do I get a transparent image if so? (I obviously want to do something more complex with the shader eventually (specifically greenscreen), but to get that to work I have to get this shader to work first, so don't just say "Set the Opacity property").
Gah, stackoverflow is better than google. The top "related question" had the answer. Handling alpha channel in WPF pixel shader effect

Building custom TextBlock control in WPF

I have built custom WPF Control which unique function is displaying text. I tried using TextBlock from System.Windows.Controls namespace but it's not working for me (I have ~10000 strings with different position and too much memory loss). So I tried making my own control by inheriting FrameworkElement, overriding OnRender method which now contain single line:
drawingContext.DrawText(...);
But...
I get a little confusing result.
After comparing performance for 10000 objects, I realized that the time needed for creating and adding to Canvas is still ~10 sec, and memory usage for my application raises from ~32MB to ~60MB !!!
So no benefits at all.
Can anyone explain why this happens, and what is the other way to create simple (simple = allocate less memory, take less time to create) visual with two functions:
display text
set position (using thickness or TranslateTransform)
Thanks.
Check out AvalonEdit
Also not sure how you are storing the strings, but have you used StringBuilder before?
Here is my code (a little bit modified):
public class SimpleTextBlock : FrameworkElement
{
#region Static
private const double _fontSize = 12;
private static Point _emptyPoint;
private static Typeface _typeface;
private static LinearGradientBrush _textBrush;
public readonly static DependencyProperty TextWidthProperty;
static SimpleTextBlock()
{
_emptyPoint = new Point();
_typeface = new Typeface(new FontFamily("Sergoe UI"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
GradientStopCollection GSC = new GradientStopCollection(2);
GSC.Add(new GradientStop(Color.FromArgb(160, 255, 255, 255), 0.0));
GSC.Add(new GradientStop(Color.FromArgb(160, 180, 200, 255), 0.7));
_textBrush = new LinearGradientBrush(GSC, 90);
_textBrush.Freeze();
SimpleTextBlock.TextWidthProperty = DependencyProperty.Register(
"TextWidth",
typeof(double),
typeof(SimpleTextBlock),
new FrameworkPropertyMetadata(0.0d, FrameworkPropertyMetadataOptions.AffectsRender));
}
#endregion
FormattedText _formattedText;
public SimpleTextBlock(string text)
{
_formattedText = new FormattedText(text, System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, _typeface, _fontSize, _textBrush);
}
public SimpleTextBlock(string text, FlowDirection FlowDirection)
{
_formattedText = new FormattedText(text, System.Globalization.CultureInfo.InvariantCulture, FlowDirection, _typeface, _fontSize, _textBrush);
}
protected override void OnRender(DrawingContext drawingContext)
{
_formattedText.MaxTextWidth = (double)GetValue(TextWidthProperty);
drawingContext.DrawText(_formattedText, _emptyPoint);
}
public double TextWidth
{
get { return (double)base.GetValue(TextWidthProperty); }
set { base.SetValue(TextWidthProperty, value); }
}
public double ActualTextWidth
{
get { return _formattedText.Width; }
}
public double ActualTextHeight
{
get { return _formattedText.Height; }
}
}
Since it sounds like we determined you should stylize a control like listbox, here are some examples of different things you can do:
Use Images as Items
Stylized and Binding
Honestly it all depends on what you want it to look like. WPF is great in how much control it gives you on how something looks.
Crazy example using a listbox to make the planet's orbits

How do I get the current mouse screen coordinates in WPF?

How to get current mouse coordination on the screen?
I know only Mouse.GetPosition() which get mousePosition of element, but I want to get the coordination without using element.
Or in pure WPF use PointToScreen.
Sample helper method:
// Gets the absolute mouse position, relative to screen
Point GetMousePos() => _window.PointToScreen(Mouse.GetPosition(_window));
To follow up on Rachel's answer.
Here's two ways in which you can get Mouse Screen Coordinates in WPF.
1.Using Windows Forms. Add a reference to System.Windows.Forms
public static Point GetMousePositionWindowsForms()
{
var point = Control.MousePosition;
return new Point(point.X, point.Y);
}
2.Using Win32
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetCursorPos(ref Win32Point pt);
[StructLayout(LayoutKind.Sequential)]
internal struct Win32Point
{
public Int32 X;
public Int32 Y;
};
public static Point GetMousePosition()
{
var w32Mouse = new Win32Point();
GetCursorPos(ref w32Mouse);
return new Point(w32Mouse.X, w32Mouse.Y);
}
Do you want coordinates relative to the screen or the application?
If it's within the application just use:
Mouse.GetPosition(Application.Current.MainWindow);
If not, I believe you can add a reference to System.Windows.Forms and use:
System.Windows.Forms.Control.MousePosition;
If you try a lot of these answers out on different resolutions, computers with multiple monitors, etc. you may find that they don't work reliably. This is because you need to use a transform to get the mouse position relative to the current screen, not the entire viewing area which consists of all your monitors. Something like this...(where "this" is a WPF window).
var transform = PresentationSource.FromVisual(this).CompositionTarget.TransformFromDevice;
var mouse = transform.Transform(GetMousePosition());
public System.Windows.Point GetMousePosition()
{
var point = Forms.Control.MousePosition;
return new Point(point.X, point.Y);
}
This works without having to use forms or import any DLLs:
using System.Windows;
using System.Windows.Input;
/// <summary>
/// Gets the current mouse position on screen
/// </summary>
private Point GetMousePosition()
{
// Position of the mouse relative to the window
var position = Mouse.GetPosition(Window);
// Add the window position
return new Point(position.X + Window.Left, position.Y + Window.Top);
}
You may use combination of TimerDispatcher (WPF Timer analog) and Windows "Hooks" to catch cursor position from operational system.
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetCursorPos(out POINT pPoint);
Point is a light struct. It contains only X, Y fields.
public MainWindow()
{
InitializeComponent();
DispatcherTimer dt = new System.Windows.Threading.DispatcherTimer();
dt.Tick += new EventHandler(timer_tick);
dt.Interval = new TimeSpan(0,0,0,0, 50);
dt.Start();
}
private void timer_tick(object sender, EventArgs e)
{
POINT pnt;
GetCursorPos(out pnt);
current_x_box.Text = (pnt.X).ToString();
current_y_box.Text = (pnt.Y).ToString();
}
public struct POINT
{
public int X;
public int Y;
public POINT(int x, int y)
{
this.X = x;
this.Y = y;
}
}
This solution is also resolving the problem with too often or too infrequent parameter reading so you can adjust it by yourself. But remember about WPF method overload with one arg which is representing ticks not milliseconds.
TimeSpan(50); //ticks
If you're looking for a 1 liner, this does well.
new Point(Mouse.GetPosition(mWindow).X + mWindow.Left, Mouse.GetPosition(mWindow).Y + mWindow.Top)
The + mWindow.Left and + mWindow.Top makes sure the position is in the right place even when the user drags the window around.
Mouse.GetPosition(mWindow) gives you the mouse position relative to the parameter of your choice.
mWindow.PointToScreen() convert the position to a point relative to the screen.
So mWindow.PointToScreen(Mouse.GetPosition(mWindow)) gives you the mouse position relative to the screen, assuming that mWindow is a window(actually, any class derived from System.Windows.Media.Visual will have this function), if you are using this inside a WPF window class, this should work.
I wanna use this code
Point PointA;
private void Button_PreviewMouseUp(object sender, MouseButtonEventArgs e) {
PointA = e.MouseDevice.GetPosition(sender as UIElement);
}
private void Button_Click(object sender, RoutedEventArgs e) {
// use PointA Here
}

Creating SelectionBorder: Bit in the face by decimal rounding?

I am currently implementing a class called SelectionBorder in WPF. It's derived from the Shape class.
It basically looks like this:
public class SelectionBorder : Shape
{
public Point StartPoint {get; set;}
public PointCollection Points {get; set;}
public double StrokeLength {get; set;}
protected override Geometry DefiningGeometry{
get{
//Magic!
}
}
}
The StartPoint and Points properties determine the corners of the border. The border is a typical stroked line border (one black stroke, one invisible stroke like that: - - - -)
The problem that I have now is that since the corner points are freely choosable it's pretty common that the count of strokes (meaning black and invisible strokes) is not even (in fact not even an integer) and therefore the first stroke looks longer than the others (visible in the picture). This maybe doesn't seem to be a big deal but I later want to animate the border so that the strokes circle round the content. When doing this animation the tiny flaw in the static view becomes clearly visible and in my opinion is highly disturbing.
alt text http://img14.imageshack.us/img14/2874/selectionborder.png
The problem is that I tried to determine a StrokeLength that gets as close to the original StrokeLength as possible and creates an even number of strokes. However the problem I've run into is that WPF (obviously) can't display the whole precision of a double decimal StrokeLength and therefore the resulting stroke number is uneven once again.
Is there any workaround for this problem? Do you probably have another solution for my problem?
Thanks in advance!
EDIT: I retested and reviewed the code after a little break for fitness today and after all it happens only on very big StrokeLengths. I plan to use StrokeLengths of 2 where the little animation jumping does matter much less than I originally thought.
You could make more than one corner "un-matched" in that regard. For example, instead of having one point be the "source" and "destination" of the animated dashes, you could pick 2 points. One would be the "source", dashes appearing to march away from it in 2 directions, and another point be the "destination", where dashes converge and disappear.
GIMP, for example, animates selection dashed lines in this way and seems to pick a point closest to the lower-left for the "source" and a point closest to the upper-right for the "destination".
You could come up with some other scheme, as well.
Just remember that while it may look disturbing to you, most users will not care.
I just found a way that makes it way easier to create such an animated SelectionBorder.
Instead of creating the animation by moving an self-created AnimationPoint through animation I just animated the StrokeDashOffset property natively provided by the Shape class and setting the StrokeDashArray to define the StrokeLength.
It would look like this in XAML:
<namespace:SelectionBorder StrokeDashArray="2" AnimationDuration="0:0:1" Stroke="Black" />
The class looks like this:
public class SelectionBorder : Shape
{
private DoubleAnimation m_Animation;
private bool m_AnimationStarted;
public SelectionBorder()
{
IsVisibleChanged += OnIsVisibleChanged;
}
protected void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (Visibility == Visibility.Visible)
{
StartAnimation();
}
else
{
StopAnimation();
}
}
public void StartAnimation()
{
if (m_AnimationStarted)
return;
if (m_Animation == null)
{
m_Animation = CreateAnimation();
}
BeginAnimation(StrokeDashOffsetProperty, m_Animation);
m_AnimationStarted = true;
}
protected virtual DoubleAnimation CreateAnimation()
{
DoubleAnimation animation = new DoubleAnimation();
animation.From = 0;
if (StrokeDashArray.Count == 0)
animation.To = 4;
else
animation.To = StrokeDashArray.First() * 2;
animation.Duration = AnimationDuration;
animation.RepeatBehavior = RepeatBehavior.Forever;
return animation;
}
public void StopAnimation()
{
if (m_AnimationStarted)
{
BeginAnimation(StrokeDashOffsetProperty, null);
m_AnimationStarted = false;
}
}
#region Dependency Properties
public Duration AnimationDuration
{
get { return (Duration)GetValue(AnimationDurationProperty); }
set { SetValue(AnimationDurationProperty, value); }
}
public static readonly DependencyProperty AnimationDurationProperty =
DependencyProperty.Register("AnimationDuration", typeof(Duration), typeof(SelectionBorder), new UIPropertyMetadata(new Duration(TimeSpan.FromSeconds(0.5))));
#endregion Dependency Properties
protected override Geometry DefiningGeometry
{
get
{
double width = (double.IsNaN(Width)) ? ((Panel)Parent).ActualWidth : Width;
double height = (double.IsNaN(Height)) ? ((Panel)Parent).ActualHeight : Height;
RectangleGeometry geometry = new RectangleGeometry(new Rect(0, 0, width, height));
return geometry;
}
}
}

Resources