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

RPRMakie

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

Экспериментальный бэкенд трассировки лучей с использованием RadeonProRender от AMD. Хотя он создан компанией AMD и адаптирован для GPU Radeon, он также хорошо работает на GPU NVidia и Intel, использующих OpenCL. Он также работает на ЦП и даже имеет гибридный режим совместного использования GPU и ЦП для отрисовки изображений.

В настоящее время RadeonProRender работает только в Windows и Linux и завершается сбоем в OSX при создании даже самого простого контекста. Если вы используете OSX и хорошо разбираетесь в отладке аварийных сбоев, помогите нам, выполнив отладку следующего вызова.

using RadeonProRender
RadeonProRender.Context()

Чтобы использовать RPRMakie на компьютере Mac с процессором серии M, на данный момент вам потребуется сборка Julia x86_64, а не сборка ARM (возможно, ее придется скачать вручную). Для RadeonProRender пока не распространяются двоичные файлы, собранные для архитектуры ARM процессоров серии M.

Активация и настройка экрана

Чтобы активировать бэкенд, вызовите RPRMakie.activate!() со следующими параметрами.

RPRMakie.activate!(; screen_config...)

Устанавливает RPRMakie в качестве текущего активного бэкенда, а также позволяет быстро задать screen_config. Обратите внимание, что screen_config также можно задать на постоянной основе с помощью Makie.set_theme!(RPRMakie=(screen_config...,)).

Аргументы, которые можно передавать через screen_config:

  • iterations = 200: итерации моделирования освещения. Чем больше итераций, тем менее зашумленным будет изображение, но тем больше времени займет обработка. Например, при значении iterations=10 можно ожидать, что отрисовка займет пару секунд, но при этом результат получится довольно зашумленным. Хорошим компромиссным вариантом будет значение 200: на старой видеокарте nVidia 1060 отрисовка займет примерно 30 секунд. Для максимального качества требуются значения более 500.

  • resource = RPR.RPR_CREATION_FLAGS_ENABLE_GPU0: используемый GPU или ЦП. Можно выбрать одновременно несколько GPU и ЦП с помощью оператора & (например, RPR.RPR_CREATION_FLAGS_ENABLE_GPU0 & RPR.RPR_CREATION_FLAGS_ENABLE_CPU).

  • plugin = RPR.Tahoe:

    • RPR.Tahoe, устаревший бэкенд RadeonProRender. Он самый стабильный, но лишен новых функций (например, материала RPR.MatX) и может работать медленнее других.

    • RPR.Northstar, новый переработанный бэкенд, более быстрый и оптимизированный для большого числа итераций. Отдельные итерации выполняются намного медленнее, поэтому он хуже подходит для интерактивного отображения. Иногда Northstar просто выдает черные дрожащие объекты. Пока не ясно, является ли это просто ошибкой либо результатом использования неподдерживаемой или устаревшей функции. Если такое происходит, переключитесь на Tahoe.

    • RPR.Hybrid: бэкенд Vulkan, подходящий для отрисовки в реальном времени с использованием новой трассировки лучей с аппаратным ускорением от AMD и NVIDIA. Пока работает ненадежно и только с материалом RPR.Uber.

    • RPR.HybridPro: то же, что и Hybrid, но работает только с GPU Radeon, используя собственный API аппаратного ускорения AMD.

Поскольку RPRMakie представляет собой довольно уникальный бэкенд и все еще находится на экспериментальной стадии, при работе с ним есть несколько подводных камней.

fig = Figure(); # RPRMakie пока не может отображать рисунки, так как поддерживает только физическую трехмерную камеру.
radiance = 10000
# Источники света гораздо важнее для трассировки лучей,
# поэтому в большинстве примеров будут использоваться дополнительные источники света и источники окружающего света.
# Обратите внимание, что RPRMakie — это единственный бэкенд,
# поддерживающий на данный момент несколько источников света и EnvironmentLights.
lights = [
    EnvironmentLight(0.5, Makie.FileIO.load(RPR.assetpath("studio026.exr"))),
    PointLight(Vec3f(0, 0, 20), RGBf(radiance, radiance, radiance))
]

# В настоящее время поддерживается только LScene,
# поскольку другие проекции не соответствуют физически точной камере в RPR.
ax = LScene(fig[1, 1]; show_axis = false, scenekw=(lights=lights,))
# Обратите внимание: поскольку RPRMakie пока не поддерживает текст (над этим ведется работа),
# пока невозможно отобразить трехмерную ось.

# Для создания материалов необходим доступ к контексту RPR.
# Обратите внимание: если вы создаете экран вручную, больше не отображайте сцену или рисунок, так как это создаст новый контекст RPR, в котором ресурсы из созданного вручную контекста будут недействительными. Так как обработка ошибок реализована в RPR довольно плохо, это обычно приводит к аварийным сбоям.
# Ниже показано, как отрисовать изображение с помощью вручную созданного контекста.
screen = RPRMakie.Screen(ax.scene; iterations=10, plugin=RPR.Northstar)
matsys = screen.matsys
context = screen.context
# Вы можете использовать множество материалов из RPR.
# Обратите внимание, что в будущем этот API может измениться на независимое от бэкенда представление —
# или по крайней мере на такое, которому не будет требоваться доступ к контексту RPR.
mat = RPR.Chrome(matsys)
# Атрибут материала специфичен для RPRMakie и игнорируется другими бэкендами. Это может измениться в будущем.
mesh!(ax, Sphere(Point3f(0), 1), material=mat)

# Преобразовать сцену Makie в изображение можно тремя основными способами.
# Получите цветовой буфер экрана. Экран также имеет перегрузку `show` для типа mime `image\png`, поэтому он должен отображаться в IJulia, Jupyter и VSCode.
image = colorbuffer(screen)::Matrix{RGB{N0f8}}
# Замените определенную сцену или подсцену LScene на RPR и отобразите всю сцену интерактивно в RPRMakie.
using RPRMakie
refresh = Observable(nothing) # Необязательный наблюдаемый объект, который запускает повторную отрисовку
display(ax.scene; backend=GLMakie) # Обязательно сначала отобразите сцену в GLMakie.
# Замените сцену интерактивно отрисованным выводом RPR.
# Подробнее об этом см. в примере взаимодействия RPRMakie.
context, task = RPRMakie.replace_scene_rpr!(ax.scene, screen; refresh=refresh)
# Если экран не создается вручную для создания пользовательских материалов:
# display(ax.scene), show(io, MIME"image/png", ax.scene), save("rpr.png", ax.scene)
# Должно работать так же, как и с другими бэкендами.
# Обратите внимание, что напрямую можно отобразить только сцену из LScene, но вскоре вызов `display(fig)` также должен заработать.

Есть несколько примеров, демонстрирующих различные аспекты использования RPRMakie. Они приведены на странице RPRMakie/examples.

MaterialX и предварительно определенные материалы (materials.jl)

В RadeonProRender можно использовать несколько предварительно определенных материалов. RPR также поддерживает стандарт MaterialX для загрузки широкого спектра предварительно определенных материалов. Обязательно используйте бэкенд Northstar для MaterialX.

using GeometryBasics, RPRMakie
using Colors, FileIO
using Colors: N0f8

radiance = 500
lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))),
            PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))]
fig = Figure(; size=(1500, 700));
ax = LScene(fig[1, 1]; show_axis=false, scenekw=(; lights=lights))
screen = RPRMakie.Screen(ax.scene; plugin=RPR.Northstar, iterations=400)

matsys = screen.matsys
emissive = RPR.EmissiveMaterial(matsys)
diffuse = RPR.DiffuseMaterial(matsys)
glass = RPR.Glass(matsys)
plastic = RPR.Plastic(matsys)
chrome = RPR.Chrome(matsys)
dielectric = RPR.DielectricBrdfX(matsys)
gold = RPR.SurfaceGoldX(matsys)

materials = [glass chrome;
                gold dielectric;
                emissive plastic]

mesh!(ax, load(Makie.assetpath("matball_floor.obj")); color=:white)
palette = reshape(Makie.wong_colors()[1:6], size(materials))

for i in CartesianIndices(materials)
    x, y = Tuple(i)
    mat = materials[i]
    mplot = if mat === emissive
        matball!(ax, diffuse; inner=emissive, color=nothing)
    else
        matball!(ax, mat; color=nothing)
    end
    v = Vec3f(((x, y) .- (0.5 .* size(materials)) .- 0.5)..., 0)
    translate!(mplot, 0.9 .* (v .- Vec3f(0, 3, 0)))
end
cam = cameracontrols(ax.scene)
cam.eyeposition[] = Vec3f(-0.3, -5.5, 0.9)
cam.lookat[] = Vec3f(0.5, 0, -0.5)
cam.upvector[] = Vec3f(0, 0, 1)
cam.fov[] = 35
emissive.color = Vec3f(4, 2, 2)
image = colorbuffer(screen)
save("materials.png", image)
materials

Расширенный пользовательский материал (earth_topography.jl)

using NCDatasets, ColorSchemes, RPRMakie
using ImageShow, FileIO

# Источник: https://lazarusa.github.io/BeautifulMakie/GeoPlots/topography/
cmap = dataset = Dataset(joinpath(@__DIR__, "ETOPO1_halfdegree.nc"))
lon = dataset["lon"][:]
lat = dataset["lat"][:]
data = Float32.(dataset["ETOPO1avg"][:, :])

function glow_material(data_normed)
    emission_weight = map(data_normed) do i
        return Float32(i < 0.7 ? 0.0 : i)
    end
    emission_color = map(data_normed) do i
        em = i * 2
        return RGBf(em * 2.0, em * 0.4, em * 0.3)
    end

    return (
        reflection_weight = 1,
        reflection_color = RGBf(0.5, 0.5, 1.0),
        reflection_metalness = 0,
        reflection_ior = 1.4,
        diffuse_weight = 1,
        emission_weight = emission_weight',
        emission_color = emission_color',
    )
end

RPRMakie.activate!(iterations=32, plugin=RPR.Northstar)
fig = Figure(; size=(2000, 800))
radiance = 30000
lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))),
            PointLight(Vec3f(0, 100, 100), RGBf(radiance, radiance, radiance))]

ax = LScene(fig[1, 1]; show_axis=false, scenekw=(lights=lights,))

mini, maxi = extrema(data)
data_normed = ((data .- mini) ./ (maxi - mini))

material = glow_material(data_normed)

pltobj = surface!(ax, lon, lat, data_normed .* 20;
                    material=material, colormap=[:black, :white, :brown],
                    colorrange=(0.2, 0.8) .* 20)
# Устанавливаем камеру под подходящим углом
cam = cameracontrols(ax.scene)
cam.eyeposition[] = Vec3f(3, -300, 300)
cam.lookat[] = Vec3f(0)
cam.upvector[] = Vec3f(0, 0, 1)
cam.fov[] = 23

save("topographie.png", ax.scene)
topographie

Взаимодействие с RPRMakie (opengl_interop.jl)

RPRMakie пока не поддерживает компоновку макетов и подсцены, но отдельную сцену можно заменить интерактивным окном, отрисованным с помощью RPR. Это особенно удобно для отображения двухмерной графики и интерактивных элементов пользовательского интерфейса рядом со сценой с трассировкой лучей, а также для интерактивной настройки параметров камеры и материалов.

using RPRMakie, GeometryBasics, RPRMakie, RadeonProRender
using Colors, FileIO
using Colors: N0f8

f = (u, v) -> cos(v) * (6 - (5 / 4 + sin(3 * u)) * sin(u - 3 * v))
g = (u, v) -> sin(v) * (6 - (5 / 4 + sin(3 * u)) * sin(u - 3 * v))
h = (u, v) -> -cos(u - 3 * v) * (5 / 4 + sin(3 * u));
u = range(0; stop=2π, length=150)
v = range(0; stop=2π, length=150)
radiance = 500
lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))),
          PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))]

fig = Figure(; size=(1500, 1000))
ax = LScene(fig[1, 1]; show_axis=false, scenekw=(; lights=lights))
screen = RPRMakie.Screen(size(ax.scene); plugin=RPR.Tahoe)
material = RPR.UberMaterial(screen.matsys)

surface!(ax, f.(u, v'), g.(u, v'), h.(u, v'); ambient=Vec3f(0.5), diffuse=Vec3f(1), specular=0.5,
         colormap=:balance, material=material)

function Input(fig, val::RGB)
    hue = Slider(fig; range=1:380, width=200)
    lightness = Slider(fig; range=LinRange(0, 1, 100), width=200)
    labels = [Label(fig, "hue"; halign=:left), Label(fig, "light"; halign=:left)]
    layout = grid!(hcat(labels, [hue, lightness]))
    hsl = HSL(val)
    set_close_to!(hue, hsl.h)
    set_close_to!(lightness, hsl.l)
    color = map((h, l) -> RGB(HSL(h, 0.9, l)), hue.value, lightness.value)
    return color, layout
end

function Input(fig, val::Vec4)
    s = Slider(fig; range=LinRange(0, 1, 100), width=200)
    set_close_to!(s, first(val))
    return map(x -> Vec4f(x), s.value), s
end

function Input(fig, val::Bool)
    toggle = Toggle(fig; active=val)
    return toggle.active, toggle
end

sliders = (reflection_color=Input(fig, RGB(0, 0, 0)), reflection_weight=Input(fig, Vec4(0)),
           reflection_roughness=Input(fig, Vec4(0)), reflection_anisotropy=Input(fig, Vec4(0)),
           reflection_anisotropy_rotation=Input(fig, Vec4(0)), reflection_mode=Input(fig, Vec4(0)),
           reflection_ior=Input(fig, Vec4(0)), reflection_metalness=Input(fig, Vec4(0)),
           refraction_color=Input(fig, RGB(0, 0, 0)), refraction_weight=Input(fig, Vec4(0)),
           refraction_roughness=Input(fig, Vec4(0)), refraction_ior=Input(fig, Vec4(0)),
           refraction_absorption_color=Input(fig, RGB(0, 0, 0)),
           refraction_absorption_distance=Input(fig, Vec4(0)), refraction_caustics=Input(fig, true),
           sss_scatter_color=Input(fig, RGB(0, 0, 0)), sss_scatter_distance=Input(fig, Vec4(0)),
           sss_scatter_direction=Input(fig, Vec4(0)), sss_weight=Input(fig, Vec4(0)),
           sss_multiscatter=Input(fig, false), backscatter_weight=Input(fig, Vec4(0)),
           backscatter_color=Input(fig, RGB(0, 0, 0)))

labels = []
inputs = []
refresh = Observable(nothing)
for (key, (obs, input)) in pairs(sliders)
    push!(labels, Label(fig, string(key); align=:left))
    push!(inputs, input)
    on(obs) do value
        setproperty!(material, key, value)
        return notify(refresh)
    end
end

fig[1, 2] = grid!(hcat(labels, inputs); width=500)
RPRMakie.activate!()

cam = cameracontrols(ax.scene)
cam.eyeposition[] = Vec3f(22, 0, 17)
cam.lookat[] = Vec3f(0, 0, -1)
cam.upvector[] = Vec3f(0, 0, 1)
cam.fov[] = 30

display(fig)

context, task = RPRMakie.replace_scene_rpr!(ax.scene, screen; refresh=refresh)

# Интерактивно изменяем параметры освещения
begin
    lights[1].intensity[] = 1.5
    lights[2].radiance[] = RGBf(1000, 1000, 1000)
    lights[2].position[] = Vec3f(3, 10, 10)
    notify(refresh)
end

Анимации (lego.jl)

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

# Пример и модель Lego позаимствованы у https://github.com/Kevin-Mattheus-Moerman.
# https://twitter.com/KMMoerman/status/1417759722963415041
using MeshIO, FileIO, GeometryBasics, RPRMakie

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]]
    trans = 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!(trans, origin)
    else
        translate!(trans, -ptrans.translation[])
    end
    return mesh!(scene, m; color=color, transformation=trans)
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.activate!(iterations=200, plugin=RPR.Northstar)
radiance = 50000
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

Пример Земли

# Автор: Лазаро Алонсо (Lazaro Alonso)
# Источник: https://lazarusa.github.io/BeautifulMakie/GeoPlots/submarineCables3D/
using GeoMakie, Downloads
using GeoJSON, GeoInterface
using FileIO
using RPRMakie
# Источник данных:
# https://github.com/telegeography/www.submarinecablemap.com
urlPoints = "https://raw.githubusercontent.com/telegeography/www.submarinecablemap.com/master/web/public/api/v3/landing-point/landing-point-geo.json"
urlCables = "https://raw.githubusercontent.com/telegeography/www.submarinecablemap.com/master/web/public/api/v3/cable/cable-geo.json"

landPoints = Downloads.download(urlPoints, IOBuffer())
landCables = Downloads.download(urlCables, IOBuffer())

land_geoPoints = GeoJSON.read(seekstart(landPoints))
land_geoCables = GeoJSON.read(seekstart(landCables))

toPoints = GeoMakie.geo2basic(land_geoPoints)
feat = GeoInterface.features(land_geoCables)
toLines = GeoInterface.coordinates.(GeoInterface.geometry.(feat))

# Ломаные линии в –180 и 180... они должны
# быть одной линией в одном массиве.

# Некоторые трехмерные преобразования
function toCartesian(lon, lat; r = 1.02, cxyz = (0, 0, 0))
    x = cxyz[1] + r * cosd(lat) * cosd(lon)
    y = cxyz[2] + r * cosd(lat) * sind(lon)
    z = cxyz[3] + r * sind(lat)
    return (x, y, z)
end

toPoints3D = [Point3f([toCartesian(point[1], point[2])...]) for point in toPoints]

splitLines3D = []
for i in 1:length(toLines)
    for j in 1:length(toLines[i])
        ptsLines = toLines[i][j]
        tmp3D = []
        for k in 1:length(ptsLines)
            x, y = ptsLines[k]
            x, y, z = toCartesian(x, y)
            push!(tmp3D, [x, y, z])
        end
        push!(splitLines3D, Point3f.(tmp3D))
    end
end

earth_img = load(Downloads.download("https://upload.wikimedia.org/wikipedia/commons/5/56/Blue_Marble_Next_Generation_%2B_topography_%2B_bathymetry.jpg"))
# Фактический график
RPRMakie.activate!(; iterations=100)
scene = with_theme(theme_dark()) do
    fig = Figure(; size=(1000, 1000))
    radiance = 30
    lights = [EnvironmentLight(0.5, load(RPR.assetpath("starmap_4k.tif"))),
              PointLight(Vec3f(1, 1, 3), RGBf(radiance, radiance, radiance))]
    ax = LScene(fig[1, 1]; show_axis=false, scenekw=(;lights=lights))
    n = 1024 ÷ 4 # 2048
    θ = LinRange(0, pi, n)
    φ = LinRange(-pi, pi, 2 * n)
    xe = [cos(φ) * sin(θ) for θ in θ, φ in φ]
    ye = [sin(φ) * sin(θ) for θ in θ, φ in φ]
    ze = [cos(θ) for θ in θ, φ in φ]
    surface!(ax, xe, ye, ze; color=earth_img)
    meshscatter!(toPoints3D; color=1:length(toPoints3D), markersize=0.005, colormap=:plasma)
    colors = Makie.default_palettes.color[]
    c = Iterators.cycle(colors)
    foreach(((l, c),) -> lines!(ax, l; linewidth=2, color=c), zip(splitLines3D, c))
    ax.scene.camera_controls.eyeposition[] = Vec3f(1.5)
    return ax.scene
end

save("submarine_cables.png", scene)
submarine cables