Shader "Custom/ToonBilal" { Properties { [Header(Global)] [Toggle] _ToonEnabled ("Enable Toon Filter", Float) = 1 _ShaderWeight ("Shader Weight", Range(0,1)) = 1.0 [Header(Toon Shading)] _ToonSteps ("Toon Steps", Float) = 4 _ShadowStrength ("Shadow Strength", Range(0,1)) = 0.15 _HighlightBoost ("Highlight Boost", Range(1,3)) = 1.6 _HighlightThreshold ("Highlight Threshold", Range(0,1)) = 0.65 [Header(Pencil Outlines)] _OutlineThickness ("Outline Thickness", Float) = 2.0 _OutlineThicknessVar ("Outline Thickness Variation", Range(0,1)) = 0.4 _DepthThreshold ("Depth Outline Threshold", Range(0,2)) = 0.3 _ColorThreshold ("Color Outline Threshold", Range(0,1)) = 0.2 _DepthOutlineStrength ("Depth Outline Strength", Range(0,1)) = 1.0 _ColorOutlineStrength ("Color Outline Strength", Range(0,1)) = 0.5 _OutlineSoftness ("Outline Softness", Range(0,1)) = 0.6 _OutlineColorBlend ("Outline Color From Surface", Range(0,1)) = 0.5 _OutlineDarken ("Outline Darken Amount", Range(0,1)) = 0.7 _OutlineDistanceFade ("Outline Distance Fade", Range(0,100)) = 30.0 [Header(Outline Brightness Fade)] _OutlineFadeStart ("Outline Fade Start", Range(0,1)) = 0.4 _OutlineFadeEnd ("Outline Fade End", Range(0,1)) = 0.9 _OutlineFadeStrength ("Outline Fade Strength", Range(0,1)) = 1.0 _OutlineBrightMin ("Outline Bright Min Visibility", Range(0,1)) = 0.2 [Header(Hatching)] _HatchTex ("Hatching Texture", 2D) = "white" {} _HatchScale ("Hatch Scale", Float) = 0.3 _HatchStrength ("Hatch Strength", Range(0,1)) = 0.95 _HatchThreshold ("Hatch Shadow Threshold", Range(0,1)) = 0.75 _CurvatureStrength ("Curvature Strength", Range(0,5)) = 2.0 _FlatSurface ("Flat Surface Reduction", Range(0,1)) = 0.3 [Header(Organic Hatching)] _HatchScale2 ("Hatch Scale Layer 2", Float) = 0.17 _HatchScale3 ("Hatch Scale Layer 3", Float) = 0.22 _HatchRotation ("Hatch Rotation", Range(0,3.14159)) = 0.785 _HatchLayerBlend ("Hatch Layer Blend", Range(0,1)) = 0.8 _HatchOrganicNoise ("Hatch Organic Noise", Range(0,1)) = 0.5 _HatchOrganicScale ("Hatch Organic Noise Scale", Float) = 2.0 _HatchDensityCurve ("Hatch Density Curve", Range(0.1,5)) = 1.0 _TriplanarSharpness ("Triplanar Sharpness", Range(1,8)) = 2.0 [Header(Pencil Hatching)] _HatchColorBlend ("Hatch Color From Surface", Range(0,1)) = 0.5 _HatchDarken ("Hatch Darken Amount", Range(0,1)) = 0.85 _HatchGrainScale ("Hatch Grain Scale", Float) = 120.0 _HatchGrainStrength ("Hatch Grain Strength", Range(0,1)) = 0.25 _HatchPressure ("Hatch Pressure Variation", Range(0,1)) = 0.5 [Header(Gouache Shadow)] _BrushTex ("Brush Texture", 2D) = "white" {} _GouacheScale ("Gouache Scale", Float) = 0.8 _GouacheStrength ("Gouache Strength", Range(0,1)) = 0.7 _GouacheThreshold ("Gouache Shadow Threshold", Range(0,1)) = 0.35 _GouacheSaturation ("Gouache Saturation Boost", Range(0,2)) = 1.3 _GouachePosterize ("Gouache Posterize Steps", Range(1,6)) = 3.0 _GouacheBlend ("Gouache Blend With Hatch", Range(0,1)) = 0.6 } SubShader { Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" } ZWrite Off ZTest Always Cull Off Pass { Name "ToonBilalPass" HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareNormalsTexture.hlsl" #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl" float _ToonEnabled; float _ShaderWeight; float _ToonSteps; float _ShadowStrength; float _HighlightBoost; float _HighlightThreshold; float _OutlineThickness; float _OutlineThicknessVar; float _DepthThreshold; float _ColorThreshold; float _DepthOutlineStrength; float _ColorOutlineStrength; float _OutlineSoftness; float _OutlineColorBlend; float _OutlineDarken; float _OutlineDistanceFade; float _OutlineFadeStart; float _OutlineFadeEnd; float _OutlineFadeStrength; float _OutlineBrightMin; float _HatchScale; float _HatchScale2; float _HatchScale3; float _HatchStrength; float _HatchThreshold; float _CurvatureStrength; float _FlatSurface; float _HatchRotation; float _HatchLayerBlend; float _HatchOrganicNoise; float _HatchOrganicScale; float _HatchDensityCurve; float _TriplanarSharpness; float _HatchColorBlend; float _HatchDarken; float _HatchGrainScale; float _HatchGrainStrength; float _HatchPressure; float _GouacheScale; float _GouacheStrength; float _GouacheThreshold; float _GouacheSaturation; float _GouachePosterize; float _GouacheBlend; TEXTURE2D(_HatchTex); SAMPLER(sampler_HatchTex); TEXTURE2D(_BrushTex); SAMPLER(sampler_BrushTex); // ─── NOISE ─────────────────────────────────────────────── float Hash21(float2 p) { p = frac(p * float2(234.34, 435.345)); p += dot(p, p + 34.23); return frac(p.x * p.y); } float SmoothNoise(float2 p) { float2 i = floor(p); float2 f = frac(p); f = f * f * (3.0 - 2.0 * f); return lerp( lerp(Hash21(i), Hash21(i + float2(1,0)), f.x), lerp(Hash21(i + float2(0,1)), Hash21(i + float2(1,1)), f.x), f.y ); } float FBM(float2 p) { float v = 0.0; float a = 0.5; float2 shift = float2(100.0, 100.0); for (int i = 0; i < 4; i++) { v += a * SmoothNoise(p); p = p * 2.0 + shift; a *= 0.5; } return v; } // ─── RECONSTRUCTION ────────────────────────────────────── float3 ReconstructWorldPos(float2 uv, float depth) { float4 clipPos = float4(uv * 2.0 - 1.0, depth, 1.0); #if UNITY_UV_STARTS_AT_TOP clipPos.y = -clipPos.y; #endif float4 worldPos = mul(UNITY_MATRIX_I_VP, clipPos); return worldPos.xyz / worldPos.w; } float3 ReconstructNormal(float2 uv, float2 texel) { float d0 = SampleSceneDepth(uv); float dr = SampleSceneDepth(uv + float2(texel.x, 0)); float du = SampleSceneDepth(uv + float2(0, texel.y)); float3 p0 = ReconstructWorldPos(uv, d0); float3 pr = ReconstructWorldPos(uv + float2(texel.x, 0), dr); float3 pu = ReconstructWorldPos(uv + float2(0, texel.y), du); return normalize(cross(pr - p0, pu - p0)); } float ComputeCurvature(float2 uv, float2 texel) { float3 n0 = ReconstructNormal(uv, texel); float3 nr = ReconstructNormal(uv + float2(texel.x * 2.0, 0), texel); float3 nu = ReconstructNormal(uv + float2(0, texel.y * 2.0), texel); float3 nl = ReconstructNormal(uv - float2(texel.x * 2.0, 0), texel); float3 nd = ReconstructNormal(uv - float2(0, texel.y * 2.0), texel); return saturate((length(nr-n0)+length(nu-n0)+length(nl-n0)+length(nd-n0)) * _CurvatureStrength); } // Linearizes the depth buffer — returns consistent meter values across the scene float LinearizeDepth(float rawDepth) { return LinearEyeDepth(rawDepth, _ZBufferParams); } float2 RotateUV(float2 uv, float angle) { float s = sin(angle); float c = cos(angle); return float2(uv.x * c - uv.y * s, uv.x * s + uv.y * c); } // ─── SATURATION ───────────────────────────────────────── half3 AdjustSaturation(half3 col, float saturation) { float lum = dot(col, float3(0.299, 0.587, 0.114)); return lerp(half3(lum, lum, lum), col, saturation); } // ─── PENCIL COLOR ──────────────────────────────────────── half3 PencilColor(half3 surfaceCol, float darken, float colorBlend) { half3 darkSurface = surfaceCol * (1.0 - darken); return lerp(half3(0,0,0), darkSurface, colorBlend); } // ─── GOUACHE SHADOW ────────────────────────────────────── half3 ApplyGouache(half3 col, float3 worldPos, float lum, float curvature) { float shadowMask = 1.0 - smoothstep(0.0, _GouacheThreshold, lum); if (shadowMask <= 0.0) return col; float2 uvBrush1 = worldPos.xz * _GouacheScale; float2 uvBrush2 = worldPos.xy * _GouacheScale * 0.7 + float2(3.1, 7.4); float2 uvBrush3 = worldPos.yz * _GouacheScale * 1.3 + float2(1.7, 2.9); float b1 = SAMPLE_TEXTURE2D(_BrushTex, sampler_BrushTex, uvBrush1).r; float b2 = SAMPLE_TEXTURE2D(_BrushTex, sampler_BrushTex, uvBrush2).r; float b3 = SAMPLE_TEXTURE2D(_BrushTex, sampler_BrushTex, uvBrush3).r; float brush = (b1 * 0.5 + b2 * 0.3 + b3 * 0.2); float3 gouacheCol = col; gouacheCol.r = floor(gouacheCol.r * _GouachePosterize) / _GouachePosterize; gouacheCol.g = floor(gouacheCol.g * _GouachePosterize) / _GouachePosterize; gouacheCol.b = floor(gouacheCol.b * _GouachePosterize) / _GouachePosterize; gouacheCol = AdjustSaturation(gouacheCol, _GouacheSaturation); float brushMask = 1.0 - brush * 0.5; return lerp(col, gouacheCol * brushMask, shadowMask * _GouacheStrength); } // ─── TRIPLANAR HATCHING ─────────────────────────────────── // Normal-oriented UVs: rotates hatch UVs based on surface normal direction // so each face gets its own stroke orientation — anchored to world geometry float2 NormalOrientedUV(float3 worldPos, float3 normal, float3 tangent, float3 bitangent, float scale) { return float2(dot(worldPos, tangent), dot(worldPos, bitangent)) * scale; } float OrganicHatch(float3 worldPos, float3 worldNormal, float shadowAmount, float curvature, float2 screenUV, float toonEdge) { float3 weights = pow(abs(worldNormal), _TriplanarSharpness); weights /= (weights.x + weights.y + weights.z + 0.0001); // Organic deformation — world-space only float2 uvY = worldPos.xz * _HatchScale; float2 off = float2( FBM(uvY * _HatchOrganicScale), FBM(uvY * _HatchOrganicScale + float2(5.2, 1.3)) ) * _HatchOrganicNoise; // World-space grain and pressure — stable under camera movement float grain = SmoothNoise(worldPos.xz * _HatchGrainScale * 0.01); float pressure = SmoothNoise(worldPos.xy * _HatchGrainScale * 0.003 + float2(3.7, 8.1)); float pressureVar = lerp(1.0, pressure * 1.5, _HatchPressure); // ── Layer 1: normal-oriented per face // Build per-face tangent frames so strokes follow surface orientation float3 upX = abs(worldNormal.x) > 0.9 ? float3(0,1,0) : float3(1,0,0); float3 tX = normalize(cross(upX, float3(1,0,0))); float3 bX = normalize(cross(float3(1,0,0), tX)); float3 upY = float3(1,0,0); float3 tY = normalize(cross(upY, float3(0,1,0))); float3 bY = normalize(cross(float3(0,1,0), tY)); float3 upZ = abs(worldNormal.z) > 0.9 ? float3(0,1,0) : float3(0,0,1); float3 tZ = normalize(cross(upZ, float3(0,0,1))); float3 bZ = normalize(cross(float3(0,0,1), tZ)); float2 uvX1 = NormalOrientedUV(worldPos, worldNormal, tX, bX, _HatchScale) + off; float2 uvY1 = NormalOrientedUV(worldPos, worldNormal, tY, bY, _HatchScale) + off; float2 uvZ1 = NormalOrientedUV(worldPos, worldNormal, tZ, bZ, _HatchScale) + off; float h1X = SAMPLE_TEXTURE2D(_HatchTex, sampler_HatchTex, uvX1).r; float h1Y = SAMPLE_TEXTURE2D(_HatchTex, sampler_HatchTex, uvY1).r; float h1Z = SAMPLE_TEXTURE2D(_HatchTex, sampler_HatchTex, uvZ1).r; float h1 = h1X * weights.x + h1Y * weights.y + h1Z * weights.z; // ── Layer 2: rotated for cross-hatching float2 uvX2 = RotateUV(NormalOrientedUV(worldPos, worldNormal, tX, bX, _HatchScale2), _HatchRotation) + off * 0.5; float2 uvY2 = RotateUV(NormalOrientedUV(worldPos, worldNormal, tY, bY, _HatchScale2), _HatchRotation) + off * 0.5; float2 uvZ2 = RotateUV(NormalOrientedUV(worldPos, worldNormal, tZ, bZ, _HatchScale2), _HatchRotation) + off * 0.5; float h2X = SAMPLE_TEXTURE2D(_HatchTex, sampler_HatchTex, uvX2).r; float h2Y = SAMPLE_TEXTURE2D(_HatchTex, sampler_HatchTex, uvY2).r; float h2Z = SAMPLE_TEXTURE2D(_HatchTex, sampler_HatchTex, uvZ2).r; float h2 = h2X * weights.x + h2Y * weights.y + h2Z * weights.z; // ── Layer 3: third angle for very dark/curved zones float angle3 = _HatchRotation * 1.7; float2 uvX3 = RotateUV(NormalOrientedUV(worldPos, worldNormal, tX, bX, _HatchScale3), angle3) + off * 0.3; float2 uvY3 = RotateUV(NormalOrientedUV(worldPos, worldNormal, tY, bY, _HatchScale3), angle3) + off * 0.3; float2 uvZ3 = RotateUV(NormalOrientedUV(worldPos, worldNormal, tZ, bZ, _HatchScale3), angle3) + off * 0.3; float h3X = SAMPLE_TEXTURE2D(_HatchTex, sampler_HatchTex, uvX3).r; float h3Y = SAMPLE_TEXTURE2D(_HatchTex, sampler_HatchTex, uvY3).r; float h3Z = SAMPLE_TEXTURE2D(_HatchTex, sampler_HatchTex, uvZ3).r; float h3 = h3X * weights.x + h3Y * weights.y + h3Z * weights.z; // Shadow-driven layer blending — more layers in darker/curved zones float darkness = 1.0 - shadowAmount; float blend12 = lerp(_HatchLayerBlend, 1.0, saturate(darkness * 1.5 + curvature)); float blend3 = saturate((darkness - 0.4) * 2.0 + curvature * 0.8 + toonEdge); float hatch = lerp(h1, min(h1, h2), blend12); hatch = lerp(hatch, min(hatch, h3), blend3); hatch = saturate(hatch + grain * _HatchGrainStrength); // Shadow mask — wider in dark zones, boosted at toon edges and curves float shadowMask = pow(smoothstep(_HatchThreshold, 0.0, shadowAmount), _HatchDensityCurve); float edgeMask = saturate(toonEdge * 2.0 + curvature); shadowMask = saturate(shadowMask + edgeMask * 0.6); float curvatureMask = lerp(_FlatSurface, 1.0, saturate(curvature * 2.0)); float curvatureBoost = 1.0 + curvature * 1.2; // Traits noirs sur fond blanc: hatch value IS the stroke return shadowMask * curvatureMask * _HatchStrength * pressureVar * hatch * curvatureBoost; } // ─── FRAGMENT ──────────────────────────────────────────── half4 Frag(Varyings input) : SV_Target { float2 uv = input.texcoord; half3 col = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv).rgb; float2 texelBase = float2(1.0 / _ScreenParams.x, 1.0 / _ScreenParams.y); float lum = dot(col, float3(0.299, 0.587, 0.114)); // ── World space — computed early, used throughout float rawDepth = SampleSceneDepth(uv); float linDepth = LinearizeDepth(rawDepth); float3 worldPos = ReconstructWorldPos(uv, rawDepth); float3 worldNormal = SampleSceneNormals(uv); float curvature = ComputeCurvature(uv, texelBase); // ── Outlines with variable thickness, fading with distance float distanceFade = 1.0 - saturate(linDepth / _OutlineDistanceFade); float noiseThick = SmoothNoise(worldPos.xz * 2.0); float thicknessVar = lerp(1.0, noiseThick * 2.0, _OutlineThicknessVar) * distanceFade; float2 texelThin = texelBase * _OutlineThickness * 0.5 * thicknessVar; float2 texelThick = texelBase * _OutlineThickness * 1.5 * thicknessVar; half3 cn = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv + float2(0, texelThin.y)).rgb; half3 cs = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv - float2(0, texelThin.y)).rgb; half3 ce = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv + float2(texelThin.x, 0)).rgb; half3 cw = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv - float2(texelThin.x, 0)).rgb; float lumN = dot(cn, float3(0.299, 0.587, 0.114)); float lumS = dot(cs, float3(0.299, 0.587, 0.114)); float lumE = dot(ce, float3(0.299, 0.587, 0.114)); float lumW = dot(cw, float3(0.299, 0.587, 0.114)); float lumMax = max(max(lumN, lumS), max(lumE, lumW)); float brightnessForFade = max(lum, lumMax); half3 cn_n = normalize(cn + 0.001); half3 cs_n = normalize(cs + 0.001); half3 ce_n = normalize(ce + 0.001); half3 cw_n = normalize(cw + 0.001); float colorEdgeInner = length(cn_n - cs_n) + length(ce_n - cw_n); half3 cnO = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv + float2(0, texelThick.y)).rgb; half3 csO = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv - float2(0, texelThick.y)).rgb; half3 ceO = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv + float2(texelThick.x, 0)).rgb; half3 cwO = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv - float2(texelThick.x, 0)).rgb; float colorEdgeOuter = length(normalize(cnO+0.001) - normalize(csO+0.001)) + length(normalize(ceO+0.001) - normalize(cwO+0.001)); float colorEdge = lerp(colorEdgeInner, colorEdgeOuter * 0.5, _OutlineSoftness); colorEdge = step(_ColorThreshold, colorEdge) * _ColorOutlineStrength; // Linearized depth outlines — consistent threshold in meters, not raw buffer float2 texelOutline = texelBase * _OutlineThickness * thicknessVar; float dn = LinearizeDepth(SampleSceneDepth(uv + float2(0, texelOutline.y))); float ds = LinearizeDepth(SampleSceneDepth(uv - float2(0, texelOutline.y))); float de = LinearizeDepth(SampleSceneDepth(uv + float2(texelOutline.x, 0))); float dw = LinearizeDepth(SampleSceneDepth(uv - float2(texelOutline.x, 0))); float depthRaw = abs(dn-ds) + abs(de-dw); float depthEdge = smoothstep(_DepthThreshold * 0.5, _DepthThreshold * 2.0, depthRaw) * _DepthOutlineStrength; depthEdge *= 1.0 - smoothstep(0.5, 0.85, brightnessForFade); depthEdge *= distanceFade; float edge = saturate(depthEdge + colorEdge); // ── Toon shading with highlight boost (Bilal: near-white highlights) float toon = floor(lum * _ToonSteps) / _ToonSteps; toon = lerp(_ShadowStrength, 1.0, toon); half3 toonCol = col * toon; // Blow out highlights — bright zones go nearly white like Bilal float highlightMask = smoothstep(_HighlightThreshold, 1.0, lum); toonCol = lerp(toonCol, toonCol * _HighlightBoost, highlightMask); toonCol = min(toonCol, 1.2); // allow slight overexposure // ── Gouache in dark areas toonCol = ApplyGouache(toonCol, worldPos, lum, curvature); // ── Hatching — fully world-space, stable under camera movement float3 lightDir = normalize(float3(1, -1, 1)); float toonEdge = saturate(fwidth(floor(lum * _ToonSteps)) * 10.0); float hatchBoost = saturate(toonEdge + curvature * 0.5); float shadowAmount = saturate(dot(worldNormal, -lightDir) + hatchBoost * 0.4); float hatchBlend = OrganicHatch(worldPos, worldNormal, shadowAmount, curvature, uv, toonEdge); float gouacheMask = 1.0 - smoothstep(0.0, _GouacheThreshold, lum); hatchBlend *= lerp(1.0, 1.0 - _GouacheBlend, gouacheMask); // Hatch fades in bright zones — highlights stay clean hatchBlend *= 1.0 - highlightMask * 0.8; half3 hatchCol = PencilColor(col, _HatchDarken, _HatchColorBlend); toonCol = lerp(toonCol, hatchCol, hatchBlend); // ── Outline fade on bright surfaces float outlineFade = lerp(_OutlineBrightMin, 1.0, 1.0 - smoothstep(_OutlineFadeStart, _OutlineFadeEnd, brightnessForFade)); outlineFade = lerp(1.0, outlineFade, _OutlineFadeStrength); half3 outlineCol = PencilColor(col, _OutlineDarken, _OutlineColorBlend); half3 finalCol = lerp(toonCol, outlineCol, edge * outlineFade); finalCol = lerp(col, finalCol, _ShaderWeight * _ToonEnabled); return half4(finalCol, 1); } ENDHLSL } } CustomEditor "ToonBilalEditor" }