Waterline Real Flow Scripting
Waterline Real Flow Scripting
Waterline Real Flow Scripting
RealFlow Tutorials
by Thomas Schlick | Next Limit Technologies
Scripting
with
RealFlow
Contents
Annoations
What Is Scripting?
The Software Development Kit (SDK)
Getting Started
Syntax, Syntax Errors, Indents
Comments
Variables
Naming
Data Types
Integers and Real Numbers
Strings
Lists
Accessing List Elements
Looping Through List Elements
Appending List Elements
Counting List Elements
Dictionaries
Vectors
Vector Maths
Disassembling and Assembling Vectors
Booleans
Operators
Conditions
7
7
8
10
12
13
13
16
16
16
17
17
17
18
18
19
19
21
22
23
23
24
27
27
28
29
29
30
32
32
34
34
36
Contents
38
38
39
39
41
42
43
45
45
46
46
47
48
61
61
62
63
Crown Splashes
What the Script Should Do
Force or Velocity?
Scene Setup
Defining a Ring
Accelerating the Ring Particles
Start and End Time
Creating a Splash-Like Shape
Tendril Creation
Finding the Tendril Positions
Circle Maths
Getting the Tendril Particles
66
66
67
67
67
69
70
71
73
73
74
77
53
53
56
56
57
58
58
Contents
79
81
85
86
88
88
89
90
92
92
93
95
97
Thomas Schlick
Annotations
All tutorials and scripts in this issue have been created with the latest version of RealFlow 2015.
Some of the scripts and simulations can also be used with previous versions, but there is no guarantee that they will really work.
Let's say there is a scale value with three elements, e.g. (1.0,5.0,1.0) a so-called vector.
This means that the object's length and width are 1 m each, while height is 5 m.
With a Z-based setup, the vector must be changed to (1.0,1.0,5.0).
This notation is also interesting for scripts, because the magazine's programs are not axes-aware.
You have to convert all XYZ vectors like position, velocity, force, or scale.
The default scale, used in this magazine, is 1.0.
What Is Scripting?
What Is Scripting?
What Is Scripting
Scripting is the development of custom tailored applications here inside of RealFlow. The scripting language provides a set of statements and functions the programmer can use to solve a certain task.
Scripts are normally interpreted. This means that all instructions are processed at runtime, and
translated into a code the computer can understand You know this from web browsers where the
code (this is the sequence of statements in your program) is often directly visible to the user. At
the moment a visitor calls a webpage, the code is sent to the interpreter and processed. With languages like C++ the code is preprocessed and converted into machine code. A compiled program
is much faster than a script, because the compiler optimizes the code for certain hardware needs.
PY
JS
VBA
C++
MEL
This API is the basis for the Software Development Kit (SDK). The SDK contains all the application-specific commands and instructions you need for working with Python inside a program.
In RealFlow the list of SDK commands can be viewed under
What Is Scripting?
When you expand the Scripting Reference tree you will see a long list of
keywords. Most of them sound familiar, for example Daemon, HY_Bubbles, or Object. Others, like GUIFormDialog, or Vector, are very abstract:
These higher-ranking elements are also called classes. When you click on an
entry you will see a list with commands. The commands are members of the
currently viewed class. Every command provides access to one of RealFlows
functions.
The SDK is your ticket to RealFlows Python interface, and when you make
your first steps with scripting, you will use the reference very often. You will
also be amazed by the number of commands and how deep you can go inside RealFlow to change and influence simulations, nodes, and parameters.
Getting Started
These commands open
a whole new world
and you can literally
do everything, but the
question is how? How
can you do all these
fancy things you have
seen in simulations, or
recreate the stuff you have read about?
Many users have the strong conviction
that it is a heavy task to get started, learn
a new language, and convert ideas into
code. There are also rumours that a deep
knowledge of maths and physics is required. This is exactly the problem that
keeps most artists away from scripting and
programming.
All this is only partially true. Scripts do not
have to be sophisticated constructions with
thousands of lines of code.
What Is Scripting?
Most scripts also do not require higher mathematics and complex functions. In fact, the big majority of scripts contains just a few lines, does not use any maths at all, and has been created to
execute repetitive tasks, and ease your daily life.
Think of scripting as a normal language. Not every sentence has to be a poem, but it is better
to speak in a clear and simple way instead. Another, very important, thing with scripting is the
feeling of success. This feeling is exactly what keeps you motivated and lets you look for more
advanced tasks. With every script you will get more routine, and self-confidence. Of course, there
will also be throwbacks, programs where you are not able to move forward, get stuck, or have to
start from scratch. But these problems will improve your knowledge not only about scripting,
but also about fundamental processes in RealFlow.
Anyway, the first steps are about the nuts and bolts, but I will keep things as easy as possible and
provide examples where ever they make sense.
Congratulations! You have just produced your first syntax error and the praised feeling of success
is blown away already. I also want to welcome you to the colourful world of bug fixing ;-)
10
What Is Scripting?
Now, remove the script, copy/paste the code from the next example, and execute it with Run:
< py >
for i in range(0, 3):
Take a look at the Messages panel, where you will see the scripts output:
The difference between both scripts are the indents and leading spaces, and this is exactly the
reason for the syntax error. Python requires leading spaces and scripts with wrong indents will
always fail. Especially with downloaded or copies scripts you have to be careful, and sometimes it
is necessary to recreate the indents. After every colon (:) you have to add a new indent:
< pseudo code >
if ( condition ):
statement 2
statement 3
statement 4
else:
loop:
if ( condition ):
statement 5
else:
statement 6
statement 7
Quotation marks are another very common source of syntax errors, because Python only accepts
a certain type. If the content between two quotation marks is displayed in red everything is Ok.
With missing brackets it is very similar. Complex commands sometimes have three or four nested
brackets. If one of these brackets is missing you will get a syntax error.
11
What Is Scripting?
Aside from syntax errors and debugging, syntax itself plays an important role, because it is the
logical structure of your program and its instructions. Every command has its own syntax and you
can find it in the SDK, but I will introduce the individual components later with examples.
There is not the one correct syntax. You can build and structure the code the way you want it to
be. There are many programmers out there who say that they really dont care about highly-optimized code as long as their scripts are running. Why not? It should be fun to write a program and
none of us is a professional coder. Of course, there are ways to accelerate a script when you take a
little time and think about it, but this feeling for the code is something that will come automatically with more practice, and also by analysing scripts from users.
Comments
Comments are one of the most important code elements, because they will help us to keep our
scripts readable and understandable even after months and years. A comment is a description
and you should add them where ever you can. They are ignored by the Python interpreter and
therefore they do not have influence on the scripts execution time. This also means that it is not
possible to declare variables inside a comment, perform calculations, assemble vectors, etc. No
matter whats inside a comment it will not be processed.
A single-line comment is introduced this way in RealFlow comments are printed in green:
# This is my first comment
"""
If the scripts last line is a comment you will receive a syntax error.
12
What Is Scripting?
Variables
Here is the next reason for headaches: variables. Fortunately, the concept behind variables is very
easy to understand. What make more users really desperate is the large number of different
variable types and the contents they can store. Basically, a variable is nothing more than a placeholder that can be filled with a value. Think of your garage as a variable. For the garage itself it
makes no difference whether you put a car, a bicycle, or your old furniture inside. But, of course it
makes a huge difference for you, and you will see it when you try to ride a cupboard instead of a
bike. With variables it is very similar, because the variables content determines its role and how it
will be treated in the script.
Variables have to be declared before they can be used within the program this is also valid if the
variables value is 0 or if it is empty at the beginning of the script.
Naming
Lets start with a simple example with three values: 27, female, Claudia. What we have to do is to
set the values into a logical context to make use of them:
Age
Name
= 27
= "Claudia"
Gender = "female"
Now, the attributes make sense and you have just declared three variables. You can also see that
meaningful names are very important. The following variable names are valid, but it is very difficult to differentiate them within a more complex script as shown here:
A = 27
B = "Claudia"
C = "Beatrice"
D = 56
Here it is difficult to find out what C and D actually stand for. Do they perhaps describe another
person or something else? Here is the solving:
Age
Name
= 27
= "Claudia"
SecondName = "Beatrice"
Weight
= 56
13
What Is Scripting?
It is important that a variables name does not change during the script. Even lower and upper
cases are differentiated. Age and age are not the same, but completely different variables.
A variables value, on the other hand, can change during the execution of the script, because
otherwise it is not possible to calculate with variables and get a result.
A few more notes on naming:
14
Data Types
Data Types
Data Types
It is true that the following explanations are not exactly exciting. Discussions about integers, lists,
or strings are very theoretical, but they are absolutely essential. They are also important for the
creation of custom GUIs, where we can assign initial values.
In the previous chapters you have already used two data types: numbers and strings. A string can
be a single character, but also a word, a sentence, or complete text from a book. Numbers, on the
other hand, are subdivided into several classes like integers, reals, or complex numbers.
Name
= 27
= "Claudia"
Gender = "female"
Age is treated as a number, or to be more precise: an integer, while Name and Gender are strings.
Integers are numbers like 64, 8524, -9610, or 7. When you perform calculations with integers
then the result will always be an integer. Real numbers are, for example, 3.1415, -0.758, 43.2, or
-416.0003. This type is also called floats. When two real numbers are combined you will get a real
number as well.
Strings
Strings are always enclosed within quotation marks, and this also applies to numbers, as shown in
the next example. Here, the numbers will be treated as strings, and one way to combine them is a
process called concatenation. The operator is a + sign and the result is a new string:
insuranceNumberClaudia = "1100"
insuranceNumberLydia
= "2200"
If you want to treat these numbers as integers you have to remove the quotation marks:
insuranceNumberClaudia = 1100
insuranceNumberLydia
= 2200
16
Data Types
Lists
Scalars can only store a single value, but we also need structures for multiple, maybe even millions of values. A good example are RealFlow particles. How can we access these huge amounts of
data? For this purpose Python provides so-called lists. A lists elements are always enclosed between brackets, and an empty list is declared as:
emptyList = []
Strings are also supported, but they must be enclosed within quotation marks:
stringList = ["this","list","contains","six","string","entries"]
this
list
contains
six
string
entries
Index
Now we can use the index to access an element directly, e.g. contains:
listElement = stringList[2]
The result of this operation is a single value, but a list with just a single element will remain a list.
17
Data Types
Always mind the leading tab. Without it you will get a syntax error and the script will not be executed):
fruitList = ["peach","apple","apricot","pineapple","orange","strawberry","mango"]
for element in fruitList:
fruitList.append(newFruit)
Result: fruitList = ["peach","apple","apricot","pineapple","orange","strawberry","mango","lemon"]
Python provides many other functions and commands in conjunction with lists, but they are not
really relevant for an introduction to scripting. It is, for example, possible to use lists within a list
and such an element is called a tuple. A typical application for tuples is a list with a polygons vertices, but they are also often used for pairs of values, or complete data sets. Imagine you want to
store a particles Id and connect it with attributes like age and mass:
tupleList = [(0,0.38,0.0004),(1,0.32,0.0004),(2,0.37,0.0004),(3,0.33,0.0004)]
18
Data Types
Dictionaries
This variable type is comparable to a phone book. In contrast to lists, dictionaries do not require
a specific order and there is no index for identifying an element. A dictionary uses key-value pairs
instead. Similar to lists, dictionaries are declared with brackets:
emptyDictionary = {}
The first entry of each pair is the key and it can be used to identify its associated value:
phoneNumber = phoneBook["Agnes"]
Result: 75064
As with lists, the number of key-value pairs is queried with the len command:
numberOfPairs = len(phoneBook)
Vectors
In 3D programs, vectors play a fundamental role: they are used to describe positions, dimensions,
velocities, rotations, but also directions, and many other attributes. In RealFlow, a vector is always
described through a trio of three values one for each spatial direction (XYZ). You can find some
of the most important RealFlow vectors under a scene element's Node panel:
19
Data Types
Velocity is a very good example for vectors, because we all have an idea of what it is. But, when
we think about velocity and speed we normally have something like 10.6 m/s in mind, not a vector
consisting of three values. Let me explain you the backgrounds and start with a velocity vector:
velocityVector = (6.5,1.6,8.2)
What we have a here are values for X, Y, and Z. In a variable-based notation, the vector can also
be written as:
velocityX = 6.5
velocityY = 1.6
velocityZ = 8.2
velocityVector = (velocityX, velocityY, velocityZ)
As you can see, the individual elements are single values so-called scalars:
Y
< x0,y0>
x0
de
tu
ni
ag
t=
h
ng
le
X
y0
What we got now is an arrow, and this is the standard representation for vectors. The arrow
does not only show the vector's direction, but its length has a certain meaning. If you measure
the length you will roughly get 10.6.
20
Data Types
So, a velocity vector's length, also known as module or magnitude, is what we call speed in daily
life. Now you know what a vector's magnitude is, but you do not know how to get this value or
how to use it. The calculation of the magnitude is done with a built-in function inside RealFlow:
vectorMagnitude = vector.module()
What we get here is again a scalar and this value can be used for comparisons, e.g. for defining
thresholds. It is not possible to compare two vectors against each other, and you cannot compare
a scalar against a vector. The following example notations are not allowed:
if (velocityVector > positionVector)
if (velocityVector == 2.5)
velocityMagnitude = velocityVector.module()
if (velocityMagnitude >= velocityThreshold):
do something
Vector Maths
The headline really spells trouble, but fortunately RealFlow will do all the maths for us. We can
calculate with vectors in pretty much the same way as with numbers. The difference to numbers is
that the result is a new vector.
vector1 = (x1, y1, z1)
vector2 = (x2, y2, z2)
Addition
vector1 + vector2
Subtraction
vector1 vector2
Division
vector1 / vector2
Vector multiplication has to be performed manually, because the * operator is reserved for another function the dot product (see table next page):
vector1 * vector2 = (x1 * x2, y1 * y2, z1 * z2)
21
Data Types
There are also more complex functions available, and some of them only require one vector:
Cross product
vector1.cross(vector2)
Dot product
vector1 * vector2
Distance
vector1.distance(vector2)
Normalization
vector1.normalize()
Magnitude
vector1.module()
Scaling
vector1.scale(factor)
Multiplies each component with the factor in brackets and returns a vector.
= velocityVector.getX()
velocityZ
= velocityVector.getZ()
velocityY
= velocityVector.getZ()
With these separated values we are able to perform any operation, swap components, or leave
certain elements untouched. We can do comparisons between another scalar and a single element, and so on. Once the components are defined they can be used to create an new vector:
newVelocityVector = Vector.new(velocityNewX, velocityNewY, velocityNewZ)
This notation is also often used to introduce a standard vector, e.g. a null vector or a reference
vector:
nullVector
= Vector.new(0,0,0)
referenceVector = Vector.new(0,1,0)
22
Data Types
Booleans
This is the last data type we have to discuss, and fortunately it will not take very long. With
Booleans we can differentiate between true and false. These states represent yes and no, and
they are used like a switch. A typical field of application is a script where the user is asked whether the scene objects' active rigid body property should be enabled or not.
Another common application is the creation of so-called flags. A flag indeed acts as a switch and
can be used to trigger events. RealFlow's Visibility option is another example for Booleans. Instead of the option's Yes and No states, we have to use true and false.
Operators
Operators are necessary to perform calculations and comparisons. Here are the most important
arithmetic operators:
Addition
15 + 17 = 32
Subtraction
64 - 30 = 34
Multiplication
10 * 12 = 120
Division
80 / 10 = 8
Exponentation
**
12 ** 2 = 144
Modulus
28 % 7 = 0
String concatenation
String repetition
"Hi" * 3 = "HiHiHi"
There are also operators for incrementing an existing value. Here, the old/existing value is stored
and we can add a new value, for example. In the next cycle this result will be used as the old value, and the increment is added again:
+=
-=
*=
/=
23
Data Types
<
Greater than
>
<=
>=
Equal
==
Not equal
!=
Conditions
Conditions are comparable to decisions. In programs this decision is made by comparing one or
more values with an operator. A condition is introduced with an if statement:
if (particleVelocity > 5.0):
do something
It is also possible to compare against more than one value. This is often used to define ranges:
if (particleVelocity >= 5.0 and particleVelocity <= 10.0):
do something
do something
You can also define an alternative case or trigger another event with the else statement:
if (particleVelocity < 5.0):
accelerate particle
else:
24
Data Types
do something
do something
Comparisons with Boolean variables are also allowed quotation marks are not allowed here:
if (objectVisibility == True):
do something
25
RealFlow's
Scripting Editors
Batch Scripts
This script type is mainly used for repetitive
tasks. Typical applications are:
27
Simulation Scripts
As the headline indicates, these scripts are created to influence simulations. One of the bestknown applications with this script type is particle swapping to simulate foam, but that is by far
not everything. Here a few more suggestions:
28
You can also see a Master tab with several entries and functions. This tab is only there for compatibility reasons with RealFlow 4. Many old scripts still use the Master tab's structure, but it is
really outdated. New scripts should always be applied to the events tree.
When your script is executed under StepsPre or StepsPost it will be executed once per substep. RealFlow uses adaptive substeps, so in the worst case, it might happen that the script is executed several hundred times per frame. With large amounts of particles or complex calculations a
simulation will become very slow.
It is therefore a good idea to execute your scripts in the frames section whenever possible at
least for testing purposes. If the result does not satisfy your requirements you can shift the script
to StepsPre or StepsPost for increasing precision.
Basic Workflows
You can add as many scripts as required. Just right-click on the appropriate event, and choose
Add Script. If you have scripts stored on your hard disk or network drives it also also possible
to load them with Add Script From File.
The checkboxes allow you to activate and deactivate a script on demand, and scripts can be shifted from one event to another with drag and drop.
29
Node-Based Scripts
RealFlow provides the possibility of creating your own RealWave deformers, standard particle
fluid solvers, and custom daemons. Each node has its own scripting editor and there you will find
several predefined sections. These sections have names like def applyForceToBody( body ) or def
computeInternalForces( emitter ), and they tell you where you have to put your script in order
to influence a certain node type. If you want to write a
daemon for MultiBodies put your script under def applyForceToMultiBody( multibody ).
custom wave deformers use def updateWave( vertices, initPositions ).
etc.
The individual editors can be found here:
Scripted daemon
Scripted RealWave deformer
Scripted standard particle fluid
Daemons shelf > Scripted > Node Params > Scripted > Edit
RealWave node > right-click > Add wave > Scripted
Emitter node > Node Params > Particles > Type > Script
30
First Steps
Coin Stacker
Number of coins
Coin's height (thickness)
Coin's diameter
Open the Batch Script editor with F10 and type:
< py >
numberOfCoins = 7
coinHeight
coinDiameter
= 1.0
= 1.5
These variables can be adjusted to our needs and changed freely, but for our basic considerations
it is better to have something catchy. Are you able to determine the variables' data types already?
Coin Positions
When a new object is added to a scene it is positioned at the scene's origin at < 0,0,0 >. When
cylinders are created they are placed at exactly the same position, but we want to stack the coins.
Therefore we have to find out the vertical position for each individual coin.
The first coin should be aligned with the scene's horizontal XZ plane (mind the axis setup!), and
the others will be stacked on top of this object. Our task is now to find a vertical offset here:
along the Y axis that will be applied to the coins.
32
7.0
6.5
6.0
5.5
5.0
4.5
4.0
3.5
3.0
2.5
2.0
1.5
1.0
0.5
1.0
From the illustration we can see that there is a common calculation rule:
offset = (coin number - 1) * 1.0 + 0.5
Coin #2
Coin #3
Coin #4
Coin #5
Coin #6
Coin #7
0 * 1.0 + 0.5
1 * 1.0 + 0.5
2 * 1.0 + 0.5
3 * 1.0 + 0.5
4 * 1.0 + 0.5
5 * 1.0 + 0.5
6 * 1.0 + 0.5
0.5
1.5
2.5
3.5
4.5
5.5
6.5
The vertical positios we get here are exactly the same as in the illustration. But what do the values
1.0 and 0.5 represent? They stand for coinHeight and coinHeight / 2. Now we have everything
for a variable-based formula:
offset = (coinNumber 1) * coinHeight + coinHeight / 2
33
Adding Coins
In the next steps, the coins are created. We will position the coins using our offset formula and we
also have to scale them accordingly. For this task a loop is added something that has been introduced in the chapter about lists. There, a loop was used to go through a lists individual elements.
Although we do not have a list here, the concept is the same. A loop is introduced this way:
for currentNumber in range(start, stop):
A good idea is to start the loop with 0, not 1, because this has advantages:
List indices always start at 0 and this makes it easier to find a specific elements.
Particle Ids also start with 0.
Therefore we can replace (coinNumber 1) through coinNumber.
< py >
for coinNumber in range(0, numberOfCoins):
coin = scene.addCylinder(50,1)
This code segment will add numberOfCoins cylinders. As you can see the cylinder object is stored in
a variable named coin. With each cycle of the loop coin will be overwritten and contains the next
cylinder. The good thing is that RealFlow does not just store the object, but we also have direct
access to all of its properties, like position and scale.
The numbers in brackets 50 and 1 are called arguments, and define the cylinder's facets and
number of height segments. This way it is possible to increase the cylinder's resolution and make
it smoother. 50 facets are a good value.
Changing Parameters
It is time to create the coins' position and scale vectors. This means that we have to replace the
cylinders' default parameter settings with our calculated values. For this purpose, RealFlow provides a very convenient method to access any parameter of an object. It is possible to read a value
and overwrite it. The commands are:
getParameter(name)
setParameter(name, value)
34
The name argument is just the parameter's name as it appears in a node's Node Params sections.
Since a parameter's name is a string it has to be enclosed between quotation marks. If you set a
value RealFlow does not only require the parameter's name, but also the new value itself. Here
you have to take care about the value's data type (integer, list, vector, etc.).
In this script, only the setParameter command is required and the new value is a position vector:
It is also possible to assemble the scale vector, because we already have the required data:
scaleVector = Vector.new(coinDiameter, coinHeight, coinDiameter)
These two vectors will now substitute the current values. The order of the script's instructions is
important. In the first step, the object is created and then we start to modify its properties using
the calculated values. Here is the entire script:
< py >
numberOfCoins = 7
coinHeight
coinDiameter
= 1.0
= 1.5
coin
= scene.addCylinder(50,1)
scaleVector
coin.setParameter("Position", positionVector)
coin.setParameter("Scale", scaleVector)
Now you can enter different initial values and execute the script with Batch Editor > Script > Run.
35
36
First Steps
Particle Swapping
Particle Swapping
Although the Coin Stacker script contains just a few lines it already uses many of the concepts we
need for most of our scripting tasks:
Variable definitions
Loops
Create (or read out) scene nodes
Manipulate values
Write back the new values
With the following script we will make use of these concepts again. Particle swapping scripts have
been discussed in many other publications, and there are also lots of free scripts available, but it is
still a very good example for beginners.
Particle swapping is used to separate particles, for example if you want to simulate foam. The
idea is to measure one or more attributes of a fluid's particles, and compare them against a
threshold. As soon as an attribute's value is greater than the associated threshold, the particle
will be moved to a second emitter. Simultaneously, the particle will be removed from the source
emitter.
This is exactly the main principle behind the Filter daemon, but scripting gives us a few more
possibilities, e.g. for combining multiple thresholds in a single script.
Scene Setup
We need a standard particle emitter, e.g. Circle, and a Container node. Both emitters must
have exactly the same Resolution, otherwise the simulation will become unstable. Then, a glass
or bin is added to catch the fluid. A Vase node is perfectly suited, but we will add this node
with a script:
Press F10 for a batch script editor and enter the following line of code:
< py >
scene.addVase(75,1,2.0)
The idea is to create an object with a higher resolution than the default node, and we did the
same in the Coin Stacker script. Scale the vase if necessary.
38
Now we only need a Gravity and k Volume daemon to remove escaping particles. Close the
batch script window without saving.
Press Ctrl/Cmd + F2 to open it, right-click on FramesPre, and choose Add Script.
You will see something like Script_embedded_407862257.
Right-click on this entry, choose Rename.
Enter Particle Swapping Script.
Now we can start with the script. The first task is to define initial variables, get access to the emitters and their particles, and to specify, which fluid attribute we want to use to filter the particles.
For this purpose, a threshold is also required. Standard particle fluids provide almost a dozen
channels, but velocity seems to be good for a first test. If necessary we can test against other
channels later as well, and introduce additional thresholds.
= scene.get_PB_Emitter("Circle01")
foamEmitter
= scene.get_PB_Emitter("Container01")
PB stands for particle-based, and if you change the emitters' names in the scene you also have
to change their names in the script.
What we will be doing now is to get the Circle01 emitter's particles and loop through them.
Within this loop, the particles' velocities are read and compared against the threshold.
39
As in the Coin Stacker script we create a loop where we have access to every particle. This time,
the script will not go through a user-defined range, but loop through particleList:
for particle in particleList:
The entire bandwidth of particle channels can be accessed this way something that also applies
to Hybrido and Dyverso (RF2015) particles; for object vertices, getVelocity() is available too.
A particle should be shifted if its speed is equal to or greater than velocityThreshold:
< pseudo code >
for particle in particleList:
particleVelocity = particle.getVelocity()
Did you spot the error in the code segment above? The condition will not work, but why? The
answer is that particleVelocity is a vector and it is not possible to compare vectors against real
numbers. We need the vector's magnitude for this job.
40
particleVelocity = particle.getVelocity()
particleVelocityMagnitude = particleVelocity.module()
I have added this bug as a reminder, to tell you once more how important it is to understand data
types. Finally, there has to be a function to identify the particle we want to remove. This can be
done via the particle's Id:
< py >
velocityThreshold = 7.5
waterEmitter
= scene.get_PB_Emitter("Circle01")
particleList
= waterEmitter.getParticles()
foamEmitter
= scene.get_PB_Emitter("Container01")
particleVelocity
= particle.getVelocity()
particleVelocityMagnitude = particleVelocity.module()
particlePosition = particle.getPosition()
particleId
= particle.getId()
foamEmitter.addParticle(particlePosition, particleVelocity)
waterEmitter.removeParticle(particleId)
Our Particle Swapping script is ready now and can be tested with different emitter Speed values, and thresholds. If you want to separate water and foam visually just assign another colour
the Container01 node:
41
This line has to be put at the beginning of the script. Python has no built-in functions for random
numbers, but some clever guy has written a so-called module or library to fix this issue. Python
comes bundled with several dozens of modules. With the import command, the module is loaded
once and then kept. random provides a long list of types, but in most cases you will only need:
random.randint(a, b)
random.uniform(a, b)
The random number will be something between a and b. You can use positive and negative values
for a and b. So, what we do is to create a new random number for every particle and add it to the
threshold.
< py >
import random
velocityThreshold = 7.5
waterEmitter
= scene.get_PB_Emitter("Circle01")
particleList
= waterEmitter.getParticles()
foamEmitter
= scene.get_PB_Emitter("Container01")
particleVelocity
= particle.getVelocity()
velocityVariation
= random.uniform(-0.25, 0.25)
particleVelocityMagnitude = particleVelocity.module()
particlePosition = particle.getPosition()
particleId
= particle.getId()
foamEmitter.addParticle(particlePosition, particleVelocity)
waterEmitter.removeParticle(particleId)
42
43
First Steps
Colour Blending
Colour Blending
The next example will be more challenging. In this script we want to create a smooth colour
blending effect from mixing particles. The original idea has been developed by Karl Richter, a TD
from Portland/Oregon. On his Vimeo account he also shares results of this technique together
with some information about how the effect has been achieved. From this description, I recreated
the following script. Here is Karl's original text:
I made a script to set one fluid's temperature to 1 and the other to 0. By averaging the temperature of each particle with some of its neighbors, this creates smoothly mixing colors.
It seems as if there is not very much information, but in fact we have almost everything we need
to get a nice colour blending effect. The temperature channel can be used, because it is only relevant for gas particles:
45
Scene Setup
The setup I have chosen here is almost the same as with the Particle Swapping script. The only
change is a second Circle emitter. I also rotated the Circle emitters a little. The Container01
node can be kept. Only the emitters are visible in this image:
Open it with Ctrl/Cmd + F2, right-click on FramesPre, and choose Add Script.
emitter3 = scene.get_PB_Emitter("Container01")
particleList1 = emitter1.getParticles()
particleList2 = emitter2.getParticles()
46
We can loop through a list's elements with a simple expression here for particleList1:
for particle1 in particleList1:
do something here
The trick is to create a new particle with the position and velocity vectors from the circle emitter's
particles and store it. Then, the temperature value is applied to the new particle, and the original particle is removed from Circle01. Temperature is a particle channel, and we have direct
access to this attribute with setTemperature(float). We also know the temperatures already. So,
let's complete the first loop the second loop uses particleList2, particle2, and setTemperature(1.0):
< py >
for particle1 in particleList1:
particle1Position = particle1.getPosition()
particle1Id
particle1Velocity = particle1.getVelocity()
= particle1.getId()
newParticle.setTemperature(0.0)
emitter1.remove(particle1Id)
And we are ready to loop through the new list. Before we proceed we should take a look at the
next steps. As written in the introduction (the recipe) we have to find particle3's neighbours
and get their temperature values. By default, these values are 0 and 1. So we need another two
variables, and these new variables have to be introduced within the loop, because they will be
reset with every new particle from the list (please go the next page for the code).
47
< py >
for particle3 in particleList3:
temperatureCurrent
neighbourParticleList = particle3.getNeighbors(0.1)
temperatureAverage
temperatureCurrent
= 0
= 0
= particle3.getTemperature()
The argument in getNeighbors(0.1) is the function's search radius All neighbours within this radius
will be added to the list. With higher values the effect will turn out smoother, but simulation time
will increase as well 0.1 m is sufficient. It is possible to initialize this value as a variable.
Temperature Calculations
The structures of neighbourParticleList and particleList1/2 are exactly the same, but we need
one more indent:
< pseudo code >
for particle3 in particleList3:
What we have here is a nested loop, and these loops can be very slow. Imagine a particle with 15
neighbours. The script has to process these 15 neighbours particles before it can proceed with the
next particle from the main list, find its neighbours, go through them again, and so on.
In order to calculate an average temperature we have to sum up the individual temperature values within the loop. This is a typical field of application for an increment operator. The average
temperature is the result of a simple calculation:
temperatureAverage = total temperature of all neighbor particles / number of neighbor particles
48
Before the loop starts we also check if neighbourParticleList contains any particles at all:
< py >
temperatureCurrent += neighbour.getTemperature()
temperatureAverage
= temperatureCurrent / (len(neighbourParticleList) + 1)
Do you remember the chapter about lists, where we were talking about how to get the number
of list elements? The key was the len(list) command. The reason, why we add 1 to the number
of neighbour particles is that we also have to include the seed particle (= particle3).
Finally, the new temperature value will be written to the neighbour particles and the currently
processed particle3. In other words: we need another loop.
< py >
for
neighbour in neighbourParticleList:
neighbour.setTemperature(temperatureAverage)
particle3.setTemperature(temperatureAverage)
That's it! This is the complete script and it is a very nice example how we can achieve interesting
results with standard procedures and some basic maths. Finally we have to find a way to visualize
the result:
49
< py >
searchRadius
= 0.1
emitter1
emitter2
emitter3
= scene.get_PB_Emitter("Circle01")
= scene.get_PB_Emitter("Circle02")
= scene.get_PB_Emitter("Container01")
particleList1 = emitter1.getParticles()
particleList2 = emitter2.getParticles()
for particle1 in particleList1:
particle1Position = particle1.getPosition()
particle1Velocity = particle1.getVelocity()
particle1Id
= particle1.getId()
newParticle
= emitter3.addParticle(particle1Position, particle1Velocity)
newParticle.setTemperature(0.0)
emitter1.removeParticle(particle1Id)
for particle2 in particleList2:
particle2Position = particle2.getPosition()
particle2Velocity = particle2.getVelocity()
particle2Id
= particle2.getId()
newParticle
= emitter3.addParticle(particle2Position, particle2Velocity)
newParticle.setTemperature(1.0)
emitter2.removeParticle(particle2Id)
particleList3 = emitter3.getParticles()
for particle3 in particleList3:
temperatureCurrent
= 0
temperatureAverage
= 0
neighbourParticleList = particle3.getNeighbors(searchRadius)
temperatureCurrent
= particle3.getTemperature()
for neighbour in neighbourParticleList:
temperatureCurrent += neighbour.getTemperature()
if (len(neighbourParticleList) > 0):
temperatureAverage = temperatureCurrent / (len(neighbourParticleList) + 1)
for neighbour in neighbourParticleList:
neighbour.setTemperature(temperatureAverage)
particle3.setTemperature(temperatureAverage)
50
Particle view of the fluid with blended colours. The emitter's "Temperature" channel is active here.
Custom-Tailored
User Interfaces
= 1.0
coinDiameter
= 1.5
Our first action is to initialize the GUI with the following command:
< py >
guiForm = GUIFormDialog.new()
In the second step, the input fields, their names, and default values are added to the GUI dialogue.
53
For this action, a fixed notation is required where the value's data type is included:
< py >
guiForm.addIntField("Number of coins", 7)
The strings between the quotation marks do not represent the variables' names, but the names of
the input fields, comparable to RealFlow's parameter names. These names have to be unique.
Here is a preview of our GUI in RealFlow 2015:
Once we have made our settings we can process the values. This part is introduced with the statement below. The if condition is often used to write out a message, for example when the GUI has
been closed with the Cancel button:
< pseudo code >
if (guiForm.show() == GUI_DIALOG_ACCEPTED):
read the field values and store them in variables (see below)
proceed with the script's functions
else:
Reading the field values always follows the same scheme. The data type does not play anymore
role here, because it has been introduced with the creation of the GUI already. This is also the moment where the actual variable names will be used, as you will see on the following page.
54
Please double-check if the strings here are exactly the same as in the add...Field() commands,
because otherwise you will receive a syntax error:
< py >
if (guiForm.show() == GUI_DIALOG_ACCEPTED):
= guiForm.getFieldValue("Coin height")
coinDiameter
= guiForm.getFieldValue("Coin diameter")
Now that we have completed the definition of variables we can add the rest of the Coin Stacker
script.
< py >
numberOfCoins = 7
coinHeight
= 1.0
guiForm
= GUIFormDialog.new()
coinDiameter
= 1.5
guiForm.addIntField("Number of coins", 7)
if (guiForm.show() == GUI_DIALOG_ACCEPTED):
coinDiameter
coinHeight
= guiForm.getFieldValue("Coin height")
= guiForm.getFieldValue("Coin diameter")
coin
offset
scaleVector
= scene.addCylinder(50,1)
coin.setParameter("Position", positionVector)
coin.setParameter("Scale", scaleVector)
else:
55
addFloatField()
addStringField()
addListField()
addVectorField()
addBoolField()
We also have fields for adding files, directories, or choosing scene nodes:
addFileField()
addDirectoryField()
addObjectField()
else:
The GUI's velocityThreshold will replace the original definition we had at the beginning of the
simulation events script. This variable has to be removed, because it will simply overwrite the GUI
value. Now, we can start the simulation, but we receive an error telling us that velocityThreshold
is not defined. What went wrong here? It is obvious that the variable has not been transferred.
56
will receive an error. We need a method to make the variable global and available to the simulation events script as well. For this purpose, the SDK provides a dedicated pair of functions:
setGlobalVariableValue(string, any)
getGlobalVariableValue(string)
Both commands are members of the scene class. The string is the variable's name as it appears
in the script, but set between quotation marks "velocityThreshold". any tells us that we can
declare any variable, or better: data type, as global. Our GUI script has to be extended by the following line:
< py | batch >
if (guiForm.show() = GUI_DIALOG_ACCEPTED):
scene.setGlobalVariableValue("velocityThreshold", velocityThreshold)
You might ask why velocityThreshold appears twice in the global variable's argument?
In the first case it is treated as a string and it is the name of the variable.
In the second case we use the variable itself, and this variable stores a value.
The stored value is what we get from the user's input (2.5 by default).
Another, shorter notation might make the idea clearer:
scene.setGlobalVariableValue("velocityThreshold", guiForm.getFieldValue("Velocity threshold"))
Now we have to switch to the Particle Swapping script under Simulation Flow and add a new
line at the beginning of the script:
velocityThreshold = scene.getGlobalVariableValue("velocityThreshold")
What we have done is to read the global variable and its value, and make it local again:
velocityThreshold = get the global velocityThreshold variable's value = 2.5
57
The problem is that we do not have direct access to these emitters, because they are not stored
by their names, but as an internal structure a reference. We can see this when we print the list's
elements:
for emitter in emitterList:
scene.message(str(emitter))
= scene.get_PB_Emitters()
emitterName = emitter.getName()
emitterNameList.append(emitterName)
58
Now, emitterNameList contains two strings, and can we can print them to the GUI dialogue:
< py >
guiForm = GUIFormDialog.new()
guiForm.addListField("Water", emitterNameList, 0)
guiForm.addListField("Foam", emitterNameList, 1)
guiForm.addFloatField("Velocity threshold", 2.5)
waterEmitterIndex = guiForm.getFieldValue("Water")
foamEmitterIndex
= guiForm.getFieldValue("Foam")
waterEmitterName
= emitterNameList[waterEmitterIndex]
foamEmitterName
= emitterNameList[foamEmitterIndex]
waterEmitterName and foamEmitterName can now be stored as global variables, and transferred to
the simulation script. There we use the names to finally get the emitters.
< py >
scene.setGlobalVariableValue("waterEmitterName", waterEmitterName)
scene.setGlobalVariableValue("foamEmitterName", foamEmitterName)
scene.setGlobalVariableValue("velocityThreshold", velocityThreshold)
59
= scene.get_PB_Emitters()
emitterName = emitter.getName()
emitterNameList.append(emitterName)
guiForm = GUIFormDialog.new()
guiForm.addListField("Water", emitterNameList, 0)
guiForm.addListField("Foam", emitterNameList, 1)
guiForm.addFloatField("Velocity threshold", 2.5)
if (guiForm.show() == GUI_DIALOG_ACCEPTED):
waterEmitterIndex = guiForm.getFieldValue("Water")
foamEmitterIndex
= guiForm.getFieldValue("Foam")
waterEmitterName = emitterNameList[waterEmitterIndex]
scene.setGlobalVariableValue("waterEmitterName", waterEmitterName)
scene.setGlobalVariableValue("velocityThreshold", velocityThreshold)
foamEmitterName
= emitterNameList[foamEmitterIndex]
scene.setGlobalVariableValue("foamEmitterName", foamEmitterName)
= scene.getGlobalVariableValue("waterEmitterName")
= scene.getGlobalVariableValue("foamEmitterName")
velocityThreshold = scene.getGlobalVariableValue("velocityThreshold")
waterEmitter
= scene.get_PB_Emitter(waterEmitterName)
particleList
= waterEmitter.getParticles()
foamEmitter
= scene.get_PB_Emitter(foamEmitterName)
60
Create a GUI where we can specify the FLW scene files for the batch simulation.
Store the paths to the FLW files in a list.
Loop through the list, load and open the FLW files.
Start the simulations one by one.
The name indicates that we will be creating a batch script here. Open the Batch Script editor
with F10.
Initial Variables
When we take another look at the task list above then it seems as if we need just two initial variables: a list where the paths to the FLW files will be stored, and the number of GUI fields for the
FLW paths:
< py >
projectFiles
= []
numberOfFields = 10
61
The GUI
The structure of the GUI is really straight-forward. All we need are 10 fields where we can specify
the FLW files' paths. A loop is a quick solution, but we have to consider a few things. With a simple loop all fields will have exactly the same name, and this is not allowed. We have to keep the
fields separated, because every field contains its own file path.
What we can do is to use the loop's current index and create a name out of it, e.g. Project file
01, Project file 02, , Project file 10. The leading 0 is certainly not necessary, but a nicely
formatted GUI does not only look better, it is also a good opportunity to show you how to combine strings.
The first field here is Project file 01 and therefore the loop will not start with 0 as usual, but
with 1:
< py >
guiForm = GUIFormDialog.new()
for fieldNumber in range(1, numberOfFields + 1):
Strings are always enclosed in quotation marks, but fieldNumber is an integer. We have to convert
the number into a string with
str(fieldNumber)
The first table in the chapter about operators contains an entry called string concatenation,
and this is exactly what we need here: it is a simple + operator. It is also necessary to differentiate
between numbers smaller than 10 and 10 (or greater):
< py >
62
A third loop will go through the entries of the projectFiles list, load the FLW files, and start the
simulation. We also have to check if there are empty entries in projectFiles, because maybe we
want to simulate just 3 or 4 project files. This can be achieved with an "is not" operator:
if (currentProject != ""):
The line above is read as: If the content of currentProject is not empty. We also print a message
to give some information about the currently running simulation (see next page).
63
currentProject in projectFiles:
if (currentProject != ""):
scene.load(currentProject)
scene.simulate()
You should now be able to assemble the entire script from the previous explanations and code
snippets.
64
Crown Splashes
Crown Splashes
Crown Splashes
We have gathered a lot of scripting knowledge already and now it is time for a more complex
project. RealFlow 2015 provides a really cool new daemon called Crown. A viewport gizmo with
editable control points is used to determine the number of tendrils and draw the splash's shape.
The idea behind the new Crown daemon is a script I have written for RealFlow 2014. I wanted
to have a tool for the creation of paint-like splashes with tendrils, but of course my program did
not have all these nice viewport features. I have been adding other functions instead functions
which are missing with the daemon. The script's concept is also completely different and here I
present a downgraded version, because waterline magazine Scripting with RealFlow is a publication for beginners. Despite of the magazine's beginner's approach you should not be under
the illusion that the following scripts came out of thin air. It will take you some time to understand what happens. And the used formulas and methods are the result of thinking! Sometimes it
also requires time, a pencil, and paper to make sketches and to do some basic maths.
The creation of artificial and user-controllable splashes, on the other hand, is a fascinating project, and this is the reason why I chose to discuss it. Of course you can improve the script, add
more features, and change it to your needs.
base radius
width (thickness)
number of tendrils
velocity
randomization
start and end time
splash position.
From now on I will use shortened variable names, but you should still be able to recognize their
meaning.
66
Crown Splashes
Force or Velocity?
We also have to make a fundamental decision now about whether we want to use forces or velocities for accelerating the particles. The main difference lies in simulation speed. A force-based
approach uses a scripted daemon, and the appropriate functions are executed per simulation
step. This makes our daemon very accurate, but rather slow. A daemon can also be placed anywhere easily, deactivated on demand, and combined with other forces.
A velocity-driven method is added to the Simulation Flow panel's events tree, and we will get a
sufficient level of quality under FramesPre. This will make the script much faster and with very
large amounts of particles we will be able to create more versions in less time. Forces also evolve
slowly, while velocities act immediately.
Since time is always the most critical part in simulations we want to focus on the velocity-based
method.
Scene Setup
Our starting point is a shallow puddle of water. The fluid is calm and we have an initial state. In
this example a standard particle emitter is used. Dyverso fluids are also possible, but at the time
this magazine has been written, this solver type has not been fully supported by RealFlow's SDK.
But, it should be no problem to translate the scripts for Dyverso. The scene's Gravity daemon
can be disabled for the following simulations.
Defining a Ring
The entire script is mainly about finding and separating particles. Once they are stored we can
treat them individually. First of all we have to look for particles within a ring. These particles will
be the seeds for the splashes.
67
Crown Splashes
To find the appropriate candidates we have to check the particles' distances to the ring's centre.
This means that we need a reference point to determine the centre, and this is done with a helper
object a null. A null's default position is < 0,0,0 > (hPos) and we keep this position for now:
A particle is inside the specified ring if its distance from the centre is inside an area around a
circle with a given radius.
We only use the horizontal values, because a particle's height plays no role. This can be
achieved by setting a vector's horizontal component to 0 (see hPosH and pPosH variables).
Furthermore we have to store the particles from inside the ring, because we want to accelerate them separately, while the remaining particles should not be moved.
splash radius
< 0,0 >
splash width
= 0.5
= 0.05
splashParticles = []
helper
= scene.getObject("Null01")
hPosH
= Vector.new(hPos.getX(), 0, hPos.getZ())
hPos
emitter
particleList
= helper.getParameter("Position")
= scene.get_PB_Emitter("Sphere01")
= emitter.getParticles()
68
Crown Splashes
In the following loop we will calculate a particle's distance from the helper object. If this distance
is inside the ring the particle is added to splashParticles:
< py | batch >
for particle in particleList:
pPos
p_hDistance = pPosH.distance(hPosH)
= particle.getPosition()
pPosH
= Vector.new(pPos.getX(), 0, pPos.getZ())
splashParticles.append(particle)
scene.setGlobalVariableValue("splashParticles", splashParticles)
This is already a very good intermediate result, but where do we have to place this code segment?
It will be outsourced to a batch script, because it contains all initial variables we need for the GUI
and the definition of the ring is also a process that has to be finished before the simulation starts.
For our first test we just want to start with an acceleration along the positive vertical axis here it
is Y. If you work with a Z-based setup use the Z axis. Of course, a velocity value is needed as well:
< py | framespre >
splashParticles = scene.getGlobalVariableValue("splashParticles")
velV
= 1.0
69
Crown Splashes
The code can be shortened by combining the last two lines. Of course, the following notation is
valid for other variables as well:
< py | framespre >
Now, deactivate the Gravity daemon, set the last simulation frame to 50, and start a first test.
What we get is a ring-shaped structure and we can also observe that our definition of splash particles works as expected. Currently, the particles are attracted during the entire simulation range,
and there is no counteracting force, e.g. Gravity, because we want to see the pure result of our
script.
= 20
scene.setGlobalVariableValue("startFrame", startFrame)
scene.setGlobalVariableValue("stopFrame", stopFrame)
70
Crown Splashes
In the "FramesPre" part of the script, the startFrame and stopFrame variables will be read:
< py | framespre >
splashParticles = scene.getGlobalVariableValue("splashParticles")
curFrame
startFrame
stopFrame
= scene.getCurrentFrame()
= scene.getGlobalVariableValue("startFrame")
= scene.getGlobalVariableValue("stopFrame")
There is just a vertical component, while the horizontal values are both 0. It seems as if we only
have to define X and Z values, right? Wrong! We have to consider the particles' positions and
motion directions, because some particles will receive negative velocities, while others require
positive values. But how can this be accomplished?
The keyword is vector normalization. When a vector is normalized you can also think of this
process as finding its direction. The normalized vector's components will help us to determine in
which direction a particle has to be accelerated. Although this is a very reliable method we are
facing one problem here: the usage of normalized vectors only works as long as our null helper is
located at in the scene's origin at < 0,0,0 >.
Fortunately, this can be solved easily by taking the helper object's position into consideration. We
need a velocity direction (velDir) from the splash's centre to its outside:
velDir = pPosH hPosH # particle position vector - helper position vector
velDir.normalize()
Since the helper's postion is used here it must be declared as a global variable as well. Again, the
horizontal components are enough (= hPosH)
71
Crown Splashes
We will also outsource the initial velocity definitions to the batch script, because later we want
them to be part of the GUI. Add these lines to the existing script:
< py | batch >
velH = 1.7 # The particles' initial vertical velocity
scene.setGlobalVariableValue("hPosH", hPosH)
And here is the complete "FramesPre" part of our script so far, where we use the normalized direction vectors. It will substitute the current code:
< py | framespre >
splashParticles = scene.getGlobalVariableValue("splashParticles")
velH
= scene.getGlobalVariableValue("velH")
hPosH
= scene.getGlobalVariableValue()
velV
= scene.getGlobalVariableValue("velV")
curFrame
startFrame
stopFrame
= scene.getCurrentFrame("hPosH")
= scene.getGlobalVariableValue("startFrame")
= scene.getGlobalVariableValue("stopFrame")
pPosH
= splash.getPosition()
splash.setVelocity(velVec)
Now it is possible to place the helper object anywhere inside the puddle and the particles will be
accelerated correctly. Since we can accelerate the horizontal and vertical components individually we are able to control the crown's width. Our test uses a "Sheeter" daemon to fill the occurring holes, and shows the typical shape. We can proceed with the next task: the creation of the
splash's tendrils.
72
Crown Splashes
The script now creates the typical crown splash shape and is position-independent.
Tendril Creation
Currently, all particles receive the same velocity value, but for the creation of tendrils we have to
modify the velocities of specific particles and make them faster than the rest of the splash. The
definition of the splash's tendrils is definitely the most difficult part, and requires some more advanced techniques. We also have to consider simulation speed, so we should look for an economic
solution. Here are our subtasks:
73
Crown Splashes
A full circle has 360 degrees. With, for example, eight tendrils, a tendril seed is placed every
360 / 8 = 45
r
X
a)
= 360.0 / numTendrils
Circle Maths
What we need now are the X and Z coordinates of the points on the circle. Again, height plays no
role. We therefore have to find a method to describe the position of every point on a circle dependent on a given angle: degPerTendril.
Maybe you still remember your time at school and there you have been dealing with the so-called
unit circle a circle with a radius of 1. The interesting thing about this certain type of circle is that
it is used to illustrate sine and cosine functions. Just do a quick Internet search for:
sine unit circle
There you will find a detailed explanation. Here we just want to complete our illustration.
74
Crown Splashes
Any coordinate on a unit circle can be found with sine, cosine, and and angle (a):
r=1
cos ()
sin ()
There is one thing we have to consider: instead of degrees the radians is required. The formula
for converting degrees into radians is:
radians = degPerTendril * Pi / 180.0
The two equations above are multiplied with splashRadius (r) to get a result for arbitrary circles.
And of course, we have to consider the splash's centre, indicated by the helper object. When we
work with mathematical functions a certain module is required:
import.math
75
Crown Splashes
The coordinates from the previous page are just for a single degPerTendril angle, but we need
numOfTendrils angles. A loop will solve this:
for i in range(0, numOfTendrils):
zCoord
xCoord
These positions will be added to a separate list (tendrilSeeds). Since the previous descriptions are
rather complex we want to take a look at the entire batch script so far. New code segments are
printed in blue:
< py | batch >
import math
# Define all initial variables, get the particles, and the helper object's position
startFrame
= 0
velH
= 1.7
stopFrame
velV
splashRadius
splashWidth
numOfTendrils
tendrilSeeds
= 20
= 2.0
= 0.5
= 0.05
= 8
= []
splashParticles = []
helper
= scene.getObject("Null01")
hPosH
= Vector.new(hPos.getX(), 0, hPos.getZ())
hPos
= helper.getParameter("Position")
emitter
particleList
= scene.get_PB_Emitter("Sphere01")
= emitter.getParticles()
pPos
p_hDistance = pPosH.distance(hPosH)
pPosH
= particle.getPosition()
= Vector.new(pPos.getX(), 0, pPos.getZ())
splashParticles.append(particle)
76
Crown Splashes
zCoord
xCoord
tPosH
tendrilSeeds.append(tPosH)
# Define the global variables
scene.setGlobalVariableValue("startFrame", startFrame)
scene.setGlobalVariableValue("stopFrame", stopFrame)
scene.setGlobalVariableValue("velH", velH)
scene.setGlobalVariableValue("velV", velV)
scene.setGlobalVariableValue("hPosH", hPosH)
scene.setGlobalVariableValue("splashParticles", splashParticles)
In the next step we loop through all particles again, but also through tendrilSeeds to calculate
the distance between particles and seeds (= nested loop). We also need another list where we
can store the found particles. And we must not forget to add the individual tendril seed particles
to the splashParticles list, because searchRadius can be greater then splashWidth.
splashWidth
searchRadius
77
Crown Splashes
This is the last code segment of the batch script. I have added a few more annotations on the
script below:
< py | batch >
# Add these new variables to the initial variable definition
searchRadius = 0.05
tendrilIds
= []
# Add this nested loop directly before the definition of the global variables
for particle in particleList:
pPos
= particle.getPosition()
p_sDist = pPosH.distance(tPosH)
tendrilIds.append(particle.getId())
splashParticles.append(particle)
scene.setGlobalVariableValue("tendrilIds", tendrilIds)
The if-condition defines the area in which we search for particles, but the last line is more interesting. Here, we do not store particles, but only their Ids. This trick will help us to avoid a
time-consuming nested loop at simulation time.
The eight big spots are the tendril positions (splashWidth = 0.05, searchRadius = 0.15).
A blossom-shaped splash appears during the simulation, but the tendrils are not yet created.
78
Crown Splashes
What we have scripted here is a very versatile approach. Imagine a spline, e.g. a spiral. In such a
case we can read out the horizontal coordinates from the spline's control points directly. There is
no need to search for these values with sine and cosine. Then, the control points are used as seeds
for pulling out tendrils at specific positions. With the knowledge from this chapter you should be
able to write such a script already!
splashParticles contains all particles inside the ring including the tendril particles.
tendrilIds contains the Ids of the particles around a tendril seed within searchRadius.
This differentiation makes it possible to accelerate the tendril particles separately from the splahes. For this purpose a factor is introduced. When the script detects the Id of a tendril particle a
factor of 1.25 is used, if it is a ring splash particles the factor is 0.75:
< py | framespre >
splashParticles = getGlobalVariableValue("splashParticles")
tendrilIds
= getGlobalVariableValue("tendrilIds")
splashId = splash.getId()
else
: factor = 0.75
In the next step, the factor has to be multiplied with the user-defined velocites and the XZ components of the normalized direction vector velDir.
To give you a complete view of this part I have added the last code segment to the next page.
79
Crown Splashes
Append the following code to the previous part of the "FramesPre" script:
< py | framespre >
sPosH
velDir
= splash.getPosition()
= sPosH - hPosH
velDir.normalize()
velVec
splash.setVelocity(velVec)
The script's mode of operation is as follows: we just go through our splash/ring particles, get their
positions as usual, and normalize these vectors. We also get the Id and then we check if the current Id is stored in the tendrilIds list. If this is the case, the particle gets an extra push to make it
faster than the remaining splash particles. Here I am using fixed factor values, but of course you
can add this parameter to a GUI as well.
Below is a rendered version with 10 tendrils and "FPS Output" set to 100. The tendrils' droplets
are the result of a "Surface Tension" daemon.
80
Crown Splashes
Randomization
At the moment we have a perfect splash, where the distances between the tendrils is fixed, and
the accelrating forces are also the same for all particles. The resulting crown is nice, but too artificial for many applications. We need a more random appearance. Adding some noise is not difficult, and we did something very similar in the Particle Swapping script already, but here we have
to be careful:
If the random speed variation is added to the velocity vector velVec in the FramesPre script we
might end up with something chaotic, because the particles' speed will change every time the calculated velocity is applied. It is better to define a fixed initial random value for every particle and
apply it during the simulation.
We therefore have to start with the batch script, load the "random" module, and define the
range. We also need another list called velVariation. The number of entries in velVariation has
to be equal to the number of particles in splashParticles:
< py | batch >
import math, random
# Add these new variables to the initial variable definition
velRange
= 0.2
velVariation = []
# Add this loop directly before the definition of the global variables
for entry in splashParticles:
velVariation.append(random.uniform(-velRange, velRange))
scene.setGlobalVariableValue("velVariation", velVariation)
In the "FramesPre" section the required global variable is read as usual. What we also need is a
method to get a splash particle's associated random velocity from the velVariation list. To do
this, a counter is introduced:
counter = 0
81
Crown Splashes
Inside the loop, 1 is added to the counter with every new particle. This process is also called incrementation (see Operators). Now it is possible to use the counter as an index to get the list positions. Since the counter is updated with every step of the loop we can go through all list elements
automatically:
-0.13
0.07
0.12
0.18
-0.02
-0.05
0.13
-0.17
0.11
0.16
-0.04
-0.10
Index
10
11
counter
10
11
= 0
else
counter += 1
That's it! We are ready and our script contains all the features we have defined at the beginning
of the chapter. We are able to user-define and control crown splashes, we have found a very fast
approach, and avoided nested loops during simulation time. Of course, there is also room for improvements, but the most important goal with this project was to show you concepts.
82
Crown Splashes
= 0
velH
= 1.7
stopFrame
velV
splashRadius
splashWidth
searchRadius
numOfTendrils
velRange
degPerTendril
tendrilSeeds
tendrilIds
= 15
= 2.0
= 0.5
= 0.05
= 0.05
= 8
= 0.5
= 360.0 / numOfTendrils
= []
= []
splashParticles = []
velVariation
= []
hPos
= helper.getParameter("Position")
helper
hPosH
emitter
particleList
= scene.getObject("Null01")
= Vector.new(hPos.getX(), 0, hPos.getZ())
= scene.get_PB_Emitter("Sphere01")
= emitter.getParticles()
pPos
p_hDistance = pPosH.distance(hPosH)
pPosH
= particle.getPosition()
= Vector.new(pPos.getX(), 0, pPos.getZ())
splashParticles.append(particle)
# Get the positions of the tendril seeds
for i in range(0, numOfTendrils):
zCoord
xCoord
tPosH
tendrilSeeds.append(tPosH)
83
Crown Splashes
pPos
pPosH
= particle.getPosition()
= Vector.new(pPos.getX(), 0, pPos.getZ())
p_sDist = pPosH.distance(tPosH)
if (p_sDist <= searchRadius):
tendrilIds.append(particle.getId())
splashParticles.append(particle)
velVariation.append(random.uniform(-velRange, velRange))
scene.setGlobalVariableValue("startFrame", startFrame)
scene.setGlobalVariableValue("stopFrame", stopFrame)
scene.setGlobalVariableValue("velH", velH)
scene.setGlobalVariableValue("velV", velV)
scene.setGlobalVariableValue("hPosH", hPosH)
scene.setGlobalVariableValue("splashParticles", splashParticles)
scene.setGlobalVariableValue("tendrilIds", tendrilIds)
scene.setGlobalVariableValue("velVariation", velVariation)
= scene.getCurrentFrame()
velV
= scene.getGlobalVariableValue("velV")
velH
hPosH
= scene.getGlobalVariableValue("velH")
= scene.getGlobalVariableValue("hPosH")
splashParticles = scene.getGlobalVariableValue("splashParticles")
tendrilIds
= scene.getGlobalVariableValue("tendrilIds")
stopFrame
= scene.getGlobalVariableValue("stopFrame")
startFrame
velVariation
counter
= scene.getGlobalVariableValue("startFrame")
= scene.getGlobalVariableValue("velVariation")
= 0
84
Crown Splashes
sPosH
= splash.getPosition()
velDir.normalize()
splash.setVelocity(velVec)
counter += 1
The GUI
Randomize the tendril's angles.
Create propagating tendril patterns.
Translate FramesPre into a scripted daemon.
Particles below the helper object should not be affected.
Take the particles' vertical positions into account.
Use spline control point positions or object vertices as tendril seeds.
Add a time-dependent function for the velocity vector to start with very low values, and increase them over time.
You should now be able to implement the above suggestions and customize your crown splash
script. For some features (propagating tendrils, tendrils from spline control points, and vertices) it
is better to discard splashParticles, and to work with tendrilSeeds only. Otherwise you will get
strange results.
85
Crown Splashes
Axis Setups
So far, all scripts have been developed for a Y-based axis setup, but this can be considered a special case. If your scripts are for personal or internal use only then you do not have to care about
this issue too much, because you just have to swap Y and Z in your vector definitions:
verticalVelocity= 2.5
velocityVector = Vector.new(0, verticalVelocity, 0)
RealFlow also provides a method for detecting the scene's axis setup as shown in this batch script:
< py >
verticalVelocity = 2.5
axisSetup = scene.getAxisSetup()
if (axisSetup == AXIS_SETUP_ZXY):
else:
scene.message(str(velocityVector))
If your axis setup is ZXY with Z as the vertical axis the result is:
0.000000 0.000000 2.500000
86
Export Manager
= GUIFormDialog.new()
The last line of code is the most interesting, but it should be familiar to you already, because it is
what we call string concatenation. Here is a preview of the GUI with a few emitters:
88
emitter = emitterList[i]
emitterName = emitter.getName()
exportPath = guiForm.getFieldValue(fieldName)
The i variable is used as an index to access the list entries, but this is also a common concept now.
Then, the fields' names are assembled, and finally read out and stored in exportPath. We are almost ready with this loop, but need to get an emitter's export resources:
By default, only Particle cache (.bin) is active, but maybe you also want to write RPC or ABC
files. The script has to detect which formats are enabled, because only these will be changed.
With this command we can check a node's export state the result is printed below:
exportResources = emitter.getAllExportResourceValues()
(1, 'Particle cache (.bin)', True)
89
Each entry has its own index ranging from 0 to 2. The attribute of interest is number 2, because
it tells us a whether a format is enabled (True) or nor (False). It is possible to directly access this
field with its index. The first number is also important, because we can use it to address a specific
export resource.
= GUIFormDialog.new()
if (guiForm.show() == GUI_DIALOG_ACCEPTED):
for i in range(0, len(emitterList)):
emitter
= emitterList[i]
fieldName
emitterName = emitter.getName()
exportPath
= guiForm.getFieldValue(fieldName)
if (exportPath != ""):
exportResources = emitter.getAllExportResourceValues()
if entry[2] == True:
emitter.setExportResourcePath(entry[0], exportPath)
Here we go through the entries of the node's export resources and address the relevant attributes
0 and 2. Finally, the content from exportPath is inserted.
90
To be more precise, it is the change of position between two points in time. In physics, changes
are represented with a D symbol:
velocity =
Ds
Dt
Our script has to calculate the differences in position between two frames (f0, f1) to get the distance, but we have to be careful with the order:
Ds
= positionf1 - positionf0
This also applies to time, but here it is much easier, because the time span between two frames is
always the same it is 1 frame. What we have to do is to convert this fixed time step to seconds.
The length of the time span depends on the adjusted frame rate:
Dt
= 1.0 / FPS
92
Zooming in reveals that the particles are not only located at the polygons' intersections, but also
in the triangles' centres. This makes it difficult to transfer the calculated velocities to the particle layer, because we do not have enough information from the vertices. It is possible to average
velocities from a triangle's vertices and use the values for the centre particles, but this approach is
too complex for our conceptual script.
We therefore go a different route and
93
= scene.getRealwave()
emitter
= scene.get_PB_Emitter("Container01")
rwVertices = rwSurface.getVertices()
nullVec
= Vector.new(0.0,0.0,0.0)
scene.enablePaint(False)
emitter.removeAllParticles()
for vertex in rwVertices:
vPos = vertex.getPosition()
emitter.addParticle(vPos, nullVec)
scene.enablePaint(True)
The removeAllParticles() statement clears the emitter, because otherwise the number of particles will be increased with every start of the simulation. With scene.enablePaint(False) the creation of the particles will be very fast. Since we want to see the result during simulation, we have
to reactivate RealFlow's paint function.
In terms of scripting, the calculation of the changes in position is the most difficult part. The challenge is to store and keep the RealWave's vertex positions from the previous frame and use them
in the current frame. A global variable is required for the positions:
< py | simulationpre >
# Add this line to the initial variable definitions
vPositionsOld = []
vPos = vertex.getPosition()
vPositionsOld.append(vPos)
emitter.addParticle(vPos, nullVec)
scene.setGlobalVariableValue("vPositionsOld", vPositionsOld)
scene.enablePaint( True )
94
= scene.get_PB_Emitter("Container01")
rwVertices
= rwSurface.getVertices()
rwSurface
dT
= scene.getRealwave()
= 1.0 / scene.getFps()
emitter.removeAllParticles()
vPos = vertex.getPosition()
In the next step we subtract the vertex's old position from its current position. This is again done
by using the counter method from Crown Splashes.The counter is updated with every loop cycle
and used as an index to extract the elements of vPositionsOld:
< py | framespre >
# Add this line to the initial variable definitions
counter = 0
vPos
...
= vertex.getPosition()
posDiff = vPos -
vPositionsOld[counter]
counter += 1
95
= scene.get_PB_Emitter("Container01")
rwVertices
= rwSurface.getVertices()
rwSurface
dT
counter
= scene.getRealwave()
= 1.0 / scene.getFps()
= 0
emitter.removeAllParticles()
for vertex in rwVertices:
vPos
= vertex.getPosition()
vel
posDiff
= vPos -
vPositionsOld[counter]
particle.freeze()
vPositionsCur.append(vPos)
counter += 1
scene.setGlobalVariableValue("vPositionsOld", vPositionsCur)
The orange line does not only create a new particle, but stores it in a variable. You know this notation from the Colour Blending script! In the next line, the particle's position is frozen, because
we have applied a velocity and this means that the particle will be moving. Finally, we store the
current position.
The last line of the script uses a trick to save the current positions for the next frame, where they
will be treated as the old positions:
scene.setGlobalVariableValue("vPositionsOld", vPositionsCur)
96
97
waterline magazine
Scripting with RealFlow
2015 by Thomas Schlick | Next Limit Technologies
Commercial distribution is prohibited. Translations only with permission.