Created
May 31, 2023 08:21
-
-
Save grapefrukt/b864b913df2fbe86c91ab95892e80146 to your computer and use it in GitHub Desktop.
SDF UI for Unity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Shader "UI/SDF Icon" { | |
Properties { | |
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} | |
_EdgeColor ("Edge Color", Color) = (1, 1, 1, 1) | |
_Bleed("Bleed", Range(-1, 1)) = 0 | |
_EdgeRadius("Edge Radius", Range(-1, 1)) = 0 | |
_EdgeFeather("Edge Feather", Range(0, 1)) = 0 | |
_Sharpness("Sharpness", Range(0, 2048)) = 1024 | |
[Toggle(MULTIPLY_ALPHA)] _MultiplyAlpha("Multiply Intensity by Alpha", Float) = 0 | |
[Toggle(USE_MSDF)] _UseMSDF("Use MSDF (instead of plain SDF)", Float) = 0 | |
[Toggle(BORDER_AS_CIRCLE)] _CircleBorder("Draw a circular border instead of a SDF one", Float) = 0 | |
[Toggle(ALPHA_AS_BLEED)] _AlphaAsBleed("Add Image Alpha to Bleed)", Float) = 0 | |
[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend("SrcBlend", Float) = 5 // SrcAlpha | |
[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend("DestBlend", Float) = 10 // OneMinusSrcAlpha | |
[HideInInspector] _StencilComp ("Stencil Comparison", Float) = 8 | |
[HideInInspector] _Stencil ("Stencil ID", Float) = 0 | |
[HideInInspector] _StencilOp ("Stencil Operation", Float) = 0 | |
[HideInInspector] _StencilWriteMask ("Stencil Write Mask", Float) = 255 | |
[HideInInspector] _StencilReadMask ("Stencil Read Mask", Float) = 255 | |
[HideInInspector] _ColorMask ("Color Mask", Float) = 15 | |
[HideInInspector] [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0 | |
} | |
SubShader { | |
Tags { | |
"Queue"="Transparent" | |
"IgnoreProjector"="True" | |
"RenderType"="Transparent" | |
"PreviewType"="Plane" | |
} | |
Cull Off | |
Lighting Off | |
ZWrite Off | |
ZTest [unity_GUIZTestMode] | |
Blend [_SrcBlend][_DstBlend] | |
Stencil { | |
Ref [_Stencil] | |
Comp [_StencilComp] | |
Pass [_StencilOp] | |
ReadMask [_StencilReadMask] | |
WriteMask [_StencilWriteMask] | |
} | |
Pass { | |
Name "Default" | |
CGPROGRAM | |
#pragma vertex vert | |
#pragma fragment frag | |
#pragma target 3.0 | |
#pragma shader_feature MULTIPLY_ALPHA | |
#pragma shader_feature USE_MSDF | |
#pragma shader_feature ALPHA_AS_BLEED | |
#pragma shader_feature BORDER_AS_CIRCLE | |
#include "UnityCG.cginc" | |
#include "UnityUI.cginc" | |
#include "../Utilities.cginc" | |
sampler2D _MainTex; | |
uniform float4 _MainTex_ST; | |
uniform float4 _MainTex_TexelSize; | |
fixed4 _EdgeColor; | |
float _Bleed; | |
float _EdgeRadius; | |
float _EdgeFeather; | |
float _Sharpness; | |
float4 _ClipRect; | |
struct appdata_t { | |
float4 vertex : POSITION; | |
float4 color : COLOR; | |
float2 uv : TEXCOORD0; | |
UNITY_VERTEX_INPUT_INSTANCE_ID | |
}; | |
struct v2f { | |
float4 vertex : SV_POSITION; | |
fixed4 color : COLOR; | |
float2 uv : TEXCOORD0; | |
float distance_range : TEXCOORD1; | |
float2 world : TEXCOORD2; | |
UNITY_VERTEX_OUTPUT_STEREO | |
}; | |
v2f vert(appdata_t v) { | |
v2f o; | |
UNITY_SETUP_INSTANCE_ID(v); | |
o.vertex = UnityObjectToClipPos(v.vertex); | |
o.color = v.color; | |
o.uv = TRANSFORM_TEX(v.uv, _MainTex); | |
o.distance_range = _Sharpness * (max(_MainTex_TexelSize.z, _MainTex_TexelSize.w) / 3072); | |
o.world = v.vertex; | |
return o; | |
} | |
fixed4 frag (v2f i) : SV_Target { | |
const float fw = fwidth(i.uv); | |
const float dxdy = fw * _MainTex_TexelSize.z; | |
#ifdef USE_MSDF | |
float m = median(tex2D(_MainTex, i.uv).rgb); | |
#else | |
float m = tex2D(_MainTex, i.uv).r; | |
#endif | |
float bleed = _Bleed; | |
#if ALPHA_AS_BLEED | |
bleed = _Bleed + i.color.a; | |
i.color.a = 1; | |
#endif | |
float alpha_inner = msdf(m, dxdy, bleed, i.distance_range); | |
#if BORDER_AS_CIRCLE | |
float sdf = sdf_circle(i.uv - float2(.5, .5), _EdgeRadius); | |
float alpha_outer = smoothstep(fw, -fw, sdf); | |
#else | |
float alpha_outer = msdf(m, dxdy, bleed + _EdgeRadius, i.distance_range * (1 - _EdgeFeather)); | |
#endif | |
fixed4 edge_color = fixed4(_EdgeColor.rgb, alpha_outer * _EdgeColor.a); | |
fixed4 inner_color = fixed4(i.color.rgb, alpha_inner * i.color.a); | |
#ifdef MULTIPLY_ALPHA | |
edge_color.rgb *= 1 - edge_color.a; | |
inner_color.rgb *= 1 - inner_color.a; | |
#endif | |
fixed4 color = lerp(inner_color, edge_color, 1 - alpha_inner); | |
// ui masking | |
color.a *= UnityGet2DClipping(i.world.xy, _ClipRect); | |
#ifdef UNITY_UI_ALPHACLIP | |
clip (color.a - 0.001); | |
#endif | |
return color; | |
} | |
ENDCG | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* \brief signed distance field circle | |
* \param p uv position | |
* \param r r is the radius of the circle | |
*/ | |
#define GLSL_MOD(x, y) (x - y * floor(x / y)) | |
float sdf_circle(const in float2 p, const in float r) { | |
return length(p) - r; | |
} | |
/** | |
* \brief signed distance field line segment | |
* \param p uv position | |
* \param a line start | |
* \param b line end | |
*/ | |
float sdf_segment( const in float2 p, const in float2 a, const in float2 b ) { | |
float2 pa = p - a, ba = b - a; | |
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); | |
return length(pa - ba * h); | |
} | |
/** | |
* \brief signed distance field circle arc | |
* \param p uv position | |
* \param sc sin/cos of the arc's aperture (ie, how much of a full circle it is 0-PI) | |
* calculate it like this: | |
* const float al = UNITY_PI * .25; | |
* const float2 sc = float2(sin(al), cos(al)); | |
* \param ra arc radius | |
* \param rb arc thickness | |
*/ | |
float sdf_arc(float2 p, float2 sc, float ra, float rb) { | |
p.x = abs(p.x); | |
return ((sc.y * p.x > sc.x * p.y) ? length(p - sc * ra) : abs(length(p) - ra)) - rb; | |
} | |
float sdf_box( in float2 p, in float2 size ) { | |
float2 d = abs(p) - size; | |
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); | |
} | |
float sdf_rounded_box( in float2 p, in float2 b, in float4 corner_radius){ | |
corner_radius.xy = p.x > 0.0 ? corner_radius.xy : corner_radius.zw; | |
corner_radius.x = p.y > 0.0 ? corner_radius.x : corner_radius.y; | |
float2 q = abs(p) - b + corner_radius.x; | |
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - corner_radius.x; | |
} | |
float sdf_triangle_isosceles(in float2 p, in float2 q){ | |
p.x = abs(p.x); | |
float2 a = p - q * clamp(dot(p, q) / dot(q, q), 0.0, 1.0); | |
float2 b = p - q * float2(clamp(p.x / q.x, 0.0, 1.0), 1.0); | |
float s = -sign(q.y); | |
float2 d = min(float2(dot(a, a), s * (p.x * q.y - p.y * q.x)), | |
float2(dot(b, b), s * (p.y - q.y))); | |
return -sqrt(d.x) * sign(d.y); | |
} | |
float sdf_capsule(in float2 uv_position, in float2 a, in float2 b, in float radius ) { | |
float2 pa = uv_position - a, ba = b - a; | |
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); | |
return length(pa - ba * h) - radius; | |
} | |
float median(float r, float g, float b) { | |
return max(min(r, g), min(max(r, g), b)); | |
} | |
float median(float3 val) { | |
return max(min(val.r, val.g), min(max(val.r, val.g), val.b)); | |
} | |
float msdf(float median, float dxdy, float bleed, float distance_range) { | |
float dist = median + min(bleed, 0.5 - 1.0 / distance_range) - 0.5; | |
return saturate(dist * distance_range / dxdy + 0.5); | |
} | |
// set fill_side to -1 to make the circle be filled on the outside instead of the inside | |
fixed4 circle_aa(float2 position, float radius, fixed4 color, float fill_side = 1){ | |
const float dist = sdf_circle(position, radius) * fill_side; | |
const float distance_change = fwidth(dist) * 0.5; | |
const float antialiased_cutoff = smoothstep(distance_change, -distance_change, dist); | |
return fixed4(color.rgb, antialiased_cutoff * color.a); | |
} | |
float _DPIRatio; | |
/** | |
* \brief | |
* \param vertex the vertex in clip pos | |
* \return | |
*/ | |
float2 normalized_screen_uv(float4 vertex){ | |
float4 screen_position = ComputeScreenPos(vertex); | |
// this divide should be in the fragment shader, but we can get away with it here | |
// because our noise "plane" is more or less coplanar with the camera | |
float2 screen_uv = screen_position.xy / screen_position.w; | |
float aspect = _ScreenParams.x / _ScreenParams.y; | |
screen_uv.x = screen_uv.x * aspect; | |
screen_uv *= _ScreenParams.y / 1080; | |
screen_uv /= _DPIRatio; | |
return screen_uv; | |
} | |
float2 rotate(float2 vertex, float radians) { | |
float sin, cos; | |
sincos(radians, sin, cos); | |
float2x2 m = float2x2(cos, -sin, sin, cos); | |
return mul(m, vertex.xy); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
here's a MSDF texture to experiment with:


and here's a SDF one, set it to be one channel!