Документация Engee

Начало моделирования графа сцены Makie

Страница в процессе перевода.

Конструктор сцен

scene = Scene(;
    # очистим все, что находится за сценой
    clear = true,
    # структура камеры для сцены.
    visible = true,
    # ssao и освещенность более подробно описываются в соответствующих разделах документации.
    ssao = Makie.SSAO(),
    # Создает освещение из темы, которая сейчас задана по умолчанию — `
    # set_theme!(lightposition=:eyeposition, ambient=RGBf(0.5, 0.5, 0.5))`
    lights = Makie.automatic,
    backgroundcolor = :gray,
    size = (500, 500);
    # заполняется заданной глобальной темой
    theme_kw...
)

Сцена выполняет четыре задачи.

  • Содержит локальную тему, которая применяется к всем объектам графика в этой сцене.

  • Управляет матрицами камеры, проекции и преобразования.

  • Определяет размер окна. Для подсцен дочерняя сцена может иметь меньшую область окна, чем родительская.

  • Содержит ссылку на все события окна.

Сцены и подокна

С помощью сцен можно создавать подокна. Расширения окон задаются с помощью Rect{2, Int}, а позиция всегда указывается в пикселях окна и относительно родительского элемента.

using GLMakie
scene = Scene(backgroundcolor=:gray)
subwindow = Scene(scene, viewport=Rect(100, 100, 200, 200), clear=true, backgroundcolor=:white)
scene
2273079

При использовании Scenes напрямую необходимо вручную настроить камеру и отцентрировать ее по содержанию сцены. Согласно более подробному описанию в разделе «Камера» существует несколько функций cam***! для установки определенной проекции и типа камеры для сцены.

cam3d!(subwindow)
meshscatter!(subwindow, rand(Point3f, 10), color=:gray)
center!(subwindow)
scene
295ed4b

Вместо применения белого фона можно отключить его очистку, сделав сцену прозрачной, и добавить контур. Создать контур проще всего путем построения подсцены с проекцией от 0…​1 для всего окна. Чтобы создать подсцену с определенным типом проекции, Makie предлагает для каждой функции камеры версию без !, которая создаст подсцену и применит тип камеры. Пространство с проекцией от 0…​1 называется относительным (relative) пространством, так что camrelative даст эту проекцию.

subwindow.clear = false
relative_space = Makie.camrelative(subwindow)
# рисует линию по границе окна сцены
lines!(relative_space, Rect(0, 0, 1, 1))
scene
65625ac

Также можно задать родительской сцене более интересный фон, используя campixel! и наложив изображение на окно.

campixel!(scene)
w, h = size(scene) # возвращает размер сцены в пикселях
# рисует линию по границе окна сцены
image!(scene, [sin(i/w) + cos(j/h) for i in 1:w, j in 1:h])
scene
6893892

Чтобы исправить это, можно сдвинуть сцену назад с помощью перемещения.

translate!(scene.plots[1], 0, 0, -10000)
scene
675020a

Нам необходимо довольно высокое преобразование, поскольку дальняя и ближняя плоскости для campixel! идут от -1000 до 1000, а для cam3d! они автоматически подстраиваются под параметры камеры. Они оказываются в одном и том же буфере глубины, преобразованном в диапазон 0..1 с помощью дальней и ближней плоскостей. Поэтому, чтобы оставаться позади трехмерной сцены, нужно задать большое значение.

При clear = true у нас не было бы этой проблемы.

В GLMakie можно взглянуть на буфер глубины, чтобы увидеть, как все выглядит сейчас.

screen = display(scene) # используем display, чтобы получить ссылку на объект экрана
depth_color = GLMakie.depthbuffer(screen)
close(screen)
# Посмотрим на результат.
f, ax, pl = heatmap(depth_color)
Colorbar(f[1, 2], pl)
f
f3971ee

События окна

Каждая сцена также содержит ссылку на все глобальные события окна.

scene.events

Эти события можно использовать, к примеру, для перемещения подокна. Если приведенные ниже действия выполнить в GLMakie, перемещать подокно можно, удерживая нажатой левую кнопку мыши и нажав клавишу CTRL.

on(scene.events.mouseposition) do mousepos
    if ispressed(subwindow, Mouse.left & Keyboard.left_control)
        subwindow.viewport[] = Rect(Int.(mousepos)..., 200, 200)
    end
end

Проекции и камера

Мы уже немного говорили о камерах, но не обсуждали, как именно они работают. Начнем с нуля. По умолчанию границы сцены по осям x/y заданы от --1 до 1. Итак, чтобы построить прямоугольник, очерчивающий окно сцены, подойдет следующее.

scene = Scene(backgroundcolor=:gray)
lines!(scene, Rect2f(-1, -1, 2, 2), linewidth=5, color=:black)
scene
b47a296

Это так, потому что матрица проекции и матрица представления по умолчанию являются матрицами тождества, а пространство единиц Makie в мире OpenGL называется Clip space.

cam = Makie.camera(scene) # Вот как получить доступ к камере сцен.

Можно изменить отображение, например построить от --3 до 5 с помощью матрицы ортогональной проекции.

cam.projection[] = Makie.orthographicprojection(-3f0, 5f0, -3f0, 5f0, -100f0, 100f0)
scene
2a25d69

Можно также изменить камеру на перспективную трехмерную проекцию.

w, h = size(scene)
nearplane = 0.1f0
farplane = 100f0
aspect = Float32(w / h)
cam.projection[] = Makie.perspectiveprojection(45f0, aspect, nearplane, farplane)
# Теперь нужно изменить матрицу представления
# для размещения камеры в определенном месте.
eyeposition = Vec3f(10)
lookat = Vec3f(0)
upvector = Vec3f(0, 0, 1)
cam.view[] = Makie.lookat(eyeposition, lookat, upvector)
scene
7499aa5

Взаимодействие с осями и макетами

Ось содержит сцену, проекция которой настроена так, чтобы координаты шли от (x/y)limits_min ... (x/y)limits_max. На ней мы и будем выполнять построение. Кроме того, это обычная сцена, которую можно использовать для создания подсцен с меньшим размером окна или другой проекцией.

Таким образом, мы можем использовать camrelative и соответствующие функции, чтобы, например, построить график в середине оси.

figure, axis, plot_object = scatter(1:4)
relative_projection = Makie.camrelative(axis.scene);
scatter!(relative_projection, [Point2f(0.5)], color=:red)
# Смещение и текст находятся в пространстве пикселей
text!(relative_projection, "Hi", position=Point2f(0.5), offset=Vec2f(5))
lines!(relative_projection, Rect(0, 0, 1, 1), color=:blue, linewidth=3)
figure
73ad7eb

Преобразования и граф сцены

До сих пор мы обсуждали только преобразования сцены с помощью камеры. В отличие от них существуют также преобразования сцены, называемые глобальными (мировыми) преобразованиями. Дополнительные сведения о различных пространствах можно найти на этой странице learn opengl.

Глобальное преобразование реализовано через структуру Transformation в Makie. Их содержат и сцены, и графики, поэтому эти типы считаются Makie.Transformable. Преобразование сцены будет унаследовано всеми графиками, добавляемыми к сцене. Для работы с Transformable существуют следующие три функции.

translate!(t::Transformable, xyz::VecTypes)
translate!(t::Transformable, xyz...)

Применяет абсолютное перемещение к данному объекту Transformable (сцене или графику), перемещая его в позицию x, y, z.

rotate!(Accum, t::Transformable, axis_rot...)

Применяет относительный поворот к трансформируемому объекту путем умножения на текущий поворот.

scale!([mode = Absolute], t::Transformable, xyz...)
scale!([mode = Absolute], t::Transformable, xyz::VecTypes)

Масштабирует заданный объект t::Transformable (сцену или график) до заданных аргументов xyz. Отсутствующее измерение будет масштабировано на 1. При значении mode == Accum заданное масштабирование будет умножено на предыдущее.

scene = Scene()
cam3d!(scene)
sphere_plot = mesh!(scene, Sphere(Point3f(0), 0.5), color=:red)
scale!(scene, 0.5, 0.5, 0.5)
rotate!(scene, Vec3f(1, 0, 0), 0.5) # 0,5 радиуса вокруг оси y
scene
7547dff

Можно также преобразовывать объекты графика напрямую. После этого преобразование из объекта графика будет добавлено поверх преобразования из сцены. Можно добавлять подсцены и динамически взаимодействовать с ними. Makie предлагает то, что обычно называют графом сцены.

translate!(sphere_plot, Vec3f(0, 0, 1))
scene
63d3c4d

Граф сцены можно использовать для создания жестких преобразований, например для руки робота.

parent = Scene()
cam3d!(parent; clipping_mode = :static)

# Задайте параметры lookat и eyeposition камеры, получив элементы управления камерой и используя `update_cam!`.
camc = cameracontrols(parent)
update_cam!(parent, camc, Vec3f(0, 8, 0), Vec3f(4.0, 0, 0))
# При настройке камеры вручную, возможно, потребуется настроить
# ближнюю и дальнюю плоскости отсечения.
camc.far[] = 100f0
s1 = Scene(parent, camera=parent.camera)
mesh!(s1, Rect3f(Vec3f(0, -0.1, -0.1), Vec3f(5, 0.2, 0.2)))
s2 = Scene(s1, camera=parent.camera)
mesh!(s2, Rect3f(Vec3f(0, -0.1, -0.1), Vec3f(5, 0.2, 0.2)), color=:red)
translate!(s2, 5, 0, 0)
s3 = Scene(s2, camera=parent.camera)
mesh!(s3, Rect3f(Vec3f(-0.2), Vec3f(0.4)), color=:blue)
translate!(s3, 5, 0, 0)
parent
2681e9e

# Теперь повернем конечность в «суставе».
rotate!(s2, Vec3f(0, 1, 0), 0.5)
rotate!(s3, Vec3f(1, 0, 0), 0.5)
parent
47de7f4

Этот базовый принцип позволит оживить даже роботов.) Kevin Moerman (Кевин Мурман) предоставил сетку Lego, которую мы будем анимировать. Когда граф сцены действительно представляет собой только граф преобразований, структуру Transformation можно использовать напрямую, что мы и сделаем. Это эффективнее и проще, чем создавать сцену для каждой модели.

using MeshIO, FileIO, GeometryBasics

colors = Dict(
    "eyes" => "#000",
    "belt" => "#000059",
    "arm" => "#009925",
    "leg" => "#3369E8",
    "torso" => "#D50F25",
    "head" => "yellow",
    "hand" => "yellow"
)

origins = Dict(
    "arm_right" => Point3f(0.1427, -6.2127, 5.7342),
    "arm_left" => Point3f(0.1427, 6.2127, 5.7342),
    "leg_right" => Point3f(0, -1, -8.2),
    "leg_left" => Point3f(0, 1, -8.2),
)

rotation_axes = Dict(
    "arm_right" => Vec3f(0.0000, -0.9828, 0.1848),
    "arm_left" => Vec3f(0.0000, 0.9828, 0.1848),
    "leg_right" => Vec3f(0, -1, 0),
    "leg_left" => Vec3f(0, 1, 0),
)

function plot_part!(scene, parent, name::String)
    # загрузим файл модели
    m = load(assetpath("lego_figure_" * name * ".stl"))
    # найдем цвет
    color = colors[split(name, "_")[1]]
    # Создадим дочернее преобразование из родительского
    child = Transformation(parent)
    # получим преобразование родительского объекта
    ptrans = Makie.transformation(parent)
    # получим начало, если оно доступно
    origin = get(origins, name, nothing)
    # отцентрируем сетку относительно начала, если оно у нас есть
    if !isnothing(origin)
        centered = m.position .- origin
        m = GeometryBasics.mesh(m, position = centered)
        translate!(child, origin)
    else
        # если начала нет, нужно сделать поправку на преобразование родительских элементов
        translate!(child, -ptrans.translation[])
    end
    # построим часть, применив преобразование и цвет
    return mesh!(scene, m; color=color, transformation=child)
end

function plot_lego_figure(s, floor=true)
    # Построим иерархическую сетку и поместим все части в словарь
    figure = Dict()
    figure["torso"] = plot_part!(s, s, "torso")
        figure["head"] = plot_part!(s, figure["torso"], "head")
            figure["eyes_mouth"] = plot_part!(s, figure["head"], "eyes_mouth")
        figure["arm_right"] = plot_part!(s, figure["torso"], "arm_right")
            figure["hand_right"] = plot_part!(s, figure["arm_right"], "hand_right")
        figure["arm_left"] = plot_part!(s, figure["torso"], "arm_left")
            figure["hand_left"] = plot_part!(s, figure["arm_left"], "hand_left")
        figure["belt"] = plot_part!(s, figure["torso"], "belt")
            figure["leg_right"] = plot_part!(s, figure["belt"], "leg_right")
            figure["leg_left"] = plot_part!(s, figure["belt"], "leg_left")

    # поднимем человечка
    translate!(figure["torso"], 0, 0, 20)
    # добавим пол
    floor && mesh!(s, Rect3f(Vec3f(-400, -400, -2), Vec3f(800, 800, 2)), color=:white)
    return figure
end

# Наконец, пусть он пройдется, а мы запишем видео с новым экспериментальным бэкендом для трассировки лучей.

# Примечание. RPRMakie все еще не очень стабилен, и визуализация выполняется довольно медленно в CI, поэтому показанное видео было отрисовано заранее.

using RPRMakie
# повторим отрисовку 200 раз, чтобы получить меньше шума и больше света
RPRMakie.activate!(iterations=200)

radiance = 50000
# Обратите внимание, что `EnvironmentLight` поддерживается пока только в RPRMakie.
lights = [
    EnvironmentLight(1.5, rotl90(load(assetpath("sunflowers_1k.hdr"))')),
    PointLight(Vec3f(50, 0, 200), RGBf(radiance, radiance, radiance*1.1)),
]
s = Scene(size=(500, 500), lights=lights)
cam3d!(s)
c = cameracontrols(s)
c.near[] = 5
c.far[] = 1000
update_cam!(s, c, Vec3f(100, 30, 80), Vec3f(0, 0, -10))
figure = plot_lego_figure(s)

rot_joints_by = 0.25*pi
total_translation = 50
animation_strides = 10

a1 = LinRange(0, rot_joints_by, animation_strides)
angles = [a1; reverse(a1[1:end-1]); -a1[2:end]; reverse(-a1[1:end-1]);]
nsteps = length(angles); #Количество шагов анимации
translations = LinRange(0, total_translation, nsteps)

Makie.record(s, "lego_walk.mp4", zip(translations, angles)) do (translation, angle)
    #Повернем правую руку
    for name in ["arm_left", "arm_right",
                            "leg_left", "leg_right"]
        rotate!(figure[name], rotation_axes[name], angle)
    end
    translate!(figure["torso"], translation, 0, 20)
end