The Book of Shaders by Patricio Gonzalez Vivo & Jen Lowe

日本語 - 中文版 - 한국어 - Español - Français - Italiano - Deutsch - Русский - English


Пол Кли - диаграмма цвета (1931)

Цвета

Ранее мы не очень подробно раскрывали тему работы с векторами в GLSL. Прежде чем двинуться дальше, не помешает изучить векторы поглубже, и работа с цветом как нельзя лучше подходит для этого.

Если вы знакомы с объектно-ориентированной парадигмой программирования, вы скорее всего заметили, что мы обращаемся к данным в векторах на манер структур (struct) в языке С.

vec3 red = vec3(1.0,0.0,0.0);
red.x = 1.0;
red.y = 0.0;
red.z = 0.0;

Работа с цветом через x, y и z может сбить с толку и ввести в заблуждение, не так ли? Поэтому в GLSL можно получить доступ к тем же самым данным через другие имена. Значения .x, .y и .z так же именуются .r, .g и .b, и .s, .t и .p. Последний набор имён обычно используется для пространственных координат текстур, с которыми мы познакомимся в следующих главах. Кроме того, для доступа к элементам вектора можно использовать численные индексы [0], [1] и [2].

Следующие строки демонстрируют все способы доступа к одним и тем же данным:

vec4 vector;
vector[0] = vector.r = vector.x = vector.s;
vector[1] = vector.g = vector.y = vector.t;
vector[2] = vector.b = vector.z = vector.p;
vector[3] = vector.a = vector.w = vector.q;

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

Другое замечательное свойство векторных типов GLSL - это возможность доступа к координатам в произвольном порядке, которая упрощает преобразование и смешивание значений.

vec3 yellow, magenta, green;

// Задаём жёлтый цвет
yellow.rg = vec2(1.0);  // Записываем 1.0 в красный и зелёный каналы
yellow[2] = 0.0;        // Записываем 0.0 в синий

// Задаём малиновый цвет
magenta = yellow.rbg;   // Меняем местами зелёный и синий

// Задаём зелёный цвет
green.rgb = yellow.bgb; // Записываем значение синего канала жёлтого цвета (0) в красный и синий

Инструментарий

Возможно, вам ранее не приходилось подбирать цвет с помощью чисел. Это выглядит очень контринтуитивно. К счастью, есть множество умных программ, которые упрощают это занятие. Найдите наиболее подходящую для себя и научите её представлять цвета в форматах vec3 или vec4. Например, вот такие шаблоны я использую в Spectrum:

vec3({{rn}},{{gn}},{{bn}})
vec4({{rn}},{{gn}},{{bn}},1.0)

Смешивание цветов

Теперь, когда вы знаете как задаются цвета, мы можем собрать все новые знания воедино. В GLSL есть очень полезная функция mix(), которая смешивает значения в указанной пропорции. Угадайте, как задаются пропорции? Конечно же, числом от 0.0 до 1.0! И это просто отлично, учитывая те долгие часы, что вы практиковали движения карате с забором. Самое время применить их на практике!

В строке 18 в следующем куске кода мы используем абсолютное значение синуса от времени для смешивания цветов A и B.

Покажите, что вы можете:

Играемся с градиентами

Функция mix() способна на большее. Вместо одного числа с плавающей точкой мы можем передать переменную того же типа, что и первые два аргумента (vec3 в нашем случае). Таким образом мы можем управлять пропорциями каждого канала r, g и b по отдельности.

Рассмотрим следующий пример. Как и в примерах предыдущей главы, мы преобразуем значение перехода к нормализованной координате x и визуализируем его с помощью линии. Сейчас все каналы изменяются по одному и тому же закону.

Теперь раскомментируйте строку 25 и посмотрите что произойдёт. Затем попробуйте раскомментировать строки 26 и 27. Помните, что линии показывают пропорции каждого канала при смешивании цветов A и B.

Вы скорее всего узнали три функции, которые мы используем в строках 25-27. Поиграйте с ними! Исследуйте и демонстрируйте ваши находки, используя умения из предыдущей главы для создания интересных градиентов. Попробуйте выполнить следующие упражнения:

Вильям Тёрнер - Последний рейс корабля Отважный (1838)

HSB

Нельзя рассказать о цветах, не упомянув цветовое пространство. Как вы возможно знаете, есть множество способов задания цвета кроме красного, зелёного и синего каналов.

HSB расшифровывается как оттенок (Hue), насыщенность (Saturation) и яркость (Brightness или Value), и является более интуитивным способом представления цвета. Прочитайте код функций rgb2hsv() и hsv2rgb() в примере ниже.

Отображая координату x в оттенок, а координату y - в яркость, мы получаем красивый спектр видимых цветов. Такое пространственное распределение цветов очень удобно. Выбор цветов в пространстве HSB более интуитивен, чем RGB.

HSB в полярных координатах

Пространство HSB изначально было разработано для представления в полярных координатах (на основе угла и радиуса), вместо декартовых (x и y). Чтобы перевести нашу функцию HSB в полярные координаты, нужно вычислить угол и расстояние, используя центр области рисования и декартовы координаты пикселя. Для этого мы используем функцию вычисления расстояния length() и двухаргументный арктангенс (atan(y,x), GLSL-версия известной функции atan2(y,x)).

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

На заметку: Возможно, вы спросите, какие ещё есть геометрически функции кроме длины (length)? Их довольно много: dot() (скалярное произведение), cross (векторное произведение), normalize() (привести вектор к единичной длине), faceforward() (вернуть вектор, указывающий в то же полупространство, что и данный), reflect() (отражение) и refract() (преломление). Так же в GLSL есть векторные функции сравнения: lessThan() (меньше), lessThanEqual() (меньше либо равно), greaterThan() (больше), greaterThanEqual() (больше либо равно), equal() (равно) и notEqual() (не равно).

Получив угол и длину, мы должны нормировать их значения в интервал от 0.0 до 1.0. В строке 27 atan(y,x) возвращает угол в радианах от минус пи до пи, поэтому сначала мы разделим его на удвоенное пи (TWO_PI, определено вначале кода), и к полученному числу от -0.5 до 0.5 прибавим 0.5, чтобы перейти в нужный интервал от 0.0 до 1.0. Максимальный радиус будет равен 0.5 (мы вычисляли расстояние от центра окна), поэтому его нужно удвоить.

Как видите, в этом разделе мы в основном играли с преобразованием значений в нужный нам промежуток от 0.0 до 1.0.

Попробуйте выполнить следующие упражнения:

Вильям Хоум Лизарс - Красный, синий и жёлтый спектры в составе солнечного спектра (1834)

Заметки о функциях и аргументах

Перед тем как нырнуть в следующую главу, давайте остановимся и немного отмотаем назад. Вернитесь и взгляните на функции в предыдущих примерах. Вы заметите слово in перед типами аргументов. Это - квалификатор, и в данном случае он означает, что переменная предназначена только для чтения. В последующих примерах мы увидим так же аргументы с квалификаторами out и inout. Последний эквивалентен передаче переменной по ссылке, при которой мы можем изменить переданное значение так, что изменения становятся видны за пределами функции.

int newFunction(in vec4 aVec4,      // только для чтения
                out vec3 aVec3,     // только на запись
                inout int aInt);    // чтение и запись

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