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 lookat-translating zoom mode for Camera3D #3793

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [Unreleased]

- Improved thread safety of rendering with CairoMakie (independent `Scene`s only) by locking FreeType handles [#3777](https://github.com/MakieOrg/Makie.jl/pull/3777).
- Added the `zoom_translates_lookat` setting to LScene's 3D camera. If set to true the camera no longer moves away from or towards lookat when zooming, avoiding issues with it clipping into a shape. [#3793](https://github.com/MakieOrg/Makie.jl/pull/3793)

## [0.20.9] - 2024-03-29

Expand Down
66 changes: 49 additions & 17 deletions src/camera/camera3d.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ struct Camera3D <: AbstractCamera3D
fov::Observable{Float32}
near::Observable{Float32}
far::Observable{Float32}

# acts as rotation center if zooming affects lookat and lookat is the rotation center
static_lookat::Observable{Vec3f}
bounding_sphere::Observable{Sphere{Float32}}
end

Expand All @@ -36,7 +39,7 @@ The behavior of the camera can be adjusted via keyword arguments or the fields
Settings include anything that isn't a mouse or keyboard button.

- `projectiontype = Perspective` sets the type of the projection. Can be `Orthographic` or `Perspective`.
- `rotation_center = :lookat` sets the default center for camera rotations. Currently allows `:lookat` or `:eyeposition`.
- `rotation_center = :lookat` sets the initial center for camera rotations. Currently allows `:lookat` or `:eyeposition`.
- `fixed_axis = true`: If true panning uses the (world/plot) z-axis instead of the camera up direction.
- `zoom_shift_lookat = true`: If true keeps the data under the cursor when zooming.
- `cad = false`: If true rotates the view around `lookat` when zooming off-center.
Expand All @@ -46,6 +49,7 @@ Settings include anything that isn't a mouse or keyboard button.
- `:view_relative` scales `near` and `far` by `norm(eyeposition - lookat)`
- `:bbox_relative` scales `near` and `far` to the scene bounding box as passed to the camera with `update_cam!(..., bbox)`. (More specifically `far = 1` is scaled to the furthest point of a bounding sphere and `near` is generally overwritten to be the closest point.)
- `center = true`: Controls whether the camera placement gets reset when calling `center!(scene)`, which is called when a new plot is added.
- `zoom_translates_lookat = false`: Controls whether lookat is moved towards the camera (true) or the camera moves towards lookat (false). The former avoids clipping from the camera moving into objects. Best used with `zoom_shift_lookat = false`.

- `keyboard_rotationspeed = 1f0` sets the speed of keyboard based rotations.
- `keyboard_translationspeed = 0.5f0` sets the speed of keyboard based translations.
Expand Down Expand Up @@ -168,7 +172,8 @@ function Camera3D(scene::Scene; kwargs...)
fixed_axis = true,
cad = false,
center = true,
clipping_mode = :adaptive # TODO: use bbox to adjust near/far automatically
clipping_mode = :adaptive, # TODO: use bbox to adjust near/far automatically
zoom_translates_lookat = false
)

replace!(settings, :Camera3D, scene, overwrites)
Expand Down Expand Up @@ -197,6 +202,8 @@ function Camera3D(scene::Scene; kwargs...)
get(overwrites, :fov, Observable(45.0)),
get(overwrites, :near, Observable(0.1)),
get(overwrites, :far, Observable(far_default)),

get(overwrites, :lookat, Observable(Vec3f(0))),
Sphere(Point3f(0), 1f0)
)

Expand Down Expand Up @@ -318,10 +325,11 @@ function on_pulse(scene, cam::Camera3D, timestep)
@extractvalue cam.controls (
right_key, left_key, up_key, down_key, backward_key, forward_key,
tilt_up_key, tilt_down_key, pan_left_key, pan_right_key, roll_counterclockwise_key, roll_clockwise_key,
zoom_out_key, zoom_in_key, increase_fov_key, decrease_fov_key
zoom_out_key, zoom_in_key, increase_fov_key, decrease_fov_key,
)
@extractvalue cam.settings (
keyboard_translationspeed, keyboard_rotationspeed, keyboard_zoomspeed, projectiontype
keyboard_translationspeed, keyboard_rotationspeed, keyboard_zoomspeed, projectiontype,
zoom_translates_lookat
)

# translation
Expand Down Expand Up @@ -377,7 +385,7 @@ function on_pulse(scene, cam::Camera3D, timestep)

if zooming
zoom_step = (1f0 + keyboard_zoomspeed * timestep) ^ (zoom_out - zoom_in)
_zoom!(scene, cam, zoom_step, false, false)
_zoom!(scene, cam, zoom_step, false, false, zoom_translates_lookat)
end

# fov
Expand All @@ -404,7 +412,7 @@ function add_mouse_controls!(scene, cam::Camera3D)
@extract cam.controls (translation_button, rotation_button, reposition_button, scroll_mod)
@extract cam.settings (
mouse_translationspeed, mouse_rotationspeed, mouse_zoomspeed,
cad, projectiontype, zoom_shift_lookat
cad, projectiontype, zoom_shift_lookat, zoom_translates_lookat
)

last_mousepos = RefValue(Vec2f(0, 0))
Expand Down Expand Up @@ -501,7 +509,7 @@ function add_mouse_controls!(scene, cam::Camera3D)
on(camera(scene), e.scroll) do scroll
if is_mouseinside(scene) && ispressed(scene, scroll_mod[])
zoom_step = (1f0 + 0.1f0 * mouse_zoomspeed[]) ^ -scroll[2]
zoom!(scene, cam, zoom_step, cad[], zoom_shift_lookat[])
zoom!(scene, cam, zoom_step, cad[], zoom_shift_lookat[], zoom_translates_lookat[])
return Consume(true)
end
return Consume(false)
Expand Down Expand Up @@ -562,8 +570,8 @@ is from the center of the scene. If `zoom_shift_lookat = true` and
`projectiontype = Orthographic` zooming will keep the data under the cursor at
the same screen space position.
"""
function zoom!(scene, cam::Camera3D, zoom_step, cad = false, zoom_shift_lookat = false)
_zoom!(scene, cam, zoom_step, cad, zoom_shift_lookat)
function zoom!(scene, cam::Camera3D, zoom_step, cad = false, zoom_shift_lookat = false, zoom_translates_lookat = cam.settings[:zoom_translates_lookat][])
_zoom!(scene, cam, zoom_step, cad, zoom_shift_lookat, zoom_translates_lookat)
update_cam!(scene, cam)
nothing
end
Expand Down Expand Up @@ -593,6 +601,7 @@ function _translate_cam!(scene, cam::Camera3D, t)

cam.eyeposition[] = eyepos + trans
cam.lookat[] = lookat + trans
cam.static_lookat[] = cam.static_lookat[] + trans
return
end

Expand All @@ -603,7 +612,7 @@ function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false)

# This applies rotations around the x/y/z axis of the camera coordinate system
# x expands right, y expands up and z expands towards the screen
lookat = cam.lookat[]
lookat = cam.static_lookat[]
eyepos = cam.eyeposition[]
up = cam.upvector[] # +y
viewdir = lookat - eyepos # -z
Expand Down Expand Up @@ -657,21 +666,24 @@ function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false)
# TODO maybe generalize this to arbitrary center?
# calculate positions from rotated vectors
if rotation_center === :lookat
cam.eyeposition[] = lookat - viewdir
cam.eyeposition[] = cam.static_lookat[] - viewdir
cam.lookat[] = cam.static_lookat[] + rotation * (cam.lookat[] - cam.static_lookat[])
else
cam.lookat[] = eyepos + viewdir
cam.static_lookat[] = eyepos + viewdir
cam.lookat[] = cam.eyeposition[] + rotation * (cam.lookat[] - cam.eyeposition[])
end

return
end


function _zoom!(scene, cam::Camera3D, zoom_step, cad = false, zoom_shift_lookat = false)
function _zoom!(scene, cam::Camera3D, zoom_step, cad, zoom_shift_lookat, zoom_translates_lookat)
lookat = cam.lookat[]
eyepos = cam.eyeposition[]
viewdir = lookat - eyepos # -z
vp = viewport(scene)[]
scene_width = widths(vp)

if cad
# Rotate view based on offset from center
u_z = normalize(viewdir)
Expand All @@ -682,7 +694,14 @@ function _zoom!(scene, cam::Camera3D, zoom_step, cad = false, zoom_shift_lookat
shift = rel_pos[1] * u_x + rel_pos[2] * u_y
shift *= 0.1 * sign(1 - zoom_step) * norm(viewdir)

cam.eyeposition[] = lookat - zoom_step * viewdir + shift
if zoom_translates_lookat
fraction = zoom_step * norm(viewdir) / norm(cam.static_lookat[] - cam.eyeposition[])
cam.eyeposition[] = eyepos + shift
cam.lookat[] = cam.eyeposition[] + fraction * (cam.static_lookat[] - cam.eyeposition[])
else
cam.eyeposition[] = lookat - zoom_step * viewdir + shift
end

elseif zoom_shift_lookat
# keep data under cursor
u_z = normalize(viewdir)
Expand All @@ -701,11 +720,21 @@ function _zoom!(scene, cam::Camera3D, zoom_step, cad = false, zoom_shift_lookat
scale = norm(viewdir) * tand(0.5 * cam.fov[])
end

cam.lookat[] = lookat + scale * shift
cam.eyeposition[] = lookat - zoom_step * viewdir + scale * shift
if zoom_translates_lookat # TODO consider rotating rather than translating
cam.lookat[] = eyepos + zoom_step * viewdir + scale * shift
cam.eyeposition[] = eyepos + scale * shift
else
cam.lookat[] = lookat + scale * shift
cam.static_lookat[] = cam.lookat[]
cam.eyeposition[] = lookat - zoom_step * viewdir + scale * shift
end
else
# just zoom in/out
cam.eyeposition[] = lookat - zoom_step * viewdir
if zoom_translates_lookat
cam.lookat[] = eyepos + zoom_step * viewdir
else
cam.eyeposition[] = lookat - zoom_step * viewdir
end
end

return
Expand Down Expand Up @@ -771,6 +800,7 @@ function update_cam!(scene::Scene, cam::Camera3D, area3d::Rect, recenter::Bool =
end

if recenter
cam.static_lookat[] = center
cam.lookat[] = center
cam.eyeposition[] = cam.lookat[] .+ dist * old_dir
cam.upvector[] = normalize(cross(old_dir, cross(cam.upvector[], old_dir)))
Expand All @@ -791,6 +821,7 @@ end

# Update camera position via camera Position & Orientation
function update_cam!(scene::Scene, camera::Camera3D, eyeposition::VecTypes, lookat::VecTypes, up::VecTypes = camera.upvector[])
camera.static_lookat[] = Vec3f(lookat)
camera.lookat[] = Vec3f(lookat)
camera.eyeposition[] = Vec3f(eyeposition)
camera.upvector[] = Vec3f(up)
Expand All @@ -815,6 +846,7 @@ function update_cam!(
sp, cp = sincos(phi)
v = Vec3f(ct * cp, ct * sp, st)
u = Vec3f(-st * cp, -st * sp, ct)
camera.static_lookat[] = center
camera.lookat[] = center
camera.eyeposition[] = center .+ radius * v
camera.upvector[] = u
Expand Down
22 changes: 21 additions & 1 deletion test/events.jl
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right
e = events(scene)
cam3d!(scene, fixed_axis=true, cad=false, zoom_shift_lookat=false)
cc = cameracontrols(scene)

# Verify initial camera state
@test cc.lookat[] == Vec3f(0)
@test cc.eyeposition[] == Vec3f(3)
Expand Down Expand Up @@ -289,6 +289,26 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right
@test cc.lookat[] ≈ Vec3f(0)
@test cc.eyeposition[] ≈ 0.6830134f0 * Vec3f(3)
@test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0)



# Reset state so this is indepentent from the last checks
scene = Scene(size=(800, 600));
e = events(scene)
cam3d!(scene, fixed_axis=true, cad=false, zoom_shift_lookat=false, zoom_translates_lookat = true)
cc = cameracontrols(scene)

# Verify initial camera state
@test cc.lookat[] == Vec3f(0)
@test cc.eyeposition[] == Vec3f(3)
@test cc.upvector[] == Vec3f(0, 0, 1)

# Zoom
e.mouseposition[] = (400, 250) # for debugging
e.scroll[] = (0.0, 4.0)
@test cc.lookat[] ≈ Vec3f(0.9509598)
@test cc.eyeposition[] ≈ Vec3f(3)
@test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0)
end

@testset "mouse state machine" begin
Expand Down