Рискуя нахватать по шапке, я все таки задам этот вопрос.
Встала необходимость написать шейдер для Unity3d, казалось бы простейший, радиально
проявляющееся изображение (как на gif ниже), но с эффектами блюра и градиентной прозрачности.
То есть кусок сегмента (например градусов 30), который заполняет изображение - не
резко видимый, а с плавным переходом прозрачности (пример на рисунке ниже).
При этом изображение при нулевой прозрачности имеет максимальную заблюренность (хоть
на прозрачном и не видно, да и на этом примере не очень понятно, но т.к. при использовании
шейдер будет применяться на переходах между фото, то там будет все четко видно) и по
мере того, как прозрачность отдельно взятых пикселей уменьшается, уменьшается и степень
блюра на них. Таким образом там, где прозрачность 100 (непрозрачный), там блюр = 0.
Прочитав несколько статей все же никак не могу разложить по полочкам это все в голове.
Прочитал как именно реализуется размытие по гауссу (усреднение значение rgb ближайших
пикселей на спрайте по каждому столбцу/линии), понимаю что к этому необходимо прицепить
прозрачность и менять одно в зависимости от другого, но, например, как это все сделать,
непосредственно в коде, прицепив еще и анимацию сюда, абсолютно не понимаю (хотя и
понимаю что есть св-во _TransVal есть параметр #pragma alpha, который за прозрачность
отвечает).
Т.к. писать шейдеры не приходилось ранее и, если честно, те статьи, что нашел в инете,
не сильно внесли ясности - именно в вопрос реализации. Cамо понимание что такое шейдер,
какие они бывают и зачем - у меня присутствует.
Буду благодарен, если кто-нибудь возьмется поэтапно расписать на примере моей задачи
процесс реализации с пояснениями: мол нам нужно это для этого, а это для этого, вот
так мы будем изменять блюр, вот так прозрачность, а вот так анимацию присобачим.
UPDATE
Чего я смог добиться: Регулируя значения sigma и cutoff собранного мною в бреду непонимания
(в силу отсутствия опыта в написании шейдеров) происходит все то что мне необходимо
кроме одного НО, спрайт позади всего этого откуда он и как от него избавиться я так
и не смог понять.
http://g.recordit.co/rUaGYdynQ3.gif (картинка в большем разрешении).
Данный материал (с шейдером) применен на image. image и canvas в котором они находятся
вынесены на слой transparentfx который рендерит отдельная камера (на основной отключен
этот слой). Осталось понять как отключить (или убрать из рендера) этот спрайт позади
и в минимальном виде задача будет решена (прозрачность на краю заполнения уже не суть
важно хотя бы с блюром разобраться).
UPDATE 2 задача решается если выставить на самом Image type filled и крутить ручку
fill amount. Но остается непонятным, как по-человечески отключить рендер image'a, оставив
рендер только того, что есть.
Результат работы шейдера:
UPDATE от 29 декабря
Поняв что с написанием по человечески у меня все не очень, я попробовал использовать
Shader Forge и в нем вроде бы добился необходимого эффекта (одного из необходимых).
Но получилось очень странно, в том смысле что в окне инспектора в юнити эффект отображался
как надо а в сцене гейм он вел себя "упрощенно" так сказать. Смотри гиф ниже для лучшего
понимания.
Гиф1, окно инспектора.
Гиф2, окно игры
Ответы
Ответ 1
А стоит все это через единый шейдер городить?
Делал бы так:
Чтобы использовать разблуреное изображение вам понадобиться еще одна
камера которая через постэффект BLUR будет писать в RenderTexture
экран без накладываемого изображения(поместите его в отдельный Layer
и в этой камере этот слой выключите).
Далее делаем анимацию проявления любой формы которая вас интересует.
Cделайте форму с анимированием маски по альфе.
А дальше уже все просто, для анимированной формы используем
простейший материал который в зависимости от прозрачности текстуры
на материале рисует через смешение либо с основной текстуры либо с
размытой в RenderTexture.
Ответ 2
Ответ, часть 2.
Так как возможно это будет нагрузка на шейдер, то можно некоторые части также довыносить
в управляющий скрипт. Оставить тут только применение значений. В итоге шейдер и скрипт
могут быть такими:
Shader "Custom/RadialFill_MoreScriptControl" {
Properties {
[PerRendererData]_MainTex ("MainTex", 2D) = "white" {}
_Color ("Color", Color) = (1,1,1,1)
_OpacityRotator ("Opacity Rotator", Range(-360, 360)) = -360 // два полных
оборота
_TextureRotator ("Texture Rotator", Range(0, 360)) = 360
[MaterialToggle] _FillClockwise ("Fill Clockwise", int ) = 1
[HideInInspector]_Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
[MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
[HideInInspector] _CutoffRightBottomLeftTop ("cRBLT", Float) = 1.0
[HideInInspector] _OpRightBottomLeftTop ("oRBLT", Float) = 1.0
[HideInInspector] _OpVector ("OpVector", Vector) = (1, -1, 0, 0)
[HideInInspector] _ReverseMaskCoords ("_ReverseMaskCoords", int) = 0
}
SubShader {
Tags {
"IgnoreProjector"="True"
"Queue"="Transparent"
"RenderType"="Transparent"
"CanUseSpriteAtlas"="True"
"PreviewType"="Plane"
}
Pass {
Name "FORWARD"
Tags {
"LightMode"="ForwardBase"
}
Blend One OneMinusSrcAlpha
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#define UNITY_PASS_FORWARDBASE
#pragma multi_compile _ PIXELSNAP_ON
#include "UnityCG.cginc"
#pragma multi_compile_fwdbase
#pragma exclude_renderers gles3 metal d3d11_9x xbox360 xboxone ps3 ps4 psp2
#pragma target 3.0
static const float TAU = float(6.283185); // это 2 * PI, кто не знает
uniform sampler2D _MainTex;
uniform float4 _MainTex_ST;
uniform float4 _Color;
uniform float _OpacityRotator;
uniform float _TextureRotator;
uniform fixed _FillClockwise;
uniform fixed _CutoffRightBottomLeftTop;
uniform fixed _OpRightBottomLeftTop;
uniform float2 _OpVector;
uniform int _ReverseMaskCoords;
struct VertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 texcoord0 : TEXCOORD0;
};
struct VertexOutput {
float4 pos : SV_POSITION;
float2 uv0 : TEXCOORD0;
float4 posWorld : TEXCOORD1;
float3 normalDir : TEXCOORD2;
float3 tangentDir : TEXCOORD3;
float3 bitangentDir : TEXCOORD4;
};
// матрица вращения
float2x2 getMatrix(float angle) {
float r_cos = cos(angle);
float r_sin = sin(angle);
return float2x2(r_cos, -r_sin, r_sin, r_cos);
}
// формирование маски
float2x2 getMask(float oAtan2MaskNormalized, float rotator, int isRotatorSubtract)
{
float oAtan2MaskRotatable = isRotatorSubtract ? oAtan2MaskNormalized
- rotator : rotator - oAtan2MaskNormalized;
return ceil(oAtan2MaskRotatable);
}
float getNormalizedAtanMask(float2 maskChannels, int reverseMaskCoords) {
float atan2var = reverseMaskCoords ? atan2(maskChannels.r, maskChannels.g)
: atan2(maskChannels.g, maskChannels.r);
return (atan2var / TAU) + 0.5;
}
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.uv0 = v.texcoord0;
o.normalDir = UnityObjectToWorldNormal(v.normal);
o.tangentDir = normalize(mul(_Object2World, float4(v.tangent.xyz,
0.0)).xyz);
o.bitangentDir = normalize(cross(o.normalDir, o.tangentDir) * v.tangent.w);
o.posWorld = mul(_Object2World, v.vertex);
o.pos = mul(UNITY_MATRIX_MVP, v.vertex );
#ifdef PIXELSNAP_ON
o.pos = UnityPixelSnap(o.pos);
#endif
return o;
}
float4 frag(VertexOutput i) : COLOR {
i.normalDir = normalize(i.normalDir);
float4 _MainTex_var = tex2D(_MainTex,TRANSFORM_TEX(i.uv0, _MainTex));
/*** Общее начало для opacity и cutoff, помогающее переключать вращение
по/против часовой стрелки BEGIN ***/
// float2(1, -1) - по часовой, float2(1, 1) - против часовой
float2 clockCounterDirection = _FillClockwise ? float2(1, -1) : float2(1, 1);
// по умолчанию "обрезание" начинается слева.
// умножение на -1 для того, чтоб началось справа.....просто потому,
что я так хочу =)
float2 CommonStartAndSwitcher = (-1 * (i.uv0 - 0.5)) * clockCounterDirection;
/*** Общее начало для opacity и cutoff с переключателем вращения
по/против часовой стрелки END ***/
/*** Секция для cutoff ***/
float tRotatorNormalized = _TextureRotator / 360.0;
float cutoffRotator_ang = _CutoffRightBottomLeftTop * -TAU;
float2x2 cutoffRotationMatrix = getMatrix(cutoffRotator_ang);
float2 cutoffRotator = mul(CommonStartAndSwitcher, cutoffRotationMatrix);
float whiteToBlackMask = getMask(getNormalizedAtanMask(cutoffRotator,
0), tRotatorNormalized, 1);
// Финальная маска
float finalMask = 1.0 - whiteToBlackMask;
clip(finalMask - 0.5);
/*** Секция для opacity ***/
float oRotatorNormalized = _OpacityRotator / 360.0;
float2 oVector = float2(_OpVector);
float oRotator_ang = _OpRightBottomLeftTop * (oRotatorNormalized
* -TAU);
float2x2 oRotationMatrix = getMatrix(oRotator_ang);
float2 oRotator = mul(oVector * CommonStartAndSwitcher, oRotationMatrix);
float oWhiteToBlackMask = getMask(getNormalizedAtanMask(oRotator,
_ReverseMaskCoords), oRotatorNormalized, 0);
// Финальная прозрачность
float oFinalMultiply = _MainTex_var.a * max(getNormalizedAtanMask(oRotator,
_ReverseMaskCoords), ceil(oWhiteToBlackMask));
/*** Излучение (Emissive) ***/
// oFinalMultiply чтоб обрезать прозрачную область, где она обрезана
в самой текстуре
float3 finalColor = _MainTex_var.rgb * _Color.rgb * oFinalMultiply;
// Конечный результат (цвет, обработанный маской и повернутый под
углом альфа канал)
return fixed4(finalColor, oFinalMultiply);
}
ENDCG
}
}
FallBack "Diffuse"
}
Скрипт будет таким:
using UnityEngine;
using System.Collections;
public enum FillOrigin {
Right,
Bottom,
Left,
Top
}
public class RadialFill_MoreScriptControl : MonoBehaviour {
public float cutoffStartAngle = 5.0f; // градусы
public float opacityStartAngle = -350.0f; // градусы, -2 * PI + 10 (небольшой
начальный угол)
public float deltaAngle = 5f;
public bool fillClockwise = true;
public FillOrigin fillOrigin = FillOrigin.Right;
private const float MAX_ANGLE = 360.0f;
private Material material;
private float _TextureRotator; // ссылка на переменную _TextureRotator в шейдере
private float _OpacityRotator; // ссылка на переменную _TextureRotator в шейдере
void Start () {
material = GetComponent().material;
}
void Update () {
if (Input.GetMouseButtonDown(0)) //if (Input.GetKeyDown("f"))
StartCoroutine(FillSprite());
}
IEnumerator FillSprite() {
var cOffStart = cutoffStartAngle;
var oStart = opacityStartAngle;
material.SetFloat("_FillClockwise", fillClockwise ? 1 : 0);
material.SetFloat("_TextureRotator", cOffStart);
material.SetFloat("_OpacityRotator", oStart);
SetCutoffData();
SetOpacityData();
_TextureRotator = cOffStart;
_OpacityRotator = oStart;
while(_OpacityRotator <= MAX_ANGLE) {
if (_TextureRotator >= MAX_ANGLE)
_TextureRotator = MAX_ANGLE;
if (_OpacityRotator >= MAX_ANGLE)
_OpacityRotator = MAX_ANGLE;
material.SetFloat("_TextureRotator", _TextureRotator);
material.SetFloat("_OpacityRotator", _OpacityRotator);
_OpacityRotator += deltaAngle;
_TextureRotator += deltaAngle;
yield return null;
}
yield break;
}
private void SetCutoffData() {
var cutoffRightBottomLeftTop = 1.0f;
if (fillOrigin == FillOrigin.Bottom)
cutoffRightBottomLeftTop = fillClockwise ? 1.75f : 1.25f;
else if (fillOrigin == FillOrigin.Left)
cutoffRightBottomLeftTop = 1.5f;
else if (fillOrigin == FillOrigin.Top)
cutoffRightBottomLeftTop = fillClockwise ? 1.25f : 1.75f;
cutoffRightBottomLeftTop += 0.001f;
material.SetFloat("_CutoffRightBottomLeftTop", cutoffRightBottomLeftTop);
}
private void SetOpacityData() {
Vector2 oVector = new Vector2(1, -1);
var oRightBottomLeftTop = 1.0f;
int reverseMaskCoords = (fillOrigin == FillOrigin.Top || fillOrigin == FillOrigin.Bottom)
? 1 : 0;
if (fillOrigin == FillOrigin.Left)
oVector = new Vector2(-1, 1);
else if (fillOrigin == FillOrigin.Top) {
oVector = fillClockwise ? new Vector2(-1, -1) : new Vector2(1, 1);
oRightBottomLeftTop = -1.0f;
} else if (fillOrigin == FillOrigin.Bottom) {
oVector = fillClockwise ? new Vector2(1, 1) : new Vector2(-1, -1);
oRightBottomLeftTop = -1.0f;
}
material.SetInt("_ReverseMaskCoords", reverseMaskCoords);
material.SetVector("_OpVector", oVector);
material.SetFloat("_OpRightBottomLeftTop", oRightBottomLeftTop);
}
}
в инспекторе управление такое:
И по поводу размытия...Так как мой ответ уже большой (из-за кода)...и уже вторая
часть, то я приведу код шейдера Blur, который вы можете перенести в шейдеры выше. А
также дописать управление размытием из скрипта по примеру выше.
Shader "Custom/Blur" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
radius ("radius", Range(0, 80)) =0
resolution ("resolution", float) = 800
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 100
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
uniform float resolution = 800;
uniform float radius = 400;
uniform float2 dir = float2(0,1);
v2f vert (appdata v) {
v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
float4 sum = float4(0.0, 0.0, 0.0, 0.0);
float2 tc = i.uv;
// радиус размытия в пикселях
float blur = radius/resolution/4;
float hstep = 1; // размытие по горизонтали
float vstep = 0; // размытие по вертикали
sum += tex2D(_MainTex, float2(tc.x - 5.0 * blur * hstep, tc.y - 5.0
* blur * vstep)) * 0.0052111262;
sum += tex2D(_MainTex, float2(tc.x - 4.0 * blur * hstep, tc.y - 4.0
* blur * vstep)) * 0.0162162162;
sum += tex2D(_MainTex, float2(tc.x - 3.0 * blur * hstep, tc.y - 3.0
* blur * vstep)) * 0.0540540541;
sum += tex2D(_MainTex, float2(tc.x - 2.0 * blur * hstep, tc.y - 2.0
* blur * vstep)) * 0.1216216216;
sum += tex2D(_MainTex, float2(tc.x - 1.0 * blur * hstep, tc.y - 1.0
* blur * vstep)) * 0.1945945946;
sum += tex2D(_MainTex, float2(tc.x, tc.y)) * 0.2270270270;
sum += tex2D(_MainTex, float2(tc.x + 1.0 * blur * hstep, tc.y + 1.0
* blur * vstep)) * 0.1945945946;
sum += tex2D(_MainTex, float2(tc.x + 2.0 * blur * hstep, tc.y + 2.0
* blur * vstep)) * 0.1216216216;
sum += tex2D(_MainTex, float2(tc.x + 3.0 * blur * hstep, tc.y + 3.0
* blur * vstep)) * 0.0540540541;
sum += tex2D(_MainTex, float2(tc.x + 4.0 * blur * hstep, tc.y + 4.0
* blur * vstep)) * 0.0162162162;
sum += tex2D(_MainTex, float2(tc.x + 5.0 * blur * hstep, tc.y + 5.0
* blur * vstep)) * 0.0052111262;
return float4(sum.rgb, 1);
}
ENDCG
}
}
}
P.S. К сожалению шейдер RadialFill работает только для Sprite Mode → Single. Как
сделать для мульти Multiple, я пока не знаю.
P.P.S. Можно сделать еще улучшения:
Вынести функцию getMatrix (матрица вращения) также в скрипт.
Сделать маску не генерируемой внутри шейдера, а взять текстуру в виде картинки atan2
и применять уже её. Глядишь еще чуть снять загрузку с шейдера можно.
P.P.P.S. Для попытки понимая шейдеров хоть на маленком уровне можно использовать
ассет Shader Forge - Визуальный редактор для программирования шейдеров. Визуальность
в данном случае очень в плюс.
Из бесплатных пока поиск показывает только uShader FREE - но его я не пробовал, не
знаю на сколько хорош.
Ссылки к прочтению, которые использовались в шейдерах выше:
Стандартные шейдерные предпроцессорные макросы
Создание программ с несколькими вариантами шейдеров
Директивы компиляции #pragma
Culling & Depth
Блендинг (Blending)
Шейдеры и эффекты в Unity. Книга рецептов
Unity 5.x Shaders and Effects Cookbook
Ответ 3
Ух ты, интересный вопрос и даже награаадааа!
Вообще, сколько я не читал, я не смог представить зачем эффект размытия применять
на прозрачности. Ведь если некая часть спрайта будет прозрачна, то размытия там итак
не будет видно и заметно, а там где не прозрачно, то и размытия нет. Либо я чего не
понял и имелось виду применение размытия вообще на весь спрайт, не зависимо от того,
какой процент радиального заполнения сейчас имеется. Если именно так, то
Можно поступить так, как написано в другом ответе: взять из стандартных ассетов Юнити
(благо их не мало предоставляется) эффект размытия, применяемый на камере. Добавить
еще одну камеру, добавить туда скрипт размытия и шейдер и в нужный момент включать
ту камеру и изменять смещение в Blur-эффекте.
Реализовать, как и хотели в шейдере)) Об этом в самом конце.
(!!!)
Да простят меня админы сайта, но в один ответ у меня не вместится (из-за количество
кода, а не из-за "воды"). Поэтому ответ будет в двух частях.
Ответ, часть 1.
Суть, что обрезки, что заливания прозрачностью будет сводиться к тому, что будет
браться маска, на основе которой всё будет происходить. Маска генерируется программно
через тригонометрическую функцию atan2. На осях она выглядит так:
В двумерной системе координат выглядит так:
Так как маска — это некая компонента, состоящая из оттенков черного и белого, в которой
черный цвет — полностью отсутствие текстуры, а белый — полностью видимая текстура,
то для прозрачности маска atan2 будет представлять из себя переход от черного к белому
(см. рисунок выше), а для обрезки будет применена дополнительная функция, чтоб было
только черное/белое, без плавных переходов.
Я попробую просто опубликовать шейдер, в котором будут комментарии того, что сделано.
Не уверен, что все будет понятно, но я хоть как-то худо бедно постараюсь.
Shader "Custom/RadialFill" {
Properties {
[PerRendererData]_MainTex ("MainTex", 2D) = "white" {}
_Color ("Color", Color) = (1,1,1,1)
_OpacityRotator ("Opacity Rotator", Range(-360, 360)) = -360 // два полных
оборота
_TextureRotator ("Texture Rotator", Range(0, 360)) = 360
[MaterialToggle] _FillClockwise ("Fill Clockwise", Float ) = 1
[HideInInspector]_Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
[MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
[KeywordEnum(Right, Bottom, Left, Top)] _Fill_Origin("Fill Origin", Int)
= 0
}
SubShader {
// https://docs.unity3d.com/ru/current/Manual/SL-SubShaderTags.html
Tags {
"IgnoreProjector"="True"
"Queue"="Transparent"
"RenderType"="Transparent"
"CanUseSpriteAtlas"="True"
"PreviewType"="Plane"
}
Pass {
Name "FORWARD"
Tags {
"LightMode"="ForwardBase" // https://docs.unity3d.com/Manual/SL-PassTags.html
}
Blend One OneMinusSrcAlpha // https://docs.unity3d.com/ru/current/Manual/SL-Blend.html
ZWrite Off // https://docs.unity3d.com/ru/current/Manual/SL-CullAndDepth.html
CGPROGRAM
#pragma vertex vert // vert - имя функции обработки вершин
#pragma fragment frag // frag - имя функции обработки пикселей
#pragma multi_compile _ PIXELSNAP_ON
// как работает shader_feature: https://docs.unity3d.com/ru/530/Manual/SL-MultipleProgramVariants.html
// он относится к свойству _Fill_Origin .... по сути - автоматически
конвертируем его имя и значения в константы
#pragma shader_feature _FILL_ORIGIN_RIGHT _FILL_ORIGIN_BOTTOM _FILL_ORIGIN_LEFT
_FILL_ORIGIN_TOP
#include "UnityCG.cginc"
#pragma exclude_renderers gles3 metal d3d11_9x xbox360 xboxone ps3 ps4 psp2
#pragma target 3.0
uniform sampler2D _MainTex;
uniform float4 _MainTex_ST;
uniform float4 _Color;
uniform float _OpacityRotator;
uniform float _TextureRotator;
uniform fixed _FillClockwise;
static const float TAU = float(6.283185); // это 2 * PI, кто не знает
struct VertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 texcoord0 : TEXCOORD0;
};
struct VertexOutput {
float4 pos : SV_POSITION;
float2 uv0 : TEXCOORD0;
float4 posWorld : TEXCOORD1;
float3 normalDir : TEXCOORD2;
float3 tangentDir : TEXCOORD3;
float3 bitangentDir : TEXCOORD4;
};
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.uv0 = v.texcoord0;
o.normalDir = UnityObjectToWorldNormal(v.normal);
o.tangentDir = normalize(mul(_Object2World, float4(v.tangent.xyz,
0.0)).xyz);
o.bitangentDir = normalize(cross(o.normalDir, o.tangentDir) * v.tangent.w);
o.posWorld = mul(_Object2World, v.vertex);
o.pos = mul(UNITY_MATRIX_MVP, v.vertex );
#ifdef PIXELSNAP_ON
o.pos = UnityPixelSnap(o.pos);
#endif
return o;
}
float4 frag(VertexOutput i) : COLOR {
i.normalDir = normalize(i.normalDir);
float4 _MainTex_var = tex2D(_MainTex,TRANSFORM_TEX(i.uv0, _MainTex));
/*** Общее начало для opacity и cutoff, помогающее переключать вращение
по/против часовой стрелки BEGIN ***/
// float2(1, -1) - по часовой, float2(1, 1) - против часовой
float2 clockCounterDirection = _FillClockwise ? float2(1, -1) : float2(1, 1);
// по умолчанию "обрезание" начинается слева.
// умножение на -1 для того, чтоб началось справа.....просто потому,
что я так хочу =)
float2 CommonStartAndSwitcher = (-1 * (i.uv0 - 0.5)) * clockCounterDirection;
/*** Общее начало для opacity и cutoff с переключателем вращения
по/против часовой стрелки END ***/
/*** Секция для cutoff ***/
float cutoffRightBottomLeftTop = 1.0; // изменение направления
// В зависимости от того, что выбрано в качестве старта вращения
право/лево/верх/низ
// нужно будет провернуть и текстурку.
// +0.25 - 90 градусов, +0.5 - 180, +0.75 - 270
#if _FILL_ORIGIN_BOTTOM
cutoffRightBottomLeftTop = _FillClockwise ? 1.75 : 1.25;
#elif _FILL_ORIGIN_LEFT
cutoffRightBottomLeftTop = 1.5;
#elif _FILL_ORIGIN_TOP
cutoffRightBottomLeftTop = _FillClockwise ? 1.25 : 1.75;
#endif
cutoffRightBottomLeftTop += 0.001;
// Матрица вращения для cutoff
float cutoffRotator_ang = cutoffRightBottomLeftTop * -TAU;
float cutoffRotator_cos = cos(cutoffRotator_ang);
float cutoffRotator_sin = sin(cutoffRotator_ang);
float2x2 cutoffRotationMatrix = float2x2(cutoffRotator_cos, -cutoffRotator_sin,
cutoffRotator_sin, cutoffRotator_cos);
float2 cutoffRotator = mul(CommonStartAndSwitcher, cutoffRotationMatrix);
// перевод из системы от 0 до 360 градусов в отсчет от 0 до 1
float tRotatorNormalized = _TextureRotator / 360.0;
// Генерирование маски для отсечения пикселей и отсечение пикселей
по предоставленной маске
// 1. Для генерации нужны исхоные две координаты....
// rg, утрированно, представляют из себя x и y
float2 cutoffMaskSource = cutoffRotator.rg;
// 2. Формируем начальную маску // в инете рисуночки глянуть как
это выглядит =)
// Угол задается в радианах и принимает значения от -PI до PI, исключая -PI
float atan2Mask = atan2(cutoffMaskSource.g, cutoffMaskSource.r);
// 3. Добавляем пол оборота (до целого) и конвертируем в значение
от 0 до 1,
// для дальнейшей удобной работы в единичном отрезке, т.к tRotatorNormalized
меняется от 0 до 1
float atan2MaskNormalized = (atan2Mask / TAU) + 0.5;
// 4. Привязка маски к повороту. хз как объяснить
float atan2MaskRotatable = atan2MaskNormalized - tRotatorNormalized;
// 5. Получаем карту заливки от белого к черному
// Белый - полностью видимый участок, Черный - обрезающиеся (не отображающиеся)
пиксели
float whiteToBlackMask = ceil(atan2MaskRotatable);
// 6. Собираем финальную маску от чёрного к белому (т.к. нужно постепенное
заполнение)
float finalMask = 1.0 - whiteToBlackMask;
clip(finalMask - 0.5);
/*** Секция для opacity ***/
// oVector меняется в зависимости от начала направления - лево/право/верх/низ
float2 oVector = float2(1, -1);
// изменение направления в зависимости от лево-право (1.0) или верх-низ
(-1.0)
float oRightBottomLeftTop = 1.0;
// В зависимости от того, что выбрано в качестве старта вращения
право/лево/верх/низ
// нужно будет провернуть и маску.
#if _FILL_ORIGIN_LEFT
oVector = float2(-1, 1);
#elif _FILL_ORIGIN_TOP
oVector = _FillClockwise ? float2(-1, -1) : float2(1, 1);
oRightBottomLeftTop = -1.0;
#elif _FILL_ORIGIN_BOTTOM
oVector = _FillClockwise ? float2(1, 1) : float2(-1, -1);
oRightBottomLeftTop = -1.0;
#endif
float oRotatorNormalized = _OpacityRotator / 360.0;
// Матрица вращения для opacity
float oRotator_ang = oRightBottomLeftTop * (oRotatorNormalized *
-TAU);
float oRotator_cos = cos(oRotator_ang);
float oRotator_sin = sin(oRotator_ang);
float2x2 oRotationMatrix = float2x2(oRotator_cos, -oRotator_sin,
oRotator_sin, oRotator_cos);
float2 oRotator = mul(oVector * CommonStartAndSwitcher, oRotationMatrix);
// Как и у cutoff формируем маску
float2 oMask = oRotator.rg;
float2 oMaskHorizOrVert = atan2(oMask.g, oMask.r);
// при формировании маски по вертикали, нужно поменять x, y местами
в функции
#if (_FILL_ORIGIN_TOP || _FILL_ORIGIN_BOTTOM)
oMaskHorizOrVert = atan2(oMask.r, oMask.g);
#endif
float oAtan2MaskNormalized = (oMaskHorizOrVert / TAU) + 0.5;
// oRotatorNormalized - oAtan2MaskNormalized для того, чтобы первый
круг просто провернуться, а на втором
// начать обрезку как у cutoff, только начиная схвоста, но при этом
продолжая вращаться.
// Если было бы oAtan2MaskNormalized - oRotatorNormalized (как в
примере с cutoff выше), то, т.к. значение oRotatorNormalized
// меняется с -1 до 1 (два полных круга), получается что маска наложена
на изображение 2 раза: 1 раз - прозрачность, 2 раз - она же
// поэтому увеличивается наложенность, белый цвет. В итоге при изменении
с -1 до 1 ушла бы в начале белизна, а потом провернулась бы маска,
// и не обрезалась бы
float oAtan2MaskRotatable = oRotatorNormalized - oAtan2MaskNormalized;
float oWhiteToBlackMask = ceil(oAtan2MaskRotatable);
// Финальная прозрачность
float oFinalMultiply = _MainTex_var.a * max(oAtan2MaskNormalized,
ceil(oWhiteToBlackMask));
/*** Излучение (Emissive) ***/
// oFinalMultiply чтоб обрезать прозрачную область, где она обрезана
в самой текстуре
float3 finalColor = _MainTex_var.rgb * _Color.rgb * oFinalMultiply;
// Конечный результат (цвет, обработанный маской и повернутый под
углом альфа канал)
return fixed4(finalColor, oFinalMultiply);
}
ENDCG
}
}
FallBack "Diffuse"
}
Чтобы в нужный момент запустить заливку, конечно же нужно дать команду. А откуда
её можно дать? Правильно — из скрипта. Он будет расположен ниже:
using UnityEngine;
using System.Collections;
public class RadialFill : MonoBehaviour {
public float cutoffStartAngle = 5.0f; // градусы
public float opacityStartAngle = -350.0f; // градусы, -2 * PI + 10 (небольшой
начальный угол)
public float deltaAngle = 5f;
private const float MAX_ANGLE = 360.0f;
private Material material;
private float _TextureRotator; // ссылка на переменную _TextureRotator в шейдере
private float _OpacityRotator; // ссылка на переменную _TextureRotator в шейдере
void Start () {
material = GetComponent().material;
}
void Update () {
if (Input.GetMouseButtonDown(0)) //if (Input.GetKeyDown("f"))
StartCoroutine(FillSprite());
}
IEnumerator FillSprite() {
var cOffStart = cutoffStartAngle;
var oStart = opacityStartAngle;
material.SetFloat("_TextureRotator", cOffStart);
material.SetFloat("_OpacityRotator", oStart);
_TextureRotator = cOffStart;
_OpacityRotator = oStart;
while(_OpacityRotator <= MAX_ANGLE) {
if (_TextureRotator >= MAX_ANGLE)
_TextureRotator = MAX_ANGLE;
if (_OpacityRotator >= MAX_ANGLE)
_OpacityRotator = MAX_ANGLE;
material.SetFloat("_TextureRotator", _TextureRotator);
material.SetFloat("_OpacityRotator", _OpacityRotator);
_OpacityRotator += deltaAngle;
_TextureRotator += deltaAngle;
yield return null;
}
yield break;
}
}
где:
cutoffStartAngle — начальный угол обрезки, opacityStartAngle — начальный угол прозрачности.
Эти параметры для того, чтобы немного отрегулировать по вкусу площадь сектора, занимаемого
прозрачностью. Замечу, что прозрачность изменяется от -360 до 360, потому что первый
круг она проворачивается сама по себе, а второй круг — плавно "заходит" за текстуру.
deltaAngle - дельта, на которую проворачиваются маски.
Что скрипт делает? При нажатии нажатии на клавишу мыши он берет шейдер у спрайта
(точнее с его материала), устанавливает изначальные углы, в цикле изменяет угол поворота
и передает это значение в шейдер, чтобы он там уже у себя применил значения в frag.
Выглядит в инспекторе так:
Opacity Rotator - вращение маски прозрачности
Texture Rotator - вращение маски обрезки
Fill Clockwise - по часовой стрелке или против
Fill Origin - с какой стороны начинать (справа/слева/сверху/снизу)
Итог будет выглядеть примерно таким:
Увы на данной гифке не получается передать то, как это выглядит в правильности
Улучшение
Так как у секции cutoff и opacity есть общие части, то их можно вынести в общие функции,
как и во всех нормальных языках программирования.
Shader "Custom/RadialFillCommonFunctions" {
Properties {
[PerRendererData]_MainTex ("MainTex", 2D) = "white" {}
_Color ("Color", Color) = (1,1,1,1)
_OpacityRotator ("Opacity Rotator", Range(-360, 360)) = -360 // два полных
оборота
_TextureRotator ("Texture Rotator", Range(0, 360)) = 360
[MaterialToggle] _FillClockwise ("Fill Clockwise", Float ) = 1
[HideInInspector]_Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
[MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
[KeywordEnum(Right, Bottom, Left, Top)] _Fill_Origin("Fill Origin", Int)
= 0
}
SubShader {
Tags {
"IgnoreProjector"="True"
"Queue"="Transparent"
"RenderType"="Transparent"
"CanUseSpriteAtlas"="True"
"PreviewType"="Plane"
}
Pass {
Name "FORWARD"
Tags {
"LightMode"="ForwardBase"
}
Blend One OneMinusSrcAlpha
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#define UNITY_PASS_FORWARDBASE
#pragma multi_compile _ PIXELSNAP_ON
#pragma shader_feature _FILL_ORIGIN_RIGHT _FILL_ORIGIN_BOTTOM _FILL_ORIGIN_LEFT
_FILL_ORIGIN_TOP
#include "UnityCG.cginc"
#pragma multi_compile_fwdbase
#pragma exclude_renderers gles3 metal d3d11_9x xbox360 xboxone ps3 ps4 psp2
#pragma target 3.0
static const float TAU = float(6.283185); // это 2 * PI, кто не знает
uniform sampler2D _MainTex;
uniform float4 _MainTex_ST;
uniform float4 _Color;
uniform float _OpacityRotator;
uniform float _TextureRotator;
uniform fixed _FillClockwise;
struct VertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 texcoord0 : TEXCOORD0;
};
struct VertexOutput {
float4 pos : SV_POSITION;
float2 uv0 : TEXCOORD0;
float4 posWorld : TEXCOORD1;
float3 normalDir : TEXCOORD2;
float3 tangentDir : TEXCOORD3;
float3 bitangentDir : TEXCOORD4;
};
// матрица вращения
float2x2 getMatrix(float angle) {
float r_cos = cos(angle);
float r_sin = sin(angle);
return float2x2(r_cos, -r_sin, r_sin, r_cos);
}
// формирование маски
float2x2 getMask(float oAtan2MaskNormalized, float rotator, int isRotatorSubtract) {
//float atan2var = reverseMaskCoords ? atan2(maskChannels.r, maskChannels.g)
: atan2(maskChannels.g, maskChannels.r);
//float oAtan2MaskNormalized = (atan2var / TAU) + 0.5;
float oAtan2MaskRotatable = isRotatorSubtract ? oAtan2MaskNormalized
- rotator : rotator - oAtan2MaskNormalized;
return ceil(oAtan2MaskRotatable);
}
float getNormalizedAtanMask(float2 maskChannels, int reverseMaskCoords) {
float atan2var = reverseMaskCoords ? atan2(maskChannels.r, maskChannels.g)
: atan2(maskChannels.g, maskChannels.r);
return (atan2var / TAU) + 0.5;
}
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.uv0 = v.texcoord0;
o.normalDir = UnityObjectToWorldNormal(v.normal);
o.tangentDir = normalize(mul(_Object2World, float4(v.tangent.xyz,
0.0)).xyz);
o.bitangentDir = normalize(cross(o.normalDir, o.tangentDir) * v.tangent.w);
o.posWorld = mul(_Object2World, v.vertex);
o.pos = mul(UNITY_MATRIX_MVP, v.vertex );
#ifdef PIXELSNAP_ON
o.pos = UnityPixelSnap(o.pos);
#endif
return o;
}
float4 frag(VertexOutput i) : COLOR {
i.normalDir = normalize(i.normalDir);
float4 _MainTex_var = tex2D(_MainTex,TRANSFORM_TEX(i.uv0, _MainTex));
/*** Общее начало для opacity и cutoff, помогающее переключать вращение
по/против часовой стрелки BEGIN ***/
// float2(1, -1) - по часовой, float2(1, 1) - против часовой
float2 clockCounterDirection = _FillClockwise ? float2(1, -1) : float2(1, 1);
// по умолчанию "обрезание" начинается слева.
// умножение на -1 для того, чтоб началось справа.....просто потому,
что я так хочу =)
float2 CommonStartAndSwitcher = (-1 * (i.uv0 - 0.5)) * clockCounterDirection;
/*** Общее начало для opacity и cutoff с переключателем вращения
по/против часовой стрелки END ***/
/*** Секция для cutoff ***/
float tRotatorNormalized = _TextureRotator / 360.0;
float cutoffRightBottomLeftTop = 1.0; // изменение направления
#if _FILL_ORIGIN_BOTTOM
cutoffRightBottomLeftTop = _FillClockwise ? 1.75 : 1.25;
#elif _FILL_ORIGIN_LEFT
cutoffRightBottomLeftTop = 1.5;
#elif _FILL_ORIGIN_TOP
cutoffRightBottomLeftTop = _FillClockwise ? 1.25 : 1.75;
#endif
cutoffRightBottomLeftTop += 0.001;
float cutoffRotator_ang = cutoffRightBottomLeftTop * -TAU;
float2x2 cutoffRotationMatrix = getMatrix(cutoffRotator_ang);
float2 cutoffRotator = mul(CommonStartAndSwitcher, cutoffRotationMatrix);
float whiteToBlackMask = getMask(getNormalizedAtanMask(cutoffRotator,
0), tRotatorNormalized, 1);
// Финальная маска
float finalMask = 1.0 - whiteToBlackMask;
clip(finalMask - 0.5);
/*** Секция для opacity ***/
float oRotatorNormalized = _OpacityRotator / 360.0;
float2 oVector = float2(1, -1);
float oRightBottomLeftTop = 1.0;
int reverseMaskCoords = 0;
#if (_FILL_ORIGIN_TOP || _FILL_ORIGIN_BOTTOM)
reverseMaskCoords = 1;
#endif
#if _FILL_ORIGIN_LEFT
oVector = float2(-1, 1);
#elif _FILL_ORIGIN_TOP
oVector = _FillClockwise ? float2(-1, -1) : float2(1, 1);
oRightBottomLeftTop = -1.0;
#elif _FILL_ORIGIN_BOTTOM
oVector = _FillClockwise ? float2(1, 1) : float2(-1, -1);
oRightBottomLeftTop = -1.0;
#endif
float oRotator_ang = oRightBottomLeftTop * (oRotatorNormalized * -TAU);
float2x2 oRotationMatrix = getMatrix(oRotator_ang);
float2 oRotator = mul(oVector * CommonStartAndSwitcher, oRotationMatrix);
float oWhiteToBlackMask = getMask(getNormalizedAtanMask(oRotator,
reverseMaskCoords), oRotatorNormalized, 0);
// Финальная прозрачность
float oFinalMultiply = _MainTex_var.a * max(getNormalizedAtanMask(oRotator,
reverseMaskCoords), ceil(oWhiteToBlackMask));
/*** Излучение (Emissive) ***/
// oFinalMultiply чтоб обрезать прозрачную область, где она обрезана
в самой текстуре
float3 finalColor = _MainTex_var.rgb * _Color.rgb * oFinalMultiply;
// Конечный результат (цвет, обработанный маской и повернутый под
углом альфа канал)
return fixed4(finalColor, oFinalMultiply);
}
ENDCG
}
}
FallBack "Diffuse"
}