This is the tenth part of a tutorial series about rendering. Last time, we used multiple textures to create complex materials. We'll add some more complexity this time, and also support multi-material editing.

This tutorial was made with Unity 5.4.3f1.

Occluded Areas

Even through we can create materials that appear complex, it is just an illusion. The triangles are still flat. Normal maps can give the impression of depth, but that only works for direct light. There is no self-shadowing. Parts that are supposedly higher, should casts shadows on areas that are lower. But this doesn't happen. This is most obvious when the normal map would suggest that there are small holes, dents, or cracks.

For example, let's say that someone has been shooting at our circuit board. The shots didn't go through the board, but left significant dents. Here's an adjusted normal map for that.

Dented circuitry normal map.

When using this normal map, the circuitry material indeed appears dented. But the deepest parts of the dents are lit just as well as the undented surface. There isn't any self-shadowing going on in the dents. As as result, they do not appear to be very deep.

Dented circuitry.

Occlusion Map To add self-shadowing, we can use what's known as an occlusion map. You can think of this as a fixed shadow map that's part of the material. Here is such a map for the dented circuitry, as a grayscale image. Occlusion map. To use this map, add a texture property for this map to our shader. Also add an occlusion strength slider property, so we can fine-tune it. [NoScaleOffset] _OcclusionMap ("Occlusion", 2D) = "white" {} _OcclusionStrength("Occlusion Strength", Range(0, 1)) = 1 Just like with the metallic map, let's use a shader feature to only sample the occlusion map when it is set. Add the feature to the base pass only, don't worry about additional lights now. #pragma shader_feature _ _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC #pragma shader_feature _OCCLUSION_MAP #pragma shader_feature _EMISSION_MAP

Occlusion UI Because we have a custom shader GUI, we have to manually add the new properties to our shader's UI. So add a DoOcclusion step to MyLightingShaderGUI.DoMain . void DoMain () { … DoNormals(); DoOcclusion(); DoEmission(); editor.TextureScaleOffsetProperty(mainTex); } This new method is nearly identical do DoMetallic , which is also about a map, a slider, and a keyword. So duplicate that method and make the required changes. While DoMetallic shows the slider when there is no map, we have to do the opposite here. Also, Unity's standard shader uses the G color channel of the occlusion map, so we'll do this as well. Indicate this in the tooltip. void DoOcclusion () { MaterialProperty map = FindProperty( "_OcclusionMap" ); EditorGUI.BeginChangeCheck(); editor.TexturePropertySingleLine( MakeLabel(map, "Occlusion (G)" ), map, map.textureValue ? FindProperty("_OcclusionStrength") : null ); if (EditorGUI.EndChangeCheck()) { SetKeyword( "_OCCLUSION_MAP" , map.textureValue); } } Inspector without and with occlusion map.

Adding Shadows To access the map in our include file, add a sampler and float variable. sampler2D _OcclusionMap; float _OcclusionStrength; Create a function to take care of sampling the map, if it exists. If not, the light should not be modulated, so the result is simply 1. float GetOcclusion (Interpolators i) { #if defined(_OCCLUSION_MAP) return tex2D(_OcclusionMap, i.uv.xy).g; #else return 1; #endif } When the occlusion strength is zero, the map should't affect the light at all. Thus, the function should return 1. When at full strength, the result is exactly what's in the map. We can do this by interpolating between 1 and the map, based on the slider. return lerp(1, tex2D(_OcclusionMap, i.uv.xy).g , _OcclusionStrength) ; To apply the shadows to the light, we have to factor the occlusion into the light attention inside CreateLight . UnityLight CreateLight (Interpolators i) { … UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos); attenuation *= GetOcclusion(i); light.color = _LightColor0.rgb * attenuation; light.ndotl = DotClamped(i.normal, light.dir); return light; } Without and with occlusion at full strength.

Shadowing Indirect Light The dents have become darker, but overall not by much. That's because a lot of the light is actually indirect light in this scene. As our occlusion map is not specific to any light, we can apply it to indirect light as well. This is done by modulating both the diffuse and specular indirect light. UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) { … #if defined(FORWARD_BASE_PASS) … float occlusion = GetOcclusion(i); indirectLight.diffuse *= occlusion; indirectLight.specular *= occlusion; #endif return indirectLight; } Without and with full occlusion. This produces much stronger shadows. In fact, they might be too strong. As the occlusion map is based on the surface shape and not on a specific light, it makes sense that it is only applied to indirect light. Light coming from all directions is reduced the deeper you go into a dent. But when a light shines directly in it, the dent should be fully lit. So let's remove occlusion from direct lights. UnityLight CreateLight (Interpolators i) { … UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos); // attenuation *= GetOcclusion(i); light.color = _LightColor0.rgb * attenuation; light.ndotl = DotClamped(i.normal, light.dir); return light; } Without and with indirect occlusion only. As far as occlusion maps go, this is as realistic as it can get. Having said that, you'll often find games where occlusion maps are applied to direct lights as well. Unity's older shaders did this too. While that is not realistic, it does give artist more control over lighting. What about screen-space ambient occlusion? SSAO is a post-processing image effect that uses the depth buffer to create an occlusion map for an entire frame on the fly. It is used to enhance the feeling of depth in a scene. Because it is a post-processing effect, it is applied to the image after all lights have been rendered. This means that the shadowing is applied to both the indirect and direct light. As a result, this effect is also not realistic.