Makie: Linking cameras in multiple 3d plots

Is there a way to link cameras in multiple 3d plot. For now I use my first axis as the main one, and update the azimuth and elevation all all the other axis based on this one. However, with this solution i can change the view only with the first axis. Is there a way to do that with any axis where all the axis are linked altogether ?

For now my solution looks like that :

using GLMakie
f = Figure()

ax1 = Axis3(f[1, 1],aspect=:data, viewmode=:fitzoom)
meshscatter!(ax1,rand(20,3))

ax2 = Axis3(f[1, 2],aspect=:data, viewmode=:fitzoom)
meshscatter!(ax2,rand(20,3))

on(ax1.attributes.azimuth) do x
    ax2.attributes.azimuth = x
end
on(ax1.attributes.elevation) do x
    ax2.attributes.elevation = x
end

# on(ax2.attributes.azimuth) do x
#     ax1.attributes.azimuth = x
# end
# on(ax2.attributes.elevation) do x
#     ax1.attributes.elevation = x
# end

Where the commented part is what i would like but doesn’t work as it create an updating loop and makes julia crash.

1 Like

You have to change the logic so the update only happens if the values differ, otherwise it’s cyclical.

Yep that’s working ! Thanks for the idea i was overthinking this problem !

For posterity this function will link all the views of Axis3 in a figure:

function link_cameras(f; step=0.01)
    axes = f.content[findall(x -> typeof(x) == Axis3,f.content)]

    for i in 1:length(axes)
        on(axes[i].attributes.azimuth) do x
            for j in collect(1:length(axes))[1:end .!= i]
                if abs(x - axes[j].attributes.azimuth[])>step
                    axes[j].attributes.azimuth = x
                end
            end
        end
        on(axes[i].attributes.elevation) do x
            for j in collect(1:length(axes))[1:end .!= i]
                if abs(x - axes[j].attributes.elevation[])>step
                    axes[j].attributes.elevation = x
                end
            end
        end
    end
    return f
end

And with a MWE:

using GLMakie
f = Figure()

ax1 = Axis3(f[1, 1],aspect=:data, viewmode=:fitzoom)
meshscatter!(ax1,rand(20,3))

ax2 = Axis3(f[1, 2],aspect=:data, viewmode=:fitzoom)
meshscatter!(ax2,rand(20,3))

ax3 = Axis3(f[2, 1],aspect=:data, viewmode=:fitzoom)
meshscatter!(ax3,rand(20,3))

ax4 = Axis3(f[2, 2],aspect=:data, viewmode=:fitzoom)
meshscatter!(ax4,rand(20,3))

f = link_cameras(f)
1 Like

For posterity, here is two functions linking all views in a figure ! Functions work either with Axis3 or with Lscene.

In axis3 there is no zoom, so azimuth and elevation of all axis in a figure are linked with this function

function link_cameras_axis3(f; step=0.01)
    axes = f.content[findall(x -> typeof(x) == Axis3,f.content)]

    for i in 1:length(axes)
        on(axes[i].attributes.azimuth) do x
            for j in collect(1:length(axes))[1:end .!= i]
                if abs(x - axes[j].attributes.azimuth[])>step
                    axes[j].attributes.azimuth = x
                end
            end
        end
        on(axes[i].attributes.elevation) do x
            for j in collect(1:length(axes))[1:end .!= i]
                if abs(x - axes[j].attributes.elevation[])>step
                    axes[j].attributes.elevation = x
                end
            end
        end
    end
    return f
end

In Lscene there is a complete camera object, so when a eye position is changed, it will update all the parameters of the other LScenes of the figure with this function:

function link_cameras_lscene(f; step=0.01)
    scenes = f.content[findall(x -> typeof(x) == LScene,f.content)]
    cameras = [x.scene.camera_controls for x in scenes]

    for i in 1:length(cameras)
        on(cameras[i].eyeposition) do x
            for j in collect(1:length(cameras))[1:end .!= i]
                if sum(abs.(x - cameras[j].eyeposition[]))>step
                    cameras[j].lookat[]       = cameras[i].lookat[]
                    cameras[j].eyeposition[]  = cameras[i].eyeposition[]
                    cameras[j].upvector[]     = cameras[i].upvector[]
                    cameras[j].zoom_mult[]    = cameras[i].zoom_mult[]
        
                    update_cam!(scenes[j].scene, cameras[j])
                end
            end
        end
    end
    return f
end

Finally, here is a MWE:

using GLMakie
f = Figure()
ax1 = Axis3(f[1, 1],aspect=:data, viewmode=:fitzoom)
meshscatter!(ax1,rand(20,3))
ax2 = Axis3(f[1, 2],aspect=:data, viewmode=:fitzoom)
meshscatter!(ax2,rand(20,3))
ax3 = Axis3(f[2, 1],aspect=:data, viewmode=:fitzoom)
meshscatter!(ax3,rand(20,3))
ax4 = Axis3(f[2, 2],aspect=:data, viewmode=:fitzoom)
meshscatter!(ax4,rand(20,3))
f = link_cameras_axis3(f)


f = Figure()
ax1 = LScene(f[1, 1],aspect=:data)
meshscatter!(ax1,rand(20,3))
ax2 = LScene(f[1, 2],aspect=:data)
meshscatter!(ax2,rand(20,3))
ax3 = LScene(f[2, 1],aspect=:data)
meshscatter!(ax3,rand(20,3))
ax4 = LScene(f[2, 2],aspect=:data)
meshscatter!(ax4,rand(20,3))
f = link_cameras_lscene(f)
2 Likes

Here is an updated (and simplified) example working on latest Makie:

using GLMakie

link_cameras_axis3(f; step=.01) = begin
  axes = filter(x -> x isa Axis3, f.content)

  for i ∈ 1:length(axes)
    lift(axes[i].azimuth, axes[i].elevation) do az, el
      for j ∈ 1:length(axes)
        i == j && continue
        if abs(az - axes[j].azimuth[]) > step
          axes[j].azimuth = az
        end
        if abs(el - axes[j].elevation[]) > step
          axes[j].elevation = el
        end
      end
    end
  end
  f
end

link_cameras_lscene(f; step=.01) = begin
  scenes = filter(x -> x isa LScene, f.content)
  cameras = map(x -> cameracontrols(x.scene), scenes)

  for i ∈ 1:length(cameras)
    on(cameras[i].eyeposition) do eye
      for j ∈ 1:length(cameras)
        i == j && continue
        if sum(abs, eye - cameras[j].eyeposition[]) > step
          update_cam!(scenes[j].scene, cameras[i])
        end
      end
    end
  end
  f
end

test_axis3() = begin
  f = Figure()
  ax1 = Axis3(f[1, 1], aspect=:data, viewmode=:fitzoom)
  meshscatter!(ax1, rand(20, 3))
  ax2 = Axis3(f[1, 2], aspect=:data, viewmode=:fitzoom)
  meshscatter!(ax2, rand(20, 3))
  ax3 = Axis3(f[2, 1], aspect=:data, viewmode=:fitzoom)
  meshscatter!(ax3, rand(20, 3))
  ax4 = Axis3(f[2, 2], aspect=:data, viewmode=:fitzoom)
  meshscatter!(ax4, rand(20, 3))
  link_cameras_axis3(f)
end

test_lscene() = begin
  f = Figure()
  ax1 = LScene(f[1, 1], scenekw=(; aspect=:data))
  meshscatter!(ax1, rand(20, 3))
  ax2 = LScene(f[1, 2], scenekw=(; aspect=:data))
  meshscatter!(ax2, rand(20, 3))
  ax3 = LScene(f[2, 1], scenekw=(; aspect=:data))
  meshscatter!(ax3, rand(20, 3))
  ax4 = LScene(f[2, 2], scenekw=(; aspect=:data))
  meshscatter!(ax4, rand(20, 3))
  link_cameras_lscene(f)
end

main() = begin
  test_axis3() |> display
  sleep(1)
  test_lscene() |> display
  return
end

main()
5 Likes