Что такое шейдер
Что такое шейдер
Шейдер
Ше́йдер (англ. Shader ; схема затенения, программа построения теней) — это программа для одной из ступеней графического конвейера, используемая в трёхмерной графике для определения окончательных параметров объекта или изображения. Она может включать в себя произвольной сложности описание поглощения и рассеяния света, наложения текстуры, отражение и преломление, затенение, смещение поверхности и эффекты пост-обработки.
Программируемые шейдеры гибки и эффективны. Сложные с виду поверхности могут быть визуализированы при помощи простых геометрических форм. Например, шейдеры могут быть использованы для рисования поверхности из трёхмерной керамической плитки на абсолютно плоской поверхности.
Содержание
Потребность в шейдерах
В программных графических движках вся цепочка рендеринга — от определения видимых частей сцены до наложения текстуры — писалась разработчиком игры. В эту цепочку можно было включать собственные нестандартные видеоэффекты. Но с появлением видеоакселераторов разработчик оказался ограничен тем набором эффектов, который заложен в аппаратное обеспечение. Вот два примера.
Предшественниками шейдеров были процедурная генерация текстур (широко применявшаяся в Unreal для создания анимированных текстур воды и огня) и мультитекстурирование (на нём был основан язык шейдеров, применявшийся в Quake 3). Но и эти механизмы не обеспечивают такой гибкости, как шейдеры.
Типы шейдеров
В настоящее время шейдеры делятся на три типа: вершинные, геометрические и фрагментные (пиксельные).
Вершинные шейдеры (Vertex Shader)
Вершинный шейдер оперирует данными, сопоставленными с вершинами многогранников. К таким данным, в частности, относятся координаты вершины в пространстве, текстурные координаты, тангенс-вектор, вектор бинормали, вектор нормали. Вершинный шейдер может быть использован для видового и перспективного преобразования вершин, генерации текстурных координат, расчета освещения и т. д.
Геометрические шейдеры (Geometry Shader)
Геометрический шейдер, в отличие от вершинного, способен обработать не только одну вершину, но и целый примитив. Это может быть отрезок (две вершины) и треугольник (три вершины), а при наличии информации о смежных вершинах (adjacency) может быть обработано до шести вершин для треугольного примитива. Кроме того, геометрический шейдер способен генерировать примитивы «на лету», не задействуя при этом центральный процессор. Впервые начал использоваться на видеокартах Nvidia серии 8.
Пиксельные шейдеры (Pixel Shader)
Пиксельный шейдер работает с фрагментами растрового изображения. Под фрагментом изображения в данном случае понимается пиксель, которому поставлен в соответствие некоторый набор атрибутов, таких как цвет, глубина, текстурные координаты. Пиксельный шейдер используется на последней стадии графического конвейера для формирования фрагмента изображения.
Шейдерные языки
Впервые использованные в системе RenderMan компании Pixar, шейдеры получали всё большее распространение со снижением цен на компьютеры. Основное преимущество от использования шейдеров — их гибкость, упрощающая и удешевляющая цикл разработки программы, и при том повышающая сложность и реалистичность визуализируемых сцен.
Шейдерные языки обычно содержат специальные типы данных, такие как матрицы, семплеры, векторы, а также набор встроенных переменных и констант для удобной интеграции со стандартной функциональностью 3D API. Поскольку компьютерная графика имеет множество сфер приложения, для удовлетворения различных потребностей рынка было создано большое количество шейдерных языков.
Профессиональный рендеринг
Данные шейдерные языки ориентированы на достижение максимального качества визуализации. Описание свойств материалов сделано на максимально абстрактном уровне, для работы не требуется особых навыков программирования или знания аппаратной части. Такие шейдеры обычно создаются художниками с целью обеспечить «правильный вид», подобно наложению текстуры, источников света и другим аспектам их работы.
Обработка таких шейдеров обычно представляет собой ресурсоёмкую задачу. Совокупная вычислительная мощность, необходимая для обеспечения их работы, может быть очень велика, так как используется для создания фотореалистичных изображений. Основная часть вычислений при подобной визуализации выполняется большими компьютерными кластерами.
Шейдерный язык RenderMan
Шейдерный язык RenderMan, описанный в Спецификации интерфейса RenderMan, является фактическим стандартом для профессионального рендеринга. API RenderMan, разработанный Робом Куком, используется во всех работах студии Pixar. Он также является первым из реализованных шейдерных языков.
Шейдерный язык Gelato
NVIDIA Gelato представляет собой оригинальную гибридную систему рендеринга изображений и анимации трехмерных сцен и объектов, использующую для расчетов центральные процессоры и аппаратные возможности профессиональных видеокарт серии Quadro FX.
Основополагающим принципом, которого неукоснительно придерживаются разработчики, является бескомпромиссное качество финального изображения, не ограниченное ничем, в том числе — современными возможностями видеокарт. Как производственный инструмент, способный создавать конечный продукт высокого качества, Gelato предназначен для профессионального использования в таких областях как кино, телевидение, промышленный дизайн и архитектурные визуализации.
Рендеринг в реальном времени
Шейдерный язык OpenGL
Шейдерный язык OpenGL носит название GLSL (The OpenGL Shading Language). GLSL основан на языке ANSI C. Большинство возможностей языка ANSI C сохранено, к ним добавлены векторные и матричные типы данных, часто применяющиеся при работе с трехмерной графикой. В контексте GLSL шейдером называется независимо компилируемая единица, написанная на этом языке. Программой называется набор откомпилированных шейдеров, связанных вместе.
Шейдерный язык Cg
Разработан nVidia совместно с Microsoft (такой же по сути язык от Microsoft называется HLSL, включён в DirectX 9). Cg расшифровывается как C for Graphics. Язык действительно очень похож на C, он использует схожие типы (int, float, а также специальный 16-битный тип с плавающей запятой — half). Поддерживаются функции и структуры. Язык обладает своеобразными оптимизациями в виде упакованных массивов (packed arrays) — объявления типа «float a[4]» и «float4 a» в нём соответствуют разным типам. Второе объявление и есть упакованный массив, операции с упакованным массивом выполняются быстрее, чем с обычными. Несмотря на то, что язык разработан nVidia, он без проблем работает и с видеокартами ATI. Однако следует учесть, что все шейдерные программы обладают своими особенностями, которые следует получить из специализированных источников.
Шейдерные языки DirectX
Низкоуровневый шейдерный язык DirectX (DirectX ASM)
По синтаксису сходен с Ассемблером. Существует несколько версий, различающихся по набору команд, а также по требуемому оборудованию. Существует разделение на вершинные (vertex) и пиксельные (pixel) шейдеры, которые различаются.
Выполняет обработку геометрии, то есть изменяет параметры вершины, такие, как позицию, текстурные координаты, цвет вершин. Также может выполнять вычисления освещения. Допустимое количество команд может достигать одной-двух сотен. Пример фрагмента кода:
Выполняет обработку цветовых данных, полученных при рисовании треугольника. Оперирует с текстурами и цветом. Количество инструкций значительно ограничено. Так, к примеру, в версии 1.4 оно не может быть больше 32-х. Пример фрагмента кода:
Высокоуровневый шейдерный язык DirectX (HLSL — High Level Shader Language)
Является надстройкой над DirectX ASM. По синтаксису сходен с C, позволяет использовать структуры, процедуры и функции.
Литература
Ссылки
Полезное
Смотреть что такое «Шейдер» в других словарях:
шейдер — Подпрограмма построения теней и др. эффектов. Шейдером в широком смысле называется программа для визуального определения поверхности объекта. Это может быть описание освещения, текстурирования, постобработки и т.п. [http://www.morepc.ru/dict/]… … Справочник технического переводчика
Цел-шейдер — Два вида рендеринга. Слева обычный. Справа цел шейдер. Цел шейдерная анимация (англ. Cel shaded), также называемая как цел шейдинг или тун шейдинг это тип нефотореалистичного рендеринга, сделанного так, чтобы выглядеть как картинка,… … Википедия
вершинный шейдер — Вершинные шейдеры это программы, выполняемые видеочипами, которые производят математические операции с вершинами (vertex, из них состоят 3D объекты в играх), иначе говоря, они предоставляют возможность выполнять программируемые алгоритмы по… … Справочник технического переводчика
пиксельный шейдер — Пиксельные шейдеры это программы, выполняемые видеочипом во время растеризации для каждого пикселя изображения, они производят выборку из текстур и/или математические операции над цветом и значением глубины (Z buffer) пикселей. [http://www.morepc … Справочник технического переводчика
Сравнение графических процессоров ATI — Эта таблица содержит основную информацию о графических процессорах ATI и видеокартах, построенных на официальных спецификациях ATI Technologies. Содержание 1 Замечания о версиях DirectX 2 Замечания о версиях OpenGL … Википедия
Сравнение графических процессоров NVIDIA — Эта таблица содержит основную информацию о графических процессорах NVIDIA серии GeForce и видеокартах, построенных на официальных спецификациях NVIDIA. Содержание … Википедия
Сравнение графических процессоров AMD — Эта таблица содержит основную информацию о графических процессорах AMD и видеокартах, построенных на официальных спецификациях AMD. Содержание … Википедия
Nvidia Quadro — Quadro серия видеокарт, разработанных фирмой NVIDIA для пользователей профессиональных приложений 2D и 3D. Quadro NVS видеокарты, разработанные для бизнес приложений (финансовых, корпоративных, ECAD) и для многомониторных решений… … Википедия
Quadro — Quadro серия видеокарт, разработанных фирмой NVIDIA для пользователей профессиональных приложений 2D и 3D. Quadro NVS видеокарты, разработанные для бизнес приложений (финансовых, корпоративных, ECAD) и для многомониторных решений… … Википедия
Direct3D 11 — Стиль этой статьи неэнциклопедичен или нарушает нормы русского языка. Статью следует исправить согласно стилистическим правилам Википедии. Direct3D 11 (D3D11) компонент интерфейса программирования приложений (англ. … Википедия
Поговорим о шейдерах, их проектировании и Unity
Воруй как художник
Для разминки я бы в целом предложил довольно простое упражнение. Подобное есть у художников, только тут оно немного сложнее. Делать базовые материалы. Фарфор, акрил, стекло, воск, грунт, кафель, пластик, металлы и так далее. Материалов с разными физическими и оптическими свойствами существует очень много. Но не в стиле подкрасил и натянул на Standard шейдер текстурки с https://textures.com/ и готово. А размышляя о том, как это сделать в рамках ограничений.
Допустим поставить себе ограничение, что шейдер обязательно должен быть однопроходным. Можно сказать, что в данном случае ряд эффектов невозможным, но это не совсем правда. Из перечисленного выше ряд эффектов становится невозможным если делать “честно”, по математической модели материала или физического явления. А тут нужно думать “на что это похоже” и “где я могу выиграть в производительности”.
В целом если вы хотите уметь создавать эффекты от и до, то лучше обладать некоторой арт экспертизой, чтобы делать что-то совсем новое и уникальное. Но в целом, на мой взгляд, основная задача разработчика реализовывать безумные требования художников/геймдизайнеров/дизайнеров/бизнеса.
Любому хорошему повару нужны заготовки
Поэтому по любой материал, эффект или явление можно разобрать на набор составляющих его компонент и разработать оптимальное решение, которое похоже на данный компонент в рамках вашей задачи. Просто скажем сингл пасс шейдер не единственное интересное ограничение, может у вас 2.5D игра и в этом случае многие эффекты делаются проще. Или вам не нужно супер реалистичное подповерхностное рассеивание, то тогда вам подойдёт просто похожий на него эффект, который +- ничего не стоит. Например в данной статье был неплохой вариант https://habr.com/ru/post/337370/ Или вы знаете размещение источников света в сцене. В каждом конкретном случае упрощающих задачу ограничений может быть очень много вплоть до знания особенностей геометрии.
Это лишь пара простых примеров. Можно ещё задать параметр ветра в виде вектора и описать его упрощённую логику или полную. Но суть размышлений +- такова во многих случаях. Думаем на что это похоже из того, что можно сделать простыми средствами.
Но что же по поводу конкретных заготовок. Которые нужны любому “повару” отвечающему за разработку эффектов. Их очень много я перечислю лишь часть.
Что такое шейдеры? Просто о сложном для начинающих
«Что такое шейдеры?» – очень частый вопрос любопытных игроков и начинающих игровых разработчиков. В этой статье доходчиво и понятно об этих страшных шейдерах расскажу.
Двигателем прогресса в сторону фотореалистичности картинки в компьютерной графике я считаю именно компьютерные игры, поэтому давайте именно в разрезе видео-игр и поговорим о том, что такое “шейдеры”.
До того, как появились первые графические ускорители, всю работу по отрисовке кадров видеоигры выполнял бедняга центральный процессор.
Так вот, центральный процессор (CPU – Central Processing Unit) слишком умный парень, чтобы заставлять его заниматься такой рутиной. Вместо этого логично выделить какой-то аппаратный модуль, который разгрузит CPU, чтобы тот смог заниматься более важным интеллектуальным трудом.
Таким аппаратным модулем стал – графический ускоритель или видеокарта (GPU – Graphics Processing Unit). Теперь CPU подготавливает данные и загружает рутинной работой коллегу. Учитывая, что GPU сейчас это не просто один коллега, это толпа миньонов-ядер, то он с такой работой справляется на раз.
Но мы пока не получили ответа на главный вопрос: Что такое шейдеры? Подождите, я подвожу к этому.
Хорошая, интересная и близкая к фото-реализму графика, требовала от разработчиков видеокарт реализовывать многие алгоритмы на аппаратном уровне. Тени, свет, блики и так далее. Такой подход – с реализацией алгоритмов аппаратно называется “Фиксированный пайплайн или конвейер” и там где требуется качественная графика он теперь не встречается. Его место занял “Программируемый пайплайн”.
Запросы игроков “давайте, завозите хороший графоний! удивляйте!”, толкали разработчиков игр (и производителей видеокарт соответственно) все к более и более сложным алгоритмам. Пока в какой-то момент зашитых аппаратных алгоритмов им стало слишком мало.
Наступило время видеокартам стать более интеллектуальными. Было принято решение позволить разработчикам программировать блоки графического процессора в произвольные конвейеры, реализующие разные алгоритмы. То есть разработчики игр, графические программисты отныне смогли писать программы для видеокарточек.
И вот, наконец, мы дошли до ответа на наш главный вопрос.
“Что такое шейдеры?”
Ше́йдер (англ. shader — затеняющая программа) — это программа для видеокарточки, которая используется в трёхмерной графике для определения окончательных параметров объекта или изображения, может включать в себя описание поглощения и рассеяния света, наложения текстуры, отражения и преломление, затенение, смещение поверхности и множество других параметров.
Графический пайплайн
Преимущество программируемого конвейера перед его предшественником в том, что теперь программистам можно создавать свои алгоритмы самостоятельно, а не пользоваться зашитым аппаратно набором опций.
Сначала видеокарты оснастили несколькими специализированными процессорами, поддерживающими разные наборы инструкций. Шейдеры делили на три типа в зависимости от того, какой процессор будет их исполнять. Но затем видеокарты стали оснащать универсальными процессорами, поддерживающими наборы инструкций всех трёх типов шейдеров. Деление шейдеров на типы сохранилось для описания назначения шейдера.
Помимо графических задач с такими интеллектуальными видеокартами появилась возможность выполнения на GPU вычислений общего назначения (не связанных с компьютерной графикой).
Впервые полноценная поддержка шейдеров появилась в видеокартах серии GeForce 3, но зачатки были реализованы ещё в GeForce256 (в виде Register Combiners).
Виды шейдеров
В зависимости от стадии конвейера шейдеры делятся на несколько типов: вершинный, фрагментный (пиксельный) и геометрический. А в новейших типах конвейеров есть еще шейдеры тесселяции. Подробно обсуждать графический конвейер мы не будем, я все думаю не написать ли об этом отдельную статью, для тех кто решит заняться изучением шейдеров и программирования графики. Напишите в комментариях если Вам интересно, я буду знать, стоит ли тратить время.
Вершинный шейдер
Вершинными шейдерами делают анимации персонажей, травы, деревьев, создают волны на воде и многие другие штуки. В вершинном шейдере программисту доступны данные, связанные с вершинами например: координаты вершины в пространстве, её текстурные координатами, её цвет и вектор нормали.
Геометрический шейдер
Геометрические шейдеры способны создавать новую геометрию, и могут использоваться для создания частиц, изменения детализации модели «на лету», создание силуэтов и т.п. В отличие от предыдущего вершинного, способны обработать не только одну вершину, но и целый примитив. Примитивом может быть отрезок (две вершины) и треугольник (три вершины), а при наличии информации о смежных вершинах (англ. adjacency) для треугольного примитива может быть обработано до шести вершин.
Пиксельный шейдер
Пиксельными шейдерами выполняют наложение текстур, освещение, и разные текстурные эффекты, такие как отражение, преломление, туман, Bump Mapping и пр. Пиксельные шейдеры также используются для пост-эффектов.
Пиксельный шейдер работает с фрагментами растрового изображения и с текстурами — обрабатывает данные, связанные с пикселями (например, цвет, глубина, текстурные координаты). Пиксельный шейдер используется на последней стадии графического конвейера для формирования фрагмента изображения.
На чем пишут шейдеры?
Изначально шейдеры можно было писать на assembler-like языке, но позже появились шейдерные языки высокого уровня, похожие на язык С, такие как: Cg, GLSL и HLSL.
Такие языки намного проще чем C, ведь задачи решаемые с их помощью, гораздо проще. Система типов в таких языках отражает нужды программистов графики. Поэтому они предоставляют программисту специальные типы данных: матрицы, семплеры, векторы и тп.
RenderMan
Все что мы обсудили выше относится к realtime графике. Но существуют non-realtime графика. В чем разница – realtime – реальное время, тоесть здесь и сейчас – давать 60 кадров в секунду в игре, это процесс реального времени. А вот рендерить комплексный кадр для ультрасовременной анимации по несколько минут это non-realtime. Суть во времени.
Например, графику такого качества как в последних мультипликационных фильмах студии Pixar получить в реальном времени мы сейчас получить не можем. Очень большие рендер-фермы обсчитывают симуляции света по совсем другим алгоритмам, очень затратным, но дающим почти фотореалистичные картинки.
Супер-реалистичная графика в Sand piper
Например, посмотрите, на вот этот милый мультфильм, песчинки, перышки птички, волны, все выглядит невероятно реальным.
*Видео могут забанить на Youtube, если оно не открывается, погуглите pixar sandpiper – короткометражный мультфильм про храброго песочника очень милый и пушистый. Умилит и продемонстрирует насколько крутой может быть компьютерная графика.
Так вот это RenderMan от фирмы Pixar. Он стал первым языком программирования шейдеров. API RenderMan является фактическим стандартом для профессионального рендеринга, используется во всех работах студии Pixar и не только их.
Полезная информация
Теперь Вы знаете что такое шейдеры, но помимо шейдеров, есть другие очень интересные темы в разработке игр и компьютерной графике, которые наверняка Вас заинтересуют:
Если остались вопросы
Как обычно, если у Вас остались какие-то вопросы, задавайте их в комментариях, я всегда отвечу. За любое доброе слово или правку ошибок я буду очень признателен.
Шейдеры. Что и как
Расскажу, как в общем случае они работают, что умеют и как их используют
Сразу оговорюсь, что материал рассчитан на тех, кто никогда не работал с шейдерами или вообще не знаком с разработкой игр, то есть это в некотором смысле научпоп.
Слово «шейдер» в контексте разработки игр очень популярно, слышать его могли и те, кто игры не делает. Само слово изначально появилось от англ. shading (затенение) — первые шейдеры использовались, чтобы передавать глубину с помощью работы со светом, блеском, тенями и прочим. Со временем шейдеры стали использоваться для совершенно разного вида постобработки и вообще отрисовки примерно всего.
Говоря общими словами, шейдер — это просто программа для графической карты. То есть то, что пишется школьниками на паскале (хипстерами на пайтоне) — это программы для вашего центрального процессора (CPU), а шейдеры — для графического (GPU). Особенность же этих программ выходит из особенностей GPU — они работают параллельно на сотнях маленьких ядех вместо нескольких больших, преимущественно осуществляя математические операции.
Теперь разберемся, как это все работает.
В общем случае цель шейдера — отрисовать некоторый объект. Поэтому возьмем куб, распишем процесс его отрисовки и посмотрим, где используются шейдеры и зачем. Сначала опишем сам куб. Для графической карты это 8 точек, между некоторыми из которых есть плоскость. Каждая из точек описывается тремя числами (правильно сказать, что это вершины). Помимо этого у кубика есть цвет и положение внутри мира.
Процесс отрисовки, если его достаточно упростить (что я и сделаю в рамках этой статьи), можно поделить на несколько шагов:
1. Получение входных данных из памяти.
2. Выполнение шейдера вершин.
3. Растеризация.
4. Выполнение шейдера пикселей (фрагментов).
5. Проведение тестов «глубины».
6. Отрисовка на текстуру для экрана.
В первом шаге видеокарта каким-то образом получает данные (вершины, плоскости, текстуры) в свою видеопамять, для нас это сейчас не так важно. Далее происходит конвертация координат относительно объекта в координаты на экране относительно камеры. После происходит растеризация — высчитывается, в каких пикселях уже на экране находится объект. Такие пиксели называют фрагментами. Отличие от пикселей заключается в том, что фрагмент помимо информации о пикселе, содержит еще и некоторую побочную информацию, полученную после растеризации. Для упрощения будем считать, что это все просто пиксели на экране. Далее для каждого пикселя выполняется шейдер фрагмента. А затем проверяется, что расстояние от камеры до фрагмента соответствует высчитанному заранее в нужном направлении в буфере глубины. Проще говоря, проверяется, нет ли перед объектом чего-либо еще, и нужно ли его отрисовывать на итоговое изображение.
Как видите, в процессе отрисовки можно заметить два вида шейдера. На самом деле, сейчас есть чуть больше видов, но они не столь важны для разбора, так как имеют более специфичный характер использования, а мы рассказываем на пальцах. Но вот те два, что нас волнуют:
1. Шейдер вершин.
2. Шейдер фрагментов.
Как сказано было ранее, этот шейдер (или группа шейдеров по очереди) занимается переводом координат относительно объекта, в координаты на текстуре.
На картинке начало координат немного не соответствует реальным, что все так же не влияет на понимание процесса 🙂
Пройдемся по состояниям. В первом у нас, очевидно, входные координаты без излишков. На втором они были перенесены в координаты относительно начала «мира». Потом они переносятся в координаты относительно точки смотрящего (видно на второй картинке), но заметно, что картинка плоская. Их проекция происходит далее и мы получаем наши итоговые координаты. Все эти операции производятся шейдером. Помимо прочего, он позволяет не только отобразить реальные координаты, но и модифицировать их так, чтобы исказить объект для эффекта. Например, я недавно писал шейдер, который переворачивал спрайт, чтобы отрисовать его тень:
После преобразований вершин и растеризации нужно высчитать цвет каждого фрагмента (помним, что для упрощения это пиксели). Для примера возьмём наш куб: мы помним, что он залит одним цветом. Просто сделаем так, чтобы цвет каждого фрагмента стал цвета куба и все:
Выглядит немного странно, да? Проблема в том, что мы не видим ни теней, ни текстур. Будь на кубе какая-либо текстура, мы бы заметили переход между гранями. Вот возьмем текстуру:
Теперь достаточно в каждом пикселе просто брать цвет из текстуры. Но чтобы это сделать, нужно добавить для каждой точки куба еще информацию: UV канал. Это координат вида (u, v). Отсюда и название, так как x и y были заняты. Она присваивается вершине объекта и обозначает точку на текстуре, которая ей соответствует. Чтобы было понятнее, если мы хотим на каждую грань куба нарисовать знакомое нам лицо, то UV координаты для каждой грани будут выглядеть предельно просто:
Модифицировать их никак не надо. Более того, считать координаты для каждой точки — тоже не нужно. Этим занимается GPU, самостоятельно интерполируя точки из вершин. Тогда достаточно просто сказать для каждой точки что-то вроде
Это очень условный пример, но примерно так в простейшем случае оно и работает:
Помимо натягивания текстур в пиксельном шейдере можно, например, получить информацию об освещенности и добавить к цвету черного пропорционально затемнению в этой точке, тогда объект будет менее плоским. Это, конечно, если просчет освещенности где-то написан, потому что сама по себе видеокарта о понятиях освещения и теней мало чего знает. Вообще делать с цветами можно что угодно, например подменять их в зависимости от игрока:
Помимо UV канала в шейдер в зависимости от его вида приходят разные данные. Они зависят от игрового движка и графической библиотеки. Обычно туда входят данные о местоположении точки, нормаль (вектор исходящий от поверхности в точке), UV канал. Но также шейдер вершин может передавать данные в пиксельный шейдер. Если это координата, то она будет проинтеполирована на основе положения фрагмента относительно вершин, между которыми он находится, как, например, UV данные.
Что такое шейдеры, зачем они нужны и как разобраться во всем этом. Краткий экскурс по рендерингу в Unity
(Осторожно! Пост является километровой портянкой)
Каждый рендер-пайплайн обладает своими свойствами, с которыми мы будем работать: свойства материалов, источники света, текстуры и все функции, которые происходят внутри шейдера, будут влиять на внешний вид и оптимизацию объектов на экране.
Итак, как же происходит этот процесс? Для этого мы должны поговорить о базовой архитектуре рендер-пайплайнов. Unity делит все на четыре этапа: прикладные функции, работа с геометрией, растеризация и обработка пикселей.
Обратите внимание, что это лишь базовая модель рендеринга в реальном времени, а каждый из этапов делится на потоки, о которых мы поговорим дальше.
Здесь же наше приложение считывает хранящиеся в памяти данные для последующей генерации наших примитивов (треугольники, вершины и пр.), а в конце этапа стадии работы приложения все это отправляется на этап обработки геометрии для работы над преобразованием вершин, используя матричные трансформации.
Когда компьютер запрашивает через CPU у нашего GPU изображения, которые мы видим на экране, это производится в два этапа:
Фаза обработки геометрии происходит на GPU и отвечает за обработку вершин нашего объекта. Эта фаза делится на четыре подпроцесса, а именно: вершинный шейдинг, проекция, клиппинг и отображение на экране.
Когда наши примитивы были успешно загружены и собраны на первом прикладном этапе, они отправляются на этап вертексного шейдинга, у которого есть две задачи:
Также во время выполнения данного этапа мы можем дополнительно выбрать свойства, которые будут нужны для следующих этапов отрисовки графики. Сюда входят нормали, тангентсы, а так же UV-координаты и другие параметры.
Проецирование и клиппинг работают как дополнительные этапы и зависят от настроек камеры на нашей сцене. Обратите внимание, что весь процесс рендеринга производится относительно Camera Frustum (поля обзора).
Проекция будет отвечать за перспективное или ортографическое отображение, а клиппинг позволяет обрезать лишнюю геометрию вне поля обзора.
Для каждого объекта на экране выполняются следующие этапы:
Далее следует последний этап, когда мы собрали все данные и готовы к выводу пикселей на экран. В этот момент запускается фрагментный шейдер (еще известный как пиксельный шейдер), который отвечает за видимость каждого пикселя. В основном он отвечает за цвет каждого пикселя для вывода на экране.
Как мы уже знаем, у Unity есть три вида пайплайнов рендеринга: Built-In, URP и HDRP. С одной стороны у нас есть Built-In (самый старый вид рендера, соответствующий всем критериям Unity), а с другой более современные, оптимизированные и гибкие пайплайны HDRP и URP (называемые Scriptable RP).
Каждый из видов рендер-пайплайна имеет свои пути для обработки графики, которые соответствуют набору операций, необходимых для прохождения от загрузки геометрии до её отрисовки на экране. Это позволяет нам графически обрабатывать освещенную сцену (например, сцену с направленным светом и ландшафтом).
Примерами путей отрисовки могут быть прямой рендеринг (forward path), отложенный шейдинг (deferred path), а также устаревшие (legacy deferred и legacy vertex lit). Каждый из них поддерживает определенные возможности, ограничения и обладает своей производительностью.
В Unity по умолчанию рендеринг производится прямым путем (forward path). Это связано с тем, что он поддерживается наибольшим количеством видео-чипов, однако обладает своими ограничениями на освещение и другие возможности.
Обратите внимание, что URP поддерживает только прямой рендеринг (forward path), в то время как HDRP обладает большим выбором и может сочетать как прямой путь рендеринга, так и отложенный.
Чтобы лучше понять эту концепцию, нам стоит рассмотреть пример, когда у нас есть некий объект и прямое освещение (directional light). То, как будут взаимодействовать эти объекты и определяет наш путь отрисовки (модель освещения).
Также на результат работы будут влиять:
Базовая модель освещения соответствует сумме трех различных свойств, таких как: ambient color, diffuse reflection и specular reflection.
Расчет освещения выполняется в шейдере, он может быть выполнен на вершину или на фрагмент. Когда освещение рассчитывается по вершинам, это называется вершинным освещением (per-vertex lighting) и выполняется на этапе вершинного шейдера, аналогично, если освещение рассчитывается по фрагментам, то оно называется per-fragment или per-pixel shader и выполняется на этапе фрагментного (пиксельного) шейдера.
Вершинное освещение работает гораздо быстрее пиксельного, однако нужно учитывать то, что для достижения красивого результата у ваших моделей должно быть большое количество полигонов.
В Unity матрицы представляют собой пространственные преобразования, и среди них мы можем найти:
Все они соответствуют матрицам четыре на четыре (4×4), то есть каждая из них имеет четыре строки и четыре столбца числовых значений. Примером матрицы может быть следующий вариант:
При помощи шейдеров мы можем получать доступ и изменять каждый из этих параметров. При работе с этими параметрами мы как правило будем использовать вектора (float4). Далее разберем каждый из параметров нашего объекта.
Вершины объекта, соответствующие набору точек, определяющих площадь поверхности в двухмерном или трехмерном пространстве. В 3D редакторах, как правило, вершины представлены как точки пересечения сетки и объекта.
Вершины характеризуются, как правило двумя моментами:
Это значит, что у каждой вершины есть свой компонент трансформаций, отвечающий за его размер, поворот и положение, а также атрибуты, которые указывают где эти вершины находятся относительно центра нашего объекта.
Нормали по своей сути помогают определить, где у нас находится лицевая сторона у фрагментов нашего объекта. Нормаль соответствует перпендикулярному вектору на поверхности полигона, который используется для определения направления или ориентации грани или вершины.
Обратившись к документации Unity, мы получим следующее описание:
Наверное, многие ребята смотрели на скины в GTA Vice City и, возможно, как и я даже пытались рисовать там что-то свое. И UV-координаты как раз таки связаны с этим. С их помощью мы можем расположить 2D текстуру на 3D объект, словно дизайнеры одежды, создавая выкройки, называемые UV-развертками.
Эти координаты действуют как опорные точки, которые управляют тем, какие тексели в текстурной карте соответствуют каждой вершине в сетке.
Помимо позиций, вращения, размера, у вершин есть еще и свои цвета. Когда мы экспортируем объект из 3D-программы, она присваивает объекту цвет, на который нужно воздействовать, либо освещением, либо копированием другого цвета.
Эта программа позволяет нам рисовать элементы (используя системы координат) на основе свойств нашего полигонального объекта. Шейдеры выполняются на GPU, поскольку он имеет параллельную архитектуру, состоящую из тысяч небольших, эффективных ядер, предназначенных для решения задач одновременно, в то время как CPU был разработан для последовательной серийной обработки.
Обратите внимание, что в Unity есть три типа файлов, связанных с шейдерами:
Во-первых, у нас есть программы с расширением «.shader», которые способны компилироваться в различные типы пайплайнов рендеринга.
Во-вторых, у нас есть программы с расширением «.shadergraph», которые могут компилироваться только в либо в URP, либо в HDRP. Кроме того, у нас есть файлы с расширением «.hlsl», которые позволяют нам создавать настраиваемые функции; обычно они используются в типе узла под названием
Custom Function, который находится в Shader Graph.
В Unity существует по крайней мере четыре типа структур, определенных для генерации шейдеров, среди которых мы можем найти комбинацию вершинного и фрагментного шейдера, шейдера поверхностей для автоматического расчета освещения и compute-шейдера для более продвинутых концепций.
Прежде чем приступить к написанию шейдеров в целом, мы должны принять во внимание, что в Unity существует есть три языка программирования шейдеров:
Мы же быстро пробежимся по Cg, ShaderLab и немного заденем HLSL. Итак.
Все шейдеры в Unity (за исключением Shader Graph и Compute) написаны на декларативном языке под названием ShaderLab. Синтаксис этого языка позволяет отображать свойства шейдера в инспекторе Unity. Это очень интересно, поскольку мы можем манипулировать значениями переменных и векторов в реальном времени, настраивая наш шейдер для получения желаемого результата.
В ShaderLab мы можем вручную определить несколько свойств и команд, среди них блок Fallback, который совместим с различными типами пайплайнов рендеринга, которые существуют в Unity.
Базовые типы шейдеров в Unity позволяют нам создавать подпрограммы, которые будут использоваться для различных целей.
Разберем, за что отвечает каждый тип:
Для анализа структуры шейдеров, достаточно создать простой шейдер на основе Unlit и проанализировать его.
Когда мы создаем шейдер в первый раз, Unity добавляет код по умолчанию, чтобы облегчить процесс компиляции. В шейдере мы можем найти блоки кода, структурированные таким образом, чтобы графический процессор мог их интерпретировать.
Если мы откроем наш шейдер, его структура будет выглядит похожим образом:
Скорее всего, посмотрев на этот код, вы не поймете, что происходит в различных его блоках. Однако, чтобы начать наше исследование, мы обратим внимание на его общую структуру.
С текущим примером и его основной структурой становится несколько понятнее. Шейдер начинается с пути в инспекторе редактора Unity (InspectorPath) и имени (shaderName), затем свойства (например.
текстуры, векторы, цвета и т.д.), затем SubShader и в конце необязательный параметр Fallback для поддержки различных вариантов.
Таким образом, мы уже понимаем что, где и зачем начать писать.
Большинство наших шейдеров, написанных в коде, начинаются с объявления шейдера и его пути в инспекторе Unity, а также его имени. Оба свойства, такие как SubShader и Fallback, записываются внутри поля «Shader» в декларативном языке ShaderLab.
И путь, и имя шейдера могут быть изменены по мере необходимости в рамках проекта.
Свойства шейдера соответствуют списку параметров, которыми можно манипулировать из инспектора Unity. Существует восемь различных свойств, как по значению, так и по полезности. Мы используем эти свойства относительно шейдера, который мы хотим создать или изменить, динамически или в рантайме. Синтаксис для объявления свойства следующий:
Вторым компонентом шейдера является Subshader. Каждый шейдер состоит как минимум из одного SubShader для идеальной загрузки. Когда имеется более одного SubShader, Unity будет обрабатывать каждый из них и выбирать наиболее подходящий в соответствии с аппаратными характеристиками, начиная с первого и заканчивая последним в списке (к примеру для того, чтобы разделить шейдер под iOS и Android). Когда SubShader не поддерживается, Unity попытается использовать компонент Fallback, соответствующий стандартному шейдеру, чтобы аппаратное обеспечение могло продолжить выполнение своей задачи без графических ошибок.
Прочитать подробнее про параметры и сабшейдеры можно здесь и здесь.
Блендинг нужен нам для процесса смешивания двух пикселей в один. Блендинг поддерживается как в Built-In, так и SRP.
Блендинг происходит на этапе, который объединяет конечный цвет пикселя с его глубиной. Этот этап, который происходит в конце пайплайна рендеринга, после этапа фрагментного (пиксельного) шейдера, при выполнении
stencil-буфера, z-буфера и смешивания цветов.
По умолчанию это свойство не записано в шейдере, так как это необязательная функция и используется в основном при работе с прозрачными объектами, например, когда мы должны нарисовать пиксель с
низким уровнем непрозрачности перед другим (часто такое используется в UI).
Мы можем включить смешивание здесь:
Чтобы понять обе концепции, мы должны сначала узнать, как работают Z-буфер (также известный как Depth Buffer) и тест глубины.
Перед началом работы мы должны учесть, что пиксели имеют значения глубины. Эти значения хранятся в буфере глубины, который определяет, идет ли объект перед или за другим объектом на экране.
Как мы уже знаем, пиксель имеет назначенное значение, которое измеряется в цвете RGB и хранится в буфере цвета. Z-буфер добавляет дополнительное значение, которое измеряет глубину пикселя с точки зрения расстояния до камеры, но только для тех поверхностей, которые находятся в пределах его фронтальной области. Это позволяет двум пикселям быть одинаковыми по цвету, но разными по глубине.
Чем ближе объект к камере, тем меньше значение Z-буфера, и пиксели с меньшими значениями буфера перезаписывают пиксели с большими значениями.
Чтобы понять концепцию, предположим, что у нас есть камера и некоторые примитивы в нашей сцене, и все они расположены на оси пространства «Z».
Слово «буфер» относится к «пространству памяти», в котором данные будут временно храниться, поэтому Z-буфер относится к значениям глубины между объектами в нашей сцене и камерой, которые присваиваются каждому пикселю.
Мы можем управлять Depth-тестом, благодаря параметрам ZTest в Unity.
Это свойство, совместимое как c Built-In RP, так и в URP/HDRP, управляет тем, какая из граней многоугольника будет удалена при обработке глубины пикселя.
Что это значит? Вспомните, что многоугольный объект имеет внутренние грани и внешние. По умолчанию внешние грани видны (CullBack);
Однако мы можем активировать внутренние грани:
Эта команда имеет три значения, а именно: Back, Front и Off. По умолчанию активна команда «Back», однако, как правило, строка кода, связанная с culling, не видна в шейдере в целях оптимизации. Если мы хотим изменить параметры, мы должны добавить слово «Cull» после которого следует режим, который мы хотим использовать.
Мы также можем динамически настраивать параметры Culling в инспекторе Unity через зависимость «UnityEngine.Rendering.CullMode», которая является Enum и передается в качестве аргумента в функции.
В нашем шейдере мы можем найти как минимум три варианта директив по умолчанию. Это директивы процессора и включаются в Cg или HLSL. Их функция заключается в том, чтобы помочь нашему шейдеру распознать и скомпилировать определенные функции, которые иначе не могут быть распознаны как таковые.
Помимо этого мы можем подключать файлы Cg / HLSL в наш шейдер. Как правило мы делаем это, когда подключаем UnityCG.cginc, который в свою очередь включает в себя координаты тумана, позиции объекта для клиппинга, трансформации текстур, перенос и принятие тумана и многое другое, включая константы UNITY_PI.
Подробнее о Cg / HLSL можно почитать здесь.
Мы должны учитывать, что при работе с Shader Graph версии, разработанные для Unity 2018 являются BETA-версиями и не получают поддержки, в то время как версии, разработанные для Unity 2019.1+ являются активно совместимыми и получают поддержку.
Еще одна проблема заключается в том, что весьма вероятно, что шейдеры, созданные с помощью этого интерфейса, могут не компилироваться правильно в различных версиях. Это происходит потому, что новые функции добавляются в каждом обновлении.
Итак, является ли Shader Graph хорошим инструментом для разработки шейдеров? Конечно. И с ним может разобраться не только графический программист, но и технический дизайнер или художник.
Чтобы создать граф, достаточно выбрать нужный нам тип в редакторе Unity:
Что такое шейдеры, зачем они нужны и как разобраться во всем этом. Краткий экскурс по рендерингу в Unity
Статья является лишь кратким обзором-шпаргалкой. И в ней обрезаны некоторые моменты, поскольку чтобы полностью расписать все процессы, наверное придется написать целую книгу 😀
Как работает рендеринг в Unity?
Каждый рендер-пайплайн обладает своими свойствами, с которыми мы будем работать: свойства материалов, источники света, текстуры и все функции, которые происходят внутри шейдера, будут влиять на внешний вид и оптимизацию объектов на экране.
Итак, как же происходит этот процесс? Для этого мы должны поговорить о базовой архитектуре рендер-пайплайнов. Unity делит все на четыре этапа: прикладные функции, работа с геометрией, растеризация и обработка пикселей.
Обратите внимание, что это лишь базовая модель рендеринга в реальном времени, а каждый из этапов делится на потоки, о которых мы поговорим дальше.
Прикладные функции
Обработка физики и просчет столкновений;
Ввод с клавиатуры и мыши;
Здесь же наше приложение считывает хранящиеся в памяти данные для последующей генерации наших примитивов (треугольники, вершины и пр.), а в конце этапа стадии работы приложения все это отправляется на этап обработки геометрии для работы над преобразованием вершин, используя матричные трансформации.
Процессинг геометрии
Когда компьютер запрашивает через CPU у нашего GPU изображения, которые мы видим на экране, это производится в два этапа:
Когда состояние рендера настроено и пройдены этапы от обработки геометрии до обработки пикселей;
Когда объект отрисовывается на экране;
Фаза обработки геометрии происходит на GPU и отвечает за обработку вершин нашего объекта. Эта фаза делится на четыре подпроцесса, а именно: вершинный шейдинг, проекция, клиппинг и отображение на экране.
Когда наши примитивы были успешно загружены и собраны на первом прикладном этапе, они отправляются на этап вертексного шейдинга, у которого есть две задачи:
Просчитать позицию вершин у объекта;
Преобразовать положение в другие пространственные координаты (с локальных на мировые, как пример), для того чтобы их можно было отрисовать на экране;
Также во время выполнения данного этапа мы можем дополнительно выбрать свойства, которые будут нужны для следующих этапов отрисовки графики. Сюда входят нормали, тангентсы, а так же UV-координаты и другие параметры.
Проецирование и клиппинг работают как дополнительные этапы и зависят от настроек камеры на нашей сцене. Обратите внимание, что весь процесс рендеринга производится относительно Camera Frustum (поля обзора).
Проекция будет отвечать за перспективное или ортографическое отображение, а клиппинг позволяет обрезать лишнюю геометрию вне поля обзора.
Растеризация и работа с пикселями
Для каждого объекта на экране выполняются следующие этапы:
Далее следует последний этап, когда мы собрали все данные и готовы к выводу пикселей на экран. В этот момент запускается фрагментный шейдер (еще известный как пиксельный шейдер), который отвечает за видимость каждого пикселя. В основном он отвечает за цвет каждого пикселя для вывода на экране.
Forward и Deferred шейдинг
Как мы уже знаем, у Unity есть три вида пайплайнов рендеринга: Built-In, URP и HDRP. С одной стороны у нас есть Built-In (самый старый вид рендера, соответствующий всем критериям Unity), а с другой более современные, оптимизированные и гибкие пайплайны HDRP и URP (называемые Scriptable RP).
Каждый из видов рендер-пайплайна имеет свои пути для обработки графики, которые соответствуют набору операций, необходимых для прохождения от загрузки геометрии до её отрисовки на экране. Это позволяет нам графически обрабатывать освещенную сцену (например, сцену с направленным светом и ландшафтом).
Примерами путей отрисовки могут быть прямой рендеринг (forward path), отложенный шейдинг (deferred path), а также устаревшие (legacy deferred и legacy vertex lit). Каждый из них поддерживает определенные возможности, ограничения и обладает своей производительностью.
В Unity по умолчанию рендеринг производится прямым путем (forward path). Это связано с тем, что он поддерживается наибольшим количеством видео-чипов, однако обладает своими ограничениями на освещение и другие возможности.
Обратите внимание, что URP поддерживает только прямой рендеринг (forward path), в то время как HDRP обладает большим выбором и может сочетать как прямой путь рендеринга, так и отложенный.
Чтобы лучше понять эту концепцию, нам стоит рассмотреть пример, когда у нас есть некий объект и прямое освещение (directional light). То, как будут взаимодействовать эти объекты и определяет наш путь отрисовки (модель освещения).
Также на результат работы будут влиять:
Характеристики источников освещения;
Базовая модель освещения соответствует сумме трех различных свойств, таких как: ambient color, diffuse reflection и specular reflection.
Расчет освещения выполняется в шейдере, он может быть выполнен на вершину или на фрагмент. Когда освещение рассчитывается по вершинам, это называется вершинным освещением (per-vertex lighting) и выполняется на этапе вершинного шейдера, аналогично, если освещение рассчитывается по фрагментам, то оно называется per-fragment или per-pixel shader и выполняется на этапе фрагментного (пиксельного) шейдера.
Вершинное освещение работает гораздо быстрее пиксельного, однако нужно учитывать то, что для достижения красивого результата у ваших моделей должно быть большое количество полигонов.
Матрицы в Unity
В Unity матрицы представляют собой пространственные преобразования, и среди них мы можем найти:
Все они соответствуют матрицам четыре на четыре (4×4), то есть каждая из них имеет четыре строки и четыре столбца числовых значений. Примером матрицы может быть следующий вариант:
Свойства полигональных объектов
При помощи шейдеров мы можем получать доступ и изменять каждый из этих параметров. При работе с этими параметрами мы как правило будем использовать вектора (float4). Далее разберем каждый из параметров нашего объекта.
Подробнее про вершины
Вершины объекта, соответствующие набору точек, определяющих площадь поверхности в двухмерном или трехмерном пространстве. В 3D редакторах, как правило, вершины представлены как точки пересечения сетки и объекта.
Вершины характеризуются, как правило двумя моментами:
Они являются дочерними компонентами компонента transform;
Они имеют определенное положение в соответствии с центром общего объекта в локальном пространстве.
Это значит, что у каждой вершины есть свой компонент трансформаций, отвечающий за его размер, поворот и положение, а также атрибуты, которые указывают где эти вершины находятся относительно центра нашего объекта.
Нормали у объектов
Нормали по своей сути помогают определить, где у нас находится лицевая сторона у фрагментов нашего объекта. Нормаль соответствует перпендикулярному вектору на поверхности полигона, который используется для определения направления или ориентации грани или вершины.
Тангенсы
Обратившись к документации Unity, мы получим следующее описание:
UV-координаты
Наверное, многие ребята смотрели на скины в GTA Vice City и, возможно, как и я даже пытались рисовать там что-то свое. И UV-координаты как раз таки связаны с этим. С их помощью мы можем расположить 2D текстуру на 3D объект, словно дизайнеры одежды, создавая выкройки, называемые UV-развертками.
Эти координаты действуют как опорные точки, которые управляют тем, какие тексели в текстурной карте соответствуют каждой вершине в сетке.
Цвета вершин
Помимо позиций, вращения, размера, у вершин есть еще и свои цвета. Когда мы экспортируем объект из 3D-программы, она присваивает объекту цвет, на который нужно воздействовать, либо освещением, либо копированием другого цвета.
Что же такое шейдер?
Эта программа позволяет нам рисовать элементы (используя системы координат) на основе свойств нашего полигонального объекта. Шейдеры выполняются на GPU, поскольку он имеет параллельную архитектуру, состоящую из тысяч небольших, эффективных ядер, предназначенных для решения задач одновременно, в то время как CPU был разработан для последовательной серийной обработки.
Обратите внимание, что в Unity есть три типа файлов, связанных с шейдерами:
Во-первых, у нас есть программы с расширением «.shader», которые способны компилироваться в различные типы пайплайнов рендеринга.
Во-вторых, у нас есть программы с расширением «.shadergraph», которые могут компилироваться только в либо в URP, либо в HDRP. Кроме того, у нас есть файлы с расширением «.hlsl», которые позволяют нам создавать настраиваемые функции; обычно они используются в типе узла под названием
Custom Function, который находится в Shader Graph.
В Unity существует по крайней мере четыре типа структур, определенных для генерации шейдеров, среди которых мы можем найти комбинацию вершинного и фрагментного шейдера, шейдера поверхностей для автоматического расчета освещения и compute-шейдера для более продвинутых концепций.
Небольшой экскурс в язык шейдеров
Прежде чем приступить к написанию шейдеров в целом, мы должны принять во внимание, что в Unity существует есть три языка программирования шейдеров:
Мы же быстро пробежимся по Cg, ShaderLab и немного заденем HLSL. Итак.
Все шейдеры в Unity (за исключением Shader Graph и Compute) написаны на декларативном языке под названием ShaderLab. Синтаксис этого языка позволяет отображать свойства шейдера в инспекторе Unity. Это очень интересно, поскольку мы можем манипулировать значениями переменных и векторов в реальном времени, настраивая наш шейдер для получения желаемого результата.
В ShaderLab мы можем вручную определить несколько свойств и команд, среди них блок Fallback, который совместим с различными типами пайплайнов рендеринга, которые существуют в Unity.
Базовые типы шейдеров в Unity
Базовые типы шейдеров в Unity позволяют нам создавать подпрограммы, которые будут использоваться для различных целей.
Разберем, за что отвечает каждый тип:
Структура шейдеров
Для анализа структуры шейдеров, достаточно создать простой шейдер на основе Unlit и проанализировать его.
Когда мы создаем шейдер в первый раз, Unity добавляет код по умолчанию, чтобы облегчить процесс компиляции. В шейдере мы можем найти блоки кода, структурированные таким образом, чтобы графический процессор мог их интерпретировать.
Если мы откроем наш шейдер, его структура будет выглядит похожим образом:
Скорее всего, посмотрев на этот код, вы не поймете, что происходит в различных его блоках. Однако, чтобы начать наше исследование, мы обратим внимание на его общую структуру.
С текущим примером и его основной структурой становится несколько понятнее. Шейдер начинается с пути в инспекторе редактора Unity (InspectorPath) и имени (shaderName), затем свойства (например.
текстуры, векторы, цвета и т.д.), затем SubShader и в конце необязательный параметр Fallback для поддержки различных вариантов.
Таким образом, мы уже понимаем что, где и зачем начать писать.
Работа с ShaderLab
Большинство наших шейдеров, написанных в коде, начинаются с объявления шейдера и его пути в инспекторе Unity, а также его имени. Оба свойства, такие как SubShader и Fallback, записываются внутри поля «Shader» в декларативном языке ShaderLab.
И путь, и имя шейдера могут быть изменены по мере необходимости в рамках проекта.
Свойства шейдера соответствуют списку параметров, которыми можно манипулировать из инспектора Unity. Существует восемь различных свойств, как по значению, так и по полезности. Мы используем эти свойства относительно шейдера, который мы хотим создать или изменить, динамически или в рантайме. Синтаксис для объявления свойства следующий:
Вторым компонентом шейдера является Subshader. Каждый шейдер состоит как минимум из одного SubShader для идеальной загрузки. Когда имеется более одного SubShader, Unity будет обрабатывать каждый из них и выбирать наиболее подходящий в соответствии с аппаратными характеристиками, начиная с первого и заканчивая последним в списке (к примеру для того, чтобы разделить шейдер под iOS и Android). Когда SubShader не поддерживается, Unity попытается использовать компонент Fallback, соответствующий стандартному шейдеру, чтобы аппаратное обеспечение могло продолжить выполнение своей задачи без графических ошибок.
Прочитать подробнее про параметры и сабшейдеры можно здесь и здесь.
Блендинг
Блендинг нужен нам для процесса смешивания двух пикселей в один. Блендинг поддерживается как в Built-In, так и SRP.
Блендинг происходит на этапе, который объединяет конечный цвет пикселя с его глубиной. Этот этап, который происходит в конце пайплайна рендеринга, после этапа фрагментного (пиксельного) шейдера, при выполнении
stencil-буфера, z-буфера и смешивания цветов.
По умолчанию это свойство не записано в шейдере, так как это необязательная функция и используется в основном при работе с прозрачными объектами, например, когда мы должны нарисовать пиксель с
низким уровнем непрозрачности перед другим (часто такое используется в UI).
Мы можем включить смешивание здесь:
Z-Buffer и тест глубины
Чтобы понять обе концепции, мы должны сначала узнать, как работают Z-буфер (также известный как Depth Buffer) и тест глубины.
Перед началом работы мы должны учесть, что пиксели имеют значения глубины. Эти значения хранятся в буфере глубины, который определяет, идет ли объект перед или за другим объектом на экране.
Как мы уже знаем, пиксель имеет назначенное значение, которое измеряется в цвете RGB и хранится в буфере цвета. Z-буфер добавляет дополнительное значение, которое измеряет глубину пикселя с точки зрения расстояния до камеры, но только для тех поверхностей, которые находятся в пределах его фронтальной области. Это позволяет двум пикселям быть одинаковыми по цвету, но разными по глубине.
Чем ближе объект к камере, тем меньше значение Z-буфера, и пиксели с меньшими значениями буфера перезаписывают пиксели с большими значениями.
Чтобы понять концепцию, предположим, что у нас есть камера и некоторые примитивы в нашей сцене, и все они расположены на оси пространства «Z».
Слово «буфер» относится к «пространству памяти», в котором данные будут временно храниться, поэтому Z-буфер относится к значениям глубины между объектами в нашей сцене и камерой, которые присваиваются каждому пикселю.
Мы можем управлять Depth-тестом, благодаря параметрам ZTest в Unity.
Culling
Это свойство, совместимое как c Built-In RP, так и в URP/HDRP, управляет тем, какая из граней многоугольника будет удалена при обработке глубины пикселя.
Что это значит? Вспомните, что многоугольный объект имеет внутренние грани и внешние. По умолчанию внешние грани видны (CullBack);
Однако мы можем активировать внутренние грани:
Эта команда имеет три значения, а именно: Back, Front и Off. По умолчанию активна команда «Back», однако, как правило, строка кода, связанная с culling, не видна в шейдере в целях оптимизации. Если мы хотим изменить параметры, мы должны добавить слово «Cull» после которого следует режим, который мы хотим использовать.
Мы также можем динамически настраивать параметры Culling в инспекторе Unity через зависимость «UnityEngine.Rendering.CullMode», которая является Enum и передается в качестве аргумента в функции.
Использование Cg / HLSL
В нашем шейдере мы можем найти как минимум три варианта директив по умолчанию. Это директивы процессора и включаются в Cg или HLSL. Их функция заключается в том, чтобы помочь нашему шейдеру распознать и скомпилировать определенные функции, которые иначе не могут быть распознаны как таковые.
Помимо этого мы можем подключать файлы Cg / HLSL в наш шейдер. Как правило мы делаем это, когда подключаем UnityCG.cginc, который в свою очередь включает в себя координаты тумана, позиции объекта для клиппинга, трансформации текстур, перенос и принятие тумана и многое другое, включая константы UNITY_PI.
Подробнее о Cg / HLSL можно почитать здесь.
Shader Graph
Мы должны учитывать, что при работе с Shader Graph версии, разработанные для Unity 2018 являются BETA-версиями и не получают поддержки, в то время как версии, разработанные для Unity 2019.1+ являются активно совместимыми и получают поддержку.
Еще одна проблема заключается в том, что весьма вероятно, что шейдеры, созданные с помощью этого интерфейса, могут не компилироваться правильно в различных версиях. Это происходит потому, что новые функции добавляются в каждом обновлении.
Итак, является ли Shader Graph хорошим инструментом для разработки шейдеров? Конечно. И с ним может разобраться не только графический программист, но и технический дизайнер или художник.
Чтобы создать граф, достаточно выбрать нужный нам тип в редакторе Unity:
Перед началом работы, сделаем небольшое введение в вершинный/фрагментный шейдер на уровне Shader Graph.
Как мы видим, на этапе вершинного шейдера есть три определенных точки входа, а именно: Position(3), Normal(3), Tangent(3), как в шейдере Cg или HLSL. Если сравнивать с обычным шейдером, то это означает, что Position(3) = POSITION[n], Normal(3) = NORMAL[n] и Tangent(3) = TANGENT[n].
Вспомним, что четвертое измерение вектора соответствует его компоненту W, который в большинстве случаев равен «единице или нулю». Когда W = 1, это означает, что вектор соответствует положению в пространстве или точке. В то время как, когда W = 0, вектор соответствует направлению в пространстве.
Чтобы создать связь между свойствами ShaderLab и нашей программой, мы должны создать переменные в поле CGPROGRAM. Однако, в Shader Graph этот процесс происходит иначе. Мы должны перетащить свойства, которые мы хотим использовать в область работы с нодами.
Все, что нам нужно сделать, чтобы текстура типа Texture2D работала в сочетании с нодой Sample Texture 2D, это соединить выход свойства _MainTex с входом типа Texture(T2).
Также мы можем обратиться к общей настройке графа, разделенной на две секции под названием Node и Graph, которые обладают настраиваемыми свойствами, позволяющими изменять цветопередачу. Мы можем найти опции блендинга, обводки, и альфа-клиппинга и др. Кроме того, мы можем настроить свойства нодов в нашей конфигурации Shader Graph.
Сами же ноды предоставляют аналоги тех или иных функций, которые мы пишем в ShaderLab. Как пример, код функции Clamp:
Превратиться в ноду:
Таким образом мы можем упростить себе жизнь и уменьшить время написание шейдеров за счет визуальных графов.
Итоги
Создание шейдеров
Освоить создание графических шейдеров — это значит взять под свой контроль всю мощь видепроцессора с его тысячами параллельно работающих ядер. При таком способе программирования требуется другой образ мышления, но раскрытие его потенциала стоит потраченных усилий.
Практически в любой современной графической симуляции используется код, написанный для видеопроцессора: от реалистичных эффектов освещения в высокотехнологичных AAA-играх до двухмерных эффектов постпроцессинга и симуляции жидкостей.
Сцена из Minecraft, до и после добавления нескольких шейдеров.
Задача этого туториала
Иногда программирование шейдеров представляется загадочной чёрной магией и его часто понимают неправильно. Существует множество примеров кода, демонстрирующих создание невероятных эффектов, но в которых практически нет объяснений. В своём руководстве я хочу восполнить этот пробел. Я сосредоточусь в основном на базовых принципах создания и понимания кода шейдеров, чтобы вы смогли с лёгкостью настраивать, сочетать или писать свои собственные шейдеры с нуля!
Это общее руководство, поэтому всё изученное в нём можно применять в любой технологии, использующей шейдеры.
Что же такое шейдер?
Шейдер — это просто программа, выполняемая в графическом конвейере. Она сообщает компьютеру, как рендерить каждый пиксель. Эти программы называются шейдерами («затенителями»), потому что их часто используют для управления эффектами освещения и затенения, но ничего не мешает использовать их и для других спецэффектов.
Шейдеры пишут на специальном языке шейдеров. Не волнуйтесь, вам не придётся изучать совершенно новый язык: мы будем использовать GLSL (OpenGL Shading Language), который похож на C. (Существует несколько языков написания шейдеров для разных платформ, но поскольку все они адаптированы под выполнение в видеопроцессоре, то похожи друг на друга.)
Примечание: эта статья посвящена исключительно фрагментным шейдерам (fragment shader). Если вам любопытно, какие ещё виды шейдеров бывают, то можно почитать о различных этапах графического конвейера в OpenGL Wiki.
Приступаем!
В этом туториале мы будем использовать ShaderToy. Он позволяет нам начать программировать шейдеры прямо в браузере, без возни с установкой и настройкой! (Для рендеринга он использует WebGL, поэтому требуется браузер с поддержкой этой технологии.) Создавать учётную запись не обязательно, но удобно для сохранения кода.
Примечание: на момент написания статьи ShaderToy находился в состоянии беты [прим. пер.: статья написана в 2015 году]. Некоторые детали интерфейса/синтаксиса могут немного отличаться.
Нажав на New Shader, вы увидите что-то вроде этого:
Если вы не зарегистрировались, интерфейс может немного отличаться.
Маленькая чёрная стрелка внизу служит для компиляции кода.
Что происходит?
Я собираюсь описать то, как работают шейдеры, одним предложением. Приготовились? Вот оно!
Именно это, и только это может делать шейдер. Функция выполняется для каждого пикселя на экране. Она возвращает эти четыре цветовых значения, которые становятся цветом пикселя. Это то, что называется пиксельным шейдером (иногда его называют фрагментным шейдером).
Поздравляю! Вот ваш первый готовый шейдер!
Задача: Попробуйте изменить цвет на сплошной серый.
vec4 — это просто тип данных, поэтому мы могли бы объявить цвет как переменную, вот так:
Выглядит не очень вдохновляюще. У нас есть мощь, способная выполнять код для сотен тысяч пикселей одновременно, а мы закрашиваем их одним цветом.
Давайте попробуем отрендерить на экране градиент. Мы не сможем добиться многого, не зная пары вещей о пикселях, на которые мы влияем, например, об их положении на экране…
Входные данные шейдера
Вы заметили проблему в приведённом выше коде? Попробуйте нажать на кнопку go fullscreen в правом нижнем углу окна предварительного просмотра.
Размеры красной части экрана будут разными и зависят от размера экрана. Чтобы закрасить красным ровно половину экрана, нам нужно знать его размер. Размер экрана не является встроенной переменной, как позиция пикселя, потому что обычно он выбирается программистом, создающим приложение. В нашем случае размер экрана устанавливают разработчики ShaderToy.
Если что-то не является встроенной переменной, то можно отправить эту информацию из центрального процессора (из основной программы) в видеопроцессор (в ваш шейдер). ShaderToy берёт эту задачу на себя. Все передаваемые шейдеру переменные указаны во вкладке Shader Inputs. Переменные, передаваемые таким образом из ЦП в видеопроцессор, называются в GLSL uniform (глобальными).
Давайте изменим код, чтобы правильно определять центр экрана. Нам нужно использовать входные данные шейдера iResolution :
Если попробовать теперь изменить размер окна предварительного просмотра, то цвета должны точно разделить экран пополам.
От разделения к градиенту
Задача: Можете превратить эту картинку в вертикальный градиент? А как насчёт диагонального? Как насчёт градиента из нескольких цветов?
Отрисовка изображений
Экспериментировать с цветами интересно, но если мы хотим сделать что-то впечатляющее, то наш шейдер должен научиться получать в качестве входных данных изображение и изменять его. Таким образом можно получить шейдер, воздействующий на весь игровой экран (например, «подводный» эффект или цветокоррекцию), или воздействовать нужным способом только на определённые объекты (например, для создания реалистичной системы освещения).
Если бы мы программировали на обычной платформе, то нужно было бы передавать видеопроцессору изображение (или текстуру) как uniform, точно так же, как мы бы передавали разрешение экрана. ShaderToy позаботится об этом за нас. В нижней части экрана есть четыре входных канала:
Четыре входных канала ShaderToy
Нажмите на iChannel0 и выберите любую текстуру (изображение).
Если мы можем только возвращать цвет, то как можно нарисовать текстуру на экране? Нам нужно каким-то образом привязать текущий пиксель шейдера к соответствующему пикселю текстуры:
В зависимости от расположения (0,0) на экране может потребоваться отразить ось Y для правильной привязки текстуры. На момент написания статьи ShaderToy был обновлён и исходная точка находится в левом верхнем углу, так что ничего отражать не требуется.
Вы можете привязывать координаты к экрану как вам угодно. Можно отрисовать всю текстуру на четверти экрана (пропуская пиксели, то есть уменьшив её масштаб) или просто отрисовать часть текстуры.
Мы хотим только увидеть изображение, поэтому пиксели будут совпадать в масштабе 1:1:
Так мы получили своё первое изображение!
Теперь, научившись правильно получать данные из текстуры, мы можем делать с ними всё, что угодно! Мы можем растягивать или масштабировать их, экспериментировать с цветами.
Давайте попробуем наложить на изображение градиент, похожий на то, что мы делали раньше:
Поздравляю, вы только что написали свой первый эффект постпроцессинга!
Задача: Сможете написать шейдер, делающий изображение чёрно-белым?
Учтите, что хотя мы и выбрали статичное изображение, всё, что вы видите, выполняется в реальном времени. Чтобы убедиться в этом, замените статичное изображение на видео: нажмите на iChannel0 и выберите любое видео.
Добавляем движение
Пока что все наши эффекты были статичными. Мы можем делать гораздо более интересные вещи, воспользовавшись входными данными, предоставляемыми ShaderToy. iGlobalTime — это постоянно увеличивающаяся переменная. Мы можем использовать её как начальное число для создания периодических эффектов. Давайте попробуем немного поиграть с цветами:
Задача: Можете ли вы сделать шейдер, меняющий изображение с чёрно-белого на цветное и обратно?
Примечание об отладке шейдеров
При отладке кода можно выполнять его пошагово или выводить на печать значения, но при написании шейдеров это невозможно. Можете поискать инструменты отладки для своей платформы, но в общем случае лучше всего привязать проверяемое значение к какой-то графической информации, которую можно увидеть.
Подводим итог
Это только самые основы работы с шейдерами, но освоившись с ними, вы сможете добиться гораздо большего. Посмотрите эффекты ShaderToy и проверьте, сможете ли вы понять или воспроизвести их!
В этом туториале я не упоминал вершинные шейдеры (Vertex Shaders). Они пишутся на том же языке, но выполняются для каждой вершины, а не пикселя, и возвращают вместе с цветом ещё и положение. Вершинные шейдеры обычно проецируют 3D-сцену на экран (эта функция встроена в большинство графических конвейеров). Пиксельные шейдеры обеспечивают большинство наблюдаемых на экране сложных эффектов, поэтому я рассматриваю их.
Ещё одна задача: Сможете ли вы написать шейдер, удаляющий зелёный цвет из видео с ShaderToy и добавляющий другое видео в качестве фона первого видео?
ShaderToy, с которым мы работали ранее, отлично подходит для быстрой проверки и экспериментов, но его возможности довольно ограничены. Например, вы не можете контролировать данные, передаваемые шейдеру. Если у нас будет собственная среда для запуска шейдеров, то мы сможем создавать всевозможные интересные эффекты и использовать их в собственных проектах! Для запуска шейдеров в браузере мы будем использовать в качестве фреймворка Three.js. WebGL — это Javascript API, позволяющий рендерить шейдеры, Three.js нужен только для упрощения работы.
Если вам не интересны JavaScript или веб-платформа, не волнуйтесь, мы не будем вдаваться в особенности веб-рендеринга (но если вы хотите узнать о фреймворке больше, то изучите этот туториал). Настройка шейдеров в браузере — самый быстрый способ начать работу, но освоившись с этим процессом, вы сможете с лёгкостью настраивать и использовать шейдеры на любой платформе.
Настройка
В этом разделе мы рассмотрим локальную настройку шейдеров. Вы можете повторять за мной без необходимости скачивать что-то благодаря этому встроенному виджету CodePen:
Hello Three.js!
Three.js — это JavaScript-фреймворк, который берёт на себя большую часть boilerplate-кода для WebGL, необходимого для рендеринга шейдеров. Проще всего начать с использования версии, выложенной на CDN.
Можете скачать файл HTML, который является простой сценой Threejs.
Сохраните файл на диск и откройте его в браузере. Вы должны увидеть чёрный экран. Пока не очень интересно, так что давайте попробуем добавить куб, просто чтобы проверить, что всё работает.
Для создания куба нам нужно определить его геометрию и материал, а затем добавить его в сцену. Добавьте этот фрагмент кода в поле Add your code here :
Мы не будем подробно рассматривать куб, потому что нам больше интересен шейдер. Но если всё сделано верно, то вы увидите в центре экрана зелёный куб:
Задача: Сможете повращать куб по другой оси? А как насчёт двух осей одновременно?
Итак, всё готово, пора добавить шейдеры!
Добавляем шейдеры
На этом этапе мы можем начать думать о процессе реализации шейдеров. Скорее всего, похожая ситуация будет у вас вне зависимости от платформы: всё настроено, на экране отрисовываются объекты, так как теперь получить доступ к видеопроцессору?
Шаг 1: загрузка кода GLSL
Для создания сцены мы используем JavaScript. В других ситуациях может использоваться C++, Lua или любой другой язык. Вне зависимости от этого шейдеры пишутся на специальном языке шейдеров. Язык шейдеров OpenGL называется GLSL (OpenGL Shading Language). Поскольку мы используем WebGL, основанный на OpenGL, то будем писать на GLSL.
В JavaScript это можно сделать простой передачей кода внутри переменной, вот так:
Это возможно, но мы в этом туториале мы будем писать код шейдеров внутри нового тега
Теперь давайте вставим очень простой шейдер, возвращающий белый цвет.
(Компоненты vec4 в этом случае соответствуют значению RGBA, ка объяснялось в начале туториала.)
Теперь нам нужно загрузить этот код. Мы сделаем это простой строкой на JavaScript, находящей HTML-элемент и получающей внутренний текст:
Он должен находится под кодом куба.
Не забывайте: только загруженный как строка код будет парситься как действительный код GLSL (то есть void main() <. >. Остальное — это просто boilerplate HTML.)
Шаг 2: применение шейдера
Способы применения шейдера могут отличаться, они зависят от используемой платформы и её интерфейса с видеопроцессором. С этим никогда не возникает трудностей, и быстрым поиском можно выяснить, как создать объект и применить к нему шейдеры с помощью Three.js.
Нам нужно создать специальный материал и передать его коду шейдера. В качестве объекта шейдера мы создадим плоскость (но можем использовать и куб). Вот что нужно сделать:
На этом этапе вы должны видеть белый экран:
Если заменить цвет в коде шейдера на любой другой и обновить страницу, то вы увидите новый цвет.
Задача: Сможете ли вы сделать одну часть экрана красной, а другую синей? (Если не получится, то на следующем шаге я дам подсказку!)
Шаг 3: передача данных
На этот момент мы уже можем делать с шейдером всё, что угодно, но пока не знаем, что можно сделать. У нас есть только встроенная возможность определения положения пикселя gl_FragCoord и, если вы помните, это положение нормализовано. Нам как минимум нужно знать размеры экрана.
Для передачи данных шейдеру нам нужно отправить то, что мы назвали uniform-переменной. Для этого мы создаём объект uniforms и добавляем к нему наши переменные. Вот синтаксис передачи разрешения экрана:
Строка типа Uniform | Тип GLSL | Тип JavaScript |
---|---|---|
‘i’, ‘1i’ | int | Number |
‘f’, ‘1f’ | float | Number |
‘v2’ | vec2 | THREE.Vector2 |
‘v3’ | vec3 | THREE.Vector3 |
‘c’ | vec3 | THREE.Color |
‘v4’ | vec4 | THREE.Vector4 |
‘m3’ | mat3 | THREE.Matrix3 |
‘m4’ | mat4 | THREE.Matrix4 |
‘t’ | sampler2D | THREE.Texture |
‘t’ | samplerCube | THREE.CubeTexture |
Мы ещё не закончили! Теперь, когда шейдер получает эту переменную, нам нужно с ней что-то сделать. Давайте создадим градиент, как мы делали это раньше: нормализовав координату и используя её для создания значения цвета.
Изменим код шейдера следующим образом:
В результате у нас получится красивый градиент!
Задача: Попробуйте разделить экран на четыре равных части разных цветов. Примерно вот так:
Шаг 4: обновление данных
Хорошо, что мы научились отправлять данные в шейдер, но что если нам нужно их обновить? Например, если открыть предыдущий пример в новой вкладке, а потом изменить размер окна, то градиент не обновится, потому что использует прежние размеры экрана.
Вот как выглядит функция рендеринга после внесения изменений:
Если открыть новый CodePen и изменить размер окна, то вы увидите, как изменяются цвета, несмотря на то, что изначальный размер окна просмотра остался тем же). Проще всего это заметить, посмотрев на цвета в углах и убедившись, что они не меняются.
Примечание: Отправка данных в видеопроцессор обычно является затратной задачей. Отправка нескольких переменных за один кадр вполне нормальна, но при передаче сотен переменных за кадр частота смены кадров значительно снизится. Кажется, что такое маловероятно, но если на экране есть несколько сотен объектов, и ко всем ним применено освещение с разными свойствами, то всё быстро выходит из под контроля. Позже мы узнаем про оптимизацию шейдеров.
Задача: Попробуйте постепенно изменять цвета.
Шаг 5: работа с текстурами
Вне зависимости от способа загрузки и формата текстур на всех платформах они передаются в шейдер как uniform-переменные.
Небольшое примечание о загрузке файлов в JavaScript: можно без проблем загружать изображения с внешнего URL (именно так мы и будем делать). Однако если вы захотите загрузить изображение локально, то возникнут проблемы с разрешениями, потому что JavaScript обычно не может и не должен иметь доступа к файлам в системе. Простейший способ решения — запустить локальный Python-сервер, что на самом деле проще, чем кажется.
В Three.js есть небольшая удобная функция для загрузки изображения как текстуры:
Первая строка задаётся только один раз. В неё можно вставить любой URL изображения.
И, наконец, нам нужно объявить uniform-переменную в коде шейдера, а потом отрисовать её тем же способом, как мы делали ранее — с помощью функции texture2D :
На экране должно появиться растянутое изображение конфет:
(Это изображение является стандартным тестовым изображением в компьютерной графике, оно взято у Института обработки сигналов и изображений (SIPI) (поэтому на нём показана аббревиатура IPI) Университета Южной Калифорнии. Думаю, оно нам подходит, ведь мы как раз изучаем графические шейдеры!)
Задача: Попробуйте постепенно менять цвета текстуры с полного цвета на градации серого.
Дополнительный шаг: применяем шейдеры к другим объектам
В созданной нами плоскости нет ничего особенного. Мы могли применить шейдер и к кубу. На самом деле можно просто заменить строку с геометрией плоскости:
Вуаля, конфеты нарисованы на кубе:
Вы можете возразить: «Постойте, но ведь это не похоже на правильную проекцию текстуры на куб!» Вы правы: если посмотреть на шейдер, то видно, что мы на самом деле говорим ему «размести все пиксели этого изображения на экране». То, что они находятся на кубе, просто означает, что все пиксели за его пределами отбрасываются.
Если бы мы хотели применить текстуру так, чтобы она выглядела физически нарисованной на кубе, то наша работа напоминала бы изобретение заново 3D-движка (что было бы довольно глупо, учитывая, что мы уже используем 3D-движок и мы можем просто попросить его отрисовать текстуру отдельно на каждой стороне). В этом туториале мы используем шейдеры для того, что иначе достичь было бы невозможно, так что мы не будем вдаваться в такие подробности. (Если вы жаждете узнать больше, на Udacity есть отличный курс по основам 3D-графики!)
Следующие шаги
На этом этапе мы уже можем делать всё, что сделали в ShaderToy, однако теперь мы способны использовать любые текстуры и любые объекты, на любой платформе.
Имея всю эту свободу, мы можем сделать что-нибудь вроде системы освещения, с реалистичными тенями и источниками света. Этим мы ниже и займёмся. Кроме того, я расскажу о техниках оптимизации шейдеров.
Освоившись с основами шейдеров мы на практике применим мощь видеопроцессора для создания реалистичного динамического освещения.
Начиная с этого момента мы будем рассматривать общие концепции графических шейдеров без привязки к конкретной платформе. (Для удобства во всех примерах кода по-прежнему будет использоваться JavaScript/WebGL.)
Для начала найдите подходящий вам способ выполнения шейдеров. (JavaScript/WebGL — это простейший способ, но я рекомендую вам поэкспериментировать со своей любимой платформой!)
К концу этого туториала вы не только начнёте хорошо ориентироваться в системах освещения, но и с нуля создадите собственную.
Вот как будет выглядеть конечный результат (нажмите мышью для включения света):
Многие игровые движки имеют уже готовые системы освещения, но понимание того, как они сделаны и как создавать собственные, даёт вам больше возможностей придать игре соответствующий ей уникальный вид. Кроме того, шейдерные эффекты не обязательно должны быть только «косметическими», они открывают двери потрясающим новым игровым механикам!
Отличным примером этого является Chroma. Игрок может бегать по динамическим теням, создаваемым в реальном времени:
Приступаем к работе: наша исходная сцена
Мы многое пропустим в первоначальной настройке, потому что она подробно рассмотрена выше. Начнём с простого фрагментного шейдера, рендерящего нашу текстуру:
Здесь не происходит ничего сложного. Код на JavaScript задаёт сцену и отправляет шейдеру текстуру для рендеринга и размеры экрана.
В коде на GLSL мы объявляем и используем эти uniform-переменные:
Прежде чем использовать координаты пикслей для отрисовки текстуры, мы их нормализуем.
Просто чтобы убедиться, что вы всё понимаете, вот вам небольшое задание на разогрев:
Задача: Отрендерите текстуру, не изменяя соотношения её сторон (Попробуйте сделать это самостоятельно, мы рассмотрим решение ниже.)
Довольно очевидно, почему текстура растянута, но если это непонятно, то вот подсказка: посмотрите на строку, в которой мы нормализуем координаты:
Мы делим x и y на разные числа (на ширину и высоту экрана). Естественно, что изображение будет растянутым.
Ради простоты дальше в туториале мы оставим нормализованный код таким же, то неплохо бы разобраться, что здесь происходит.
Шаг 1: добавление источника света
Прежде чем создать что-то интересное, нам нужен источник света. «Источник света» — это просто точка, передаваемая шейдеру. Для этой точки мы создадим новую uniform:
Мы создали вектор с тремя измерениями, потому что мы хотим использовать x и y в качестве положения источника на экране, а z — в качестве радиуса.
Давайте присвоим в JavaScript значения нашему источнику света:
Мы будем использовать радиус как процент от размеров экрана, поэтому 0.2 будет составлять 20% экрана. (В этом выборе нет ничего особенного. Мы могли бы задать размер в пикселях. Это число ничего не значит, пока мы не начнём с ним делать что-нибудь в коде GLSL.)
Чтобы получить положение мыши, нужно просто добавить получатель события (event listener):
Давайте теперь напишем код шейдера, чтобы воспользоваться этой координатой источника света. Начнём с простой задачи: сделаем так, чтобы каждый пиксель в пределах радиуса источника света был видимым, а остальные были чёрными.
На GLSL это может выглядеть примерно так:
Здесь мы сделали следующее:
Ой-ёй! Источник света следует за мышью как-то странно.
Задача: Сможете это исправить? (Попробуйте снова разобраться самостоятельно, прежде чем мы решим эту задачу ниже.)
Исправление движения источника света
Возможно, вы помните, что ось Y здесь перевёрнута. Вы можете поторопиться просто ввести:
Это математически верно, но если поступить так, то шейдер не скомпилируется! Проблема в том, что uniform-переменные невозмжоно изменять. Чтобы понять, почему, нужно помнить, что этот код выполняется для каждого отдельного пикселя параллельно. Представьте, что все процессорные ядра попытаются изменить единственную переменную одновременно. Плохая ситуация!
Мы можем исправить ошибку, создав новую переменную вместо uniform. Или ещё лучше — мы можем просто сделать этот шаг до передачи данных в шейдер:
Теперь мы успешно определили видимый радиус нашей сцены. Однако он выглядит слишком резким…
Добавление градиента
Вместо простой обрезки до чёрного цвета за пределами радиуса давайте попробуем создать плавный градиент у краёв. Можно сделать это с помощью расстояния, которое мы уже вычисляем.
Вместо присвоения всем пикселям в пределах радиуса цвета текстуры:
мы можем умножать его на коэффициент расстояния:
На этом рисунке dist вычисляется для произвольного пикселя. dist меняется в зависимости от того, в каком пикселе мы находимся, а значение light.z * res.x постоянно.
Если мы посмотрим на пиксель на границе круга, то dist равно длине радиуса, то есть в результате мы умножаем color на 0 и получаем чёрный цвет.
Шаг 2: добавляем глубину
Пока мы не сделали ничего особенного, просто добавили нашей текстуре градиентную маску. Всё по-прежнему выглядит плоским. Чтобы понять, как это исправить, давайте посмотрим, что сейчас делает система освещения, и сравним с тем, что она должна делать.
В представленном выше случае стоит ожидать, что точка A будет освещена сильнее всего, потому что источник света находится прямо над ней, а B и C будут темнее, потому что на боковых сторонах практически нет лучей.
Однако вот что видит наша система освещения сейчас:
Все точки обрабатываются одинаково, потому что единственный фактор, который учитывает система — это расстояние на плоскости xy. Вы можете подумать, что нам всего лишь нужна высота каждой их этих точке, но это не совсем так. Чтобы понять, почему, рассмотрите этот рисунок:
A находится наверху фигуры, а B и C — по бокам. D — это ещё одна точка на земле. Мы видим, что A и D должны быть самыми яркими, причём D немного темнее, потому что свет достигает её под углом. С другой стороны, B и C должны быть очень тёмными, потому что до них почти не доходит свет, ведь они направлены от источника света.
Не так важна высота, как направление, в котором повёрнута поверхность. Оно называется нормалью поверхности.
Но как передать эту информацию шейдеру? Мы ведь наверно не можем передавать огромный массив из тысяч чисел для каждого отдельного пикселя? На самом деле, мы так и делаем! Только мы называем это не массивом, а текстурой.
На рисунке выше показана простая карта нормалей. Если воспользоваться инструментом «пипетка» мы увидим, что направление по умолчанию («плоское») представлено цветом (0.5, 0.5, 1) (синий цвет, занимающий бо́льшую часть изображения). Это направление, указывающее прямо вверх. Значения x, y и z присваиваются значениям r, g и b.
Наклонная сторона справа повёрнута вправо, поэтому её значение x выше. Значение x также является значением красного, именно поэтому сторона выглядит немного красноватой или розоватой. То же самое относится ко всем остальным сторонам.
Карта выглядит забавно, потому что не предназначена для рендеринга, она просто кодирует значения нормалей этих поверхностей.
Давайте загрузим эту простую карту нормалей для теста:
И добавим её как одну из uniform-переменных:
Чтобы проверить, что мы загрузили её правильно, давайте попробуем отрендерить её вместо текстуры, изменив код на GLSL (помните, что мы на этом этапе используем просто фоновую текстуру, а не карту нормалей):
Шаг 3: применение модели освещения
Теперь, когда у нас есть данные нормалей поверхностей, нам нужно реализовать модель освещения. Иными словами, надо сообщить поверхности, как учесть все имеющиеся факторы для вычисления окончательной яркости.
Простейшей для реализации моделью является модель Фонга. Вот как она работает: пусть у нас есть поверхность с данными о нормалях:
Мы просто вычисляем угол между источником света и нормалью поверхности:
Чем меньше угол, тем ярче пиксель.
Это значит, что когда пиксель находится непосредственно под источником света, где разность углов равна 0, он будет самым ярким. Самые тёмные пиксели будут указывать в том же направлении, что и источник света (это будет похоже на заднюю часть объекта).
Давайте реализуем эту модель.
Поскольку для проверки мы используем простую карту нормалей, давайте зальём текстуру сплошным цветом, чтобы чётко понимать, всё ли у нас получается.
Давайте сделаем сплошной белый цвет (или любой другой цвет):
Вот как выглядит алгоритм:
1. Получаем вектор нормали текущего пикселя
Нам нужно знать, в каком направлении «смотрит» поверхность, чтобы можно было вычислить количество света, попадающего на этот пиксель. Это направление хранится в карте нормалей, поэтому получение вектора нормали заключается в получении цвета текущего пикселя в текстуре нормалей:
Поскольку альфа-значение ничего не обозначает на карте нормалей, нам требуются только первые три компонента.
2. Получаем вектор направления света
Теперь нам нужно знать, в каком направлении указывает свет. Можно представить, что поверхность освещения — это фонарик, направленный на экран в точке расположения курсора мыши. Поэтому можно вычислить вектор направления света просто с помощью расстояния между источником света и пикселем:
Он должен иметь и координату Z (чтобы можно было вычислить угол относительно трёхмергого вектора нормали поверхности). С этим значением можно поэкспериментировать. Вы заметите, что чем оно меньше, тем резче контраст между яркими и тёмными областями. Можно представить, что это высота фонарика над сценой: чем он дальше, тем равномернее распространяется свет.
3. Нормализуем векторы
Теперь нам нужно нормализировать:
4. Вычисляем угол между векторами
Давайте сделаем это с помощью встроенной функции dot:
Я назвал переменную diffuse потому что этот термин используется в модели освещения по Фонгу, ведь она определяет количество света, достигающее поверхности сцены.
5. Умножаем конечный цвет на этот коэффициент
И мы получили работающую модель освещения! (Попробуйте увеличить радиус источника света, чтобы эффект был сильнее заметен.)
Хм, кажется, что-то не так. Похоже, что источник как-то наклонён.
Давайте ещё раз посмотрим на наши вычисления. У нас есть вектор света:
Задача: Понимаете ли вы, в чём заключается решение? Сможете реализовать его?
Вот как демо выглядит после вычитания 0.5 из координат x и y вектора нормали:
И у нас получилась работающая модель освещения!
Можно поставить на фон каменную текстуру, а настоящую карту нормалей взять в репозитории этого туториала на GitHub (а именно здесь):
Нам нужно только изменить одну строку на JavaScript, с:
И одну строку на GLSL:
Нам больше не нужен сплошной белый цвет, мы загрузим настоящую текстуру, вот так:
И вот окончательный результат:
Советы по оптимизации
Видеопроцессор выполняет свою работу очень эффективно, но очень важно знать, что может её замедлять. Вот несколько советов:
Ветвление
В шейдерах обычно желательно по возможности избегать ветвления. Хотя большое количество конструкций if редко бывает проблемой в любом коде для ЦП, в шейдерах для видеопроцессора они могут стать узким местом.
Отложенный рендеринг
Это очень полезная концепция при работе с освещением. Представьте, что нам нужны два источника освещения, или три, или десяток. Нам придётся вычислять угол между каждой нормалью поверхности и каждой точкой источника света. В результате шейдер будет выполняться с черепашьей скоростью. Отложенный рендеринг — это способ оптимизации такого процесса разбиением работы шейдера на несколько проходов. Вот статья, в которой подробно объяснено, что это значит. Я процитирую важную для нас часть:
Освещение — это основная причина выбора того или иного пути. В стандартном прямолинейном конвейере рендеринга вычисления освещения должны выполняться для каждой вершины и каждого фрагмента видимой сцены для каждого источника света в сцене.
Например, вместо передачи массива точек источников света, можно отрисовать их на текстур в виде кругов, цвет каждого пикселя которых будет представлять интенсивность освещения. Таким образом мы сможем вычислять комбинированный эффект всех источников освещения в сцене, а потом просто передать его конечной текстуре (или буферу, как её иногда называют) для вычисления освещения.
Способность разделить работу на несколько проходов — это очень полезная техника при создании шейдеров. Например, она используется для ускорения шейдера при вычислении эффекта размывки, а также в шейдерах жидкостей/дыма.
Следующие шаги
Теперь, когда вы получили работающий шейдер освещения, вот с чем ещё можно поэкспериментировать:
Ссылки
Каменная текстура и карта нормалей для этого туториала взяты с OpenGameArt: http://opengameart.org/content/50-free-textures-4-normalmaps.
Существует множество программ, помогающих в создании карт нормалей. Если вам интересно узнать больше о создании собственных карт нормалей, то вам может помочь эта статья.
- Что такое шезлонг
- Что такое шелкография