HolyLab / FlyThroughPaths.jl

Create "fly-throughs" in 3d visualization
MIT License
11 stars 1 forks source link

FlyThroughPaths

Stable Dev Build Status Coverage

This package makes it easier to create "fly-through" animations of 3d visualizations. All of the examples below assume you've loaded the package with using FlyThroughPaths.

Generic tools

Representation of paths and view state

Paths are parametrized by time t, represented in units of seconds. All paths implicitly start at t=0.

The representation of view state is independent of any particular plotting package, although our parametrization is inspired by Makie's 3D camera:

Set these as follows:

julia> state = ViewState(eyeposition=[-10, 0, 0], lookat=[0, 0, 0], upvector=[0, 0, 1], fov=45)
ViewState{Float32}(eyeposition=[-10.0, 0.0, 0.0], lookat=[0.0, 0.0, 0.0], upvector=[0.0, 0.0, 1.0], fov=45.0)

You can set just a subset of these:

julia> newstate = ViewState(eyeposition=[-5, 0, 0])
ViewState{Float32}(eyeposition=[-5.0, 0.0, 0.0])

This syntax is often used for updating a previous view; for the unspecified settings, the previous value is left intact.

Initializing a path

julia> path = Path(state)
Path{Float32}(ViewState{Float32}(eyeposition=[-10.0, 0.0, 0.0], lookat=[0.0, 0.0, 0.0], upvector=[0.0, 0.0, 1.0], fov=45.0), FlyThroughPaths.PathChange{Float32}[])

The path starts at state at time t=0.

Evaluating at a particular time

Once you have a path, you can get the current ViewState with path(t):

julia> path(0)
ViewState{Float32}(eyeposition=[-10.0, 0.0, 0.0], lookat=[0.0, 0.0, 0.0], upvector=[0.0, 0.0, 1.0], fov=45.0)

julia> path(10)
ViewState{Float32}(eyeposition=[-10.0, 0.0, 0.0], lookat=[0.0, 0.0, 0.0], upvector=[0.0, 0.0, 1.0], fov=45.0)

So far, nothing much is happening. Things get more interesting when we add movements.

Holding steady

The simplest thing you can do is insert a pause:

julia> path2 = path * Pause(5)
Path{Float32}(ViewState{Float32}(eyeposition=[-10.0, 0.0, 0.0], lookat=[0.0, 0.0, 0.0], upvector=[0.0, 0.0, 1.0], fov=45.0), FlyThroughPaths.PathChange{Float32}[Pause{Float32}(5.0f0, nothing)])

The view will hold steady for 5 seconds. Typically you add Pause when you also plan to add other movements later.

Moving the camera, option 1: constrained movements

This option is typically used for things like rotations around a center point.

julia> path2 = path * ConstrainedMove(5, newstate; constraint=:none, speed=:constant)
Path{Float32}(ViewState{Float32}(eyeposition=[-10.0, 0.0, 0.0], lookat=[0.0, 0.0, 0.0], upvector=[0.0, 0.0, 1.0], fov=45.0), FlyThroughPaths.PathChange{Float32}[ConstrainedMove{Float32}(5.0f0, ViewState{Float32}(eyeposition=[-5.0, 0.0, 0.0]), :none, :constant, nothing)])

This indicates that over a 5-second period, the camera state gradually adopts any values specified in newstate.

julia> path2(0)
ViewState{Float32}(eyeposition=[-10.0, 0.0, 0.0], lookat=[0.0, 0.0, 0.0], upvector=[0.0, 0.0, 1.0], fov=45.0)

julia> path2(5)
ViewState{Float32}(eyeposition=[-5.0, 0.0, 0.0], lookat=[0.0, 0.0, 0.0], upvector=[0.0, 0.0, 1.0], fov=45.0)

julia> path2(2.5)
ViewState{Float32}(eyeposition=[-7.5, 0.0, 0.0], lookat=[0.0, 0.0, 0.0], upvector=[0.0, 0.0, 1.0], fov=45.0)

Keyword options include:

Moving the camera, option 2: Bezier movements

With this option, you can approximately simulate the feeling of flight, preserving momentum:

path2 = path * BezierMove(Δt::Real, P1::ViewState, P2::ViewState...)

where a bezier path is specified as indicated in this diagram:

bezier diagram

The starting state, P0 in the diagram, is taken from the endpoint of path. Over the next Δt seconds, one then moves towards the last ViewState argument of bezier, orienting successively towards any prior arguments. Probably the most robust option is to use bezier(Δt, P1, P2, P3), which can be interpreted as "depart P0 traveling towards P1, and arrive at P3 as if you had come from P2." The view does not actually pass through P1 and P2, but these set the initial and final tangents of the curve.

To see this in action, let's create a move that "rotates" around the origin but moves outward (to a more distant orbit) on its way there:

julia> move = BezierMove(5, ViewState(eyeposition=[0, 10, 0]), [ViewState(eyeposition=[-20, 20, 0])])
BezierMove{Float32}(5.0f0, ViewState{Float32}(eyeposition=[0.0, 10.0, 0.0]), ViewState{Float32}[ViewState{Float32}(eyeposition=[-20.0, 20.0, 0.0])], nothing)

julia> path2 = path * move;

julia> path2(2.5)
ViewState{Float32}(eyeposition=[-12.5, 12.5, 0.0], lookat=[0.0, 0.0, 0.0], upvector=[0.0, 0.0, 1.0], fov=45.0)

Backend-specific tools

These require interaction with a plotting package supported by one of the extensions. Currently supported:

You need to load the visualization package, e.g., using GLMakie, in your session before any of the commands below will work.

Capturing the current view state

This can be handy for constructing a path, for example you can interactively set the approximate position and view parameters and then query them for use by the tools above.

state = capture_view(scene)

state is a ViewState object.

Setting the current view state

oldstate = set_view!(camera, path, t)

This updates the current camera settings from path at time t.

Displaying the path

plot(path)