> For the complete documentation index, see [llms.txt](https://dgtototo.gitbook.io/rendering/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://dgtototo.gitbook.io/rendering/rendering/pa9-more-complexity.md).

# PA9 More Complexity

## 1 这可是弹坑啊：Occluded Areas

在之前的凹凸方案中使用了 bump 进行法线扰动。但是 bump 产生的凹凸仅支持直射光环境下的深度错觉，没有自阴影。这种缺陷在法线贴图显示有小孔、凹痕或裂缝时最为明显，它们无法表达出足够的深度。

为了解决这一问题，这一节准备了 occlusion map。可以将其理解为一个固定的 shadow map，是材质的一部分。

{% tabs %}
{% tab title="Main" %}

<pre class="language-hlsl"><code class="lang-hlsl">Shader "Code/PA9_MoreComplexity/OccludedAreas" {
	Properties {
		_Tint ("Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Albedo", 2D) = "white" {}

		[NoScaleOffset] _NormalMap ("Normals", 2D) = "bump" {}
		_BumpScale ("Bump Scale", Float) = 1

		[NoScaleOffset] _MetallicMap ("Metallic", 2D) = "white" {}
		[Gamma] _Metallic ("Metallic", Range(0, 1)) = 0
		_Smoothness ("Smoothness", Range(0, 1)) = 0.1

<strong>		[NoScaleOffset] _OcclusionMap ("Occlusion", 2D) = "white" {}
</strong><strong>		_OcclusionStrength("Occlusion Strength", Range(0, 1)) = 1
</strong>
		[NoScaleOffset] _EmissionMap ("Emission", 2D) = "black" {}
		_Emission ("Emission", Color) = (0, 0, 0)

		_DetailTex ("Detail Albedo", 2D) = "gray" {}
		[NoScaleOffset] _DetailNormalMap ("Detail Normals", 2D) = "bump" {}
		_DetailBumpScale ("Detail Bump Scale", Float) = 1
	}

	CGINCLUDE
	#define BINORMAL_PER_FRAGMENT
	ENDCG

	SubShader {
		Pass {
			Tags {
				"LightMode" = "ForwardBase"
			}
			CGPROGRAM
			#pragma target 3.0
			#pragma shader_feature _METALLIC_MAP
			#pragma shader_feature _ _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC
<strong>			#pragma shader_feature _OCCLUSION_MAP // 仅应用于 base pase
</strong>			#pragma shader_feature _EMISSION_MAP
			#pragma multi_compile _ SHADOWS_SCREEN
			#pragma multi_compile _ VERTEXLIGHT_ON
			#pragma vertex Vert
			#pragma fragment Frag
			#define FORWARD_BASE_PASS
			#include "OccludedAreas_Light.cginc"
			ENDCG
		}

		...
	}

	CustomEditor "OccludedAreas_GUI"
}
</code></pre>

{% endtab %}

{% tab title="Light" %}

<pre class="language-hlsl"><code class="lang-hlsl">#if !defined(MORECOMPLEXITY_OCCLUDEDAREAS_LIGHT)
#define MORECOMPLEXITY_OCCLUDEDAREAS_LIGHT

#include "UnityPBSLighting.cginc"
#include "AutoLight.cginc"

float4 _Tint;
sampler2D _MainTex, _DetailTex;
float4 _MainTex_ST, _DetailTex_ST;

sampler2D _NormalMap, _DetailNormalMap;
float _BumpScale, _DetailBumpScale;

sampler2D _MetallicMap;
float _Metallic;
float _Smoothness;

<strong>sampler2D _OcclusionMap;
</strong><strong>float _OcclusionStrength;
</strong>
sampler2D _EmissionMap;
float3 _Emission;

...

<strong>float GetOcclusion(Interpolators i) {
</strong><strong>	#if defined(_OCCLUSION_MAP)
</strong><strong>		//返回 1 和 map 之间的插值（系数由 slider _OcclusionStrength 设定）
</strong><strong>		return lerp(1, tex2D(_OcclusionMap, i.uv.xy).g, _OcclusionStrength);
</strong><strong>	#else
</strong><strong>		return 1;
</strong><strong>	#endif
</strong><strong>}
</strong>
UnityIndirect CreateIndirectLight(Interpolators i, float3 viewDir) {
	UnityIndirect indirectLight;
	indirectLight.diffuse = 0;
	indirectLight.specular = 0;

	#if defined(VERTEXLIGHT_ON)
		indirectLight.diffuse = i.vertexLightColor;
	#endif

	#if defined(FORWARD_BASE_PASS)
		
		...

<strong>		float occlusion = GetOcclusion(i);
</strong><strong>		indirectLight.diffuse *= occlusion;
</strong><strong>		indirectLight.specular *= occlusion;
</strong>	#endif

	return indirectLight;
}

...

#endif
</code></pre>

{% endtab %}

{% tab title="GUI" %}

<pre class="language-csharp"><code class="lang-csharp">using UnityEngine;
using UnityEditor;

public class OccludedAreas_GUI : ShaderGUI {
	enum SmoothnessSource {
		Uniform, Albedo, Metallic
	}
	static GUIContent staticLabel = new GUIContent();
	Material target;
	MaterialEditor editor;
	MaterialProperty[] properties;

	public override void OnGUI(
		MaterialEditor editor, MaterialProperty[] properties
	) {
		...
	}

	void DoMain() {
		GUILayout.Label("Main Maps", EditorStyles.boldLabel);
		MaterialProperty mainTex = FindProperty("_MainTex");
		editor.TexturePropertySingleLine(MakeLabel(mainTex, "Albedo(RGB)"), mainTex, FindProperty("_Tint"));
		DoMetallic();
		DoSmoothness();
		DoNormals();
<strong>		DoOcclusion();
</strong>		DoEmission();
		editor.TextureScaleOffsetProperty(mainTex);
	}
	
	...

<strong>	void DoOcclusion() {
</strong><strong>		MaterialProperty map = FindProperty("_OcclusionMap");
</strong><strong>		EditorGUI.BeginChangeCheck();
</strong><strong>		editor.TexturePropertySingleLine(
</strong><strong>			MakeLabel(map, "Occlusion (G)"), map,
</strong><strong>			map.textureValue ? FindProperty("_OcclusionStrength") : null
</strong><strong>		);
</strong><strong>		if (EditorGUI.EndChangeCheck()) {
</strong><strong>			SetKeyword("_OCCLUSION_MAP", map.textureValue);
</strong><strong>		}
</strong><strong>	}
</strong>
	...

}
</code></pre>

{% endtab %}
{% endtabs %}

<details>

<summary>为什么 Occlusion 选择使用 G 通道存储信息？</summary>

Occlusion map 默认单通道存储。

一般 A 通道用于存储透明度信息，而 RGB 三个通道中，因为人眼对绿色更敏感，所以在常用的纹理压缩格式中，绿色通道通常分配更多比特数，提供更高的存储精度。因此最终选择 G 通道。

</details>

<details>

<summary>为什么将 occlusion map 的影响应用在 indirect light 上？</summary>

也可以应用在 direct light 上，将其与 attenuation 相乘：

<pre class="language-hlsl"><code class="lang-hlsl">UnityLight CreateLight (Interpolators i) {
    ...
    UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
<strong>    attenuation *= GetOcclusion(i);
</strong>    light.color = _LightColor0.rgb * attenuation;
    light.ndotl = DotClamped(i.normal, light.dir);
    return light;
}
</code></pre>

这时可以观察到弹坑深了许多，但总体上并不明显。

因为 occlusion map 只基于表面形状而非特定光线（indirect / direct 都可），而同时应用于两个光线时，看起来又效果过强了。考虑到这个场景中很多光是间接光，所以最终选择仅应用在 indirect light 上。

</details>

## 2 Mask 技术它终于来了：Masking Details

实现一个 mask 效果，使得弹坑仅影响电路板，不会覆盖金属部分。

Unity standard shader 使用 alpha 通道存储 detail mask。

将 fragment shader 中的 albedo 提取为单独的函数，使其不仅仅将 albedo 和 detail 值简单相乘，而是将 GetDetailMask 函数返回值作为插值。

与此同时修改 InitializeFragmentNormal 函数。这里将法线向上方向（0，0，1）作为未修改的值。

{% tabs %}
{% tab title="Main" %}

<pre class="language-hlsl"><code class="lang-hlsl">Shader "Code/PA9_MoreComplexity/MaskingDetails" {

	Properties {
		
		...

<strong>		[NoScaleOffset] _DetailMask ("Detail Mask", 2D) = "white" {}
</strong>		_DetailTex ("Detail Albedo", 2D) = "gray" {}
		[NoScaleOffset] _DetailNormalMap ("Detail Normals", 2D) = "bump" {}
		_DetailBumpScale ("Detail Bump Scale", Float) = 1
	}

	CGINCLUDE
	#define BINORMAL_PER_FRAGMENT
	ENDCG

	SubShader {
		Pass {
			Tags {
				"LightMode" = "ForwardBase"
			}
			CGPROGRAM
			#pragma target 3.0
			#pragma shader_feature _METALLIC_MAP
			#pragma shader_feature _ _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC
			#pragma shader_feature _OCCLUSION_MAP
			#pragma shader_feature _EMISSION_MAP
<strong>			#pragma shader_feature _DETAIL_MASK
</strong>			#pragma multi_compile _ SHADOWS_SCREEN
			#pragma multi_compile _ VERTEXLIGHT_ON
			#pragma vertex Vert
			#pragma fragment Frag
			#define FORWARD_BASE_PASS
			#include "MaskingDetails_Light.cginc"
			ENDCG
		}

		Pass {
			Tags {
				"LightMode" = "ForwardAdd"
			}
			Blend One One
			ZWrite Off
			CGPROGRAM
			#pragma target 3.0
			#pragma shader_feature _METALLIC_MAP
			#pragma shader_feature _ _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC
<strong>			#pragma shader_feature _DETAIL_MASK
</strong>			#pragma multi_compile_fwdadd_fullshadows
			#pragma vertex Vert
			#pragma fragment Frag
			#include "MaskingDetails_Light.cginc"
			ENDCG
		}
		
		...
	}

	CustomEditor "MaskingDetails_GUI"
}
</code></pre>

{% endtab %}

{% tab title="Light" %}

<pre class="language-hlsl"><code class="lang-hlsl">#if !defined(MORECOMPLEXITY_MASKINGDETAILS_LIGHT)
#define MORECOMPLEXITY_MASKINGDETAILS_LIGHT

#include "UnityPBSLighting.cginc"
#include "AutoLight.cginc"

float4 _Tint;
<strong>sampler2D _MainTex, _DetailTex, _DetailMask;
</strong>float4 _MainTex_ST, _DetailTex_ST;

...

<strong>float GetDetailMask(Interpolators i) {
</strong><strong>	#if defined (_DETAIL_MASK)
</strong><strong>		return tex2D(_DetailMask, i.uv.xy).a;
</strong><strong>	#else
</strong><strong>		return 1;
</strong><strong>	#endif
</strong><strong>}
</strong><strong>
</strong><strong>float3 GetAlbedo(Interpolators i) {
</strong><strong>	float3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Tint.rgb;
</strong><strong>	float3 details = tex2D(_DetailTex, i.uv.zw) * unity_ColorSpaceDouble;
</strong><strong>	albedo = lerp(albedo, albedo * details, GetDetailMask(i));
</strong><strong>	return albedo;
</strong><strong>}
</strong>
...

void InitializeFragmentNormal(inout Interpolators i) {
	float3 mainNormal =
		UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale);
	float3 detailNormal =
		UnpackScaleNormal(tex2D(_DetailNormalMap, i.uv.zw), _DetailBumpScale);
<strong>	detailNormal = lerp(float3(0, 0, 1), detailNormal, GetDetailMask(i));
</strong>	float3 tangentSpaceNormal = BlendNormals(mainNormal, detailNormal);

	#if defined(BINORMAL_PER_FRAGMENT)
		float3 binormal = CreateBinormal(i.normal, i.tangent.xyz, i.tangent.w);
	#else
		float3 binormal = i.binormal;
	#endif
	
	i.normal = normalize(
		tangentSpaceNormal.x * i.tangent +
		tangentSpaceNormal.y * binormal +
		tangentSpaceNormal.z * i.normal
	);
}

float4 Frag(Interpolators i) : SV_TARGET {
	InitializeFragmentNormal(i);

	float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);

	// float3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Tint.rgb;
	// albedo *= tex2D(_DetailTex, i.uv.zw) * unity_ColorSpaceDouble;
	
	float3 specularTint;
	float oneMinusReflectivity;
<strong>	<a data-footnote-ref href="#user-content-fn-1">float3 albedo</a> = DiffuseAndSpecularFromMetallic(
</strong><strong>		<a data-footnote-ref href="#user-content-fn-2">GetAlbedo(i)</a>, GetMetallic(i), specularTint, oneMinusReflectivity
</strong><strong>	);
</strong>
	float4 color = UNITY_BRDF_PBS(
		albedo, specularTint,
		oneMinusReflectivity, GetSmoothness(i),
		i.normal, viewDir,
		CreateLight(i), CreateIndirectLight(i, viewDir)
	);
	color.rgb += GetEmission(i);
	return color;
}

#endif
</code></pre>

{% endtab %}

{% tab title="GUI" %}

<pre class="language-csharp"><code class="lang-csharp">using UnityEngine;
using UnityEditor;

public class MaskingDetails_GUI: ShaderGUI {
	enum SmoothnessSource {
		Uniform, Albedo, Metallic
	}
	static GUIContent staticLabel = new GUIContent();
	Material target;
	MaterialEditor editor;
	MaterialProperty[] properties;

	void DoMain() {
		GUILayout.Label("Main Maps", EditorStyles.boldLabel);
		MaterialProperty mainTex = FindProperty("_MainTex");
		editor.TexturePropertySingleLine(MakeLabel(mainTex, "Albedo(RGB)"), mainTex, FindProperty("_Tint"));
		DoMetallic();
		DoSmoothness();
		DoNormals();
		DoOcclusion();
		DoEmission();
<strong>		DoDetailMask();
</strong>		editor.TextureScaleOffsetProperty(mainTex);
	}
	
	...

<strong>	void DoDetailMask() {
</strong><strong>		MaterialProperty mask = FindProperty("_DetailMask");
</strong><strong>		EditorGUI.BeginChangeCheck();
</strong><strong>		editor.TexturePropertySingleLine(
</strong><strong>			MakeLabel(mask, "Detail Mask (A)"), mask
</strong><strong>		);
</strong><strong>		if (EditorGUI.EndChangeCheck()) {
</strong><strong>			SetKeyword("_DETAIL_MASK", mask.textureValue);
</strong><strong>		}
</strong><strong>	}
</strong>
	...
}
</code></pre>

{% endtab %}
{% endtabs %}

## 3 更多操控：More Keywords

没啥好说的。

1. 动态管理关键字（Main + GUI 代码）；

{% tabs %}
{% tab title="Main" %}

<pre class="language-hlsl"><code class="lang-hlsl">Shader "Code/PA9_MoreComplexity/MoreKeywords" {

	Properties {
		...
	}

	CGINCLUDE
	#define BINORMAL_PER_FRAGMENT
	ENDCG

	SubShader {
		Pass {
			Tags {
				"LightMode" = "ForwardBase"
			}
			CGPROGRAM
			#pragma target 3.0
			#pragma shader_feature _METALLIC_MAP
			#pragma shader_feature _ _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC
<strong>			#pragma shader_feature _NORMAL_MAP
</strong>			#pragma shader_feature _OCCLUSION_MAP
			#pragma shader_feature _EMISSION_MAP
			#pragma shader_feature _DETAIL_MASK
<strong>			#pragma shader_feature _DETAIL_ALBEDO_MAP
</strong><strong>			#pragma shader_feature _DETAIL_NORMAL_MAP
</strong>			#pragma multi_compile _ SHADOWS_SCREEN
			#pragma multi_compile _ VERTEXLIGHT_ON
			#pragma vertex Vert
			#pragma fragment Frag
			#define FORWARD_BASE_PASS
			#include "MoreKeywords_Light.cginc"
			ENDCG
		}

		Pass {
			Tags {
				"LightMode" = "ForwardAdd"
			}
			Blend One One
			ZWrite Off
			CGPROGRAM
			#pragma target 3.0
			#pragma shader_feature _METALLIC_MAP
			#pragma shader_feature _ _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC
<strong>			#pragma shader_feature _NORMAL_MAP
</strong>			#pragma shader_feature _DETAIL_MASK
<strong>			#pragma shader_feature _DETAIL_ALBEDO_MAP
</strong><strong>			#pragma shader_feature _DETAIL_NORMAL_MAP
</strong>			#pragma multi_compile_fwdadd_fullshadows
			#pragma vertex Vert
			#pragma fragment Frag
			#include "MoreKeywords_Light.cginc"
			ENDCG
		}

		...
		
	}

	CustomEditor "MoreKeywords_GUI"
}
</code></pre>

{% endtab %}

{% tab title="Light" %}

<pre class="language-hlsl"><code class="lang-hlsl">#if !defined(MORECOMPLEXITY_MOREKEYWORDS_LIGHT)
#define MORECOMPLEXITY_MOREKEYWORDS_LIGHT

#include "UnityPBSLighting.cginc"
#include "AutoLight.cginc"

...

float3 GetAlbedo(Interpolators i) {
	float3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Tint.rgb;
<strong>	#if defined (_DETAIL_ALBEDO_MAP)
</strong>		float3 details = tex2D(_DetailTex, i.uv.zw) * unity_ColorSpaceDouble;
		albedo = lerp(albedo, albedo * details, GetDetailMask(i));
<strong>	#endif
</strong>	return albedo;
}

<strong>float3 GetTangentSpaceNormal(Interpolators i) {
</strong><strong>	float3 normal = float3(0, 0, 1);
</strong><strong>	#if defined(_NORMAL_MAP)
</strong><strong>		normal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale);
</strong><strong>	#endif
</strong><strong>	#if defined(_DETAIL_NORMAL_MAP)
</strong><strong>		float3 detailNormal = UnpackScaleNormal(tex2D(_DetailNormalMap, i.uv.zw), _DetailBumpScale);
</strong><strong>		detailNormal = lerp(float3(0, 0, 1), detailNormal, GetDetailMask(i));
</strong><strong>		normal = BlendNormals(normal, detailNormal);
</strong><strong>	#endif
</strong><strong>	return normal;
</strong><strong>}
</strong>
...

void InitializeFragmentNormal(inout Interpolators i) {
	// float3 mainNormal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale);
	// float3 detailNormal = UnpackScaleNormal(tex2D(_DetailNormalMap, i.uv.zw), _DetailBumpScale);
	// detailNormal = lerp(float3(0, 0, 1), detailNormal, GetDetailMask(i));
	// float3 tangentSpaceNormal = BlendNormals(mainNormal, detailNormal);
<strong>	float3 tangentSpaceNormal = GetTangentSpaceNormal(i);
</strong>	#if defined(BINORMAL_PER_FRAGMENT)
		float3 binormal = CreateBinormal(i.normal, i.tangent.xyz, i.tangent.w);
	#else
		float3 binormal = i.binormal;
	#endif
	
	i.normal = normalize(
		tangentSpaceNormal.x * i.tangent +
		tangentSpaceNormal.y * binormal +
		tangentSpaceNormal.z * i.normal
	);
}

...

#endif
</code></pre>

{% endtab %}

{% tab title="GUI" %}

<pre class="language-csharp"><code class="lang-csharp">using UnityEngine;
using UnityEditor;

public class MoreKeywords_GUI : ShaderGUI {
	
	...

	void DoNormals() {
		MaterialProperty map = FindProperty("_NormalMap");
<strong>		EditorGUI.BeginChangeCheck();
</strong>		editor.TexturePropertySingleLine(MakeLabel(map), map, map.textureValue ? FindProperty("_BumpScale") : null);
<strong>		if (EditorGUI.EndChangeCheck()) {
</strong><strong>			SetKeyword("_NORMAL_MAP", map.textureValue);
</strong><strong>		}
</strong>	}
	
	...

	void DoSecondary() {
		GUILayout.Label("Secondary Maps", EditorStyles.boldLabel);
		MaterialProperty detailTex = FindProperty("_DetailTex");
<strong>		EditorGUI.BeginChangeCheck();
</strong>		editor.TexturePropertySingleLine(MakeLabel(detailTex, "Albedo(RGB) multiplied by 2"), detailTex);
<strong>		if (EditorGUI.EndChangeCheck()) {
</strong><strong>			SetKeyword("_DETAIL_ALBEDO_MAP", detailTex.textureValue);
</strong><strong>		}
</strong>		DoSecondaryNormals();
		editor.TextureScaleOffsetProperty(detailTex);
	}

	void DoSecondaryNormals() {
		MaterialProperty map = FindProperty("_DetailNormalMap");
<strong>		EditorGUI.BeginChangeCheck();
</strong>		editor.TexturePropertySingleLine(MakeLabel(map), map, map.textureValue ? FindProperty("_DetailBumpScale") : null);
<strong>		if (EditorGUI.EndChangeCheck()) {
</strong><strong>			SetKeyword("_DETAIL_NORMAL_MAP", map.textureValue);
</strong><strong>		}
</strong>	}

	...
}
</code></pre>

{% endtab %}
{% endtabs %}

## 4 一 shader 多用：Editing Multiple Materials

在使用一个 shader 编辑多个材质时，主要考虑解决两个冲突问题。

**关键字过少：“选不到” 的问题**

在编辑多个材质时，如果材质 GUI 仅设置了选中材质中的第一个材质的关键字（`editor.target`），其他材质的关键字可能无法正确更新。

因此在 SetKeyword 的函数了使用 foreach 循环，遍历以确保所有材质应用更新后的关键字。

**关键字过多：“连坐” 问题**

当选中的多个材质之间的属性不一致，可能会导致错误。比如第一个材质使用了 normal map，但第二个材质没有。如果通过 GUI 修改了 normal map 相关的属性，可能会错误地为第二个材质启用了 normal map 的关键字。

为解决这个问题，需要在修改属性之前记录贴图的当前引用值。此时仅在贴图值真正发生改变时，才更新关键字。

{% hint style="info" %}
这里仅以 `DoNormals` 为例。需将同样的方法用在 `DoMetallic`，`DoOcclusion`，`DoEmission` 和 `DoSecondaryNormals` 函数中。
{% endhint %}

<pre class="language-csharp"><code class="lang-csharp">void DoNormals() {
	MaterialProperty map = FindProperty("_NormalMap");
<strong>	Texture tex = map.textureValue;
</strong>	EditorGUI.BeginChangeCheck();
<strong>	editor.TexturePropertySingleLine(MakeLabel(map), map, <a data-footnote-ref href="#user-content-fn-3">tex</a> ? FindProperty("_BumpScale") : null);
</strong><strong>	if(EditorGUI.EndChangeCheck() &#x26;&#x26; tex != map.textureValue) {
</strong>		SetKeyword("_NORMAL_MAP", map.textureValue);
	}
}

...

void SetKeyword(string keyword, bool state) {
	if(state) {
<strong>		foreach(Material m in editor.targets) {
</strong><strong>			m.EnableKeyword(keyword);
</strong>		}
	} else {
<strong>		foreach(Material m in editor.targets) {
</strong><strong>			m.DisableKeyword(keyword);
</strong>		}
	}
}
</code></pre>

***

## APPENDIX

**Anki**

{% file src="/files/RnNTppH291SowBvIe2UP" %}

**Reference**

[Catlike Coding - Rendering 10: More Complexity](https://catlikecoding.com/unity/tutorials/rendering/part-10/)

[^1]: pre: albedo (此前 albedo 来自 \_MainTex 和 \_DetailTex 的采样，写在 Frag shader 内)

[^2]: pre: albedo

[^3]: pre: map.textureValue
