Предыдущая статья

Это вторая часть этой серии; Предыдущая статья здесь.

В прошлой статье мы закончили кодирование в cpp и шейдерах BasePass, так что теперь он может выводить некоторую информацию, которая нам нужна для освещения. В этой статье мы собираемся проделать основную работу по реализации cel-shading. И после прочтения я надеюсь, что это может быть полезно для вас, чтобы отследить код шейдера (хотя бы немного :)).

Измените шейдеры: DeferredPass и освещение

После рендеринга BasePass вся необходимая информация, необходимая для расчета освещения, теперь собирается в GBuffers. Затем средство визуализации света также будет обрабатывать каждый источник света как примитив (например, точечный свет - это сфера, точечный свет - это конус, а направленный - это полноэкранный четырехугольник с его информацией о свете.) Для каждого примитива света параметр Пиксельный шейдер отложенного затенения будет использовать данные в GBuffers и информацию о освещении для расчета освещения.

1. Расчет освещения

Точкой входа для этого является функция DeferredLightPixelMain (), которая находится в DeferredLightPixelShaders.usf. Как и в предыдущей версии UE, вычислением по-прежнему занимается функция GetDynamicLighting (). В приведенном ниже фрагменте кода запомните место, где я только что вставил комментарий, здесь мы разместим эффект контура позже.

И продолжайте отслеживать код в функции GetDynamicLighting (), которая находится в DeferredLightingCommon.ush. Эта функция сначала получает теневой член, а затем вызывает основную функцию затенения IntegrateBxDF (). В этой функции выполняются различные методы затенения в соответствии с моделью затенения. Итак, давайте добавим новую функцию затенения в случае переключателя и реализуем новый вызов CelShadingBxDF (). Вы можете просто запустить эту функцию, расширив стандартный DefaultLitBxDF ().

Есть разные способы выполнения cel-shading; Я просто показываю здесь свои. Многие учебники используют smoothstep() или step(). Но я бы не стал их использовать, так как хотел бы сделать Cel-Bands гибкими. Поэтому я добавляю контрольное значение Cel Bands из материального штифта, а затем вычисляю значение интервала, дискретизируя диапазон интенсивности [0,1] на несколько полос. Значение интервала представляет собой значение шага, которое мы добавляем при переходе ячейки к следующему диапазону. Концепция заключается в использовании floor ([OriginalVal] x [#Bands]), чтобы получить в каком диапазоне эта интенсивность, а затем умножить на интервал, чтобы получить окончательный «округленный» »Интенсивность. В этой работе я буду использовать ту же идею, чтобы «разбить на ячейки» все значения интенсивности.

В следующем коде вы можете запутаться с «InvBands» и «Band». Напомним, что мы упоминали в предыдущей статье, данные, помещенные в CustomData, будут ограничены до 1. Так что это всего лишь обходной путь, чтобы использовать 1 / Bands в качестве входных данных и инвертировать их обратно в шейдере. .

(На самом деле, я придумал другое решение в этой статье, которое использует Global Shader Uniform, или я думаю, что оно также будет работать с Material-Parameter-Collection)

Следующий вопрос: что нам нужно «клеточить»? Насколько мне известно, это должно быть значение «интенсивности», что означает, что вы должны избавиться от информации о цвете. Для диффузного это значение NoL (что означает нормальный-точечный-светлый). Это ясно показано в приведенном выше коде.

Для зеркального отражения это может быть что-то связанное с NoH или VoH, но каким-то образом в UE4.22 эти два значения интегрированы в SpecularGGX () функция, и она напрямую возвращает окончательный цвет. Чтобы решить эту проблему, мы можем просто добавить альтернативную версию, которая возвращает только ScaleTerm, а затем поместить это значение в ячейку и выполнить умножение цвета в конце. Функции SpecularGGX_ScaleTerm () и F_Schlick_ScaleTerm () ниже показывают, как я это делаю.

Хорошо, после этого вы можете увидеть базовый результат cel-shading следующим образом:

2. Применить к тени

На самом деле, мы неправильно поняли слежку. Если вы выполните описанные выше шаги, то, что вы увидите, должно выглядеть как на следующем изображении, где тень все еще гладкая, а не в стиле cel-shading.

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

Вернитесь к функции GetDynamicLighting (). Вы можете видеть, что перед вызовом IntegrateBxDF () вызывается функция GetShadowTerms (). Эта функция вычисляет коэффициент тени и помещает их в FShadowTerms «Тень». После BxDF ShadowTerms затем используется для масштабирования результата освещения. Поэтому мы просто применяем ту же обработку размера ячейки к этому ShadowTerms, которая является SurfaceMultiplier на следующем изображении.

Теперь затухание теней также выполнено, вернитесь в редактор, перекомпилируйте шейдеры, тени также должны быть в стиле cel-shading.

3. Применить к отражению

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

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

Например, из части внутри красных кругов вы можете видеть, что отражение небесной сферы все еще гладкое и, следовательно, делает этот стеклянный объект не столь стилизованным в стиле cel-shading.

Вы также можете исправить (?) Это с помощью той же концепции, но немного посложнее. Сначала отражение выполняется в функции ReflectionEnvironment () в файле шейдера ReflectionEnvironmentPixelShader.usf. При вычислении отражения самое сложное - выяснить, «что можно разбить на ячейки?»

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

Во-первых, часть NoV - это то, что вы, по крайней мере, легко можете сделать, но я обнаружил, что это не сильно улучшает эффект затенения cel-shading.

Мой последний принятый эксперимент - напрямую разместить цвет в ячейках. В следующем фрагменте кода блок else {} - это исходный код (обратите внимание на «+ =»). В моей реализации я разбиваю эту операцию на основной цвет (Color.rgb) + дополнительный цвет. Честно говоря, я ничего не могу поделать с базовым Color.rgb, поэтому сосредоточусь на дополнительной части.

Мой метод состоит в том, чтобы использовать цветовое пространство LAB для замены отсутствующей интенсивности: преобразовать RGB в LAB и получить значение «L», поместить значение L в ячейку как интенсивность. А затем конвертируйте LAB обратно в RGB. (Обратите внимание, что диапазон L равен [0,100] вместо [0,1], поэтому вам нужно сопоставление диапазона.)

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

Эффект контура в пользовательской модели затенения

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

1. Идея

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

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

2. Добавьте коды

Вернемся к DeferredLightPixelMain () в DeferredLightPixelShaders.usf, который мы видели ранее. Лучшее время для обнаружения краев будет до расчета освещения, поскольку, если пиксель обнаружен как контур, мы можем пропустить освещение. Итак, давайте вставим код после строки CalcSceneDepth ().

Структура SceneTexturesStruct хранит в себе несколько текстур SceneTexture, здесь вы можете получить GBufferA / B / C / DTexture и текстуру глубины сцены. Информация вектора нормали хранится в GBufferA.

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

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

(К сведению, на самом деле встроенная функция CalcSceneDepth в UE4 также извлекает SceneDepthTexture в SceneTexturesStruct. :) )

3. Не размышлять снова

Теперь у нас есть контур, но если вы уделите больше внимания стеклянному объекту, вы обнаружите, что цвет контура варьируется по поверхности. Нет, это снова отражение!

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

4. Результат

Давайте посмотрим на результат того, что мы сделали до сих пор! Стеклоподобный объект с отражением неба. И металлический объект с некоторой шероховатостью и небольшим отображением нормалей.

Финал

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

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

На этом мой рассказ о модели cel-shading заканчивается. Надеюсь, это будет полезно для вас. И если вам понравился мой обмен, не стесняйтесь оставлять комментарии :)