Developed using Unity and HLSL

Textured Cel Shader

A customizable cel shader with adjustable textures and color transitions.


This shader could be used for heavily stylized games, and could be expanded to make an illusion of painted on brush-strokes on 3D models.

Game a Week award project gameplay

Toon Shader.shader

Shader "Custom/ToonRegular"
{
    Properties
    {
        // base textures
        _BaseMap ("base texture", 2D) = "white" {}
        _PatternTexture ("edge pattern texture", 2D) = "gray" {}


        // toon shadow control
        _ShadowThreshold ("shadow threshold", Range(0,1)) = 0.5
        _ShadowSmoothness ("shadow smoothness", Range(0.001,0.5)) = 0.1

        //edge pattern stuff
        _PatternScale ("pattern scale", Float) = 5.0
        _PatternStrength ("pattern strength", Range(0,1)) = 0.3
        _PatternContrast ("pattern contrast", Range(0.1,5)) = 1.5


        
        //outline band shaping
        _EdgeWidth ("edge width", Range(0.01,0.5)) = 0.15
        _EdgeSharpness ("edge sharpness", Range(0.5,10)) = 3.0


        // scrolling pattern offset
        _ScrollSpeed ("pattern scroll speed", Vector) = (0,0,0,0)
    }


    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
            "RenderPipeline" = "UniversalPipeline"
        }


        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }


            HLSLPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #pragma multi_compile _ _ADDITIONAL_LIGHTS
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS


            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"


            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);

            TEXTURE2D(_PatternTexture);
            SAMPLER(sampler_PatternTexture);

            float4 _BaseMap_ST;


            float _ShadowThreshold;
            float _ShadowSmoothness;


            float _PatternScale;
            float _PatternStrength;
            float _PatternContrast;


            float _EdgeWidth;
            float _EdgeSharpness;


            float2 _ScrollSpeed;


            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float2 uv : TEXCOORD0;
            };


            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float3 normalWS : TEXCOORD0;
                float3 positionWS : TEXCOORD1;
                float2 uv : TEXCOORD2;
            };


            Varyings vert (Attributes input)
            {
                Varyings output;

                // object to world
                output.positionWS = TransformObjectToWorld(input.positionOS.xyz);

                // world to clip
                output.positionHCS = TransformWorldToHClip(output.positionWS);



                //normal in world space for lighting
                output.normalWS = TransformObjectToWorldNormal(input.normalOS);

                // uv mapping
                output.uv = TRANSFORM_TEX(input.uv, _BaseMap);

                return output;
            }


            // shared lighting function for main + additional lights
            float CalculateToonLighting(
                float3 normalWS,
                float3 lightDirection,
                float attenuation,
                float2 uv,
                float time
            )
            {
                float ndotl = dot(normalWS, lightDirection);


                // basic toon shadow ramp
                float shadowRamp = smoothstep(
                    _ShadowThreshold - _ShadowSmoothness,
                    _ShadowThreshold + _ShadowSmoothness,
                    ndotl
                );


                // band where pattern shows up near shadow edge
                float edgeBand = smoothstep(
                    _ShadowThreshold - _EdgeWidth,
                    _ShadowThreshold + _EdgeWidth,
                    ndotl
                );

                edgeBand = pow(edgeBand * (1.0 - edgeBand) * 4.0, _EdgeSharpness);


                // animated pattern lookup
                float2 patternUv = uv * _PatternScale + (_ScrollSpeed * time);

                float patternValue = SAMPLE_TEXTURE2D(
                    _PatternTexture,
                    sampler_PatternTexture,
                    patternUv
                ).r;

                patternValue = pow(patternValue, _PatternContrast);


                // pattern slightly shifts shadow threshold in edge zones
                float adjustedThreshold =
                    _ShadowThreshold +
                    (patternValue - 0.5) * _PatternStrength * edgeBand;


                float finalLighting = smoothstep(
                    adjustedThreshold - _ShadowSmoothness,
                    adjustedThreshold + _ShadowSmoothness,
                    ndotl
                );

                return finalLighting * attenuation;
            }


            float4 frag (Varyings input) : SV_Target
            {
                float3 normalWS = normalize(input.normalWS);

                float4 baseColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);

                float3 totalLighting = 0;

                float time = _Time.y;



                // main light
                Light mainLight = GetMainLight();

                totalLighting += CalculateToonLighting(
                    normalWS,
                    mainLight.direction,
                    mainLight.distanceAttenuation,
                    input.uv,
                    time
                ) * mainLight.color;



                // extra lights
                #ifdef _ADDITIONAL_LIGHTS

                int additionalLightCount = GetAdditionalLightsCount();

                for (int i = 0; i < additionalLightCount; i++)
                {
                    Light additionalLight = GetAdditionalLight(i, input.positionWS);

                    totalLighting += CalculateToonLighting(
                        normalWS,
                        additionalLight.direction,
                        additionalLight.distanceAttenuation,
                        input.uv,
                        time
                    ) * additionalLight.color;
                }

                #endif



                float3 finalColor = baseColor.rgb * totalLighting;

                return float4(finalColor, 1.0);
            }

            ENDHLSL
        }
    }
}