PopcornFX v2.19

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
  1. Home
  2. Docs
  3. PopcornFX v2.19
  4. Scripting reference
  5. Language syntax

Language syntax

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


The PopcornFX script general syntax is very similar to shader languages such as GLSL or HLSL.

Every name/symbol in PopcornFX-scripts is case-sensitive, and each statement must end with a semicolon.



You can add comments anywhere in a script. There are two ways to write comments, just as in 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 (allows multiline comments)



some_statement; // comment
something /* comment */ something_else;
this /*is a
multiline*/ comment;



Data-types in PopcornFX scripts are almost identical to the data-types available in the nodegraph. See the particle graph page for more details.

The following data-types are available in scripts:


Boolean scalar & vectors

1-bit true / false boolean values

bool, bool2, bool3, bool4

You can directly create boolean values with the following syntax:

false			// immediate bool value
bool(0)			// immediate bool value, same as above
bool2(true, false)	// immediate bool2 value
bool3(false, 1, true)	// immediate bool3 value
bool4(0, 1, false, 1)	// immediate bool4 value

When given to an explicit bool constructor, integer values 0 and 1 will automatically be converted to boolean false / true values respectively.
Note: Even though a bool can be represented in 1 bit, they will be stored as 1 byte (8 bits) inside the particle storage (for both CPU & GPU sims), and expanded to 4 bytes (32 bits) inside the vector-VM of the CPU simulation backend for more efficient runtime execution, then converted back to 8 bits if the simulation needs to store them back into the particle storage.
They could be stored as 1 bit, but it introduces overhead in multiple places to pack/unpack, and the memory gains compared to 32->8 bits are not really significant, as bool storages are almost never the storage bottleneck.


Integer scalar & vectors

32-bits signed integer values

int, int2, int3, int4

You can directly create integer values 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

You can use different prefixes to specify which integer format you’re about to write:

42               // no specific prefix: will be treated as a decimal value
042              // no specific prefix: 42 in decimal
0x42             // '0x' hexadecimal prefix: 66 in decimal
0x100            // '0x' hexadecimal prefix: 256 in decimal
0xC0FFEE         // '0x' hexadecimal prefix: 12648430 in decimal
0b101            // '0b' binary prefix: 5 in decimal
0b1000010010010  // '0b' binary prefix: 4242 in decimal



The int type can store values from roughly negative 2 billion up to roughly positive 2 billion
Smallest value: -2147483648 (In hexadecimal: 0x80000000)
Largest value: 2147483647 (In hexadecimal: 0x7FFFFFFF)
On overflow/underflow, the value will wrap around. Ex: 2147483647 + 1 will be equal to -2147483648


Floating-point scalar & vectors

32-bit floating-point values

float, float2, float3, float4

You can directly create floating-point values with the following syntax:

1.2			// immediate float value
float(1.2)		// immediate float value, same as above
float2(1.2, 3.4)	// immediate float2 value
float3(-0.5, 4, 5.5)	// immediate float3 value
float4(3, 4, 5, 6)	// immediate float4 value

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


The explicit suffix f won’t make any concrete difference on the script compiler’s point of view, it is supported mainly to be able to copy/paste values from HLSL or C++.



Smallest value: -3.4028235e+38f (-3 followed by 38 zeroes)
Largest value: 3.4028235e+38f (3 followed by 38 zeroes)
Special values: +infinity and -infinity
On overflow/underflow, the value will saturate to infinity. Ex: 3.0e+38f * 2.0f will be equal to infinity
Precision: all floating-point values suffer from precision issues, when adding two values that have extremely different magnitudes, the result might simply be equal to the largest value, unchanged. Ex: 10000000.0f + 0.5f will be equal to 10000000.0f
This is not specific to PopcornFX, and holds true for all floating-point values, in all programming languages, including shaders.

For more information about the float type, see The following links:
IEEE754 format: wikipedia page
IEEE754 floating-point converter



128-bits value encoding a 3D rotation.


Orientations cannot be directly constructed with an inline constructor like the bool, int, or float types.
You will need to call one of the explicit orientation construction functions, or grab the orientation from one of your script’s input nodes of type orientation.

orientation_ea(pitch, yaw, roll); // returns a value of type 'orientation', constructed from 3 euler angles in degrees
orientation_aa(float3(0,1,0), 45.0);   // returns a value of type 'orientation' constructed from an axis and an angle in degrees along that axis


Note: Orientations are internally stored as quaternions using 4 float values, but are exposed as euler angles when editing them in the editor propertygrid for more “artist-friendlyness” when tweaking orientations.

Type promotion

PopcornFX script performs 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, combining a float and an int3:

123.4 + int3(5,6,7)
  • the float – integer operation will lead to floats
  • the scalar – vector3 operation will lead to a vector3
  • the result will be a float3

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

Local variables are useful to store intermediate computation results and reuse them in multiple places further down in your script.

They are also good for readability in general, as it allows you to explicitly “name” computation results and make clear what they are.


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, 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


When declaring variables outside of curly braces, they are placed in the base default scope of the script (scope 0)


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


Vector shuffling is often also called 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).

So 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 because 'vec3' is a float3, not a float4.
float3	g = float3(vec3.xy, vec4.w);
float2	h = float3(vec4.x, vec3.z);


Other swizzle codes

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);
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       -->   float3(0,2,1)
(2).0x1      -->   int3(0,2,1)

You can also use swizzles on more complex expressions using parentheses in order 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.



Some operations are done by calling “functions”.

Functions receive parameters (or sometimes no parameters at all), and usually return a value.

Parameters are passed in parentheses after the function name, and are separated by commas.

You cannot currently define custom functions inside a PopcornFX script.


For example, to compute the length of a float3 vector, you can use the length function, which takes the vector as a parameter, and returns its length:

float3 v = float3(1, 2, 3); // some 3D vector named "v"
float vecLength = length(v) // compute the length of "v" and store it in "vecLength"


To compute a random value between -5 and 10.5, you would typically use the rand function, which takes two parameters: the min and the max value, and returns the random number:

float myRandValue = rand(-5, 10.5); // picks a random number in the [-5, 10.5[ range and stores it in the "myRandValue" variable


To rotate a 3D vector around an axis and an angle, you can use the rotate function, which takes three parameters: the vector to rotate, the axis along which it should be rotated, and the rotation angle in degrees:

float3 v = float3(1, 2, 3); // some 3D vector named "v"
float3 vRot = rotate(v, float3(0,1,-2.5), 45); // rotates 'v' along the axis {0, 1, -2.5} by a 45 degrees angle


Functions taking no parameters are called using an empty parameter list ()

Note: constructors, such as float3(1, 2, 3), are also functions.
See the builtin functions reference page for more details on the available builtin functions.


Member functions

Member functions are a special kind of functions that belong to a specific type, for example samplers.

You call them by using the name of the object, then the dot operator ‘.‘, followed by the function name.


For example, if you create a script with a float3 output named Output, and an input of type dataGeometry named MyShape, and wire a shape node in, you can pick a random position on the shape by calling the samplePosition member function:

Script member functions: sampling a shape

Output = MyShape.samplePosition(); // member function taking no arguments



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 ‘select‘, or ‘iif‘ builtins. (iif stands for “inline if” and 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:

bool	isSlow = length(Velocity) < 1.2; // returns true if length < 1.2, otherwise returns false
Color = select(float4(1.5,0,0,1), float4(0.1,0.8,0.05,1), isSlow);

or shorter:

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 is a static compile-time if statement, that allows you to switch between different behaviors based on the build version tags.
Build version tags are a way to toggle different parts of an effect simulation graph on or off based on which target platform it is exported for.
For example, when exporting the effect for mobile platforms such as android or iOS, you might want to disable a more expensive computation that you’re willing to include on PC/desktop builds, or simply change some LOD metrics if you’re building for mobile, consoles, or PC.
The version keyword works like an if/else statement would. Because version is statically evaluated at compile time, it does not suffer from the limitations of a dynamic runtime if/else construct.
It expects a list of comma-separated version tags. The commas act as an “or” operator. If any of the build tags are active in the comma-separated list, it will evaluate to ‘true’ and execute the following scope. Otherwise, it will enter the else statement, if it exists.
Here is an example of a turbulence computation getting killed on mobile but not on other platforms:

float3 windStrength = 0.0f;
version (android, ios)
	windStrength = float3suf(10, 0, 0); // 10 units/s along the side axis
	float  turbFadeOff = MyFadingCurve.sample(eval.full(self.lifeRatio)); // Fade the tubulence over life (moderately expensive)
	float3 turb = MyTurbulence.sample(Position);  // Sample the turbulence vector-field (very expensive)
	windStrength = float3suf(10, 0, 0) + turb * turbFadeOff; // 10 units/s along the side axis, plus the faded turbulence



Namespaces are prefixes used to organize a set of functions or symbols in distinct categories. They are not instances of actual objects like samplers, but follow the same syntax.
For example, the scene and effect namespaces exposes a set of helper functions and symbols, such as scene.time, scene.dt, effect.age(), scene.axisUp(), scene.intersect(position, rayDir, rayLength), and more:

float3 raycastDir = -scene.axisUp();
float4 raycastResult = scene.intersect(Position, -raycastDir, 100.0f); // raycast 100 units downwards


The degrees and radians namespaces contain alternative versions of all functions manipulating angles, which by default, when called outside the namespace, manipulate degrees:

float angleInDegrees = x;
float a = sin(angleInDegrees);
float b = degrees.sin(angleInDegrees);
float c = radians.sin(deg2rad(angleInDegrees));
float d = radians.sin(pi / 3.0); // equal to 'sin(60)'


Namespaces can contain sub namespaces, for example fast.radians.sin(...), fast.degrees.tan(...), etc…


Namespaces available in PopcornFX scripts

  • self : Access to the current particle (ex: self.lifeRatio, self.kill(), …)
  • effect : Access to the current effect (ex: effect.age(), effect.isRunning(), effect.position(), effect.orientation(), …)
  • view : Access to views / cameras (ex: view.position(), view.closest(...), view.axisForward(), …)
  • scene : Access to the scene (ex: scene.axisUp(), scene.intersect(...), scene.time, scene.dt, …)
  • debug : Debug helpers (ex: debug.assert(...), debug.warning(...), …)
  • shape : Contains shape-related functions (ex: shape.buildPCoordsMesh(...), shape.buildPCoordsBox(...), …)
  • shapeType : Contains shape types (ex: shapeType.Box, shapeType.Mesh, …)
  • textureAddr : Contains texture sampler address modes (ex: textureAddr.Wrap, textureAddr.Clamp, …)
  • textureFilter: Contains texture sampler filter modes (ex: textureFilter.Point, textureFilter.Linear, …)
  • degrees : Contains all functions working with degrees (ex: degrees.sin(...), degrees.cos(...), degrees.rotate(...), …)
  • radians : Contains all functions working with radians (ex: radians.sin(...), radians.cos(...), radians.rotate(...), …)
  • fast : Contains all fast versions of complex math functions (ex: fast.sqrt(...), fast.exp(...), fast.sin(...), …)
  • fast.degrees : Contains all fast versions of functions working with degrees (ex: fast.degrees.sin(...), fast.degrees.cos(...), fast.degrees.atan2(...), …)
  • fast.radians : Contains all fast versions of functions working with radians (ex: fast.radians.sin(...), fast.radians.cos(...), fast.radians.atan2(...), …)
  • accurate : Contains all accurate (but slower) versions of complex math functions (ex: accurate.sqrt(...), accurate.exp(...), accurate.sin(...), …)
  • accurate.degrees : Contains all accurate (but slower) versions of functions working with degrees (ex: accurate.degrees.sin(...), accurate.degrees.cos(...), accurate.degrees.atan2(...), …)
  • accurate.radians : Contains all accurate (but slower) versions of functions working with radians (ex: accurate.radians.sin(...), accurate.radians.cos(...), accurate.radians.atan2(...), …)
  • sim : [v2.2.0] Contains all internal simulation details functions (ex: sim.lod(), sim.updateRate(), …)
  • sim.wave : [v2.2.0] Contains all internal simulation unit (wavefront) reduction functions (ex: sim.wave.min(), sim.wave.any(), …)


See the namespace reference pages for more details on these namespaces and functions.



Operators are all the special symbols in the scripts. You will find the exhaustive list of supported operators and what they do in the table below:


(parenthesis openused for function calls or expression isolation
)parenthesis closeevery opened parenthesis must be matched by a closing parenthesis
++post incrementa++adds 1 to a variable, and returns the value the var had before the incrementation: b = a++; -> b = a; a = a + 1;
--post decrementa--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++asame as post-increment, except the value returned is the value after the incrementation: b = ++a; -> a = a + 1; b = a;
--pre decrement--asame as pre-increment, but with a decrementation
+unary plus+adoes nothing for basic types: +a is still equal to a
-unary minus-anegates a value: -a -> a * -1
*mula * bmultiplies a and b together
/diva / bdivides a by b
%moda % breturns the remainder of the division of a by b: a % b –> a - (((int)(a / b)) * b)
+adda + badds a and b together
-suba - bsubtracts b from a
<<shift lefta << b[integer only] performs a binary shift to the left: 0b10101110 << 2 -> 0b10111000 the result is an integer multiplication by 2^shiftCount
>>shift righta >> b[integer only] performs a binary shift to the right: 0b10101110 >> 2 -> 0b00101011 the result is an integer division by 2^shiftCount
<lowera < b[boolean test] checks if a is strictly lower than b: a < b
<=lower or equala <= b[boolean test] checks if a is lower or equal to b: a <= b
>greatera > b[boolean test] checks if a is strictly greater than b: a > b
>=greater or equala >= b[boolean test] checks if a is greater or equal to b: a >= b
==equala == b[boolean test] checks if a and b are equal: a == b
!=not equala != b[boolean test] checks if a and b are not equal: a != b
&anda & b[integer only] binary AND between a and b: a & b
^xora ^ b[integer only] binary XOR between a and b: a ^ b
|ora | b[integer only] binary OR between a and b: a | b
&&logical anda && b[boolean test] checks if a AND b are both ‘true’: a && b
||logical ora || b[boolean test] checks if a OR b are not equal: a || b
=assigna = bassigns b to a: a = b;
+=add and assigna += badds b to a, and assigns the result to a: a += b; –> a = a + b;
-=sub and assigna -= bsubtracts b from a, and assigns the result to a
*=mul and assigna *= bmultiplies a by b, and assigns the result to a
/=div and assigna /= bdivides a by b, and assigns the result to a
%=mod and assigna %= bcomputes a modulo b, and assigns the result to a
&=and and assigna &= b[integer only] computes the binary ‘AND’ of a and b, and assigns the result to a
|=or and assigna |= b[integer only] computes the binary ‘OR’ of a and b, and assigns the result to a
^=xor and assigna ^= b[integer only] computes the binary ‘XOR’ of a and b, and assigns the result to a
<<=shift left and assigna <<= b[integer only] binary-shifts left a by b bits, and assigns the result to a
>>=shift right and assigna >>= b[integer only] binary-shifts right (arithmetic) a by b bits, and assigns the result to a
;semicolona;used to delimit expressions
{scope openused to open a new scope
}scope closeevery scope opening bracket must be matched by a scope closing bracket.


Was this article helpful to you? Yes No

How can we help?