by Kevin Goldsmith

Created

October 23, 2007

Previous parts to this tutorial:
Steps 1-3
Steps 4-6

At this point, we have a hydra filter which does an interactive cross-disolve from a color image to a luminance based black and white image: Download Hydra Filter

The current version of the filter is:

kernel FadeToHistory

{
parameter float crossfade;
const float3 lumMult = float3(0.3, 0.59, 0.11);

void
evaluatePixel(in image4 src, out pixel4 dst)
{
dst = sampleNearest(src,outCoord());
float luminance = dot(dst.rgb, lumMult);
dst.rgb = mix(dst.rgb, float3(luminance), crossfade);
}
}

Step 7: Adding the Sepia Conversion

We’re about halfway to being done. We are fading from color to black and white. Time to add the sepia toning so that we can finish this sucker off. There are a bunch of different methods for doing sepia conversion, but essentially the general idea is to mix the red, green and blue channels in different amounts to preserve the original image while giving it that old-time photographic feel. Unlike computing the luminance, we’ll need to produce all three color channels, so the calculation is a bit more cumbersome:

        float3 sepia = float3( dst.r * 0.4 +
dst.g * 0.769 +
dst.b * 0.189,
dst.r * 0.349 +
dst.g * 0.686 +
dst.b * 0.168,
dst.r * 0.272 +
dst.g * 0.534 +
dst.b * 0.131 );

I’ve modified the filter above so that we can test the sepia toning.

kernel FadeToHistory

{
parameter float crossfade;
const float3 lumMult = float3(0.3, 0.59, 0.11);

void
evaluatePixel(in image4 src, out pixel4 dst)
{
dst = sampleNearest(src,outCoord());
float luminance = dot(dst.rgb, lumMult);
float3 sepia = float3( dst.r * 0.4 +
dst.g * 0.769 +
dst.b * 0.189,
dst.r * 0.349 +
dst.g * 0.686 +
dst.b * 0.168,
dst.r * 0.272 +
dst.g * 0.534 +
dst.b * 0.131 );
dst.rgb = mix(dst.rgb, sepia, crossfade);
}
}

So now we’ll run the filter and made sure that it compiles without error and does something like what we want. If you set the crossfade parameter to 1.0, you should see something like this:

step 8: doing the full fade

We have black and white, we have sepia, now we want to fade from color to black and white and then to sepia. First, I’ll show how I would do this with Flash compatibility mode off and then I’ll show how I would do it with Flash compatibility on.

With Flash warnings and errors off, I would write the code like this:

kernel FadeToHistory

{
parameter float crossfade<minValue:0.0; maxValue:2.0; defaultValue:0.0;>;

const float3 lumMult = float3(0.3, 0.59, 0.11);

void
evaluatePixel(in image4 src, out pixel4 dst)
{
dst = sampleNearest(src,outCoord());
float luminance = dot(dst.rgb, lumMult);
float3 sepia = float3( dst.r * 0.4 +
dst.g * 0.769 +
dst.b * 0.189,
dst.r * 0.349 +
dst.g * 0.686 +
dst.b * 0.168,
dst.r * 0.272 +
dst.g * 0.534 +
dst.b * 0.131 );

float3 startMix = dst.rgb;
float3 endMix = float3(luminance);
float mixValue = crossfade;
if ( crossfade > 1.0 )
{
// normalize mix value to the range of 0-1
mixValue -= 1.0;
startMix = float3(luminance);
endMix = sepia;
}
dst.rgb = mix(startMix, endMix, mixValue);
}
}



If you want to try this, be sure to uncheck the
"Turn on Flash warnings and Errors" in the build menu, or you’ll get the error "ERROR: (line 30): If statements not supported in Hydra byte code"

A few things to note:

  • I’ve added metadata to the crossfade parameter to specify that its range is now from 0.0 to 2.0 instead of the default 0.0 to 1.0
  • I’ve added temporary variables for startMix, endMix and mixValue to simplify the code. By default, they are the original color, the luminance color and the crossfade value.
  • If crossfade is larger than 1.0
    • I overwrite the temporary variable mixValue to be mapped into the range of 0.0 to 1.0 so that we can use that value in the mix function
    • I overwrite the startMix value with luminance color so that is where we mix from instead of the original color
    • I overwrite the endMix value with the sepia color

Now, about that error we get if we try to compile with Flash warnings and errors on. In order to support older graphics cards, we will not support conditional branching (if statements) in Astro. That makes things a bit harder, but in no way impossible. This is how I would rewrite the above filter to get around that limitation:

 

kernel FadeToHistory

{
parameter float crossfade<minValue:0.0; maxValue:2.0; defaultValue:0.0;>;

const float3 lumMult = float3(0.3, 0.59, 0.11);

void
evaluatePixel(in image4 src, out pixel4 dst)
{
dst = sampleNearest(src,outCoord());
float3 luminance = float3( dot(dst.rgb, lumMult) );
float3 sepia = float3( dst.r * 0.4 +
dst.g * 0.769 +
dst.b * 0.189,
dst.r * 0.349 +
dst.g * 0.686 +
dst.b * 0.168,
dst.r * 0.272 +
dst.g * 0.534 +
dst.b * 0.131 );

float3 startMix = dst.rgb;
startMix = (crossfade <= 1.0) ? startMix : luminance;
float3 endMix = (crossfade <= 1.0) ? luminance : sepia;
float mixMinusOne = crossfade - 1.0;
float mixValue = (crossfade <= 1.0) ? crossfade : mixMinusOne;
dst.rgb = mix(startMix, endMix, mixValue);
}
}

Instead of doing conditional branching in Flash, I can do conditional assignments of pre-computed values. What this means is that I have to do a little more computation, but I can still get the results I want. Instead of only doing computation in an if or else statement, I do all the computation in the filter and then only assign the result to the final variables if the condition is met. Of course you can do conditional assignments in regular Hydra too to be clear.

The big changes in this version of the filter is that I have a couple of extra statements and an extra variable, but the output is exactly the same.

All we have left to do is a bit more cleanup and then we’re done!

Step 9: final cleanup

That sepia calculation is annoying. There is a lot of math there and it takes up a lot of space. Doing these kinds of calculations can be pretty common in image and video processing and there is a simpler way to do them in Hydra. Doing this kind of operation where we combine each channel of a color with a blend of all of the colors added together is exactly the same thing we do when we multiply a vector by a matrix. Matricies are built-in to Hydra and you can use them like any other type. This is how that looks:

 

kernel FadeToHistory

{
parameter float crossfade<minValue:0.0; maxValue:2.0; defaultValue:0.0;>;

const float3 lumMult = float3(0.3, 0.59, 0.11);
const float3x3 sepiaMatrix = float3x3(0.400, 0.769, 0.189,
0.349, 0.686, 0.168,
0.272, 0.534, 0.131);

void
evaluatePixel(in image4 src, out pixel4 dst)
{
dst = sampleNearest(src,outCoord());
float3 luminance = float3( dot(dst.rgb, lumMult) );
float3 sepia = dst.rgb * sepiaMatrix;

float3 startMix = dst.rgb;
startMix = (crossfade <= 1.0) ? startMix : luminance;
float3 endMix = (crossfade <= 1.0) ? luminance : sepia;
float mixMinusOne = crossfade - 1.0;
float mixValue = (crossfade <= 1.0) ? crossfade : mixMinusOne;

dst.rgb = mix(startMix, endMix, mixValue);
}
}

Once again, I’ve used a const for sepiaMatrix because it stays the same from pixel to pixel. Now everything looks cleaner and simpler. It is easier to understand and more importantly, it is easier to mess around with and make changes. Download the final version and try it for yourself!

Thanks for following this tutorial and let me know in the comments if you have questions or would like other tutorials.