Еще один шейдер травы. Часть 1
Введение
В предыдущем уроке мы посмотрели некоторые вещи, связанные с геометрическими шейдерами. Те имеют много действительно интересных приложений, и шейдер для травы — одно из самых популярных!
Шейдеры травы, как и шейдеры воды, могут быть бесконечно увлекательными. Вы можете достичь любого уровня физической точности, какого захотите: от карикатурного и стилизованного до реалистичного. Результат, очевидно, зависит от работы, которую вы вложили в эффект.
Шейдер, который я покажу здесь, несет в себе тенденцию к стилизации, на самом деле он совершенно не освещен и не реагирует на тени. В следующей части мы добавим эти вещи и кое-что еще, но если вы спешите узнать все сами, я настоятельно рекомендую обучающий курс по шейдерам для травы от Roystan. Кроме того, я сам почерпнул некоторые приемы по лайтингу из этого урока.
Надеюсь, вы догадаетесь сами, что надо бы сначала повторить предыдущий урок, так как я не буду слишком углубляться в некоторые из более базовых понятий, которые уже рассмотрел.
Обзор шейдера
Прежде чем показать код, я сначала сделаю краткое изложение того, что делает этот конкретный шейдер и как он это делает. Короче говоря, его особенности:
- Шейдер генерирует на каждый треугольник переменное количество травинок в случайных положениях, высотах и ориентациях.
- Максимальное количество установленных травинок на данный момент составляет 15.
- Шейдер также вычисляет новые UV-координаты для сгенерированных травинок.
- Для небольшой оптимизации шейдер будет генерировать меньше травинок в зависимости от расстояния до камеры.
- Травинки будут колебаться по текстуре смещения, напоминающей движение на ветру.
- Травинки будут градиентно окрашены, с использованием собственных цветов вершин. Чтобы подчеркнуть движение на ветру, при изгибе они будут получать дополнительный серебристый цвет.
Алгоритм генерации травинки следующий:
- Мы получаем вектор нормали текущего треугольника и количество травинок, которые породим (в зависимости от расстояния до камеры).
- Для каждой травинки:
- Мы получаем случайные числа, основанные на мировом положении вершин треугольника
- Рассчитываем случайную точку в треугольнике, используя эту технику. Назовем ее «серединой».
- Рассчитываем две другие точки, по одной на каждой стороне от средней точки. В качестве ориентира для направления смещения этих точек мы используем одну из вершин треугольника.
- Рассчитываем вклад ветра путем выборки текстуры смещения.
- Рассчитываем смещение высоты путем выборки текстуры шума.
- Верхняя точка травинки будет в том же положении, что и средняя точка, но она будет смещена вдоль вектора нормали треугольника. Точка также будет смещена по осям X и Z на основе случайной величины, умноженной на внешнее свойство, и также смещена по величине ветра.
- В цветах вершин для верхней точки мы сохраняем вклад ветра в зеленом канале и устанавливаем для других каналов значение 1.
- Для остальных двух точек, которые мы рассчитали, цвет вершин будет установлен на черный.
- Три точки добавляются к потоку треугольника
Мы добавляем исходные вершины треугольника к потоку треугольника. - ???
- PROFIT!!!
Более визуализированное описание алгоритма генерации травинки будет таким:
Не переживайте, если что-то сейчас кажется непонятным. Большинство из этих вещей мы еще будем обсуждать в коде.
Код
Давайте теперь посмотрим на код:
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/