Skip to content

Commit

Permalink
Merge pull request #95 from JuliaComputing/traces
Browse files Browse the repository at this point in the history
add path tracing and loop rendering option to animations
  • Loading branch information
baggepinnen authored Jun 26, 2024
2 parents 51a8052 + 6b768ad commit 2dde592
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 12 deletions.
3 changes: 2 additions & 1 deletion docs/src/examples/pendulum.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,10 @@ sol = solve(prob, Rodas4())
plot(sol, layout=4)
```

In the animation below, we visualize the path that the origin of the pendulum tip traces by providing the tip frame in a vector of frames passed to `traces`
```@example pendulum
import GLMakie
Multibody.render(model, sol, filename = "furuta.gif")
Multibody.render(model, sol, filename = "furuta.gif", traces=[model.tip.frame_a])
nothing # hide
```
![furuta](furuta.gif)
Expand Down
4 changes: 4 additions & 0 deletions docs/src/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ Many components allows the user to select with which color it is rendered. This
## 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])`.
See the Furuta-pendulum demonstration [Going 3D](@ref) for an example of this.


## Rendering API

Expand Down
70 changes: 64 additions & 6 deletions ext/Render.jl
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
module Render
using Makie
using Multibody
import Multibody: render, render!, encode, decode, get_rot, get_trans, get_frame
import Multibody: render, render!, loop_render, encode, decode, get_rot, get_trans, get_frame
using Rotations
using LinearAlgebra
using ModelingToolkit
export render
export render, loop_render
using MeshIO, FileIO
using StaticArrays

Expand Down Expand Up @@ -129,6 +129,9 @@ function render(model, sol,
up = Vec3f(0,1,0),
show_axis = false,
timescale = 1.0,
traces = nothing,
display = false,
loop = 1,
kwargs...
)
scene, fig = default_scene(x,y,z; lookat,up,show_axis)
Expand All @@ -139,30 +142,85 @@ function render(model, sol,
t = Observable(timevec[1])

recursive_render!(scene, complete(model), sol, t)
fn = record(fig, filename, timevec; framerate) do time
t[] = time/timescale

if traces !== nothing
tvec = range(sol.t[1], stop=sol.t[end], length=500)
for frame in traces
(frame.metadata !== nothing && get(frame.metadata, :frame, false)) || error("Only frames can be traced in animations.")
points = get_trans(sol, frame, tvec) |> Matrix
Makie.lines!(scene, points)
end
end
if loop > 1
timevec = repeat(timevec, loop)
end
if display
Base.display(fig)
sleep(2)
fnt = @async begin
record(fig, filename, timevec; framerate) do time
if time == timevec[1]
Base.display(fig)
end
t[] = time/timescale
sleep(max(0, 1/framerate))
end
end
fn = fetch(fnt)
else
fn = record(fig, filename, timevec; framerate) do time
t[] = time/timescale
end
end

fn, scene, fig
end

function render(model, sol, time::Real;
traces = nothing,
x = 2,
y = 0.5,
z = 2,
kwargs...,
)

# fig = Figure()
# scene = LScene(fig[1, 1]).scene
# cam3d!(scene)
scene, fig = default_scene(0,0,10; kwargs...)
scene, fig = default_scene(x,y,z; kwargs...)
# mesh!(scene, Rect3f(Vec3f(-5, -3.6, -5), Vec3f(10, 0.1, 10)), color=:gray) # Floor

steps = range(sol.t[1], sol.t[end], length=3000)

t = Slider(fig[2, 1], range = steps, startvalue = time).value

recursive_render!(scene, complete(model), sol, t)

if traces !== nothing
tvec = range(sol.t[1], stop=sol.t[end], length=500)
for frame in traces
(frame.metadata !== nothing && get(frame.metadata, :frame, false)) || error("Only frames can be traced in animations.")
points = get_trans(sol, frame, tvec) |> Matrix
Makie.lines!(scene, points)
end
end
fig, t
end

function Multibody.loop_render(model, sol; timescale = 1.0, framerate = 30, max_loop = 5, kwargs...)
fig, t = render(model, sol, sol.t[1]; kwargs...)
sleeptime = 1/framerate
timevec = range(sol.t[1], sol.t[end]*timescale, step=sleeptime)
display(fig)
@async begin
for i = 1:max_loop
for ti in timevec
execution_time = @elapsed t[] = ti
sleep(max(0, sleeptime - execution_time))
end
end
end
end

"""
Internal function: Recursively render all subsystem components of a multibody system. If a particular component returns `true` from its `render!` method, indicating that the component performaed rendering, the recursion stops.
"""
Expand Down
16 changes: 14 additions & 2 deletions src/Multibody.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export Rotational, Translational
export render, render!

"""
scene, time = render(model, sol, t::Real; framerate = 30)
path = render(model, sol, timevec = range(sol.t[1], sol.t[end], step = 1 / framerate); framerate = 30, timescale=1)
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
Expand All @@ -23,7 +23,9 @@ Create a 3D animation of a multibody system
- `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 of `timescale = 2` will 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 when `display = true` for 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.
- `filename` controls the name and the file type of the resulting animation
- `traces`: An optional array of frames to show the trace of.
# Camera control
The following keyword arguments are available to control the camera pose:
Expand All @@ -32,9 +34,19 @@ The following keyword arguments are available to control the camera pose:
- `z = 2`
- `lookat = [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`: if `true`, 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`](@ref)
"""
function render end

"""
loop_render(model, sol; framerate = 30, timescale = 1, max_loop = 5, kwargs...)
Similar to the method of [`render`](@ref) that produces an animation, but instead opens an interactive window where the time is automatically advanced in real time. This allows the user to manually manipulate the camera using the mouse is a live animation.
"""
function loop_render end

"""
did_render::Bool = render!(scene, ::typeof(ComponentConstructor), sys, sol, t)
Expand Down
5 changes: 2 additions & 3 deletions src/orientation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,8 @@ end
Extract the translational part of a frame from a solution at time `t`.
See also [`get_rot`](@ref), [`get_frame`](@ref), [Orientations and directions](@ref) (docs section).
"""
function get_trans(sol, frame, t)
SVector{3}(sol(t, idxs = collect(frame.r_0)))
end
get_trans(sol, frame, t::Number) = SVector{3}(sol(t, idxs = collect(frame.r_0)))
get_trans(sol, frame, t::AbstractArray) = sol(t, idxs = collect(frame.r_0))

"""
T_W_F = get_frame(sol, frame, t)
Expand Down

0 comments on commit 2dde592

Please sign in to comment.