Using Global Illumination in native shaders in Unity 5

    Hello, Habr! Unity 5 provides us with a Global Illumination (GI) system out of the box, which allows you to get a really very nice picture in real time, which the developers showed in their sensational video The Blacksmith . Along with the global lighting system, the standard universal material has transferred all old materials to the category of obsolete ones. Despite the coolness of the standard material (and it, no less, is based on a physical model), I wondered if it was possible to connect the global lighting system to my own surface shader. What came of this, as well as what I had to face in the process, read under the cut.

    What good is Unity 5 global lighting?

    In the Unity 5 global lighting system, I was primarily attracted not by the simulation of ambient lighting, but by the built-in reflections. The developers added a new mechanism to the engine, called Reflection Probes.. The principle of its operation is quite simple: on the stage we place special markers (probes) in the right places that retain reflections around us in cubic textures. When moving between the markers, a pair of the most significant is selected, and the reflections received from both are mixed. Reflections can be calculated in real time, at the time of activation of the marker or even 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 the material of car paint. Agree, I really do not want to reinvent the wheel when everything is 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 reflect the light incident on them and themselves become sources of light. To calculate such indirect indirect illumination in real time, there is not enough modern computing power, but it can be calculated for static objects and occasionally recalculated for dynamic ones, which is implemented in Unity 5. The calculated data is packed into textures, which are then used for rendering in real time. These textures are called lightmaps or lightmaps.
    Unity 5 provides several mechanisms that influence the formation of global coverage:
    • Assigning a Baked or Mixed light source. Such a light source will work through a 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 preserved. In real-time rendering, the data interpolated over the graph grid are used to calculate lighting.
    • Directional Lightmapping Technology . When calculating indirect illumination, all surfaces can be considered perfectly flat, i.e., they reflect light equally in all directions, and the preferred direction of re-reflection can be taken into account using data from the normal map. This is the essence of this technology. The Directional Specular mode is also supported, which allows you to take into account glare, which allows you to create full-fledged secondary lighting.

    All this has many parameters, allowing to take into account the balance of performance-quality, which further raised the technology in my eyes. However, exploring the new features of the engine, I worked on the 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

    Here the first surprise was waiting for me: in the documentation for the engine there is no information on how to properly connect your own surface shader to the GI system. Instead, the documentation is replete with notes of the type “we switch everything to Standard material” and “why your materials are worse than Standard”. Fortunately, the sources of all the built-in shaders are in the public domain and are either in the CGIncludes folder, or they can be downloaded from the official site. According to the results of the study of the sources, the following was found out:
    • In order for your shader to interact with the GI system, you must redefine 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 for calculating global lighting, UnityGI is the result of calculating global lighting.
      Inside this function, of course, it is necessary to calculate the global lighting, 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. For example, you can transfer the results of any variation of the Ambient Occlusion algorithm into it . The oneMinusRoughness parameter defines something like the glossiness of a material. This parameter is used in the calculation of reflections, the lower the gloss, the less clear reflections we will get. The boolean parameter reflections allows you to turn off the calculation of reflections, the purpose of the remaining 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 slightly from previous versions of Unity. Now she has a signature

      inline fixed4 LightingYourModelName(YourSurfaceOutput s, half3 viewDir, UnityGI gi)

      The atten and lightDir parameters (familiar to some from previous versions of the engine) gave way to the UnityGI structure. 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 lighting is divided into two components: diffuse - diffused light from secondary light sources and specular - glare component (it is through it that reflections are transmitted). 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
              #ifdef DYNAMICLIGHTMAP_ON
                  // В случае использования динамического лайтмапа параметры источника света будут в gi.light3
          	// Здесь добавляется вклад indirect-освещения

      It is easy to see that the function that describes the lighting model contains several blocks under conditional compilation directives. Unity shaders are built according to a popular paradigm called über shaders. According to this paradigm, the most general shader is written (ideally one single), the code blocks in which turn into conditional compilation. The shader compiles according to the needs of the material. As a result, one source - many 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 this shader passage. The other two light sources can only be used in Directional Lightmapping with Directional Specular on. Light sourcegi.light2 will be active only if a static lightmap is used, and the source gi.light3 will work in conditions of dynamic lightmapping. At the end of the function, under the UNITY_LIGHT_FUNCTION_APPLY_INDIRECT directive , 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 that describes the lighting model, the viewDir parameter can be omitted if you do not need it. This allows the shader code generator to generate a slightly more optimal code. However, if you plan to use reflections from the GI system, the viewDir parameteryou will have to leave the function in the signature, even if you do not need it. The fact is that the built-in UnityGlobalIllumination function uses the gaze direction to calculate the reflection vector. If the code generator does not detect the viewDir parameter function in the signature , it will optimize the code and 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 got.

    Full surface shader code
    Shader "ShadersLabs/Universal"
            _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
            Tags { "RenderType"="Opaque" }
            LOD 200
            #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);
                    #ifdef DYNAMICLIGHTMAP_ON
                        c.rgb += CalculateCookTorrance(s, n, vdn, viewDir, gi.light3);
                	c.rgb += (s.Albedo * gi.indirect.diffuse + CalculateIndirectSpecular(s, vdn, gi.indirect.specular));
                return c;
            inline void LightingUniversal_GI(SurfaceOutputUniversal s, UnityGIInput data, inout UnityGI gi)
                gi = UnityGlobalIllumination(data, 1.0 /* occlusion */, 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 = e.a;
                o.Emission = e.rgb;
                o.Metallic = _Metallic;
                o.Roughness = _Roughness;
                o.ReflectionPower = _ReflectionPower;
                o.Alpha = c.a;
        FallBack "Diffuse"


    On the stage were placed 3 balls, a plane and 2 light sources (directional, simulating the Sun, and a point type Mixed right in front of the balls). One Reflection Probe has also been added to create reflections.

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

    The image below shows the contribution of reflections and reflected light that are generated by the global lighting system.

    If you replace the Directional Specular light-mapping mode in favor of the Directional mode, the picture will become boring, but this will allow you to slightly gain in performance. In addition, the Directional Specular mode is not supported on older versions of the graphics APIs, such as Open GL ES 2.0.

    A spoon of tar

    The results, in general, satisfied me. However, the last unresolved issue remained. Everything that I implemented did not support deferred shading mode . Unity 5 provides such an out-of-box lighting mode, and for the sake of completeness, it would be cool to support it.
    The biggest disappointment awaited me here. 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 method that requires certain additional squats. The engine documentation on this says the following:
    “The only lighting model available is Standard. If a different model is wanted you can modify the lighting pass shader, by placing the modified version of the Internal-DeferredShading.shader file from the Built-in shaders into a folder named “Resources” in your “Assets” folder. ”

    Thus, the only theoretical way to achieve what you want is to modify the internal shader and put it in a specific place in the project. The documentation does not provide any other more detailed instructions. By the way, simply copying to the right place did not bring me any results, the engine still used the internal source shader.
    Mr. marked-one suggested the right decision to me , for which he is very grateful. In order to force Unity to use your own shader to decode G-buffers, you need to go to  Edit -> Project Settings -> Graphics  and for  Built-in shader settings -> Deferred select Custom Shader from the list, then select your own shader. After that, Unity about using the exposed shader for the entire engine, including the editor.


    What I learned for myself from this story, 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 get ready to modify the internal Unity shaders). As an alternative, you can consider the full transition to Standard material, which Unity developers seem to be betting on. This material works in all modes, however, I would not begin to transfer my project to it. The price would be the loss of control over both the visual image of the game and its performance. You will make your own conclusions, for my part, I hope you enjoyed reading this post. Love the quality rendering, see you soon!

    Also popular now: