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

Прозрачность

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

Чтобы сделать график прозрачным, необходимо добавить альфа-значение к его атрибуту color или colormap.

using CairoMakie
using FileIO

# color
fig, ax, p = image(0..11, -1..11, rotr90(FileIO.load(Makie.assetpath("cow.png"))))
scatter!(ax, 1:10,fill(10, 10), markersize = 40, color = :red)
scatter!(ax, 1:10, fill(9, 10), markersize = 40, color = (:red, 0.5))
scatter!(ax, 1:10, fill(8, 10), markersize = 40, color = RGBf(0.8, 0.6, 0.1))
scatter!(ax, 1:10, fill(7, 10), markersize = 40, color = RGBAf(0.8, 0.6, 0.1, 0.5))

# colormap
scatter!(ax, 1:10, fill(5, 10), markersize = 40, color = 1:10, colormap = :viridis)
scatter!(ax, 1:10, fill(4, 10), markersize = 40, color = 1:10, colormap = (:viridis, 0.5),)
scatter!(ax, 1:10, fill(3, 10), markersize = 40, color = 1:10, colormap = [:red, :orange],)
scatter!(ax, 1:10, fill(2, 10), markersize = 40, color = 1:10, colormap = [(:red, 0.5), (:orange, 0.5)])
cm = [RGBf(x^2, 1 - x^2, 0.2) for x in range(0, 1, length=100)]
scatter!(ax, 1:10, fill(1, 10), markersize = 40, color = 1:10, colormap = cm)
cm = [RGBAf(x^2, 1 - x^2, 0.2, 0.5) for x in range(0, 1, length=100)]
scatter!(ax, 1:10, fill(0, 10), markersize = 40, color = 1:10, colormap = cm)
fig
25aedd6

Проблемы с прозрачностью

Цвет, получаемый при перекрытии двух прозрачных объектов, зависит от их порядка. Возьмем, например, красный и синий маркеры с одинаковым уровнем прозрачности. Если синий маркер находится впереди, в месте их перекрытия следует ожидать более синего цвета. Если красный маркер находится впереди, следует ожидать более красного цвета.

using CairoMakie
scene = Scene(size = (400, 275))
campixel!(scene)
scatter!(
    scene, [100, 200, 300], [100, 100, 100],
    color = [RGBAf(1,0,0,0.5), RGBAf(0,0,1,0.5), RGBAf(1,0,0,0.5)],
    markersize=200
)
scatter!(scene, Point2f(150, 175), color = (:green, 0.5), markersize=200)
p = scatter!(scene, Point2f(250, 175), color = (:green, 0.5), markersize=200)
translate!(p, 0, 0, -1)
scene
4cdaa2e

На рисунке выше соблюдаются три правила прозрачности.

  1. Если два графика находятся на разных z-уровнях, график с более высоким z-уровнем будет располагаться перед графиком с более низким уровнем. (Зеленый круг справа находится позади всех остальных графиков.)

  2. Порядок отрисовки графиков на одном z-уровне соответствует очередности их создания. (Красный и синий круги находятся за левым зеленым кругом.)

  3. Элементы графика отрисовываются по порядку. (Левый красный круг находится за средним синим кругом, который находится за правым красным кругом.)

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

using CairoMakie
fig = Figure()
ax = LScene(fig[1, 1], show_axis=false)
p1 = mesh!(ax, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = NoShading)
p2 = mesh!(ax, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = NoShading)
rotate!(p1, Vec3f(0, 1, 0), 0.1)
rotate!(p2, Vec3f(0, 1, 0), -0.1)
fig
f60489a

using GLMakie
fig = Figure()
ax = LScene(fig[1, 1], show_axis=false)
p1 = mesh!(ax, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = NoShading)
p2 = mesh!(ax, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = NoShading)
rotate!(p1, Vec3f(0, 1, 0), 0.1)
rotate!(p2, Vec3f(0, 1, 0), -0.1)
fig
4f2f7d4

Оба бэкенда обрабатывают этот случай неправильно. Кажется, что CairoMakie игнорирует глубину и просто отрисовывает плоскости в порядке их построения. Однако это не совсем так: бэкенд CairoMakie учитывает глубину для каждого графика, а в некоторых случаях и для каждого элемента (например, треугольников в трехмерной сетке). Но он не может обрабатывать глубину на уровне пикселей.

В свою очередь, GLMakie может обрабатывать глубину на уровне пикселей, как видно из правильного порядка выше. Проблема с прозрачностью здесь заключается в том, что очередность применения цветов к пикселю заранее неизвестна. GLMakie сначала отрисует красную плоскость и зарегистрирует значения глубины для каждого пикселя. Затем он отрисует синюю плоскость, если она находится перед красной. Точное решение этой задачи потребовало бы сбора цветов и значений глубины для каждого пикселя, их сортировки, а затем смешивания в определенном порядке. Это было бы очень затратно и поэтому редко применяется.

Прозрачность, независимая от порядка

В GLMakie реализована приблизительная схема смешения прозрачных цветов — прозрачность, независимая от порядка (OIT). Вместо использования обычного зависимого от порядка смешения alpha * color + (1 - alpha) * background_color здесь применяется взвешенная сумма с весами, основанными на глубине и альфа-значении. Вы можете включить OIT, задав transparency = true для определенного графика.

using GLMakie
fig = Figure()
ax = LScene(fig[1, 1], show_axis=false)
p1 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:red, 0.5), shading = NoShading, transparency = true)
p2 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:blue, 0.5), shading = NoShading, transparency = true)
p3 = mesh!(ax, Rect2f(-2, -2, 4, 4), color = (:red, 0.5), shading = NoShading, transparency = true)
for (dz, p) in zip((-1, 0, 1), (p1, p2, p3))
    translate!(p, 0, 0, dz)
end
fig
fb317df

Будучи приблизительной схемой, OIT имеет свои сильные и слабые стороны. У OIT есть два существенных недостатка.

  1. Смешение происходит всегда, даже если полностью непрозрачный цвет (альфа-значение = 1) должен скрывать другой.

  2. Смешение не является четким — когда два цвета с одинаковым альфа-значением смешиваются при схожих значениях глубины, их выходной цвет будет схожим.

using GLMakie
fig = Figure(size = (800, 400))
ax1 = LScene(fig[1, 1], show_axis=false)
p1 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :red, shading = NoShading, transparency = true)
p2 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :blue, shading = NoShading, transparency = true)
p3 = mesh!(ax1, Rect2f(-2, -2, 4, 4), color = :red, shading = NoShading, transparency = true)
for (dz, p) in zip((-1, 0, 1), (p1, p2, p3))
    translate!(p, 0, 0, dz)
end

ax2 = LScene(fig[1, 2], show_axis=false)
p1 = mesh!(ax2, Rect2f(-1.5, -1, 3, 3), color = (:red, 0.5), shading = NoShading, transparency=true)
p2 = mesh!(ax2, Rect2f(-1.5, -2, 3, 3), color = (:blue, 0.5), shading = NoShading, transparency=true)
rotate!(p1, Vec3f(0, 1, 0), 0.1)
rotate!(p2, Vec3f(0, 1, 0), -0.1)
fig
c4139b3

Обратите внимание, что вы можете без проблем смешивать непрозрачные графики с атрибутом transparency = false с прозрачными графиками OIT. Таким образом, первая проблема на самом деле актуальна не для действительно непрозрачных графиков, а скорее для графиков, близких к непрозрачным.

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

  1. Переместите плоскость отсечения near ближе к камере. Для ax::LScene это можно сделать, приблизив ax.scene.camera_controls.near[] к 0 и вызвав update_cam!(ax.scene, ax.scene.camera_controls).

  2. Уменьшите значение GLMakie.transparency_weight_scale[], которое контролирует максимальный вес, придаваемый прозрачному цвету. Значение по умолчанию — 1000f0.

Настройка второго из этих значений также может помочь повысить резкость.