Scripting Tips and Tricks

From Filter Forge Wiki

Jump to: navigation, search

This article covers various scripting tips and tricks. The subjects covered here will both help you make more optimal scripts and gain insight in basic scripting/programming techniques.

Note that optimizations and tips for better performance mentioned in the article might not be applied in all script examples in the article. The examples are created to illustrate the given subject and no more.

Please direct questions and requests to this forum discussion.

Feel free to add/correct things :)

-- Sphinx.

Contents

Preparation and Setup

This section covers some basic steps for optimal script construction and general advices regarding scripting. While most programmers might not find anything new here, it covers subjects that are not that obvious to beginners.

"To script or not to script"

When you start to get hold of scripting in Filter Forge, you'll quickly notice that the time to code a simple script is often shorter than the time used to manually drag in components and do the setup. However there is good reason not to do scripts for simple constructions: Speed!

By default the scripting functionality itself costs additional processing time, even though the script is as simple as it gets (e.g. return 1,1,1,1). I assume this is because Filter Forge need to check things (is the output valid etc) and do various conversions. Moreover the executable code generated by Lua is not as optimal as the dedicated implementations of the specialized Filter Forge components. A non-script component based construction will almost always be faster than its scripted alternative.


Invert comparison

Compared to the build-in Filter Forge Invert, a script version takes four times as long to render, and this was with a very optimal script construction (see last script snippet in Conditional get_sample setup).


Minimize no. of separate scripts

I started chaining build-in Inverts until I reached the same processing time as the single scripted Invert.. ~18 Inverts! This illustrates the low overhead of adding individual non-script components. Repeating the actual invert code inside the script 18 times did not add much overhead though. The conclusion here is that when you already entered the scripting context of a single script component, the script execution is pretty fast. Instead of introducing several separate script components (each with their function), try to stick to one and use functions etc to structure it.


Consider a non-script alternative

If you, like me, find it easier to prototype ideas via scripting, do so, but also consider if a non-script alternative is feasible. Non-script versions are likely to be easier to understand, faster and more in line with the overall visual modular design concept of Filter Forge. There are cases though where the non-script version requires way too much fiddling and end up being quite complex monsters. At this point it very much a matter of personal taste and preferences.

Where to put the script parts

When you create a new script, you see two functions already entered, function prepare and function get_sample. These two functions are essential for the script to work, but moreover they each have a specific purpose: prepare is called once per rendering and get_sample is called for each output sample (600 x 600 x Anti-alias sample count for default image). Obviously thats a pretty significant difference, and if the script should run as optimal as possible, it is important that the script parts are sorted correctly here.

In the article Referencing Inputs you can read more about which input types you should set up in the prepare function.

A rule of thumb: input types where you only can connect Controls (the gray ones) should be set up in function prepare. And calculations that only depends on values from these (or your own number constants), should also take place in prepare. Here is a don't/do example:

Not optimal:

function prepare()
	value = get_slider_input(VALUE)
end;

function get_sample(x, y)
	x = x * math.cos(value * 2 * math.pi)
	return x,x,x,1
end;

Optimal:

function prepare()
	value = get_slider_input(VALUE)
	value = math.cos(value * 2 * math.pi)
end;

function get_sample(x, y)
	x = x * value
	return x,x,x,1
end;

Since the calculation is not depending on x, y or any map input value, it stays constant for the whole rendering, and we don't need to calculate it more than once (namely in prepare). Even that simple a change will make a big difference in rendering time.

Creating an initialization section

EDIT: Changes in the FF architecture may prevent this kind of initialization to work properly. See [1] for more details.

(this section requires that you know about if..then constructions)

Sometimes it is not possible to do all precalculations in function prepare. The typical scenario is that you want to use your map or curve inputs to prepare values for your main get_sample script. Since you can't use get_sample_map, get_sample_grayscale and get_sample_curve in the prepare function, you need to set up an initialization step in your get_sample function.

Here is how you set up an initialization section in an optimal manner:

function prepare()
	-- indicate that we need to initialize
	INITIALIZE = true
end;

function get_sample(x, y)
	-- Initialization section
	if INITIALIZE then
		-- get color at (0.5, 0.5)
		cr, cg, cb = get_sample_map(0.5, 0.5, SOURCE)		
		--indicate that we're done:
		INITIALIZE = false
	end

	-- add color from (0.5, 0.5) to current color
	local r,g,b,a = get_sample_map(x, y, SOURCE)
	r,g,b = r + cr, g + cg, b + cb

	return r, g, b, a
end;

In prepare we set variable INITIALIZE to true, and the first time get_sample is called we enter the initialization section because the variable, INITIALIZE, is true. We do what we need to do, and set INITIALIZE to false. This way the initialization section is never called again during rendering. If we did not add this check, we would call get_sample_map(0.5, 0.5, SOURCE) for every sample in the rendering, which is not very smart considering that it will always return the same color (from the coordinate 0.5, 0.5)

The script above is tailored for explanatory purpose. A real world example could be to search through a given number of samples for minimum and maximum values and then use these to adjust levels.

Conditional get_sample setup

(this section requires that you know about if..then constructions)

In many cases, the script parts in your get_sample function relies directly on constant settings from gray inputs (for example a check box). You could set this up using a conditional (if..then) check in your get_sample, something like this:

function prepare()
	-- get the checkbox settings as a boolean (true or false)
	doInvert = (1 == get_checkbox_input(INVERT))
end;

function get_sample(x, y)
	local r,g,b,a = get_sample_map(x, y, SOURCE)
	if doInvert then
		--invert red, green and blue channels
		r = 1 - r
		g = 1 - g
		b = 1 - b
	end
	return r, g, b, a
end;

The if doInvert then.. check is a fast check, but it is also a check that will have the same outcome for all sample renderings, because the INVERT checkbox stays the same throughout the rendering. This tells us that there must be a more optimal setup:

-- very important, must be located here:
-- link to function that corresponds to initial checkbox settings (checked)
get_sample = get_sample_invert

function prepare()
	-- get the checkbox settings as a boolean (true or false)
	doInvert = (1 == get_checkbox_input(INVERT))

	if doInvert then
		-- link get_sample to our special invert function
		get_sample = get_sample_invert
	else
		-- use a "dummy" function that simply passes through the sample
		get_sample = get_sample_dummy
	end
end;

function get_sample_invert(x, y)
	local r,g,b,a = get_sample_map(x, y, SOURCE)
	--invert red, green and blue channels
	r = 1 - r
	g = 1 - g
	b = 1 - b
	return r, g, b, a
end;

function get_sample_dummy(x, y)
	return get_sample_map(x, y, SOURCE)
end;

Now this is pretty neat! We can actually treat the get_sample function as a variable or reference. Notice the first line after the comments: get_sample = get_sample_invert. Here get_sample is linked to a function, get_sample_invert. This means that when Filter Forge internally calls get_sample it will actually call get_sample_invert. We need to do this initial setup, otherwise FF thinks that the script is invalid (it can't find get_sample)

In the prepare function we look at the INVERT checkbox settings: if it is checked, we link get_sample to our own get_sample_invert, if it is not checked we link it to get_sample_dummy. This get_sample_dummy function is very simple and fast, since it just returns our SOURCE input directly.

While this INVERT example in general is very simple and fast enough to be acceptable even with a less efficient conditional check inside get_sample (as in first example), this may not always be the case (if you have a much heavier script). Also the method can be extended to many different functions, based on input settings, so it is definitely worth paying attention to.

Using Tables

Sometimes you need to prepare multiple values in a systematic manner. This can be random values, offset values etc.; Stuff you can calculate in function prepare which you need in the get_sample function. Tables are excellent for that purpose. Tables in Lua are similar to general array concepts in other languages.

Basically a table is a variable that can hold several other variables:

	-- normal seperate variables:
	x1 = 2
	x2 = 4
	x3 = 8

	-- a table with the same values
	myTable = {2,4,8}

	-- how to get values from table:
	p = myTable[1] -- p = 2
	p = myTable[3] -- p = 8

	-- how to set a value in the table
	myTable[2] = 5

Now the smart thing about tables is that we can add, change and delete its content via scripting - we don't have to type the assignment for each entry as with normal variables (like x1, x2 and x3 above):

	-- set up the table variable, empty for now
	myTable = {}

	-- fill the table with a loop
	for i = 1, 3 do
		myTable[i] = 2 ^ i
	end

Now the values in the table are calculated in a loop. Doing this for three simple values like above is a bit overkill, but as soon as you need to set up values via Inputs (for example an Int Slider), you'll get the point:

function prepare()
	local loop_count = get_intslider_input(LOOP_COUNT)
	-- set up the table variable, empty for now
	myTable = {}

	-- fill the table with a loop
	for i = 1, loop_count do
		myTable[i] = 2 ^ i
	end	
end;

At this point we don't know what the loop count is, because it is something the user will set via the int slider input. We can now use the values in myTable in the get_sample function.

Tables can hold any type of variable, also other tables or function references (and even direct function code). Moreover Lua has some useful functions to work with tables, like sorting. Check that out here.

Low-level Performance and Math tricks

This section covers minor tricks that will help you create more optimal scripts.

Localize variables

A variable in Lua can either be local or global (or a table field, but forget that for now). This refers to the variables scope or visibility in the script. Here is a simple explanatory script:

function prepare()
	local aLocalVariable = 3.14
	aGlobalVariable = aLocalVariable
end;

function get_sample(x, y)
	local r = aGlobalVariable
	local g = aGlobalVariable
	local b = aGlobalVariable
	local a = 1
	return r, g, b, a
end;

The first line in function prepare is a local variable called aLocalVariable. When we add local in front of a variable name we state that it is only to be used inside the containing script block (in this case function prepare). If we tried to use in in function get_sample Filter Forge would throw a script error. When we simply write a variable name, it is automatically understood as global, thus aGlobalVariable can be used in function get_sample.

You might think "why not just use global variables all the time?" - The answer has two sides. First of all you might run into problems with common variable names, like for example x and y when you have complex scripts. It would be quite annoying if you could only use a name once in the whole script context. Secondly, on a low level there is a difference in how Lua treats local and global variables. The details here are pretty boring, so lets skip on to the important part:

When variables are local, Lua can optimize the code it generates from your script. So use local declarations whenever possible.

Be aware that conditionals, loops amongst other code structures will create a new scope level:

local v = 2

if state == true then
  local v = 1
end

return v

No matter if we enter the conditional (and set local v = 1) or not, the value returned here is 2. This is because the conditional is a new script block or scope level.


"Localizing" Global Variables

Consider this (useless) script:

function prepare()
	PI = math.pi
end;

function get_sample(x, y)
	local local_PI = PI
	local v = local_PI * local_PI * local_PI
	return v, v, v, 1
end; 

In get_sample we start out by "loading" the global variable PI into a local variable. This will be faster than referencing the global PI variable several times, i.e. local v = PI * PI * PI, because it allows Lua to do its magic.


"local" and readability

When you have that local prefix sitting in front of most of your variables, the script becomes less readable. Here is a simple trick to get around that:


-- "unreadable"
function something(x,y,z)
	local p = x + y
	local q = y + z
	local r = z + x
	
	local pgr = p * q * r

	return pgr
end;

-- "readable"
function something(x,y,z)
	local p, q, r, pgr

	p = x + y
	q = y + z
	r = z + x
	
	pgr = p * q * r

	return pgr
end;

Nice! One line that declares local status for all of them. As long as you are in the same scope, this will work just fine.

Conditional execution

A conditional expression is probably one of the most fundamental code constructions. In Lua it can look like this (you can actually read it out loud):

	if a > b then
		c = a
	else
		c = b
	end

This simple little construction will find the greater value of a and b and assign that to c. You might wonder how it figures out when b > a, since it only checks if a > b.. well since both cannot be higher than the other (non-sense!), we can safely conclude that if not a is greater than b, b must be greater or they are equal. If they are equal it does not matter which we those.

Now the beauty of these structures is that we can use them to control which part of the script is executed based on e.g. input settings:

	if index == 1 then
		r, g, b, a = get_sample_map(x, y, SOURCE_1)	
	elseif index == 2 then
		r, g, b, a = get_sample_map(x, y, SOURCE_2)	
	else
		-- index must be 3 or something else
		r, g, b, a = get_sample_map(x, y, SOURCE_3)	
	end

In the above example (part of a 3 Position Switch) we only call one of the sources, because the if..then structure will simply skip the cases where index don't equal the value we check for.

Using such control structures will not only make sure we get the correct result, but also "optimize" the script in the sense that it will not execute the cases that are not true. Here is a poorly coded version of the above script:

	r1, g1, b1, a1 = get_sample_map(x, y, SOURCE_1)	
	r2, g2, b2, a2 = get_sample_map(x, y, SOURCE_2)	
	r3, g3, b3, a3 = get_sample_map(x, y, SOURCE_3)	

	if index == 1 then
		r, g, b, a = r1, g1, b1, a1
	elseif index == 2 then
		r, g, b, a = r2, g2, b2, a2
	else
		-- index must be 3 or something else
		r, g, b, a = r3, g3, b3, a3
	end

Obviously this is a bad idea since get_sample_map calls can be very expensive (depending on what the user hooked up to them - it could be the rendering tree of a whole planetary system for what you know).

Operators and Performance

Operations like addition, subtraction and trigonometric routines (sin, cos etc.) all take time to calculate. Some are slower than others. Here is a preliminary speed rating list of some of the common operations.

Fast:

  • Addition (+)
  • Subtraction and Negation (-)
  • Multiplication (*)
  • Compare (>, >=, <=, <=)
  • Logical operators (and, or, not)

Medium:

  • Division (/)
  • Modulus (%)

Slow

  • Exponentiation (^)
  • All math.() routines

Avoid unnecessary division

Computers are in general much faster at doing multiplication than division. The difference in standard Lua is not that great, but it is still there (LuaJIT would show a greater difference). Often you have to divide several values by the same value (for example for each r,g,b channel), and in such cases you can reduce the number of divisions by doing reciprocal multiplication, which is not as tough as it might sound. Consider the following example:

6 / 2 = 3

..which is exactly the same as..

6 * 0.5 = 3

..only it uses multiplication instead.

Now it might not be straight forward "thinking reciprocal", but we can always find the value needed by dividing 1 with the divisor:

1 / 2 = 0.5

Here are some Lua examples:


	-- using division:
	r = r / x
	g = g / x
	b = b / x

	-- using reciprocal multiplication:
	rx = 1 / x

	r = r * rx
	g = g * rx
	b = b * rx

	-- BAD: division by constant in loop:
	for i = 1, 100 do
	  v = v + v / x
	end

	-- getting the division out of the loop
	rx = 1 / x
	for i = 1, 100 do
	  v = v + v * rx
	end

The loop example is worth taking notice of: it exchanges 100 divisions with 100 multiplications (and one division). In general pay attention to operations going on in a loop, often you can "precalculate" or prepare variables outside the loop.

Common Operations

The following section contains small script snippets of various operations related to image processing. Many of them are equivalent to the functionality of the build-in Filter Forge components. They are shown here for informative reasons, so you can use them as a part of your script - you should never seriously use a script version of something that can be done with another Filter Forge component as it will be slower.

Note that local declarations and other optimizations are left out here (so they don't occlude the purpose of the examples).

Invert

Inverting a value is very easy. To invert a value we must know the range or absolute span of possible values. This could be the range 0..1, which in fact is how Filter Forge define visible ranges for r,g,b,a and grayscale samples (0 = lowest visible intensity, 1 = brightest visible intensity). The invert operation flips the range so that e.g. bright becomes dark and visa versa.

Here is how it can be done:

	-- invert operation, assumed range: 0..1

	-- grayscale
	value = get_sample_grayscale(x,y,SOURCE_GRAYSCALE)
	inverted_value = 1 - value

	-- color (r,g,b)
	r,g,b = get_sample_map(x,y,SOURCE_COLOR)
	inverted_r = 1 - r
	inverted_g = 1 - g
	inverted_b = 1 - b

Simple stuff, but something you'll get to use often for intermediate calculations and such. Note that Invert only makes sense when you have a known fixed range. Theoretically you can't invert a HDR image without first doing a search for minimum and maximum values (which then defines the range).

Desaturate

Colors appear when there is difference between the r,g,b values. So if we want to create a grayscale or desaturated version of a color, we need to clear out the difference between the channels, i.e. ensure that they all contain the same value. This value can be calculated in various ways (note that I put several examples into the following script snippet, you should only use one of the methods):

	
-- Simple Average

	grayscale = (r + g + b) / 3

-- Weighted Average (weights from BT709)

	red_weight   = 0.2125
	green_weight = 0.7154
	blue_weight  = 0.0721
	grayscale = r * red_weight + g * green_weight + b * blue_weight

-- MinMax based variants (use one of the three grayscale lines)

	min_rgb = math.min(r,g,b)
	max_rgb = math.max(r,g,b)
	grayscale = max_rgb -- "brightness"
	grayscale = min_rgb -- "darkness"
	grayscale = (min_rgb + max_rgb) / 2 -- "lightness"

-- Distance from zero

	grayscale = math.sqrt(r^2 + g^2 + b^2) / math.sqrt(3)

-- Finally: put the grayscale value in r,g,b channels

	r = grayscale
	g = grayscale
	b = grayscale

There are other methods too, and in particular the weighted average approach is subject to discussion. The idea behind that solution is that red, green and blue color do not have the same visual intensity (we see red as brighter than blue, and green as brighter than red), so the weights try to reflect that idea. If you do a search on weighted average desaturation you will find many different weights and theories behind them. The important thing is that they sum up to 1, i.e. (red_weight + green_weight + blue_weight) = 1, otherwise you will not be able to get full brightness, or end up with a result > 1.

Also for a particular image one weight scheme might look better than another - you could make customized version that lets the user adjust the weights via sliders :)

Threshold

The the basic threshold operation is very simple. Assuming that source and threshold are grayscale, the operation consist of a simple conditional construction, where we check if the source value is less than the threshold value:

	if (source < threshold) then
		--low
		result = 0 -- "black"
	else
		--high
		result = 1 -- "white"
	end

The Threshold in Filter Forge is more complex than that though. First of all we get to map the input and output:

	source = get_sample_grayscale(x, y, SOURCE)
	threshold = get_sample_grayscale(x, y, THRESHOLD)

	if (source < threshold) then
		--low
		r,g,b,a = get_sample_map(x, y, LOW)
	else
		--high
		r,g,b,a = get_sample_map(x, y, HIGH)
	end

	return r,g,b,a

Secondly we have the Smooth parameter which is changing the fundamental construction of the Threshold operation, making it work a bit like a reverse contrast operation. We will not be covering the Smooth parameter here, as it really changing the basic threshold operation into something else.

Posterize

The Posterize operation reduces the steps in the visible range, as if we reduced the bitdepth of an image. The Posterize effect is a bit similar to using the Stairs curve in Filter Forge, only much simpler.

To make this work, we need to use the auxiliary math library in Lua (it is build into Filter Filter, no worries). The library contains many useful functions, like floor, ceil, sin and cos. We're interested in the floor function here.

The floor function removes the fractional part by rounding downward: math.floor(1.7) returns 1.0, and math.floor(-1.7) returns -2.0 (FYI the ceil works the other way around)

	-- steps is an integer > 1

	-- Photoshop style:
	posterized = math.floor(source * steps) / (steps - 1)

	-- Variant, more Stairs like:
	posterized = math.floor(source * steps + 0.5) / steps

So what is going on here? Before we call the floor function we multiply the source by the number of steps (to get the integers), then we divide the result again afterwards to get a proper range (e.g. 0..1). The reason why we divide with steps - 1 is that we want the result to have a full span from 0..1.

In the variant we add a constant instead, and this way get a full span, but this also creates a visual offset (the outermost steps or bands will be half the size of the inner steps, like with the Stairs curve).

The RGB Math Floor component will do something similar to this, only you have to tweak the Granularity parameter to get different step settings.

Contrast & Brightness

(under construction)

	-- brightness adjustment, a simple offset
	source = source + (brightness - 0.5)

	if contrast < 1 then
		-- temporary range change: 0..1 -> -1..1, 0 being gray
		source = source * 2 - 1
		
		-- contrast adjustment
		result = (source / (1 - contrast)) * contrast

		-- back to the 0..1 range
		result = result * 0.5 + 0.5
	else
		-- full contrast, avoid division by zero
		-- use simple threshold:
		if source > 0.5 then
			result = 1
		else
			result = 0
		end
	end

Positioning (Offset)

This operation is really simple. Instead of doing calculations on the samples, we change the coordinates and this way receive samples from another location than the x,y input. I assume you are familiar with Filter Forge's Sample-based Architecture - if not, read up immediately ;)

Consider the following very simple script

function get_sample(x, y)
	return get_sample_map(x, y, SOURCE)
end

Here we directly return the result of a Map input (SOURCE) - the script does nothing at all. Now if we want to return a sample from another location than what is requested (via the x,y parameters in function get_sample(x, y)), we need to modify the x, y values we pass on to get_sample_map(x, y, SOURCE).

Like this for example:

function get_sample(x, y)
	return get_sample_map(x + 0.5, y + 0.5, SOURCE)
end

Now we added a constant offset of 0.5, which would result in a rendering where the center is displaced to the corners. You can exchange the constant offset with mapped values (like the Offset component in Filter Forge 2):

function get_sample(x, y)
	offset_x = get_sample_grayscale(x, y, OFFSET_X)
	offset_y = get_sample_grayscale(x, y, OFFSET_Y)

	return get_sample_map(x + offset_x, y + offset_y, SOURCE)
end

If you need to offset the coordinates by the span of one output pixel exactly, use the special variable SIZE (See Scripting API) to find the value:

function get_sample(x, y)
	one_pixel = 1 / SIZE
	return get_sample_map(x + one_pixel, y + one_pixel, SOURCE)
end

Mix/Lerp (Linear Interpolation)

This very basic operation, technically called linear interpolation, is something you probably will need often, so its a good trick to be familiar with.

For example if you want to tweak how much influence your filter/effect script thingie has, this is useful. It is similar to the Lerp component in Filter Forge, and to some degree also the Blend (if we disregard alpha channels and blend modes).

Let's take a look at the math:

	-- working range: 0..1
	balance = 0.7

	-- standard "notation":
	mixed_result = source_b * balance + source_a * (1 - balance)

	-- more optimal and shorter version
	mixed_result = source_a + (source_b - source_a) * balance

Pretty simple: balance (also called "weight", "factor" or "opacity" in other contexts) controls how much of the two sources is present in the mixed result. This is done by multiplying one source with with balance and the other with inverted balance and then adding them.

The more optimal one can be harder to understand, but it is a little faster, and the result is identical with the other approach. Consider a - a * w where w is in range 0..1, here it is pretty obvious what happens when w = 1: a - a * 1 = 0, a gets negated out. So add in the other source variable, a + (b - a) * w, and what you have left when a is negated out, is b, and when w is 0 the bracket part has no influence at all.

(that was a non mathematical attempt at explaining things, not sure I succeeded :-D )

Further reading

Filter Forge Forum Discussions

Filter Forge Help

General Lua Links

Personal tools