Начало моделирования графа сцены 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
При использовании Scenes напрямую необходимо вручную настроить камеру и отцентрировать ее по содержанию сцены. Согласно более подробному описанию в разделе «Камера» существует несколько функций cam***! для установки определенной проекции и типа камеры для сцены.
cam3d!(subwindow)
meshscatter!(subwindow, rand(Point3f, 10), color=:gray)
center!(subwindow)
scene
Вместо применения белого фона можно отключить его очистку, сделав сцену прозрачной, и добавить контур. Создать контур проще всего путем построения подсцены с проекцией от 0…1 для всего окна. Чтобы создать подсцену с определенным типом проекции, Makie предлагает для каждой функции камеры версию без !, которая создаст подсцену и применит тип камеры. Пространство с проекцией от 0…1 называется относительным (relative) пространством, так что camrelative даст эту проекцию.
subwindow.clear = false
relative_space = Makie.camrelative(subwindow)
# рисует линию по границе окна сцены
lines!(relative_space, Rect(0, 0, 1, 1))
scene
Также можно задать родительской сцене более интересный фон, используя 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
translate!(scene.plots[1], 0, 0, -10000)
scene
Нам необходимо довольно высокое преобразование, поскольку дальняя и ближняя плоскости для campixel! идут от -1000 до 1000, а для cam3d! они автоматически подстраиваются под параметры камеры. Они оказываются в одном и том же буфере глубины, преобразованном в диапазон 0..1 с помощью дальней и ближней плоскостей. Поэтому, чтобы оставаться позади трехмерной сцены, нужно задать большое значение.
При clear = true у нас не было бы этой проблемы.
screen = display(scene) # используем display, чтобы получить ссылку на объект экрана
depth_color = GLMakie.depthbuffer(screen)
close(screen)
# Посмотрим на результат.
f, ax, pl = heatmap(depth_color)
Colorbar(f[1, 2], pl)
f
События окна
Каждая сцена также содержит ссылку на все глобальные события окна.
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
Это так, потому что матрица проекции и матрица представления по умолчанию являются матрицами тождества, а пространство единиц Makie в мире OpenGL называется Clip space.
cam = Makie.camera(scene) # Вот как получить доступ к камере сцен.
Можно изменить отображение, например построить от --3 до 5 с помощью матрицы ортогональной проекции.
cam.projection[] = Makie.orthographicprojection(-3f0, 5f0, -3f0, 5f0, -100f0, 100f0)
scene
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
Взаимодействие с осями и макетами
Ось содержит сцену, проекция которой настроена так, чтобы координаты шли от (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
Преобразования и граф сцены
До сих пор мы обсуждали только преобразования сцены с помощью камеры. В отличие от них существуют также преобразования сцены, называемые глобальными (мировыми) преобразованиями. Дополнительные сведения о различных пространствах можно найти на этой странице learn opengl.
Глобальное преобразование реализовано через структуру Transformation в Makie. Их содержат и сцены, и графики, поэтому эти типы считаются Makie.Transformable. Преобразование сцены будет унаследовано всеми графиками, добавляемыми к сцене. Для работы с Transformable существуют следующие три функции.
#
Makie.translate! — Function
translate!(t::Transformable, xyz::VecTypes)
translate!(t::Transformable, xyz...)
Применяет абсолютное перемещение к данному объекту Transformable (сцене или графику), перемещая его в позицию x, y, z.
#
Makie.rotate! — Function
rotate!(Accum, t::Transformable, axis_rot...)
Применяет относительный поворот к трансформируемому объекту путем умножения на текущий поворот.
#
Makie.scale! — Function
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
Можно также преобразовывать объекты графика напрямую. После этого преобразование из объекта графика будет добавлено поверх преобразования из сцены. Можно добавлять подсцены и динамически взаимодействовать с ними. Makie предлагает то, что обычно называют графом сцены.
translate!(sphere_plot, Vec3f(0, 0, 1))
scene
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
# Теперь повернем конечность в «суставе».
rotate!(s2, Vec3f(0, 1, 0), 0.5)
rotate!(s3, Vec3f(1, 0, 0), 0.5)
parent
Этот базовый принцип позволит оживить даже роботов.) 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