Shader "Custom/CannyEdgeDetection"
_MainTex("Base Texture", 2D) = "white" {}
_ThresholdLow("Lower Threshold", Float) = 0.1
_ThresholdHigh("Upper Threshold", Float) = 0.3
Tags { "RenderType" = "Opaque" }
float4 _MainTex_TexelSize;
float4 vertex : POSITION;
float4 vertex : SV_POSITION;
o.vertex = UnityObjectToClipPos(v.vertex);
float4 frag(v2f i) : SV_Target
// Fetch texture coordinates offsets for sampling neighboring pixels
float2 texelSize = _MainTex_TexelSize.xy;
// Step 1: Gaussian Blur (3x3 kernel)
float blurKernel[9] = { 1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0,
2.0 / 16.0, 4.0 / 16.0, 2.0 / 16.0,
1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0 };
for (int y = -1; y <= 1; y++)
for (int x = -1; x <= 1; x++)
float2 offset = texelSize * float2(x, y);
float sample = tex2D(_MainTex, i.uv + offset).r;
blurSum += sample * blurKernel[index++];
float blurredValue = blurSum;
// Step 2: Sobel Gradient Calculation (Using the blurred depth value)
float tl = tex2D(_MainTex, i.uv + texelSize * float2(-1.0, 1.0)).r; // top-left
float t = tex2D(_MainTex, i.uv + texelSize * float2(0.0, 1.0)).r; // top
float tr = tex2D(_MainTex, i.uv + texelSize * float2(1.0, 1.0)).r; // top-right
float l = tex2D(_MainTex, i.uv + texelSize * float2(-1.0, 0.0)).r; // left
float r = tex2D(_MainTex, i.uv + texelSize * float2(1.0, 0.0)).r; // right
float bl = tex2D(_MainTex, i.uv + texelSize * float2(-1.0, -1.0)).r; // bottom-left
float b = tex2D(_MainTex, i.uv + texelSize * float2(0.0, -1.0)).r; // bottom
float br = tex2D(_MainTex, i.uv + texelSize * float2(1.0, -1.0)).r; // bottom-right
// Use blurredValue for Sobel calculation instead of original texture value
float Gx = -tl - 2.0 * l - bl + tr + 2.0 * r + br;
float Gy = -tl - 2.0 * t - tr + bl + 2.0 * b + br;
// Step 3: Gradient Magnitude and Angle Calculation
float gradientMagnitude = sqrt(Gx * Gx + Gy * Gy);
float gradientAngle = atan2(Gy, Gx);
// Step 4: Non-Maximum Suppression
float suppressedValue = 0.0;
float angle = gradientAngle * (180.0 / 3.14159); // Convert to degrees
// Align angle to nearest 0, 45, 90, or 135 degrees
if (angle < 0.0) angle += 180.0;
if ((angle >= 0.0 && angle < 22.5) || (angle >= 157.5 && angle <= 180.0))
neighbor1 = tex2D(_MainTex, i.uv + float2(texelSize.x, 0)).r;
neighbor2 = tex2D(_MainTex, i.uv - float2(texelSize.x, 0)).r;
else if (angle >= 22.5 && angle < 67.5)
neighbor1 = tex2D(_MainTex, i.uv + float2(texelSize.x, -texelSize.y)).r;
neighbor2 = tex2D(_MainTex, i.uv - float2(texelSize.x, texelSize.y)).r;
else if (angle >= 67.5 && angle < 112.5)
neighbor1 = tex2D(_MainTex, i.uv + float2(0, texelSize.y)).r;
neighbor2 = tex2D(_MainTex, i.uv - float2(0, texelSize.y)).r;
neighbor1 = tex2D(_MainTex, i.uv + float2(texelSize.x, texelSize.y)).r;
neighbor2 = tex2D(_MainTex, i.uv - float2(texelSize.x, texelSize.y)).r;
// If gradient magnitude is greater than neighbors, keep it
if (gradientMagnitude >= neighbor1 && gradientMagnitude >= neighbor2)
suppressedValue = gradientMagnitude;
// Step 5: Double Thresholding and Edge Tracking by Hysteresis
float strongEdge = step(_ThresholdHigh, suppressedValue);
float weakEdge = step(_ThresholdLow, suppressedValue) * (1.0 - strongEdge);
// Combine results: Strong edges are white, weak edges are gray, others are black
float edge = strongEdge + weakEdge * 0.5;
return float4(edge, edge, edge, 1.0);