3D animation
3D animation provides a way to visualize simulations in 3D.
Salabim supports the ‘classical’ 2D animation along with a 3D animation. The 3D animation is particularly useful for showing the functionality of a system to domain experts, where objects are moving up and down, like stacking systems, multi story facilities and complex road infrastructure. But also just to make a visualy more attractive presentation (video).
3D animations always run in parallel with the tkinter (2D) window, to control the simulation (pausing, exiting, etc). Also custom buttons may be installed in that window. And of course, a 2D animation as well.
As with 2D animations, animation calls can be still given when animation is actually off. In that case, there is hardly any impact on the performance.
The 3D animation uses its own coordinate system.
All 3D animation objects are so called new style animation classes.
The following 3D classes are available as of now:
Animate3dBox
Animate3dBar
Animate3dCylinder
Animate3dGrid
Animate3dLine
Animate3dObj
Animate3dRectangle
On top of that, animation of the components of a queue in 3D is accomplished with Animate3dQueue()
, similar
to the 2D AnimateQueue
.
It is possible to use the standard shape of components, which is a box of size 1 in all directions. The queue can be build up in +x, -x, +y, -y, +z, -z direction. It is possible to limit the number of component shown.
It is possible to overlay a 2d animation object (like AnimateText) over the 3D windows. This is done by adding (over3d=True) to the Animatexxx call. This feature is particularly useful for videos.
Set up
In order to enable 3D-animations, you have to specify
env.animate3d(True)
If you want to start the animation (both 2D and 3D) it is required then to do as well
env.animate(True)
Also, you might have to set the position and size of both the 2D and 3D windows, like
env.width(950)
env.height(768)
env.position((960, 100))
env.width3d(950)
env.height3d(768)
env.position3d((0, 100))
And then you have a 2D-window on the left and a 3D-window on the right.
General description
It is important to realize that all 3D-animation take place in a xyz-coordinate system, without a real notion of scaling.
The actual animation is a view from a camera which points to so call center point, which is always at z=0. The camera uses a field_of_view which is essentially the focal distance of a lens.
On top of that, there’s a light source.
Note
It might help to have a look at the OpenGL documentation for these concepts.
Finding a proper camera setting
It is usually difficult to define the camera location, direction, center point and field of view from scratch. Instead, it is common to find these parameters by using control keys. After finding the right position, etc, it is possible to print these and paste them into the code.
The following controls are available
Key |
Effect |
---|---|
<Left> |
rotate camera -1 degree |
<Right> |
rotate camera +1 degree |
<Up> |
zoom in 0.9 * |
<Down> |
zoom out 1.1 * |
<z> |
lower the camera 0.9 * |
<Z> |
raise the camera 1.1 * |
<Shift-Up> |
move camera in z-plane 0.9 * |
<Shift-Down> |
move camera in z-plane 1.1 * |
<Alt-Left> |
move camera x - 10 |
<Alt-Right> |
move camera x + 10 |
<Alt-Down> |
move camera y - 10 |
<Alt><Up> |
move camera y + 10 |
<Control-Left> |
center x - 10 |
<Control-Right> |
center x + 10 |
<Control-Down> |
center y - 10 |
<Control-Down> |
center y + 10 |
<o> |
field of view * 0.9 |
<O> |
field of view * 1.1 |
<t> |
tilt camera +1 degree |
<T> |
tilt camera -1 degree |
<r> |
rotate camera axis +1 degree |
<R> |
rotate camera axis -1 degree |
<p> |
print current camera control settings |
When <p> is pressed, it will print something like
view(x_eye=16.3435,y_eye=7.5267,z_eye=7.5267,x_center=-10.0000,y_center=0.0000,z_center=0.0000,field_of_view_y=40.5000) # t=22.8377
Just put env. in front of it, like
env.view(x_eye=16.3435,y_eye=7.5267,z_eye=7.5267,x_center=-10.0000,y_center=0.0000,z_center=0.0000,field_of_view_y=40.5000) # t=22.8377
And the camera will be positioned properly.
It is also possible to see the current values dynamically with
env.show_camera_position(True) # show in the 2D-window
or
env.show_camera_position(True, over3d=True) # show in the 3D-window
Tracking the camera
If you want the camera to follow a certain path (particularly when producing a video), it might be best to do the correct movements with the keys and track these movements.
The way to do that is
env.camera_auto_print(True)
Each time the user enters a (valid) control, it is printed out on the console (along with the time).
Once this has been done, grab the output (a number of lines starting with view(
) and put them in your code, like
env.camera_move("""\
view(x_eye=4.0000,y_eye=4.0000,z_eye=4.0000,x_center=0.0000,y_center=0.0000,z_center=0.0000,field_of_view_y=45.0000) # t=0.0000
view(x_eye=3.6000,y_eye=3.6000,z_eye=3.6000) # t=0.4957
view(x_eye=3.2400,y_eye=3.2400,z_eye=3.2400) # t=0.9443
view(x_eye=3.2961,y_eye=3.1830) # t=1.6593
view(field_of_view_y=40.5000) # t=6.4102
view(field_of_view_y=36.4500) # t=6.7709
view(field_of_view_y=84.6754) # t=8.4003
view(field_of_view_y=94.0838) # t=8.5829
view(field_of_view_y=104.5376) # t=8.7949
view(field_of_view_y=116.1529) # t=9.0388
""", lag=1)
When you now run this code, the camera will follow the controls given. The lag parameter is used to make the movement smoother.
Please refer to the reference section or the docstring for more information.
Example
This program
import salabim as sim
env = sim.Environment()
env.background_color("90%gray")
env.width(900)
env.height(700)
env.position((1000,0))
env.width3d(900)
env.height3d(700)
env.position3d((0,100))
env.animate(True)
env.animate(True)
env.animate3d(True)
env.show_camera_position()
env.show_camera_position(over3d=True)
sim.Animate3dGrid(x_range=range(-2,3), y_range=range(-2,3))
traj0 = sim.TrajectoryCircle(radius=2, vmax=1)
d0 =traj0.duration()
sim.Animate3dBox(x_len=1,y_len=1,z_len=1,color="red", x=lambda t:traj0.x(t%d0), y=lambda t:traj0.y(t%d0), z=0.5, z_angle=lambda t:traj0.angle(t%d0))
traj1 = sim.TrajectoryCircle(radius=1, vmax=1,angle0=360,angle1=0)
d1 =traj1.duration()
sim.Animate3dBox(x_len=0.5,y_len=0.5,z_len=0.5,color="green", x=lambda t:traj1.x(t%d1), y=lambda t:traj1.y(t%d1), z=0.25, z_angle=lambda t:traj1.angle(t%d1))
env.camera_move("""\
view(x_eye=4.0000,y_eye=4.0000,z_eye=4.0000,x_center=0.0000,y_center=0.0000,z_center=0.0000,field_of_view_y=45.0000) # t=0.0000
view(x_eye=3.6000,y_eye=3.6000,z_eye=3.6000) # t=0.4957
view(x_eye=3.2400,y_eye=3.2400,z_eye=3.2400) # t=0.9443
view(x_eye=3.2961,y_eye=3.1830) # t=1.6593
view(x_eye=3.2961,y_eye=3.1830,z_eye=2.9160) # t=3.3829
view(x_eye=3.2961,y_eye=3.1830,z_eye=2.6244) # t=3.5666
view(x_eye=3.2961,y_eye=3.1830,z_eye=2.3620) # t=3.7464
view(x_eye=3.2961,y_eye=3.1830,z_eye=2.1258) # t=3.8976
view(x_eye=3.2961,y_eye=3.1830,z_eye=1.9132) # t=4.1154
view(x_eye=3.2961,y_eye=3.1830,z_eye=1.7219) # t=4.3627
view(x_eye=3.2961,y_eye=3.1830,z_eye=1.5497) # t=4.5476
view(x_eye=3.2961,y_eye=3.1830,z_eye=1.3947) # t=4.7275
view(x_eye=3.3511,y_eye=3.1250) # t=5.3634
view(field_of_view_y=40.5000) # t=6.4102
view(field_of_view_y=36.4500) # t=6.7709
view(field_of_view_y=40.5000) # t=7.2002
view(field_of_view_y=45.0000) # t=7.3853
view(field_of_view_y=50.0000) # t=7.5354
view(field_of_view_y=55.5556) # t=7.6858
view(field_of_view_y=61.7284) # t=7.8731
view(field_of_view_y=68.5871) # t=8.0212
view(field_of_view_y=76.2079) # t=8.2113
view(field_of_view_y=84.6754) # t=8.4003
view(field_of_view_y=94.0838) # t=8.5829
view(field_of_view_y=104.5376) # t=8.7949
view(field_of_view_y=116.1529) # t=9.0388
view(field_of_view_y=129.0587) # t=9.2250
view(field_of_view_y=143.3986) # t=9.4486
view(field_of_view_y=129.0587) # t=11.0184
view(x_eye=4.0000,y_eye=4.0000,z_eye=4.0000,x_center=0.0000,y_center=0.0000,z_center=0.0000,field_of_view_y=45.0000) # t=11.6400
""", lag=1)
env.run(sim.inf)
will run as (only the 3D window is shown here)

3D-animation classes
General
All classes have a number of parameters, like x, x-len, z_angle. These parameters can be:
scalar, like 10
a function with zero arguments, like lambda: my_x
a function with one argument, being the time t, like lambda t: t + 10
a function with two parameters, being arg (as given) and the time, like lambda comp, t: comp.state
a method instance arg for time t, like self.state, actually leading to arg.state(t) to be called
For instance
an = Animate3dBox(x_len=lambda t: 2 *t, x=10)
And then it posible to reassign any of these parameters, like
an.x = lambda t: math.sin(t) * 10 an.x_len = 5
All classes have a visible attribute that can be dynamic as well.
Animate3dBox
This class makes a box where the user can specify the length in x-, y- and z-direction.
By default, the given position of the box (x, y and z) refers to the center of the box. However, with x_ref, y_ref, and z_ref these values can denote either the left (?_ref=-1) or right (?_ref=+1) side as. So, in order to place a box ON the z=0 plane, we can use
an = Animate3dBox(x=0, y=0, z=0, z_ref=1)
Apart from the color of the box, it is possible to specify the color of the edges. And when shaded=True, the various sides of the cube will be coloured differently.
This is an example, created with
sim.Animate3dBox(
x_len=1, y_len=1, z_len=1, z_ref=1, color="red", edge_color="white", shaded=True
)

Animate3dBar
This is like a box, but here the start and end coordinates determine the location. The rotation can also be given.
You can specify whether is hollow or has ‘lids’.
Apart from the color of the box, it is possible to specify the color of the edges. And when shaded=True, the various sides of the cube will be coloured differently.
This is an example, created with
an = sim.Animate3dBar(
x0=-2,
y0=0,
z0=0,
x1=1,
y1=-1,
z1=1,
bar_width=0.4,
color="red",
edge_color="white",
shaded=True,
)

Animate3dCylinder
This is like a bar, but now as cylinder, where the user can specify the number of sides. The rotation can also be given.
Like Animte3dBar, you can specify whether is hollow or has ‘lids’.
This is an example, created with
an = sim.Animate3dCylinder(
x0=-2,
y0=0,
z0=0,
x1=2,
y1=1,
z1=2,
radius=0.4,
number_of_sides=20,
color="red",
show_lids=False,
)

Animate3dGrid
This class is very useful to show (or debug) a 3D-animation.
It is possible to specify the grid in three dimensions, although usually only the z=0 plane will be used.
This is an example, created with
an = sim.Animate3dGrid(x_range=sim.arange(-10, 11,0.5), y_range=sim.arange(-10, 11,0.5),color="red")

Animate3dLine
Although not used often, this class makes it is possible to draw lines in a 3D-animation.
Please note that a line has no real width. If you need something ‘more substantial’, use Animate3dBar.
This is an example, created with
an0 = sim.Animate3dLine(x0=-2, y0=-2,z0=0, x1=2, y1=1,z1=1, color="red")
an1 = sim.Animate3dLine(x0=2, y0=1,z0=1, x1=0, y1=0,z1=-1, color="blue")
an2 = sim.Animate3dLine(x0=0, y0=0,z0=-1, x1=-2, y1=-2,z1=0, color="yellow")

Animate3dObj
This is an advanced class that allows a .obj file to be displayed.
As the .obj format is not very well standardized/described, some experimentation might be required.
This is an example of what can be done.

Animate3dRectangle
This class makes it possible to draw a plane with a given color. Note that it it will be always in z-plane.
Please note that the plane has no physical width.
This is an example, created with
an0 = sim.Animate3dRectangle(x0=-1, y0=-1, x1=1, y1=1, z=0, color="red")
an1 = sim.Animate3dRectangle(x0=-1, y0=-1, x1=1, y1=1, z=1, color="blue")
an2 = sim.Animate3dRectangle(x0=-1, y0=-1, x1=1, y1=1, z=2, color="yellow")

Animate3dQueue
Animate3dQueue
is like AnimateQueue
.
The equivalent of Queue.animate()
is Queue.animate3d()
.
The animation objects of a component should be returned in Component.animation3dobjects
def animation3d_objects(self, id: Any) -> Tuple:
...
This method defaults to a white, shaded, cube, with sides of length 8. The default displacement is 10 in each direction.
Animate3Queue has a direction parameter, which can be “x+”, “x-”, “y+”, “y-”, “z+” or “z-” denoting how the queue should be displayed (“x+” is the default).
Suppose we have a component, defined with
def animation3d_objects(self):
ao = sim.Animate3dSphere(radius=0.1, color=self.color)
return 0.25, 0.25, 0.25, ao
When we do then
q.animate3d(z=0.5,direction="z+")
or
sim.Animate3dQueue(q, z=0.5,direction="z+")
, we will get something like:
