EasyPainter: Watercolor, Blood and Gloom Pixel Shaders

Feb 20, 2010

Click here to try the new effects in EasyPainter.

Download source code.

Original vs Zombie – can you figure out which one is which? :)

nikolami zombie

The new Blood shader works only on skin colors. To use it, you have to use the selection brush to select skin tones from the image.

Here is the Watercolor shader. It's very subtle, but I think it looks nice nevertheless. If anyone has suggestions for improvements, including a proper name for this shader, since it is really subtle - please let me know!

image watercolor

I also added a Gloom effect, the one from http://wpffx.codeplex.com

Little Tech Stuff

The "blood" is in fact the pastel texture from the Pastel effect, blended with Burn.

The watercolor is based on real tutorials on doing water color effects: it is a combination of [cheap horizontal] blur and sobel edge detection. Unfortunately, the blur has to be 2-pass to look good, and span larger area. I might improve it in the future. Anyway, the effect looks pleasing to me for now :)

  

Old Photo, Pastel, and Pencil Sketch Pixel Shaders For Silverlight

Feb 18, 2010

EasyPainter has new filters! Source code below! Try them out here

Edit: I'd actually like to improve the pastel shader further - real-life pastel is usually on white background, and the shaders are on black...

Samples

Original (http://www.flickr.com/photos/-arpi/4280428638/):

Original

Pastel Drawing:

Pastel

Old Photo:

OldPhoto1

Old Photo ("younger"): More...

  

Simplex Noise (like Perlin Noise) in Pixel Shader 2.0 for Silverlight and WPF

Feb 11, 2010

Yay! 2D and 3D noise in pixel shaders!

After tackling the Hue shader I decided to move to something even more interesting, Simplex noise.

You've probably heard of Perlin Noise, if not see Rene's post with realtime 3D noise here and also check out my Living Noise sample and Living Noise blog post .

The "Noise" can be used to make effects such as fire, water, marbles, smooth motion, hand-drawn UI, space travel effects, nebulas and a lot more!

Simplex noise has similar characteristics to Perlin noise - it's continuous, smooth and its first derivative is smooth too. The nice thing about the Simplex, is that its much easier to compute and thus can be implemented in Pixel shaders.

Another advantage of the Simplex noise over Perlin noise is that the permutation bitmaps compress very well (8KB total), making the whole XAP about 30KB total, including the sample JPEG.  In comparison Perlin noise's bitmaps compress to about 350KB on my box.

Note you can also generate the maps almost instantly on the fly, eliminating the "size argument" when comparing Perlin and Simplex.

The source code download contains both 2D and 3D Pixel shader Simplex noise implementation. The 3D implementation still lacks some features - read on below.

Here is the live sample:

Download Source Code

The sample is based on this public powerpoint presentation by Natalya Tatarchu (AMD), who is the hero of today and I believe she made a lot of people happy with her great implementation of the Simplex noise! :)

I also re-visited Ken Perlin's homepage to remind myself of the noise a couple of times.

2D noise

This was (relatively) easy, since the reference implementation readily fits into PS 2.0 shaders, with lots of instructions available to experiement.

To overlay couple of noises on top of each other, I had to use the shader several times.

The biggest issue was generating the maps - I had to make sure to set the alpha to 255, otherwise weird things may happen (due to alpha premultiplication).

3D Noise

The 3d noise was more challenging - the implementation I have does not fit in PS 2.0 (84 instructions total, and PS 2.0 is 64 max).

I had to split the function in the middle, and make 2 shaders that build on top of each other. This means that the second shader works with the output from the first. There are several challenges:

1. There is color and shader information to be passed around, which means that either I have to pass the original image every time to the shader, or I have to compress the color space

I chose to compress the color space, and make it (R, S, G+B) where S is the shader value from the previous pass and RGB are the original image components. In the source code, I skipped the G+B part altogether ending with (R, S, 0) (monochrome source image), but you may change it to fix the issue.

If you do, please let me know, since I'm not planning to do it for now!

Note: in .NET 4.0 the 3D Noise shader fits into the PS 3.0 instruction limits. I left the original function in the source for people who want to use it with WPF.

Lot of writing! Hope you enjoy it!

 

        

Someone Said it Was Impossible: Hue Shift in Pixel Shader 2.0 (EasyPainter, Silverlight)

Feb 9, 2010

I read somewhere online that Hue changes can't be done in pixel shader 2.0, due to limitation of 64 instructions per slot.

Here's the sample that proves otherwise:

Download source code

Indeed the RGB-to-HSL-to-RGB conversion takes about 100 instructions in its typical implementation. PS 2.0 which is the shader model supported by Silverlight 3 only allows for 64 arithmetic instructions, as outlined in this comparison between pixel shaders on Wikipedia

How can we optimize it?

Optimizing pixel shader instruction slots is nice - in a typical C# world, you'd be adding if() statements to make your code run faster like this:

    if ( HSV.y != 0 )
       QUAD_REAL var_h = HSV.x * 6;
       QUAD_REAL var_i = floor(var_h);   // Or ... var_i = floor( var_h )
       QUAD_REAL var_1 = HSV.z * (1.0 - HSV.y);
       QUAD_REAL var_2 = HSV.z * (1.0 - HSV.y * (var_h-var_i));
       QUAD_REAL var_3 = HSV.z * (1.0 - HSV.y * (1-(var_h-var_i)));
       if      (var_i == 0) { RGB = QUAD_REAL3(HSV.z, var_3, var_1); }
       else if (var_i == 1) { RGB = QUAD_REAL3(var_2, HSV.z, var_1); }
       else if (var_i == 2) { RGB = QUAD_REAL3(var_1, HSV.z, var_3); }
       else if (var_i == 3) { RGB = QUAD_REAL3(var_1, var_2, HSV.z); }
       else if (var_i == 4) { RGB = QUAD_REAL3(var_3, var_1, HSV.z); }
       else                 { RGB = QUAD_REAL3(HSV.z, var_1, var_2); }
   }

Not with pixel shaders. If you look carefully at the bold if statement, removing it does not change the program logic. It just takes an extra instruction slot. In reality, I think the pixel shader code will run with the same speed with or without the if() (not 100% sure so correct me if needed).

With this knowledge, I decided to do these optimizations:

1. Instead of HSL-to-RGB, use HSV-to-RGB. The reference NVidia Shader Library implementation (source code here) of HSV-RGB-HSV takes ~70 or so slots.

2. Combine the min_channel() and max_channel() functions into 1 - saves a couple if() statements

3. Take out the if (x < 0) (x += 1) checks in the RGB-HSV function, and execute them once instead of twice, after the hue is modified.

4. Remove the "obsolete" if()-s like the one above

I was very happy to see that it just fit in the 64-instruction slot of PS 2.0! Note that it hits the limit and more complex Hue stuff may need further optimizations! :) If you do so, please let me know! Anyway hue tricks that don't use more slots are OK.

Here's the complete Shazzam-friendly source of the .fx file (also included in the sample project source above).

/// <summary>Hue shift</summary>
/// <minValue>0</minValue>
/// <maxValue>1</maxValue>
/// <defaultValue>0</defaultValue>
float HueShift : register(c0);
sampler2D Samp : register(S0);
#define QUAD_REAL float
#define QUAD_REAL3 float3
QUAD_REAL3 rgb_to_hsv_no_clip(QUAD_REAL3 RGB)
{
    QUAD_REAL3 HSV;
   
 float minChannel, maxChannel;
 if (RGB.x > RGB.y) {
  maxChannel = RGB.x;
  minChannel = RGB.y;
 }
 else {
  maxChannel = RGB.y;
  minChannel = RGB.x;
 }
 
 if (RGB.z > maxChannel) maxChannel = RGB.z;
 if (RGB.z < minChannel) minChannel = RGB.z;
   
    HSV.xy = 0;
    HSV.z = maxChannel;
    QUAD_REAL delta = maxChannel - minChannel;             //Delta RGB value
    if (delta != 0) {                    // If gray, leave H & S at zero
       HSV.y = delta / HSV.z;
       QUAD_REAL3 delRGB;
       delRGB = (HSV.zzz - RGB + 3*delta) / (6.0*delta);
       if      ( RGB.x == HSV.z ) HSV.x = delRGB.z - delRGB.y;
       else if ( RGB.y == HSV.z ) HSV.x = ( 1.0/3.0) + delRGB.x - delRGB.z;
       else if ( RGB.z == HSV.z ) HSV.x = ( 2.0/3.0) + delRGB.y - delRGB.x;
    }
    return (HSV);
}
QUAD_REAL3 hsv_to_rgb(QUAD_REAL3 HSV)
{
    QUAD_REAL3 RGB = HSV.z;
    //if ( HSV.y != 0 ) { // we don't really need this since it just adds an obsolete instruction slot
       QUAD_REAL var_h = HSV.x * 6;
       QUAD_REAL var_i = floor(var_h);   // Or ... var_i = floor( var_h )
       QUAD_REAL var_1 = HSV.z * (1.0 - HSV.y);
       QUAD_REAL var_2 = HSV.z * (1.0 - HSV.y * (var_h-var_i));
       QUAD_REAL var_3 = HSV.z * (1.0 - HSV.y * (1-(var_h-var_i)));
       if      (var_i == 0) { RGB = QUAD_REAL3(HSV.z, var_3, var_1); }
       else if (var_i == 1) { RGB = QUAD_REAL3(var_2, HSV.z, var_1); }
       else if (var_i == 2) { RGB = QUAD_REAL3(var_1, HSV.z, var_3); }
       else if (var_i == 3) { RGB = QUAD_REAL3(var_1, var_2, HSV.z); }
       else if (var_i == 4) { RGB = QUAD_REAL3(var_3, var_1, HSV.z); }
       else                 { RGB = QUAD_REAL3(HSV.z, var_1, var_2); }
   //}
   return (RGB);
}
float4 main(float2 uv : TEXCOORD) : COLOR
{
 float4 col = tex2D(Samp, uv);
 float3 hsv = rgb_to_hsv_no_clip(col.xyz);
    hsv.x+=HueShift;
    //if ( hsv.x < 0.0 ) { hsv.x += 1.0; }
    if ( hsv.x > 1.0 ) { hsv.x -= 1.0; }
    return float4(hsv_to_rgb(hsv),col.w);
}
 
btw, Visual Studio 2010 RC is out for MSDN subsribers (public tomorrow) and I'm going to publish all samples in VS 2010 from now on :)

Hope you like it!

      

nokola.com | Terms | Log in

Recent

About the author

Happy & enjoying life. Software enthusiast.
The opinions I express here and on nokola.com are mine and not my employeer's (Microsoft).
This is the official blog of nokola.com. You can find Silverlight samples, coding stuff, and hopefully other interesting things here.