PA9 More Complexity

Shader 与渲染算法 - 高级属性 - 复杂材质 2

1 这可是弹坑啊:Occluded Areas

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

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

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

		[NoScaleOffset] _OcclusionMap ("Occlusion", 2D) = "white" {}
		_OcclusionStrength("Occlusion Strength", Range(0, 1)) = 1

		[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
			#pragma shader_feature _OCCLUSION_MAP // 仅应用于 base pase
			#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"
}
为什么 Occlusion 选择使用 G 通道存储信息?

Occlusion map 默认单通道存储。

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

为什么将 occlusion map 的影响应用在 indirect light 上?

也可以应用在 direct light 上,将其与 attenuation 相乘:

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;
}

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

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

2 Mask 技术它终于来了:Masking Details

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

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

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

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

Shader "Code/PA9_MoreComplexity/MaskingDetails" {

	Properties {
		
		...

		[NoScaleOffset] _DetailMask ("Detail Mask", 2D) = "white" {}
		_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
			#pragma shader_feature _DETAIL_MASK
			#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
			#pragma shader_feature _DETAIL_MASK
			#pragma multi_compile_fwdadd_fullshadows
			#pragma vertex Vert
			#pragma fragment Frag
			#include "MaskingDetails_Light.cginc"
			ENDCG
		}
		
		...
	}

	CustomEditor "MaskingDetails_GUI"
}

3 更多操控:More Keywords

没啥好说的。

  1. 动态管理关键字(Main + GUI 代码);

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
			#pragma shader_feature _NORMAL_MAP
			#pragma shader_feature _OCCLUSION_MAP
			#pragma shader_feature _EMISSION_MAP
			#pragma shader_feature _DETAIL_MASK
			#pragma shader_feature _DETAIL_ALBEDO_MAP
			#pragma shader_feature _DETAIL_NORMAL_MAP
			#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
			#pragma shader_feature _NORMAL_MAP
			#pragma shader_feature _DETAIL_MASK
			#pragma shader_feature _DETAIL_ALBEDO_MAP
			#pragma shader_feature _DETAIL_NORMAL_MAP
			#pragma multi_compile_fwdadd_fullshadows
			#pragma vertex Vert
			#pragma fragment Frag
			#include "MoreKeywords_Light.cginc"
			ENDCG
		}

		...
		
	}

	CustomEditor "MoreKeywords_GUI"
}

4 一 shader 多用:Editing Multiple Materials

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

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

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

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

关键字过多:“连坐” 问题

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

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

这里仅以 DoNormals 为例。需将同样的方法用在 DoMetallicDoOcclusionDoEmissionDoSecondaryNormals 函数中。

void DoNormals() {
	MaterialProperty map = FindProperty("_NormalMap");
	Texture tex = map.textureValue;
	EditorGUI.BeginChangeCheck();
	editor.TexturePropertySingleLine(MakeLabel(map), map,  ? FindProperty("_BumpScale") : null);
	if(EditorGUI.EndChangeCheck() && tex != map.textureValue) {
		SetKeyword("_NORMAL_MAP", map.textureValue);
	}
}

...

void SetKeyword(string keyword, bool state) {
	if(state) {
		foreach(Material m in editor.targets) {
			m.EnableKeyword(keyword);
		}
	} else {
		foreach(Material m in editor.targets) {
			m.DisableKeyword(keyword);
		}
	}
}

APPENDIX

Anki

Reference

Catlike Coding - Rendering 10: More Complexity

Last updated