LerpworksAcademy
На этой странице

Урок по шагам

Цвет — это числа

17 шагов

  • Зачем это
  • Доли вместо 0–255
  • Картинка — таблица чисел
  • Три операции
  • Кривая: что операция делает с каждым значением
  • Где ломается
  • Связи: та же идея в других областях
  • Главное

Графика › Основы

Цвет — это числа

Этот урок есть в курсе «Азбука графики».

Всё, что делает материал, — это арифметика над числами. Здесь разберём, как записывают цвет, и три операции, из которых собрана почти любая формула в шейдере: умножение, сложение и смешивание по маске. Их хватает, чтобы читать большую часть графов в Unreal и Substance Designer.

Зачем это

Открой любой материал в Unreal или граф в Substance Designer: там десятки нод, и кажется, что за каждой стоит отдельное знание. На деле большинство из них — одни и те же три действия над числами. В материалах одного популярного курса по стилизованному окружению нода Multiply стоит в 25 материалах из 27, а «магические» функции движка внутри оказываются парой умножений и смешиваний.

Если понимать, что каждое действие делает с картинкой, граф перестаёт быть заклинанием. Результат можно предсказать до того, как соединишь провода, а чужой материал — прочитать как текст.

Доли вместо 0–255

В палитре графического редактора цвет часто задают тремя числами от 0 до 255, например (255, 128, 64). В шейдерах тот же цвет записывают как (1.0, 0.5, 0.25): каждое число — доля от максимума, от 0 до 1. Чтобы перевести одно в другое, раздели на 255.

  • (1.0, 0.5, 0.25)
  • (0.5, 0.5, 1.0)
  • (0.5, 0.3, 0.15)

Доли удобно умножать. Половина от половины — четверть, и результат остаётся между 0 и 1. А 128 × 128 = 16 384: для цвета такое число ничего не значит. Ещё доли не зависят от того, как цвет хранится: 8 бит на канал, 16 бит или дробные числа — формула в шейдере одна и та же.

Как цвет лежит в памяти и что такое HDR

Обычная картинка хранит канал в 8 битах: это 28 = 256 ступеней, от 0 до 255. Отсюда числа от 0 до 255 в палитрах редакторов. Это глубина цвета. Пиксель RGBA занимает 4 байта, поэтому несжатая текстура 1024 × 1024 весит 4 МБ, а 4096 × 4096 — уже 64 МБ. Поэтому текстуры в играх почти всегда сжаты особыми форматами, которые видеокарта читает без распаковки. Какой формат для какой карты — в шпаргалке по настройкам текстур.

Внутри шейдера числа дробные и не ограничены единицей. Солнце в движке может давать освещённость в тысячи раз больше, чем лампочка, а свечение может быть «ярче белого». Такие значения называют HDR. Экран всё равно показывает только 0..1, поэтому в самом конце кадра картинку сжимают в этот диапазон. Это делает тонмаппинг, о нём будет урок про пост-обработку.

Отсюда правило: промежуточные значения в материале могут выходить за 1, и это нормально. Важно, что в итоге попадёт на экран и что попадёт в маску.

Картинка — таблица чисел

Текстура 1024 × 1024 — это миллион пикселей, в каждом по три числа: R, G и B. Удобно думать о ней как о трёх наложенных таблицах, по одной на каждый канал. Каждая такая таблица — чёрно-белая картинка: где канал сильный, там светло.

Чёрно-белая маска устроена так же, только таблица одна: в каждом пикселе одно число от 0 до 1, «сколько». Сколько мха, сколько влаги, сколько грязи. Бывает и четвёртый канал, A (альфа), — обычно в нём прозрачность или ещё одна маска. Раз каналы независимы, в одну текстуру можно сложить три-четыре разные маски, по одной в канал. Это называется упаковкой каналов.

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

Три операции

Умножение — это фильтр

Представь цветное стекло. Через красное стекло проходит почти весь красный свет и почти ничего из зелёного и синего. Умножение цвета на цвет работает так же: каждый канал проходит на свою долю.

A × B = (A.r × B.r,  A.g × B.g,  A.b × B.b)
  • Умножить на белый (1, 1, 1) — ничего не изменится: единица — нейтральное значение умножения.
  • Умножить на чёрный (0, 0, 0) — всё погаснет.
  • Умножением на цвет с числами от 0 до 1 можно только затемнить: доля от числа не больше самого числа.

Умножением тонируют текстуры: серый камень × тёплый цвет = тёплый камень, и все светлые и тёмные места текстуры сохраняются. И так же, умножением, свет ложится на поверхность — об этом следующий урок.

Сложение — это свет

Два фонарика на одной стене дают пятно ярче каждого: их свет складывается. Сложение цветов работает так же. Его нейтральное значение — ноль: прибавить (0, 0, 0) значит ничего не изменить.

Сумма быстро перевалит за 1. В шейдере это не ошибка, но на экране всё, что больше единицы, будет одинаково белым. Сложением делают свечение (emissive), блики, подсветку краёв — всё, что добавляет свет, а не красит.

Смешивание по маске (lerp)

Представь наплыв в кино: одна сцена плавно перетекает в другую, и в какой-то момент на экране три четверти первой и четверть второй. Lerp (linear interpolation, линейная интерполяция) делает то же с числами:

lerp(A, B, t) = A × (1 − t) + B × t

t — это доля второго входа. При t = 0 получается чистое A, при t = 1 — чистое B, при 0.5 — поровну. Маска даёт своё t каждому пикселю, поэтому lerp — главный инструмент, чтобы положить один материал поверх другого: мох на камень, тропинку на траву, снег на крышу.

Заметь, что lerp собран из двух предыдущих операций: двух умножений и сложения. Это взвешенное среднее. Веса (1 − t) и t в сумме всегда дают 1, поэтому при t от 0 до 1 результат не темнеет и не пересвечивается — он всегда где-то между A и B.

Краски так не смешиваются. Синяя и жёлтая краска дают зелёный: каждая краска забирает часть света, и в смеси остаётся то, что не забрала ни одна. А lerp синего (0, 0, 1) и жёлтого (1, 1, 0) при t = 0.5 даёт серый (0.5, 0.5, 0.5).

Как осветлить

Сложить или умножить на число больше 1: в шейдерах числа не ограничены единицей. Но результат разный. Сложение поднимает все значения на одну величину — и тени тоже, поэтому картинка выглядит как сквозь туман и теряет глубину. Умножение на 1.5 осветляет светлое сильнее тёмного: чёрное остаётся чёрным, и контраст сохраняется. Как это выглядит на графике — в следующем разделе.

Попробуй

Выбери операцию и наведи мышь на любую картинку. Под ними появится расчёт для этого пикселя.

B — цвет фильтра, как цветное стекло

A — текстура камня
маска, нужна для смешивания
результат

Кривая: что операция делает с каждым значением

На операцию над одним каналом удобно смотреть как на график. По горизонтали — значение до, по вертикали — после. Пунктирная диагональ означает «ничего не изменилось». Такой график называют кривой тона, и по нему сразу виден характер операции.

  • Умножение поворачивает линию вокруг нуля. Чёрное остаётся на месте, остальное растягивается или сжимается. При множителе больше 1 верх упирается в единицу, и светлые места сливаются в белое.
  • Сложение сдвигает линию вверх или вниз. Сдвиг вверх поднимает чёрное — это и есть «туман».
  • Степень гнёт линию, а концы оставляет на месте: 0 в любой степени — 0, а 1 — всё ещё 1. При степени больше 1 середина темнеет, меньше 1 — светлеет. Поэтому степень — любимый способ поднять контраст маски, ничего не обрезав. Но гнёт она в одну сторону: при степени больше 1 светлые значения расходятся, а тёмные прижимаются к нулю и сливаются. Если контраст нужен и в светлом, и в тёмном, берут S-образную кривую, например smoothstep.

Попробуй

Выбери операцию и двигай ползунок. Слева кривая, справа та же текстура до и после. Красным на кривой отмечено, где значения упёрлись в 0 или 1.

до
после

То, что упёрлось в 0 или 1, — пересвет, или обрезка. Пиксели, которые отличались, стали одинаковыми, и различие уже не вернёт никакая следующая операция. Поэтому порядок важен: сначала поднять контраст, потом прибавить свет — не то же самое, что наоборот.

Levels, Curves и CheapContrast — это те же кривые

В Photoshop и Krita есть «Уровни» (Levels) и «Кривые» (Curves). Уровни двигают чёрную и белую точки — это вычитание и умножение: сначала вычесть чёрную точку, потом умножить так, чтобы белая точка стала единицей. Кривые позволяют нарисовать график руками.

Levels(x) = (x − чёрная точка) / (белая точка − чёрная точка)

В Substance Designer то же делают ноды Levels и Histogram Scan, а в Unreal — CheapContrast. Внутри неё всего одна операция:

CheapContrast(x, c) = saturate(lerp(−c, 1 + c, x))

Это lerp, у которого концы вынесены за пределы 0..1: линия становится круче, а всё, что вышло за края, обрезает saturate. Получается кривая с изломами: дешёвый контраст, который съедает края. Отсюда выбор между двумя нодами: CheapContrast режет самые тёмные и самые светлые значения, Power их сохраняет, но гнёт только в одну сторону.

Где ломается

  • Белые пятна без деталей. Сложение или умножение на число больше 1 вытолкнуло значения за единицу, и их обрезало. Помогает меньшая прибавка или степень вместо умножения.
  • Грязный, слишком тёмный цвет. Перемножили несколько тёмных карт: 0.5 × 0.5 × 0.5 = 0.125. Каждое умножение на число меньше 1 затемняет, и затемнения копятся. Частый случай — умножить затенение в щелях (AO) на цвет, где тени уже нарисованы художником.
  • Белёсая, плоская картинка. Осветлили сложением, и чёрное перестало быть чёрным.
  • Мох вырос везде, кроме камня. Перепутаны входы lerp или маска инвертирована. Поменяй A и B местами или прогони маску через 1 − x.
  • Странные полосы на границе смешивания. Маска вышла за пределы 0..1, например после сложения, и lerp начал заходить дальше B: при t = 1.3 получается «больше, чем B». Поставь Saturate перед lerp.

Главное

  • В шейдере цвет — три числа от 0 до 1, доли от максимума. Внутри расчётов они могут выходить за 1.
  • Картинка — таблица чисел по каналам, маска — таблица с одним числом в пикселе.
  • Умножение — фильтр: нейтраль 1, числами до 1 только затемняет. Свет на поверхности — умножение.
  • Сложение — свет: нейтраль 0, быстро пересвечивает. Свет плюс свет — сложение.
  • Lerp(A, B, t) — смешивание, где t — доля второго входа. Маска даёт t каждому пикселю.
  • Любую операцию над каналом можно нарисовать кривой. Всё, что упёрлось в 0 или 1, потеряно.

Проверь себя

7 вопросов. В профиль идёт лучший результат, проходить можно сколько угодно.

1. Серая текстура камня (0.5, 0.5, 0.5) умножается на тинт (1.0, 0.6, 0.3). Что получится?

2. Художник хочет сделать камень светлее. Что сработает?

3. В одном пикселе маска мха равна 0.25. Что там будет после lerp(камень, мох, маска)?

4. Красное яблоко (1, 0, 0) освещено чисто синим светом (0, 0, 1). Каким оно будет?

5. После сложения на камне появились белые пятна без деталей. Что случилось?

6. Какая операция поднимет контраст маски, не сдвинув чисто чёрное и чисто белое?

7. Мох по маске вырос везде, кроме тех мест, где маска белая. Как проще всего исправить?

Что-то осталось непонятным? Выдели этот кусок текста и нажми «Спросить про это». Вопрос уйдёт автору, и это место перепишут понятнее.