Toon Shading Question

Hey there, Artist here with no programming experience. I really like the look of the basic Unity Toon Shader, but I’ve got shadows in the environment that it doesn’t take into account. Is there any way to do this with this Shader in particular? I’ve experimented with other Toon Shaders that have custom ramps but they just don’t look quite as good as the one that uses the Cubemap.

Any help would be much appreciated!

Thanks

If you create an example scene with two shaders (good one which has problem with shadows) and bad one in one scene, I’ll try to look into it and understand if it’s possible to fix shadow issue

Hey! Thanks for getting back to me. I can set that up, but it’s not so much an error in the Unity Basic Toon shader it’s just that it doesn’t receive shadows at all, I was hoping there was an easy way to turn that on. I’d like it to use the cubemap it has by default but also receive shadows.

It would be easier to hijack an existing shader and modifying it to have the correct behavior. Generally shader do their stuff and send the result to a final gather, with one line of code you can pick that final gather and transform it into ramped toon lighting.

That’s all I can say now, sorry. I hope someone else pick from there !

Haha, thanks for the post neoshaman. I hear what you’re saying, but I’m I can’t code. This is the code for Unity’s Basic Toon Shader that I like. Any idea what I would add to this so it’ll receive shadows? Is this even possible?

Shader "Toon/Basic" {
    Properties {
        _Color ("Main Color", Color) = (.5,.5,.5,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _ToonShade ("ToonShader Cubemap(RGB)", CUBE) = "" { }
    }


    SubShader {
        Tags { "RenderType"="Opaque" }
        Pass {
            Name "BASE"
            Cull Off
         
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            samplerCUBE _ToonShade;
            float4 _MainTex_ST;
            float4 _Color;

            struct appdata {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };
         
            struct v2f {
                float4 pos : SV_POSITION;
                float2 texcoord : TEXCOORD0;
                float3 cubenormal : TEXCOORD1;
                UNITY_FOG_COORDS(2)
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.cubenormal = mul (UNITY_MATRIX_MV, float4(v.normal,0));
                UNITY_TRANSFER_FOG(o,o.pos);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = _Color * tex2D(_MainTex, i.texcoord);
                fixed4 cube = texCUBE(_ToonShade, i.cubenormal);
                fixed4 c = fixed4(2.0f * cube.rgb * col.rgb, col.a);
                UNITY_APPLY_FOG(i.fogCoord, c);
                return c;
            }
            ENDCG         
        }
    }

    Fallback "VertexLit"
}

I have never dabble with shadow directly that’s the problem, I do have modified shader who receive shadow, so the idea was to start with a shader that receive shadow and modify it, now this one don’t seem to have it so I a first glance I can’t help.

There is many varient of teh toon shader

Use code tag to display code please :hushed:

What I can tell you is that the vert part is doing a bunch of conversion of reference I always forget which one but it’s only a pick away from the doc :smile: The vert is basically doing some operation at EACH vertex.

Then the GPU automatically interpolate those data across a triangle and for EACH pixel the frag part process its code.

  • The first line of the frag is multiplying the color field with the texture.
  • the second is using the normal at the pixel to sample the cubemap
  • the third multiply the sampled cubemap (from the second line) by the result of the first line and multiply that by 2 again, and put it in the final gather (c)
  • the fourth line apply fog to the final gather
  • the fifth simply return the result the final gather for displaying on the screen

Observation.:

  • it has no concept of light, light direction or presence will do nothing, normally it shouldn’t respond to ambient light either.
  • the light is entirely dependent on the cubemap and its orientation.
  • it doesnt react to normal map, there is no hook for it.
  • It’s only toon shader if the cubemap is using a step drawing, it can be any image or lighting drawn on the cubemap.

You should start with a shader that already have lighting and mix it with this one.

Apparently it’s easier to work with a standard surface shader. So now I’m looking to add a cubemap input to this shader since it already has shadows.

Shader "Sky/Toon/Shadows" {
    Properties{
        _Color("Main Color", Color) = (0.5,0.5,0.5,1)
        _MainTex("Base (RGB)", 2D) = "white" {}
    _Ramp("Toon Ramp (RGB)", 2D) = "grey" {}
    }

        SubShader{
        Tags{ "RenderType" = "Opaque" }
        LOD 200
        Lighting On
        Cull off
        CGPROGRAM
#pragma surface surf ToonRamp

        sampler2D _Ramp;
    // custom lighting function that uses a texture ramp based
    // on angle between light direction and normal
#pragma lighting ToonRamp exclude_path:prepass
    inline half4 LightingToonRamp(SurfaceOutput s, half3 lightDir, half atten)
    {
#ifndef USING_DIRECTIONAL_LIGHT
        lightDir = normalize(lightDir);
#endif

        half d = (dot(s.Normal, lightDir)*0.5 + 0.5) * atten;
        half3 ramp = tex2D(_Ramp, float2(d,d)).rgb;

        half4 c;
        c.rgb = s.Albedo * _LightColor0.rgb * ramp * (atten * 2);
        c.a = 0;
        return c;
    }


    sampler2D _MainTex;
    float4 _Color;

    struct Input {
        float2 uv_MainTex : TEXCOORD0;
        float3 cubenormal : TEXCOORD1;
    };

    void surf(Input IN, inout SurfaceOutput o) {
        half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
        o.Albedo = c.rgb;
        o.Alpha = c.a;
    }
    ENDCG

    }

        Fallback "Diffuse"
}

Thanks Neoshaman, a friend mentioned that too. I’ll look around!

Now Looking at that code you can see it has a custom lighting part, but there isn’t a vert part. I don’t remember how unity handle lack of vert part (I’m sure it’s automated when not mentioned) So I need to check that, though just adding it shouldn’t be a problem. It should be trivial to add normal map too.

This shader work differently, it takes ONE light and use a texture to replace the light gradient by your own. So it beg the question what kind of rendering you really want! If I have full specification I can help you more.

Now you must understand how light work, it takes the direction of the light relative to the point and then the normal of the surface, it does an operation (dot product) and return the value of light. Now this isn’t a full shader, it’s a surface shader, it mean unity already do a lot of stuff behind the curtain.

Now I see the code I remember where the shadow are :smile: they are automatically put within the atten variable (which is the light attenuation). Attenuation store 2 things in a single value, it has the occlusion from light (shadow) and the distance attenuation (ie light is less bright the further away from the light point). Of course this shader only use directional light so it only has shadow. Given that it is a gradient, you might need to process this to have toon shadow on top.

I’m really quite out of my element with this stuff, I was hoping for an easy fix cause it’s all over my head.

Is it possible to make a toon shader with a cube map input and receive shadows?

You can buy one from the asset store like Toony Colors Pro, or you can try to add shadows on your own as you’ve already started attempting to. This page has examples on how to add shadow receiving and casting to a vertex / fragment shader:

Also read this, yes all 5 parts:

Yes

But there is many issue, it’s never a quickfix lol

Both shader use different coding paragdim so I can’t easily port one to the other, without checking stuff in doc.

The second one use surface shader (ie write to a surface structure) while the second is mostly a typical shader. The main problem is to document what’s the equivalence to make modification. The main thing is to convert the first one to a surface shader, it will make everything simpler, however I don’t know about the use of vert data necessary for environment within surface method :hushed:

I haven’t that down by heart :sweat_smile:

wrong answer

Okay I’ll try something:

  1. I think I was wrong it’s about the shader only receiving one light
  2. You can have a quick version by modifier the first shader
    a. add the to the pragma list
#pragma lighting ToonRamp exclude_path:prepass

then add

inline half4 LightingToonRamp(SurfaceOutput s, half atten)
    {
#ifndef USING_DIRECTIONAL_LIGHT
        lightDir = normalize(lightDir);
#endif
        half4 c;
        c.rgb = s.Albedo * _LightColor0.rgb  * (atten * 2);
        c.a = 0;
        return c;
    }

Digging in the doc using @bgolus convenient link I found this

which has an example of cubemap writing in surface shader, this should do to adapt the code :smile:

Shader "Example/WorldRefl" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _Cube ("Cubemap", CUBE) = "" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input {
          float2 uv_MainTex;
          float3 worldRefl;
      };
      sampler2D _MainTex;
      samplerCUBE _Cube;
      void surf (Input IN, inout SurfaceOutput o) {
          o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb * 0.5;
          o.Emission = texCUBE (_Cube, IN.worldRefl).rgb;
      }
      ENDCG
    }
    Fallback "Diffuse"
  }

Which give us

Shader "Example/Custom/Toon" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _Cube ("Cubemap", CUBE) = "" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM

      #pragma surface surf Lambert
      #pragma lighting ToonRamp exclude_path:prepass
      struct Input {
          float2 uv_MainTex;
          float3 worldRefl;
      };
      sampler2D _MainTex;
      samplerCUBE _Cube;
      void surf (Input IN, inout SurfaceOutput o) {
          o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb * texCUBE (_Cube, IN.worldRefl).rgb * 2;
      }

inline half4 LightingToonRamp(SurfaceOutput s, half atten)
    {
#ifndef USING_DIRECTIONAL_LIGHT
        lightDir = normalize(lightDir);
#endif
        half4 c;
        c.rgb = s.Albedo * _LightColor0.rgb  * (atten * 2);
        c.a = 0;
        return c;
    }


      ENDCG
    }
    Fallback "Diffuse"
  }

Try this and tell me any error

Hey, thanks for the help. I’ve tried it out, and it’s saying "undeclared identifier ‘lightDir’

Thanks for that, it’s great. I’ve tried looking into some tutorials but they don’t ease into it enough. I have plenty of 3D content creation experience but zero coding, so this is great. Thanks again.

Think I’m going to go with this one!

Oh I made a mistake I haven’t corrected

I remove dir the first time but then kept it for having the light color You can change this part to

inline half4 LightingToonRamp(SurfaceOutput s, half atten)
    {
        half4 c;
        c.rgb = s.Albedo * _LightColor0.rgb  * (atten * 2);
        c.a = 0;
        return c;
    }

Tell me if there is any error left