Возможности геометрического шейдера: треугольники в частицы
VFX Mike расскажет про сумасшедшие возможности геометрического шейдера: превратить треугольники в частицы заданной формы, добавить им сложное движение — и это не предел!
Геометрические шейдеры — это крутая штука, потому что они позволяют превращать треугольник практически во что угодно. При условии, что выходная информация не превышает 1 килобайт (не спрашивайте меня почему). Перед вами простой геометрический шейдер, который превращает в четырехугольники все треугольники, обращенные к экрану, и придает им движение наподобие перемещения частиц, которое может управляться некоторыми параметрами. Если хотите запустить приведенный выше пример в Unity, качайте пакет ресурсов ниже. Экспортируется с 2017.4.3f1, но должно работать и для других версий, так как это просто неосвещенный шейдер.
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