Я буду це робити в декілька шарів, щоб ви могли бачити, як воно поєднується.
Почніть зі створення нового шейдера в Unity, вибравши Create --> Shader --> Unlit
або меню Активи або контекстне меню правою кнопкою миші у вікні проекту.
У верхньому блоці ми додамо _ScrollSpeeds
параметр для контролю швидкості руху текстури в кожному напрямку:
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_ScrollSpeeds ("Scroll Speeds", vector) = (-5, -20, 0, 0)
}
Це відкриває нове 4-компонентне властивість float в інспекторі матеріалів з дружньою назвою "Scroll Speeds" (подібно до додавання public
або Serialized
змінної до MonoBehaviour
сценарію)
Далі ми використаємо цю змінну в шейдері Vertex для переміщення координат текстури ( o.uv
), додавши до шейдера за замовчуванням лише два рядки:
sampler2D _MainTex;
float4 _MainTex_ST;
// Declare our new parameter here so it's visible to the CG shader
float4 _ScrollSpeeds;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// Shift the uvs over time.
o.uv += _ScrollSpeeds * _Time.x;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
Пощеплюйте це на квадраті (з прекрасною текстурою жирафа Кенні ), і ви отримаєте:
Щоб текстура прокручувалася назовні в кільце, ми могли просто використовувати сітку, поділену як павутина, при цьому координата uv v збільшується від центру назовні. Але це дасть окремі артефакти у формі пиляння самостійно. Натомість я покажу, як ми можемо обчислити наші УФ-речовини на фрагмент.
Це трохи дорожче, як через операції триггерів та тривалості (які дорожчі, ніж основна математика), так і тому, що не так ефективно прогнозувати та кешувати текстурні дані в апараті при обчисленні текстових рядків на фрагмент, порівняно з простою інтерполяцією їх між вершинами. Але для такого особливого ефекту, як це, він не надмірний.
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// Shift the UVs so (0, 0) is in the middle of the quad.
o.uv = v.uv - 0.5f;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// Convert our texture coordinates to polar form:
float2 polar = float2(
atan2(i.uv.y, i.uv.x)/(2.0f * 3.141592653589f), // angle
length(i.uv) // radius
);
// Apply texture scale
polar *= _MainTex_ST.xy;
// Scroll the texture over time.
polar += _ScrollSpeeds.xy * _Time.x;
// Sample using the polar coordinates, instead of the original uvs.
// Here I multiply by MainTex
fixed4 col = tex2D(_MainTex, polar);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
Це дає нам щось подібне (тут я збільшив параметри плитки в матеріалі, щоб було зрозуміліше, що відбувається - загортання лише одного повторення плитки навколо кола виглядає спотвореним і дивним)
Нарешті, щоб відтінити текстуру за допомогою градієнта прокрутки, ми можемо просто додати градієнт як другу текстуру і помножити їх між собою.
Спочатку додаємо новий параметр текстури вгорі:
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_TintTex("Tint Texture", 2D) = "white" {}
_ScrollSpeeds ("Scroll Speeds", vector) = (-5.0, -20.0, 0, 0)
}
І оголосити це в нашому блоці CGPROGRAM, щоб шейдер CG міг його бачити:
sampler2D _MainTex;
float4 _MainTex_ST;
// Declare our second texture sampler and its Scale/Translate values
sampler2D _TintTex;
float4 _TintTex_ST;
float4 _ScrollSpeeds;
Потім оновіть наш фрагмент шейдер, щоб використовувати обидві текстури:
fixed4 frag(v2f i) : SV_Target
{
float2 polar = float2(
atan2(i.uv.y, i.uv.x) / (2.0f * 3.141592653589f), // angle
length(i.uv) // radius
);
// Copy the polar coordinates before we scale & shift them,
// so we can scale & shift the tint texture independently.
float2 tintUVs = polar * _TintTex_ST.xy;
tintUVs += _ScrollSpeeds.zw * _Time.x;
polar *= _MainTex_ST.xy;
polar += _ScrollSpeeds.xy * _Time.x;
fixed4 col = tex2D(_MainTex, polar);
// Tint the colour by our second texture.
col *= tex2D(_TintTex, tintUVs);
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
А тепер наша жирафа стає по-справжньому хитрою:
При трохи більш художньому підборі текстур та швидкості прокрутки це може створити ефект, подібний до того, який показано в питанні.
Ви можете помітити два невеликі артефакти з версією, яку я показав вище:
Обличчя біля центру кола отримують розтягнутий / худий / точковий, потім, рухаючись до зовнішньої сторони, вони стискаються / ширяються.
Це викривлення відбувається тому, що у нас є фіксована кількість граней по всьому периметру, але окружність, яку вони розтягують, стає ширшою, оскільки радіус збільшується, а їх висота залишається однаковою.
Ми можемо це виправити, переставивши вертикальний компонент зразка текстури, щоб слідувати логарифмічній кривій, тому повтори текстури віддаляються в міру збільшення радіуса і ближче до центру. (Насправді це дає нам нескінченний регрес все менших і менших жирафів ...)
Уздовж середини зліва від квадрата є ряд з одного або двох розмитих пікселів.
Це відбувається тому, що GPU розглядає дві суміжні координати зразка текстури, щоб визначити, яку фільтрацію використовувати. Коли зразки близькі один до одного, він показує, що текстура відображається великою / близькою та показує найточніший рівень мип. Коли зразки знаходяться далеко один від одного, він здогадується, що ми мусимо показувати текстуру в невеликому масштабі або далеко, і це зразки з меншої / розмитої карти карти, щоб гарантувати, що ми не отримаємо іскробезпечних артефактів.
Проблема тут полягає в тому, що ми знаходимося в точці обгортання в полярних координатах, від -180 до 180 градусів. Таким чином, ми фактично відбираємо вибірку з дуже схожих точок у нашому повторюваному просторі текстур, навіть якщо їх числові координати роблять їх схожими. Таким чином, ми можемо надати власні вектори градієнтів вибірки для виправлення цього.
Ось версія з цими виправленнями:
fixed4 frag(v2f i) : SV_Target
{
float2 polar = float2(
atan2(i.uv.y, i.uv.x) / (2.0f * 3.141592653589f), // angle
log(dot(i.uv, i.uv)) * 0.5f // log-radius
);
// Check how much our texture sampling point changes between
// neighbouring pixels to the sides (ddx) and above/below (ddy)
float4 gradient = float4(ddx(polar), ddy(polar));
// If our angle wraps around between adjacent samples,
// discard one full rotation from its value and keep the fraction.
gradient.xz = frac(gradient.xz + 1.5f) - 0.5f;
// Copy the polar coordinates before we scale & shift them,
// so we can scale & shift the tint texture independently.
float2 tintUVs = polar * _TintTex_ST.xy;
tintUVs += _ScrollSpeeds.zw * _Time.x;
polar *= _MainTex_ST.xy;
polar += _ScrollSpeeds.xy * _Time.x;
// Sample with our custom gradients.
fixed4 col = tex2Dgrad(_MainTex, polar,
_MainTex_ST.xy * gradient.xy,
_MainTex_ST.xy * gradient.zw
);
// Since our tint texture has its own scale,
// its gradients also need to be scaled to match.
col *= tex2Dgrad(_TintTex, tintUVs,
_TintTex_ST.xy * gradient.xy,
_TintTex_ST.xy * gradient.zw
);
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
_Time
змінна, яку ви можете додати до текстурних координат у (вершинній) шейдері, щоб дешево отримати бруд ефекту прокрутки. Ефект шестикутника теж був би досить простим. Якщо ви відредагуєте своє запитання, щоб виділити лише один ефект і запитати "як би я реалізував це в шейдері Unity", ми можемо вам допомогти. Обов’язково вкажіть, чи повинен шейдер реагувати на освітлення / затінення, оскільки це впливає на те, як це було б написано.