Еще один шейдер травы. Часть 1

Введение
В предыдущем уроке мы посмотрели некоторые вещи, связанные с геометрическими шейдерами. Те имеют много действительно интересных приложений, и шейдер для травы — одно из самых популярных!
Шейдеры травы, как и шейдеры воды, могут быть бесконечно увлекательными. Вы можете достичь любого уровня физической точности, какого захотите: от карикатурного и стилизованного до реалистичного. Результат, очевидно, зависит от работы, которую вы вложили в эффект.
- Devolver Digital опровергли слух о появлении Fall Guys: Ultimate Knockout в сервисе Xbox Game Pass
- Геймеры шутят о «Дворце Путина». Подборка мемов
- Студия Liquid Engine Development анонсировала проект Buried in Ice, вдохновлённый фильмом «Нечто»
- Античит от Denuvo стал доступен для участников программы Valve Steamworks
- Обзор Jet Kave Adventure - захватывающее приключение в доисторическом сеттинге
- Шейдеры в Unity: введение в тему от Cyanilux
- Control войдёт в PC-библиотеку Game Pass в ближайшее время
- «Кинохоррор» Erica вышел на iOS
- Xbox Wire опубликовал обращение от разработчиков S.T.A.L.K.E.R. 2.
Шейдер, который я покажу здесь, несет в себе тенденцию к стилизации, на самом деле он совершенно не освещен и не реагирует на тени. В следующей части мы добавим эти вещи и кое-что еще, но если вы спешите узнать все сами, я настоятельно рекомендую обучающий курс по шейдерам для травы от Roystan. Кроме того, я сам почерпнул некоторые приемы по лайтингу из этого урока.
Надеюсь, вы догадаетесь сами, что надо бы сначала повторить предыдущий урок, так как я не буду слишком углубляться в некоторые из более базовых понятий, которые уже рассмотрел.
Обзор шейдера
Прежде чем показать код, я сначала сделаю краткое изложение того, что делает этот конкретный шейдер и как он это делает. Короче говоря, его особенности:
- Шейдер генерирует на каждый треугольник переменное количество травинок в случайных положениях, высотах и ориентациях.
- Максимальное количество установленных травинок на данный момент составляет 15.
- Шейдер также вычисляет новые UV-координаты для сгенерированных травинок.
- Для небольшой оптимизации шейдер будет генерировать меньше травинок в зависимости от расстояния до камеры.
- Травинки будут колебаться по текстуре смещения, напоминающей движение на ветру.
- Травинки будут градиентно окрашены, с использованием собственных цветов вершин. Чтобы подчеркнуть движение на ветру, при изгибе они будут получать дополнительный серебристый цвет.
Алгоритм генерации травинки следующий:
- Мы получаем вектор нормали текущего треугольника и количество травинок, которые породим (в зависимости от расстояния до камеры).
- Для каждой травинки:
- Мы получаем случайные числа, основанные на мировом положении вершин треугольника
- Рассчитываем случайную точку в треугольнике, используя эту технику. Назовем ее «серединой».
- Рассчитываем две другие точки, по одной на каждой стороне от средней точки. В качестве ориентира для направления смещения этих точек мы используем одну из вершин треугольника.
- Рассчитываем вклад ветра путем выборки текстуры смещения.
- Рассчитываем смещение высоты путем выборки текстуры шума.
- Верхняя точка травинки будет в том же положении, что и средняя точка, но она будет смещена вдоль вектора нормали треугольника. Точка также будет смещена по осям X и Z на основе случайной величины, умноженной на внешнее свойство, и также смещена по величине ветра.
- В цветах вершин для верхней точки мы сохраняем вклад ветра в зеленом канале и устанавливаем для других каналов значение 1.
- Для остальных двух точек, которые мы рассчитали, цвет вершин будет установлен на черный.
- Три точки добавляются к потоку треугольника
Мы добавляем исходные вершины треугольника к потоку треугольника. - ???
- PROFIT!!!
Более визуализированное описание алгоритма генерации травинки будет таким:




Не переживайте, если что-то сейчас кажется непонятным. Большинство из этих вещей мы еще будем обсуждать в коде.
Код
Давайте теперь посмотрим на код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
Shader "Geometry/GrassGeometryShaderUnlit" { Properties { // Цвет _Color("Color", Color) = (1,1,1,1) _GradientMap("Gradient map", 2D) = "white" {} // Шум и ветер _NoiseTexture("Noise texture", 2D) = "white" {} _WindTexture("Wind texture", 2D) = "white" {} _WindStrength("Wind strength", float) = 0 _WindSpeed("Wind speed", float) = 0 _WindColor("Wind color", Color) = (1,1,1,1) // Положение и размеры _GrassHeight("Grass height", float) = 0 _GrassWidth("Grass width", Range(0.0, 1.0)) = 1.0 _PositionRandomness("Position randomness", float) = 0 // Травинки _GrassBlades("Grass blades per triangle", Range(0, 15)) = 1 _MinimunGrassBlades("Minimum grass blades per triangle", Range(0, 15)) = 1 _MaxCameraDistance("Max camera distance", float) = 10 } SubShader { CGINCLUDE #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; }; struct v2g { float4 vertex : POSITION; }; struct g2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; float4 col : COLOR; }; fixed4 _Color; sampler2D _GradientMap; sampler2D _NoiseTexture; float4 _NoiseTexture_ST; sampler2D _WindTexture; float4 _WindTexture_ST; float _WindStrength; float _WindSpeed; fixed4 _WindColor; float _GrassHeight; float _GrassWidth; float _PositionRandomness; float _GrassBlades; float _MinimunGrassBlades; float _MaxCameraDistance; float random (float2 st) { return frac(sin(dot(st.xy, float2(12.9898,78.233)))* 43758.5453123); } g2f GetVertex(float4 pos, float2 uv, fixed4 col) { g2f o; o.vertex = UnityObjectToClipPos(pos); o.uv = uv; o.col = col; return o; } v2g vert (appdata v) { v2g o; o.vertex = v.vertex; return o; } // 3 + 3 * 15 = 48 [maxvertexcount(48)] void geom(triangle v2g input[3], inout TriangleStream < g2f > triStream) { float3 normal = normalize(cross(input[1].vertex - input[0].vertex, input[2].vertex - input[0].vertex)); int grassBlades = ceil(lerp(_GrassBlades, _MinimunGrassBlades, saturate(distance(_WorldSpaceCameraPos, mul(unity_ObjectToWorld, input[0].vertex)) / _MaxCameraDistance))); for (uint i = 0; i < grassBlades; i++) { float r1 = random(mul(unity_ObjectToWorld, input[0].vertex).xz * (i + 1)); float r2 = random(mul(unity_ObjectToWorld, input[1].vertex).xz * (i + 1)); // Случайные барицентрические координаты по https://stackoverflow.com/a/19654424 float4 midpoint = (1 - sqrt(r1)) * input[0].vertex + (sqrt(r1) * (1 - r2)) * input[1].vertex + (sqrt(r1) * r2) * input[2].vertex; r1 = r1 * 2.0 - 1.0; r2 = r2 * 2.0 - 1.0; float4 pointA = midpoint + _GrassWidth * normalize(input[i % 3].vertex - midpoint); float4 pointB = midpoint - _GrassWidth * normalize(input[i % 3].vertex - midpoint); float4 worldPos = mul(unity_ObjectToWorld, midpoint); float2 windTex = tex2Dlod(_WindTexture, float4(worldPos.xz * _WindTexture_ST.xy + _Time.y * _WindSpeed, 0.0, 0.0)).xy; float2 wind = (windTex * 2.0 - 1.0) * _WindStrength; float noise = tex2Dlod(_NoiseTexture, float4(worldPos.xz * _NoiseTexture_ST.xy, 0.0, 0.0)).x; float heightFactor = noise * _GrassHeight; triStream.Append(GetVertex(pointA, float2(0,0), fixed4(0,0,0,1))); float4 newVertexPoint = midpoint + float4(normal, 0.0) * heightFactor + float4(r1, 0.0, r2, 0.0) * _PositionRandomness + float4(wind.x, 0.0, wind.y, 0.0); triStream.Append(GetVertex(newVertexPoint, float2(0.5, 1), fixed4(1.0, length(windTex), 1.0, 1.0))); triStream.Append(GetVertex(pointB, float2(1,0), fixed4(0,0,0,1))); triStream.RestartStrip(); } for (int i = 0; i < 3; i++) { triStream.Append(GetVertex(input[i].vertex, float2(0,0), fixed4(0,0,0,1))); } triStream.RestartStrip(); } fixed4 frag (g2f i) : SV_Target { fixed4 gradientMapCol = tex2D(_GradientMap, float2(i.col.x, 0.0)); fixed4 col = (gradientMapCol + _WindColor * i.col.g) * _Color; return col; } ENDCG Pass { Tags { "RenderType"="Opaque"} Cull Off CGPROGRAM #pragma vertex vert #pragma geometry geom #pragma fragment frag ENDCG } } } |
И еще не паникуйте заранее. Этот шейдер следует иному формату, который будет полезен во второй части. Вы можете заметить, что проход отделен от большей части логики и вместо «CGPROGRAM» в строке 29 «CGINCLUDE». Этот формат означает, что мы можем заранее объявить функциональность фрагмента / вершинного / геометрического шейдера, а затем просто включить его в проход. Если бы нам, например, потребовалось сделать второй проход с тем же геометрическим шейдером, но с другим фрагментным, этот формат очень полезен, потому что не надо снова писать тело шейдера.
Перейдем теперь к свойствам:
Свойства
_Color | Цвет будет умножен на результат карты градиента |
_GradientMap | Текстура карты градиента, которая будет использоваться для закрашивания |
_NoiseTexture | Текстура шума, которая определяет случайную высоту травинок |
_WindTexture | Текстура смещения для эффекта ветра |
_WindStrength | Сила эффекта ветра |
_WindSpeed | Скорость, с которой применяется текстура смещения ветром |
_WindColor | Это странное название говорит о том, что данный цвет будет добавлен к конечному цвету для имитации блеска травы, гнущейся под порывами ветра |
_GrassHeight | Максимальная высота травинок |
_GrassWidth | Ширина травинок |
_PositionRandomness | Величина случайного смещения, которое кончик травинки будет иметь по осям X и Z. При 0 травинки будут вертикально прямыми. |
_GrassBlades | Максимальное количество травинок на треугольник. |
_MinimunGrassBlades | Минимальное количество травинок на треугольник, когда камера находится далеко |
_MaxCameraDistance | Расстояние от камеры, при котором травинок становится меньше |
Промежуточные рассуждения
Сначала в строках 33-48 я объявляю структуры, которые буду использовать для передачи данных из вершинного шейдера в геометрический шейдер, а затем во фрагментный шейдер. На данный момент мне просто нужно положение вершин и ничего больше. Поскольку я не добавляю текстуру в исходную сетку, мне даже не нужны UV-координаты объекта, я просто генерирую UV-координаты для травинок.
На самом деле, поскольку я не даю текстуру травинкам, мне даже не нужно генерировать их UV-координаты, но я оставил эту часть на случай, как мы могли бы их генерировать, если понадобятся.
Вы могли также заметить, что структура appdata точно такая же, как структура v2g, и да, мы могли бы фактически использовать одну и ту же структуру в обоих случаях! Но чтобы было понятнее и чтобы сохранить весь конвейер вершинный > геометрический > фрагментный, я сделал их отдельными.
В структуру g2f я также добавляю UV и цвет вершин, чтобы использовать его во фрагментном шейдере.
В строках 50-67 я заново объявляю свои свойства (не забывайте поля _ST!), а затем в строках 69-73 я добавляю случайную хэш-функцию, которую вы, возможно, уже видели в других уроках, и почти во всех шейдерах, которые используют случайные величины.
Наконец, чтобы избежать повторения кода, в строках 76-82 у меня есть метод для передачи необходимой мне информации в объект g2f и добавления ее в поток. Этот метод всего лишь конвертирует заданную позицию вершины в пространстве объектов в пространство отсечения и передает ее, UV и цвет во вновь созданный объект g2f. Достаточно просто.
Вершинный шейдер
Здесь шейдер вершин теряет значительную часть своего гламура и всего лишь передает положение из appdata в объект v2g. Даже немного грустно.
Геометрический шейдер
Вот где все сходится вместе.
Во-первых, в строке 92 максимальное число вершин у меня 48. Это потому, что, как я упоминал выше, у нас может быть максимум 15 травинок на треугольник. Следовательно, нам нужно 3 стандартных вершины граней плюс 3 вершины для каждой травинки, поэтому 3 * 15 + 3 = 48. Если мы хотим больше травинок на треугольник, это число должно увеличиться на 3 для каждой дополнительной травинки.
В строке 95 я вычисляю вектор нормали к плоскости треугольника. Это не тот способ, что в предыдущем уроке, но он так же эффективен. Я просто привожу его здесь как альтернативу.
В строке 96 я определяю количество травинок, которые будут появляться в текущем треугольнике. Я делаю это, используя lerp для интерполяции между максимальным и минимальным количеством травинок, основываясь на расстоянии первой вершины от камеры. Более правильным подходом было бы взять барицентр треугольника, но так тоже работает. В большинстве случаев смещение не будет значительным. Суть данной строки кода в том, что чем меньше расстояние от камеры, тем больше будет сгенерированных травинок. Если расстояние равно или больше «_MaxCameraDistance», то количество сгенерированных травинок равно «_MinimumGrassBlades».
Двигаясь внутри цикла для каждой травинки, в строках 99 и 100 я получаю два случайных числа, основанные на положении в мире первой и второй вершин текущего треугольника, соответственно. Опять же, использование первых двух точек треугольников немного произвольно, но преобразование в мировое пространство помогает удерживать начало привязанным к мировому пространству и иметь множество участков травы без повторений, если мы захотим. Умножение на (i + 1) также помогает получить разные начала для каждой травинки. Опять же, не совсем правильно, но вы понимаете, в чем суть.
В строке 103 я вычисляю случайную среднюю точку травинки, используя этот метод, а затем в строках 105 и 106 я переназначаю случайные числа из промежутка [0,1] в [-1,1], который будет использоваться для рандомизации положения.
В строках 108-109 я вычисляю две точки основания вновь сгенерированного треугольника, смещая среднюю точку на значение «_GrassWidth» в направлении точки треугольника. Еще раз, «i% 3» является произвольным, и оно нужно, чтобы травинки не вращались одинаково. Сейчас это работает так, что основание первой травинки направлено вдоль линии к первой точке треугольника, и каждая травинка будет циклически проходить через точки.
В строке 111 я получаю положение в мировом пространстве для «средней точки», чтобы использовать в качестве координат для текстур ветра и шума. Сэмплирование текстуры ветра происходит в строках 113-114, и это все тот же самый метод, который мы видели множество раз, когда дело касается текстур смещения. Сэмплировать текстуру, сместить UV по времени (здесь это мировое положение компонентов x и z) и затем поместить все в диапазон от -1 до 1 и умножить на силу эффекта. Здесь я также умножаю мировую позицию на «_WindTexture_ST.xy», чтобы можно было регулировать масштаб ветра с помощью наложения текстуры в инспекторе материалов.
В строках 116 и 117 я вычисляю окончательную высоту травинки путем сэмплирования текстуры шума так же, как для текстуры ветра, и умножения результата на “_GrassHeight”.
Держите в голове, что оба сэмплирования текстур здесь происходят с “tex2Dlod” вместо“tex2D”, потому что мы находимся не во фрагментном шейдере.
Теперь, наконец-то пришло время делать травинки! Перед тем, как перейти к коду, я хочу объяснить что делать здесь с UV-шками травинок:

Если мы хотим добавить к травинкам текстуру, вот хороший способ сделать это. Как вы знаете, UV координаты идут от (0,0) внизу слева к (1,1) вверху справа. Поэтому если нам нужно, чтобы травинки покрывали весь спектр равнобедренных треугольников, первая точка в основании должна иметь UV (0,0), вторая (1,0), а верхняя — (0.5,1).
Сказав это, в строке 119 я добавляю первую точку травинки с (0,0) в качестве UV координаты и черным в качестве цвета вершины. Затем в строке 121 я вычисляю положение верхней точки следующим образом:
Я получаю среднюю точку и сначала смещаю ее по вектору нормали с помощью “heightFactor”, затем я смещаю ее по осям x и z, сначала используя полученные раньше и умноженные на “_PositionRandomness” случайные числа “r1” и “r2”, затем используя ветер.
Затем я добавляю эту точку в поток, назначая (0.5,1) в качестве ее UV координат. В качестве цвета я использую 1 для красного, синего и альфа каналов, и длину красного и зеленого значений из текстуры ветра для зеленого канала. Таким способом в дальнейшем я смогу настроить соответствие цветов и интенсивности ветра.
После этого я добавляю третью точку с UV (1,0) и также назначаю ей черную вершину. Потом я еще перезагружаю стрип, поскольку когда цикл перезагружается, мы делаем новый треугольник.
Наконец, в строках 130-132 я добавляю реальные вершины поверхности треугольника к потоку и также использую (0,0) как UV и черный как цвет вершины.
Фрагментный шейдер
Фрагментный шейдер сейчас достаточно прост: я лишь сэмплирую карту градиента в красном канале цвета вершин, который, как вы помните, был 1 на кончике травинок и 0 у основания. Это дает нам красивый серый градиент, от черного основания до белого кончика. Вдобавок, левая сторона текстуры градиента должна быть темнее, а правая светлее. Я применил описанную текстуру для этого изображения:

Окончательный цвет затем вычисляется прибавлением “_WindColor”, умноженного на зеленый канал цвета вершин , к цвету из карты градиента, и затем умножению суммы на “_Color”. Цвет получился, все отлично!
Реальный проход
Как я отметил вначале, здесь применен несколько отличающийся формат. Наши шейдеры находятся не в блоке «Pass», как мы привыкли, а в состоянии неопределенности внутри блока «CGINCLUDE». Круто, однако, что они все еще находятся в одном файле с проходом. Поэтому в блоке «Pass» в строках 148–158 нам просто нужно сопоставить соответствующие директивы «#pragma» с именами шейдеров, и мы в порядке! Они будут в просто включены в проход, как если бы мы написали их там сначала.
Поскольку наши шейдеры носят имена “vert”, “geom” и “frag” для вершинного, геометрического и фрагментного соответственно, тэги “#pragma” в строках 135-155 делают для нас всю работу. Также не забудьте добавить директиву “Cull off” в строке 151, иначе большая часть травинок станет невидима с определенной точки просмотра, относительно порядка из визуализации.
Заключение
Это окончание первой части шейдера для травы! В следующий раз мы увидим некоторые более увлекательные вещи вроде лайтинга, теней и т.п. Но даже с таким шейдером вы можете сделать множество различных штук! Например, вместо генерации отдельных травинок вы можете попробовать генерацию квадов в форме звезды (смотрите GPU Gems on grass) и добавлять к ним текстуру травы. Надеюсь, при помощи этого тьюториала вы сделаете потрясающие сцены с травой. Мне бы хотелось, чтобы вы поделились своими работами или хотя бы показали их мне. Так что любым способом дайте знать, если вы хоть как-то смогли использовать данный шейдер!
Увидимся в следующий раз.
Код в тьюториалах по шейдерам распространяется по лицензии CC0, поэтому вы можете свободно использовать его в коммерческих и некоммерческих проектах без каких-либо упоминаний/благодарностей Гарри Алисавакису.
Источник: https://halisavakis.com/my-take-on-shaders-grass-shader-part-i/