Before diving into lighting implementation, it is essential to understand two fundamental color operations: addition and multiplication. Additive blending is used to combine light sources, increasing the overall brightness, whereas multiplicative blending is used to modulate textures or mix colors, simulating the absorption of light by materials.
A complete lighting model typically consists of three components: ambient lighting, diffuse reflection, and specular reflection. In vertex shaders, these calculations are performed per vertex, making them computationally efficient compared to pixel-level calculations, though they may lack the smoothness of per-pixel lighting.
Diffuse Reflection Implementation
Diffuse rfelection simulates the way light scatters evenly across a matte surface. The intensity is determined by the angle between the surface normal vector and the direction of the incoming light. This is calculated using the dot product.
- If the dot product is 1, the surface is directly facing the light (maximum intensity).
- If the dot product is 0, the surface is perpendicular to the light (grazing angle, no illumination).
- If the dot product is < 0, the surface is facing away from the light.
It is crucial to normalize all vectors before performing the dot product to ensure the angle calculation remains accurate regardless of the object's scale. The following shader code implements a diffuse lighting model, including support for the main directional light and approximate point light contributions.
Shader "Custom/VertexLighting/Diffuse"
{
SubShader
{
Pass
{
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
fixed4 col : COLOR;
};
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// Transform normal from object space to world space
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
// Normalize the light direction vector
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
// Calculate Lambertian diffuse factor
float NdotL = saturate(dot(worldNormal, lightDir));
// Apply main light color
fixed3 diffuse = _LightColor0.rgb * NdotL;
// Calculate world position for point lights
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// Add contributions from 4 point lights (unity approximation)
fixed3 pointLights = Shade4PointLights(
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
unity_LightColor[0].rgb, unity_LightColor[1].rgb,
unity_LightColor[2].rgb, unity_LightColor[3].rgb,
unity_4LightAtten0,
worldPos, worldNormal
);
o.col.rgb = diffuse + pointLights;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// Add global ambient light in the fragment shader
return i.col + fixed4(UNITY_LIGHTMODEL_AMBIENT.rgb, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
Specular Reflection Implementation
Specular reflection creates the shiny highlight seen on smooth surfaces. It simulates the reflection of the light source directly into the camera. The intensity depends on the angle between the reflection vector and the view vector (Phong model) or the angle between the normal and the half-vector (Blinn-Phong model).
The Blinn-Phong model is generally preferred in vertex shaders as it involves fewer computational steps. The half-vector ($H$) is the normalized vector exactly halfway between the light direction ($L$) and the view direction ($V$). The shininess is controlled by a power function, which tightens the highlight as the exponent increases.
Shader "Custom/VertexLighting/Specular"
{
Properties
{
_Shininess ("Glossiness", Range(1, 64)) = 8
_SpecColor ("Specular Tint", Color) = (1,1,1,1)
}
SubShader
{
Pass
{
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
float _Shininess;
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
fixed4 col : COLOR;
};
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// Calculate necessary vectors in World Space
float3 N = UnityObjectToWorldNormal(v.normal); // Surface Normal
float3 L = normalize(_WorldSpaceLightPos0.xyz); // Light Direction
float3 V = normalize(WorldSpaceViewDir(v.vertex)); // View Direction
// --- 1. Ambient Component ---
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
// --- 2. Diffuse Component ---
float NdotL = saturate(dot(N, L));
fixed3 diffuse = _LightColor0.rgb * NdotL;
// --- 3. Specular Component (Blinn-Phong) ---
// Calculate the Half-Vector
float3 H = normalize(L + V);
// Calculate specular intensity based on the alignment of Normal and Half-Vector
float NdotH = saturate(dot(N, H));
float specularFactor = pow(NdotH, _Shininess);
fixed3 specular = _LightColor0.rgb * _SpecColor.rgb * specularFactor;
// Combine all lighting components
o.col.rgb = ambient + diffuse + specular;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return i.col;
}
ENDCG
}
}
FallBack "Specular"
}