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

Статья разбита на следующие части:
- Часть 1. Объёмное атмосферное рассеяние
- Часть 2. Теория атмосферного рассеяния
- Часть 3. Математика рэлеевского рассеяния
- Часть 4. Путешествие сквозь атмосферу
- Часть 5. Атмосферный шейдер
- Часть 6. Пересечение атмосферы
- Часть 7. Шейдер атмосферного рассеяния
Часть 1. Объёмное атмосферное рассеяние
Введение
Воссоздать атмосферные явления так сложно потому, что небо не является непрозрачным объектом. В традиционных техниках рендеринга предполагается, что объекты — это просто пустые оболочки. Все графические вычисления производятся на поверхностях материалов и не зависят от того, что находится внутри. Это сильное упрощение позволяет очень эффективно выполнять рендеринг непрозрачных объектов. Однако свойства некоторых материалов определяются тем, что свет может проходить сквозь них. Конечный внешний вид просвечивающих объектов становится результатом взаимодействия света с их внутренней структурой. В большинстве случаев такое взаимодействие можно очень эффективно имитировать, как это можно увидеть в туториале "Быстрый шейдер для Subsurface Scattering в Unity". К сожалению, в нашем случае, если мы хотим создать убедительное небо, это не так. Вместо рендеринга только «внешней оболочки» планеты нам придётся симулировать то, что происходит с лучами света, проходящими сквозь атмосферу. Выполнение вычислений внутри объекта называется объёмным рендерингом; эту тему мы подробно рассматривали в серии статей Volumetric Rendering. В этой серии статей я рассказал о двух техниках (raymarching и знаковых функциях расстояния), которые невозможно эффективно использовать для симуляции атмосферного рассеяния. В этой статье мы познакомимся с более подходящей для рендеринга просвечивающих твёрдых объектов техникой, часто называемой единичным объёмным рассеянием.
Единичное рассеяние
В комнате без света мы ничего не увидим. Объекты становятся видимыми, только когда от них отражаются лучи света и попадают в наш глаз. В большинстве игровых движков (таких как Unity и Unreal) предполагается, что свет движется в «вакууме». Это значит, что на свет могут влиять только объекты. На самом же деле, свет всегда движется в среде. В нашем случае такой средой является вдыхаемый нами воздух. Поэтому на внешний вид объекта влияет расстояние, проходимое светом в воздухе. На поверхности Земли плотность воздуха относительно мала, её влияние настолько незначительно, что её стоит учитывать только когда свет проходит большие расстояния. Далёкие горы сливаются с небом, однако на близкие объекты атмосферное рассеяние почти не влияет.
Первым шагом в воссоздании оптического эффекта атмосферного рассеяния будет анализ того, как свет проходит через такие среды, как воздух. Как сказано выше, мы можем увидеть предмет только тогда, когда свет падает в наш глаз. В контексте 3D-графики нашим глазом является камера, используемая для рендеринга сцены. Молекулы, из которых состоит воздух вокруг нас, могут отражать проходящие через них световые лучи. Поэтому они способны менять то, как мы воспринимаем объекты. Если сильно упростить, то существует два способа, которыми молекулы могут повлиять на наше зрение.
Рассеяние наружу
Наиболее очевидный способ взаимодействия молекул со светом заключается в том, что они отражают свет, меняя его направление. Если направленный в камеру луч света отражается, то мы наблюдаем процесс рассеяния наружу.

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

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

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

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

Чтобы корректно учитывать величину рассеяния наружу, происходящего в каждой точке

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

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

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

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

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

Соотношение
Мы можем использовать его для обозначения процента света, который не был рассеян (то есть был пропущен) при путешествии от
Следовательно, количество света, получаемого
Функция рассеяния
Точка

Значение
Теперь у нас есть все необходимые инструменты для записи общего уравнения, показывающего количество света, переданного от
Благодаря предыдущему определению мы можем развернуть
Уравнение говорит само за себя:
- Свет проходит от солнца к
, не рассеиваясь в вакууме космоса;
- Свет входит в атмосферу и проходит от
к
. В этом процессе из-за рассеяния наружу только часть
достигает точки назначения;
- Часть света, который добрался от солнца до
, отражается назад в камеру. Доля света, подверженного рассеянию внутрь равна
;
- Оставшийся свет проходит от
до
, и снова проспускается только часть
.
Численное интегрирование
Если вы внимательно прочитали предыдущие параграфы, то могли заметить, что яркость записывалась по-разному. Символ
Общее количество света, получаемого

Такой процесс аппроксимации называется численным интегрированием и приводит к следующему выражению:
Чем больше точек мы учитываем, тем точнее будет конечный результат. В реальности же в нашем атмосферном шейдере мы просто циклически обойдём несколько точек
Почему мы умножаем на ds?
Здесь мы используем аппроксимацию непрерывного явления. Чем больше точек мы рассмотрим, тем ближе будем к реальному результату. Умножая каждую точку на
, мы придаём её влиянию вес согласно её длине. Чем больше точек у нас есть, тем менее важна каждая из них.
Можно посмотреть на это и иначе: умножая на
, мы «усредняем» влияние всех точек.
Можно посмотреть на это и иначе: умножая на
Направленное освещение
Если солнце относительно близко, то его лучше всего моделировать как точечный источник света. В этом случае количество полученного

Мы можем использовать это допущение, чтобы упростить наши уравнения.
Давайте заменим
Есть ещё одна оптимизация, которую мы можем выполнить, она включает в себя функцию рассеяния
Коэффициент поглощения
При описании возможных результатов взаимодействия между светом и молекулами воздуха, мы допустили всего два варианта — или прохождение насквозь, или отражение. Но существует и третья возможность. Некоторые химические соединения поглощают свет. В атмосфере Земли есть множество веществ, обладающих таким свойством. Озон, например, находится в верхних слоях атмосферы и активно взаимодействует с ультрафиолетовым излучением. Однако его присутствие не оказывает практически никакого воздействия на цвет неба, потому что он поглощает свет за пределами видимого спектра. Здесь, на Земле, влияние светопоглощающих веществ часто игнорируется. Но в случае других планет от него отказаться нельзя. Например, обычная окраска Нептуна и Урана вызвана присутствием большого количества метана в их атмосфере. Метан поглощает красный свет, что даёт синий оттенок. В оставшейся части туториала мы будем игнорировать коэффициент поглощения, однако добавим способ «подкрашивания» атмосферы.
Почему Солнце стало красным во время урагана Офелия 2017 года?
Если вы живёте в Великобритании, то могли заметить, что во время урагана Офелия солнце становилось красным. Так происходило потому, что Офелия принесла песок из Сахары. Эти крошечные частицы, висевшие в воздухе, усиливали эффект рассеяния. Как мы увидим в следующей части, синий свет рассеивается сильнее, чем красный.
Если посмотреть на цвета видимого спектра (ниже), то легко увидеть, что если рассеивается достаточное количество синего света, то небо и в самом деле может стать жёлтым или красным.

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

Есть искушение сказать, что цвет окраски неба связан с оттенком жёлтого сахарского песка. Однако такой же эффект можно увидеть во время больших пожаров из-за частиц дыма, которые обычно близки к чёрному цвету.
Часть 3. Математика рэлеевского рассеяния.
В этой части мы познакомимся с математикой рэлеевского рассеяния — оптического явления, из-за которого небо выглядит голубым. Выведенные в этой части уравнения мы перенесём в код шейдера следующей части.
Введение
В предыдущей части мы вывели уравнение, представляющее собой хорошую основу для аппроксимации атмосферного рассеяния в шейдере. Однако мы упустили то, что одно уравнение не даст нам убедительных результатов. Если нам нужен красиво выглядящий атмосферный шейдер, то нужно немного углубиться в математику.
Взаимодействие между светом и материей невероятно сложно, и нам не удастся полностью описать его лёгким способом. На самом деле, моделирование атмосферного рассеяния очень трудоёмко. Часть проблемы вызвана тем, что атмосфера не является однородной средой. Её плотность и состав значительно меняются как функция от высоты, что делает практически невозможным создание «идеальной» модели.
Именно поэтому в научной литературе приводится несколько моделей рассеяния, каждая из которых предназначена для описания подмножества оптических явлений, возникающих при определённых условиях. Большинство оптических эффектов, демонстрируемых планетами, можно воссоздать, учитывая всего две разные модели: рэлеевского рассеяния и рассеяния света сферической частицей. Эти два математических инструмента позволяют прогнозировать рассеяния света на объектах различного размера. Первая моделирует отражение света молекулами кислорода и азота, составляющими бОльшую часть состава воздуха. Последняя моделирует отражение света в более крупных структурах, присутствующих в нижних слоях атмосферы, таких как пыльца, пыль и загрязнители.
Рэлеевское рассеяние является причиной голубого неба и красных закатов. Рассеяние света сферической частицей придаёт облакам их белый цвет. Если вы хотите знать, как это происходит, то нам придётся глубже погрузиться в математику рассеяния.
Рэлеевское рассеяние
Какова судьба фотона, ударяющегося о частицу? Чтобы ответить на этот вопрос, нам нужно перефразировать его более формально. Представим, что луч света проходит сквозь пустое пространство и внезапно сталкивается с частицей. Результат такого столкновения сильно зависит от размера частицы и цвета светового луча. Если частица достаточно мала (размером с атомы или молекулы), то поведение света лучше прогнозируется с помощью рэлеевского рассеяния.
Что же происходит? Часть света продолжает свой путь, «не ощутив» никакого влияния. Однако небольшой процент этого исходного света взаимодействует с частицей и рассеивается во всех направлениях. Однако не все направления получают одинаковое количество света. Фотоны с большей вероятностью проходят прямо через частицу или отражаются назад. То есть вариант отражения фотона на 90 градусов менее велик. Такое поведение можно увидеть на схеме ниже. Голубой линией показаны наиболее вероятные направления рассеянного света.

Это оптическое явление математически описывается уравнением рэлеевского рассеяния
Где:
: длина волны (wavelength) приходящего света;
: угол рассеияния (scattering angle);
: высота (altitude) точки;
: коэффициент преломления воздуха;
: количество молекул на кубический метр стандартной атмосферы;
: коэффициент плотности.Это число на уровне моря равно
, и экспоненциально уменьшается с увеличением
. Об этой функции можно многое сказать, и мы рассмотрим её в следующих частях.
Но это не уравнение рэлеевского рассеяния!
Если вы встречались с рэлеевским рассеянием не в области компьютерной графики, то есть вероятность, что вы видели другое уравнение. Например, представленное в статье «Рэлеевское рассеяни» на Википедии, сильное отличается.
Использованное в этом туториале уравнение взято из научной статьи Display of The Earth Taking into Account Atmospheric Scattering, Нишиты et al.
Использованное в этом туториале уравнение взято из научной статьи Display of The Earth Taking into Account Atmospheric Scattering, Нишиты et al.
Откуда взялась эта функция?
Одна из задач этого блога — объяснение вывода всех получаемых величин. К сожалению, это не относится к рэлеевскому рассеянию.
Если вам всё-таки интересно разобраться, почему частицы света так странно отражаются от молекул воздуха, то интуитивное понимание происходящего может дать следующее объяснение.
Причиной рэлеевского рассеяния на самом деле является не «отталкивание» частиц. Свет — это электромагнитная волна, и она может взаимодействовать с неравновесием зарядов, присутствующим в определённых молекулах. Эти заряды насыщаются поглощением получаемого электромагнитного излучения, которое позже испускается снова. Показанная в угловой функции двудольная форма показывает, как молекулы воздуха становятся электрическими диполями, излучающими подобно микроскопическим антеннам.
Если вам всё-таки интересно разобраться, почему частицы света так странно отражаются от молекул воздуха, то интуитивное понимание происходящего может дать следующее объяснение.
Причиной рэлеевского рассеяния на самом деле является не «отталкивание» частиц. Свет — это электромагнитная волна, и она может взаимодействовать с неравновесием зарядов, присутствующим в определённых молекулах. Эти заряды насыщаются поглощением получаемого электромагнитного излучения, которое позже испускается снова. Показанная в угловой функции двудольная форма показывает, как молекулы воздуха становятся электрическими диполями, излучающими подобно микроскопическим антеннам.
Первое, что можно заметить в рэлеевском рассеянии — это то, что в некоторых направлениях распространяется больше света, чем в других. Второй важный аспект заключается в том, что количество рассеянного света сильно зависит от длины волны

На рисунке ниже показана визуализация коэффициентов рассеяния для непрерывного диапазона длин волн/цветов видимого спектра (код доступен в ShaderToy).

Центр изображения выглядит чёрным, потому что длины волн в этом диапазоне находятся за пределами видимого спектра.
Коэффициент рэлеевского рассеяния
Уравнение рэлеевского рассеяния показывает, какое количество света рассеивается в определённом направлении. Однако оно не говорит нам, сколько энергии всего рассеялось. Для вычисления этого нам нужно учесть рассеивание энергии во всех направлениях. Вывод уравнения нелёгок; если вы не освоили сложный матанализ, то вот результат:
Где
Если вы читали предыдущую часть туториала, то можете догадаться, что
К сожалению, вычисление
Покажите мне вычисления!
Уравнение рэлеевского рассеяния показывает нам долю энергии, рассеиваемой в определённом направлении. Чтобы вычислить полные потери, нам нужно учитывать все возможные направления. Для суммирования на непрерывном интервале требуется интегрирование.
Можно попробовать проинтегрировать
по
на интервале
, но это будет ошибкой.
Несмотря на то, что мы визуализировали рэлеевское рассеяние в двух измерениях, на самом деле это трёхмерное явление. Угол рассеяния
может принимать любое направление в 3D-пространстве. Вычисления с учётом полного распределения функции, которое зависит от
в трёхмерном пространстве (как
) называется интегрированием по телесному углу:
Внутренний интеграл перемещает
в плоскости XY, а внешний — поворачивает результат вокруг оси X, чтобы учесть третье измерение. Прибавляемый
используется для сферических углов.
Процесс интегрирования интересует только то, что зависит от
. Несколько членов
постоянны, поэтому их можно перенести из под знака интеграла:
Это значительно упрощает внутренний интеграл, который теперь принимает следующий вид:
Теперь мы можем выполнить внешнее интегрирование:
Что приводит нас к окончательному виду:
Поскольку это интеграл, учитывающий влияние рассеивания во всех направлениях, логично, что выражение больше не зависит от
.
Можно попробовать проинтегрировать
Несмотря на то, что мы визуализировали рэлеевское рассеяние в двух измерениях, на самом деле это трёхмерное явление. Угол рассеяния
Внутренний интеграл перемещает
Процесс интегрирования интересует только то, что зависит от
Это значительно упрощает внутренний интеграл, который теперь принимает следующий вид:
Теперь мы можем выполнить внешнее интегрирование:
Что приводит нас к окончательному виду:
Поскольку это интеграл, учитывающий влияние рассеивания во всех направлениях, логично, что выражение больше не зависит от
Это новое уравнение даёт нам ещё один способ понимания того, как рассеиваются разные цвета. На графике ниже показано количество рассеяния, которому подвержен свет, как функция от длины его волны.

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

Значение
Разделив истинную плотность на
Если мы хотим аппроксимировать коэффициент плотности через экспоненциальную кривую, то мы можем сделать это следующим образом:
где

Значение, использованное для
Экспоненциальное уменьшение
В предыдущей части туториала мы вывели уравнение, показывающее нам, как учитывать рассеяние наружу, которому подвергается луч света после взаимодействия с отдельной частицей. Величина, использованная для моделирования этого явления, называлась коэффициентом рассеяния
В случае рэлеевского рассеяния мы также вывели замкнутую форму для вычисления количества света, подвержденного атмосферному рассеянию при одиночном взаимодействии:
При вычислении на уровне моря, то есть при
Где
В чём смысл этих чисел? Они представляют собой долю света, которая теряется при одиночном взаимодействии с частицей. Если предположить, что луч света изначально имеет яркость
Это справедливо для единственного столкновения, но нас интересует, какая энергия рассеивается при прохождении определённого расстояния. Это значит, что оставшийся свет подвергается этому процессу в каждой точке.
Когда свет проходит сквозь однородную среду с коэффициентом рассеяния
Для тех из вас, кто изучал матанализ, это может показаться знакомым. Когда на непрерывном отрезке повторяется мультипликативный процесс
И снова мы столкнулись с экспоненциальной функцией. Она никак не связана с экспоненциальной функцией, описывающей коэффициент плотности
Откуда взялось exp?
Если вы незнакомы с матанализом, то можете не понимать, почему такой простой процесс, как
преобразуется в
.
Мы можем рассматривать исходное выражение как первую аппроксимацию к решению. Если мы хотим получить более близкую аппроксимацию, то нам нужно учитывать два события рассеяния, разделив пополам коэффициент рассеяния для каждого из них:
Что приводит нас к следующему:
Это новое выражение для
показывает количество сохранившейся после двух столкновений энергии. А как насчёт трёх столкновений? Или четырёх? Или пяти? В общем виде это можно записать следующим выражением:
где
— это математическая конструкция, позволяющая повторять процесс бесконечное количество раз. Это необходимо нам, потому что
технически не является чилом, так как в данном контексте не имеет смысла записывать что-то вроде
.
Это выражение является самим определением экспоненциальной функции:
которое описывает количество энергии, сохранившееся после мультипликативного процесса на непрерывном интервале.
Мы можем рассматривать исходное выражение как первую аппроксимацию к решению. Если мы хотим получить более близкую аппроксимацию, то нам нужно учитывать два события рассеяния, разделив пополам коэффициент рассеяния для каждого из них:
Что приводит нас к следующему:
Это новое выражение для
где
Это выражение является самим определением экспоненциальной функции:
которое описывает количество энергии, сохранившееся после мультипликативного процесса на непрерывном интервале.
Равномерное пропускание
Во второй части туториала мы ввели концепцию пропускания
Давайте посмотрим на схему ниже и подумаем, как можно вычислить коэффициент пропускания для отрезка

Количество рассеянного света зависит от пройденного расстояния. Чем длиннее путь, тем сильнее будет затухание. Согласно закону экспоненциального уменьшения, количество света в точке
где
Атмосферное пропускание
Мы основали наше уравнение на предположении, что вероятность отражения (коэффициент рассеяния
Коэффициент рассеяния сильно зависит от плотности атмосферы. Чем больше молекул воздуха на кубический метр, тем выше вероятность столкновения. Плотность атмосферы планеты неоднородна, и изменяется в зависимости от высоты. Это также значит, что мы не можем вычислить рассеяния наружу по
Чтобы понять, как это работает, давайте начнём с аппроксимации. Отрезок

Сначала мы вычисляем количество света от
Затем мы используем тот же подход для вычисления количества света, достигающего
Если мы подставим

Если

В случае двух отрезков одинаковой длины с разными коэффициентами рассеяния рассеяния наружу вычисляется суммированием коэффициентов рассеяния отдельных отрезков и умноженных на длины отрезков.
Мы можем повторить этот процесс с произвольным количеством отрезков, всё сильнее приближаясь к истинному значению. Это приведёт нас к следующему уравнению:
где
Способ разделения прямой на множество отрезков, который мы только что использовали, называется численным интегрированием.
Если мы предположим, что изначальное количество принятого света равно
Мы можем ещё больше развернуть это выражение. заменив общее
Многие множители
Выраженная суммированием величина называется оптической толщиной
Подведём итог:
Часть 5. Атмосферный шейдер.
Введение
Написание шейдера
Мы можем начать писать шейдер для этого эффекта бесконечным количеством способов. Поскольку мы хотим рендерить атмосферное рассеяния на планетах, то логично предположить, что он будет использоваться на сферах.
Если вы используете этот туториал для создания игры, есть вероятность, что вы примените шейдер к уже готовой планете. Добавление вычислений атмосферного рассеяния поверх сверы возможно, но обычно даёт плохие результаты. Причина в том, что атмосфера больше, чем радиус планеты, поэтому её нужно рендерить на прозрачной сфере чуть большего размера. На рисунке ниже показано, как атмосфера простирается над поверхностью планеты, смешиваясь с пустым космосом за ней.

Применение рассеиваемого материала к отдельной сфере возможно, но избыточно. В этом туториале я предлагаю расширить Standard Surface Shader Unity, добавив проход шейдера, который рендерит атмосферу на немного большей сфере. Мы будем называть её атмосферной сферой.
Шейдер с двумя проходами
Если вы раньше работали с поверхностными шейдерами Unity, то могли заметить, что в них нет поддержки блока
Pass
, с помощью которого в вершинном и фрагментном шейдере задаётся несколько проходов.Создание шейдера с двумя проходами возможно, достаточно просто добавить два отдельны раздела кода CG в один блок
SubShader
:Shader "Custom/NewSurfaceShader" {
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
}
SubShader {
// --- Первый проход ---
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Здесь код Cg
ENDCG
// ------------------
// --- Второй проход ---
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Здесь код Cg
ENDCG
// -------------------
}
FallBack "Diffuse"
}
Можно изменить первый проход для рендеринга планеты. С этого момента мы сосредоточимся на втором проходе, реализовав в нём атмосферное рассеяние.
Экструзия нормалей
Атмосферная сфера чуть больше, чем планета. Это значит, что второй проход должен экструдировать сферу наружу. Если в используемой вами модели применены плавные нормали, то мы можем достичь этого эффекта, воспользовавшись техникой под названием экструзией нормалей.
Экструзия нормалей — один из старейших шейдерных трюков, и обычно его изучают одним из первых. В моём блоге есть множество упоминаний о нём; хорошим началом может стать пост Surface Shader из серии A Gentle Introduction to Shaders.
Если вы незнакомы с тем, как работает экструзия нормалей, то я объясню: все вершины обрабатываются шейдером с помощью его вершинной функции. Мы можем использовать эту функцию для изменения положения каждой вершины, сделав сферу больше.
Первым шагом будет изменение директивы
pragma
добавлением в неё vertex:vert
; это заставит Unity выполнять для каждой вершины функцию vert
.#pragma surface surf StandardScattering vertex:vert
void vert (inout appdata_full v, out Input o)
{
UNITY_INITIALIZE_OUTPUT(Input,o);
v.vertex.xyz += v.normal * (_AtmosphereRadius - _PlanetRadius);
}
В этом фрагменте кода показана вершинная функция, экструдирующая сферу вдоль её нормалей. Величина экструзии сферы зависит от размера атмосферы и размера планеты. Обе эти величины нужно передавать в шейдер как свойства, к которым можно получить доступ через material inspector.
Нашему шейдеру также нужно знать, где находится центр планеты. Мы можем добавить его расчёт тоже в вершинную функцию. Нахождение центральной точки объекта в пространстве мира мы обсуждали в статье Vertex and Fragment Shaders.
struct Input
{
float2 uv_MainTex;
float3 worldPos; // Автоматически инициализируется Unity
float3 centre; // Инициализируется в вершинной функции
};
void vert (inout appdata_full v, out Input o)
{
UNITY_INITIALIZE_OUTPUT(Input,o);
v.vertex.xyz += v.normal * (_AtmosphereRadius - _PlanetRadius);
o.centre = mul(unity_ObjectToWorld, half4(0,0,0,1));
}
Что такое UNITY_INITIALIZE_OUTPUT?
Если посмотреть на вершинную функцию, то можно заметить, что она всегда содержит таинственный вызов
И это одна из операций, выполняемых
UNITY_INITIALIZE_OUTPUT
. Шейдер получает позицию вершин в пространстве объекта, и ему нужно спроецировать их в координаты мира с помощью предоставляемых Unity положения, масштаба и поворота.И это одна из операций, выполняемых
UNITY_INITIALIZE_OUTPUT
. Без него нам бы пришлось писать необходимый для этих вычислений код самостоятельно.Аддитивное смешивание
Ещё одна интересная характеристика, с которой нам нужно разобраться — это прозрачность. Обычно прозрачный материал позволяет видеть, что находится за объектом. Это решение в нашем случае не сработает, потому что атмосфера — это не просто прозрачный кусок пластика. Через неё проходит свет, поэтому нам нужно использовать режим аддитивного смешивания, чтобы увеличить светимость планеты.
В стандартном поверхностном шейдере Unity по умолчанию не включен никакой режим смешивания. Чтобы изменить ситуацию, нам придётся заменить метки во втором проходе на следующие:
Tags { "RenderType"="Transparent"
"Queue"="Transparent"}
LOD 200
Cull Back
Blend One One
Выражение
Blend One One
используется шейдером для обращения к режиму аддитивного смешивания.Собственная функция освещения
Чаще всего при написании поверхностного шейдера программисты изменяют его функцию
surf
, которая используется для обеспечения «физических» свойств, таких как albedo, гладкость поверхности, металлические свойства и т.д. Все эти свойства затем используются шейдером для вычисления реалистичного затенения.В нашем случае эти вычисления не потребуются. Чтобы избавиться от них, нам нужно удалить используемую шейдером модель освещения. Я подробно рассматривал эту тему; можете изучить следующие посты, чтобы разобраться, как это делать:
- 3D Printer Shader Effect (перевод на Хабре)
- CD-ROM Shader: Diffraction Grating
- Fast Subsurface Scattering in Unity (перевод на Хабре)
Новая модель освещения будет называться
StandardScattering
; мы должны создать функции для освещения в реальном времени и для глобального освещения, то есть, соответственно, LightingStandardScattering
и LightingStandardScattering_GI
.В коде, который нам нужно написать, также будут использоваться такие свойства, как направление освещения и направление обзора. Их можно получить с помощью следующего фрагмента кода.
#pragma surface surf StandardScattering vertex:vert
#include "UnityPBSLighting.cginc"
inline fixed4 LightingStandardScattering(SurfaceOutputStandard s, fixed3 viewDir, UnityGI gi)
{
float3 L = gi.light.dir;
float3 V = viewDir;
float3 N = s.Normal;
float3 S = L; // Направление освещения от солнца
float3 D = -V; // Направление луча видимости, проходящего через атмосферу
...
}
void LightingStandardScattering_GI(SurfaceOutputStandard s, UnityGIInput data, inout UnityGI gi)
{
LightingStandard_GI(s, data, gi);
}
В
...
будет содержаться сам код шейдера, который нужен для реализации этого эффекта.Точность чисел с плавающей запятой
В этом туториале мы будем считать, что все вычисления производятся в метрах. Это значит, что если нам нужно симулировать Землю, то потребуется сфера с радиусом 6371000 метров. На самом деле, в Unity это невозможно из-за ошибок чисел с плавающей запятой, которые возникают, когда приходится одновременно работать с очень большими и очень маленькими числами.
Чтобы обойти эти ограничения, можно для компенсации изменить масштаб коэффициента рассеяния. Например, если планета имеет радиус всего 6,371 метров, то коэффициент рассеяния
В моём проекте Unity все свойства и вычисления выражены в метрах. Это позволяет нам использовать реальные, физические значения для коэффициентов рассеяния и приведённой высоты. Однако, шейдер получает в метрах и размер сферы, чтобы он мог выполнить преобразование масштаба из единиц Unity в метры реального масштаба.
Часть 6. Пересечение атмосферы.
Как сказано выше, единственный способ вычисления оптической толщины отрезка, проходящего сквозь атмосферу — это численное интегрирование. Это значит, что нужно разделить интервал на меньшие отрезки длины

На изображении выше оптическая толщина
Первым шагом, очевидно, будет нахождение точек
worldPos
в структуре Input
. Вот какой объём работы выполняет шейдер; единственная доступная нам информация — это 
Во-первых, стоит заметить, что
float3
), а float
). где запись с чертой сверху
В коде шейдера для эффективности мы будем использовать
Также стоит заметить, что отрезки
Отрезок
Стоит также заметить, что
Далее нам нужно вычислить длину отрезка
И это значит, что:
Длина
Теперь у нас есть все необходимые величины. Подведём итог:
В этом наборе уравнений есть квадратные корни. Они определены только для неотрицательных чисел. Если
Мы можем преобразовать это в следующую функцию Cg:
bool rayIntersect
(
// Ray
float3 O, // Origin
float3 D, // Направление
// Сфера
float3 C, // Центр
float R, // Радиус
out float AO, // Время первого пересечения
out float BO // Время второго пересечения
)
{
float3 L = C - O;
float DT = dot (L, D);
float R2 = R * R;
float CT2 = dot(L,L) - DT*DT;
// Точка пересечения за пределами круга
if (CT2 > R2)
return false;
float AT = sqrt(R2 - CT2);
float BT = AT;
AO = DT - AT;
BO = DT + BT;
return true;
}
Здесь возвращается не одно, а три значения
out
, которые сохраняют после завершения функции любые изменения, которые она внесла в эти параметры.Столкновение с планетой
Есть ещё одна проблема, которую нужно учитывать. Некоторые лучи видимости сталкиваются с планетой, поэтому их путешествие через атмосферу завершается рано. Один из вариантов решения — пересмотреть выведенное выше.
Более простой, но менее эффективный подход — выполнить
rayIntersect
дважды и при необходимости изменить конечную точку.
Это преобразуется в следующий код:
// Пересечения с атмосферной сферой
float tA; // Точка входа в атмосферу (worldPos + V * tA)
float tB; // Точка выхода из атмосферы (worldPos + V * tB)
if (!rayIntersect(O, D, _PlanetCentre, _AtmosphereRadius, tA, tB))
return fixed4(0,0,0,0); // Лучи видимости смотрят в глубокий космос
// Проходит ли луч через ядро планеты?
float pA, pB;
if (rayIntersect(O, D, _PlanetCentre, _PlanetRadius, pA, pB))
tB = pA;
Часть 7. Шейдер атмосферного рассеяния.
В этой части мы наконец завершим работу над симуляцией рэлеевского рассеяния в атмосфере планеты.
GIF

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

Количество отрезков в
_ViewSamples
. Поскольку это свойство, мы сможем получить к нему доступ из material inspector. Это позволяет нам снизить точность шейдера ради его производительности.Следующий фрагмент кода позволяет обойти в цикле все отрезки в атмосфере.
// Численное интегрирование для вычисления
// влияния света в каждой точке P отрезка AB
float3 totalViewSamples = 0;
float time = tA;
float ds = (tB-tA) / (float)(_ViewSamples);
for (int i = 0; i < _ViewSamples; i ++)
{
// Позиция точки
// (сэмплирование в середине отрезка сэмпла видимости)
float3 P = O + D * (time + ds * 0.5);
// T(CP) * T(PA) * ρ(h) * ds
totalViewSamples += viewSampling(P, ds);
time += ds;
}
// I = I_S * β(λ) * γ(θ) * totalViewSamples
float3 I = _SunIntensity * _ScatteringCoefficient * phase * totalViewSamples;
Переменная
time
используется для отслеживания того, насколько далеко мы находимся от начальной точки ds
.Оптическая толщина PA
Каждая точка на луче видимости
Давайте попробуем упростить это выражение, как в прошлом параграфе. Мы можем ещё больше развернуть представленное выше выражение, заменив
Результат пропускания по
Комбинированное пропускание моделируется как экспоненциальное уменьшение, коэффициентом которого является сумма оптических толщин по пути, пройденному светом (
Первая величина, которую мы начинаем вычислять, это оптическая толщина отрезка
Если бы пришлось реализовывать его «в лоб», то мы бы создали функцию
opticalDepth
, сэмплирующую в цикле точки между opticalDepthSegment
), и продолжим суммировать её в цикле for (opticalDepthPA
).// Накопитель оптической толщины
float opticalDepthPA = 0;
// Численное интегрирование для вычисления
// влияния света каждой точки P в AB
float time = tA;
float ds = (tB-tA) / (float)(_ViewSamples);
for (int i = 0; i < _ViewSamples; i ++)
{
// Позиция точки
// (сэмплируется посередине сэмпла отрезка видимости)
float3 P = O + D * (time + viewSampleSize*0.5);
// Оптическая толщина текущего отрезка
// ρ(h) * ds
float height = distance(C, P) - _PlanetRadius;
float opticalDepthSegment = exp(-height / _ScaleHeight) * ds;
// Накопление оптических толщин
// D(PA)
opticalDepthPA += opticalDepthSegment;
...
time += ds;
}
Сэмплирование света
Если мы вернёмся к выражению для влияния света
Мы переместим код, вычисляющий оптическую толщину отрезка
lightSampling
. Название взято от луча света, который является отрезком, начинающимся в Однако функция
lightSampling
не просто вычисляет оптическую толщину 
На схеме ниже легко увидеть, что влияние света
lightSampling
также проверяет, не было ли столкновения с планетой. Это можно реализовать проверкой высоты точки на отрицательность.bool lightSampling
( float3 P, // текущая точка внутри атмосферной сферы
float3 S, // Направление к солнцу
out float opticalDepthCA
)
{
float _; // это нас не интересует
float C;
rayInstersect(P, S, _PlanetCentre, _AtmosphereRadius, _, C);
// Сэмплы на отрезке PC
float time = 0;
float ds = distance(P, P + S * C) / (float)(_LightSamples);
for (int i = 0; i < _LightSamples; i ++)
{
float3 Q = P + S * (time + lightSampleSize*0.5);
float height = distance(_PlanetCentre, Q) - _PlanetRadius;
// Внутри планеты
if (height < 0)
return false;
// Оптическая толщина для луча света
opticalDepthCA += exp(-height / _RayScaleHeight) * ds;
time += ds;
}
return true;
}
Представленная выше функция сначала вычисляет точку
rayInstersect
. Затем она разделяет отрезок _LightSamples
длиной ds
. Вычисление оптической толщины то же самое, которое применяется в самом внешнем цикле.Функция возвращает false, если произошло столкновение с планетой. Мы можем использовать это для обновления отсутствующего кода самого внешнего цикла, заменив
...
. // D(CP)
float opticalDepthCP = 0;
bool overground = lightSampling(P, S);
if (overground)
{
// Комбинированное пропускание
// T(CP) * T(PA) = T(CPA) = exp{ -β(λ) [D(CP) + D(PA)]}
float transmittance = exp
(
-_ScatteringCoefficient *
(opticalDepthCP + opticalDepthPA)
);
// Влияния света
// T(CPA) * ρ(h) * ds
totalViewSamples += transmittance * opticalDepthSegment;
}
Теперь, когда мы учли все элементы, наш шейдер готов.
[Прим. пер.: на странице Patreon автора можно купить доступ к Standard и Premium версиям готового шейдера.]