From 017f80ae41918a891b0adc2e5ce97f4cefd58be4 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Wed, 26 Jun 2024 09:04:57 +0200 Subject: [PATCH 1/3] add path tracing option to animations --- docs/src/examples/pendulum.md | 3 ++- docs/src/rendering.md | 4 ++++ ext/Render.jl | 27 +++++++++++++++++++++++++-- src/Multibody.jl | 3 ++- src/orientation.jl | 5 ++--- 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/docs/src/examples/pendulum.md b/docs/src/examples/pendulum.md index 551fe775..1a9a9aba 100644 --- a/docs/src/examples/pendulum.md +++ b/docs/src/examples/pendulum.md @@ -225,9 +225,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) diff --git a/docs/src/rendering.md b/docs/src/rendering.md index 75a0f8ab..b830339f 100644 --- a/docs/src/rendering.md +++ b/docs/src/rendering.md @@ -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 diff --git a/ext/Render.jl b/ext/Render.jl index 4b977c10..5456ce11 100644 --- a/ext/Render.jl +++ b/ext/Render.jl @@ -129,6 +129,7 @@ function render(model, sol, up = Vec3f(0,1,0), show_axis = false, timescale = 1.0, + traces = nothing, kwargs... ) scene, fig = default_scene(x,y,z; lookat,up,show_axis) @@ -139,6 +140,16 @@ function render(model, sol, t = Observable(timevec[1]) 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 + fn = record(fig, filename, timevec; framerate) do time t[] = time/timescale end @@ -146,20 +157,32 @@ function render(model, sol, 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 diff --git a/src/Multibody.jl b/src/Multibody.jl index f1ba5db8..1824523d 100644 --- a/src/Multibody.jl +++ b/src/Multibody.jl @@ -11,7 +11,7 @@ export Rotational, Translational export render, render! """ - scene, time = render(model, sol, t::Real; framerate = 30) + 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) Create a 3D animation of a multibody system @@ -24,6 +24,7 @@ Create a 3D animation of a multibody system - `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. - `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: diff --git a/src/orientation.jl b/src/orientation.jl index fd5091a4..ecceea4a 100644 --- a/src/orientation.jl +++ b/src/orientation.jl @@ -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) From fb118ce8b6b8606f1864c7f3c796ba295cdab1a3 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Wed, 26 Jun 2024 09:42:14 +0200 Subject: [PATCH 2/3] add live looped rendering --- docs/src/examples/pendulum.md | 2 +- ext/Render.jl | 40 +++++++++++++++++++++++++++++++---- src/Multibody.jl | 12 ++++++++++- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/docs/src/examples/pendulum.md b/docs/src/examples/pendulum.md index 1a9a9aba..28055bad 100644 --- a/docs/src/examples/pendulum.md +++ b/docs/src/examples/pendulum.md @@ -220,7 +220,7 @@ 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) ``` diff --git a/ext/Render.jl b/ext/Render.jl index 5456ce11..dadd6540 100644 --- a/ext/Render.jl +++ b/ext/Render.jl @@ -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 @@ -130,6 +130,7 @@ function render(model, sol, show_axis = false, timescale = 1.0, traces = nothing, + display = false, kwargs... ) scene, fig = default_scene(x,y,z; lookat,up,show_axis) @@ -150,9 +151,25 @@ function render(model, sol, end end - fn = record(fig, filename, timevec; framerate) do time - t[] = time/timescale + 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(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 @@ -186,6 +203,21 @@ function render(model, sol, time::Real; fig, t end +function Multibody.loop_render(model, sol; timescale = 1.0, framerate = 30, 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:5 + for ti in timevec + execution_time = @elapsed t[] = ti + sleep(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. """ diff --git a/src/Multibody.jl b/src/Multibody.jl index 1824523d..2e259b30 100644 --- a/src/Multibody.jl +++ b/src/Multibody.jl @@ -12,7 +12,7 @@ export render, render! """ 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) + path = render(model, sol, timevec = range(sol.t[1], sol.t[end], step = 1 / framerate); framerate = 30, timescale=1, display=false) Create a 3D animation of a multibody system @@ -33,9 +33,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, 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) From 6b768adc638ae8ce33dfe988a3d8ea2316a4e541 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Wed, 26 Jun 2024 09:57:13 +0200 Subject: [PATCH 3/3] add animation looping option --- ext/Render.jl | 13 ++++++++----- src/Multibody.jl | 5 +++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/ext/Render.jl b/ext/Render.jl index dadd6540..35fe12f7 100644 --- a/ext/Render.jl +++ b/ext/Render.jl @@ -131,6 +131,7 @@ function render(model, sol, timescale = 1.0, traces = nothing, display = false, + loop = 1, kwargs... ) scene, fig = default_scene(x,y,z; lookat,up,show_axis) @@ -150,7 +151,9 @@ function render(model, sol, Makie.lines!(scene, points) end end - + if loop > 1 + timevec = repeat(timevec, loop) + end if display Base.display(fig) sleep(2) @@ -160,7 +163,7 @@ function render(model, sol, Base.display(fig) end t[] = time/timescale - sleep(1/framerate) + sleep(max(0, 1/framerate)) end end fn = fetch(fnt) @@ -203,16 +206,16 @@ function render(model, sol, time::Real; fig, t end -function Multibody.loop_render(model, sol; timescale = 1.0, framerate = 30, kwargs...) +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:5 + for i = 1:max_loop for ti in timevec execution_time = @elapsed t[] = ti - sleep(sleeptime - execution_time) + sleep(max(0, sleeptime - execution_time)) end end end diff --git a/src/Multibody.jl b/src/Multibody.jl index 2e259b30..6c182385 100644 --- a/src/Multibody.jl +++ b/src/Multibody.jl @@ -12,7 +12,7 @@ export render, render! """ 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) + 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 @@ -23,6 +23,7 @@ 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. @@ -40,7 +41,7 @@ See also [`loop_render`](@ref) function render end """ - loop_render(model, sol; framerate = 30, timescale = 1, kwargs...) + 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. """