By default, Unity’s spot lights have a (roughly) linear falloff. Gross. Any CG artist will tell you that this presents a massive obstacle to anybody hoping to achieve physically realistic visuals in the engine.
But what if we could change Unity’s default spot lights to use a different falloff rate? After a fair bit of research, it turns out there are multiple ways to achieve this! This guide will help walk you through the most effective and flexible way that I’ve found to date.
A big shout out to the user manutoo on the Unity forums for sharing his knowledge of this process. Without you, I wouldn’t have known about the GlobalTexture editing via scripts.
Download the Unity Built-in shaders for your engine version here: https://unity3d.com/get-unity/download/archive
Copy the UnityDeferredLibrary.cginc and Internal-DeferredShading.shader files into your project. Go to Edit > Project Settings > Graphics and set the Deferred Shader to your Internal-DeferredShading.shader file.
Open up UnityDeferredLibrary.cginc. You should see a section that looks like this (starting at line 163, but may vary by engine version)
// spot light case #if defined (SPOT) float3 tolight = _LightPos.xyz - wpos; half3 lightDir = normalize (tolight); float4 uvCookie = mul (unity_WorldToLight, float4(wpos,1)); // negative bias because http://aras-p.info/blog/2010/01/07/screenspace-vs-mip-mapping/ float atten = tex2Dbias (_LightTexture0, float4(uvCookie.xy / uvCookie.w, 0, -8)).w; atten *= uvCookie.w < 0; float att = dot(tolight, tolight) * _LightPos.w; atten *= tex2D (_LightTextureB0, att.rr).UNITY_ATTEN_CHANNEL; atten *= UnityDeferredComputeShadow (wpos, fadeDist, uv);
This is Unity’s spot light algorithm. What this tells us is Unity’s lights are actually lookup textures rather than raw mathematical values. You can always just mess with the function directly if you desire, but it can lead to unintended consequences (like losing cookie support).
Instead, we’re going to replace Unity’s default spot light texture with our own. To do this, start by declaring a new sampler2D to represent our Spot light texture. You’ll want to do this near the top of the file, shortly after the camera’s depth texture is declared (line 40 for Unity 5.6.1.1f).
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture); //existing shader line sampler2D _LightTextureSpot;
Then, return to the spot function we viewed earlier (line 163) and swap out _LightTextureB0 for your new sampler.
#if defined (SPOT) float3 tolight = _LightPos.xyz - wpos; half3 lightDir = normalize (tolight); float4 uvCookie = mul (unity_WorldToLight, float4(wpos,1)); // negative bias because http://aras-p.info/blog/2010/01/07/screenspace-vs-mip-mapping/ float atten = tex2Dbias (_LightTexture0, float4(uvCookie.xy / uvCookie.w, 0, -8)).w; atten *= uvCookie.w < 0; float att = dot(tolight, tolight) * _LightPos.w; atten *= tex2D (_LightTextureSpot, att.rr).UNITY_ATTEN_CHANNEL; atten *= UnityDeferredComputeShadow (wpos, fadeDist, uv);
Great. We’ve successfully circumvented Unity’s linear falloff texture. Next we need to actually replace it with our own texture. The secret to this is Unity’s Shader.SetGlobalTexture function.
Create a new C# script and add it to a GameObject in your scene. Place the following code in its body:
if (Shader.GetGlobalTexture("_LightTextureSpot")) return; int pixelCount = 16; Texture2D m_AttenTex = new Texture2D(pixelCount, 1, TextureFormat.ARGB32, false, true); m_AttenTex.filterMode = FilterMode.Bilinear; m_AttenTex.wrapMode = TextureWrapMode.Clamp; Color[] pixels = new Color[pixelCount * pixelCount]; Vector2 center = new Vector2(0, pixelCount / 2); int blackLimit = pixelCount - 1; int maxDistance = 10; for (int i = 1; i <= pixelCount; i++) { float v; if (i < blackLimit) { float normalizedIntensity = (float)i / blackLimit; float linearIntensity = normalizedIntensity * maxDistance; v = 1.0f / (linearIntensity * linearIntensity); } else v = 0.0f; pixels[i - 1] = new Color(v, v, v, v); } m_AttenTex.SetPixels(pixels); m_AttenTex.Apply(); Shader.SetGlobalTexture("_LightTextureSpot", m_AttenTex);
Alternatively, place that code in its Update function with a conditional check on it and the [ExecuteInEditMode] command. Obviously, this texture can be changed in any way you desire.
Our final result has a similar gradient to a typical inverse squared falloff (ISF) model. Unfortunately, with the arbitrary range modifier on Unity's lights (attenuation in UE4), it's difficult to achieve perfect physical accuracy without doing some manual calculations. You'll also notice that Unreal's lights provide a more rounded spotlight, while Unity's still feel like a linear gradient. I may look into that in the near future. In the meantime, experiment with this technique to get some different lighting effects.
And that’s it! Custom light falloff in Unity. If anybody has found another way to set Unity’s GlobalTexture list within the editor itself and without the script, be sure to let me know.