atten = highest of all lights, light color = weighted average

Is it possible to make a surface shader like that?
My objective is to create a ‘toon shader’ that has 3 light levels, fully lighted, medium and dark.
Each light level has it’s own color, defined in 3 different albedo textures
Multiple lights should be able to affect the shader, the atten of the pixel should be the highest (default implementation seems to add all lights)
Light color should be weighted average of all light colors based on their respective attens.

Is it possible at all? reading the generated frag_surf code it seems to run once per light source, making it impossible?


So far I’ve almost achieved what I want, the issue being that I can’t seem to change the default shadow color (ambient light) to the per pixel target shadow color.
This is the shader I have so far
Code So Far

Shader "Custom/Surface Toon Shader"
{

    Properties
    {
        _MainTex("Albedo (RGB)", 2D) = "white" {}
        _BumpMap ("Normals ", 2D) = "bump" {}
        _HeightMap("Height Map", 2D) = "white" {}

        _Color("Color", Color) = (1, 1, 1, 1)
        _SColor1("ShadowColor 1", Color) = (1, 1, 1, 1)
        _SColor2("ShadowColor 2", Color) = (1, 1, 1, 1)
        _SHADOW_SATURATOR1("Saturate Shadows 1", Range(1, 4)) = 1.3
        _SHADOW_SATURATOR2("Saturate Shadows 2", Range(1, 4)) = 1.6
        _SHADOW_NERF("Nerf/Buff Shadows", Range(-1, 1)) = 0.40
        _BaseColor_Step("Shadow Step", Range(0, 1)) = 0.66
        _BaseShade_Feather("Shadow Feather", Range(0, 1)) = 0
        _ShadeColor_Step("Shadow 2 Step", Range(0, 2)) = 1.03
        _1st2nd_Shades_Feather("Shadow 2 Feather", Range(0, 1)) = 0
        _HeightPower("Height Power", Range(0,.125)) = 0.0226
    }


    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
        }
        LOD 200
        ZWrite on

        CGPROGRAM
        #include "AutoLight.cginc"
        #pragma surface surf CelShadingForward addshadow fullforwardshadows
        #pragma target 3.0
        #include "Lighting.cginc"

        fixed _numofshadows;
        fixed _cheat;
        fixed4 _Color;
        fixed4 _SColor1;
        fixed4 _SColor2;
        fixed _SHADOW_SATURATOR1;
        fixed _SHADOW_SATURATOR2;

        fixed _SHADOW_NERF;
        fixed _BaseColor_Step;
        fixed _BaseShade_Feather;
        fixed _ShadeColor_Step;
        fixed _1st2nd_Shades_Feather;

        sampler2D _MainTex;
        sampler2D _BumpMap;
        sampler2D _HeightMap;
        float _HeightPower;

        float3 shift_col(float3 RGB, float3 shift)
        {
            float3 RESULT = float3(RGB);
            float VSU = shift.z * shift.y * cos(shift.x * 3.14159265 / 180);
            float VSW = shift.z * shift.y * sin(shift.x * 3.14159265 / 180);

            RESULT.x = (.299 * shift.z + .701 * VSU + .168 * VSW) * RGB.x
                + (.587 * shift.z - .587 * VSU + .330 * VSW) * RGB.y
                + (.114 * shift.z - .114 * VSU - .497 * VSW) * RGB.z;

            RESULT.y = (.299 * shift.z - .299 * VSU - .328 * VSW) * RGB.x
                + (.587 * shift.z + .413 * VSU + .035 * VSW) * RGB.y
                + (.114 * shift.z - .114 * VSU + .292 * VSW) * RGB.z;

            RESULT.z = (.299 * shift.z - .3 * VSU + 1.25 * VSW) * RGB.x
                + (.587 * shift.z - .588 * VSU - 1.05 * VSW) * RGB.y
                + (.114 * shift.z + .886 * VSU - .203 * VSW) * RGB.z;

            return (RESULT);
        }

        struct SurfaceOutput2
        {
            fixed3 Albedo;
            fixed3 Normal;
            fixed3 Emission;
            half Specular;
            fixed Gloss;
            fixed Alpha;
        };

        inline fixed4 LightingCelShadingForward(SurfaceOutput2 s, half3 lightDir, half atten) // * (atten)
        {
            const float Set_LightColor = length(saturate(_LightColor0.rgb) * atten);
            const float3 Set_BaseColor = s.Albedo * _Color;

            float4 _1st_ShadeMap_var = float4(s.Albedo * _SColor1, 1);//This version of the shader auto calculates the shadow 1 and shadow 2 colors instead of taking it from a texture
            _1st_ShadeMap_var.rgb = shift_col(_1st_ShadeMap_var.rgb, float3(1, _SHADOW_SATURATOR1, 1));
            float4 _2nd_ShadeMap_var = float4(s.Albedo * _SColor2, 1);
            _2nd_ShadeMap_var.rgb = shift_col(_2nd_ShadeMap_var.rgb, float3(1, _SHADOW_SATURATOR2, 1));

            const float NoTL = dot(s.Normal, lightDir);
            const float ShadowNerfed = (1 - _SHADOW_NERF) * NoTL + _SHADOW_NERF;
            const float _HalfLambert_var = 1 - min(ShadowNerfed, ShadowNerfed * Set_LightColor);

            const float Set_FinalShadowMask = saturate(
                1.0 + (_HalfLambert_var - (_BaseColor_Step - _BaseShade_Feather)) / (_BaseColor_Step - (_BaseColor_Step
                    - _BaseShade_Feather)));

            float3 Set_FinalBaseColor = lerp(
                Set_BaseColor,
                lerp(
                    _1st_ShadeMap_var,
                    _2nd_ShadeMap_var,
                    saturate(
                        (1.0 + ((_HalfLambert_var - (_ShadeColor_Step - _1st2nd_Shades_Feather))) / (_ShadeColor_Step -
                            (_ShadeColor_Step - _1st2nd_Shades_Feather))))),
                Set_FinalShadowMask);//This logic was taken form the Unity Toon Shader package

            return lerp(half4(Set_FinalBaseColor, 1), half4(0, 0, 0, 1), 1-saturate(atten*100));
        }

        struct Input
        {
            float2 uv_MainTex;
            float2 uv_BumpMap;
            float3 worldNormal;
            float2 uv_HeightMap;
            float3 viewDir;
            INTERNAL_DATA
        };

        inline void surf(Input IN, inout SurfaceOutput2 o)
        {
            const float2 texOffset = ParallaxOffset(tex2D(_HeightMap, IN.uv_HeightMap).r, _HeightPower, IN.viewDir);
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex + texOffset);
            o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap + texOffset));
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

Yes, if you want to do that in builtin, there’s no way to do it with just the shader. If you are willing to switch to URP (eg if your project is just getting started), URP has all lights passed into the shader (at least for forward and forward+). That’s your simplest option.

oof. No, the project is 5 years old, can’t simply switch to URP. I guess it’s another thing to the long list of reasons to switch

Another option if you only have one shadowed light is to pass all lights in a compute buffer, skip drawing for all the passes other than the shadow light, and then do the complete lighting evaluation in the fragment shader for that light.

Or else, you could do all the drawing in a post-pass. Accumulate light results for opaque geometry to a separate UAV and then draw it again. So it’s kind of the reverse of deferred. I don’t know if that would work out well on all platforms, but it would be a way to hack it into the current rendering system.

Really, though, take a look at what other cel shading shaders do to fake the effect. Trying to get it perfect on the BIRP is going to require sacrificing a lot of performance.

EDIT: Also, I don’t know what effect you’re going for, but it might not look as good as normal cel shading. I’d at least prototype it in URP or something with multiple shadow-casting lights and see if it looks like you want it to.

I guess I will just give up for now and try again when I manage to convert to URP. I’ve even tried to stop using albedo and write all color to emission, while that worked for one light, the shader stopped recognizing other lights.

I didn’t mean to discourage you, just that it’s not a trivial task. Cel shading is all about faking certain effects, so you may want to look at what other cel shading techniques do and take those things as inspiration. Eg…

https://roystan.net/articles/toon-shader/

1 Like