Cel/Toon shading with multiple light sources on mobile

For the past couple of days I’ve been trying to implement a mechanism that would enable me to have a several light sources in my toon shaded scene. The game on which I’m currently working is designated for mobile phones, therefore it is important for me to keep it as simple as possible. Having said that, at this moment I don’t know if it’s even possible to have a cel shaded game with several light sources running on a mobile… just a thought…

Anyway, in the first approach I’ve successfully implemented a toon shader that utilized one directional light. The simplified body of that shader can be seen at the bottom of my previous [question][1]. Of course because one directional light wasn’t enough for me I’ve struggle a bit to have my shader based on multiple light sources and after several trials I’ve end up with the code attached below. Now, the shader do a great job on my desktop machine, all the objects are properly lit up, but unfortunately on mobile what I get is probably simple Lambert (?) shading. Anyone help really appreciated.

Shader "Custom/Outlined Diffuse" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" { }
		_Ramp ("Shading Ramp", 2D) = "gray" {}

	SubShader {
		Pass {
			Name "OUTLINE"
			Tags { "LightMode" = "Always" }
			Cull Front
			ZWrite Off
			ZTest Always
			//Offset 15,15
			#include "UnityCG.cginc"

			struct appdata {
				half4 vertex : POSITION;
				half3 normal : NORMAL;

			struct v2f {
				half4 pos : POSITION;

			v2f vert(appdata v) {
				// just make a copy of incoming vertex data but scaled according to normal direction
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

				half3 norm  = mul ((half3x3)UNITY_MATRIX_IT_MV, v.normal);
				half2 offset = TransformViewToProjection(norm.xy);
				o.pos.xy += offset * o.pos.z * 0.002;   
				return o;

			float4 frag(v2f i) : COLOR {
				return float4(0.0, 0.0, 0.0, 1.0);


			#pragma vertex vert
			#pragma fragment frag			
		} // Name "OUTLINE"

// Cel shading based on one directional light with the use of a surface shader
//		Tags {"LightMode" = "ForwardBase" }
//		#pragma surface surf Ramp noambient noforwardadd approxview
//		sampler2D_half _MainTex;
//	  	sampler2D_half _Ramp;
//		half4 LightingRamp (SurfaceOutput s, half3 lightDir, half atten) {
//          half NdotL = dot (s.Normal, lightDir);
//          half diff = NdotL * 0.5 + 0.5;
//          half3 ramp = tex2D (_Ramp, half2(diff)).rgb;
//          half4 c;          
//          c.rgb = s.Albedo * _LightColor0.rgb * ramp * (atten * 2);
//          c.a = s.Alpha;
//          return c;
//      	}
//		struct Input {
//			half2 uv_MainTex;
//		};
//		void surf (Input IN, inout SurfaceOutput o) {
//			fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
//			o.Albedo = c.rgb;
//			o.Alpha = c.a;
//		}

		Pass {
			Tags { "LightMode" = "ForwardBase" }
			Cull Back

			#pragma vertex vert_pass_2
			#pragma fragment frag_pass_2						
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			sampler2D_half _MainTex;
			sampler2D_half _Ramp;

         	struct vertexInput {
            	half4 vertex : POSITION;
            	half2 texcoord : TEXCOORD0;
            	half3 normal : NORMAL;
         	struct vertexOutput {
            	half4 pos : SV_POSITION;
            	half2 uv : TEXCOORD0;
            	half4 light : TEXCOORD1;
         vertexOutput vert_pass_2(vertexInput v) 
            vertexOutput output;
 			output.pos = mul(UNITY_MATRIX_MVP, v.vertex);
 			output.uv = v.texcoord; 			

// 1st approach

//			half3 worldPos = mul(_Object2World, v.vertex).xyz;
//			half3 worldN = mul((half3x3)_Object2World, SCALED_NORMAL);

//            half3 vertexL = Shade4PointLights(
//            	unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
//				unity_LightColor0, unity_LightColor1, unity_LightColor2, unity_LightColor3,
//				unity_4LightAtten0, worldPos, worldN);

// 2nd approach 
            half3 worldPos = mul(_Object2World, v.vertex).xyz;
            half3 worldN = normalize(mul(half4(v.normal, 0.0), _World2Object).xyz);
			half3 vertexL = half3(0.0, 0.0, 0.0);
			for (int i = 0; i < 4; ++i)
				half3 lightPos = half3(unity_4LightPosX0<em>, unity_4LightPosY0<em>, unity_4LightPosZ0*);*</em></em>

* half3 vertexToLightSource = lightPos - worldPos;*
half3 lightDirection = normalize(vertexToLightSource);
half squaredDistance = dot(vertexToLightSource, vertexToLightSource);
half attenuation = 1.0 / (1.0 + unity_4LightAtten0 * squaredDistance);
half3 diffuseReflection = attenuation * half3(unity_LightColor_)
max(0.0, dot(worldN, lightDirection));*_

vertexL += diffuseReflection;
half3 dir = normalize(half3(_WorldSpaceLightPos0));
half3 directionL = _LightColor0.rgb * dot(worldN, dir);

half3 l = vertexL + directionL;
* half intensity = (l.r + l.g + l.b) / 3.0;*
* output.light = half4(l, intensity);*

* return output;*
* }*

half4 frag_pass_2(vertexOutput input) : COLOR
half4 ambientL = half4(half3(UNITY_LIGHTMODEL_AMBIENT), 1.0) * 2;
half4 c = tex2D(MainTex, input.uv) * (ambientL + input.light * tex2D(Ramp, half2(input.light.w, input.light.w)));
* return c;*


* } *
* } *
_*[1]: http://answers.unity3d.com/questions/654757/what-cartoon-shading-on-mobile.html*_

The solution to this one is to define an additional pass which is tagged with “LightMode” = “ForwardAdd”, and then do Blend One One to enable additive blending in this pass. Unity’s lighting system will pick up that pass for the additional light sources and then add the contributions iteratively for each light.

Please see this post on anisotropic specular highlights. It has a complete shader which demonstrates how to plug into Unity’s lighting system like that from a low-level vert/frag pair. Ignore all the math about the anisotropic BRDF and the implementation of that - it’s an awesome shader, but I didn’t link you to it for the sake of that information. :wink: I just wanted to show you its example of different passes for ForwardBase + ForwardAdd.

Ok, I finally got it worked out. The problem was with the Ramping texture from which the color/light intensity was taken. The texture was handmade by me. Its size was quite small 4x4 (four intensity thresholds laid on squared png bitmap). What I think happened is that on a mobile device the texture’s size was simply too small and some sort of approximation was implicitly made. Probably the whole texture was extended making its size larger (If someone can explain what really happened, please do so). In the result the Ramping intensities got washed up making ideal gradient which applied to the game objects resulted in soft shades.