Возможности геометрического шейдера: треугольники в частицы
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
