[Custom Tool] - fun with CT: PixelShifty

Moderator: SecondMan

User avatar
SecondMan
Site Admin
Posts: 3489
Joined: Thu Jul 31, 2014 5:31 pm
Answers: 5
Location: Vancouver, Canada
Been thanked: 95 times
Contact:

[Custom Tool] - fun with CT: PixelShifty

#1

Post by SecondMan » Mon May 20, 2019 12:18 am

It has been a while since I've done a tutorial-y thing on the forum, so I thought I'd give a little introduction of the Custom Tool that on the one hand covers some of the basics, but also goes into some more advanced or lesser known stuff. It will even cover something small that's not in the manual :D

For this how-to, I'm going to show you how to make a tool that allows you to shift images in X or Y by whole pixels. Handy for a number of things like aberrations or slight position adjustments without filter hits. And sometimes you just want control over exactly how many pixels you want to move something.

"Hang on", I hear you say, "I can already do that with a Crop node, or a Transform node". And you'd be right. But that won't teach you anything about the Custom Tool, will it? And we may take it a little further after... we'll see where this takes us.

Let's start with something to shift. How about a lovely Mandelbrot. We don't see this node used enough anyway:

Image

And once we start a Custom Tool and rename it to PixelShifty we have our very advanced setup ready:

Image

or for our friends using Fusion 16:

Image

Fusion's Tool Manual (for Fusion 9 that's page 494) has this to say about the Custom Tool:


"The Custom tool is quite likely the most complex, and the most powerful, tool in Fusion. Any
user moderately experienced with scripting, or C++ programming, should find the structure and
terminology used by the Custom tool to be familiar."

Frankly, I bet this doesn't exactly sound terribly inviting for everyone wanting to just get in there and twiddle some knobs. The UI is also seemingly just a bunch of numbers and sliders:

Image

But it's not that bad, as we'll discover. First of all, let's think of what we are trying to do here. We're going to shift an image in X and Y. This means I need two inputs that are numbers, that will serve as the Controls for my tool. One for X, one for Y.

I'll take two sliders then, please :)

Image

We can name these sliders, as well as getting rid of some of the stuff we don't need in the Custom Tool. For this we go to the Config tab.

For Fusion 16 users to get to the Config tab, that's this Icon here:

Image

In there, we'll rename the first two Number Controls to X Pixel Shift and Y Pixel Shift, and we'll turn off everything else we don't need:

Image

Now our Controls look a lot less daunting:

Image



!

The values of those two sliders in the Controls tab are available in the Custom Tool as n1 and n2.


Next up, we need to figure out by how much we want to shift our image. In other words, how much is a pixel? Well, we'll need to know how large our image is, and that data is available to our Custom Tool, if we look in the Fu9 Tool Manual on page 498:

Image

We can test variables in a Custom Tool by simply putting them in one of the expressions in the Channels tab.

For Fusion 16 users to get to the Channels tab, that's this Icon here:

Image

And when we put w1 as a Red Expression we get a very red Mandelbrot, apparently. When we hover over the image and look in the lower left corner we can see that the red channel now has a value of 1920, which is - suprpise! - the same as the width of our image.

Image

Image

We also know that Fusion is resolution independent and describes positions in an image not in absolute pixel values, but relative ones. So a position inside of an image is described in values between 0 and 1, rather than (for example) 0 and 1920. So how much of that 0 to 1, represents one pixel, then?

It's 1 divided by the dimensions of our image. For X this would be [math] and for Y [math]

And those are the values we need to multiply our X Pixel Shift and Y Pixel Shift values by, because those are the amounts of pixels we want to shift. Great!

OK.

Now that we have figured this out, we can use the Custom Tool to its full potential. Note that there are some other Tabs in there, that all have expression fields, but they calculate differently. The Setup tab's expressions calculate once per frame.

For Fusion 16 users to get to the Setup tab, that's this Icon here:

Image

There are also the Inter and Channels tabs, and the expressions in there calculate once per pixel.

For Fusion 16 users to get to the Inter tab, that's this Icon here:

Image

And just as a reminder, the Channels tab in Fusion 16 is this icon here:

Image

This is important to know, because for an HD image, an expression in the Setup tab will be calculated once, but in the Inter or Channel tabs, that same expression would be calculated 2073600 times! As you can imagine, the difference in performance is pretty massive.

So for our friends using Fusion 16, take care to not put expressions that could go in this tab: Image in this tab: Image or this tab: Image because you may have a performance hit of a factor 2 million.

Now, we only have to calculate the relative amount that we need to shift by X or Y once per frame. So we can put that calculation inside the Setup tab. I think I've milked my one joke for this topic enough so let's just look at what those expression are. We want our pixel value, multiplied by the amount of pixels we wish to shift:

Image



!

The results of those two expressions in the Setup tab are available in the Custom Tool as s1 and s2.


Now that we know this, we need to look up the value of our pixels at the position offset by the value that we have just calculated. When we look on page 499 of the Fusion 9 Tool Manual, we learn that the expression to do this is

Image

Using that template we can now fill in our Channel expressions as:

Image

And believe it or not, that's all it took to get a tool that does what we set out to do: shift an image in X and/or Y by a whole amount of pixels:

Image

Here is a copy of PixelShifty tool so far:

Code: [Select all] [Expand/Collapse] [Download] (PixelShifty.setting)
  1. {
  2.     Tools = ordered() {
  3.         CustomTool1LUTIn1 = LUTBezier {
  4.             KeyColorSplines = {
  5.                 [0] = {
  6.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  7.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  8.                 }
  9.             },
  10.             SplineColor = { Red = 204, Green = 0, Blue = 0 }
  11.         },
  12.         CustomTool1LUTIn2 = LUTBezier {
  13.             KeyColorSplines = {
  14.                 [0] = {
  15.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  16.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  17.                 }
  18.             },
  19.             SplineColor = { Red = 0, Green = 204, Blue = 0 }
  20.         },
  21.         CustomTool1LUTIn3 = LUTBezier {
  22.             KeyColorSplines = {
  23.                 [0] = {
  24.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  25.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  26.                 }
  27.             },
  28.             SplineColor = { Red = 0, Green = 0, Blue = 204 }
  29.         },
  30.         CustomTool1LUTIn4 = LUTBezier {
  31.             KeyColorSplines = {
  32.                 [0] = {
  33.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  34.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  35.                 }
  36.             },
  37.             SplineColor = { Red = 204, Green = 204, Blue = 204 }
  38.         },
  39.         PixelShifty = Custom {
  40.             CtrlWZoom = false,
  41.             ViewInfo = OperatorInfo { Pos = { 1001.32531738281, 136.970565795898 } },
  42.             Inputs = {
  43.                 ShowNumber5 = Input { Value = 0 },
  44.                 NumberIn1 = Input { Value = -500 },
  45.                 NumberIn2 = Input { Value = -400 },
  46.                 ShowNumber3 = Input { Value = 0 },
  47.                 ShowLUT4 = Input { Value = 0 },
  48.                 LUTIn4 = Input {
  49.                     Source = "Value",
  50.                     SourceOp = "PixelShiftyLUTIn4"
  51.                 },
  52.                 ShowLUT3 = Input { Value = 0 },
  53.                 ShowLUT2 = Input { Value = 0 },
  54.                 ShowLUT1 = Input { Value = 0 },
  55.                 Setup2 = Input { Value = "n2*1/h1" },
  56.                 LUTIn1 = Input {
  57.                     Source = "Value",
  58.                     SourceOp = "PixelShiftyLUTIn1"
  59.                 },
  60.                 ShowNumber7 = Input { Value = 0 },
  61.                 GreenExpression = Input { Value = "getg1b(x-s1, y-s2)" },
  62.                 BlueExpression = Input { Value = "getb1b(x-s1, y-s2)" },
  63.                 NameforNumber2 = Input { Value = "Y Pixel Shift" },
  64.                 ShowNumber8 = Input { Value = 0 },
  65.                 ShowNumber4 = Input { Value = 0 },
  66.                 ShowNumber6 = Input { Value = 0 },
  67.                 LUTControls = Input { Value = 1 },
  68.                 RedExpression = Input { Value = "getr1b(x-s1, y-s2)" },
  69.                 PointControls = Input { Value = 1 },
  70.                 ShowPoint2 = Input { Value = 0 },
  71.                 LUTIn3 = Input {
  72.                     Source = "Value",
  73.                     SourceOp = "PixelShiftyLUTIn3"
  74.                 },
  75.                 ShowPoint1 = Input { Value = 0 },
  76.                 ShowPoint3 = Input { Value = 0 },
  77.                 LUTIn2 = Input {
  78.                     Source = "Value",
  79.                     SourceOp = "PixelShiftyLUTIn2"
  80.                 },
  81.                 ShowPoint4 = Input { Value = 0 },
  82.                 Setup1 = Input { Value = "n1*1/w1" },
  83.                 NameforNumber1 = Input { Value = "X Pixel Shift" },
  84.                 AlphaExpression = Input { Value = "geta1b(x-s1, y-s2)" },
  85.                 NumberControls = Input { Value = 1 }
  86.             }
  87.         },
  88.         PixelShiftyLUTIn1 = LUTBezier {
  89.             KeyColorSplines = {
  90.                 [0] = {
  91.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  92.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  93.                 }
  94.             },
  95.             NameSet = true,
  96.             SplineColor = { Red = 204, Green = 0, Blue = 0 }
  97.         },
  98.         PixelShiftyLUTIn2 = LUTBezier {
  99.             KeyColorSplines = {
  100.                 [0] = {
  101.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  102.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  103.                 }
  104.             },
  105.             NameSet = true,
  106.             SplineColor = { Red = 0, Green = 204, Blue = 0 }
  107.         },
  108.         PixelShiftyLUTIn3 = LUTBezier {
  109.             KeyColorSplines = {
  110.                 [0] = {
  111.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  112.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  113.                 }
  114.             },
  115.             NameSet = true,
  116.             SplineColor = { Red = 0, Green = 0, Blue = 204 }
  117.         },
  118.         PixelShiftyLUTIn4 = LUTBezier {
  119.             KeyColorSplines = {
  120.                 [0] = {
  121.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  122.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  123.                 }
  124.             },
  125.             NameSet = true,
  126.             SplineColor = { Red = 204, Green = 204, Blue = 204 }
  127.         }
  128.     }
  129. }

We're not done yet. Next up we'll look at something that doesn't quite work yet and fix it, as well as expanding the functionality of our tool which may make it a LOT slower. But we'll fix that, too.


Tags:

User avatar
SecondMan
Site Admin
Posts: 3489
Joined: Thu Jul 31, 2014 5:31 pm
Answers: 5
Location: Vancouver, Canada
Been thanked: 95 times
Contact:

Re: [Custom Tool] - fun with CT: PixelShifty

#2

Post by SecondMan » Mon May 20, 2019 7:36 am

For our second installment, let's first look at a small oddity that happens when we are playing around with our newly built PixelShifty:

Image

Odd indeed! We're not changing any values in the tool, and yet our shift is changing.

As it turns out, this little guy is what causes it to happen:

Image

Our tool doesn't work properly with Proxy Scaling. Which is a bit of a mortal sin in Fusion, since it's such an important thing to have and use for working interactively at decent speeds, especially when composites get a bit complex.

So why does this happen? Remember our first expression in the Setup tab (or Image):

Image

We are multiplying the pixels by the inverse of our image dimensions... By the way, this is of course much simpler to write as n1/w (we can, but we don't need to use the image input number for w and h if we're referring to the primary input image in a Custom Tool)

w and h will return the input image dimensions - in this case 1920 and 1080 - but when we turn on our Proxy Scale, the image dimensions change. With a Proxy Scale of 2, which I've used in the above example, our image effectively becomes 960 x 540. So our relative image shift values n1/w and n2/h become twice as large, because our absolute pixel offsets remain unchanged. That drives our amount of shift, which is also twice as large now.

How can we know this to be true? By testing, of course. We'll run the same variable test as in the previous post, and see what happens when we turn on Proxy Scale:

Image

The same w as a Channel expression yields 960 instead of 1920 with a Proxy Scale of 2 turned on.

We need a third variable to put in our Setup expression, to scale our pixel offsets with. Remember, we want to cram as much as possible into those first Setup expressions to keep our tool nimble.

This time the Fusion Scripting Guide will help us. A quick search for Proxy Scale takes us to one of the Image class members on page 145:

Image

And here is how we set that up in our Custom Tool:

Image

Now that we have this as a working input, we can adjust our Setup expression like this, very simply:

Image

Or like this in Fusion 16:

Image

With all the arithmetic operators in red for additional eye strain.

As an aside, once this is set up, we can turn off the Number 3 visibility again in the Config tab (or Image), to keep our Tool UI clean.

Keen readers of this post may have spotted a different way of solving this problem, using the same techniques :)

Here is our adjusted PixelShifty to play with:

Code: [Select all] [Expand/Collapse] [Download] (PixelShifty.setting)
  1. {
  2.     Tools = ordered() {
  3.         CustomTool1LUTIn1 = LUTBezier {
  4.             KeyColorSplines = {
  5.                 [0] = {
  6.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  7.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  8.                 }
  9.             },
  10.             SplineColor = { Red = 204, Green = 0, Blue = 0 }
  11.         },
  12.         CustomTool1LUTIn2 = LUTBezier {
  13.             KeyColorSplines = {
  14.                 [0] = {
  15.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  16.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  17.                 }
  18.             },
  19.             SplineColor = { Red = 0, Green = 204, Blue = 0 }
  20.         },
  21.         CustomTool1LUTIn3 = LUTBezier {
  22.             KeyColorSplines = {
  23.                 [0] = {
  24.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  25.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  26.                 }
  27.             },
  28.             SplineColor = { Red = 0, Green = 0, Blue = 204 }
  29.         },
  30.         CustomTool1LUTIn4 = LUTBezier {
  31.             KeyColorSplines = {
  32.                 [0] = {
  33.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  34.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  35.                 }
  36.             },
  37.             SplineColor = { Red = 204, Green = 204, Blue = 204 }
  38.         },
  39.         PixelShifty = Custom {
  40.             CtrlWZoom = false,
  41.             ViewInfo = OperatorInfo { Pos = { 1002.73950195313, 103.029441833496 } },
  42.             Inputs = {
  43.                 ShowLUT1 = Input { Value = 0 },
  44.                 NameforNumber3 = Input { Value = "Proxy Scale" },
  45.                 NumberIn2 = Input { Value = -400 },
  46.                 LUTIn4 = Input {
  47.                     Source = "Value",
  48.                     SourceOp = "PixelShiftyLUTIn4"
  49.                 },
  50.                 LUTIn1 = Input {
  51.                     Source = "Value",
  52.                     SourceOp = "PixelShiftyLUTIn1"
  53.                 },
  54.                 ShowLUT3 = Input { Value = 0 },
  55.                 GreenExpression = Input { Value = "getg1b(x-s1, y-s2)" },
  56.                 BlueExpression = Input { Value = "getb1b(x-s1, y-s2)" },
  57.                 NameforNumber2 = Input { Value = "Y Pixel Shift" },
  58.                 ShowLUT2 = Input { Value = 0 },
  59.                 RedExpression = Input { Value = "getr1b(x-s1, y-s2)" },
  60.                 NumberIn3 = Input {
  61.                     Expression = "Image1.ProxyScale",
  62.                     Value = 1
  63.                 },
  64.                 ShowPoint3 = Input { Value = 0 },
  65.                 LUTIn2 = Input {
  66.                     Source = "Value",
  67.                     SourceOp = "PixelShiftyLUTIn2"
  68.                 },
  69.                 ShowPoint4 = Input { Value = 0 },
  70.                 AlphaExpression = Input { Value = "geta1b(x-s1, y-s2)" },
  71.                 ShowNumber5 = Input { Value = 0 },
  72.                 NumberIn1 = Input { Value = -500 },
  73.                 ShowNumber3 = Input { Value = 0 },
  74.                 ShowNumber4 = Input { Value = 0 },
  75.                 Setup2 = Input { Value = "n2/h/n3" },
  76.                 ShowNumber7 = Input { Value = 0 },
  77.                 ShowPoint1 = Input { Value = 0 },
  78.                 ShowLUT4 = Input { Value = 0 },
  79.                 ShowNumber6 = Input { Value = 0 },
  80.                 LUTControls = Input { Value = 1 },
  81.                 NumberControls = Input { Value = 1 },
  82.                 LUTIn3 = Input {
  83.                     Source = "Value",
  84.                     SourceOp = "PixelShiftyLUTIn3"
  85.                 },
  86.                 ShowNumber8 = Input { Value = 0 },
  87.                 Setup1 = Input { Value = "n1/w/n3" },
  88.                 NameforNumber1 = Input { Value = "X Pixel Shift" },
  89.                 PointControls = Input { Value = 1 },
  90.                 ShowPoint2 = Input { Value = 0 }
  91.             }
  92.         },
  93.         PixelShiftyLUTIn1 = LUTBezier {
  94.             KeyColorSplines = {
  95.                 [0] = {
  96.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  97.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  98.                 }
  99.             },
  100.             NameSet = true,
  101.             SplineColor = { Red = 204, Green = 0, Blue = 0 }
  102.         },
  103.         PixelShiftyLUTIn2 = LUTBezier {
  104.             KeyColorSplines = {
  105.                 [0] = {
  106.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  107.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  108.                 }
  109.             },
  110.             NameSet = true,
  111.             SplineColor = { Red = 0, Green = 204, Blue = 0 }
  112.         },
  113.         PixelShiftyLUTIn3 = LUTBezier {
  114.             KeyColorSplines = {
  115.                 [0] = {
  116.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  117.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  118.                 }
  119.             },
  120.             NameSet = true,
  121.             SplineColor = { Red = 0, Green = 0, Blue = 204 }
  122.         },
  123.         PixelShiftyLUTIn4 = LUTBezier {
  124.             KeyColorSplines = {
  125.                 [0] = {
  126.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  127.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  128.                 }
  129.             },
  130.             NameSet = true,
  131.             SplineColor = { Red = 204, Green = 204, Blue = 204 }
  132.         }
  133.     }
  134. }

So now that we have made our PixelShifty satisfyingly more "Fusion-compliant", we will look at expanding it further, and we'll use a little bit of scripting for it! Up next!


User avatar
SecondMan
Site Admin
Posts: 3489
Joined: Thu Jul 31, 2014 5:31 pm
Answers: 5
Location: Vancouver, Canada
Been thanked: 95 times
Contact:

Re: [Custom Tool] - fun with CT: PixelShifty

#3

Post by SecondMan » Mon May 20, 2019 7:36 am

So we a little bit of delay, let's get crackin' with the third and final (or is it?) part...

Let's take another look at the expression we're using for shifting our pixels:

Image

We can read this as "get, from a channel at an input, the pixel value at this coordinate, and zero if we're outside the image". It's the "zero outside the image" we're interested in for this part.

Looking at the manual again, we learn that there are 3 flavours of this expression:

Image

It's the one letter difference we're interested in, and that determines what happens when we go outside our input image boundaries. b will return 0, d will return whatever is at the image edge, w will go to the opposite edge of the image and continue there, wrapping the image around.

You may notice that this corresponds to the Edges parameter in, say, the Transform node.

b is Canvas
d is Duplicate
w is Wrap

And because the Transform node also has mirror, I tried m, and indeed there is a 4th flavour of the expression for Mirror:

Code: Select all

get[ch][#]m(x,y)

Sweet! This is stuff we want for our PixelShifty, so we can move something over slightly without creating empty edge issues, or we can have a couple of ways to create seamless textures, for example.

For my first try, I tried adding a custom MultiButtonControl (love those, they are fast and allow you to change parameters with many options using only one single click!).

Using that input to drive an if statement, I created one very slow tool - note that this only contains the 3 expression flavours from the manual, and we want 4. What happens is that Fusion will calculate the result of each of the different options, and then return the one for which the if statement returns true (i.e. what button is active).

Here's that prototype, for fun and learning:

Code: [Select all] [Expand/Collapse] [Download] (PixelShiftyPrototype_2.setting)
  1. {
  2.     Tools = ordered() {
  3.         Shake1 = Shake {
  4.         },
  5.         Mandelbrot2 = Mandel {
  6.             ViewInfo = OperatorInfo { Pos = { 605, 115.5 } },
  7.             Inputs = {
  8.                 ["Gamut.SLogVersion"] = Input { Value = FuID { "SLog2" } },
  9.                 GlobalOut = Input { Value = 100 },
  10.                 Width = Input { Value = 1920 },
  11.                 Height = Input { Value = 1080 }
  12.             }
  13.         },
  14.         CustomTool1 = Custom {
  15.             UserControls = ordered() {
  16.                 XPixelShift = {
  17.                     LINKS_Name = "X Pixel Shift",
  18.                     INP_MaxAllowed = 1000000,
  19.                     INP_MaxScale = 100,
  20.                     ICS_ControlPage = "Controls",
  21.                     LINKID_DataType = "Number",
  22.                     INP_Integer = true,
  23.                     INPID_InputControl = "ScrewControl",
  24.                     INP_MinAllowed = -1000000,
  25.                     INP_MinScale = 0
  26.                 },
  27.                 YPixelShift = {
  28.                     LINKS_Name = "Y Pixel Shift",
  29.                     INP_MaxAllowed = 1000000,
  30.                     INP_MaxScale = 100,
  31.                     ICS_ControlPage = "Controls",
  32.                     LINKID_DataType = "Number",
  33.                     INP_Integer = true,
  34.                     INPID_InputControl = "ScrewControl",
  35.                     INP_MinAllowed = -1000000,
  36.                     INP_MinScale = 0
  37.                 },
  38.                 Edges = {
  39.                     { MBTNC_AddButton = "Zero" },
  40.                     { MBTNC_AddButton = "Edge" },
  41.                     { MBTNC_AddButton = "Wrap" },
  42.                     MBTNC_ShowBasicButton = true,
  43.                     LINKS_Name = "Edges",
  44.                     MBTNC_StretchToFit = true,
  45.                     INP_MaxAllowed = 1000000,
  46.                     INP_MaxScale = 1,
  47.                     INP_MinAllowed = -1000000,
  48.                     LINKID_DataType = "Number",
  49.                     MBTNC_ShowToolTip = true,
  50.                     INP_MinScale = 0,
  51.                     MBTNC_ShowName = true,
  52.                     INPID_InputControl = "MultiButtonControl",
  53.                     ICS_ControlPage = "Controls",
  54.                     INP_Integer = true
  55.                 }
  56.             },
  57.             CtrlWZoom = false,
  58.             Inputs = {
  59.                 ShowLUT1 = Input { Value = 0 },
  60.                 NumberIn2 = Input {
  61.                     Expression = "YPixelShift",
  62.                     Value = 300
  63.                 },
  64.                 LUTIn4 = Input {
  65.                     Source = "Value",
  66.                     SourceOp = "CustomTool1LUTIn4"
  67.                 },
  68.                 LUTIn1 = Input {
  69.                     Source = "Value",
  70.                     SourceOp = "CustomTool1LUTIn1"
  71.                 },
  72.                 ShowLUT3 = Input { Value = 0 },
  73.                 GreenExpression = Input { Value = "if (n3==0, getg1b(x-n1*s1, y-n2*s2), if (n3==1,  getg1d(x-n1*s1, y-n2*s2), getg1w(x-n1*s1, y-n2*s2)))" },
  74.                 BlueExpression = Input { Value = "if (n3==0, getb1b(x-n1*s1, y-n2*s2), if (n3==1,  getb1d(x-n1*s1, y-n2*s2), getb1w(x-n1*s1, y-n2*s2)))" },
  75.                 ShowPoint2 = Input { Value = 0 },
  76.                 ShowLUT2 = Input { Value = 0 },
  77.                 Setup8 = Input { Value = "if (n3==1, max (0, min(n1, w1)), if (n3==2, n1-int(n1/w1)*w1, if (n3==3, 1-n1-int(n1/w1)*w1, n1)" },
  78.                 RedExpression = Input { Value = "if (n3==0, getr1b(x-n1*s1, y-n2*s2), if (n3==1,  getr1d(x-n1*s1, y-n2*s2), getr1w(x-n1*s1, y-n2*s2)))" },
  79.                 NumberIn3 = Input { Expression = "Edges" },
  80.                 XPixelShift = Input { Value = -300 },
  81.                 YPixelShift = Input { Value = 300 },
  82.                 ShowNumber1 = Input { Value = 0 },
  83.                 ShowPoint3 = Input { Value = 0 },
  84.                 LUTIn2 = Input {
  85.                     Source = "Value",
  86.                     SourceOp = "CustomTool1LUTIn2"
  87.                 },
  88.                 ShowPoint4 = Input { Value = 0 },
  89.                 AlphaExpression = Input { Value = "if (n3==0, geta1b(x-n1*s1, y-n2*s2), if (n3==1,  geta1d(x-n1*s1, y-n2*s2), geta1w(x-n1*s1, y-n2*s2)))" },
  90.                 ShowNumber5 = Input { Value = 0 },
  91.                 NumberIn1 = Input {
  92.                     Expression = "XPixelShift",
  93.                     Value = -300
  94.                 },
  95.                 ShowNumber3 = Input { Value = 0 },
  96.                 ShowNumber4 = Input { Value = 0 },
  97.                 Setup2 = Input { Value = "1.0/h1" },
  98.                 ShowNumber7 = Input { Value = 0 },
  99.                 ShowPoint1 = Input { Value = 0 },
  100.                 ShowLUT4 = Input { Value = 0 },
  101.                 ShowNumber6 = Input { Value = 0 },
  102.                 LUTControls = Input { Value = 1 },
  103.                 NumberControls = Input { Value = 1 },
  104.                 LUTIn3 = Input {
  105.                     Source = "Value",
  106.                     SourceOp = "CustomTool1LUTIn3"
  107.                 },
  108.                 Image1 = Input {
  109.                     Source = "Output",
  110.                     SourceOp = "Mandelbrot2"
  111.                 },
  112.                 Setup1 = Input { Value = "1.0/w1" },
  113.                 ShowNumber2 = Input { Value = 0 },
  114.                 PointControls = Input { Value = 1 },
  115.                 ShowNumber8 = Input { Value = 0 }
  116.             },
  117.             ViewInfo = OperatorInfo { Pos = { 715, 114.5 } }
  118.         },
  119.         CustomTool1LUTIn1 = LUTBezier {
  120.             KeyColorSplines = {
  121.                 [0] = {
  122.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  123.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  124.                 }
  125.             },
  126.             NameSet = true,
  127.             SplineColor = { Red = 204, Green = 0, Blue = 0 }
  128.         },
  129.         CustomTool1LUTIn2 = LUTBezier {
  130.             KeyColorSplines = {
  131.                 [0] = {
  132.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  133.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  134.                 }
  135.             },
  136.             NameSet = true,
  137.             SplineColor = { Red = 0, Green = 204, Blue = 0 }
  138.         },
  139.         CustomTool1LUTIn3 = LUTBezier {
  140.             KeyColorSplines = {
  141.                 [0] = {
  142.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  143.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  144.                 }
  145.             },
  146.             NameSet = true,
  147.             SplineColor = { Red = 0, Green = 0, Blue = 204 }
  148.         },
  149.         CustomTool1LUTIn4 = LUTBezier {
  150.             KeyColorSplines = {
  151.                 [0] = {
  152.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  153.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  154.                 }
  155.             },
  156.             NameSet = true,
  157.             SplineColor = { Red = 204, Green = 204, Blue = 204 }
  158.         }
  159.     }
  160. }

Surely we can find something faster? Right now we're 3 (if not 4) times slower than we strictly need to be...

What if we could, instead, change the actual Channel expression itself?



!

Scripting ahead!


If you've never (or barely ever) scripted before, not a problem. There will be a good few things to take in but in the grand scheme of all things scripting we're not going to cover a lot of ground. However, I don't want to do "Hello World!" either, so if this is indeed your first time scripting it may feel a little dense towards the end. Take your time to read this post, and come back to it a couple of times for a refresher.

I'm taking a single example of something you may actually want to use scripting for. So I'm going to start right at the beginning, work my way through thinking how to solve my riddle, and how to find the necessary documentation to do it.

I will do my best to write it so that the order of the steps makes sense until the end, but if doesn't, please feel free to ask as many questions as you like until they do...

In Fusion, to get the value of any parameter using scripting, we can use the following script command:

Code: Select all

tool.parameter[time]

Try it in the Console (it's fun, I promise :)) - add a Merge to an otherwise empty comp and type:

Code: Select all

print (Merge1.Size[0])

(and press Enter)
This is what you should be seeing:

Image

Fusion needs to know the time for which you are requesting the parameter value. Note that if instead you type:

Code: Select all

print (Merge1.Size)

You get this gobbledygook:

Image

Of course it's not quite gobbledygook. It's the parameter's handle, which is a reference to the parameter. Think of it as the parameter's mailing address inside Fusion. Something like that :) - we'll use this in just a moment.

So now that we know we need to specify a time to get the actual value, we have a bit of a problem. We don't know what time we're going to request in advance. We can try to use 0 like in our example, but that may well fall outside Fusion's Global Range, which will cause the tool to fail. Ouch. Even something like CurrentTime (try print (CurrentTime) in the Console :)) may fail in tools that do funny stuff with time value parameters (i.e. anything that requests from frames that are not the current frame - retimers come to mind...).

Therefore, for time that is not defined, Fusion provides a special variable which is appropriately named fu.TIME_UNDEFINED. A bit more typing required than 0 for sure, but the extra effort is worth it ;)

However, for putting values into parameters, you don't need to define a time (though if you want to, you can). You can simply do:

PixelShifty.GreenExpression="blah" and it will be done (PixelShifty will fail in this case) or PixelShifty.GreenExpression=1.

All these same things apply for pretty much any parameter, including expression fields. So knowing all this, let's go back to our PixelShifty tool from the previous post, and type the following in the Console:

Code: Select all

print (PixelShifty.GreenExpression[fu.TIME_UNDEFINED])

This is what you should see as a result:

Image

:cheer:

What we are getting back is a string containing our Green Channel expression. In programming, a string is typically a sequence of characters (like letters, numbers and others) that you can do things with. Above, we are printing it out, but we can also type something like:

Code: Select all

green=PixelShifty.GreenExpression[fu.TIME_UNDEFINED]

and now we have created a variable called green which contains a string which is our Green Channel expression (told you this was going to be fun).

It just so happens that in Lua (one of two scripting languages provided by FusionScript, and the one we're using for our expressions in this tutorial) you can do all sorts of nifty things with strings. This is called string manipulation.

Rewind a little bit. In order to get different edge operations in our Custom Tool, all we need to do is change one letter in our expression and then put it back. For that purpose, we're especially interested in this function from Lua's String Library:

Code: Select all

string.gsub(s, pattern, replace [, n])

While that looks complicated if you've never used it, the principle of string.gsub is simple: take a string s, inside it look for a pattern, replace the pattern with something else, optionally do this a maximum number of times.

Try this example:

Code: Select all

mystring = string.gsub("we suck less", "we", "you too")
print (mystring)

From that same documentation, you can also use the function like this:

Code: Select all

s:gsub(pattern, replace [,n])

Take note of the use of : instead of . in that syntax - I've stumbled over this many times when I started scripting.

So you can also do something like:

Code: Select all

mystring = "we suck less"
mystring = mystring:gsub("we", "you too")
print (mystring)

By the way, even though the Fusion Console input box is only a single line high, you can paste multi-line scripts in there, like the above snippet:

Image

There's no size limit. If you want you can do the same with the entire Reactor installer script, for example.

Back to string manipulation. The above example is of course pretty simple. All we do is replace "we" with "you" just once and we're done. That's easy to figure out. But when we look at our blue expression, we immediately see that this is not going to be quite that straightforward:

Code: Select all

getb1b(x-s1, y-s2)

For starters, we won't be able to say "replace b with d" because there are 2 b's in there. But it doesn't stop there. Say we replace b with d. The next time around we will want to replace a d, not a b.

The first part of the answer lies in Character Classes. In Lua, a pattern can be a group of specific characters. There are many (many!) possibilities and I'm still not able to read that entire paragraph of the manual in one go without collapsing into a sobbing heap of misery. So I'm going to tell you right away that the pattern we are after is %a+, which means "get me all the letters, in the largest possible sequence".

If we would run that Character Class through our expression, the "captures" of %a+ would be:

  • getb

  • b

  • x

  • s

  • y

  • s

And look, in that sequence there is a capture that is an isolated b. Which happens to be the one we need to replace with d.

In string.gsub(s, pattern, replace [, n]), replace can be a table, with the capture as the key (or index). This immediately solves a second challenge!

See, once we replace b with d and turn getr1b(x-s1, y-s2) into getr1d(x-s1, y-s2) (for red), we are left with a variation of the same problem. If we want to cycle through all the edge behaviour modes, next up we want to turn getr1d(x-s1, y-s2) into getr1w(x-s1, y-s2). Of course we can brute-force this and come up with something like this:

if we find b, then replace it with d,
else if we find d, then replace it with w,
else if we find w, then replace it with m,
etc...

Basically we want to create a mechanism that cycles through the following pattern:

b -> d -> w -> m -> b...

Now then, because our replace argument can be a table, we can do this in one fell swoop. Look at the following command and run it in the Fusion console in a comp that contains our PixelShifty tool. At the same time, view the Green Channel expression.

Code: Select all

PixelShifty.GreenExpression = PixelShifty.GreenExpression[fu.TIME_UNDEFINED]:gsub("%a+", {["b"] = "d", ["d"] = "w", ["w"] = "m", ["m"] = "b"})

Image

That's it! We've solved our challenge! Really, this was the hard part. Next up is shaping it somewhat, so it's in a half decent state to be used in our tool. In compositing terms, this is the point where our Flow View is that massive spider web of nodes and connections, and now we have to clean it up so it makes sense for someone else (or our future selves) to pick it up later.

First of all, we know we are going to have to apply this to all 4 of our RGBA channels. We can repeat the same expression 4 times over, but that's not great practice. Say you want to adjust it a little bit later, who wants to do that 4 times over every time? Also, the more you repeat in scripting, the more prone to typos (syntax errors) and therefore bugs (eww) your script will be.

So we will write a function for this. A function is like a compositing template. Something you can re-use, you put something into it that goes through a common process, and it will give you something back. Basically we will take our solution from above, and "general-purpose" it. (Oh, and if you want to learn about compositing templates, there's a great topic for you to start with here: viewtopic.php?f=16&t=2447)

Our function will look like this:

Code: Select all

function edgecycle(channel)
	return channel[fu.TIME_UNDEFINED]:gsub("%a+", {["b"] = "d", ["d"] = "w", ["w"] = "m", ["m"] = "b"})
end

A little explanation: I'm defining a function named edgecycle (my name of choice) and I intend to put into my function (in programming terms, my function takes an argument) something named channel (also my name of choice). I'm going to put my Channel Expressions through my function, and when that is done, I will get my Channel Expression back, after I have applied all of my commands to it. So it really is somewhat like running it through a comp template; I'm putting some footage into the Loader, it runs through the nodes that I have set up, and I'm getting my footage adjusted back from the Saver.

Now that I have my function, all I need to do is get all my Channel Expressions from PixelShifty, run them through my function, and put the adjusted ones back into PixelShifty:

Code: Select all

function edgecycle(channel)
	return channel[fu.TIME_UNDEFINED]:gsub("%a+", {["b"] = "d", ["d"] = "w", ["w"] = "m", ["m"] = "b"})
end

r=PixelShifty.RedExpression
g=PixelShifty.GreenExpression
b=PixelShifty.BlueExpression
a=PixelShifty.AlphaExpression
PixelShifty.RedExpression = (edgecycle(r))
PixelShifty.GreenExpression = (edgecycle(g))
PixelShifty.BlueExpression = (edgecycle(b))
PixelShifty.AlphaExpression = (edgecycle(a))

Another little explanation: my function is defined as above. I am now creating 4 variables r, g, b and a which are not the Channel Expressions themselves, but the handles to them (told you we'd get to that). I've put the fu.TIME_UNDEFINED bit inside the function as well, you see, so I don't need to do that 4 times either. After that I'm putting back into the Channel Expressions the 4 variables, processed through my function. Again, I don't need to add fu.TIME_UNDEFINED here either, because as we've seen earlier, that's not required for putting values into a parameter. It all comes together at the end! :)

Go ahead, run the above code block in the Console and look at the Channel Expressions in PixelShifty. All of them now change every time you run the script :cheer:

HANG ON! I hear you say... what if my tool is not called PixelShifty? What if I rename PixelShifty to something else (renaming tools is often excellent practice, you see)... ?

Good question! Up next we are going to put that script in our tool itself. Which basically means it becomes a Tool Script, rather than a Comp Script (in other fancy words, its scope changes). You can read all about that in Fusion's (admittedly sparse) Scripting Guide. In Tool Scripts, you can refer to the tool from which the script is run, by using the variable tool. So as a Tool Script, our script looks like this:

Code: Select all

function edgecycle(channel)
	return channel[fu.TIME_UNDEFINED]:gsub("%a+", {["b"] = "d", ["d"] = "w", ["w"] = "m", ["m"] = "b"})
end

r=tool.RedExpression
g=tool.GreenExpression
b=tool.BlueExpression
a=tool.AlphaExpression
tool.RedExpression = (edgecycle(r))
tool.GreenExpression = (edgecycle(g))
tool.BlueExpression = (edgecycle(b))
tool.AlphaExpression = (edgecycle(a))

And this is where we go back to Fusion and PixelShifty, and put this script to work properly.



!

No more scripting further down... :)


Finally, we're going to add a button to PixelShifty that will run our little script every time we click it. Easy to do with Fusion's Edit Controls... . Here we simply add a button to the Controls tab, and paste the entire script as above in the Execute field:

Image

And there we have it!
(Wait, what, all this for a button?)

I'm adding the result below, with my magnificent Mandelbrot already attached so to see the magic, literally all you need to do is press our freshly added button:

Code: [Select all] [Expand/Collapse] [Download] (WSLsnippet-2019-06-09--23.14.12.setting)
  1. {
  2.     Tools = ordered() {
  3.         CustomTool1LUTIn1 = LUTBezier {
  4.             KeyColorSplines = {
  5.                 [0] = {
  6.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  7.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  8.                 }
  9.             },
  10.             SplineColor = { Red = 204, Green = 0, Blue = 0 }
  11.         },
  12.         CustomTool1LUTIn2 = LUTBezier {
  13.             KeyColorSplines = {
  14.                 [0] = {
  15.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  16.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  17.                 }
  18.             },
  19.             SplineColor = { Red = 0, Green = 204, Blue = 0 }
  20.         },
  21.         CustomTool1LUTIn3 = LUTBezier {
  22.             KeyColorSplines = {
  23.                 [0] = {
  24.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  25.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  26.                 }
  27.             },
  28.             SplineColor = { Red = 0, Green = 0, Blue = 204 }
  29.         },
  30.         CustomTool1LUTIn4 = LUTBezier {
  31.             KeyColorSplines = {
  32.                 [0] = {
  33.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  34.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  35.                 }
  36.             },
  37.             SplineColor = { Red = 204, Green = 204, Blue = 204 }
  38.         },
  39.         CustomTool1LUTIn1_1 = LUTBezier {
  40.             KeyColorSplines = {
  41.                 [0] = {
  42.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  43.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  44.                 }
  45.             },
  46.             SplineColor = { Red = 204, Green = 0, Blue = 0 }
  47.         },
  48.         CustomTool1LUTIn2_1 = LUTBezier {
  49.             KeyColorSplines = {
  50.                 [0] = {
  51.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  52.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  53.                 }
  54.             },
  55.             SplineColor = { Red = 0, Green = 204, Blue = 0 }
  56.         },
  57.         CustomTool1LUTIn3_1 = LUTBezier {
  58.             KeyColorSplines = {
  59.                 [0] = {
  60.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  61.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  62.                 }
  63.             },
  64.             SplineColor = { Red = 0, Green = 0, Blue = 204 }
  65.         },
  66.         CustomTool1LUTIn4_1 = LUTBezier {
  67.             KeyColorSplines = {
  68.                 [0] = {
  69.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  70.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  71.                 }
  72.             },
  73.             SplineColor = { Red = 204, Green = 204, Blue = 204 }
  74.         },
  75.         CustomTool1LUTIn1_2 = LUTBezier {
  76.             KeyColorSplines = {
  77.                 [0] = {
  78.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  79.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  80.                 }
  81.             },
  82.             SplineColor = { Red = 204, Green = 0, Blue = 0 }
  83.         },
  84.         CustomTool1LUTIn2_2 = LUTBezier {
  85.             KeyColorSplines = {
  86.                 [0] = {
  87.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  88.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  89.                 }
  90.             },
  91.             SplineColor = { Red = 0, Green = 204, Blue = 0 }
  92.         },
  93.         CustomTool1LUTIn3_2 = LUTBezier {
  94.             KeyColorSplines = {
  95.                 [0] = {
  96.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  97.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  98.                 }
  99.             },
  100.             SplineColor = { Red = 0, Green = 0, Blue = 204 }
  101.         },
  102.         CustomTool1LUTIn4_2 = LUTBezier {
  103.             KeyColorSplines = {
  104.                 [0] = {
  105.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  106.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  107.                 }
  108.             },
  109.             SplineColor = { Red = 204, Green = 204, Blue = 204 }
  110.         },
  111.         CustomTool1LUTIn1_3 = LUTBezier {
  112.             KeyColorSplines = {
  113.                 [0] = {
  114.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  115.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  116.                 }
  117.             },
  118.             SplineColor = { Red = 204, Green = 0, Blue = 0 }
  119.         },
  120.         CustomTool1LUTIn2_3 = LUTBezier {
  121.             KeyColorSplines = {
  122.                 [0] = {
  123.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  124.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  125.                 }
  126.             },
  127.             SplineColor = { Red = 0, Green = 204, Blue = 0 }
  128.         },
  129.         CustomTool1LUTIn3_3 = LUTBezier {
  130.             KeyColorSplines = {
  131.                 [0] = {
  132.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  133.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  134.                 }
  135.             },
  136.             SplineColor = { Red = 0, Green = 0, Blue = 204 }
  137.         },
  138.         CustomTool1LUTIn4_3 = LUTBezier {
  139.             KeyColorSplines = {
  140.                 [0] = {
  141.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  142.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  143.                 }
  144.             },
  145.             SplineColor = { Red = 204, Green = 204, Blue = 204 }
  146.         },
  147.         Mandelbrot1 = Mandel {
  148.             ViewInfo = OperatorInfo { Pos = { 1627, 75 } },
  149.             Inputs = {
  150.                 YPosition = Input { Value = 0.117 },
  151.                 Rotation = Input { Value = -21.1375067140683 },
  152.                 ["Gamut.SLogVersion"] = Input { Value = FuID { "SLog2" } },
  153.                 Width = Input { Value = 1920 },
  154.                 Height = Input { Value = 1080 },
  155.                 Zoom = Input { Value = 3.434 }
  156.             }
  157.         },
  158.         PixelShifty = Custom {
  159.             UserControls = ordered() {
  160.                 ["Edges_ClickToCycle)"] = {
  161.                     LINKID_DataType = "Number",
  162.                     LINKS_Name = "Edges (Click To Cycle)",
  163.                     INP_Integer = false,
  164.                     INPID_InputControl = "ButtonControl",
  165.                     BTNCS_Execute = "function edgecycle(channel)\n  return channel[fu.TIME_UNDEFINED]:gsub(\"%a+\", {[\"b\"] = \"d\", [\"d\"] = \"w\", [\"w\"] = \"m\", [\"m\"] = \"b\"})\nend\n\nr=tool.RedExpression\ng=tool.GreenExpression\nb=tool.BlueExpression\na=tool.AlphaExpression\ntool.RedExpression = (edgecycle(r))\ntool.GreenExpression = (edgecycle(g))\ntool.BlueExpression = (edgecycle(b))\ntool.AlphaExpression = (edgecycle(a))",
  166.                     ICS_ControlPage = "Controls"
  167.                 }
  168.             },
  169.             CtrlWZoom = false,
  170.             Inputs = {
  171.                 ShowLUT1 = Input { Value = 0 },
  172.                 NameforNumber3 = Input { Value = "Proxy Scale" },
  173.                 NumberIn2 = Input { Value = -400 },
  174.                 LUTIn4 = Input {
  175.                     Source = "Value",
  176.                     SourceOp = "PixelShiftyLUTIn4"
  177.                 },
  178.                 LUTIn1 = Input {
  179.                     Source = "Value",
  180.                     SourceOp = "PixelShiftyLUTIn1"
  181.                 },
  182.                 ShowLUT3 = Input { Value = 0 },
  183.                 GreenExpression = Input { Value = "getg1b(x-s1, y-s2)" },
  184.                 BlueExpression = Input { Value = "getb1b(x-s1, y-s2)" },
  185.                 NameforNumber2 = Input { Value = "Y Pixel Shift" },
  186.                 ShowLUT2 = Input { Value = 0 },
  187.                 RedExpression = Input { Value = "getr1b(x-s1, y-s2)" },
  188.                 NumberIn3 = Input {
  189.                     Expression = "Image1.ProxyScale",
  190.                     Value = 1
  191.                 },
  192.                 ShowPoint3 = Input { Value = 0 },
  193.                 LUTIn2 = Input {
  194.                     Source = "Value",
  195.                     SourceOp = "PixelShiftyLUTIn2"
  196.                 },
  197.                 ShowPoint4 = Input { Value = 0 },
  198.                 AlphaExpression = Input { Value = "geta1b(x-s1, y-s2)" },
  199.                 ShowNumber5 = Input { Value = 0 },
  200.                 NumberIn1 = Input { Value = -500 },
  201.                 ShowNumber3 = Input { Value = 0 },
  202.                 ShowNumber4 = Input { Value = 0 },
  203.                 Setup2 = Input { Value = "n2/h/n3" },
  204.                 ShowNumber7 = Input { Value = 0 },
  205.                 ShowPoint1 = Input { Value = 0 },
  206.                 ShowLUT4 = Input { Value = 0 },
  207.                 ShowNumber6 = Input { Value = 0 },
  208.                 LUTControls = Input { Value = 1 },
  209.                 NumberControls = Input { Value = 1 },
  210.                 LUTIn3 = Input {
  211.                     Source = "Value",
  212.                     SourceOp = "PixelShiftyLUTIn3"
  213.                 },
  214.                 Image1 = Input {
  215.                     Source = "Output",
  216.                     SourceOp = "Mandelbrot1"
  217.                 },
  218.                 ShowNumber8 = Input { Value = 0 },
  219.                 Setup1 = Input { Value = "n1/w/n3" },
  220.                 NameforNumber1 = Input { Value = "X Pixel Shift" },
  221.                 PointControls = Input { Value = 1 },
  222.                 ShowPoint2 = Input { Value = 0 }
  223.             },
  224.             ViewInfo = OperatorInfo { Pos = { 1745, 75 } }
  225.         },
  226.         PixelShiftyLUTIn1 = LUTBezier {
  227.             KeyColorSplines = {
  228.                 [0] = {
  229.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  230.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  231.                 }
  232.             },
  233.             SplineColor = { Red = 204, Green = 0, Blue = 0 }
  234.         },
  235.         PixelShiftyLUTIn2 = LUTBezier {
  236.             KeyColorSplines = {
  237.                 [0] = {
  238.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  239.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  240.                 }
  241.             },
  242.             SplineColor = { Red = 0, Green = 204, Blue = 0 }
  243.         },
  244.         PixelShiftyLUTIn3 = LUTBezier {
  245.             KeyColorSplines = {
  246.                 [0] = {
  247.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  248.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  249.                 }
  250.             },
  251.             SplineColor = { Red = 0, Green = 0, Blue = 204 }
  252.         },
  253.         PixelShiftyLUTIn4 = LUTBezier {
  254.             KeyColorSplines = {
  255.                 [0] = {
  256.                     [0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
  257.                     { 1, Flags = { Linear = true }, LH = { 0.666666666666667, 0.666666666666667 } }
  258.                 }
  259.             },
  260.             SplineColor = { Red = 204, Green = 204, Blue = 204 }
  261.         }
  262.     }
  263. }

A couple of quick closing notes. This is by no means the only way to solve this problem. Or the best. Or even the most elegant. My script may well be more complicated than it needs to be. Apparently, there are always at least four ways to do things in Fusion.

There are certainly things that could be improved in PixelShifty. For starters, our UI isn't exactly terrific, is it? There is currently no way of telling what Edge behaviour the tool is currently set to. Wouldn't it be nice if Fusion had MultiButtonControls that could execute scripts? :)

But here's a challenge for you, then: try to think of a way to solve this, using some of the techniques described in this post. If you find one, consider posting it in this topic.

For polish, our tool can be cleaned up further. It doesn't need to look like a Custom Tool at all. What are all those tabs still doing there? ;)

Or perhaps you can think of more functionality altogether?

I hope you have enjoyed reading it (I've quite enjoyed writing it anyway) and if you've learned something, that's even better. In fact, I have learned a thing or two myself. Funny how you can suddenly realise that your understanding is lacking a bit, once you need to explain it to someone else... :)

Until the next one - good luck, and good night!


User avatar
SirEdric
Fusionator
Posts: 1873
Joined: Tue Aug 05, 2014 10:04 am
Answers: 2
Real name: Eric Westphal
Been thanked: 112 times
Contact:

Re: [Custom Tool] - fun with CT: PixelShifty

#4

Post by SirEdric » Mon May 20, 2019 10:23 am

NIce one, mate!
The icon-joke totally rocks.
(If it only was a joke, rather than bitter reality...:-))


User avatar
Tory
Fusioneer
Posts: 68
Joined: Fri Apr 13, 2018 11:29 am
Real name: Tory Hooton
Been thanked: 6 times
Contact:

Re: [Custom Tool] - fun with CT: PixelShifty

#5

Post by Tory » Tue May 21, 2019 2:49 pm

Yep that was funny :)
At the very least they should "fix" that by adding a number or letter to that default icon so that it can at least be quicker to sort visually ;)

I am learning a lot! looking forward to the next installments of the lesson.


User avatar
SecondMan
Site Admin
Posts: 3489
Joined: Thu Jul 31, 2014 5:31 pm
Answers: 5
Location: Vancouver, Canada
Been thanked: 95 times
Contact:

Re: [Custom Tool] - fun with CT: PixelShifty

#6

Post by SecondMan » Tue May 21, 2019 11:23 pm

Glad you like it! Added part 2 :)


JPDoc
Fusioneer
Posts: 182
Joined: Tue Sep 02, 2014 8:26 am
Been thanked: 1 time

Re: [Custom Tool] - fun with CT: PixelShifty

#7

Post by JPDoc » Wed May 22, 2019 1:51 am

Illuminating stuff, particularly the proxy bits which I had no idea about.


User avatar
Tory
Fusioneer
Posts: 68
Joined: Fri Apr 13, 2018 11:29 am
Real name: Tory Hooton
Been thanked: 6 times
Contact:

Re: [Custom Tool] - fun with CT: PixelShifty

#8

Post by Tory » Wed May 22, 2019 8:12 am

Just went though the second part thanks again... As a newer Fusion user I feel like scripting is going to be a big part of making the most of Fusion and tutorials like this really help to see where to begin!


User avatar
SecondMan
Site Admin
Posts: 3489
Joined: Thu Jul 31, 2014 5:31 pm
Answers: 5
Location: Vancouver, Canada
Been thanked: 95 times
Contact:

Re: [Custom Tool] - fun with CT: PixelShifty

#9

Post by SecondMan » Wed May 22, 2019 3:40 pm

I'm going to split this off into a separate topic, and I'll be right back...

Added in 12 minutes 24 seconds:
There. Did some cleanup and for those wanting to join the icons vs text discussion, it continues here: viewtopic.php?f=16&t=3061

Added in 5 minutes 56 seconds:

Tory wrote:
Wed May 22, 2019 8:12 am

As a newer Fusion user I feel like scripting is going to be a big part of making the most of Fusion and tutorials like this really help to see where to begin!

Thanks! All I hope to do is to make the prospect seem less scary. It's amazing what you can already do if you simply know what you can ask for, or where to look for solutions.

Part 3 will be fun :)


User avatar
SecondMan
Site Admin
Posts: 3489
Joined: Thu Jul 31, 2014 5:31 pm
Answers: 5
Location: Vancouver, Canada
Been thanked: 95 times
Contact:

Re: [Custom Tool] - fun with CT: PixelShifty

#10

Post by SecondMan » Sun Jun 09, 2019 11:45 pm

And Part 3 is finally done!

Enjoy :)

User avatar
SirEdric
Fusionator
Posts: 1873
Joined: Tue Aug 05, 2014 10:04 am
Answers: 2
Real name: Eric Westphal
Been thanked: 112 times
Contact:

Re: [Custom Tool] - fun with CT: PixelShifty

#11

Post by SirEdric » Tue Jun 11, 2019 1:56 am

SecondMan wrote:
Mon May 20, 2019 7:36 am

If you find one, consider posting it in this topic.

Well, another approach would be to use different expressions.
The entire expression field becomes a mess though (and it would be slightly horrible with more complex formulas....),
but after all we now have a multibutton control for the four(!..:-)) modes.

modeSwitch.png

Added in 29 minutes 4 seconds:
Alternatively, using some naughty hackaround, considerably less clutter in the expressions...:-)

modeSwitch2.png
You do not have the required permissions to view the files attached to this post.

User avatar
Tory
Fusioneer
Posts: 68
Joined: Fri Apr 13, 2018 11:29 am
Real name: Tory Hooton
Been thanked: 6 times
Contact:

Re: [Custom Tool] - fun with CT: PixelShifty

#12

Post by Tory » Tue Jun 11, 2019 8:57 am

Thanks so much! I will be coming back to this one for sure :)

Are there more "lessons" like this? This has been a great primer and I can see how this will lead to much more advanced things... For people new to fusion this is what we are looking for, a guided hint from those that know where to focus.

You said it had been awhile since you did something like this... is there a thread that could gather these?


User avatar
SecondMan
Site Admin
Posts: 3489
Joined: Thu Jul 31, 2014 5:31 pm
Answers: 5
Location: Vancouver, Canada
Been thanked: 95 times
Contact:

Re: [Custom Tool] - fun with CT: PixelShifty

#13

Post by SecondMan » Wed Jun 12, 2019 11:25 pm

SirEdric wrote:
Tue Jun 11, 2019 2:25 am
Well, another approach...
Terrific! :)

This is exactly what I was hoping to see happen in this topic :cheer:

You've certainly met the MultiButton challlenge!

I was also sent a third approach, which uses a Frame Render Script to adjust the expressions:

Code: Select all

{
	Tools = ordered() {
		Mandelbrot2 = Mandel {
			Inputs = {
				Width = Input { Value = 1920, },
				Height = Input { Value = 1080, },
				["Gamut.SLogVersion"] = Input { Value = FuID { "SLog2" }, },
			},
			ViewInfo = OperatorInfo { Pos = { 219.333, 152.818 } },
		},
		CustomTool1 = Custom {
			CtrlWZoom = false,
			Inputs = {
				NumberIn1 = Input {
					Value = -600,
					Expression = "XPixelShift",
				},
				NumberIn2 = Input {
					Value = 300,
					Expression = "YPixelShift",
				},
				NumberIn3 = Input {
					Value = 2,
					Expression = "Edges",
				},
				LUTIn1 = Input {
					SourceOp = "CustomTool1LUTIn1_4",
					Source = "Value",
				},
				LUTIn2 = Input {
					SourceOp = "CustomTool1LUTIn2_4",
					Source = "Value",
				},
				LUTIn3 = Input {
					SourceOp = "CustomTool1LUTIn3_4",
					Source = "Value",
				},
				LUTIn4 = Input {
					SourceOp = "CustomTool1LUTIn4_4",
					Source = "Value",
				},
				Setup1 = Input { Value = "1.0/w1", },
				Setup2 = Input { Value = "1.0/h1", },
				Setup8 = Input { Value = "if (n3==1, max (0, min(n1, w1)), if (n3==2, n1-int(n1/w1)*w1, if (n3==3, 1-n1-int(n1/w1)*w1, n1)", },
				RedExpression = Input { Value = "myget(x-n1*s1, y-n2*s2)", },
				GreenExpression = Input { Value = "myget(x-n1*s1, y-n2*s2)", },
				BlueExpression = Input { Value = "myget(x-n1*s1, y-n2*s2)", },
				AlphaExpression = Input { Value = "myget(x-n1*s1, y-n2*s2)", },
				NumberControls = Input { Value = 1, },
				ShowNumber1 = Input { Value = 0, },
				ShowNumber2 = Input { Value = 0, },
				ShowNumber3 = Input { Value = 0, },
				ShowNumber4 = Input { Value = 0, },
				ShowNumber5 = Input { Value = 0, },
				ShowNumber6 = Input { Value = 0, },
				ShowNumber7 = Input { Value = 0, },
				ShowNumber8 = Input { Value = 0, },
				PointControls = Input { Value = 1, },
				ShowPoint1 = Input { Value = 0, },
				ShowPoint2 = Input { Value = 0, },
				ShowPoint3 = Input { Value = 0, },
				ShowPoint4 = Input { Value = 0, },
				LUTControls = Input { Value = 1, },
				ShowLUT1 = Input { Value = 0, },
				ShowLUT2 = Input { Value = 0, },
				ShowLUT3 = Input { Value = 0, },
				ShowLUT4 = Input { Value = 0, },
				Image1 = Input {
					SourceOp = "Mandelbrot2",
					Source = "Output",
				},
				CommentsNest = Input { Value = 0, },
				FrameRenderScript = Input { Value = "mode = ({'b','d','w','m'})[Edges+1]\n\nRedExpression = Text(RedExpression.Value:gsub(\"myget\", \"getr1\"..mode))\nGreenExpression = Text(GreenExpression.Value:gsub(\"myget\", \"getg1\"..mode))\nBlueExpression = Text(BlueExpression.Value:gsub(\"myget\", \"getb1\"..mode))\nAlphaExpression = Text(AlphaExpression.Value:gsub(\"myget\", \"geta1\"..mode))\n", },
				XPixelShift = Input { Value = -600, },
				YPixelShift = Input { Value = 300, },
				Edges = Input { Value = 2, },
			},
			ViewInfo = OperatorInfo { Pos = { 329.333, 151.818 } },
			UserControls = ordered() {
				XPixelShift = {
					INP_MaxAllowed = 1000000,
					INP_Integer = true,
					ICS_ControlPage = "Controls",
					INP_MaxScale = 100,
					INP_MinScale = 0,
					INP_MinAllowed = -1000000,
					LINKID_DataType = "Number",
					INPID_InputControl = "ScrewControl",
					LINKS_Name = "X Pixel Shift"
				},
				YPixelShift = {
					INP_MaxAllowed = 1000000,
					INP_Integer = true,
					ICS_ControlPage = "Controls",
					INP_MaxScale = 100,
					INP_MinScale = 0,
					INP_MinAllowed = -1000000,
					LINKID_DataType = "Number",
					INPID_InputControl = "ScrewControl",
					LINKS_Name = "Y Pixel Shift"
				},
				Edges = {
					{ MBTNC_AddButton = "Zero" },
					{ MBTNC_AddButton = "Edge" },
					{ MBTNC_AddButton = "Wrap" },
					{ MBTNC_AddButton = "Mirror" },
					INP_MaxAllowed = 1000000,
					INP_Integer = true,
					INPID_InputControl = "MultiButtonControl",
					MBTNC_ShowBasicButton = true,
					INP_MaxScale = 1,
					ICS_ControlPage = "Controls",
					INP_MinScale = 0,
					INP_MinAllowed = -1000000,
					LINKID_DataType = "Number",
					MBTNC_ShowName = true,
					MBTNC_StretchToFit = true,
					MBTNC_ShowToolTip = true,
					LINKS_Name = "Edges"
				}
			}
		},
		CustomTool1LUTIn1_4 = LUTBezier {
			KeyColorSplines = {
				[0] = {
					[0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
					[1] = { 1, LH = { 0.666666666666667, 0.666666666666667 }, Flags = { Linear = true } }
				}
			},
			SplineColor = { Red = 204, Green = 0, Blue = 0 },
		},
		CustomTool1LUTIn2_4 = LUTBezier {
			KeyColorSplines = {
				[0] = {
					[0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
					[1] = { 1, LH = { 0.666666666666667, 0.666666666666667 }, Flags = { Linear = true } }
				}
			},
			SplineColor = { Red = 0, Green = 204, Blue = 0 },
		},
		CustomTool1LUTIn3_4 = LUTBezier {
			KeyColorSplines = {
				[0] = {
					[0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
					[1] = { 1, LH = { 0.666666666666667, 0.666666666666667 }, Flags = { Linear = true } }
				}
			},
			SplineColor = { Red = 0, Green = 0, Blue = 204 },
		},
		CustomTool1LUTIn4_4 = LUTBezier {
			KeyColorSplines = {
				[0] = {
					[0] = { 0, RH = { 0.333333333333333, 0.333333333333333 }, Flags = { Linear = true } },
					[1] = { 1, LH = { 0.666666666666667, 0.666666666666667 }, Flags = { Linear = true } }
				}
			},
			SplineColor = { Red = 204, Green = 204, Blue = 204 },
		}
	}
}
We should run a benchmark next, just out of interest and love for science and discovery... :)

User avatar
SecondMan
Site Admin
Posts: 3489
Joined: Thu Jul 31, 2014 5:31 pm
Answers: 5
Location: Vancouver, Canada
Been thanked: 95 times
Contact:

Re: [Custom Tool] - fun with CT: PixelShifty

#14

Post by SecondMan » Wed Jun 12, 2019 11:57 pm

Tory wrote:
Tue Jun 11, 2019 8:57 am
Thanks so much!

Are there more "lessons" like this? [...] is there a thread that could gather these?
You're welcome :)

There certainly could be... I haven't done many in this "style" yet but it's a format I'm really starting to like. The previous one I did recently was about the Displace node, and specifically the XY Type, which I would say is incredibly useful to master, do check it out. A couple more Custom Tool tricks in there that are good to know...

The forum is full of incredible goodies if you look for them. Start with just browsing, really. You'll come across very detailed topics about a wide variety of subjects. Some you'll find touch more on scripting, like @AndrewHazelden's UI Manager topic (it's massive), or @Midgardsormr's excellent collaborative learning topic for building a version control script. Just to name a couple. People like @SirEdric and @ShadowMaker SdR (and others! many others!) are constantly lurking for fresh questions, too :)

I could spend days just summing up. And that's also a challenge. Centralising all that has been on my to-do list ever since the forum started and there have been/are a couple of ideas in progress. It's not a question of if, really, but of when. But the when simply depends on how much time I manage to liberate. It's also a bit of a choice - do I make another one of these "lessons" or do I trawl the forum, indexing goodies? The other day I was fighting spam :cry:

That said, it's a forum, and it's entirely public. I'm certainly not going to stop anyone from starting such a thread themselves ;)