📜 ⬆️ ⬇️

Using Global Illumination in its own shaders in Unity 5

image
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:

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:

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 /* 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 = 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!

Source: https://habr.com/ru/post/251045/


All Articles