
What it is (one-liner)
A lightweight per-material outline that detects edges by comparing the base texture to a slightly UV-offset sample; large color differences are drawn as a crisp outline.
Core idea
Edges often coincide with high local color contrast. If the color at uv differs enough from the color at a nearby uv + δ, we’re crossing a boundary—mark it with an outline color. This is a simple, fast edge detector (a 1-tap gradient approximation) you can extend by sampling several directions.
Minimal shader sketch (HLSL)
float _OutlineThreshold = 0.10; // sensitivity (0–1)
float _OutlineWidth = 1.0; // in texels (scaled below)
float4 _OutlineColor = float4(0,0,0,1);
float4 frag (v2f i) : SV_Target
{
float4 col = tex2D(_MainTex, i.uv);
// Convert width (in texels) to UV offset using Unity’s built-in _MainTex_TexelSize
float2 texel = _MainTex_TexelSize.xy; // (1/width, 1/height)
float2 d = texel * _OutlineWidth;
// Single-direction sample (left as in your slide). Add more directions for thicker/robust outlines.
float4 offsetCol = tex2D(_MainTex, i.uv - d);
// Edge strength = color gradient magnitude
float edge = length(col.rgb - offsetCol.rgb);
// Hard or soft edge
float m = step(_OutlineThreshold, edge); // hard threshold
// float m = smoothstep(_OutlineThreshold, _OutlineThreshold*1.3, edge); // softer
// Composite: draw outline where edge is strong
float4 outCol = lerp(col, _OutlineColor, m);
return outCol;
}
Parameters & what they do
Making it production-friendly
Multi-direction sampling (cleaner edges):
Sample ±X, ±Y (and optionally diagonals) and take the max edge value.
float4 c = tex2D(_MainTex, i.uv);
float2 d = _MainTex_TexelSize.xy * _OutlineWidth;
float3 e1 = abs(c.rgb - tex2D(_MainTex, i.uv + float2( d.x, 0)).rgb);
float3 e2 = abs(c.rgb - tex2D(_MainTex, i.uv + float2(-d.x, 0)).rgb);
float3 e3 = abs(c.rgb - tex2D(_MainTex, i.uv + float2(0, d.y)).rgb);
float3 e4 = abs(c.rgb - tex2D(_MainTex, i.uv + float2(0, -d.y)).rgb);
float edge = max(max(length(e1), length(e2)), max(length(e3), length(e4)));
This mimics a tiny gradient kernel without the cost of a full Sobel.
Thickness control:
Increase _OutlineWidth or run a tiny dilation by sampling a second, larger offset and OR-ing the mask.
Stability tips:
_OutlineThreshold.Performance notes:
Where to use it:
Disables (“hides”) a set of renderers/GameObjects only when the camera is in front of the portal and has a clear view to it. If anything between the camera and the portal is hit by a ray/spherecast (your occlusion mask), the objects are kept enabled to avoid the black void that appears when two stencil volumes overlap.