
Hi, Habr! Unity 5 provides us with a global illumination system (Global Illumination, GI) out of the box, which allows you to get a really nice picture in real time, which the developers demonstrated in their acclaimed video clip
The Blacksmith . Along with the system of global illumination, the universal material Standard transformed into a category of obsolete all former materials. Despite the coolness of the standard material (and he, no less, is based on a physical model), I wondered if it was possible to connect the global illumination system to its own surface shader. What came out of it, as well as what I had to face in the process, read under the cut.
What is good about Unity 5 global coverage?
In the system of global illumination in Unity 5, I was primarily attracted not by the ambient lighting simulation, but by the built-in reflections. The developers have added a new mechanism to the engine, which is called
Reflection Probes . The principle of its operation is quite simple: on the stage we place special markers (probes) in the right places, which retain reflections around themselves in cubic textures. When moving between markers, a pair of the most significant is selected, and the reflections obtained from both are mixed. Reflections can be calculated in real time, at the time of the activation of the marker, or generally controlled by a script where you can implement, for example, timers. Such systems are often implemented in games where reflections are constantly needed, in particular, to simulate automotive paint material. Agree, I really do not want to reinvent the wheel when everything is already done in the engine.
Simulating secondary lighting is also very cool and can increase the realism of your rendering. In the real world, many materials re-reflect the light falling on them and become light sources themselves. To calculate such a secondary (indirect) lighting in real time, modern computing power is not enough, but it can be pre-calculated for static objects and rarely recalculated for dynamic ones, which is realized in Unity 5. The calculated data is packed into textures, which are then used during rendering in real time. These textures are called lightmaps or lightmaps.
Unity 5 provides several mechanisms that influence global lighting:
- Assigned to a light source of type Baked or Mixed. Such a light source will work through lightmap and not affect dynamic objects (Baked type) or work for dynamic objects as a full-fledged light source (Mixed type).
- Creating light markers ( Light Probes ). Illumination markers are a three-dimensional graph, in the nodes of which the level of illumination created by various light sources is maintained. When rendering in real time, the interpolated data on the graph grid is used to calculate the lighting.
- Technology Directional Lightmapping . When calculating indirect-illumination, all surfaces can be considered ideally flat, i.e., the light is equally reflective in all directions, and one can take into account the preferential direction of the reflections by using data from the normal map. This is the essence of this technology. Also supported is the Directional Specular mode, which allows you to take into account glare, which allows you to create a full secondary lighting.
All this has many parameters that allow to take into account the balance of performance and quality, which further raised the technology in my eyes. However, exploring new features of the engine, I worked on a test scene with Standard material, which ultimately did not suit me, I wanted to connect my own surface shader to the global lighting system.
Creating a surface shader
The first surprise was waiting for me here: there is no information in the documentation for the engine on how to properly connect your own surface shader to the GI system. Instead, the documentation is replete with notes like “we are switching to Standard material” and “why your materials are worse than Standard”. Fortunately, the source codes of all embedded shaders are in the public domain and are either in the CGIncludes folder, or you can download them from the official site. According to the results of the study of the source, it turned out the following:
- In order for your shader to interact with the GI system, you must override the function with the following signature:
')
inline void LightingYourModelName_GI(YourSurfaceOutput s, UnityGIInput data, inout UnityGI gi)
where YourModelName is the name of your lighting model, YourSurfaceOutput is the name of your data structure with surface parameters, UnityGIInput is the input data for calculating global illumination, UnityGI is the result of calculating global illumination.
Inside this function, of course, it is necessary to calculate the global illumination, for which the built-in function serves
inline UnityGI UnityGlobalIllumination (UnityGIInput data, half occlusion, half oneMinusRoughness, half3 normalWorld, bool reflections)
This function is defined in the CGIncludes / UnityGlobalIllumination.cginc file. The occlusion parameter is responsible for additional shading. You can, for example, transfer the results of any variation of the Ambient Occlusion algorithm to it. The oneMinusRoughness parameter defines something like the glossiness of the material. This parameter is used when calculating reflections; the less glossiness, the less clear reflections we will receive. The Boolean parameter reflections allows you to turn off the reflection calculation, the purpose of the other parameters is obvious.
As a result, I got the following function:
inline void LightingUniversal_GI(SurfaceOutputUniversal s, UnityGIInput data, inout UnityGI gi) { gi = UnityGlobalIllumination(data, 1.0 /* occlusion */, 1.0 - s.Roughness, normalize(s.Normal)); }
- The function containing the lighting model has changed somewhat from previous versions of Unity. Now she has a signature
inline fixed4 LightingYourModelName(YourSurfaceOutput s, half3 viewDir, UnityGI gi)
Parameters atten and lightDir (someone familiar from previous versions of the engine) gave way to the structure of UnityGI. This structure can contain up to 3 light sources ( light , light2 and light3 ), as well as data on secondary lighting in the indirect parameter. Secondary illumination is divided into two components: diffuse - diffused light from secondary light sources and specular - specular component (reflections are transmitted through it). To better understand how to apply all this data, consider the following pseudocode:
inline fixed4 LightingYourModelName(YourSurfaceOutput s, half3 viewDir, UnityGI gi) { // gi.light #if defined(DIRLIGHTMAP_SEPARATE) #ifdef LIGHTMAP_ON // gi.light2 #endif #ifdef DYNAMICLIGHTMAP_ON // gi.light3 #endif #endif #ifdef UNITY_LIGHT_FUNCTION_APPLY_INDIRECT // indirect- #endif }
It is easy to see that the function describing the lighting model contains several blocks under the conditional compilation directives. Shaders in Unity are built according to a popular paradigm called ĂĽber shaders. According to this paradigm, the most common shader is written (ideally, the only one), the blocks of code in which turn into conditional compilation. The shader is compiled according to the needs of the material. As a result, one source - a set of compiled options. So, returning to our function, the gi.light light source should always be used, since it contains the parameters of the main light source for a given shader pass. The remaining two light sources can only be used in Directional Lightmapping mode with the glare (Directional Specular) turned on. The gi.light2 light source will be active only if a static lightmap is used, and the gi.light3 source will work under dynamic light mapping conditions. At the end of the function, under the directive UNITY_LIGHT_FUNCTION_APPLY_INDIRECT , secondary lighting is applied.
I also want to note a curious story that happened with the viewDir parameter. As some probably know, in the function describing the lighting model, the viewDir parameter can be omitted if you do not need it. This allows the shader code generator to form a slightly more optimal code. However, if you plan to use reflections from the GI system, the viewDir parameter in the function signature will have to be left even if you do not need it. The point is that the built-in function UnityGlobalIllumination uses the direction of gaze to calculate the reflection vector. If the code generator does not detect the viewDir function in the signature, it optimizes the code and the reflections will stop working.
I used the Cook-Torrance lighting model as the basis for my surface shader, which you can read about
here or
here . Under the spoiler you will find the full code of the resulting shader. Now let's look at what we did.
Full surface shader code Shader "ShadersLabs/Universal" { Properties { _MainColor("Color", Color) = (1,1,1,1) _MainTex("Albedo", 2D) = "white" {} _NormalMap("Normal", 2D) = "bump" {} _EmissionMap("Emission (RGB), Specular (A)", 2D) = "black" {} _Roughness("Roughness", Range(0,1)) = 0.1 _ReflectionPower("Reflection Power", Range(0.01, 5)) = 3 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Universal fullforwardshadows exclude_path:prepass exclude_path:deferred #pragma target 3.0 struct Input { half2 uv_MainTex; }; struct SurfaceOutputUniversal { fixed3 Albedo; fixed3 Normal; fixed3 Emission; fixed Specular; fixed Metallic; fixed Roughness; fixed ReflectionPower; fixed Alpha; }; sampler2D _MainTex; sampler2D _NormalMap; sampler2D _SpecularMap; sampler2D _EmissionMap; fixed4 _MainColor; fixed _Roughness; fixed _ReflectionPower; fixed _Metallic; inline fixed3 CalculateCookTorrance(SurfaceOutputUniversal s, half3 n, fixed vdn, half3 viewDir, UnityLight light) { half3 h = normalize(light.dir + viewDir); fixed ndl = saturate(dot(n, light.dir)); fixed ndh = saturate(dot(n, h)); fixed vdh = saturate(dot(viewDir, h)); fixed ndh2 = ndh * ndh; fixed sp2 = max(s.Roughness * s.Roughness, 0.001); fixed G = min(1.0, 2.0 * ndh * min(vdn, ndl) / vdh); fixed D = exp((ndh2 - 1.0)/(sp2 * ndh2)) / (4.0 * sp2 * ndh2 * ndh2); fixed F = 0.5 + 0.5 * pow(1.0 - vdh, s.ReflectionPower); fixed spec = saturate(G * D * F / (vdn * ndl)); return light.color * (s.Albedo * ndl + fixed3(s.Specular, s.Specular, s.Specular) * spec); } inline fixed3 CalculateIndirectSpecular(SurfaceOutputUniversal s, fixed vdn, half3 indirectSpec) { fixed rim = saturate(pow(1.0 - vdn, s.ReflectionPower)); return indirectSpec * rim * s.Metallic; } inline fixed4 LightingUniversal(SurfaceOutputUniversal s, half3 viewDir, UnityGI gi) { half3 n = normalize(s.Normal); fixed vdn = saturate(dot(viewDir, n)); fixed4 c = fixed4(CalculateCookTorrance(s, n, vdn, viewDir, gi.light), s.Alpha); #if defined(DIRLIGHTMAP_SEPARATE) #ifdef LIGHTMAP_ON c.rgb += CalculateCookTorrance(s, n, vdn, viewDir, gi.light2); #endif #ifdef DYNAMICLIGHTMAP_ON c.rgb += CalculateCookTorrance(s, n, vdn, viewDir, gi.light3); #endif #endif #ifdef UNITY_LIGHT_FUNCTION_APPLY_INDIRECT c.rgb += (s.Albedo * gi.indirect.diffuse + CalculateIndirectSpecular(s, vdn, gi.indirect.specular)); #endif return c; } inline void LightingUniversal_GI(SurfaceOutputUniversal s, UnityGIInput data, inout UnityGI gi) { gi = UnityGlobalIllumination(data, 1.0 , 1.0 - s.Roughness, normalize(s.Normal)); } void surf(Input IN, inout SurfaceOutputUniversal o) { fixed4 c = _MainColor * tex2D(_MainTex, IN.uv_MainTex); fixed4 e = tex2D(_EmissionMap, IN.uv_MainTex); o.Albedo = c.rgb; o.Normal = normalize(UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex))); o.Specular = ea; o.Emission = e.rgb; o.Metallic = _Metallic; o.Roughness = _Roughness; o.ReflectionPower = _ReflectionPower; o.Alpha = ca; } ENDCG } FallBack "Diffuse" }
results
On the scene were placed 3 balls, a plane and 2 light sources (directed, imitating the Sun, and a point type Mixed right in front of the balls). A single Reflection Probe has also been added to create reflections.

As a result, we get the following picture (on the left, global illumination is off, on the right - on).

The image below shows the contribution of reflections and re-reflected light, which are formed by a global illumination system.

If you replace the Directional Specular light mapping mode in favor of the Directional mode, the picture will become more boring, but this will allow you to gain some performance. In addition, Directional Specular mode is not supported on older versions of the graphics API, for example, Open GL ES 2.0.

A spoon of tar
The results, in general, I was satisfied. However, the last outstanding question remained. All that I implemented did not support
deferred shading . Unity 5 provides such a lighting mode out of the box, and, for the sake of completeness, it would be cool to support it.
Here I was waiting for the biggest disappointment. In the current version of the engine (I used 5.1.3), you can override the function for writing data to the G-buffer (
LightingYourModelName_Deferred ), but the function that decodes the G-buffers cannot be redefined. More precisely, there is a way that requires certain extra squats. The documentation for the engine about this says the following:
“The only lighting model available is Standard. If you want to make it, it’s possible to modify your lighting passport shader.
Thus, the only theoretical way to achieve the desired is to modify the inner shader and put it in a certain place in the project. The documentation does not provide any other more detailed instructions. By the way, a simple copy to the right place did not bring me the result, the engine still used the internal source shader.
The correct decision was suggested to me by Mr.
marked-one , for which he was very grateful. In order to force Unity to use your own shader to decode G-buffers, go to
Edit -> Project Settings -> Graphics and for the
Built-in shader settings -> Deferred select Custom Shader from the list, then select your own shader. After that, Unity is about using the exposed shader for the entire engine, including the editor.
findings
What I learned from this story for myself, the Unity developers have created a very good global lighting system. It can and should be used if you do not use deferred lighting in your project (and if you do, then prepare to modify the internal shaders of Unity). As an alternative, you can consider a full transition to the Standard material, to which the Unity developers, apparently, make a big bet. This material works in all modes, however, I would not switch my project to it. The price would be a loss of control over both the visual image of the game and its performance. You will draw conclusions for yourself, but for my part, I hope that you enjoyed reading this post. Love quality rendering, see you soon!