Имитируем иридисценцию: шейдер CD-ROM +17


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


Туториал будет состоять из следующих частей:

  • Часть 1. Природа света
  • Часть 2. Усовершенствуем радугу — 1
  • Часть 3. Усовершенствуем радугу — 2
  • Часть 4. Разбираемся с дифракционной решёткой
  • Часть 5. Математика дифракционной решётки
  • Часть 6. Шейдер CD-ROM: дифракционная решётка — 1
  • Часть 7. Шейдер CD-ROM: дифракционная решётка — 2

Введение


Иридисценция — это оптическое явление, при котором объекты изменяют цвета при изменении угла освещения или угла обзора. Именно благодаря этому эффекту пузыри обладают такой широкой палитрой цветов.


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


Так происходит потому, что иридисценция возникает вследствие взаимодействия света и микроскопических структур, которые находятся на поверхностях всех этих объектов. И дорожки CD-ROM, и чешуйки наружного скелета насекомого (см. изображения ниже) имеют тот же порядок величины длины волн света, с которым они взаимодействуют. На самом деле, иридисценция стала первым явлением, которое позволило раскрыть истинную волновую природу света. Мы не сможем объяснить и воспроизвести иридисценцию, не поняв сначала, что же такое свет, как он работает и как воспринимается глазом человека.




Природа света


Как и многие субатомные частицы, свет одновременно проявляет свойства частиц и волн. Довольно часто свет моделируют как первые или вторые. Для большинства применений свет можно рассматривать как созданный из триллионов отдельных частиц, называемых фотонами. Например, большинство шейдеров считает, что фотоны ведут себя как крошечные бильярдные шары и отражаются от объектов по тем углом, под которым столкнулись с ним (см. схему ниже).


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

Всем нам известны океанские волны. Каждая точка на поверхности океана имеет высоту. Чем выше она от среднего значения, тем выше волна. Если потревожить поверхность воды, волны начинают распространяться по океану, пока их энергия не рассеется.

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

Величина переносимой фотоном энергии определяет цвет света. Фотоны с низкой энергией воспринимаются как красный цвет; фотоны с высокой энергией воспринимаются как фиолетовый. Волны обладают свойством, аналогичным энергии частицы: длиной волны. Для интуитивного понимания можно сказать, что это расстояние между двумя пиками волны.


Свет всегда движется с одной скоростью (приблизительно 299 792 458 метров в секунду), то есть электромагнитные волны распространяются с одинаковой скоростью. Хотя их скорость постоянна, длина волн может быть разной. Фотоны с высокой энергией — это волны с короткой длиной. Именно длина волны света в конце концов определяет его цвет.


Как вы видите на схеме выше, глаз человека может воспринимать фотоны с длиной волны в интервале примерно от 700 нанометров до 400 нанометров. Нанометр — это миллиардная часть метра.

Насколько мал нанометр?
Когда пытаешься разобраться с наименьшими масштабами, в которых работает Природа, сложно представить обсуждаемые размеры. Средний человек имеет рост примерно 1,6 метра. Толщина человеческого волоса примерно 50 микрометров (50 мкм). Микрометр — это миллионная часть метра (1 мкм = 0,000001 метра = $10^{-6}$ метра). Нанометр — это одна тысячная микрометра (1 нм = 0,000000001 метра = $10^{-9}$ метра). То есть длина волны видимого света равна примерно одной сотой толщины человеческого волоса.

Что дальше?

После этого краткого введения в оставшейся части туториала мы сосредоточимся на понимании иридисценции и её реализации в Unity.

  • Усовершенствуем радугу. Как сказано выше, разные длины волн света воспринимаются человеческим глазом как разные цвета. В следующих двух частях мы разберёмся с тем, как связать эти длины волн с цветами RGB. Этот шаг необходим для воссоздания иридисцентных отражений с высокой степенью точности. В этих частях я также представлю новый подход, который будет и физически точным, и эффективным с точки зрения вычислений.
  • Дифракционная решётка. В частях 4 и 5 этого туториала мы рассмотрим дифракционную решётку. Это техническое название одного из эффектов, заставляющих материалы демонстрировать иридисцентные отражения. Несмотря на свою «техничность», выведенное уравнение, управляющее этим оптическим явлением, будет очень простым. Если вас не интересует математика дифракционной решётки, то можете пропустить часть 5.
  • Шейдер CD-ROM. Ядро этого туториала — реализация шейдера CD-ROM. В нём для реализации дифракционной решётки в Unity будут использоваться знания, собранные в предыдущих частях. Он является расширением стандартного поверхностного шейдера (Standard Surface shader) Unity 5; что делает этот эффект и физически правильнмы, и фотореалистичным. Приложив небольшие усилия, вы сможете изменить его так, чтобы он соответствовал другим типам иридисцентных отражений, основанных на дифракционной решётке.

Подведём итог


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

Часть 2. Усовершенствуем радугу — 1.


Наше путешествие в мир фотореализма требует от нас понимания не только того, как работает свет, но и как мы воспринимаем цвета. Сколько цветов есть в радуге? Почему розовый в них не входит? Вот только некоторые из вопросов, которые мы рассмотрим в этой части.


Введение


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

В следующей части, «Усовершенствуем радугу — 2», мы введём новый подход, который очень хорошо оптимизирован для шейдеров и при этом создаёт наилучшие на данный момент результаты (см. ниже).

Сравнение WebGL-версий всех рассмотренных в этом туториале техник можно посмотреть в Shadertoy.

Восприятие цветов


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


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

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

Спектральный цвет


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

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

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

fixed3 spectralColor (float wavelength);

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

Почему оптимального решения не существует?
На этот вопрос лучше всех ответил Эрл Ф. Глинн:
«Не существует уникального соответствия между длиной волны и значениями RGB. Цвет — это удивительное сочетание физики и человеческого восприятия».

Поиск окончательно верного соответствия длин волн и цветов неизбежно обречён на провал. Природа света объективна, а наше восприятие — нет. Колбочки, воспринимающие определённые длины волн видимого спектра, имеют значительные вариации у разных людей. Даже если предположить, что все колбочки работают одинаково и постоянно для всех людей, их распределение и количество в сетчатке в основном случайны. Никакие две сетчатки не одинаковы, даже у одного человека.

Наконец, восприятие цвета зависит от того, как эти входные данные воспринимает мозг. Благодаря этому возникают различные оптические иллюзии и нейроадаптации, которые делают восприятие цветов уникальным и истинно личностным опытом.

Спектральная карта


На рисунке ниже показано, как глаз человека воспринимает волны длиной от 400 нанометров (синие) до 700 нанометров (красные).


Легко увидеть, что распределение цветов в видимом спектре очень нелинейно. Если мы нанесём на график для каждой длины волны соответствующие компоненты R, G и B воспринимаемого цвета, то в результате получим нечто подобное:


Не существует простой функции, которая может полностью описать эту кривую. Простейшим и наименее затратным подходом реализации будет использование этой текстуры в шейдере как средства привязки длин волн к цветам.

Первое, что нужно сделать — обеспечить шейдеру доступ к новой текстуре. Мы можем сделать это, добавив в блок Properties нового шейдера свойство текстуры.

// Свойства
Properties
{
    ...
    _SpectralTex("Spectral Map (RGB)",2D) = "white" {}
    ...
}
// Код шейдера
SubShader
{
    ...
    CGPROGRAM
    ...
    sampler2D _SpectralTex;
    ...
    ENDCG
    ...
}

Наша функция spectralColor просто преобразует длины волн в интервале [400,700] в UV-координаты в интервале [0,1]:

fixed3 spectral_tex (float wavelength)
{
    // длина волны: [400, 700]
    // u:          [0,   1]
    fixed u = (wavelength -400.0) / 300.0;
    return tex2D(_SpectralTex, fixed2(u, 0.5));
}

В нашем конкретном случае нам не нужно принудительно ограничивать длины волн интервалом [400, 700]. Если спектральная текстура импортируется с Repeat: Clamp, все значения за пределами этого интервала будут автоматически иметь чёрный цвет.

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

Цветовая схема JET


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

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


Цветовая схема JET является сочетанием трёх разных кривых: синей, зелёной и красной. Это чётко видно при разбиении цвета:


Мы с лёгкостью можем самостоятельно реализовать цветовую схему JET, написав уравениня линий, составляющих представленную выше схему.

// Цветовая схема MATLAB Jet
fixed3 spectral_jet(float w)
{
 // w: [400, 700]
 // x: [0,   1]
 fixed x = saturate((w - 400.0)/300.0);
 fixed3 c;
 
 if (x < 0.25)
 c = fixed3(0.0, 4.0 * x, 1.0);
 else if (x < 0.5)
 c = fixed3(0.0, 1.0, 1.0 + 4.0 * (0.25 - x));
 else if (x < 0.75)
 c = fixed3(4.0 * (x - 0.5), 1.0, 0.0);
 else
 c = fixed3(1.0, 1.0 + 4.0 * (0.75 - x), 0.0);
 
 // Ограничиваем компоненты цвета интервалом [0,1]
 return saturate(c);
}

Значения R, G и B получившегося цвета ограничены интервалом [0,1] с помощью функции Cg saturate. Если для камеры выбран режим HDR (High Dynamic Range Rendering), это необходимо, чтобы избежать наличия цветов с компонентами больше единицы.

Стоит заметить, что если вы хотите строго придерживаться цветовой схемы JET, то значения за пределами видимого диапазона не будут чёрными.

Цветовая схема Брутона


Ещё одним подходом к преобразованию длин волн в видимые цвета является схема, предложенная Дэном Брутоном в статье "Approximate RGB values for Visible Wavelengths". Аналогично тому, что происходит в цветовой схеме JET, Брутон начинает с аппроксимированного распределения воспринимаемых цветов.


Однако его подход лучше аппроксимирует активность длинных колбочек, что приводит к более сильному оттенку фиолетового в нижней части видимого спектра:


Такой подход преобразуется в следующий код:

// Дэн Брутон
fixed3 spectral_bruton (float w)
{
 fixed3 c;
 
 if (w >= 380 && w < 440)
 c = fixed3
 (
 -(w - 440.) / (440. - 380.),
 0.0,
 1.0
 );
 else if (w >= 440 && w < 490)
 c = fixed3
 (
 0.0,
 (w - 440.) / (490. - 440.),
 1.0
 );
 else if (w >= 490 && w < 510)
 c = fixed3
 ( 0.0,
 1.0,
 -(w - 510.) / (510. - 490.)
 );
 else if (w >= 510 && w < 580)
 c = fixed3
 (
 (w - 510.) / (580. - 510.),
 1.0,
 0.0
 );
 else if (w >= 580 && w < 645)
 c = fixed3
 (
 1.0,
 -(w - 645.) / (645. - 580.),
 0.0
 );
 else if (w >= 645 && w <= 780)
 c = fixed3
 ( 1.0,
 0.0,
 0.0
 );
 else
 c = fixed3
 ( 0.0,
 0.0,
 0.0
 );
 
 return saturate(c);
}

Цветовая схема Bump


Цветовые схемы JET и Брутона используют прерывные функции. Поэтому в них создаются довольно резкие цветовые вариации. Более того, за пределами видимого диапазона они не становятся чёрным цветом. В книге «GPU Gems» эта проблема решается заменой резких линий предыдущих цветовых схем на гораздо более плавные изгибы (bumps). Каждый изгиб является обычной параболой вида $y=1-x^2$. А конкретнее

$bump\left(x \right ) = \left\{\begin{matrix} 0 & \left|x\right|>1 \\ 1-x^2 & \mathit{otherwise} \end{matrix}\right.$


Автор схемы Рандима Фернандо использует для всех компонентов цвета параболы, расположенные следующим образом:



Мы можем написать следующий код:

// GPU Gems
inline fixed3 bump3 (fixed3 x)
{
 float3 y = 1 - x * x;
 y = max(y, 0);
 return y;
}
 
fixed3 spectral_gems (float w)
{
   // w: [400, 700]
 // x: [0,   1]
 fixed x = saturate((w - 400.0)/300.0);
 
 return bump3
 ( fixed3
 (
 4 * (x - 0.75), // Red
 4 * (x - 0.5), // Green
 4 * (x - 0.25) // Blue
 )
 );
}

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

Цветовая схема Spektre


Одной из самых точных цветовых схем является схема, созданная пользователем Stack Overflow Spektre. Он объясняет свою методологию в посте RGB values of visible spectrum, где он сэмплирует синий, зелёный и красный компоненты вещественных данных из солнечного спектра. После чего он заполняет отдельные интервалы простыми функциями. Результат показан на следующей схеме:


Что даёт нам:


А вот как выглядит код:

// Spektre
fixed3 spectral_spektre (float l)
{
 float r=0.0,g=0.0,b=0.0;
 if ((l>=400.0)&&(l<410.0)) { float t=(l-400.0)/(410.0-400.0); r=    +(0.33*t)-(0.20*t*t); }
 else if ((l>=410.0)&&(l<475.0)) { float t=(l-410.0)/(475.0-410.0); r=0.14         -(0.13*t*t); }
 else if ((l>=545.0)&&(l<595.0)) { float t=(l-545.0)/(595.0-545.0); r=    +(1.98*t)-(     t*t); }
 else if ((l>=595.0)&&(l<650.0)) { float t=(l-595.0)/(650.0-595.0); r=0.98+(0.06*t)-(0.40*t*t); }
 else if ((l>=650.0)&&(l<700.0)) { float t=(l-650.0)/(700.0-650.0); r=0.65-(0.84*t)+(0.20*t*t); }
 if ((l>=415.0)&&(l<475.0)) { float t=(l-415.0)/(475.0-415.0); g=             +(0.80*t*t); }
 else if ((l>=475.0)&&(l<590.0)) { float t=(l-475.0)/(590.0-475.0); g=0.8 +(0.76*t)-(0.80*t*t); }
 else if ((l>=585.0)&&(l<639.0)) { float t=(l-585.0)/(639.0-585.0); g=0.82-(0.80*t)           ; }
 if ((l>=400.0)&&(l<475.0)) { float t=(l-400.0)/(475.0-400.0); b=    +(2.20*t)-(1.50*t*t); }
 else if ((l>=475.0)&&(l<560.0)) { float t=(l-475.0)/(560.0-475.0); b=0.7 -(     t)+(0.30*t*t); }
 
 return fixed3(r,g,b);
}

Заключение


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

Название Градиент
JET
Bruton
GPU Gems
Spektre
Zucconi
Zucconi6
Видимый спектр

Часть 3. Усовершенствуем радугу — 2.


Введение


В предыдущей части мы проанализировали четыре различных способа преобразования длин волн видимого диапазона электромагнитного спектра (400-700 нанометров) в соответствующие им цвета.

В трёх из этих решений (JET, Bruton и Spektre) активно используются конструкции if. Для C# это стандартная практика, однако в шейдере ветвление является плохим подходом. Единственным подходом, в котором не используется ветвление, является рассмотренный в книге GPU Gems. Однако он не обеспечивает оптимальную аппроксимацию цветов видимого спектра.

Название Градиент
GPU Gems
Видимый спектр

В этой части я расскажу про оптимизированную версию цветовой схемы, описанной в книге GPU Gems.

Цветовая схема «Bump»


Исходная цветовая схема, изложенная в книге GPU Gems, для воссоздания распределения компонентов R, G и B цветов радуги использует три параболы (называемые автором bumps).


Каждый bump описывается следующим уравнением:

$bump\left(x \right ) = \left\{\begin{matrix} 0 & \left|x\right|>1 \\ 1-x^2 & \mathit{otherwise} \end{matrix}\right.$


Каждая длина волны $w$ в диапазоне [400, 700] сопоставляется с нормализованным значением $x$ в интервале [0,1]. Затем компоненты R, G и B видимого спектра задаются следующим образом:

$R\left(x \right) = bump\left( 4 \cdot x - 0.75\right)$



$G\left(x \right) = bump\left( 4 \cdot x - 0.5\right)$



$B\left(x \right) = bump\left( 4 \cdot x - 0.25\right)$


Все численные значения подобраны автором экспериментально. Однако вы видите, насколько плохо они соответствуют истинному распределению цветов.


Оптимизация качества


В первом решении, к которому пришёл я, использовались точно такие же уравнения, что и в цветовой схеме GPU Gems. Однако я оптимизировал все численные значения, так что конечный диапазон цветов соответствует, насколько это возможно, настоящим цветам из видимого спектра.

Результат сводится к следующему решению:


И приводит к гораздо более реалистичному результату:

Название Градиент
GPU Gems
Zucconi
Видимый спектр

Как и исходное решение, новый подход не содержит ветвления. Поэтому он идеально подходит для шейдеров. Код имеет следующий вид:

// На основе кода из GPU Gems
// Оптимизовано Аланом Цуккони
inline fixed3 bump3y (fixed3 x, fixed3 yoffset)
{
 float3 y = 1 - x * x;
 y = saturate(y-yoffset);
 return y;
}
fixed3 spectral_zucconi (float w)
{
    // w: [400, 700]
 // x: [0,   1]
 fixed x = saturate((w - 400.0)/ 300.0);
 
 const float3 cs = float3(3.54541723, 2.86670055, 2.29421995);
 const float3 xs = float3(0.69548916, 0.49416934, 0.28269708);
 const float3 ys = float3(0.02320775, 0.15936245, 0.53520021);
 
 return bump3y ( cs * (x - xs), ys);
}

Расскажите подробнее о своём решении!
Чтобы найти алгоритм оптимизации, я воспользовался библиотекой Python scikit.

Вот параметры, необходимые для воссоздания моих результатов:

  • Algorithm: L-BFGS-B
  • Tolerance: $1\cdot 10^{-8}$
  • Iterations: $1\cdot 10^{8}$
  • Weighted MSE:
    • $W_R=0.3$
    • $W_G=0.59$
    • $W_B=0.11$
  • Fitting
  • Исходное решение:
    • $C_R =4$
    • $C_G = 4$
    • $C_B = 4$
    • $X_R = 0.75$
    • $X_G = 0.5$
    • $X_B = 0.25$
    • $Y_R = 0$
    • $Y_G = 0$
    • $Y_B = 0$
  • Конечное решение:
    • $C_R = 3.54541723$
    • $C_G = 2.86670055$
    • $C_B = 2.29421995$
    • $X_R = 0.69548916$
    • $X_G = 0.49416934$
    • $X_B = 0.28269708$
    • $Y_R = 0.02320775$
    • $Y_G = 0.15936245$
    • $Y_B = 0.53520021$

Усовершенствуем радугу


Если внимательнее присмотреться к распределению цветов в видимом спектре, то мы заметим, что параболы на самом деле не могут повторить кривые цветов R, G и B. Немного лучше будет использовать шесть парабол вместо трёх. Привязав к каждому основному компоненту по два bump, мы получим гораздо более правильную аппроксимацию. Разница очень заметна в фиолетовой части спектра.


Разница хорошо заметна в фиолетовой и оранжевой частях спектра:

Название Градиент
Zucconi
Zucconi6
Видимый спектр

Вот как выглядит код:

// На основе кода из GPU Gems
// Оптимизировано Аланом Цуккони
fixed3 spectral_zucconi6 (float w)
{
 // w: [400, 700]
 // x: [0,   1]
 fixed x = saturate((w - 400.0)/ 300.0);
 
 const float3 c1 = float3(3.54585104, 2.93225262, 2.41593945);
 const float3 x1 = float3(0.69549072, 0.49228336, 0.27699880);
 const float3 y1 = float3(0.02312639, 0.15225084, 0.52607955);
 
 const float3 c2 = float3(3.90307140, 3.21182957, 3.96587128);
 const float3 x2 = float3(0.11748627, 0.86755042, 0.66077860);
 const float3 y2 = float3(0.84897130, 0.88445281, 0.73949448);
 
 return
 bump3y(c1 * (x - x1), y1) +
 bump3y(c2 * (x - x2), y2) ;
}

Нет никаких сомнений, что spectral_zucconi6 обеспечивает более качественную аппроксимацию цветов без использования ветвления. Если для вас важна скорость, то можно использовать упрощённую версию алгоритма — spectral_zucconi.

Подводим итог


В этой части мы рассмотрели новый подход к генерированию в шейдерах похожих на радугу паттернов.

Название Градиент
JET
Bruton
GPU Gems
Spektre
Zucconi
Zucconi6
Видимый спектр

Часть 4. Разбираемся с дифракционной решёткой


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

Отражения: свет и зеркала


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


Объекты, отрендеренные в такой технике, походят на зеркала. Более того, если свет падает с направления L, то наблюдатель может увидеть его только тогда, когда смотрит с направления R. Такой тип отражения также называется specular, что означает «зеркалоподобный».

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


В большинстве современных движков (наподобие Unity и Unreal) эти два поведения моделируются с помощью разных наборов уравнений. В своём предыдущем туториале Physically Based Rendering and Lighting Models я объяснял модели отражаемости Ламберта и Блинна-Фонга, которые используются соответственно для рассеянных и зеркальных отражений.

Несмотря на то, что они выглядят по-разному, рассеянное отражение можно объяснить через зеркальное. Никакая поверхность не является совершенно плоской. Можно смоделировать грубую поверхность как созданную из крошечных зеркал, каждое из которых полностью характеризуется зеркальной отражаемостью. Наличие таких микрограней приводит к рассеянию лучей во всех направлениях.


Разнонаправленность таких микрограней часто моделируется физически точными шейдерами с помощью таких свойств, как Smoothness (гладкость) или Roughness (шероховатость). Подробнее об этом можно прочитать на странице справки Unity, объясняющей свойство Smoothness стандартного шейдера движка.


А что насчёт отражаемости?
В этом разделе мы сказали, что рассеянное (диффузное) отражение можно полностью объяснить, рассмотрев зеркальное отражение на поверхности, состоящей из разнонаправленных микрограней. Однако это не совсем правда. Если поверхность демонстрирует только зеркальное отражение, то это означает, что при полной полировке она будет выглядеть чёрной. Хорошим контрпримером можно считать белый мрамор: никакая полировка не способна сделать его чёрным. Даже если нам удастся достичь идеально гладкой поверхности, белый мрамор всё равно будет проявлять белый диффузный компонент отражения.

И в самом деле, за этот эффект ответственно кое-что ещё. Диффузный компонент поверхности также возникает из вторичного источника: преломления. Свет может проникать сквозь поверхность объекта, отражаться внутри него и выходить под другим углом (см. рисунок выше). Это значит, что какой-то процент всего падающего света будет повторно излучаться поверхностью материала в любой произвольной точке и под любым углом. Такое поведение часто называют подповерхностным рассеянием (subsurface scattering) и вычисления для его симуляции часто бывают очень затратны.

Подробнее об этих эффектах (и их симуляции) можно прочитать в статье Basic Theory of Physically Based Rendering компании Marmoset.

Свет как волна


Очень удобно моделировать лучи света так, как будто они состоят из частиц. Однако это не позволит нам воссоздать поведение, которое демонстрирует множество материалов, в том числе и иридисценцию. Некоторые явления можно полностью понять только в том случае, если принять тот факт, что свет в определённых условиях ведёт себя как волна.

Большинство шейдеров работает со светом как с частицами. Результатом этого огромного упрощения становится то, что подвергается аддитивной композиции. Если два луча достигают наблюдателя, то их яркость просто складывается. Чем больше лучей испускает поверхность, тем она ярче.

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

Анимация

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

Взаимодействие волн может казаться странным принципом. Однако все мы испытывали его в повседневной жизни. Популяризатор науки Дерек Мюллер хорошо объясняет это в своём видео The Original Double Slit Experiment, где он демонстрирует усиливающую и гасящую интерференцию волн воды.


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

Дифракция


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

Анимация

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

Анимация

Теперь у нас все необходимые основы, чтобы обсудить причины возникновения иридисценции.

Дифракционная решётка

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

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

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

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


Подводим итог


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

Часть 5. Математика дифракционной решётки.


Введение


В предыдущей части мы объяснили, почему в некоторых материалах возникает иридисценция. Теперь у нас есть всё необходимое, чтобы начать моделировать это явление математически. Давайте начнём с того, что представим материал, в котором есть неоднородности, повторяющиеся через известные расстояния $d$. В целях вывода уравнений обозначим угол между падающими лучами света и нормалью поверхности как $\theta_L$. Давайте также представим, что наблюдатель расположен таким образом, что он получает все отражённые лучи с углом $\theta_L$. Каждая неоднородность рассеивает свет во всех направлениях, поэтому всегда будут присутсвовать лучи света, падающие на наблюдателя, вне зависимости от $\theta_L$.


Поскольку неоднородности повторяются регулярно с шагом $d$ нанометров, то сам паттерн рассеяния повторяется каждые $d$ нанометров. Значит, что существует хотя бы один луч света, приходящий к наблюдателю от каждой щели.

Вывод уравнений


Два луча света, изображённые на схеме выше, прежде чем достигнуть наблюдателя, проходят разное расстояние. Если они начинают двигаться, совпадая по фазе, то достигнув наблюдателя, могут и не совпадать Чтобы понять, как эти два луча взаимодействуют (усиливая или гася друг друга), нам нужно вычислить, насколько не совпадают они по фазе, когда достигают наблюдателя.

Эти два луча точно будут совпадать по фазе, пока первый не упадёт на поверхность. Второй луч проходит дополнительное расстояние $x$ (выделено зелёным), после чего тоже падает на поверхность. Воспользовавшись простой тригонометрией, можно показать, что длина зелёного отрезка $x$ равна $d \cdot \sin{\theta_L}$.


С помощью похожей конструкции мы можем вычислить дополнительное расстояние $y$, которое проходит первый луч, пока второй не столкнётся с поверхностью. В этом случае мы видим, что $y=d \cdot \sin{\theta_V}$.


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

Однако два луча могут совпадать по фазе не только в этом случае. Если разность длин равна целому числу, кратному длине волны $w$, то они по-прежнему будут совпадать по фазе. С математической точки зрения, два луча совпадают по фазе, если они удовлетворяют следующему условию:

$d \sin{\theta_L} - d \sin{ \theta_V } = n \cdot w$


$\sin{\theta_L} -  \sin{ \theta_V } = \frac{n \cdot w}{d}$


Визуализация


Давайте уделим минуту, чтобы разобраться в значении этого уравнения. Если свет падает под углом $\theta_L$, то что увидит наблюдатель, смотрящий на материал под углом $\theta_V$? Все длины волн $w$, являющиеся целыми кратными $d \left( \sin{\theta_L} - \sin{ \theta_V } \right)$, будут взаимодействовать с усилением, и сильно проявятся в конечном отражении. Поэтому именно эти цвета увидит зритель.

Этот эффект визуализируется следующей схемой, взятой из очень интересного обсуждения A complex approach: Iridescence in cycles:


Белый луч следует по пути, пройденному фотонами для зеркального отражения. Наблюдатель, смотрящий на материал под разными углами, увидит циклический радужный паттерн. Каждый цвет соответствует своей длине волны, а порядок определяет соответствующее целое $n$. Как вы видите, уравнение дифракционной решётки удовлетворяется даже при отрицательных значениях $n$, потому что величина $\sin{\theta_L} - \sin{ \theta_V }$ может быть отрицательной. С вычислительной точки зрения имеет смысл упростить пространство поиска, ограничившись только положительными значениями $n$. Новое уравнение, которое мы будем использовать, имеет вид:

$\left | \sin{\theta_L} -  \sin{ \theta_V } \right |= \frac{n \cdot w}{d}$


Часть 6. Шейдер CD-ROM: дифракционная решётка — 1


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

Введение


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

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


Мы хотим, чтобы шейдер добавлял иридисцентные отражения поверх обычных эффектов, которые обычно создаёт стандартный материал. Поэтому мы расширим функцию освещения стандартного поверхностного шейдера (Standard Surface shader). Если вы не знакомы с этой процедурой, то стоит изучить мой туториал Physically Based Rendering and Lighting Models.

Создание поверхностного шейдера


Первым шагом будет создание нового шейдера. Так как мы хотим расширить возможности шейдера, уже поддерживающего физически точное освещение, то начнём с Standard Surface Shader.


Создаваемому шейдеру CD-ROM потребуется новое свойство: расстояние $d$, используемое в уравнении дифракционной решётки. Давайте добавим его в блок Properties, который должен выглядеть следующим образом:

Properties
{
 _Color ("Color", Color) = (1,1,1,1)
 _MainTex ("Albedo (RGB)", 2D) = "white" {}
 _Glossiness ("Smoothness", Range(0,1)) = 0.5
 _Metallic ("Metallic", Range(0,1)) = 0.0
 
 _Distance ("Grating distance", Range(0,10000)) = 1600 // nm
}

Так мы создадим новый ползунок в Material Inspector. Однако свойство _Distance по-прежнему нужно связать с переменной в разделе CGPROGRAM:

float _Distance;

Теперь мы готовы к работе.

Изменение функции освещения


Первое, что нам нужно сделать — заменить функцию освещения шейдера CD-ROM на собственную. Мы можем сделать это, изменив директиву #pragma здесь:

#pragma surface surf Standard fullforwardshadows

на:

#pragma surface surf Diffraction fullforwardshadows

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

#include "UnityPBSLighting.cginc"
inline fixed4 LightingDiffraction(SurfaceOutputStandard s, fixed3 viewDir, UnityGI gi)
{
 // Исходный цвет
 fixed4 pbr = LightingStandard(s, viewDir, gi);
 // <здесь будет код дифракционной решётки>
 return pbr;
}

Как видно из представленного выше фрагмента кода, новая функция LightingDiffraction просто вызывает LightingStandard и возвращает её значение. Если мы скомпилируем шейдер сейчас, то не увидим никакой разницы в способе рендеринга материалов.

Однако прежде чем двигаться дальше, нам нужно создать дополнительную функцию для обработки глобального освещения (Global Illumination). Поскольку нам не нужно менять это поведение, наша новая функция глобального освещения будет просто прокси-функцией стандартной PBR-функции Unity:

void LightingDiffraction_GI(SurfaceOutputStandard s, UnityGIInput data, inout UnityGI gi)
{
 LightingStandard_GI(s, data, gi); 
}

Стоит также заметить, что поскольку мы используем непосредственно LightingStandard и LightingDiffraction_GI, то нужно включить в наш шейдер UnityPBSLighting.cginc.

Реализация дифракционной решётки


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

$\left | \sin{\theta_L} -  \sin{ \theta_V } \right |= \frac{n \cdot w}{d}$


где $n$ — целое число больше $0$.

Для каждого пикселя значения $\theta_L$ (определяемого направлением света), $\theta_V$ (определяемого направлением обзора) и $d$ (расстояние между зазорами) известны. Неизвестными переменными являются $w$ и $n$. Проще всего будет перебрать в цикле значения $n$, чтобы увидеть, какие длины волн удовлетворяют уравнению решётки.

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

Давайте рассмотрим следующую возможную реализацию:

inline fixed4 LightingDiffraction(SurfaceOutputStandard s, fixed3 viewDir, UnityGI gi)
{
 // Исходный цвет
 fixed4 pbr = LightingStandard(s, viewDir, gi);
 
 // Вычисляет цвет отражения
 fixed3 color = 0;
 for (int n = 1; n <= 8; n++)
 {
 float wavelength = abs(sin_thetaL - sin_thetaV) * d / n;
 color += spectral_zucconi6(wavelength);
 }
 color = saturate(color);
 
 // Прибавляет цвет отражения к цвету материала
 pbr.rgb += color;
 return pbr;
}

В этом фрагменте кода мы используем значения $n$ до 8. Для более качественных результатов можно взять бОльшие значения, однако и этого уже достаточно, чтобы учесть значительную часть иридисцентного отражения.

Нам осталось последнее — вычислить sin_thetaL и sin_thetaV. Для этого понадобится ввести ещё одну концепцию: касательного вектора. В следующей части мы узнаем, как его вычислить.

Часть 7. Шейдер CD-ROM: дифракционная решётка — 2


Введение


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

Ориентация щелей


Выведенное нами уравнение решётки имеет большое ограничение: оно предполагает, что все щели расположены в одном направлении. Это часто справедливо для наружных скелетов насекомых, но дорожки на поверхности CD-ROM расположены по кругу. Если мы реализуем решение буквально, то получим довольно неубедительное отражение (правая часть изображения).


Чтобы решить эту проблему, нам нужно учесть локальную ориентацию щелей на CD-ROM. Использование вектора нормали не поможет, потому что все щели имеют одинаковое направление нормалей, перпендикулярное поверхности диска. Локальную ориентацию щели можно определить с помощью вектора касательной (левая часть верхнего изображения).


На представленной выше схеме направление нормали $N$ показано синим, а направление касательной $T$ — красным. Углы источника света и наблюдателя, образуемые с направлением нормали $N$, называются $\theta_L$ и $\theta_V$. Аналогичные углы относительно $T$ — это $\Theta_L$ и $\Theta_V$. Как сказано выше, при использовании в вычислениях $\theta_L$ и $\theta_V$ мы получим «плоское» отражение, потому что все щели имеют одинаковую $N$. Нам нужно найти способ, как использовать $\Theta_L$ и $\Theta_V$, потому что они правильно соответствуют локальным направлениям.

Пока нам известно, что:

$N \cdot L = \cos{\theta_L}\; \; \; N \cdot V = \cos{\theta_V}$


$T \cdot L = \cos{\Theta_L}\; \; \;  T \cdot V = \cos{\Theta_V}$


Поскольку $T$ и $N$ перпендикулярны, то они обладают следующим свойством:

$T \cdot L = \cos{\Theta_L} = \sin{\theta_L}$


$T \cdot V = \cos{\Theta_V} = \sin{\theta_V}$


Это очень удобно ещё и потому, что Cg обеспечивает нативную реализацию скалярного произведения. Нам осталось только вычислить $T$.

Откуда взялся косинус?
Все рассматриваемые здесь векторы имеют общее свойство: их длина равна 1. По этой причине их также называют единичными векторами. Простейшая операция, применимая к единичным векторам — это скалярное произведение.

Грубо говоря, скалярное произведение двух единичных векторов является мерой из сонаправленности. На самом деле оказывается, что скалярное произведение двух единичных векторов является косинусом угла между ними. Поэтому в уравнениях появляется косинус.

Вычисление вектора касательной


Чтобы доделать наш шейдер, мы должны вычислить вектор касательной $T$. Обычно он указывается непосредственно в вершинах мешей. Однако с учётом того, насколько проста поверхность CD-ROM, мы можем вычислить его самостоятельно. Стоит учесть, что показанный в этом туториале подход довольно прост и будет работать только в том случае, если поверхность меша CD-ROM имеет правильную UV-развёртку.


На схеме выше показано, как вычисляются направления касательных. При этом предполагается, что поверхность диска UV-развёрнута как четырёхугольник с координатами в интервале от (0,0) до (1,1). Зная это, мы переназначаем координаты каждой точки поверхности CD-ROM в интервале от (-1,-1) до (+1,+1). Взяв за основу этот принцип, мы получаем, что новая координата точки также соответствует направлению наружу из центра (зелёная стрелка). Мы можем повернуть это направление на 90 градусов, чтобы найти вектор, являющийся касательным к концентрическим дорожкам CD-ROM (показан красным).


Эти операции необходимо выполнять в функции surf шейдера, потому что UV-координаты недоступны в функции освещения LightingDiffraction.

// IN.uv_MainTex: [ 0, +1]
// uv:            [-1, +1]
fixed2 uv = IN.uv_MainTex * 2 -1;
fixed2 uv_orthogonal = normalize(uv);
fixed3 uv_tangent = fixed3(-uv_orthogonal.y, 0, uv_orthogonal.x);

Нам осталось только преобразовать вычисленную касательную из пространства объекта в мировое пространство. При преобразовании учитываются положение, поворот и масштаб объекта.

worldTangent = normalize( mul(unity_ObjectToWorld, float4(uv_tangent, 0)) );

Как передать направление касательной в функцию освещения?
Иридисцентное отражение вычисляется в функции освещения LightingDiffraction. Однако ей требуется вектор касательной worldTangent, который вычисляется в поверхностной функции surf. Сигнатуру функции освещения нельзя изменить, то есть её нельзя заставить получать больше параметров, чем она уже имеет.

Если вы незнакомы с шейдерами, то я подскажу: существует очень простой способ передачи дополнительных параметров. Нужно просто добавить их как переменные в тело шейдера. В нашем случае мы можем использовать общую переменную worldTangent, которая инициализирована функцией surf и используется функцией LightingDiffraction.

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

В контексте шейдеров можно менять пространства координат точки, просто умножая их на особую матрицу. Матрица, позволяющая преобразовывать координаты, выраженные в пространстве объектов, в мировое пространство, называется unity_ObjectToWorld. Если вы пользуетесь старой версией Unity, то эта константа будет называться _Object2World.

Соединяем всё вместе


Теперь у нас есть всё необходимое для вычисления влияния цвета на иридисцентное отражение:

inline fixed4 LightingDiffraction(SurfaceOutputStandard s, fixed3 viewDir, UnityGI gi)
{
 // Исходный цвет
 fixed4 pbr = LightingStandard(s, viewDir, gi);
 
 // --- Эффект дифракционной решётки ---
 float3 L = gi.light.dir;
 float3 V = viewDir;
 float3 T = worldTangent;
 
 float d = _Distance;
 float cos_ThetaL = dot(L, T);
 float cos_ThetaV = dot(V, T);
 float u = abs(cos_ThetaL - cos_ThetaV);
 
 if (u == 0)
 return pbr;
 
 // Цвет отражения
 fixed3 color = 0;
 for (int n = 1; n <= 8; n++)
 {
 float wavelength = u * d / n;
 color += spectral_zucconi6(wavelength);
 }
 color = saturate(color);
 
 // Прибавляет отражение к цвету материала
 pbr.rgb += color;
 return pbr;
}

Как это связано с радугой?
Переменная wavelength, объявленная в цикле for, содержит длины волн света, влияющие на иридисцентное отражение текущего пикселя.

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

Части «Усовершенствуем радугу» мы показали, что любую длину волны можно преобразовать в соответствующие цвета. Функция, используемая для такого проецирования, называется spectral_zucconi6. Она является оптимизированной версией решения, представленного в учебнике по шейдерам GPU Gems.




К сожалению, не доступен сервер mySQL