Posterize: The alternative to Toon

My goal was to set out in creating a cel-shader that reacts to point lights, rather than using the included Toon shader which uses a premade cubemap and a directional light source to control the movement of the cel-shaded regions. I began by deconstructing the Normal-Diffuse.shader until I had revealed the dot product calculation for figuring out the Lambertian reflectance. After that it was simply a matter of dividing the range into segments based on a Levels value to control how many tones are produced.

Shader "Experimental/Posterize" {

   Properties {
      _Color ("Main Color", Color) = (1,1,1,1)
      _MainTex ("Base (RGB)", 2D) = "white" {}
	  _Levels ("Levels", Range (2, 10)) = 2
   }
   
   SubShader {
      Pass {
         Tags { "LightMode" = "Pixel" }
         
         CGPROGRAM
         #pragma vertex vert
         #pragma fragment frag
         #pragma multi_compile_builtin
         #pragma fragmentoption ARB_fog_exp2
         #pragma fragmentoption ARB_precision_hint_fastest
         #include "UnityCG.cginc"
         #include "AutoLight.cginc"
         
         float4 _Color;
		 float _Levels;

         struct v2f {
            V2F_POS_FOG;
			LIGHTING_COORDS
            float2   uv;
            float3   normal;
            float3   lightDir;
         };
		 
		 uniform float4 _MainTex_ST;
         
         v2f vert (appdata_base v) {
            v2f o;
            PositionFog( v.vertex, o.pos, o.fog );
            o.normal = v.normal;
            o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
            o.lightDir = ObjSpaceLightDir( v.vertex );
            TRANSFER_VERTEX_TO_FRAGMENT(o);
            return o;
         }

         uniform sampler2D _MainTex;

         float4 frag (v2f i) : COLOR {
            half4 texcol = tex2D(_MainTex, i.uv);
			#ifndef USING_DIRECTIONAL_LIGHT
			i.lightDir = normalize(i.lightDir);
			#endif
			half diffuse = dot(i.normal, i.lightDir);
			half4 c;
			
			half step = 1 / round(_Levels);
			half currentLevel = floor(diffuse / step);
			if(diffuse < step-1){
				c.rgb = texcol.rgb * _ModelLightColor0.rgb * (0 * LIGHT_ATTENUATION(i) * 2);
			} else {
				c.rgb = texcol.rgb * _ModelLightColor0.rgb * (currentLevel * step);
			}
			
			c.a = 0;
			return c;
         }
         
         ENDCG
         
      }
   }
}

However I’m running into one issue now that I am not sure how to get around. I’d like to affect the shader with multiple point lights with potentially different colors, so cool overlapping effects may occur. However when I use two lights, it seems like whichever one is closest is what affects the shader. This is most likely because of the _ModelLightColor0.rgb variable. Ideally I would like to iterate over any point lights within range and apply my algorithm within that loop. Can anyone suggest a method for doing this?

Unity’s renderer uses a base pass for ambient lighting, and additive passes for dynamic lights. Take a look at the first and third passes of the diffuse shader to see how it’s done there.

Yeah, what I’ve done so far is based on the 3rd pass of the first sub shader. I completely skipped the first and second passes. I have no interest in ambient light at this point, but if the vertex lights in the second pass allow me to access multiple point lights at a time, then that might be something to look into. I’m just very new to shaders and I’m not sure if that’s how it works.

At the very least, your base pass should fill in a black silhouette. Subsequent passes should be additive, so that they don’t overwrite one another.

The default blend mode is Off, which overwrites whatever the last pass put there. You can either:

  1. Use a Category with Blend AppSrcAdd AppDstAdd
  2. Explicitly use Blend Off for your base pass, and Blend SrcAlpha One for your lighting pass.

Ok, that sounds good… but I still don’t see how I can separately address different point light’s directions and colors. That’s kind of the key issue I’m trying to grapple with. If I process each light with a different pass, then how do I know how many passes I need?

Your Pixel pass will be run once for each pixel light that affects your object. You can’t get information about more than one pixel light per pass.

This makes much more sense, and for some reason didn’t realize this after reading all of the nvidia Cg docs and Unity shaderlab docs. :? We’ll see what I come up with today.