Skip to content

Instantly share code, notes, and snippets.

@grapefrukt
Created May 31, 2023 08:21
Show Gist options
  • Save grapefrukt/b864b913df2fbe86c91ab95892e80146 to your computer and use it in GitHub Desktop.
Save grapefrukt/b864b913df2fbe86c91ab95892e80146 to your computer and use it in GitHub Desktop.
SDF UI for Unity
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
}
}
}
/**
* \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);
}
@grapefrukt
Copy link
Author

grapefrukt commented May 31, 2023

here's a MSDF texture to experiment with:
msdf-icon-sleep
and here's a SDF one, set it to be one channel!
msdf-icon-new

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment