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.

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)

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()
4 Likes