All examples in this section are located in Shaders.cs of the Tutorial project.

A pixel shader is installed on a UI object by assigning function to the Effect.Custom defined by its bling object. The type of a pixel shader is Func<Texture, PointBl, ColorBl>, where the first argument is the input texture, the second argument is the pixel being shaded, and the return value is the resulting color of the pixel. A pixel effect is automatically coercible into a pixel shader.

As an example consider the following code:
ImageBl image = new ImageBl(canvas) { 
  Source = new BitmapImage("Resources/Autumn.jpg".MakePackUri(typeof(Shaders))),
};
image.Effect.Custom = (input,uv) => input[uv];

This custom effect merely returns the original color of the pixel, leading to no change in the pixel.
The current pixel, uv, represents the pixel's coordinate as a percentage offset in the input texture; e.g., the coordinate (.25,.75) represents a pixel whose X pixel value is 25% the texture's width and whose Y pixel value is 75% the texture's height. We can mirror flip the image horizontally by reversing the accessed pixel's X coordinate:
image.Effect.Custom = (input,uv) => input[new PointBl(1 - uv.X, uv.Y)];

The expression 1 - n addresses an opposing percent of n; e.g., if n is .33, 1 - n is .66. We can also flip the image vertically by reversing the accessed pixel's Y coordinate:
image.Effect.Custom = (input,uv) =>  input[new PointBl(uv.X, 1 - uv.Y)];

The following is the result of the last shaders. The first image has no effect applied, the second one flips horizontally, the last one flips vertically.

a.jpg
a_flipX.jpg
a_flipY.jpg

For more fun, we can distort the image according to a draggable thumb. Consider:
ThumbBl thumb = new ThumbBl(canvas) {
  CenterPosition = canvas.CenterSize, ZIndex = 1,
  CanDrag = true, Background = Brushes.Red
};
image.Effect.Custom = (input,uv) => {
  PointBl v = ((uv * image.Size) - (thumb - image.LeftTop));
  DoubleBl l = (v.Length / 100);
  l = l.Pow(3);
  l = (l >= 1).Condition(0, l);
  return input[uv + l * v.Normal * .1];
};
This code draws a lense around the current position of the thumb. Because we want to compare the current pixel being shaded (uv) to the thumb's position, we multiply uv by the size of the image and subtract the image's position from thumb (coerced to a position). This gives us a vector that we can then used to find the pixel distance between the current pixel and the thumb, which we divide by 100 so the lense's radius is 100 pixels. We take this length, raise it to the third power (so that the lense looks curvy) and use a condition to filter any length at one or greater to zero to create a clear border for the length. This value is then used to displace pixels in the lense according to l and the normal vector of the pixel from the thumb thumb. Result:
a_distort.jpg

The pixel shaders shown so far operate by displacing pixels. Pixel shaders can also manipulate colors. Taking our last example, we can interpolate gray into the distorted part of the lense for a more realistic effect:
image.Effect.Custom = (input,uv) => {
  PointBl v = ((uv * image.Size) - (thumb - image.LeftTop));
  DoubleBl l = (v.Length / 100);
  l = l.Pow(3);
  l = (l >= 1).Condition(0, l);
  ColorBl clr = input[uv + l * v.Normal * .1];
  return l.Lerp(clr, Colors.Gray);
};
The last line interpolates ("lerp") between the computed color and gray based on the l value. Result:
a_distortB.jpg

MetaBalls

Pixel shaders that compute colors based on distances are very powerful. One example application of such a shader is to create MetaBalls, which are used to simulate organic objects. The MetaBall example in the tutorial is located in MetaBalls.cs. First, we create a second canvas in the main canvas to hold the meta-ball effect; we'll add thumbs in the main canvas on top of this second canvas to control the positions of the metaballs:
new CanvasBl(canvas) {
  Size = canvas.Size, LeftTop = new Point(0, 0),
  Background = Brushes.Black,
}.Effect.Custom = (input, uv) => {
We don't bother assigning the canvas to a variable as we don't need to refer to it again (its just there to hold the effect). The rest of the metaball code takes place inside the custom effect function. Each metaball has its own primary color, which we store in a colors array. Each pixel has a color that depends on how close the pixel is collectively to the metaball centers, which we express as metaball. Code:
Random r = new Random();
ColorBl[] colors = new ColorBl[] {
  Colors.Red, Colors.Blue,
  Colors.Green, Colors.Cyan,
  Colors.Orange, Colors.Yellow,
};
ColorBl color = Colors.Black;
DoubleBl metaball = 0;
Now, the metaball algorithm measures the distance and color of each pixel through a basic loop. We also create the WPF thumbs in this loop, as the loop will only be executed once when the pixel shader is generated. This behavior sounds a bit weird as the loop is only building an abstract value that is compiled into the pixel shader; even if it directly appears to be computing the pixel's color. Here is the loop:
foreach (Color primary in colors) {
  PointBl point = new ThumbBl(canvas) {
    Background = primary,
    ZIndex = 1,
    CenterPosition = canvas.CenterSize - new PointBl(150, 150) +
                              r.NextPoint() * new PointBl(300, 300),
    CanDrag = true,
  }.CenterPosition;
  var v = (uv * canvas.Size - point);
  v = v * v;
  var at = 3d / (v.X + v.Y);
  color += primary * at;
  metaball += at;
}
The first statement creates the thumb at a random location (r.NextPoint()) in the canvas with a z-index of one so that the thumb appears above the effect. The current location of the thumb is then compared to the pixel using a tuned metaball distance function. The resulting at value is then used to accumulate the pixel's color (weighted) and metaball values. Finally, we compute and return the color:
var useColor = color / metaball;
return (metaball > .0016).Condition(useColor, Colors.White);
We use a threshholding value of .0016 found via tuning, leading to a ball-like organization of the meta-balls. The result looks like this:
metaball.jpg

Limitations

A pixel shader in Bling is translated into a conventional WPF pixel shader. As a result, they run on the GPU and are subject to various restrictions. Pixel shaders must be simple: they can only contain around 96 instructions and use 32 registers, where instruction and register are defined according to the {url:HLSL|http://en.wikipedia.org/wiki/High_Level_Shader_Language} specification. If you try to build a shader that is too complicated, Bling will throw an exception. Bling will attempt to optimize your pixel shaders (basic common sub-expression elimination), and the HLSL compiler will optimize more, but there are many cases where you'll have to hand tune your shader to make it fit.

Only simple computations that touch textures or the coordinate of the current pixel will occur in the pixel shader. All other computations occur outside the pixel shader and their results are brought in via registers. To save instructions, you can try to re-arrange your pixel shader so that as many computations occur as possible before textures and the current pixel coordinate is touched. Additionally, many computations cannot occur in shaders; e.g., if it involves creating a widget (as in the metaball example) or if it involves a transform matrix. In these cases, a NotSupportedOperationException is thrown.

A pixel shader does not support real looping. Instead, loops are unrolled so must be fixed in length. For example, the loop in the meta-ball example is only of size six. Unrolling is an important consideration since it will cause instructions to be consumed faster. Likewise, "if" is not supported in a pixel shader, and you must use Condition instead ({url:constraints|Constraints} share this limitation).

Last edited Jul 21, 2009 at 3:37 AM by mcdirmid, version 10

Comments

No comments yet.