460 lines
17 KiB
Plaintext
460 lines
17 KiB
Plaintext
Shader "Ichni/UI/Expanding Contours"
|
|
{
|
|
Properties
|
|
{
|
|
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
|
|
_Color ("Graphic Tint", Color) = (1, 1, 1, 1)
|
|
|
|
[Header(Line)]
|
|
_LineColor ("Line Color", Color) = (1, 1, 1, 1)
|
|
_Opacity ("Opacity", Range(0, 1)) = 0.5
|
|
_LineWidth ("Line Width", Range(0.0005, 0.02)) = 0.0005
|
|
_EdgeSoftness ("Edge Softness", Range(0.5, 4)) = 0.5
|
|
_ContourSpacing ("Contour Spacing", Range(0.01, 0.2)) = 0.1
|
|
_TrailLength ("Contour Trail Length", Range(0.02, 1.5)) = 0.5
|
|
_TrailFade ("Inner Line Fade", Range(0, 1)) = 1
|
|
|
|
[Header(Emission)]
|
|
_EmitterCount ("Simultaneous Emitters", Range(1, 6)) = 4
|
|
_SpawnRate ("Event Cycles Per Second", Range(0.02, 1)) = 0.05
|
|
_MaxRadius ("Maximum Radius", Range(0.1, 2)) = 0.5
|
|
_ExpansionCurve ("Expansion Curve", Range(0.3, 3)) = 0.85
|
|
_FadeDuration ("Lifetime Fade", Range(0.01, 0.45)) = 0.16
|
|
_CenterMargin ("Random Center Margin", Range(0, 0.45)) = 0
|
|
_RandomSeed ("Random Seed", Float) = 0
|
|
|
|
[Header(Shape)]
|
|
_Irregularity ("Shape Irregularity", Range(0, 0.45)) = 0.45
|
|
_ShapeDetail ("Shape Detail", Range(0, 1)) = 1
|
|
_MorphAmount ("Shape Morph Amount", Range(0, 1)) = 1
|
|
_ShapeVariation ("Per Shape Variation", Range(0, 1)) = 1
|
|
_FusionStrength ("Intersection Fusion", Range(0, 0.2)) = 0.04
|
|
_TopologyRelease ("Topology Release", Range(0, 2)) = 1
|
|
|
|
[Header(UI)]
|
|
_StencilComp ("Stencil Comparison", Float) = 8
|
|
_Stencil ("Stencil ID", Float) = 0
|
|
_StencilOp ("Stencil Operation", Float) = 0
|
|
_StencilWriteMask ("Stencil Write Mask", Float) = 255
|
|
_StencilReadMask ("Stencil Read Mask", Float) = 255
|
|
_ColorMask ("Color Mask", Float) = 15
|
|
[Toggle(UNITY_UI_CLIP_RECT)] _UseClipRect ("Use UI Clip Rect", Float) = 0
|
|
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use UI Alpha Clip", Float) = 0
|
|
}
|
|
|
|
SubShader
|
|
{
|
|
Tags
|
|
{
|
|
"Queue" = "Transparent"
|
|
"IgnoreProjector" = "True"
|
|
"RenderType" = "Transparent"
|
|
"PreviewType" = "Plane"
|
|
"CanUseSpriteAtlas" = "True"
|
|
}
|
|
|
|
Stencil
|
|
{
|
|
Ref [_Stencil]
|
|
Comp [_StencilComp]
|
|
Pass [_StencilOp]
|
|
ReadMask [_StencilReadMask]
|
|
WriteMask [_StencilWriteMask]
|
|
}
|
|
|
|
Cull Off
|
|
Lighting Off
|
|
ZWrite Off
|
|
ZTest [unity_GUIZTestMode]
|
|
Blend SrcAlpha OneMinusSrcAlpha
|
|
ColorMask [_ColorMask]
|
|
|
|
Pass
|
|
{
|
|
Name "Expanding Contours UI"
|
|
|
|
CGPROGRAM
|
|
#pragma target 3.0
|
|
#pragma vertex vert
|
|
#pragma fragment frag
|
|
#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
|
|
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP
|
|
|
|
#include "UnityCG.cginc"
|
|
#include "UnityUI.cginc"
|
|
|
|
#define MAX_EMITTERS 6
|
|
struct appdata_t
|
|
{
|
|
float4 vertex : POSITION;
|
|
fixed4 color : COLOR;
|
|
float2 texcoord : TEXCOORD0;
|
|
UNITY_VERTEX_INPUT_INSTANCE_ID
|
|
};
|
|
|
|
struct v2f
|
|
{
|
|
float4 vertex : SV_POSITION;
|
|
fixed4 color : COLOR;
|
|
float2 uv : TEXCOORD0;
|
|
float4 worldPosition : TEXCOORD1;
|
|
half4 mask : TEXCOORD2;
|
|
UNITY_VERTEX_OUTPUT_STEREO
|
|
};
|
|
|
|
fixed4 _Color;
|
|
fixed4 _LineColor;
|
|
float _Opacity;
|
|
float _LineWidth;
|
|
float _EdgeSoftness;
|
|
float _ContourSpacing;
|
|
float _TrailLength;
|
|
float _TrailFade;
|
|
|
|
float _EmitterCount;
|
|
float _SpawnRate;
|
|
float _MaxRadius;
|
|
float _ExpansionCurve;
|
|
float _FadeDuration;
|
|
float _CenterMargin;
|
|
float _RandomSeed;
|
|
|
|
float _Irregularity;
|
|
float _ShapeDetail;
|
|
float _MorphAmount;
|
|
float _ShapeVariation;
|
|
float _FusionStrength;
|
|
float _TopologyRelease;
|
|
|
|
float4 _ClipRect;
|
|
float _UIMaskSoftnessX;
|
|
float _UIMaskSoftnessY;
|
|
|
|
v2f vert(appdata_t v)
|
|
{
|
|
v2f o;
|
|
UNITY_SETUP_INSTANCE_ID(v);
|
|
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
|
|
|
|
o.worldPosition = v.vertex;
|
|
o.vertex = UnityObjectToClipPos(o.worldPosition);
|
|
o.uv = v.texcoord;
|
|
o.color = v.color * _Color;
|
|
|
|
float2 pixelSize = o.vertex.w;
|
|
pixelSize /= abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));
|
|
|
|
float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
|
|
o.mask = half4(
|
|
v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw,
|
|
0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs(pixelSize.xy))
|
|
);
|
|
|
|
return o;
|
|
}
|
|
|
|
float Hash11(float value)
|
|
{
|
|
value = frac(value * 0.1031);
|
|
value *= value + 33.33;
|
|
value *= value + value;
|
|
return frac(value);
|
|
}
|
|
|
|
float2 Hash21(float value)
|
|
{
|
|
float3 value3 = frac(
|
|
float3(value, value, value) *
|
|
float3(0.1031, 0.1030, 0.0973)
|
|
);
|
|
value3 += dot(value3, value3.yzx + 33.33);
|
|
return frac((value3.xx + value3.yz) * value3.zy);
|
|
}
|
|
|
|
float2 GetRandomDirection(float seed)
|
|
{
|
|
float2 direction = Hash21(seed) * 2.0 - 1.0;
|
|
return direction * rsqrt(max(dot(direction, direction), 0.0001));
|
|
}
|
|
|
|
float2 NormalizeSafe(float2 direction)
|
|
{
|
|
return direction * rsqrt(max(dot(direction, direction), 0.0001));
|
|
}
|
|
|
|
float2 ComplexMultiply(float2 a, float2 b)
|
|
{
|
|
return float2(
|
|
a.x * b.x - a.y * b.y,
|
|
a.y * b.x + a.x * b.y
|
|
);
|
|
}
|
|
|
|
float GetRectAspect(float2 uv)
|
|
{
|
|
float uGradient = max(length(float2(ddx(uv.x), ddy(uv.x))), 0.000001);
|
|
float vGradient = max(length(float2(ddx(uv.y), ddy(uv.y))), 0.000001);
|
|
return clamp(vGradient / uGradient, 0.1, 10.0);
|
|
}
|
|
|
|
float GetIrregularDistance(
|
|
float2 uv,
|
|
float2 center,
|
|
float aspect,
|
|
float eventSeed,
|
|
float life
|
|
)
|
|
{
|
|
float2 delta = uv - center;
|
|
delta.x *= aspect;
|
|
|
|
float2 shapeAxis = GetRandomDirection(eventSeed + 17.23);
|
|
float2 perpendicularAxis = float2(-shapeAxis.y, shapeAxis.x);
|
|
float axisStretch = lerp(
|
|
1.0,
|
|
lerp(0.72, 1.28, Hash11(eventSeed + 21.91)),
|
|
_ShapeVariation
|
|
);
|
|
float parallelDistance = dot(delta, shapeAxis) / axisStretch;
|
|
float perpendicularDistance = dot(delta, perpendicularAxis) * axisStretch;
|
|
delta =
|
|
shapeAxis * parallelDistance +
|
|
perpendicularAxis * perpendicularDistance;
|
|
|
|
float radialDistance = max(length(delta), 0.000001);
|
|
float2 direction = delta / radialDistance;
|
|
|
|
// Complex powers provide closed angular harmonics without
|
|
// texture noise, atan2, or per-pixel trigonometry.
|
|
float2 harmonic2 = ComplexMultiply(direction, direction);
|
|
float2 harmonic3 = ComplexMultiply(harmonic2, direction);
|
|
float2 harmonic5 = ComplexMultiply(harmonic3, harmonic2);
|
|
float2 harmonic7 = ComplexMultiply(harmonic5, harmonic2);
|
|
|
|
float morph = smoothstep(0.0, 1.0, life) * _MorphAmount;
|
|
float2 phaseA = NormalizeSafe(lerp(
|
|
GetRandomDirection(eventSeed + 2.31),
|
|
GetRandomDirection(eventSeed + 102.31),
|
|
morph
|
|
));
|
|
float2 phaseB = NormalizeSafe(lerp(
|
|
GetRandomDirection(eventSeed + 7.17),
|
|
GetRandomDirection(eventSeed + 107.17),
|
|
morph
|
|
));
|
|
float2 phaseC = NormalizeSafe(lerp(
|
|
GetRandomDirection(eventSeed + 13.73),
|
|
GetRandomDirection(eventSeed + 113.73),
|
|
morph
|
|
));
|
|
|
|
float broadShape =
|
|
0.58 * dot(harmonic2, phaseA) +
|
|
0.29 * dot(harmonic3, phaseB);
|
|
float fineShape =
|
|
0.09 * dot(harmonic5, phaseB) +
|
|
0.04 * dot(harmonic7, phaseC);
|
|
float detailVariation = lerp(
|
|
1.0,
|
|
lerp(0.6, 1.4, Hash11(eventSeed + 29.37)),
|
|
_ShapeVariation
|
|
);
|
|
float irregularityVariation = lerp(
|
|
1.0,
|
|
lerp(0.65, 1.35, Hash11(eventSeed + 37.61)),
|
|
_ShapeVariation
|
|
);
|
|
float shape =
|
|
broadShape +
|
|
fineShape * _ShapeDetail * detailVariation;
|
|
return radialDistance *
|
|
(1.0 + shape * _Irregularity * irregularityVariation);
|
|
}
|
|
|
|
float2 GetEmitterField(float2 uv, float aspect, float emitterIndex, float activeEmitters)
|
|
{
|
|
float stagger = emitterIndex / activeEmitters;
|
|
float timeline = _Time.y * _SpawnRate + stagger;
|
|
float cycle = floor(timeline);
|
|
float life = frac(timeline);
|
|
|
|
// R2 low-discrepancy ordering keeps both consecutive events and
|
|
// simultaneously active centers distributed across the UI.
|
|
float sequenceIndex =
|
|
cycle * activeEmitters +
|
|
(activeEmitters - 1.0 - emitterIndex);
|
|
float eventSeed =
|
|
sequenceIndex * 17.17 +
|
|
_RandomSeed * 101.13;
|
|
|
|
float2 sequenceOffset = Hash21(_RandomSeed * 43.17 + 0.71);
|
|
float2 randomCenter = frac(
|
|
sequenceOffset +
|
|
sequenceIndex * float2(0.754877666, 0.569840296)
|
|
);
|
|
float centerRange = max(1.0 - _CenterMargin * 2.0, 0.001);
|
|
float2 center = _CenterMargin + randomCenter * centerRange;
|
|
|
|
float irregularDistance = GetIrregularDistance(
|
|
uv,
|
|
center,
|
|
aspect,
|
|
eventSeed,
|
|
life
|
|
);
|
|
|
|
float radiusVariation = lerp(
|
|
1.0,
|
|
lerp(0.85, 1.15, Hash11(eventSeed + 41.93)),
|
|
_ShapeVariation
|
|
);
|
|
float expansion = pow(saturate(life), _ExpansionCurve);
|
|
float currentRadius = _MaxRadius * radiusVariation * expansion;
|
|
|
|
float fadeDuration = max(_FadeDuration, 0.01);
|
|
half fadeIn = smoothstep(0.0, fadeDuration, life);
|
|
half fadeOut = 1.0 - smoothstep(1.0 - fadeDuration, 1.0, life);
|
|
half lifeFade = fadeIn * fadeOut;
|
|
|
|
// Alpha fading alone leaves the full SDF in the smooth union
|
|
// until the emitter is removed, which makes connected contours
|
|
// snap closed. Erode the field continuously during fade-out so
|
|
// its topology is already released before the final removal.
|
|
float retirement = 1.0 - fadeOut;
|
|
float releaseDistance =
|
|
currentRadius +
|
|
_TrailLength +
|
|
_ContourSpacing +
|
|
_FusionStrength;
|
|
float releaseOffset =
|
|
retirement *
|
|
retirement *
|
|
releaseDistance *
|
|
_TopologyRelease;
|
|
float releasedDistance =
|
|
irregularDistance -
|
|
currentRadius +
|
|
releaseOffset;
|
|
|
|
return float2(releasedDistance, lifeFade);
|
|
}
|
|
|
|
fixed4 frag(v2f i) : SV_Target
|
|
{
|
|
float aspect = GetRectAspect(i.uv);
|
|
float activeEmitters = clamp(floor(_EmitterCount + 0.5), 1.0, MAX_EMITTERS);
|
|
float unionDistance = 100000.0;
|
|
half unionFade = 0;
|
|
half hasEmitter = 0;
|
|
|
|
[unroll]
|
|
for (int emitter = 0; emitter < MAX_EMITTERS; emitter++)
|
|
{
|
|
UNITY_BRANCH
|
|
if ((float)emitter < activeEmitters)
|
|
{
|
|
float2 emitterField = GetEmitterField(
|
|
i.uv,
|
|
aspect,
|
|
(float)emitter,
|
|
activeEmitters
|
|
);
|
|
|
|
UNITY_BRANCH
|
|
if (emitterField.y > 0.001)
|
|
{
|
|
if (hasEmitter < 0.5)
|
|
{
|
|
unionDistance = emitterField.x;
|
|
unionFade = emitterField.y;
|
|
hasEmitter = 1;
|
|
}
|
|
else if (_FusionStrength > 0.00001)
|
|
{
|
|
float blend = saturate(
|
|
0.5 +
|
|
0.5 * (emitterField.x - unionDistance) /
|
|
_FusionStrength
|
|
);
|
|
unionDistance =
|
|
lerp(emitterField.x, unionDistance, blend) -
|
|
_FusionStrength * blend * (1.0 - blend);
|
|
unionFade = lerp(emitterField.y, unionFade, blend);
|
|
}
|
|
else if (emitterField.x < unionDistance)
|
|
{
|
|
unionDistance = emitterField.x;
|
|
unionFade = emitterField.y;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
float contourDepth = -unionDistance;
|
|
float spacing = max(_ContourSpacing, 0.0001);
|
|
float ringCoordinate = contourDepth / spacing;
|
|
float ringDistance =
|
|
abs(frac(ringCoordinate + 0.5) - 0.5) *
|
|
spacing;
|
|
float antialiasing = max(
|
|
fwidth(unionDistance) * _EdgeSoftness,
|
|
0.00001
|
|
);
|
|
half lineAlpha = 1.0 - smoothstep(
|
|
_LineWidth,
|
|
_LineWidth + antialiasing,
|
|
ringDistance
|
|
);
|
|
|
|
half outerMask = smoothstep(
|
|
-antialiasing,
|
|
antialiasing,
|
|
contourDepth
|
|
);
|
|
half innerMask = 1.0 - smoothstep(
|
|
_TrailLength - antialiasing,
|
|
_TrailLength + antialiasing,
|
|
contourDepth
|
|
);
|
|
half trailPosition = 1.0 - saturate(
|
|
contourDepth / max(_TrailLength, 0.0001)
|
|
);
|
|
half trailOpacity = lerp(
|
|
1.0 - _TrailFade,
|
|
1.0,
|
|
trailPosition
|
|
);
|
|
half contourAlpha =
|
|
lineAlpha *
|
|
outerMask *
|
|
innerMask *
|
|
trailOpacity *
|
|
unionFade *
|
|
hasEmitter;
|
|
|
|
fixed4 color;
|
|
color.rgb = _LineColor.rgb * i.color.rgb;
|
|
color.a =
|
|
saturate(contourAlpha) *
|
|
_LineColor.a *
|
|
_Opacity *
|
|
i.color.a;
|
|
|
|
#ifdef UNITY_UI_CLIP_RECT
|
|
half2 clipMask = saturate(
|
|
(_ClipRect.zw - _ClipRect.xy - abs(i.mask.xy)) * i.mask.zw
|
|
);
|
|
color.a *= clipMask.x * clipMask.y;
|
|
#endif
|
|
|
|
#ifdef UNITY_UI_ALPHACLIP
|
|
clip(color.a - 0.001);
|
|
#endif
|
|
|
|
return color;
|
|
}
|
|
ENDCG
|
|
}
|
|
}
|
|
}
|