Страницы

Поиск по вопросам

суббота, 14 декабря 2019 г.

Шейдеры в unity3d

#unity3d #shader


Рискуя нахватать по шапке, я все таки задам этот вопрос. 

Встала необходимость написать шейдер для 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" }

Комментариев нет:

Отправить комментарий