EN

Математика цвета: Внутри Photoshop Gradients

Также доступно на английском

Прежде чем начать

На тему «Как работает цвет» написано гигантское количество статей, и куда более компетентными людьми. В этой статье мы ограничимся только теми знаниями, которые нужны для понимания этой серии статей.

Если вы хотите проникнуться глубиной этих знаний — я крайне рекомендую следующие ресурсы:

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

Цвет сложнее, чем кажется

Возьмём два цвета и построим переход между ними. Простейший способ — линейная интерполяция: разложить на R, G, B и для каждого канала вычислить промежуточное значение.

f(t)=a+(ba)tf(t) = a + (b - a) \cdot t

Где t ∈ [0, 1] — позиция в переходе: t = 0 — начальный цвет, t = 1 — конечный.

Казалось бы, готово? Но вот вам четыре метода интерполяции, которые предлагает Photoshop для одного и того же градиента:

Все четыре градиента заметно отличаются. Можно решить, что дело в разных формулах интерполяции — но главная причина в другом: интерполяция происходит в разных цветовых пространствах.

Gamma, sRGB и оптический обман зрения

sRGB — цветовое пространство «для глаз»

Когда мы выбираем цвет в Photoshop, задаём RGB в CSS, или сохраняем PNG — мы работаем в sRGB. Это стандарт кодирования цвета, определяющий как числа (R, G, B ∈ [0, 255]) соответствуют видимым цветам.

Но у меня есть для вас загадка: Почему значение 128 — это не половина яркости 255?

Отгадка на этот вопрос кроется в чувствительности восприятия человеческого глаза. Зрение устроено таким образом, что оно значительно более восприимчиво к тёмным тонам. То есть вы можете отличить больше тёмных тонов друг от друга, чем светлых того же объёма.

Это приводит нас к ответу на вопрос про яркость — 128 не половина яркости, потому что зависимость намеренно нелинейна, и отражает чувствительность человеческого зрения, чтобы избежать ступенчатости (цвета с разной яркостью сливаются в один и образуют «плоскую ступеньку»).

sRGB кодирует значения нелинейно, распределяя больше уровней яркости в тёмных тонах.

Но возникает ещё больше вопросов: если sRGB кодирует и сжимает, то что конкретно он кодирует и что сжимает?

Linear-light — физическое пространство света

Linear-light RGB — это несжатое пространство физического света, где каждый канал выражен значением от 0 до 1. Здесь значение 0.5 — это ровно половина физической яркости.

Именно его сжимает sRGB при помощи gamma-коррекциитрансферной функции.

Преобразование Linear RGB в sRGB
VsRGB={12.92Vlinearif Vlinear0.00313081.055Vlinear1/2.40.055otherwiseV_{\text{sRGB}} = \begin{cases} 12.92 \cdot V_{\text{linear}} & \text{if } V_{\text{linear}} \leq 0.0031308 \\ 1.055 \cdot V_{\text{linear}}^{1 / 2.4} - 0.055 & \text{otherwise} \end{cases}

Гамма-функция применяется поканально, и она полностью обратима, позволяя перемещаться из одного цветового пространства в другое.

Преобразование sRGB в Linear RGB
Vlinear={VsRGB12.92if VsRGB0.04045(VsRGB+0.0551.055)2.4otherwiseV_{\text{linear}} = \begin{cases} \dfrac{V_{\text{sRGB}}}{12.92} & \text{if } V_{\text{sRGB}} \leq 0.04045 \\[6pt] \left(\dfrac{V_{\text{sRGB}} + 0.055}{1.055}\right)^{2.4} & \text{otherwise} \end{cases}

Почему это важно для градиентов

Метод «Classic» в Photoshop интерполирует в sRGB. А метод Linear — в linear-light RGB. Одна и та же формула интерполяции, по RGB каналам, но визуально разный результат:

  • В sRGB середина (128, 128, 128) → яркость ≈ 21%.
  • В linear-light середина (0.5, 0.5, 0.5) → sRGB ≈ (184, 184, 184) → яркость = 50%.

Можно заметить, что sRGB интерполяция выглядит «равномерной», это происходит именно по той причине, что мы описали выше — ваш глаз гораздо чувствительнее к тёмным тонам, нежели к светлым.

Oklab — пространство для восприятия

Ни sRGB, ни linear RGB не являются перцептуально-равномерными. Одинаковое числовое расстояние между двумя цветами может восприниматься как большое или наоборот как маленькое, в зависимости от оттенка и яркости.

В 2020 году Björn Ottosson разработал цветовое пространство Oklab, в котором выделил три компонента:

  • L — lightness,
  • a — ось от зелёного к красному,
  • b — ось от синего к жёлтому.

Основное достоинство Oklab в том, что интерполяция в этом цветовом пространстве приближена к перцептуально-равномерной, и интуитивно воспринимается как более точная и верная.

На примерах отчётливо видно, что sRGB интерполяция выглядит «грязной» на стыках переходов, там где Oklab даёт яркий насыщенный цвет.

Математически преобразование между sRGB и Oklab несколько сложнее, так как требует двух промежуточных цветовых пространств, а именно linear RGB (который мы разобрали выше) и LMS.

LMS описывает отклик трёх типов цветовых рецепторов глаза (колбочки в сетчатке глаза):

  • L — Long wavelength (красный, пик ~564 нм)
  • M — Medium wavelength (зелёный, пик ~534 нм)
  • S — Short wavelength (синий, пик ~420 нм)

Oklab строит цветовое пространство начиная с того, как глаз физически воспринимает цвет. Кубический корень в формулах преобразования (см. ниже) моделирует нелинейный отклик рецепторов — это аналог gamma, но для биологической системы.

Преобразование sRGB в Oklab
  1. sRGB → linear RGB (формула находится выше)

  2. linear RGB → LMS

[lms]=[0.41222147080.53633253630.05144599290.21190349820.68069954510.10739695660.08830246190.28171883760.6299787005][RGB]linear\begin{bmatrix} l \\ m \\ s \end{bmatrix} = \begin{bmatrix} 0.4122214708 & 0.5363325363 & 0.0514459929 \\ 0.2119034982 & 0.6806995451 & 0.1073969566 \\ 0.0883024619 & 0.2817188376 & 0.6299787005 \end{bmatrix} \begin{bmatrix} R \\ G \\ B \end{bmatrix}_{\text{linear}}
  1. Кубический корень
l=l3,m=m3,s=s3l' = \sqrt[3]{l}, \quad m' = \sqrt[3]{m}, \quad s' = \sqrt[3]{s}
  1. LMS’ → Oklab:
[Lab]Oklab=[0.21045425530.79361778500.00407204681.97799849512.42859220500.45059370990.02590403710.78277176620.8086757660][lms]\begin{bmatrix} L \\ a \\ b \end{bmatrix}_{\text{Oklab}} = \begin{bmatrix} 0.2104542553 & 0.7936177850 & -0.0040720468 \\ 1.9779984951 & -2.4285922050 & 0.4505937099 \\ 0.0259040371 & 0.7827717662 & -0.8086757660 \end{bmatrix} \begin{bmatrix} l' \\ m' \\ s' \end{bmatrix}
Преобразование Oklab в sRGB
  1. Oklab → LMS
[lms]=[10.39633777740.215803757310.10556134580.063854172810.08948417751.2914855480][Lab]Oklab\begin{bmatrix} l' \\ m' \\ s' \end{bmatrix} = \begin{bmatrix} 1 & 0.3963377774 & 0.2158037573 \\ 1 & -0.1055613458 & -0.0638541728 \\ 1 & -0.0894841775 & -1.2914855480 \end{bmatrix} \begin{bmatrix} L \\ a \\ b \end{bmatrix}_{\text{Oklab}}
  1. Возведение в куб
l=l3,m=m3,s=s3l = l'^3, \quad m = m'^3, \quad s = s'^3
  1. LMS → linear RGB
[RGB]linear=[4.07674166213.30771159130.23096992921.26843800462.60975740110.34131939650.00419608630.70341861471.7076147010][lms]\begin{bmatrix} R \\ G \\ B \end{bmatrix}_{\text{linear}} = \begin{bmatrix} 4.0767416621 & -3.3077115913 & 0.2309699292 \\ -1.2684380046 & 2.6097574011 & -0.3413193965 \\ -0.0041960863 & -0.7034186147 & 1.7076147010 \end{bmatrix} \begin{bmatrix} l \\ m \\ s \end{bmatrix}
  1. linear RGB → sRGB (формулу можно найти выше)

Кубические кривые и плавные переходы

Мы разобрались, в каком пространстве интерполировать. Теперь разберёмся, как — какую форму может принимать кривая перехода.

До сих пор мы рассматривали линейную интерполяцию — прямую линию от V0 к V1. На полпути (t = 0.5) получаем ровно среднее двух значений. Никаких изгибов, никаких сюрпризов.

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

И Эрмит, и Безье описывают одну и ту же математику — построение S-образных кривых. Отличаются же они только параметризацией и способом вычисления.

Кубический сплайн Эрмита определяется через значения на концах V0, V1 и касательные на концах T0 и T1. Касательная определяет скорость и направление, с которыми кривая входит в точку или выходит из неё.

  • Большая касательная -> крутой вход/выход
  • Нулевая -> горизонтальный
  • Отрицательная (при положительной ΔV) -> кривая сначала идёт «не туда».

Влияние каждого компонента можно записать в виде строительного блока (базисной формулы):

hV0(t)=2t33t2+1— влияние начального значенияhT0(t)=t32t2+t— импульс начальной касательнойhV1(t)=2t3+3t2— влияние конечного значенияhT1(t)=t3t2— притяжение конечной касательной\def\arraystretch{1.8} \begin{array}{r l l} h_{V0}(t) &= 2t^3 - 3t^2 + 1 & \text{— влияние начального значения} \\ h_{T0}(t) &= t^3 - 2t^2 + t & \text{— импульс начальной касательной} \\ h_{V1}(t) &= -2t^3 + 3t^2 & \text{— влияние конечного значения} \\ h_{T1}(t) &= t^3 - t^2 & \text{— притяжение конечной касательной} \end{array}
Базисные функции Эрмита
hV0 hV1 hT0 hT1

Финальная формула интерполяции — сумма базисных функций, умноженных на значения кривой:

H(t)=hV0(t)V0+hT0(t)T0+hV1(t)V1+hT1(t)T1H(t) = h_{V0}(t) \cdot V_0 + h_{T0}(t) \cdot T_0 + h_{V1}(t) \cdot V_1 + h_{T1}(t) \cdot T_1

Откуда берутся касательные

Формула Эрмита принимает четыре параметра: значения на концах (V0, V1) и касательные (T0, T1). Значения — это цвета наших стопов (color stops), тут всё понятно. А вот касательные нужно откуда-то взять.

У градиента с двумя стопами — один сегмент, оба стопа крайние. У крайнего стопа сосед только с одной стороны, поэтому касательная определяется единственным сегментом — кривая плавно поднимается (или опускается) от V0 до V1:

T0=T1=V1V02T_0 = T_1 = \frac{V_1 - V_0}{2}

У градиента с тремя и более стопами появляются внутренние стопы (interior stops) — те, что между первым и последним. У каждого внутреннего стопа есть два соседних сегмента: левый (от предыдущего стопа) и правый (к следующему). Касательная в этой точке должна учитывать оба.

Некоторые из методов интерполяции используют формулу касательной Catmull-Rom — среднее направление между левым и правым сегментами:

Ti=Vi+1Vi12T_i = \frac{V_{i+1} - V_{i-1}}{2}

Если красный канал рос на левом сегменте (100 → 200) и продолжает расти на правом (200 → 250) — касательная положительная, кривая плавно проходит через стоп. Если рос слева (100 → 200), но падает справа (200 → 50) — касательная близка к нулю, кривая замедляется и разворачивается.

Concordant, discordant и overshoot

Для каждого канала на каждом внутреннем стопе возникает вопрос: куда направлена касательная относительно следующего сегмента?

Concordant (согласованный): касательная направлена туда же, куда идёт сегмент. Канал рос и продолжает расти, или падал и продолжает падать. Кривая плавно проходит через стоп, не выходя за пределы значений на его концах.

Discordant (рассогласованный): касательная направлена в противоположную сторону от сегмента. Канал рос, а теперь падает — но касательная по инерции продолжает тянуть вверх. Кривая вынуждена сначала пойти «не туда», потом развернуться. Этот заход за пределы называется overshoot — кривая выходит за границы значений на концах сегмента.

Flat (плоский): канал не меняется на сегменте (ΔV ≈ 0), но касательная ненулевая, потому что соседний сегмент имеет наклон. Кривая «вздувается» выше или ниже плоского значения.

На графике ниже — три кривых Эрмита на одном сегменте. Серые линии показывают границы значений на концах:

Concordant Discordant Flat

Overshoot — это не баг, а математическое свойство кубических кривых с Catmull-Rom касательными. Разные методы интерполяции в Photoshop решают проблему overshoot по-разному — и в этом кроется одно из главных отличий между ними.

Кубические кривые Безье

Помимо Hermite, кубические кривые можно задать и через контрольные точки — это кривые Безье. Вместо значений и касательных на концах — четыре контрольные точки (P0, P1, P2, P3). Кривая проходит через первую и последнюю точки, а две средние «притягивают» её к себе, формируя изгиб.

Любую кривую Безье можно пересчитать в эквивалентный сплайн Эрмита и наоборот — это одна и та же математика, записанная по-разному. Подробно мы разберём Безье в статье про метод Smooth — единственный метод Photoshop, который использует именно эту форму.

Midpoint как центр тяжести

Между каждыми двумя соседними стопами в Photoshop есть маленький ромбик — это midpoint. По умолчанию он стоит посередине (50%), но его можно перетащить ближе к одному из стопов.

Midpoint смещает центр тяжести градиента, заставляя его быстрее (при значениях < 50%) или медленнее (при значениях > 50%) проходить через центральный цвет сегмента. Начальный и конечный цвет сегмента не изменяются, смещается только его центр — место, где проходит основной переход.

  • Midpoint 50% — всё плавно, середина цвета в середине сегмента.
  • Midpoint 5% — уже на 5% пути цвет почти на полпути от V0 к V1. Переход происходит стремительно в самом начале, а оставшиеся 95% плавно «дотягивают» до V1.
  • Midpoint 95% — наоборот. Почти весь сегмент плавно тянется к переходу, а потом резко переходит к V1.

Технически это реализовано через пересчёт позиции пикселя внутри сегмента:

φ(u,m)={u2mif um12+um2(1m)if u>m\varphi(u, m) = \begin{cases} \dfrac{u}{2m} & \text{if } u \leq m \\[8pt] \dfrac{1}{2} + \dfrac{u - m}{2(1 - m)} & \text{if } u > m \end{cases}
  • u ∈ [0, 1] — нормализованная позиция пикселя в сегменте
  • m ∈ [0, 1] — позиция midpoint
  • φ ∈ [0, 1] — пересчитанная позиция для интерполяции.

Переназначение позиции в сегменте через midpoint всегда применяется до интерполяции — и кубической, и линейной. Эрмитова интерполяция и Безье работают с уже пересчитанной позицией φ(u), не с исходной u. Поэтому midpoint одинаково влияет на все методы интерполяции.

Приблизительный пайплайн градиентов

Подведём итог. Каждый метод интерполяции в Photoshop проходит через одни и те же этапы:

  1. Преобразование стопов в цветовое пространство интерполяции
  2. Вычисление касательных
  3. Кубическая интерполяция с учётом midpoint
  4. Пост-процессинг (обработка overshoot)
  5. Преобразование обратно в sRGB
ШагClassicLinearPerceptualSmooth
Цветовое пространство????????????
Кривая????????????
Касательная????????????
Пост-процессинг????????????

В следующих статьях мы заполним каждую ячейку этой таблицы.

Что дальше

В следующей статье мы разберём самый простой из известных методов интерполяции — Classic, и узнаем, какие ухищрения были придуманы инженерами Adobe для борьбы с вечно убегающими за края кривыми.