3D rendering and animations
MultibodyComponents.jl has an automatic 3D-rendering feature that draws a mechanism in 3D. This can be used to create animations of the mechanism's motion from a solution trajectory, as well as to create interactive applications where the evolution of time can be controlled by the user.
The functionality requires the user to install and load one of the Makie backend packages, e.g.,
using GLMakie # Preferredor
using WGLMakieBackend choice
GLMakie and WGLMakie produce much nicer-looking animations and are also significantly faster than CairoMakie. CairoMakie may be used to produce the graphics in some web environments if constraints imposed by the web environment do not allow any of the GL alternatives. CairoMakie struggles with the Z-order of drawn objects, sometimes making bodies that should have been visible hidden behind bodies that are further back in the scene.
After that, the render function is the main entry point to create 3D renderings. This function has the following methods:
render(model, prob::ODEProblem): this method creates an interactive figure corresponding to the mechanisms configuration at the specified initial condition.render(model, solution): this method creates an animation corresponding to the mechanisms evolution in a simulation trajectory.scene, time = render(model, solution, t::Real): this method opens an interactive window with the mechanism in the configuration corresponding to the timet. Displaysceneto display the interactive window, and change the time by either dragging the slider in the window, or write to the observabletime[] = new_time.
Colors
Many components allows the user to select with which color it is rendered. This choice is made by providing a 4-element array with color values in the order (RGBA), where each value is between 0 and 1. The last value is the alpha channel which determines the opacity, i.e., 1 is opaque and 0 is invisible.
Rendering the world frame
The display of the world frame can be turned off by setting world.render => false in the variable map.
Tracing the path of a frame in 3D visualizations
The path that a frame traces out during simulation can be visualized by passing a vector of frames to the render function using the traces keyword, e.g., render(..., traces=[frame1, frame2]).
Camera controls
The camera controls are inherited from Makie, see their documentation for more information. Of particular interest may be the keyboard shortcuts x, y, z, by holding one of these keys and dragging the mouse, the camera will rotate around the corresponding axis. Use keyword argument show_axis = true to function render or pass parameter world.render => true to ODEProblem to display plot axes and/or world axes in the plot.
Shape primitives – declarative rendering for components
MultibodyComponents uses an object-oriented rendering system. A component that wants to declare how it should be visualized includes one or more shape sub-components and sets their variables through equations. The rendering system then walks the component tree and draws every shape it finds.
All shape types are defined in dyad/shape.dyad and extend a common Shape base. The available primitives are:
| Shape | Typical use |
|---|---|
CylinderShape | Rods, axles, wheels |
SphereShape | Point masses, bodies |
BoxShape | Prismatic joints |
ConeShape | Tapered parts |
ArrowShape | Force / torque vectors |
SpringShape | Spring coils |
Shape variables
Every shape carries the following variables, which the parent component sets in its relations section:
| Variable | Description |
|---|---|
r | 3D position of the shape origin in world frame |
R | 3×3 rotation matrix (orientation) in world frame |
r_shape | Offset from r to the actual shape origin, resolved in the shape frame |
length_direction | Primary axis direction (unit vector in shape frame) |
width_direction | Secondary axis direction (unit vector in shape frame) |
length, width, height | Dimensions along the respective directions |
color | RGBA color (parameter, 4-element array, values 0–1) |
render | Boolean parameter that toggles visibility |
Example: adding a sphere to a Body
The PlanarMechanics.Body component instantiates a SphereShape and passes its variables at the point of instantiation:
component Body
extends MultibodyComponents.Renderable(
color = MultibodyComponents.world_default_body_color())
parameter radius::Real = 0.1
shape = MultibodyComponents.SphereShape(
render = render, color = color,
r = [frame_a.x, frame_a.y, z_position],
R = MultibodyComponents.RR(
MultibodyComponents.axis_rotation(3, phi)),
length_direction = [0, 0, 1],
width_direction = [1, 0, 0],
length = 2 * radius,
width = 2 * radius,
height = 2 * radius
)
endFor a sphere, length, width and height are all set to the diameter. The position r lifts the 2D frame coordinates into 3D (with a configurable z_position), and R is the 3D rotation matrix corresponding to the planar angle phi.
Using multiple shapes
A single component can include several shapes. For example, SlipBasedWheelJoint uses three CylinderShape instances – one for the tire and two for cross-spokes that rotate with the wheel:
tire_shape = MultibodyComponents.CylinderShape(render = render, color = [0.1, 0.1, 0.1, 1.0])
rim1_shape = MultibodyComponents.CylinderShape(render = render, color = [0.8, 0.8, 0.8, 1.0])
rim2_shape = MultibodyComponents.CylinderShape(render = render, color = [0.8, 0.8, 0.8, 1.0])Each shape gets its own position, rotation and dimensions passed as constructor arguments. The rim shapes use a different rotation matrix that includes the wheel's rolling angle, making them spin in the animation.
Custom render! methods
If the built-in shape primitives are not enough, you can implement a custom Julia method of render! that dispatches on your component. The signature is:
function MultibodyComponents.render!(scene, ::typeof(MyComponent), sys, sol, t)
# Draw into `scene` using Makie primitives.
# `sys` – the simplified subsystem for this component
# `sol` – the ODE solution
# `t` – an Observable{Float64} (current time)
# Return `true` to signal that rendering was handled.
return true
endThe rendering system walks the model tree recursively. When render! returns true for a component, its children are not visited further, giving you full control over the subtree's appearance. Return false (or omit the method) to let the default tree-walking continue into sub-components.
Rendering API
MultibodyComponents.render Function
scene = render(model, prob)
scene, time = render(model, sol, t::Real; framerate = 30, traces = [])
path = render(model, sol, timevec = range(sol.t[1], sol.t[end], step = 1 / framerate); framerate = 30, timescale=1, display=false, loop=1)Create a 3D animation of a multibody system
Arguments:
model: The unsimplified multibody model, i.e., this is the model before any call tostructural_simplify.prob: If anODEProblemis passed, a static rendering of the system at the initial condition is generated.sol: If anODESolutionproduced by simulating the system usingsolveis passed, an animation or dynamic rendering of the system is generated.t: If a single numbertis provided, the mechanism at this time is rendered and a scene is returned together with the time as anObservable. Modifytime[] = new_timeto change the rendering.timevec: If a vector of times is provided, an animation is created and the path to the file on disk is returned.framerate: Number of frames per second.timescale: Scaling of the time vector. This argument can be made to speed up animations (timescale < 1) or slow them down (timescale > 1). A value oftimescale = 2will be 2x slower than real time.loop: The animation will be looped this many times. Please note: looping the animation using this argument is only recommended whendisplay = truefor camera manipulation purposes. When the camera is not manipulated, looping the animation by other means is recommended to avoid an increase in the file size.filenamecontrols the name and the file type of the resulting animationtraces: An optional array of frames to show the trace of.show_axis = false: Whether or not to show the plot axes, including background grid.
Camera control
The following keyword arguments are available to control the camera pose:
x = 2y = 0.5z = 2lookat = [0,0,0]: a three-vector of coordinates indicating the point at which the camera looks.up = [0,1,0]: A vector indicating the direction that is up.display: iftrue, the figure will be displayed during the recording process and time will advance in real-time. This allows the user to manipulate the camera options using the mouse during the recording.
See also loop_render
MultibodyComponents.render! Function
did_render::Bool = render!(scene, ::typeof(ComponentConstructor), sys, sol, t)Each component that can be rendered must have a render! method. This method is called by render for each component in the system.
This method is responsible for drawing the component onto the scene the way it's supposed to look at time t in the solution sol. t is an Observable. It's recommended to follow the pattern
thing = @lift begin
acces relevant coordinates from sol at time t
create a geometric object that can be rendered
end
mesh!(scene, thing; style...)Returns
A boolean indicating whether or not the component performed any rendering. Typically, all custom methods of this function should return true, while the default fallback method is the only one returning false.