Shader "Custom/ToonBilal" { Properties { [Header(Global)] _ShaderWeight ("Shader Weight", Range(0,1)) = 1.0 [Header(Toon Shading)] _ToonSteps ("Toon Steps", Float) = 3 _ShadowStrength ("Shadow Strength", Range(0,1)) = 0.3 [Header(Pencil Outlines)] _OutlineThickness ("Outline Thickness", Float) = 2.0 _OutlineThicknessVar ("Outline Thickness Variation", Range(0,1)) = 0.4 _DepthThreshold ("Depth Outline Threshold", Range(0,0.1)) = 0.01 _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 [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.4 [Header(Hatching)] _HatchTex ("Hatching Texture", 2D) = "white" {} _HatchScale ("Hatch Scale", Float) = 0.3 _HatchStrength ("Hatch Strength", Range(0,1)) = 0.6 _HatchThreshold ("Hatch Shadow Threshold", Range(0,1)) = 0.5 _CurvatureStrength ("Curvature Strength", Range(0,5)) = 1.5 _FlatSurface ("Flat Surface Reduction", Range(0,1)) = 0.15 [Header(Organic Hatching)] _HatchScale2 ("Hatch Scale Layer 2", Float) = 0.17 _HatchRotation ("Hatch Rotation", Range(0,3.14159)) = 0.785 _HatchLayerBlend ("Hatch Layer Blend", Range(0,1)) = 0.5 _HatchOrganicNoise ("Hatch Organic Noise", Range(0,1)) = 0.4 _HatchOrganicScale ("Hatch Organic Noise Scale", Float) = 2.0 _HatchDensityCurve ("Hatch Density Curve", Range(0.1,5)) = 2.0 _TriplanarSharpness ("Triplanar Sharpness", Range(1,8)) = 2.0 [Header(Pencil Hatching)] _HatchColorBlend ("Hatch Color From Surface", Range(0,1)) = 0.6 _HatchDarken ("Hatch Darken Amount", Range(0,1)) = 0.75 _HatchGrainScale ("Hatch Grain Scale", Float) = 120.0 _HatchGrainStrength ("Hatch Grain Strength", Range(0,1)) = 0.3 _HatchPressure ("Hatch Pressure Variation", Range(0,1)) = 0.4 [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.core/Runtime/Utilities/Blit.hlsl" float _ShaderWeight; float _ToonSteps; float _ShadowStrength; float _OutlineThickness; float _OutlineThicknessVar; float _DepthThreshold; float _ColorThreshold; float _DepthOutlineStrength; float _ColorOutlineStrength; float _OutlineSoftness; float _OutlineColorBlend; float _OutlineDarken; float _OutlineFadeStart; float _OutlineFadeEnd; float _OutlineFadeStrength; float _OutlineBrightMin; float _HatchScale; float _HatchScale2; 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); } 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); } // ─── COULEUR CRAYON ─────────────────────────────────────── 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) { // On est dans la zone sombre ? float shadowMask = 1.0 - smoothstep(0.0, _GouacheThreshold, lum); if (shadowMask <= 0.0) return col; // Brush texture en triplanar pour coller aux surfaces 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); // Posterize la couleur pour effet aplat gouache float3 gouacheCol = col; gouacheCol.r = floor(gouacheCol.r * _GouachePosterize) / _GouachePosterize; gouacheCol.g = floor(gouacheCol.g * _GouachePosterize) / _GouachePosterize; gouacheCol.b = floor(gouacheCol.b * _GouachePosterize) / _GouachePosterize; // Boost saturation — les ombres gouache sont riches en couleur gouacheCol = AdjustSaturation(gouacheCol, _GouacheSaturation); // La texture de brush module l'opacité de l'aplat // Zones où le brush est foncé = plus de gouache visible float brushMask = 1.0 - brush * 0.5; // Résultat final gouache half3 result = lerp(col, gouacheCol * brushMask, shadowMask * _GouacheStrength); return result; } // ─── HACHURES TRIPLANAR ─────────────────────────────────── float OrganicHatch(float3 worldPos, float3 worldNormal, float shadowAmount, float curvature, float2 screenUV) { float3 weights = pow(abs(worldNormal), _TriplanarSharpness); weights /= (weights.x + weights.y + weights.z + 0.0001); float2 uvX = worldPos.yz * _HatchScale; float2 uvY = worldPos.xz * _HatchScale; float2 uvZ = worldPos.xy * _HatchScale; float2 off = float2( FBM(uvY * _HatchOrganicScale), FBM(uvY * _HatchOrganicScale + float2(5.2, 1.3)) ) * _HatchOrganicNoise; float grain = SmoothNoise(screenUV * _HatchGrainScale); float pressure = SmoothNoise(screenUV * _HatchGrainScale * 0.3 + float2(3.7, 8.1)); float pressureVar = lerp(1.0, pressure * 1.5, _HatchPressure); float h1X = SAMPLE_TEXTURE2D(_HatchTex, sampler_HatchTex, uvX + off).r; float h1Y = SAMPLE_TEXTURE2D(_HatchTex, sampler_HatchTex, uvY + off).r; float h1Z = SAMPLE_TEXTURE2D(_HatchTex, sampler_HatchTex, uvZ + off).r; float h1 = h1X * weights.x + h1Y * weights.y + h1Z * weights.z; float2 uvX2 = RotateUV(worldPos.yz * _HatchScale2, _HatchRotation) + off * 0.5; float2 uvY2 = RotateUV(worldPos.xz * _HatchScale2, _HatchRotation) + off * 0.5; float2 uvZ2 = RotateUV(worldPos.xy * _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; float layerBlend = lerp(_HatchLayerBlend, 1.0, shadowAmount); float hatch = lerp(h1, min(h1, h2), layerBlend); hatch = saturate(hatch + grain * _HatchGrainStrength); float shadowMask = pow(1.0 - smoothstep(0.0, _HatchThreshold, 1.0 - shadowAmount), _HatchDensityCurve); float curvatureMask = lerp(_FlatSurface, 1.0, curvature); return shadowMask * curvatureMask * _HatchStrength * pressureVar * (1.0 - hatch); } // ─── FRAGMENT ──────────────────────────────────────────── half4 Frag(Varyings input) : SV_Target { float2 uv = input.texcoord; float2 texelBase = float2(1.0 / _ScreenParams.x, 1.0 / _ScreenParams.y); half3 col = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv).rgb; float lum = dot(col, float3(0.299, 0.587, 0.114)); // ── Outlines avec épaisseur variable float noiseThick = SmoothNoise(uv * 80.0); float thicknessVar = lerp(1.0, noiseThick * 2.0, _OutlineThicknessVar); 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; float2 texelOutline = texelBase * _OutlineThickness * thicknessVar; float dn = SampleSceneDepth(uv + float2(0, texelOutline.y)); float ds = SampleSceneDepth(uv - float2(0, texelOutline.y)); float de = SampleSceneDepth(uv + float2(texelOutline.x, 0)); float dw = 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); float edge = saturate(depthEdge + colorEdge); // ── Toon shading float toon = floor(lum * _ToonSteps) / _ToonSteps; toon = lerp(_ShadowStrength, 1.0, toon); half3 toonCol = col * toon; // ── World space float depth = SampleSceneDepth(uv); float3 worldPos = ReconstructWorldPos(uv, depth); float3 worldNormal = ReconstructNormal(uv, texelBase); float curvature = ComputeCurvature(uv, texelBase); // ── Gouache dans les zones sombres — appliqué avant les hachures toonCol = ApplyGouache(toonCol, worldPos, lum, curvature); // ── Hachures par dessus la gouache float shadowAmount = 1.0 - lum; float hatchBlend = OrganicHatch(worldPos, worldNormal, shadowAmount, curvature, uv); // Dans les zones très sombres on réduit les hachures (la gouache prend le relais) float gouacheMask = 1.0 - smoothstep(0.0, _GouacheThreshold, lum); hatchBlend *= lerp(1.0, 1.0 - _GouacheBlend, gouacheMask); half3 hatchCol = PencilColor(col, _HatchDarken, _HatchColorBlend); toonCol = lerp(toonCol, hatchCol, hatchBlend); // ── Outline fade 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); // ── Weight global finalCol = lerp(col, finalCol, _ShaderWeight); return half4(finalCol, 1); } ENDHLSL } } }