Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add path tracing and loop rendering option to animations #95

Merged
merged 3 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/src/examples/pendulum.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,14 +220,15 @@ end
model = complete(model)
ssys = structural_simplify(IRSystem(model))

prob = ODEProblem(ssys, [model.shoulder_joint.phi => 0.0, model.elbow_joint.phi => 0.1], (0, 12))
prob = ODEProblem(ssys, [model.shoulder_joint.phi => 0.0, model.elbow_joint.phi => 0.1], (0, 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
Loading