Шейдер травы

Геометрический шейдер травы на Unity

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

В конце статьи дан готовый код. Для понятности он сопровождается обильными комментариями.

Подготовка

Чтобы понять этот урок, вам понадобятся практические знания движка Unity и промежуточное понимание синтаксиса и функциональности шейдеров.

Скачать начальный проект .zip

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

Начало

Скачайте приведенный выше начальный проект и откройте его в редакторе Unity. Откройте главную сцену и шейдер Grass в любимом редакторе кода.

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

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

1. Геометрические шейдеры

Геометрические шейдеры являются необязательной частью конвейера визуализации. Они выполняются после вершинного шейдера (или тесселяционного — если используется тесселяция) и до обработки вершин для фрагментного шейдера.

пайплайн

Графический конвейер Direct3D 11. Обратите внимание, что на этой диаграмме фрагментный шейдер называется пиксельным шейдером. Изображение получено отсюда.

Геометрические шейдеры принимают в качестве входных данных один примитив и могут генерировать ноль, один или несколько примитивов. Мы начнем с написания геометрического шейдера, принимающего вершину (или точку) в качестве входных данных, и выведем один треугольник, представляющий травинку.

Выше объявляется геометрический шейдер с названием geo, имеющий два параметра. Первый float4 IN [1] утверждает, что в качестве входных данных мы возьмем одну точку. Второй, типа TriangleStream настраивает наш шейдер на вывод потока треугольников, причем каждая вершина использует структуру geometryOutput для переноса своих данных.

Кроме того, мы добавляем последний параметр перед объявлением функции в квадратных скобках: [maxvertexcount (3)]. Это говорит GPU, что мы будем выводить (но не обязательно) максимум 3 вершины. Мы также гарантируем, что наш SubShader использует геометрический шейдер, объявив тот внутри Pass.

Геометрический шейдер пока ничего не делает; чтобы вывести треугольник, добавьте внутри него следующий код.

первые попытки построения шейдера

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

треугольник

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

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

основа для травинок

Почему некоторые вершины не выводят треугольник?

расположение вершин на полигональной сетке

Мы определили входной примитив как точку. Однако реальная входная сетка (в нашем случае GrassPlane10x10 из папки Mesh) имеет треугольную топологию. Для Direct3D нет необходимости, чтобы входны примитивы шейдера совпадали с топологий сетки. Он просто передаст примитив (в нашем случае треугольник) и проигнорирует последние две точки.

Это можно устранить, если брать сетки с топологий точечного типа, как входная сетка для нашего геометрического шейдера.

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

тестирование на сферической сетке

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

2. Касательное пространство

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

касательное пространство для сферы
В касательном пространстве оси X, Y и Z определяются относительно нормали и положения поверхности (в нашем случае, вершины). Изображение переработано на основе приведенного в Википедии.

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

Доступ к векторам направо и вверх можно получить, добавив несколько новых вершинных входов.

Третий вектор можно рассчитать, взяв векторное произведение двух первых. Оно возвращает вектор, перпендикулярный двум входным.

Почему результат векторного произведения умножается на касательную координату w?

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

С тремя векторами мы можем построить матрицу преобразования между касательным и локальным пространством. Мы умножим каждую вершину травинки на эту матрицу до передачи в UnityObjectToClipPos, которая работает с вершинами в локальном пространстве.

Перед использованием матрицы мы переместим выходной код вершины в функцию, чтобы избежать повторной записи одних и тех же строк кода. Это часто называют принципом DRY или “не повторяйте себя”.

Наконец, мы умножим выходные вершины на матрицу tangentToLocal, правильно выровняв их по нормали относительно входной точки.

Читай также:  Как устроена графика в современных играх. Часть 2

правильный шейдер травы на сферической сетке

Это выглядит ближе к тому, что мы хотим, но не совсем правильно. Проблема в том, что мы изначально определили направление «вверх», чтобы быть на оси Y; в касательном пространстве, однако, соглашение обычно диктует направление вверх вдоль оси Z. Сделаем это изменение сейчас.

трава смотрит вверх

3. Облик травы

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

3.1 Градиент цвета

Наша цель — позволить дизайнеру определить два цвета (верх и низ) — и интерполировать их между концом травинки и основанием. Эти цвета уже определены в файле шейдера как _TopColor и _BottomColor. Для их правильной выборки нам потребуется предоставить фрагментный шейдер с UV-координатами.

Мы строим UV-шки для травинки в форме треугольника, с двумя вершинами основания слева и справа, а вершина кончика травинки размещена по центру.

координаты треугольника

UV координаты трех вершин травинок. Хотя мы будем окрашивать их простым градиентом, такое размещение координат позволяет наложение текстуры.

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

градиент окрашивания травы

3.2 Случайное направление

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

В файле шейдера есть две функции, которые помогут нам сделать это: rand, которая генерирует случайное число из трехмерного ввода, и AngleAxis3x3, которая принимает угол (в радианах) и возвращает матрицу вращений на определенную величину вокруг указанной оси. Последняя функция работает так же, как и Quaternion.AngleAxis в C# (но возвращает матрицу, а не кватернион).

Функция rand возвращает число в диапазоне 0 … 1; мы умножим это на два Пи, чтобы получить полный набор угловых значений.

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

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

случайный поворот травинок

3.3 Рандомизованный наклон

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

Мы снова используем это положение в качестве нашего случайного начала, на этот раз применяя свизлинг, чтобы создать уникальное. Мы также умножаем UNITY_PI на 0,5; это дает нам случайный диапазон 0 … 90 градусов.

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

3.4 Ширина и высота

Размеры травинок сейчас равны 1 единице в ширину и 1 единице в высоту. Мы добавим свойства для управления этим, а также свойства, чтобы внести случайные изменения.

толщина травинок

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

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

4. Тесселяция

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

Для поверхностных шейдеров Unity имеет встроенную реализацию тесселяции. Однако, поскольку мы не используем поверхностные шейдеры, необходимо реализовать пользовательские шейдерные оболочки и зональные шейдеры. В этой статье не будет подробно рассказываться про реализацию тесселяции — вместо этого мы используем включенный файл CustomTessellation.cginc. Он взят с изменениями из этой статьи от Catlike Coding, которая является отличным справочником по означенной теме.

Если мы включим объект TessellationExample в сцене, мы увидим, что к нему уже применен материал, реализующий тесселяцию. Изменение свойства Tessellation Uniform продемонстрирует эффект разбиения.

разбиение сетки тесселяцией

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

Если открыть CustomTessellation.cginc, вы заметите, что он уже определил структуры vertexInput и vertexOutput, а также вершинный шейдер. Нет необходимости переопределять их в нашем шейдере для травы; поэтому их можно удалить.

Обратите внимание, что вершина от вершинного шейдера в CustomTessellation.cginc просто передает данные непосредственно на стадию тесселяции. Работа по созданию структуры vertexOutput выполняется функцией tessVert, вызываемой внутри зонального шейдера.

Теперь можно добавить шейдер поверхности и зональный в наш шейдер травы. Также мы добавим новое свойство _TessellationUniform для управления степенью разбивки — соответствующая переменная для этого свойства уже объявлена в CustomTessellation.cginc.

Изменение свойства Tessellation Uniform теперь позволяет контролировать плотность травы. Я обнаружил, что хорошие результаты дает значение 5.

Читай также:  Для начинающих Unity3d программистов

шейдер травы

5. Ветер

Мы реализуем ветер путём сэмплирования текстуры искажения. Эта текстура будет похожа на карту нормалей, но у нее только два канала (красный и зеленый) вместо трех. Мы будем использовать эти два канала в качестве направления ветра X и Y.

текстура для эффекта ветра

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

Мы применяем масштаб и смещение _WindDistortionMap к нашей позиции, а затем дополнительно смещаем их по _Time.y, масштабируемому по _WindFrequency. Теперь мы используем этот UV для сэмплирования нашей текстуры и создадим свойство для контроля силы ветра.

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

Теперь мы можем построить матрицу, чтобы вращаться вокруг этого вектора, и умножить ее на transformMatrix.

Наконец, в редакторе Unity примените текстуру ветра (находится в корне проекта) к слоту карты искажения ветра материала травы Wind Distortion Map. Также установите Tiling текстуры на 0.01, 0.01.

Если трава не анимируется в представлении Scene, переключите небо, туман и другие эффекты, чтобы включить анимированные материалы.

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

проблема с пересечением порождающей поверхности

Основание травинки больше не прикреплено к поверхности: оно пересекает ее (обозначено красным пунктиром), а также плавает над ним (зеленым).

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

6. Кривизна травинки

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

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

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

травинка сконструированная из треугольников

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

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

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

Мы изначально определяем число сегментов как 3 и обновляем maxvertexcount, чтобы вычислить количество вершин на основе числа сегментов.

Чтобы создать сегментированную травинку, мы будем использовать цикл for. Каждая итерация цикла добавит две вершины: левую и правую. После того, как вершина завершена, мы добавим окончательную вершину на конце травинки.

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

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

Когда функция работает правильно, мы готовы переместить код генерации вершин в цикл for. Добавьте следующую строку под строкой с объявлением изменяемой ширины.

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

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

Обратите внимание на строку, объявляющую float3x3 transformMatrix — здесь мы выбираем между двумя матрицами преобразования, TransformationMatrixFacing для вершин в основании и TransformationMatrix для всех остальных.

очередной этап программирования шейдера

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

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

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

трава с естественным наклоном

7. Освещение и тени

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

7.1 Отбрасывание теней

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

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

Помимо наличия нового фрагментного шейдера, в этом проходе есть пара ключевых отличий. Тег LightMode имеет значение ShadowCaster вместо ForwardBase — это сообщает Unity, что данный проход следует использовать для рендеринга объекта в карты теней. У нас также есть директива препроцессора multi_compile_shadowcaster. Это гарантирует, что шейдер компилирует все варианты, необходимые для отбрасывания теней.

Установите в сцене активный игровой объект Забор (Fence). Так мы получим поверхность, на которую травинки отбрасывают тень.

трава отбрасывает тень

7.2 Получение теней

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

В фрагментном шейдере прохода ForwardBase мы можем использовать макрос для получения значения с плавающей запятой, представляющего, находится ли поверхность в тени или нет. Это значение находится в диапазоне 0 … 1, где 0 полностью затенено, а 1 полностью подсвечено.

Почему UV-координата экранного пространства называется _ShadowCoord? Это не соответствует предыдущим соглашениям.

Многие из встроенных макросов для шейдеров Unity делают предположения об именах определенных полей в различных структурах шейдеров (некоторые даже делают предположения об именах самих структур). Макрос, который мы используем ниже, SHADOW_ATTENUATION, ничем не отличается. Если мы получим код для этого макроса из Autolight.cginc, мы увидим, что для координат тени требуется конкретное имя.

Если нужно сделать другое имя для координаты, можно просто скопировать приведенное выше определение в наш собственный шейдер.

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

забор отбрасывает тень

При увеличении мы можем увидеть какие-то артефакты на поверхности травинок; это вызвано тем, что отдельные травинки отбрасывают на себя тени. Мы можем исправить это, применив линейное смещение или немного переместив положения вершин в пространство отсечения на экране. Для этого мы будем использовать макрос Unity и включим его в инструкцию #if, чтобы операция выполнялась только во время теневого прохода.

реализация затенения

После применения линейного смещения теней артефакты с полосами удаляются с поверхности треугольников.

Почему по краям травинок есть артефакты, которые получили тень?

артефакты на травинках

Даже когда мультисэмплированное сглаживание (MSAA) включено, Unity не применяет сглаживание к текстуре глубины сцены, которая используется при построении карты теней экранного пространства. Следовательно, когда сглаженная сцена сэмплирует не сглаженную карту теней, возникают артефакты.

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

7.3 Освещение

Мы будем реализовывать освещение, используя этот общий, очень простой алгоритм для расчета рассеянного освещения.

формула освещенности

… где N — нормаль поверхности, L — нормализованное направление основного направленного света, а I — расчетное освещение. Про отраженное освещение не будем говорить в этом тьюториале.

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

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

Параметр tangentNormal, определяемый как обратное направление по оси Y, преобразуется той же самой матрицей, которую мы использовали для преобразования точек касания в локальное пространство. Теперь мы можем передать это в функцию VertexOutput, а затем в структуру geometryOutput.

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

Теперь мы можем визуализировать нормали в фрагментном шейдере ForwardBase, чтобы проверить работу.

Поскольку для нашего шейдера Cull установлено на значение Off, отображаются обе стороны травинки. Чтобы нормаль шла в правильном направлении, мы используем опциональный параметр VFACE, который включили в фрагментный шейдер.

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

подготовка к отсечению задних поверхностей

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

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

шейдер травы готов

Заключение

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

текстура травы в Юнити

Текстура травы включенная как часть стандартных ресурсов Unity. Многие травинки нарисованы на одном кваде, уменьшая количество треугольников.

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

Посмотреть исходники в репозиторие GitHub

Взаимодействие

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

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

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

Понравилась статья? Поделиться с друзьями: