Scripting language reference

From PopcornFX
Jump to navigation Jump to search

This page goes through the main syntactic elements of PopcornFX scripts.

For the full particle-script reference, see the Scripting reference page.


You can add comments anywhere in a script. There are two ways to write comments, just as C++ or High-level shader languages:

  • by preceding them with a double forward slash: // the whole rest of the line will be treated as a comment.
  • by embedding the comment in a /* and */ pair


some_statement; // comment
something /* comment */ something_else;


scalars and vectors

numeric values in popcorn-script are vectors from 1 to 4 dimensions.

'vectors' are basically a list of values, here are a few concrete examples:

  • an RGBA color is a 4-dimensional vector, containing 4 components: red, green, blue, and alpha.
  • a position in 3D space is a 3-dimensional vector, containing the 3 X, Y, and Z coordinates.
  • an UV Texture coordinate is a 2-dimensional vector.
  • a life duration in seconds is a 1-dimensional vector, containing a single value: the life duration.

1-dimensional vectors are also called 'scalars'. they are a simple, single number.


all available integers are signed 32 bits only, except for the 'byte' type, which is a simple 8-bit unsigned value.

int, int2, int3, int4

'int' largest value : 2147483647
'int' smallest value : -2147483648

'byte' largest value : 255
'byte' smallest value : 0

'int', 'int2', 'int3', 'int4' are signed integers vectors of dimensions 1, 2, 3, and 4 respectively.
for simplicity reasons, and to avoid silent signed/unsigned pitfalls, there are no unsigned versions of the main integer vectors.
be careful with sign conversions if for some reason you find the need to use the 'byte' type with integer vectors.

You can directly create values of each type with the following syntax:

3			// immediate int value
int(3)			// immediate int value, same as above
int2(3, 4)		// immediate int2 value
int3(3, 4, 5)		// immediate int3 value
int4(3, 4, 5, 6)	// immediate int4 value

floating point numbers

also called "floats" all available floating point numbers are 32 bits only.

float, float2, float3, float4

'float' largest value: +- 3.4028234663852886e+38
'float' smallest value: +- 1.1754943508222875e-38

You can directly create values of each type with the following syntax:

1.4				// immediate float value
float(1.4)			// immediate float value, same as above
float2(1.4, 2.5)		// immediate float2 value
float3(1.4, 2.5, 3.6)		// immediate float3 value
float4(1.4, 2.5, 3.6, 4.7)	// immediate float4 value

note that floating point numbers can be followed by an optional 'f', like so:


This is just a habit coming from C/C++ where there are different floating point types, it doesn't make any concrete difference on the script compiler's point of view.

However, we might add native halfs support in a future version (16-bit floats mainly used by graphics hardware), so the float format specification might become useful.

type promotion

popcorn-script does implicit type promotion.

This means that a value of a given type combined with a value of another type through a mathematical operation will lead to a value of a third type, that might or might not be equal to one of the first two types.

  • floats combined with integers always leads to floats.
  • vectors combined with scalars will always lead to vectors whose dimension is equal to that of the original vector.

For example, if we consider the following expression:

123.4 + int3(5,6,7)
  • the float <-> integer operation will lead to floats
  • the scalar <-> vector3 operation will lead to a vector3

this expression will therefore give the following result:

float3(128.4, 129.4, 130.4)

this automatic type promotion also allows to construct float vectors from integers, without the need to worry about the decimal point:

float2(42, 69)

will be understood by the popcorn-script compiler as:

float2(42.0, 69.0)

Local variables


You can declare local variables in the following ways:

type name;
type name = value;
type name(arguments);

the first form declares the variable without assigning anything to it. its contents are undefined, and you will have to initialize it to something before using it in a computation.
the second form contains the initial assignment directly on the same line as the declaration.
the third form is an immediate initialization constructor. you use it the same way as immediate vector creation:

as you'd write:


to create the vector { 1.0, 2.0, 3.0 }, you would write:

float3	myVar(1,2,3);

to create a variable named "myVar" of type float3, containing the 3 values { 1.0, 2.0, 3.0 }

The following are all equivalent:

float3	myVar;
myVar = float3(1,2,3);
float3	myVar = float3(1,2,3);
float3	myVar(1,2,3);


scopes can be seen as "blocks" of script, delimited by curly braces. they can be nested inside each other, and have an arbitrary depth:

	// we are in the first scope
		// we are in the second scope

	// we are back in the first scope
		// we are in the third scope 

	// we are back in the first scope

Local variables are active and can be accessed in the current scope and all its child scopes.
You cannot access a local variable once it is said to have gone "out of scope", that is, once its containing scope has been closed:

	int	a;
	float	b;
	// can access a and b

		int c;
		// can access a, b, c

		int d;
		// can access a, b, c, d

			float e;
			// can access a, b, c, d, e
		// can access a, b, c, d

		float f;
		// can access a, b, c, d, f
	// can access a, b

Variables can be declared anywhere within a scope, but they must not have the same name as another variable inside the same scope:

	int	a;
	int	a;	// error
	float	b;
	int	b;	// error

variables in child scopes can have the same name as a variable in a parent scope, and if they do, they "hide" the parent variable in their scope, and all the child scopes:

{ // scope1
	int	a;
	// we have access to a in scope1
	{ // scope2
		int	b;
		// we have access to a in scope1, and b in scope2
		{ // scope3
			int	a;	// overrides a in scope1
			// we have access to b in scope2, and a in scope3
				// we have access to b in scope2, and a in scope3
			// we have access to b in scope2, and a in scope3
		// we have access to a in scope1, and b in scope2
	// we have access to a in scope1

Vector scalar access and shuffling

Most builtin math functions and operators fully handle native vector types, as well as scalars. However, it is sometimes necessary to access individual components of a vector.
In order to do so, vector types expose 4 accessors: 'x', 'y', 'z', and 'w', one for each of the 4 respective dimensions.
trying to access a dimension that goes beyond the vector's dimension count will result in a compile error (ex: accessing the 'z' component of a 2 dimensional vector is invalid. only 'x' and 'y' are available).

float3	vec3 = float3(0.1, 1.42, 100);
float	xValue = vec3.x;
float	yValue = vec3.y;
float	zValue = vec3.z;
float	wValue = vec3.w;	// error: 'w' is not defined for 3-dimensional vectors

(shuffling is also called vector swizzling)

The individual components of builtin native vector types can be accessed and swizzled around freely, allowing implicit generation of another vector, possibly of different dimension.

swizzles are built using a combination of their respective scalar member accessors 'x', 'y', 'z', or 'w', if valid with respect to the vector dimension (for example, the 'w' accessor won't be available in a 3 dimensional vector).

if we have:

int4	vec4(5,6,7,8);
float3	vec3(0.5,0.6,0.7);
int2	vec2(0);
int	vec(1);

we can write:

int2	a = vec4.xz;
int4	b = vec4.xxzy;
int4	c = vec.xxxx;
float2	d = vec3.zx;
int3	e = vec2.xyx;
float2	f = vec3.wx;	// error: 'w' isn't valid considering 'shuf0' is only a float3.
float3	g = float3(vec3.xy, vec4.w);
float2	h = float3(vec4.x, vec3.z);

In addition to the x, y, z, and w swizzle codes, you can also use '0' and '1' to easily insert zeroes or ones in the final result:

float2(5,6).x01y   -->   float4(5, 0, 1, 6);

NOTE: in previous versions of popcorn (before 1.5.4), if one of these numeric swizzle codes has to appear first in the swizzle expression, you had to prefix it with an underscore '_' :

float2(5,6)._01yx   -->   float4(0, 1, 6, 5)

since 1.5.4, this is no longer needed, and you can directly write:

float2(5,6).01yx   -->   float4(0, 1, 6, 5)

note that a side-effect of scalars being vectors of dimension 1 is that swizzles can also be used on scalar values:

1.5.xx01   -->   float4(1.5, 1.5, 0, 1)

note that there will be an ambiguity if you want to apply a swizzle like '0x1' to an integer value, as when the parser will think you are trying to add a decimal point when it sees the '0' after the first dot:

2.0x1      -->   error

you can do either of these to disambiguate:

2.0.0x1      -->   float3(0,2,1)
(2).0x1      -->   int3(0,2,1)

you can also use swizzles on more complex expressions using parentheses to isolate the sub-expression you want to apply it to:

(1.5 + 4*5).0x1   -->   float3(0, 21.5, 1)

In addition to 'x', 'y', 'z', and 'w', you can also use 'r', 'g', 'b', and 'a', in case you find this more readable when maniplulating color values in a float4.


Operator Description Example UsageDetails
( parenthesis open used for function calls or expression isolation
) parenthesis close every opened parenthesis must be matched by a closing parenthesis
++ post increment a++ adds 1 to a variable, and returns the value the var had before the incrementation: b = a++; -> b = a; a = a + 1;
-- post decrement a-- subtracts 1 from a variable, and returns the value the var had before the decrementation: b = a--; -> b = a; a = a - 1;
~ binary not ~a [integer only] returns the binary inverse of all bits: a = 0b01100101 will give: ~a == 0b10011010
! logical not !a [boolean test] returns the opposite boolean value: if (!(a > 3 || a <= -2)) -> if (a <= 3 && a > -2)
++ pre increment ++a same as post-increment, except the value returned is the value after the incrementation: b = ++a; -> a = a + 1; b = a;
-- pre decrement --a same as pre-increment, but with a decrementation
+ unary plus +a does nothing for basic types: +a is still equal to a
- unary minus -a negates a value: -a -> a * -1
* mul a * b multiplies 'a' and 'b' together
/ div a / b divides 'a' by 'b'
% mod a % b returns the remainder of the division of 'a' by 'b': a % b --> a - (((int)(a / b)) * b)
+ add a + b adds 'a' and 'b' together
- sub a - b subtracts 'b' from 'a'
<< shift left a << b [integer only] performs a binary shift to the left: 0b10101110 << 2 -> 0b10111000 the result is an integer multiplication by 2^shiftCount
>> shift right a >> b [integer only] performs a binary shift to the right: 0b10101110 >> 2 -> 0b00101011 the result is an integer division by 2^shiftCount
< lower a < b [boolean test] checks if 'a' is strictly lower than 'b': a < b
<= lower or equal a <= b [boolean test] checks if 'a' is lower or equal to 'b': a <= b
> greater a > b [boolean test] checks if 'a' is strictly greater than 'b': a > b
>= greater or equal a >= b [boolean test] checks if 'a' is greater or equal to 'b': a >= b
== equal a == b [boolean test] checks if 'a' and 'b' are equal: a == b
!= not equal a != b [boolean test] checks if 'a' and 'b' are not equal: a != b
& and a & b [integer only] binary AND between 'a' and 'b': a & b
^ xor a ^ b [integer only] binary XOR between 'a' and 'b': a ^ b
| or a | b [integer only] binary OR between 'a' and 'b': a | b
&& logical and a && b [boolean test] checks if 'a' AND 'b' are both 'true': a && b
|| logical or a || b [boolean test] checks if 'a' OR 'b' are not equal: a || b
= assign a = b assigns 'b' to 'a': a = b;
+= add and assign a += b adds 'b' to 'a', and assigns the result to 'a': a += b; --> a = a + b;
-= sub and assign a -= b subtracts 'b' from 'a', and assigns the result to 'a'
*= mul and assign a *= b multiplies 'a' by 'b', and assigns the result to 'a'
/= div and assign a /= b divides 'a' by 'b', and assigns the result to 'a'
%= mod and assign a %= b computes 'a' modulo 'b', and assigns the result to 'a'
&= and and assign a &= b [integer only] computes the binary 'AND' of 'a' and 'b', and assigns the result to 'a'
|= or and assign a |= b [integer only] computes the binary 'OR' of 'a' and 'b', and assigns the result to 'a'
^= xor and assign a ^= b [integer only] computes the binary 'XOR' of 'a' and 'b', and assigns the result to 'a'
<<= shift left and assign a <<= b [integer only] binary-shifts left 'a' by 'b' bits, and assigns the result to 'a'
>>= shift right and assign a >>= b [integer only] binary-shifts right (arithmetic) 'a' by 'b' bits, and assigns the result to 'a'
; semicolon a; used to delimit expressions
{ scope open used to open a new scope
} scope close every scope opening bracket must be matched by a scope closing bracket.

Language constructs


! WARNING ! calling custom script functions within particle scripts will have horrible performance on all current platforms.
We will adress this issue, but in the meantime, please do not use custom functions in particle scripts for anything other than initial prototyping.

function definitions obey the following syntax:

function _type_ _name_([_arg0_[, _arg1_[, ...]]]) { _body_ }

a function can be called by writing its name '_name_', followed by the argument list. its return type will be '_type_'.
For example:

function int Factorial(int n)
	if (n > 1)
		return n * Factorial(n - 1);
	return n;

function void Eval()
	Life = 1.0;
	Size = 0.1;
	float angle = Factorial(spawner.EmittedCount) * 0.01;
	Position = float3suf(sincos(angle),0);

Here, if we consider this is the spawn-script of a particle, and its parent layer emits 6 particles, 'spawner.EmittedCount' will be zero for the first particle, 1 for the second, 2 for the third, etc.. up to 5
Those 6 particles will have the following values for the 'angle' variable:

  • 0*0.01 = 0
  • 1*0.01 = 0.01
  • 1*2*0.01 = 0.02
  • 1*2*3*0.01 = 0.06
  • 1*2*3*4*0.01 = 0.24
  • 1*2*3*4*5*0.01 = 1.2


used to return from a function. if the function returns 'void', an explicit call to 'return' is not necessary at the end of the function.
implicit calls to 'return' from a function returning void take no parameters.
however, return statements are mandatory for functions returning a value. otherwise, the scripting system doesn't know what value it should return. it cannot be implicit.
the value to return follows the 'return' statement:

function void	Stuff()
	return;	// optional, this is not needed

function void	Something()
}	// no return statement. this is valid for a function returning nothing ('void')

function int	Grill(int bacon)
	return bacon + 21 * 2 + 3 - 4 + 1;	// explicit return statement (script compiler will collapse this into 'return bacon + 42;' at compile-time
function float	Cook(int eggs, int bacon, int cheese)
	float	omelette = eggs + cheese;
	int	tasty = Grill(bacon);
	return tasty * omelette;


Popcorn scripts do not support any flow-control constructs at the moment. This includes if/else, switch/case, as well as loop constructs such as for, while, or foreach.

The usual way to do an 'if' is to use masking and selection primitives, such as the 'step', 'select', or 'iif' builtins. (iif is just a select in disguise)

For example, instead of writing this:

// when the particle goes slower than 1.2 units/s, set its color to green, otherwise, set it to red:
if (length(Velocity) < 1.2)
	Color = float4(0.1,0.8,0.05,1); // green
	Color = float4(1.5,0,0,1); // bright red

You can do:

float	selector = step(length(Velocity), 1.2); // returns 0.0 if length < 1.2, returns 1.0 otherwise
Color = float4(1.5,0,0,1) * selector + float4(0.1,0.8,0.05,1) * (1-selector);


float	selector = step(length(Velocity), 1.2); // returns 0.0 if length < 1.2, returns 1.0 otherwise
Color = lerp(float4(0.1,0.8,0.05,1), float4(1.5,0,0,1), selector);

or even better:

Color = select(float4(1.5,0,0,1), float4(0.1,0.8,0.05,1), length(Velocity) < 1.2);

or, if you prefer the 'iif' notation:

Color = iif(length(Velocity) < 1.2, float4(0.1,0.8,0.05,1), float4(1.5,0,0,1));


the 'version' keyword allows for conditional build-time code activation or deactivation. the deactivated code still goes through basic parsing and type-checking.
'version' accepts one or multiple parameters, separated by commas.

	version (debug, debug_release)
		[...] // block of code

here, the block of code will only be built it the target is 'debug' or 'debug_release'

the arguments can contain matching tokens, such as '*' and '?'
for instance, the above example, rewritten as:

	version (debug*)
		[...] // block of code

will see the code block activated if the build target is 'debug' or 'debug_release' too. but also if it is 'debug_none' or 'debugTagadaTsointsoin', or whatever that matches the pattern 'debug*'

'none' is a special builtin target that never gets built.

	version (none)
		[...] // block of code

'all' is a special builtin target that always gets built.

	version (all)
		[...] // block of code

in the above example, 'version' is used to quickly enable or disable a whole block of code, without having to resort to comments.
note that in case of multiple targets, as soon as 'none' is present, the whole block is discarded regardless of the other targets:

	version (debug, none, debug_release)
		[...] // block of code

Here, even if 'debug' or 'debug_release' are set as build targets, the block won't get built, because there is 'none' specified

You can test and change the build targets in the 'ScriptBuildVersions' property of the 'Global Properties' node in the treeview of effect editor.
A build target such as "debug, test" can be seen as "include in the build all versioned code blocks that match the targets 'debug' and 'test'
setting a build target as 'none' is invalid, it will output an error, and be ignored.

cascading 'version' statements can be used through the 'else' keyword:

	version (debug*)
		[...] // block of code, will be compiled and run in debug
		[...] // other block of code, will be compiled and run when not in debug
	version (x86)
		// runs on x86 machines
	else version(xbox)
		// runs on xbox
 	else version(ps4)
		// runs on ps4
 	else version(macosx)
		// runs on MacOsX;
		// Unknown platform;

	version (x86, debug)
		// runs on x86 in debug builds
	version (x86, release)
		// runs on x86 in release builds