Возможности геометрического шейдера: треугольники в частицы

Возможности геометрического шейдера: треугольники в частицы

VFX Mike расскажет про сумасшедшие возможности геометрического шейдера: превратить треугольники в частицы заданной формы, добавить им сложное движение — и это не предел!

Геометрические шейдеры — это крутая штука, потому что они позволяют превращать треугольник практически во что угодно. При условии, что выходная информация не превышает 1 килобайт (не спрашивайте меня почему). Перед вами простой геометрический шейдер, который превращает в четырехугольники все треугольники, обращенные к экрану, и придает им движение наподобие перемещения частиц, которое может управляться некоторыми параметрами. Если хотите запустить приведенный выше пример в Unity, качайте пакет ресурсов ниже. Экспортируется с 2017.4.3f1, но должно работать и для других версий, так как это просто неосвещенный шейдер.

MeshToParticle.unitypackage

Shader "Unlit/MeshToParticle"
{
 Properties
 {
  _MainTex ("Texture", 2D) = "white" {}
  _Color ("Color", Color) = (1,1,1,1)
  _Factor ("Factor", Float) = 2.0
 
  _Ramp ("Ramp", Range(0,1)) = 0
  _Size ("Size", Float) = 1.0
  _Spread ("Random Spread", Float) = 1.0
  _Frequency ("Noise Frequency", Float) = 1.0
  _Motion ("Motion Distance", Float) = 1.0
 
  _InvFade ("Soft Particles Factor", Range(0.01,3.0)) = 1.0
 }

Это просто параметры, которые будут управлять частицами.

_MainTex текстура частиц.
_Color цвет частицы, получается умножением на цвет вершины.
_Factor насколько яркими должны быть частицы (для повышения значений более1).
_Ramp управляет временем жизни частиц и сдвигает их вперед и назад.
_Size размер частиц в мировом пространстве.
_Spread насколько далеко будут сдвигаться частицы в случайном направлении.
_Frequency частота волнообразного шума, который будет добавляться к частицам в течение их времени жизни.
_Motion как далеко частицы будут перемещаться.
_InvFade для смешивания глубины с непрозрачными объектами.

SubShader
{
 Tags { "Queue"="Transparent" "RenderType"="Transparent"}
 Blend One OneMinusSrcAlpha
 ColorMask RGB
 Cull Off Lighting Off ZWrite Off
 LOD 100
 
 Pass
 {
  CGPROGRAM
  #pragma vertex vert
  #pragma geometry geom
  #pragma fragment frag
  #pragma target 4.0
  #pragma multi_compile_particles

О шейдерах. Геометрический шейдер располагается между вершинным и пиксельным шейдерами: Vertex Shader -> Geometry Shader -> Pixel Shader

#include "UnityCG.cginc"
 
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
float _Factor;
 
float _Ramp;
float _Size;
float _Frequency;
float _Spread;
float _Motion;
 
sampler2D_float _CameraDepthTexture;
float _InvFade;

Просто определяем используемые переменные.

// данные, которые приходят из unity
struct appdata
{
 float4 vertex : POSITION;
 float4 texcoord : TEXCOORD0;
 fixed4 color : COLOR;
};

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

// вершинный шейдер в основном просто передает информацию в геометрический
appdata vert (appdata v)
{
 appdata o;
 
 // отобразить положение на мировое пространство
 float3 worldPos = mul( unity_ObjectToWorld, v.vertex ).xyz;
 o.vertex = float4(worldPos,1);
 
 // передать дальше без изменения
 o.texcoord = v.texcoord;
 o.color = v.color;
 
 return o;
}

Вершинный шейдер просто отображает положение на мировое пространство.

// информация для пересылки в пиксельный шейдер
struct v2f {
 float4 vertex : SV_POSITION;
 fixed4 color : COLOR;
 float2 texcoord : TEXCOORD0;
 #ifdef SOFTPARTICLES_ON
  float4 projPos : TEXCOORD1;
 #endif
};

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

// геометрическая функция для вершин
// она вызывается в геометрическом шейдере
// очень полезно хранить все что нужно в самостоятельной функции
v2f geomVert (appdata v)
{
 v2f o;
 o.vertex = UnityWorldToClipPos(v.vertex.xyz);
 o.color = v.color;
 o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
 #ifdef SOFTPARTICLES_ON
  o.projPos = ComputeScreenPos (o.vertex);
  // поскольку вершины уже находятся в мировом пространстве
  // нужно убрать кое-что из функции COMPUTE_EYEDEPTH 
  // COMPUTE_EYEDEPTH(o.projPos.z);
  o.projPos.z = -mul(UNITY_MATRIX_V, v.vertex).z;
 #endif
 
 return o;
}

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

   // геометрический шейдер
   [maxvertexcount(4)]
   void geom(triangle appdata input[3], inout TriangleStream stream )
   {
    // получить значения для центров треугольника
    float3 pointPosWorld = (input[0].vertex.xyz + input[1].vertex.xyz + input[2].vertex.xyz ) * 0.3333333;
    float4 pointColor = (input[0].color + input[1].color + input[2].color ) * 0.3333333;
    float4 uv = (input[0].texcoord + input[1].texcoord + input[2].texcoord ) * 0.3333333;
 

Это реальный геометрический шейдер, настоящее мясо всего эффекта. Этот геометрический шейдер получает треугольник (массив из 3 структур appdata), поэтому для начала мы получаем значения для центра треугольника, усредняя 3 его точки.

// время жизни, основанное на параметрах сетки
half lifeTime = saturate( uv.x + lerp( -1.0, 1.0, _Ramp ) );
 
// зажечь или потушить частицу в зависимости от времени жизни
float fade = smoothstep( 0.0, 0.1, lifeTime);
fade *= 1.0 - smoothstep( 0.1, 1.0, lifeTime);
 
// не рисовать невидимые частицы
if( fade == 0.0 ){
 return;
}
 
// умножить прозрачность цвета на значение затухания
pointColor.w *= fade;

Время жизни частицы основано на значении uv.x и находится в пределах _Ramp. Это заставляет время жизни изменяться от 0 до 1 в координатах текстуры. Значение затухания генерируется на основе времени жизни, и если оно равно 0,0 (до или после жизни), мы ничего не возвращаем, пропуская пиксельный шейдер и оставшуюся работу для этой частицы.

// ядро случайного числа взято из uv координат
float3 seed = float3( uv.x + 0.3 + uv.y * 2.3, uv.x + 0.6 + uv.y * 3.1, uv.x + 0.9 + uv.y * 9.7 );
// случайное число для каждой частицы, сделанное из ядра
float3 random3 = frac( sin( dot( seed * float3(138.215, 547.756, 318.269), float3(167.214, 531.148, 671.248) ) * float3(158.321,456.298,725.681) ) * float3(158.321,456.298,725.681) );
// случайное направление из случайного числа
float3 randomDir = normalize( random3 - 0.5 );

Для каждой частицы мы производим случайное число. Оно может использоваться, чтобы добавить разнообразия в размеры, цвет, поворот. Но в данном примере добавляется только случайное направление движения.

// волнообразный шум, чтобы частицы двигались интересным образом
float3 noise3x = float3( uv.x, uv.x + 2.3, uv.x + 5.7 ) * _Frequency;
float3 noise3y = float3( uv.y + 7.3, uv.y + 9.7, uv.y + 12.3 ) * _Frequency;
float3 noiseDir = sin(noise3x.yzx * 5.731 ) * sin( noise3x.zxy * 3.756 ) * sin( noise3x.xyz * 2.786 );
noiseDir += sin(noise3y.yzx * 7.731 ) * sin( noise3y.zxy * 5.756 ) * sin( noise3y.xyz * 3.786 );

Мы также генерируем шум с синусоидальными функциями на основе uvs. Это придает частицам красивое спиралевидное движение.

// добавить случайное направление перемещения и спирали в мировое пространство
pointPosWorld += randomDir * lifeTime * _Motion * _Spread;
pointPosWorld += noiseDir * lifeTime * _Motion;

Теперь добавляем случайное и зашумленное движение в мировое пространство частиц.

// верхнее и левое направления камеры, чтобы она смотрела на квад частицы
float3 camUp = UNITY_MATRIX_V[1].xyz * _Size * 0.5;
float3 camLeft = UNITY_MATRIX_V[0].xyz * _Size * 0.5;
 
// v1-----v2
// |     / |
// |    /  |
// |   C   |
// |  /    |
// | /     |
// v3-----v4
 
float3 v1 = pointPosWorld + camUp + camLeft;
float3 v2 = pointPosWorld + camUp - camLeft;
float3 v3 = pointPosWorld - camUp + camLeft;
float3 v4 = pointPosWorld - camUp - camLeft;

Направления вверх и влево для камеры спрятаны в видовую матрицу и мы можем использовать их для генерации положений 4 вершин.

// отправить информацию для каждой вершины в функции geomVert
 
 appdata vertIN;
 vertIN.color = pointColor;
 
 vertIN.vertex = float4(v1,1);
 vertIN.texcoord.xy = float2(0,1);
 stream.Append( geomVert(vertIN) );
 
 vertIN.vertex = float4(v2,1);
 vertIN.texcoord.xy  = float2(1,1);
 stream.Append( geomVert(vertIN) );
 
 vertIN.vertex = float4(v3,1);
 vertIN.texcoord.xy  = float2(0,0);
 stream.Append( geomVert(vertIN) );
 
 vertIN.vertex = float4(v4,1);
 vertIN.texcoord.xy  = float2(1,0);
 stream.Append( geomVert(vertIN) );
 
}

Теперь мы можем отправить обновленные данные appdata в функцию geomVert и добавить результат. Цвет будет одинаковым для всех вершин в кваде, но координаты положения и текстуры перед отправкой данных appdata в функцию geomVert должны быть обновлены.

stream.Append() добавляет вершину к треугольному стрипу. Первые 3 добавления создают первый треугольник, а четвертое добавление производит второй треугольник из 1 новой вершины и 2 старых. Эта штупа известна как треугольный стрип — triangle strip (супер олдскульный термин). Вы можете продолжить добавление вершин, каждая новая вершина будет производить новый треугольник с парой старых вершин. Таким способом можно сделать траву.

   // простой пиксельный шейдер, как с частицами
   fixed4 frag (v2f IN) : SV_Target
   {
    #ifdef SOFTPARTICLES_ON
     float sceneZ = LinearEyeDepth (SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(IN.projPos)));
     float partZ = IN.projPos.z;
     IN.color.w *= saturate (_InvFade * (sceneZ-partZ));
    #endif
 
    // сэмплирование текстуры
    fixed4 col = tex2D(_MainTex, IN.texcoord);
    col *= _Color;
    col *= IN.color;
    col.xyz *= _Factor;
 
    // предварительно перемноженная прозрачность
    col.xyz *= col.w;
 
    return col;
   }
   ENDCG
  }
 }
}

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

Источник: http://vfxmike.blogspot.com/2018/07/geometry-shader-adventures-mesh.html

Понравилась статья? Поделиться с друзьями:
Автор natalya
Переводит для Вас самые интересные статьи про разработку игр. По образованию физик-программист. Техническими переводами начала подрабатывать еще на старших курсах и постепенно это переросло в основное занятие. Интересуется гуманитарными технологиями, пробует себя в журналистике.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *