Fake Dynamic Lightmaps

Table of Contents

Lightmapping is a technique where the effects of our lights are baked into our textures or a separate set of textures (like in Unity) to render our scenes without per-frame runtime cost. We use more memory, video memory and disk space, we can’t use dynamic lighting effects like self shadowed normal maps unless we fake them and our lights and shadows can’t be dynamic this way but we don’t calculate the lights every frame, we are free to use as many lights as we want without cost difference (except the baking/render times) and the rendered lightmaps can be of higher quality than dynamic lighting with additions like baked GI with final gathering, baked ambient occlusion and if our memory and bandwith allows it, higher resolution.

The technique described in this text has very limited application. It works best in top-down or maybe isometric games where the camera looks at the scene from afar. It also helps if most your surfaces are flat and your lightmap UV is not tightly packed. We will move the lightmap UV coordinates on a sine so it’s possible that the shadows of some objects can jump to non-neighboring geometry.

1. Dynamic Flickering Lightmaps

Lightmaps are used extensively especially in mobile games for low performance impact (when compared to dynamic lighting). But there are some cases where the static baked lighting and shadows just won’t cut it. In my current game in development I have a number of fire based lights sources like braziers and lamps so I wanted my shadows to “flicker”. Thus the following shader will move the lightmap UV coordinates back and forth on a sine function with tweakable magnitude and speed.

Dynamic Lightmap Figure 1.1 Fake dynamic lightmap in motion (albeit being highly compressed, sped up and with decreased frames, still visible) (Defender 3D © Accidental Empire Entertainment)

1.1 Uses

  • Works well on large flat surfaces like the ground plane. Top-down games would be the best fit.
  • Works best on games where the camera looks at the scene from a distance. RPG, RTS, MOBA etc.
  • Works best on low resolution lightmaps as it’s harder to see that it’s faked due to the area between lights and shadows being blurry
  • Can be applied per object so you can just apply it to a brazier which contains a fire and it will look like the shadows are dancing as the fire moves. It can also be toggled in realtime just by changing the shader.
  • Much lower cost performance wise compared to dynamic shadows and dynamic projections/decals. It’s just a per-vertex sine function.

1.2 How to Use

Just create a material with the shader selected, apply the material to the desired object, play with the flicker speed and magnitude until satisfied with the results.

1.3 Source Code

/* 
    A shader that moves the lightmap coordinates on a sine wave with customizable speed and magnitude
    Sarper Soher
    http://sarpersoher.com/unity3d/flickering-lightmaps/
*/

Shader "SarperSoher/LightmapFlicker" {
	Properties {
		_FlickerSpeed("LM Flicker Speed", float) = 250
		_FlickerMagnitude("LM Flicker Magnitude", Range(0.0001, 0.01)) = 0.0005

		_MainTex ("Base (RGB)", 2D) = "white" {}
	}

	SubShader {
		Tags { "RenderType" = "Opaque" "RenderQueue" = "Geometry"}
		LOD 200

		Pass {
			Lighting Off

			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fog

			#include "UnityCG.cginc"

			uniform sampler2D _MainTex;
			uniform float4 _MainTex_ST;

			uniform float _FlickerSpeed;
			uniform float _FlickerMagnitude;

			struct VertexInput {
				float4 vertex : POSITION;
				float2 texcoord : TEXCOORD0;
				float2 lightmapcoord : TEXCOORD1;
			};

			struct VertexOutput{
				float4 pos : SV_POSITION;
				float2 tex : TEXCOORD0;
				float2 lightmap : TEXCOORD1;
				UNITY_FOG_COORDS(2)
			};

			VertexOutput vert(VertexInput input) {
				VertexOutput output;

				output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
				output.tex = input.texcoord;

				UNITY_TRANSFER_FOG(output, output.pos);

				// Separately modify the UV coords on two axes
				// You may want to move it slower or with a different function on one of those axes
				// If so feel free to use constant values on one or introduce more properties (e.g. _FlickerSpeed2)

				//unity_LightmapST is an internal variable, means lightmap texture coordinates
				//_Time is an internal variable, it's the equivalent of Time.timeSinceLevelLoad from the Unity API

				// What's happening here is;
				//--Take the UV2 coords from the vertex
				//--Multiply it with Unity lightmap texture coordinates

				//--Add the return value of the below function to our coordinates
				//--sin(_Time * _FlickerSpeed) returns the sine of ever increasing _Time times our speed in radians
				//----So more speed modifies the sine value faster, thus moves our coords faster

				//--Finally multiply with _FlickerMagnitude to increase/Decrease the range of our coordinates
				float xCoord = input.lightmapcoord.x * unity_LightmapST.x + sin(_Time * _FlickerSpeed) * _FlickerMagnitude;
				float yCoord = input.lightmapcoord.y * unity_LightmapST.y + sin(_Time * _FlickerSpeed) * _FlickerMagnitude;

				// Assemble the modified lightmap coords back to a vector/float4 and pass it back to our output struct
				output.lightmap = float2(xCoord, yCoord) + unity_LightmapST.zw;

				return output;
			}

			float4 frag(VertexOutput input) : COLOR {
				float4 color = tex2D(_MainTex, input.tex.xy * _MainTex_ST.xy + _MainTex_ST.zw);

				// Decode and multiply the lightmap to our diffuse
				color.rgb *= DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, input.lightmap));

				UNITY_APPLY_FOG(input.fogCoord, color);

				return color;
			}

			ENDCG
		}
	}
}