源码 / Ch14/ToonLit.shader
Ch14/ToonLit.shader
// 本章的完整卡通 Shader:色阶(两层阴影色 / 可选 Ramp 贴图)+ 卡通化接收阴影 + 风格化高光 + 边缘光 // + 附加光 + 平坦环境光 + 背面外扩描边(裁剪空间等宽,可选平滑法线来源)。 Shader "Cookbook/Ch14/ToonLit" { Properties { [MainTexture] _BaseMap ("Base Map", 2D) = "white" {} [MainColor] _BaseColor ("Base Color", Color) = (1, 1, 1, 1) [Header(Shading)] _ShadeColor ("1st Shade Color", Color) = (0.7, 0.6, 0.75, 1) _ShadeColor2 ("2nd Shade Color", Color) = (0.45, 0.38, 0.55, 1) _Threshold ("1st Shade Threshold", Range(0, 1)) = 0.5 _Shade2Threshold ("2nd Shade Threshold", Range(0, 1)) = 0.2 _Shade2Strength ("2nd Shade Strength", Range(0, 1)) = 1 _Softness ("Edge Softness", Range(0, 0.3)) = 0.02 [Toggle(_RAMPMAP)] _UseRamp ("Use Ramp Map (overrides shade colors)", Float) = 0 [NoScaleOffset] _RampTex ("Ramp Map", 2D) = "white" {} [Header(Shadows)] _ReceiveShadow ("Receive Shadow", Range(0, 1)) = 1 _ShadowSoftness ("Shadow Edge Softness", Range(0, 0.5)) = 0.05 _ShadowNormalOffset ("Shadow Receiver Normal Offset (m)", Range(0, 0.1)) = 0.03 [Header(Specular)] [HDR] _SpecColor ("Specular Color", Color) = (1, 1, 1, 1) _SpecThreshold ("Specular Threshold", Range(0.5, 1)) = 0.97 _SpecStrength ("Specular Strength", Range(0, 1)) = 1 [Header(Rim)] [HDR] _RimColor ("Rim Color", Color) = (1, 0.95, 0.9, 1) _RimWidth ("Rim Width", Range(0, 1)) = 0.25 _RimStrength ("Rim Strength", Range(0, 1)) = 0.6 [Header(Ambient and Extra Lights)] _AmbientStrength ("Ambient Strength", Range(0, 1)) = 0.35 _AddLightStrength ("Additional Lights Strength", Range(0, 2)) = 1 [Header(Outline)] _OutlineColor ("Outline Color", Color) = (0.15, 0.1, 0.12, 1) _OutlineWidth ("Outline Width (px)", Range(0, 10)) = 2.5 [KeywordEnum(Mesh, Tangent, VertexColor)] _OutlineNormal ("Outline Normal Source", Float) = 0 } SubShader { Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" } HLSLINCLUDE #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl" CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor, _ShadeColor, _ShadeColor2, _SpecColor, _RimColor, _OutlineColor; half _Threshold, _Shade2Threshold, _Shade2Strength, _Softness; half _ReceiveShadow, _ShadowSoftness, _ShadowNormalOffset; half _SpecThreshold, _SpecStrength, _RimWidth, _RimStrength; half _AmbientStrength, _AddLightStrength, _OutlineWidth; CBUFFER_END TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); TEXTURE2D(_RampTex); SAMPLER(sampler_RampTex); ENDHLSL Pass { Name "ToonForward" Tags { "LightMode"="UniversalForwardOnly" } HLSLPROGRAM #pragma target 3.5 #pragma vertex ToonVert #pragma fragment frag #pragma shader_feature_local_fragment _ _RAMPMAP #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN #pragma multi_compile _ _ADDITIONAL_LIGHTS #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH #pragma multi_compile _ _FORWARD_PLUS #pragma multi_compile_fog #pragma multi_compile_instancing #include "Assets/Cookbook/Ch14/ToonCommon.hlsl" // 主光的色阶:输入是“半 Lambert × 卡通化后的阴影”,输出是最终的漫反射颜色 half3 ToonDiffuse(half3 albedo, half halfLambert, half shadow) { // 接收阴影卡通化:把 PCF 的软阴影值也切成硬边,再“乘进”明暗,而不是在最后乘一次 half toonShadow = lerp(1.0, ToonBand(shadow, 0.5, _ShadowSoftness), _ReceiveShadow); // 投影区域只压到“第一层阴影”的位置,而不是压到 0(那会掉进第二层阴影,比背光面还暗) half shadowedX = min(halfLambert, (_Threshold + _Shade2Threshold) * 0.5); half x = lerp(shadowedX, halfLambert, toonShadow); #if defined(_RAMPMAP) return albedo * SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, float2(clamp(x, 0.01, 0.99), 0.5)).rgb; #else half band1 = ToonBand(x, _Threshold, _Softness); half band2 = ToonBand(x, _Shade2Threshold, _Softness); half3 shade = lerp(albedo * _ShadeColor2.rgb, albedo * _ShadeColor.rgb, lerp(1.0, band2, _Shade2Strength)); return lerp(shade, albedo, band1); #endif } // 附加光:同样做一次硬边,得到一圈有清晰边界的彩色光斑 half3 ToonAdditional(half3 albedo, half3 N, Light light) { half atten = light.distanceAttenuation * light.shadowAttenuation; half x = saturate(dot(N, light.direction)) * saturate(atten * 4.0); return albedo * light.color * ToonBand(x, 0.3, _Softness + 0.02) * _AddLightStrength; } half4 frag(ToonVaryings i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); half3 N = normalize(i.normalWS); half3 V = GetWorldSpaceNormalizeViewDir(i.positionWS); half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, TRANSFORM_TEX(i.uv, _BaseMap)).rgb * _BaseColor.rgb; // ---------- 主光 + 阴影 ---------- // 接收端法线偏移:沿法线挪一点再查阴影图,消除明暗交界处的自阴影锯齿(shadow acne) float4 shadowCoord = TransformWorldToShadowCoord(i.positionWS + N * _ShadowNormalOffset); Light mainLight = GetMainLight(shadowCoord, i.positionWS, half4(1, 1, 1, 1)); half NoL = dot(N, mainLight.direction); half3 color = ToonDiffuse(albedo, NoL * 0.5 + 0.5, mainLight.shadowAttenuation) * mainLight.color; // 高光与边缘光只出现在“真正被照亮”的区域 half litMask = ToonBand((NoL * 0.5 + 0.5) * lerp(1.0, ToonBand(mainLight.shadowAttenuation, 0.5, _ShadowSoftness), _ReceiveShadow), _Threshold, _Softness); half3 H = normalize(mainLight.direction + V); half spec = ToonBand(saturate(dot(N, H)), _SpecThreshold, 0.005) * litMask; color += spec * _SpecStrength * _SpecColor.rgb * mainLight.color; half rim = FresnelRim(N, V, _RimWidth, 0.02) * litMask; color = lerp(color, _RimColor.rgb * albedo * mainLight.color * 1.6, rim * _RimStrength); // ---------- 附加光 ---------- #if defined(_ADDITIONAL_LIGHTS) InputData inputData = (InputData)0; // Forward+ 的光源循环宏需要它 inputData.positionWS = i.positionWS; inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(i.positionCS); #if USE_FORWARD_PLUS for (uint li = 0; li < min(URP_FP_DIRECTIONAL_LIGHTS_COUNT, MAX_VISIBLE_LIGHTS); li++) color += ToonAdditional(albedo, N, GetAdditionalLight(li, i.positionWS, half4(1, 1, 1, 1))); #endif uint lightCount = GetAdditionalLightsCount(); LIGHT_LOOP_BEGIN(lightCount) color += ToonAdditional(albedo, N, GetAdditionalLight(lightIndex, i.positionWS, half4(1, 1, 1, 1))); LIGHT_LOOP_END #endif // ---------- 环境光:只取球谐常数项,整块抬亮暗部 ---------- color += SampleSH(half3(0, 0, 0)) * albedo * _AmbientStrength; return half4(MixFog(color, i.fogFactor), 1); } ENDHLSL } Pass { Name "Outline" Tags { "LightMode"="SRPDefaultUnlit" } Cull Front ZWrite On HLSLPROGRAM #pragma vertex OutlineVert #pragma fragment OutlineFrag #pragma shader_feature_local_vertex _OUTLINENORMAL_MESH _OUTLINENORMAL_TANGENT _OUTLINENORMAL_VERTEXCOLOR #pragma multi_compile_fog #pragma multi_compile_instancing #include "Assets/Cookbook/Ch14/ToonOutlinePass.hlsl" ENDHLSL } Pass { Name "ShadowCaster" Tags { "LightMode"="ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 Cull Back HLSLPROGRAM #pragma vertex ShadowPassVertex #pragma fragment ShadowPassFragment #pragma multi_compile_instancing #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW // URP 自带的阴影 Pass 只在 _ALPHATEST_ON 时才读 _BaseMap,其余部分与材质无关,直接复用 #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl" ENDHLSL } Pass { Name "DepthOnly" Tags { "LightMode"="DepthOnly" } ZWrite On ColorMask R Cull Back HLSLPROGRAM #pragma vertex DepthOnlyVertex #pragma fragment DepthOnlyFragment #pragma multi_compile_instancing #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl" ENDHLSL } Pass { Name "DepthNormals" Tags { "LightMode"="DepthNormals" } ZWrite On Cull Back HLSLPROGRAM #pragma vertex DepthNormalsVertex #pragma fragment DepthNormalsFragment #pragma multi_compile_instancing #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthNormalsPass.hlsl" ENDHLSL } } }