// Minimal cel/toon shader with an inverted-hull outline for the Pose Icon stage // (icon screenshots of the staged Paintman). Deliberately NOT a URP/Lit clone: // the stage is a controlled studio setup, so main-light cel banding + ambient is // all the lighting it needs. Outline runs as a second pass (SRPDefaultUnlit, so // URP renders it without a Render Feature); width is screen-space (pixel-ish) // and distance-independent for consistent icon thickness. Shader "Custom/PoseIconToon" { Properties { [MainTexture] _BaseMap("Albedo", 2D) = "white" {} [MainColor] _BaseColor("Color", Color) = (1,1,1,1) [Header(Toon Shading)] _ShadeThreshold("Shade Threshold", Range(0.0, 1.0)) = 0.5 _ShadeSoftness("Shade Softness", Range(0.001, 0.5)) = 0.05 _ShadowTint("Shadow Tint", Color) = (0.55, 0.5, 0.65, 1) [Header(Outline)] _OutlineColor("Outline Color", Color) = (0.1, 0.08, 0.2, 1) _OutlineWidth("Outline Width (px at 1080p)", Range(0.0, 20.0)) = 6.0 } SubShader { Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "Queue" = "Geometry" } HLSLINCLUDE #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; half _ShadeThreshold; half _ShadeSoftness; half4 _ShadowTint; half4 _OutlineColor; half _OutlineWidth; CBUFFER_END ENDHLSL Pass { Name "ToonForward" // UniversalForwardOnly (not UniversalForward): drawn by BOTH the Forward and the // Deferred renderer (the deferred path only picks up ForwardOnly/SRPDefaultUnlit // passes for materials without a GBuffer pass - plain UniversalForward is skipped). Tags { "LightMode" = "UniversalForwardOnly" } HLSLPROGRAM #pragma vertex ToonVertex #pragma fragment ToonFragment #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 normalWS : TEXCOORD1; }; Varyings ToonVertex(Attributes input) { Varyings output; output.positionCS = TransformObjectToHClip(input.positionOS.xyz); output.normalWS = TransformObjectToWorldNormal(input.normalOS); output.uv = TRANSFORM_TEX(input.uv, _BaseMap); return output; } half4 ToonFragment(Varyings input) : SV_Target { half4 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor; Light mainLight = GetMainLight(); half ndl = dot(normalize(input.normalWS), mainLight.direction) * 0.5h + 0.5h; half lit = smoothstep(_ShadeThreshold - _ShadeSoftness, _ShadeThreshold + _ShadeSoftness, ndl); half3 litColor = albedo.rgb * mainLight.color; half3 shadeColor = albedo.rgb * _ShadowTint.rgb; half3 ambient = albedo.rgb * SampleSH(input.normalWS) * 0.25h; return half4(lerp(shadeColor, litColor, lit) + ambient, albedo.a); } ENDHLSL } Pass { Name "Outline" Tags { "LightMode" = "SRPDefaultUnlit" } Cull Front HLSLPROGRAM #pragma vertex OutlineVertex #pragma fragment OutlineFragment struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; }; struct Varyings { float4 positionCS : SV_POSITION; }; Varyings OutlineVertex(Attributes input) { Varyings output; output.positionCS = TransformObjectToHClip(input.positionOS.xyz); // Extrude in clip space: constant on-screen width regardless of distance. float3 normalWS = TransformObjectToWorldNormal(input.normalOS); float3 normalCS = normalize(mul((float3x3)UNITY_MATRIX_VP, normalWS).xyz); float2 offset = normalCS.xy * _OutlineWidth * 2.0 / 1080.0 * output.positionCS.w; offset.x *= _ScreenParams.y / _ScreenParams.x; // keep width uniform on non-square aspect output.positionCS.xy += offset; return output; } half4 OutlineFragment(Varyings input) : SV_Target { return _OutlineColor; } ENDHLSL } } }