Toon ramp sampling at too low resolution?

Hey all,
I wrote a surface shader to get a toon ramp effect, but realised that it samples the ramp texture for all applicable lights seperately. This means that instead of having one set of toon cels, or “bands”, the toon bands on objects overlap. I figured I would have to write a vertex/pixel shader to handle this so that the ramp lookup could be done after the diffuse value for all available lights was calculated, making the object only have one set of “bands” dependant on the ramp lookup texture.


The circled areas should only have three seperate areas of shading

I decided to first write a vertex/fragment shader that just handled the input from one directional light first. It seems to be working, and it samples the lookup texture, but the bands of light are extremely low resolution.
1440776--77342--$LowResBands.JPG
The bands of light are extremely low res here

The code for the vertex shader is as follows:

Shader "Character/Banded Light Ramp Tex Vertex" {
	Properties {
		_MainTex ("Texture", 2D) = "white" {}
		_RampTex ("Ramp", 2D) = "white"{}
		_Color ("Color", Color) = (1,1,1,1)
		_LightCutoff("Maximum distance", Float) = 5.0
	}
	SubShader {
		Pass{
		Tags {"LightMode" = "ForwardBase"}
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			#pragma target 3.0
			#pragma glsl
			
			#include "UnityCG.cginc"
			
			uniform sampler2D _MainTex;
			uniform float4 _MainTex_ST;
			uniform sampler2D _RampTex;
			uniform float4 _Color;
			uniform float _LightCutoff;
			uniform float4 _LightColor0;
			
			
			struct vertexInput {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};
			
			struct fragmentInput{
				float4 pos : SV_POSITION;
				float4 color : COLOR0;
				half2 uv : TEXCOORD0;
			};
			
//			struct vertexOutput {
//				float4 pos : SV_POSITION;
//			};
			
			fragmentInput vert(vertexInput i){
				fragmentInput o;
				o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
				o.uv = TRANSFORM_TEX(i.texcoord, _MainTex);
				
				float3 normalDirection = normalize(mul(float4(i.normal,1.0), _World2Object).xyz);
				
				float3 lightDirection = normalize( _WorldSpaceLightPos0.xyz);
				
				float NdotL = max(0.0, dot(normalDirection, lightDirection));
				
				float2 lookUpPos = (NdotL,NdotL);
				
				float rampDiffuse = tex2D(_RampTex, lookUpPos);
				
				float3 theColor = _LightColor0.xyz * _Color.rgb * rampDiffuse * 2 ;
				
				o.color = float4(theColor, 1.0);
				
				return o;
			}
			
			float4 frag(fragmentInput i) : COLOR
			{
				return tex2D(_MainTex, i.uv) * i.color;
			}
			
			ENDCG
			Lighting On
		}
	
	}
}

I’m very new to shaders, and if there’s a way to sample the ramp texture after the total light value has been calculated in a surface shader that would also be super helpful. Mainly, I would like to know why the sampling is resulting in such low resolution light bands. Thanks for any help.

Currently the ramp UV is being interpolated per-vertex.

You’d need to move the dot product to the fragment shader and use normalized vectors in it, then put that in as the uvs for the ramp texture.

I thought that might be the case. How would I go about accessing the normal and the light direction in the fragment shader? do I just add those variables to the fragment input struct and pass them in the vertex shader? or is there a way of accessing the normals per-pixel?

Yeah, you have to pass the normal and the light dir through from the vertex shader to the pixel shader. Then normalize them both in the pixel shader (to account for the interpolation, which shortens the vectors the further they are from the vertices) and do the dot product with those.

OK so I’ve made some adjustments, It grabs all the light from the point and direction lights in the scene then looks up the ramp texture. But for some reason the light from the point lights in the scene is inverted or similar, it doesn’t seem to actually light up the area around it.

Shader "Character/Banded Light Ramp Tex Vertex" {
	Properties {
		_MainTex ("Texture", 2D) = "white" {}
		_RampTex ("Ramp", 2D) = "white"{}
		_Color ("Color", Color) = (1,1,1,1)
	}
	SubShader {
		Pass{
		Tags {"LightMode" = "ForwardBase"}
			CGPROGRAM
			// Upgrade NOTE: excluded shader from DX11 and Xbox360 because it uses wrong array syntax (type[size] name)
			//#pragma exclude_renderers d3d11 xbox360			
			#pragma vertex vert
			#pragma fragment frag
			#pragma target 3.0
			//#pragma glsl
			
			#include "UnityCG.cginc"
			
			uniform sampler2D _MainTex;
			uniform float4 _MainTex_ST;
			uniform sampler2D _RampTex;
			uniform float4 _Color;
			uniform float4 _LightColor0;
			
			
			struct vertexInput {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 texcoord : TEXCOORD0;
			};
			
			struct fragmentInput{
				float4 pos : SV_POSITION;
				float4 color : COLOR0;
				half2 uv : TEXCOORD0;
				float3 theNormal;
				float3 [5] theLightDirection;
				float4 atten;
			};
			
			
			fragmentInput vert(vertexInput i){
				fragmentInput o;
				o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
				o.uv = TRANSFORM_TEX(i.texcoord, _MainTex);
				o.theLightDirection[4] = normalize(_WorldSpaceLightPos0).xyz;
				o.theNormal = normalize(mul(float4(i.normal,1.0), _World2Object).xyz);
				float [4] finalAtten;
				
				// This code is most likely outdated, from the CG wikibooks appropriated for what I need
	            for (int index = 0; index < 4; index++)
	            {    
	               	float4 lightPosition = float4(unity_4LightPosX0[index], 
	                  	unity_4LightPosY0[index], 
	                  	unity_4LightPosZ0[index], 1.0);
	                if(unity_LightPosition[index].w != 0.0)
	            	{
		                float3 vertexToLightSource = float3(lightPosition - mul(_Object2World, i.vertex).xyz);        
	               		o.theLightDirection[index] = normalize(vertexToLightSource);
	               		float squaredDistance = dot(vertexToLightSource, vertexToLightSource);
	               		finalAtten[index] = 1.0 / (1.0 + unity_4LightAtten0[index] * squaredDistance);
               		} else { 
               			finalAtten[index] = 1;
               			o.theLightDirection[index] = normalize(mul(lightPosition, _Object2World).xyz);
               		}
	            }
				// Attenuation is put into this float4
				o.atten = float4 (finalAtten[0],finalAtten[1],finalAtten[2], finalAtten[3]);
				
				o.color = float4(_LightColor0.xyz * _Color.rgb, 1.0);
				
				return o;
			}
			
			float4 frag(fragmentInput i) : COLOR
			{
				float finalLight = 0;
				
				float finalNormal = normalize(i.theNormal);
				
				finalLight += (max(0.0, dot(finalNormal, normalize(i.theLightDirection[0])) * i.atten.x));
				finalLight += (max(0.0, dot(finalNormal, normalize(i.theLightDirection[1])) * i.atten.y));
				finalLight += (max(0.0, dot(finalNormal, normalize(i.theLightDirection[2])) * i.atten.z));
				finalLight += (max(0.0, dot(finalNormal, normalize(i.theLightDirection[3])) * i.atten.w));
				finalLight += (max(0.0, dot(finalNormal, normalize(i.theLightDirection[4]))));
				
				float2 lookUpPos = (finalLight,finalLight);
				
				float rampDiffuse = tex2D(_RampTex, lookUpPos);
				
				return tex2D(_MainTex, i.uv) * rampDiffuse * 2 * i.color;
			}
			
			ENDCG
			//Lighting On
		}
		
	
	}
}

Uhh, alright, you’re now trying to do all of the vertex lights at once? That’s not what your previous version was doing. You only had to make a few adjustments to your previous version for it to work;

Shader "Character/Banded Light Ramp Tex Vertex" {
	Properties {
		_MainTex ("Texture", 2D) = "white" {}
		_RampTex ("Ramp", 2D) = "white"{}
		_Color ("Color", Color) = (1,1,1,1)
	}
	SubShader {
		Pass{
			Tags {"LightMode" = "ForwardBase"}
			CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag
				#pragma target 3.0
				#pragma glsl
				
				#include "UnityCG.cginc"
				
				uniform sampler2D _MainTex;
				uniform float4 _MainTex_ST;
				uniform sampler2D _RampTex;
				uniform fixed4 _Color;          // Made this a fixed4, you won't get greater precision using float here.
				uniform fixed4 _LightColor0;    // Made this a fixed4, you won't get greater precision using float here.
				
				
				struct vertexInput {
					float4 vertex : POSITION;
					float3 normal : NORMAL;
					float4 texcoord : TEXCOORD0;
				};

				struct fragmentInput{
					float4 pos : SV_POSITION;
					float2 uv : TEXCOORD0;			// Made this a float, fixed sometimes isn't precise enough for UVs.
					float3 normal : TEXCOORD1;		// Added this to store vertex normal for fragment shader.
					float3 lightDir : TEXCOORD2;	// Added this to store light direction for fragment shader.
				};

				fragmentInput vert(vertexInput i){
					fragmentInput o;
					o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
					o.uv = TRANSFORM_TEX(i.texcoord, _MainTex);

					// Here we set up the normal and light dir for use in the fragment shader, both in object space.
					o.normal = i.normal;
					o.lightDir = ObjSpaceLightDir(i.vertex);

					return o;
				}

				// Made this a fixed4, you won't gain precision with a float.
				fixed4 frag(fragmentInput i) : COLOR
				{
					// Normalize the vectors to fix interpolation shortening.
					i.normal = normalize(i.normal);
					i.lightDir = normalize(i.lightDir);

					// Get dot product to use as ramp UV coords.
					// Transformed from -1 to 1 range to 0 - 1 range so it can use the full dimensions of the ramp texture.
					float NdotL = dot(i.normal, i.lightDir) * 0.5 + 0.5;

					// Put the vector maths in brackets so it doesn't try and do scalar-vector maths where it doesn't need to.
					fixed4 c;
					c.rgb = (tex2D(_MainTex, i.uv).rgb * tex2D(_RampTex, NdotL.xx).rgb * _LightColor0.rgb * _Color.rgb) * 2;
					c.a = 1.0;
					return c;
				}
			ENDCG

			// You do not need this next line.
			// Lighting On
		}
	}
}

Thanks! I got it working with just a directional light so I figured I would attempt to get it working with all vertex lights. I’ve just been googling stuff like crazy to find out how you handle more lights in unity shaders, looks like I’ll have to keep searching and reading until I have a better understanding. Thanks again for all your help!