diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b12fe5d985..e05d337ddfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 6c8d72d0647..aa1bb12d418 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -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 @@ -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. @@ -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. @@ -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) @@ -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) ) @@ -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 @@ -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 @@ -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)) @@ -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) @@ -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 @@ -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 @@ -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 @@ -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) @@ -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) @@ -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 @@ -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))) @@ -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) @@ -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 diff --git a/test/events.jl b/test/events.jl index 41b7a94d440..572beb7b3d9 100644 --- a/test/events.jl +++ b/test/events.jl @@ -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) @@ -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