Dynamic Ambient Lighting in Unity

Dynamic Ambient Lighting

I’ve been working on getting dynamic ambient lighting working within Unity. Based off Valve’s 6-colour pre-baked ambient lighting (detailed here, pg 5, ch 8.4.1), but it grabs the 6 colours dynamically.

There’s probably lots more you could do to optimise it further (e.g. use replacement shaders when rendering the cubemap that do simpler lighting calcs). But you could do that yourself as required.

I’d also advise against using it on anything other than your main character, as it’s likely too expensive to run on multiple objects.

Cubemap Camera Script

Create a new camera and turn off the GUI, Flare and Audio components.

Set up it’s Culling Layers to not render non-essential things like particles or incidental detail. Also move your character to it’s own layer and set the camera not to render it (we don’t want bits of the character rendered into the cubemap).

Attach this javascript to it and set the target to be your character and set up the offset from your character’s position so that it’s in the centre of your character (i.e. a 2m tall character wants to be offset 0, 1, 0 so that the camera renders from the characters centre.

@script ExecuteInEditMode

public var target : Transform;
public var cubemapSize : int = 128;
public var oneFacePerFrame : boolean = true;
public var offset : Vector3 = Vector3.zero;
private var cam : Camera;
private var rtex : RenderTexture;

function Start () {
	cam = camera;
	cam.enabled = false;
	// render all six faces at startup
	UpdateCubemap( 63 );
	transform.rotation = Quaternion.identity;
}

function LateUpdate () {
    if ( oneFacePerFrame ) {
        var faceToRender = Time.frameCount % 6;
        var faceMask = 1 << faceToRender;
        UpdateCubemap ( faceMask );
    } else {
        UpdateCubemap ( 63 ); // all six faces
    }
}

function UpdateCubemap ( faceMask : int ) {
	if ( !rtex ) {
		rtex = new RenderTexture ( cubemapSize, cubemapSize, 16 );
		rtex.isPowerOfTwo = true;
		rtex.isCubemap = true;
		rtex.useMipMap = true;
		rtex.hideFlags = HideFlags.HideAndDontSave;
		rtex.SetGlobalShaderProperty ( "_WorldCube" );
	}

	transform.position = target.position + offset;

	cam.RenderToCubemap ( rtex, faceMask );
}

function OnDisable () {
	DestroyImmediate ( rtex );
}

Dynamic Ambient Shader

This is the shader that generates and applies the ambient lighting from the cubemap rendered by the camera above.

Create a new shader, paste this code into it and save it. We’ll integrate it into our shaders next.

Shader "DynamicAmbient" {
	Properties {
		_MainTex ("Diffuse (RGB) Alpha (A)", 2D) = "white" {}
		_BumpMap ("Normal (Normal)", 2D) = "bump" {}
	}

	SubShader{
		Pass {
			Name "DynamicAmbient"
			Tags {"LightMode" = "Always"}

			CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag
				#pragma fragmentoption ARB_precision_hint_fastest

				#include "UnityCG.cginc"

				struct v2f
				{
					float4	pos : SV_POSITION;
					float2	uv : TEXCOORD0;
					float3	normal : TEXCOORD2;
					float3	tangent : TEXCOORD3;
					float3	binormal : TEXCOORD4;
				}; 

				v2f vert (appdata_tan v)
				{
					v2f o;
					o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
					o.uv = v.texcoord.xy;
					o.normal = mul(_Object2World, float4(v.normal, 0)).xyz;
					o.tangent = v.tangent.xyz;
					o.binormal = cross(o.normal, o.tangent) * v.tangent.w;
					return o;
				}

				sampler2D _MainTex;
				sampler2D _BumpMap;
				samplerCUBE _WorldCube;

				float4 frag(v2f i) : COLOR
				{
					fixed4 albedo = tex2D(_MainTex, i.uv);
					float3 normal = UnpackNormal(tex2D(_BumpMap, i.uv));

					float3 worldNormal = normalize((i.tangent * normal.x) + (i.binormal * normal.y) + (i.normal * normal.z));

					float3 nSquared = worldNormal * worldNormal;
					fixed3 linearColor;
					linearColor = nSquared.x * texCUBEbias(_WorldCube, float4(worldNormal.x, 0.00001, 0.00001, 999)).rgb; // For unknown reasons, giving an absolute vector ignores the mips....
					linearColor += nSquared.y * texCUBEbias(_WorldCube, float4(0.00001, worldNormal.y, 0.00001, 999)).rgb; // ...so unused components must have a tiny, non-zero value in.
					linearColor += nSquared.z * texCUBEbias(_WorldCube, float4(0.00001, 0.00001, worldNormal.z, 999)).rgb;

					float4 c;
					c.rgb = linearColor * albedo.rgb;
					c.a = albedo.a;
					return c;
				}
			ENDCG
		}
	}
	FallBack Off
}

Integrating the Ambient Shader into Surface Shaders

Now, we can use the above shader wherever we want it via the UsePass command, and blending everything else on top.

The key here is to ensure your surface shader’s blend mode is set to additive (One One) otherwise it’ll just write clean over the lovely ambient light that’s been applied.
So, before your surface shader’s CGPROGRAM block, add the lines;

UsePass "DynamicAmbient/DYNAMICAMBIENT"
Blend One One

We’ve also got to ensure that our surface shader doesn’t use the ambient light value that’s set in the editor, otherwise it’ll add the two together and defeat the purpose. So when you define the surface shader to use, ensure you add the noambient argument. e.g;

#pragma surf BlinnPhong noambient

Your new surface shader with dynamic ambient lighting should look something like this;

Shader "Bumped Specular" {
	Properties {
		_Color ("Main Color", Color) = (1,1,1,1)
		_SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 1)
		_Shininess ("Shininess", Range (0.03, 1)) = 0.078125
		_MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {}
		_BumpMap ("Normalmap", 2D) = "bump" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 400

	UsePass "DynamicAmbient/DYNAMICAMBIENT"
	Blend One One
	CGPROGRAM
		#pragma surface surf BlinnPhong noambient

		sampler2D _MainTex;
		sampler2D _BumpMap;
		fixed4 _Color;
		half _Shininess;

		struct Input {
			float2 uv_MainTex;
			float2 uv_BumpMap;
		};

		void surf (Input IN, inout SurfaceOutput o) {
			fixed4 tex = tex2D(_MainTex, IN.uv_MainTex);
			o.Albedo = tex.rgb * _Color.rgb;
			o.Gloss = tex.a;
			o.Alpha = tex.a * _Color.a;
			o.Specular = _Shininess;
			o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
		}
	ENDCG
	}
	FallBack "Specular"
}

Now apply your new shader to your character’s material and we’re done 🙂

About Farfarer

Author of this blog.
Posted in Tutorial, Unity. Bookmark the permalink.

Comments are closed.