- Трава и другие детали
- Включение деталей
- Создание леса, травы и заката в Unity
- Как добавить дерево
- Как добавить траву
- Добавление скайбокса и создание заката
- Как я делал «фэнтезийный» лес 😀
- Как исправлять ошибки в коде ассетов
- Включение вертикальной синхронизации
- Увеличил деревья
- Создание шейдера травы в движке Unity
- Требования
- Приступаем к работе
- 1. Геометрические шейдеры
- 2. Касательное пространство
- 3. Внешний вид травы
- 3.1 Цветовой градиент
- 3.2 Случайное направление травинок
- 3.3 Случайный изгиб forward
- 3.4 Ширина и высота
- 4. Тесселяция
- 5. Ветер
- 6. Кривизна травинок
- 7. Освещение и тени
- 7.1 Отбрасывание теней
- 7.2 Получение теней
- 7.3 Освещение
- Заключение
- Дополнение: ваимодействие
Трава и другие детали
Terrain (объект, представляющий собой земную поверхность) может иметь сгустки травы и другие небольшие объекты, типа камней, покрывающие его поверхность. Трава отрисовывается с использованием плоских (2D) изображений для представления отдельных пучков, в то время как другие элементы генерируются из обычных мешей.
Terrain с травой
Включение деталей
Кнопка details на панели инструментов включает заливку травой/деталями.
Изначально, у terrain нет доступной травы или деталей, но если вы нажмете на кнопку Edit Details в инспекторе, откроется меню с пунктами Add Grass Texture и Add Detail Mesh. Эти окна позволяют выбрать ассеты, которые вы хотите добавить к terrain’у для заливки.
For grass, the window looks like this:
Окно Add Grass Texture
The Detail Texture is the texture that represents the grass. A few suitable textures are included in the Unity Standard Assets downloadable from the Asset Store. You can also create your own. The texture is simply a small image with alpha set to zero for the empty areas. (“Grass” is a generic term, of course — you can use the images to represent flowers, brush and perhaps even artificial objects like barbed wire coils.)
Значения полей Min Width, Min Height, Max Width и Max Height задают верхние и нижние пределы размеров пучков генерируемой травы. Для создания более естественного вида, трава генерируется со случайными “шумными” шаблонами, которые имеют “проплешины”, засаженные травой.
Значение поля Noise Spread задает приблизительный размер чередующихся участков, большее значение определяет большее распределение в заданной области. Техническое замечание: шум на самом деле генерируется с использованием Perlin noise (шума Перлина); распределение шума ссылается на применённое масштабирование между x,y координатами на terrain’е и картинкой шума. Чередующиеся скопления травы считаются более свежими ближе к центру, чем на краях, и настройки Healthy/Dry Color задают цвета степени свежести травы.
Если опция Billboard включена, изображение травы будет всегда поворачиваться лицевой стороной к камере. Эта опция может быть полезна при создании густой травы, так как у неё нет “боковой” стороны. Но как бы там ни было, в случае редкой травы, вращение отдельных кустов будет заметно, и это создаст неприятный эффект.
Для мешей деталей, таких как камни, окно выбора выглядит так:-
Окно Add Detail Mesh
Значение поля Detail используется, чтобы выбрать префаб из проекта, который будет масштабирован в пределах Random Width и Random Height для каждого экземпляра. Параметры Noise Spread и Healthy/Dry Color делают тоже, что и в случае с травой (хотя концепт “свежести” несколько натянут, когда речь идёт о применении к таким объектам, как камни!). Выпадающий список Render Mode может быть установлен в состояние Grass или Vertex Lit. В режиме Grass Mode, экземпляры объектов детализации на сцене будут преобразованы в плоские двумерные изображения, которые ведут себя как трава. В режиме Vertex Lit, элементы детализации будут отрисованы как объемные объекты с вершинным освещением.
Источник
Создание леса, травы и заката в Unity
Создание леса, травы, скайбокса и заката в Unity
В прошлом посте этого блога я рассказал, как создал персонажа и землю. Теперь было бы неплохо создать какой-то лес, потому что зеленые горы выглядят слегка непривлекательно.
Делать, опять же, буду на готовых ассетах в целях обучения и творческого самовыражения.
Как добавить дерево
- Зашел на Unity Assets Store и вбил в поиск «forest». Галочкой справа отметил, что нужны ассеты бесплатные (free).
- Нашел демо ассет Fantasy Forest, установил. О том, как это делается, читайте в предыдущем посте о создании персонажа и земли.
- Теперь в Unity нужно выбрать свой Terrain. Затем Инспекторе справа выбрать режим Paint Trees.
- Из папки Meshes > Prefabs скачанного ассета добавить единственное доступное дерево.
- Дальше выбрать кисть, настроить рандом и так далее… Разберетесь – просто подергайте ползунки, как это сделал я.
Ну и вот у меня получился какой-то лес…
Как добавить траву
Процесс добавления травы полностью идентичен процессу добавления дерева, только находится в другой вкладке.
- Выбираем Terrain. В инспекторе находим заходим в раздел Paint Details.
- Нажимает Edit Details → Add Detail Mesh и добавляем туда единственную доступную траву из установленного ассета.
- Дальше все также: настраиваем кисть и разрисовываем свою землю прямо в редакторе.
Надо сказать, трава мне это совсем не понравилась. Несмотря на то, что она анимированная, моделька все же слишком высокая и закрывает львиную долю обзора.
Чтобы повысить дальность прорисовки травы, нужно сделать следующее:
- Выбрать террейн, в инспекторе зайти в раздел Terrain Settings.
- Опцию Detail Distance выкрутить на максимум.
Вот все и готов. Траву стало видно дальше, но картинка все еще не очень.
Добавление скайбокса и создание заката
Решил добавить красивый закат. Первым делом настроил освещение.
- Выбираем элемент Directional Light (или создаем).
- В Инспекторе справа выбираем ему цвет, интенсивность и прочее. Тип должен быть Directional – это вроде как глобальное освещение.
- Вращаем так, чтобы светило как бы сбоку – как солнце на закате. Местоположение объекта не имеет значение – только вращение.
Ну и вот у нас закат.
Правда небо все еще стандартное – страшное и скучное. Надо это исправить.
Скайбокс, естественно, тоже будет на готовых ассетах.
- Пошел в Asset Store, вбил в поиск «skybox».
- Нашел первый же попавшийся хороший скайбокс под названием All Sky Free – 10 Sky. Установил в свой проект (устанавливалось минут 5).
- Дальше все просто: заходим в папку с нужным скайбоксом (в моем случае – Epic_BlueSunset) и просто перетягиваем его на сцену.
- После этого разворачиваем Directional Light так, чтобы солнце светило оттуда, где оно нарисовано на скайбоксе.
- В инспекторе настраиваем освещение на свой вкус и в графе Flare (блеск, сияние) выбираем Sun Glare.
И вот у нас уже что-то похожее на нормальный закат. Конечно, похожее только отдаленно, но давайте договоримся, что это он и есть… 😁
Прошу заметить, что вот это красивое сияние видно только тогда, когда персонаж смотрит на солнце. Если взгляд отвести в сторону, то все снова очень плохо. Но поскольку я просто играюсь, мне достаточно пока и этого результата.
Все портит только стремный Terrain с одинаковыми деревьями и откровенно неуклюжей травой на половину экрана. Поэтому я решил его исправить.
Как я делал «фэнтезийный» лес 😀
Во-первых, создал новый Terrain. В настройках поставил размер 1000×1000. Если вы уже игрались с этим объектом, то наверняка заметили, что scale на него не работает.
Делать слишком большой террейн я пока не вижу особого смысла. Как говорится, лучше меньше, да лучше.
Пошел искать деревья и растительность на Asset Store. Тут ничего нового – методом «тыка». Какие-то ассеты выдают сомнительные ошибки, какие-то просто криво реализованы (деревья дергаются, меняют цвет, светятся и так далее).
Как исправлять ошибки в коде ассетов
Если код ассетов содержит незначительные ошибки, их можно исправить самостоятельно. Например, когда я устанавливал ассет Nature Starter Kit 2, движок начал выдавать какие-то ошибки. Я открыл проект в Visual Studio Code и пошел смотреть, что там такое.
Оказалось, все очень просто. Файлы с ошибками подсвечиваются красным в иерархии. Сами ошибки подчеркнуты волнистой линией. При наведении курсора показывают окошко с описанием проблемы и рекомендациями. В конкретном случае нам сообщают, что конструкция «DrawProceduralIndirect» является устаревшей, а вместо нее надо использовать «DrawProceduralIndirectNow». Просто дописал «Now» в конце конструкции (их было 2), и все заработало.
Кстати, в коде First Person Controller из предыдущего поста были похожие «ахтунги», но не критичные (помечаются желтым). Их я тоже исправил. На всякий случай… Решил привыкать к подобным телодвижениям, так как чувствую, что столкнусь с ними еще не одну сотню раз.
Неизвестные ошибки лучше всего копировать из консоли и вбивать в Google. Скорее всего, решение уже есть. Нужно только его найти. Встроенный переводчик Chrome в помощь 😉.
Включение вертикальной синхронизации
В процессе «садоводства» столкнулся с проблемой перегрузки видеокарты. По умолчанию в Unity не включена вертикальная синхронизация, поэтому видеокарта выдается свой максимум по FPS и почти всегда загружена на 100%. В моем случае – это 400 FPS, которые ни уму, ни сердцу. Монитор-то 60-герцовый. Включить отображение можно кнопкой Stat в окне игры.
Решение, как оказалось, весьма простое. В том же окне игры нужно нажать на кнопку Free Aspect (это переключатель разрешения экрана) и внутри отметить опцию VSync (вертикальная синхронизация). Теперь видеокарта будет выдавать ровно столько FPS, сколько герц у монитора.
Увеличил деревья
Немного подумав над своей композицией, понял, что портит пейзаж. Деревья какие-то слишком маленькие. В фэнтезийном лесу они должны возвышаться над головой игрока величественной громадой. А на деле 3 пенька, 2 елки… Окей.
С горем пополам выяснил, как это делается. Нужно всего лишь ткнуть на префаб модели и в инспекторе увеличить ей масштаб по всем 3-м осям. Вот вам и вековые дубы до небес. Получите, распишитесь.
Тем же примерно методом увеличил траву, которую скачал из Asset Store. Ну и вот такой пейзаж у меня получился.
Надо сказать, фэнтезийность этого леса видится мне весьма и весьма сомнительной. Но так как я еще учусь, это нестрашно 😉. Про улучшение графики расскажу в следующих постах.
Источник
Создание шейдера травы в движке Unity
Из этого туториала вы научитесь писать геометрический шейдер для генерации травинок из вершин входящего меша и использовать тесселяцию для управления плотностью травы.
Статья описывает поэтапный процесс написания шейдера травы в Unity. Шейдер получает входящий меш, и из каждой вершины меша генерирует при помощи геометрического шейдера травинку. Ради интереса и реализма травинки будут иметь рандомизированные размеры и поворот, а ещё на них будет влиять ветер. Чтобы управлять плотностью травы, мы используем тесселяцию для разделения входящего меша. Трава сможет и отбрасывать, и получать тени.
Готовый проект выложен в конце статьи. В созданном файле шейдера содержится большое количество комментариев, упрощающих понимание.
Требования
Для прохождения этого туториала вам понадобятся практические знания о движке Unity и начальное понимание синтаксиса и функциональности шейдеров.
Приступаем к работе
Скачайте заготовку проекта и откройте его в редакторе Unity. Откройте сцену Main , а затем откройте в своём редакторе кода шейдер Grass .
Этот файл содержит шейдер, выдающий белый цвет, а также некоторые функции, которые мы будем применять в этом туториале. Вы заметите, что эти функции вместе с вершинным шейдером включены в блок CGINCLUDE , расположенный снаружи SubShader . Код, размещённый в этом блоке, будет автоматически включён во все проходы в шейдере; это пригодится позже, потому что у нашего шейдера будет несколько проходов.
Мы начнём с написания геометрического шейдера, генерирующего треугольники из каждой вершины поверхности нашего меша.
1. Геометрические шейдеры
Геометрические шейдеры — это необязательная часть конвейера рендеринга. Они выполняются после вершинного шейдера (или шейдера тесселяции, если используется тесселяция) и до того, как вершины обрабатываются для фрагментного шейдера.
Графический конвейер Direct3D 11. Заметьте, что на этой схеме фрагментный шейдер называется пиксельным (pixel shader).
Геометрические шейдеры получают на входе одиночный примитив и могут сгенерировать ноль, один или множество примитивов. Мы начнём с того, что напишем геометрический шейдер, получающий на входе вершину (или точку), а на выход подающий один треугольник, представляющий травинку.
Представленный выше код объявляет геометрический шейдер под названием geo с двумя параметрами. Первый, triangle float4 IN[3] , сообщает, что он будет брать в качестве ввода один треугольник (состоящий из трёх точек). Второй, типа TriangleStream , настраивает шейдер для вывода потока треугольников, чтобы каждая вершина использовала для передачи своих данных структуру geometryOutput .
Однако поскольку наш входящий меш (в данном случае это GrassPlane10x10 , находящийся в папке Mesh ) имеет топологию меша из треугольников, это вызовет несоответствие между топологией входящего меша и требуемым примитивом ввода. Хоть это и допускается в DirectX HLSL, но не допускается в OpenGL, поэтому будет выведена ошибка.
Кроме того, мы добавляем последний параметр в квадратных скобках над объявлением функции: [maxvertexcount(3)] . Он говорит GPU, что мы будем выводить (но не обязаны это делать) не более 3 вершин. Также мы делаем так, чтобы SubShader использовал геометрический шейдер, объявив его внутри Pass .
Наш геометрический шейдер пока ничего не делает; чтобы вывести треугольник, добавим внутрь геометрического шейдера следующий код.
Это дало очень странные результаты. При перемещении камеры становится ясно, что треугольник рендерится в экранном пространстве. Это логично: поскольку геометрический шейдер выполняется непосредственно перед обработкой вершин, он забирает у вершинного шейдера ответственность за то, чтобы вершины выводились в пространстве усечения. Мы изменим свой код, чтобы отразить это.
Теперь наш треугольник рендерится в мире правильно. Однако, похоже, он создаётся только один. На самом деле, по одному треугольнику отрисовывается для каждой вершины нашего меша, но позиции, присваиваемые вершинам треугольника, постоянны — они не изменяются для каждой входящей вершины. Поэтому все треугольники располагаются один на другом.
Мы исправим это, сделав выходящие позиции вершин смещениями относительно входящей точки.
Хоть мы и определили, что входящий примитив будет треугольником, передаётся травинка только из одной из точек треугольника, отбрасывая остальные две. Конечно, мы можем передавать травинку из всех трёх входящих точек, но это приведёт к тому, что соседние треугольники избыточно будут создавать травинки поверх друг друга.
Или же эту проблему можно решить, взяв в качестве входящих мешей геометрического шейдера меши, имеющие тип топологии Points.
Треугольники теперь отрисовываются правильно, а их основание расположено в испускающей их вершине. Прежде чем двигаться дальше, сделаем объект GrassPlane неактивным в сцене, а объект GrassBall сделаем активным. Мы хотим, чтобы трава правильно генерировалась на разных типах поверхностей, поэтому важно протестировать её на мешах разной формы.
Пока все треугольники испускаются в одном направлении, а не наружу от поверхности сферы. Чтобы разрешить эту проблему, мы будем создавать травинки в касательном пространстве.
2. Касательное пространство
В идеале мы бы хотели создавать травинки, устанавливая разную ширину, высоту, кривизну и поворот, не учитывая угол поверхности, из которой испускается травинка. Проще говоря, мы зададим травинку в пространстве, локальном к испускающей её вершине, а затем преобразуем её так, чтобы она была локальной к мешу. Это пространство называется касательным пространством.
В касательном пространстве оси X, Y и Z задаются относительно нормали и позиции поверхности (в нашем случае вершины).
Как и любое другое пространство, мы можем задать касательное пространство вершины тремя векторами: right, forward и up. С помощью этих векторов мы можем создать матрицу для поворота травинки из касательного в локальное пространство.
Можно получить доступ к векторам right и up, добавив новые входящие данные вершин.
Третий вектор можно вычислить, взяв векторное произведение между двумя другими. Векторное произведение возвращает вектор, перпендикулярный к двум входящим векторам.
Имея все три вектора, мы можем создать матрицу для преобразования между касательным и локальным пространствами. Мы будем умножать каждую вершину травинки на эту матрицу перед передачей в UnityObjectToClipPos , который ожидает вершину в локальном пространстве.
Прежде чем использовать матрицу, мы перенесём код вывода вершин в функцию, чтобы не писать снова и снова одинаковые строки кода. Это называется принципом DRY, или don’t repeat yourself («не повторяйся»).
Наконец, мы умножим выходные вершины на матрицу tangentToLocal , правильно выровняв их с нормалью их входящей точки.
Это уже больше похоже на то, что нам нужно, но не совсем верно. Проблема здесь заключается в том, что изначально мы назначили направление «up» (вверх) оси Y; однако в касательном пространстве направление «вверх» обычно располагается вдоль оси Z. Сейчас мы внесём эти изменения.
3. Внешний вид травы
Чтобы треугольники больше походили на травинки, нужно добавить цветов и вариативности. Начнём мы с добавления градиента, идущего с верхушки травинки вниз.
3.1 Цветовой градиент
Наша цель заключается в том, чтобы позволить художнику задать два цвета — верхушки и низа, и выполнять интерполяцию между этими двумя цветами он кончика до основания травинки. Эти цвета уже определены в файле шейдера как _TopColor и _BottomColor . Для их правильного сэмплирования нужно передать фрагментному шейдеру UV-координаты.
Мы создали UV-координаты для травинки в форме треугольника, две вершины основания которого находятся слева и справа внизу, а вершина кончика расположена по центру вверху.
UV-координаты трёх вершин травинок. Хотя мы раскрашиваем травинки простым градиентом, подобное расположение текстур позволит накладывать текстуры.
Теперь мы можем сэмплировать верхний и нижний цвета во фрагментном шейдере при помощи UV, а затем интерполировать их при помощи lerp . Также нам понадобится модифицировать параметры фрагментного шейдера, сделав входящими данными geometryOutput , а не только позицию float4 .
3.2 Случайное направление травинок
Чтобы создать вариативность и придать траве более естественный вид, мы заставим каждую травинку смотреть в случайном направлении. Для этого нам понадобится создать матрицу поворота, поворачивающую травинку на случайную величину вокруг её оси up.
В файле шейдера есть две функции, которые помогут нам это сделать: rand , генерирующая случайное число из трёхмерного ввода, и AngleAxis3x3 , получающая угол (в радианах) и возвращающая матрицу, которая выполняет поворот на эту величину вокруг указанной оси. Последняя функция работает точно так же, как функция C# Quaternion.AngleAxis (только AngleAxis3x3 возвращает матрицу, а не кватернион).
Функция rand возвращает число в интервале 0. 1; мы умножим его на 2 Pi, чтобы получить полный интервал угловых значений.
Мы используем входящую позицию pos в качестве seed для случайного поворота. Благодаря этому каждая травинка будет иметь собственный поворот, постоянный в каждом кадре.
Поворот можно применить к травинке, умножив его на созданную матрицу tangentToLocal . Учтите, что умножение матриц не является коммутативным; порядок операндов важен.
3.3 Случайный изгиб forward
Если все травинки будут стоять идеально ровно, то они будут казаться одинаковыми. Это может подходить для ухоженной травы, например, на подстригаемой лужайке, но в природе трава так не растёт. Мы создадим новую матрицу для поворота травы по оси X, а также свойство для управления этим поворотом.
Снова используем в качестве случайного seed позицию травинки, на этот раз выполнив её свизлинг для создания уникального seed. Также мы умножим UNITY_PI на 0.5; это даст нам случайный интервал 0. 90 градусов.
Мы опять применяем эту матрицу через поворот, умножая всё в правильном порядке.
3.4 Ширина и высота
Пока размеры травинок ограничены шириной в 1 единицу и высотой в 1 единицу. Мы добавим свойства для управления размером, а также свойства для добавления случайной вариативности.
Треугольники теперь намного больше напоминают травинки, но и слишком мало. Во входящем меше просто недостаточно вершин, чтобы создать впечатление густо заросшего поля.
Одно из решений заключается в создании нового, более плотного меша или с помощью C#, или в 3D-редакторе. Это сработает, но не позволит нам динамически управлять плотностью травы. Вместо этого мы подразделим входящий меш при помощи тесселяции.
4. Тесселяция
Тесселяция — это необязательный этап конвейера рендеринга, выполнямый после вершинного шейдера и до геометрического шейдера (если он есть). Его задача — подразделение одной входящей поверхности на множество примитивов. Тесселяция реализуется двумя программируемыми этапами: оболочечным (hull) и domain-шейдерами.
Для поверхностных шейдеров в Unity есть встроенная реализация тесселяции. Однако поскольку мы не используем поверхностные шейдеры, нам придётся реализовать собственные оболочечный и domain-шейдеры. В этой статье я не буду подробно рассматривать реализацию тесселяции, и мы просто воспользуемся имеющимся файлом CustomTessellation.cginc . Этот файл адаптирован из статьи Catlike Coding, которая является превосходным источником информации о реализации тесселяции в Unity.
Если мы включим объект TessellationExample в сцену, то увидим, что у него уже есть материал, реализующий тесселяцию. Изменение свойства Tessellation Uniform демонстрирует эффект подразделения.
Мы реализуем тесселяцию в шейдере травы для управления плотностью плоскости, а значит и для управления количеством генерируемых травинок. Для начала нужно добавить файл CustomTessellation.cginc . Мы будем ссылаться на него по его относительному пути к шейдеру.
Если вы откроете CustomTessellation.cginc , то заметите, что в нём уже заданы структуры vertexInput и vertexOutput , а также вершинные шейдеры. Не нужно переопределять их в нашем шейдере травы; их можно удалить.
Заметьте, что вершинный шейдер vert в CustomTessellation.cginc просто передаёт входные данные напрямую на этап тесселяции; задачу по созданию структуры vertexOutput берёт на себя функция tessVert , вызываемая внутри domain-шейдера.
Теперь мы можем добавить оболочечный и domain-шейдеры в шейдер травы. Также мы добавим новое свойство _TessellationUniform для управления величиной подразделения — соответствующая этому свойству переменная уже объявлена в CustomTessellation.cginc .
Теперь изменение свойства Tessellation Uniform позволит нам управлять плотностью травы. Я выяснил, что хорошие результаты получаются при значении 5.
5. Ветер
Мы реализуем ветер сэмплированием текстуры искажения. Эта текстура будет похожа на карту нормалей, только в ней вместо трёх каналов будет только два. Мы воспользуемся этими двумя каналами как направлениями ветра по X и Y.
Прежде чем сэмплировать текстуру ветра, нам нужно создать UV-координату. Вместо использования координат текстур, назначенных мешу, мы применим позицию входящей точки. Благодаря этому, если в мире будет несколько мешей с травой, создастся иллюзия того, что они все являются частью одной системы ветров. Также мы используем встроенную переменную шейдера _Time для прокрутки текстуры ветра вдоль поверхности травы.
Мы применяем к позиции масштаб и смещение _WindDistortionMap , а затем ещё больше смещаем её на _Time.y , отмасштабированную на _WindFrequency . Теперь мы будем использовать эти UV для сэмплирования текстуры и создадим свойство для управления силой ветра.
Заметьте, что мы изменяем масштаб сэмплируемого значения из текстуры с интервала 0. 1 на интервал -1. 1. Далее мы можем создать нормализованный вектор, обозначающий направление ветра.
Теперь мы можем создать матрицу для поворота вокруг этого вектора и умножить её на нашу transformationMatrix .
Наконец, перенесём в редакторе Unity текстуру Wind (находящуюся в корне проекта) в поле Wind Distortion Map материала травы. Также зададим для параметра Tiling текстуры значения 0.01, 0.01 .
Если трава не анимируется в окне Scene, то нажмите на кнопку Toggle skybox, fog, and various other effects, чтобы включить анимированные материалы.
Издалека трава выглядит правильно, однако если мы взглянем травинки вблизи, то заметим, что поворачивается вся травинка, из-за чего основание больше не прикреплено к земле.
Основание травинки больше не прикреплено к земле, а пересекается с ней (показано красным), и висит над плоскостью земли (обозначенной зелёной линией).
Мы исправим это, задав вторую матрицу преобразования, которую применим только к двум вершинам основания. В эту матрицу не будут включены матрицы windRotation и bendRotationMatrix , благодаря чему основание травинки будет прикреплено к поверхности.
6. Кривизна травинок
Сейчас отдельные травинки задаются одним треугольником. На больших расстояниях это не проблема, но вблизи травинки выглядят очень жёсткими и геометричными, а не органическими и живыми. Мы исправим это, построив травинки из нескольких треугольников и согнув их вдоль кривой.
Каждая травинка будет подразделена на несколько сегментов. Каждый сегмент будет иметь прямоугольную форму и состоять из двух треугольников, за исключением верхнего сегмента — он будет одним треугольником, обозначающим кончик травинки.
Пока мы выводили только три вершины, создавая единственный треугольник. Как же при наличии большего количества вершин геометрический шейдер узнает, какие из них нужно соединять и образовывать треугольники? Ответ находится в структуре данных triangle strip. Первые три вершины соединяются и образуют треугольник, а каждая новая вершина образует треугольник с предыдущими двумя.
Подразделённая травинка, представленная в виде triangle strip и создаваемая по одной вершине за раз. После первых трёх вершин каждая новая вершина образует новый треугольник с предыдущими двумя вершинами.
Это не только более эффективно с точки зрения использования памяти, но и позволяет легко и быстро создавать в коде последовательности треугольников. Если бы мы хотели создать несколько полос треугольников, то могли бы вызвать для TriangleStream функцию RestartStrip.
Прежде чем мы начнём выводить из геометрического шейдера больше вершин, нам нужно увеличить maxvertexcount . Мы воспользуемся конструкцией #define , чтобы позволить автору шейдера управлять количеством сегментов и вычислять из него количество выводимых вершин.
Изначально мы задаём количество сегментов равным 3 и обновляем maxvertexcount , чтобы вычислить количество вершин на основании количества сегментов.
Для создания сегментированной травинки мы используем цикл for . Каждая итерация цикла будет добавлять по две вершины: левую и правую. После завершения верхушки мы добавим последнюю вершину на кончике травинки.
Прежде чем мы это сделаем, будет полезно переместить часть вычисляющего позиции вершин травинок кода в функцию, потому что мы будем использовать этот код несколько раз внутри и за пределами цикла. Добавим в блок CGINCLUDE следующее:
Эта функция выполняет те же задачи, потому что ей передаются аргументы, которые мы ранее передавали VertexOutput для генерации вершин травинки. Получая позицию, высоту и ширину, она правильно преобразовывает вершину при помощи передаваемой матрицы и назначает ей UV-координату. Мы обновим имеющийся код для правильной работы функции.
Функция начала работать правильно, и мы готовы переместить код генерации вершин в цикл for . Добавим под строкой float width следующее:
Мы объявляем цикл, который будет выполняться по разу для каждого сегмента травинки. Внутри цикла добавляем переменную t . Эта переменная будет хранить значение в интервале 0. 1, обозначающее, насколько далеко мы продвинулись по травинке. Это значение мы используем для вычисления ширины и высоты сегмента в каждой итерации цикла.
При движении вверх по травинке высота увеличивается, а ширина уменьшается. Теперь мы можем добавить в цикл вызовы GenerateGrassVertex , чтобы добавлять в поток треугольников вершины. Также мы добавим один вызов GenerateGrassVertex за пределами цикла, чтобы создать вершину кончика травинки.
Взгляните на строку с объявлением float3x3 transformMatrix — здесь мы выбираем одну из двух матриц преобразования: берём transformationMatrixFacing для вершин основания и transformationMatrix для всех остальных.
Травинки теперь разделены на множество сегментов, но поверхность травинки по-прежнему плоская — новые треугольники пока не задействованы. Мы добавим травинке кривизны, сместив позицию вершин по Y. Во-первых, нам нужно модифицировать функцию GenerateGrassVertex , чтобы она получала смещение по Y, которое мы назовём forward .
Для вычисления смещения каждой вершины мы подставим в функцию pow значение t . После возведения t в степень её влияние на смещение forward будет нелинейным и превратит травинку в кривую.
Это довольно большой фрагмент кода, но вся работа выполняется аналогично тому, что делалось для ширины и высоты травинки. При меньших значениях _BladeForward и _BladeCurve мы получим упорядоченную, ухоженную лужайку, а большие значения дадут противоположный эффект.
7. Освещение и тени
В качестве последнего этапа для завершения шейдера мы добавим возможность отбрасывать и получать тени. Также мы добавим простое освещение, получаемое от основного источника направленного света.
7.1 Отбрасывание теней
Для отбрасывания теней в Unity в шейдер нужно добавить второй проход. Этот проход будет использоваться создающими тени источниками освещения в сцене для рендеринга глубины травы в их карту теней. Это значит, что геометрический шейдер придётся запускать и в проходе теней, чтобы травинки могли отбрасывать тени.
Поскольку геометрический шейдер записан внутри блоков CGINCLUDE , мы можем использовать его в любых проходах файла. Создадим второй проход, который будет использовать те же шейдеры, как и первый, за исключением фрагментного шейдера — мы определим новый, в который запишем макрос, обрабатывающий выходные данные.
Кроме создания нового фрагментного шейдера, в этом проходе есть ещё пара важных отличий. Метка LightMode имеет значение ShadowCaster , а не ForwardBase — это говорит Unity, что данный проход должен использоваться для рендеринга объекта в карты теней. Также здесь есть директива препроцессора multi_compile_shadowcaster . Она гарантирует, что шейдер скомпилирует все необходимые варианты, требуемые для отбрасывания теней.
Сделаем игровой объект Fence активным в сцене; так мы получим поверхность, на которую травинки смогут отбрасывать тень.
7.2 Получение теней
После того, как Unity отрендерит карту теней с точки зрения создающего тени источника света, он запускает проход, «собирающий» тени в текстуру экранного пространства. Для сэмплирования этой текстуры нам нужно будет вычислять позиции вершин в экранном пространстве и передавать их во фрагментный шейдер.
Во фрагментном шейдере прохода ForwardBase мы можем использовать макрос для получения значения float , обозначающего, находится ли поверхность в тенях, или нет. Это значение находится в интервале 0. 1, где 0 — полное затенение, 1 — полная освещённость.
Если мы бы захотели создать другое имя для этой координаты или это по каким-то причинам нам бы потребовалось, то можно было бы просто скопировать данное определение в наш собственный шейдер.
Наконец, нам нужно сделать так, чтобы шейдер был правильно сконфигурирован для получения теней. Для этого мы добавим к проходу ForwardBase директиву препроцессора, чтобы он компилировал все необходимые варианты шейдера.
Приблизив камеру, мы можем заметить на поверхности травинок артефакты; они вызваны тем, что отдельные травинки отбрасывают тени сами на себя. Мы можем исправить это, применив линейный сдвиг или перенеся позиции вершин в пространстве усечения слегка вдаль от экрана. Мы будем использовать для этого макрос Unity и включим его в конструкцию #if , чтобы операция выполнялась только в проходе теней.
После применения линейного сдвига теней артефакты теней в виде полос исчезают с поверхности треугольников.
Даже при включенном многосэмпловом сглаживании (multisample anti-aliasing MSAA) Unity не применяет сглаживания к текстуре глубин сцены, которая используется для построения карты теней экранного пространства. Поэтому когда сглаженная сцена сэмплирует несглаженную карту теней, возникают артефакты.
Одно из решений — использовать сглаживание, применяемое на этапе постобработки, доступное в пакете постобработки Unity. Однако иногда сглаживание постобработки неприменимо (например при работе с виртуальной реальностью); альтернативные решения проблемы рассматриваются в этом треде форумов Unity.
7.3 Освещение
Мы будем реализовывать освещение при помощи очень простого и распространённого алгоритма вычисления рассеянного освещения.
… где N — нормаль к поверхности, L — нормализованное направление основного источника направленного освещения, а I — вычисленное освещение. В этом туториале мы не будем реализовывать отражённое освещение.
На данный момент вершинам травинок не назначены нормали. Как и в случае с позициями вершин, мы сначала вычислим нормали в касательном пространстве, а затем преобразуем их в локальное.
Когда Blade Curvature Amount имеет значение 1, все травинки в касательном пространстве направлены в одну сторону: прямо противоположно оси Y. В качестве первого прохода нашего решения мы вычислим нормаль, предполагая отсутствие кривизны.
tangentNormal , определяемая как прямо противоположная оси Y, преобразуется той же матрицей, которую мы использовали для преобразования касательных точек в локальное пространство. Теперь мы можем передавать её в функцию VertexOutput , а затем в структуру geometryOutput .
Заметьте, что перед выводом мы преобразуем нормаль в мировое пространство; Unity передаёт шейдерам направление основного источника направленного света в мировом пространстве, поэтому это преобразование необходимо.
Теперь мы можем визуализировать нормали во фрагментом шейдере ForwardBase , чтобы проверить результат своей работы.
Так как в нашем шейдере Cull присвоено значение Off , рендерятся обе стороны травинки. Чтобы нормаль была направлена в нужную сторону, мы используем вспомогательный параметр VFACE , добавленный нами во фрагментный шейдер.
Аргумент fixed facing будет возвращать положительное число, если мы отображаем переднюю грань поверхности, и отрицательное число, если обратную. Мы используем это в коде выше, чтобы при необходимости переворачивать нормаль.
Когда Blade Curvature Amount больше 1, касательная позиция Z каждой вершины будет смещена на величину forward , передаваемую функции GenerateGrassVertex . Мы воспользуемся этим значением для пропорционального масштабирования оси Z нормалей.
Наконец, добавим код во фрагментный шейдер, чтобы объединить тени, направленное освещение и окружающее освещение. Более подробную информацию о реализации настраиваемого освещения в шейдерах рекомендую изучить в моём туториале по toon-шейдерам.
Заключение
В этом туториале трава покрывает небольшую область размером 10×10 единиц. Чтобы шейдер мог покрывать обширные открытые пространства с сохранением высокой производительности, необходимо внести оптимизации. Можно применить тесселяцию на основании расстояния, чтобы вдали от камеры рендерилось меньше травинок. Кроме того, на дальних расстояниях вместо отдельных травинок можно отрисовывать группы травинок при помощи одного четырёхугольника с наложенной текстурой.
Текстура травы, включённая в пакет Standard Assets движка Unity. Множество травинок отрисовано на одном четырёхугольнике, что снижает количество треугольников в сцене.
Хоть нативно мы и не можем использовать геометрические шейдеры с поверхностными шейдерами, для усовершенствования или расширения функциональности освещения и затенения при необходимости применения стандартной модели освещения Unity можно изучить этот репозиторий GitHub, демонстрирующий решение проблемы при помощи отложенного рендеринга и ручного заполнения G-буферов.
Дополнение: ваимодействие
Без возможности взаимодействия графические эффекты могут казаться игрокам статичными или безжизненными. Этот туториал уже и так очень длинный, поэтому я не стал добавлять раздел о взаимодействии объектов мира с травой.
Наивная реализация интерактивной трав содержала бы два компонента: нечто в игровом мире, способное передавать данные в шейдер, чтобы сообщать ему, с какой частью травы выполняется взаимодействие, и код в шейдере для интерпретирования этих данных.
Пример того, как это можно реализовать с водой, показан здесь. Его можно адаптировать для работы с травой; вместо отрисовки ряби в месте, где находится персонаж, можно поворачивать травинки вниз для имитации воздействия шагов.
Источник