PA8 Complex Materials

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

1 复刻编辑器:User Interface

当贴图、功能语法丰富起来,使用默认的 inspector 管理和拓展这些数值就显得尤为不便。此时,就需要自己做编辑器扩展了。

Unity 默认 shader 的 inspector 看起来就是个很好的 mimicking 对象。本章的任务就是将其完美复刻。

1.1 制作 GUI 的核心

Drawing

本体:Material

Material 是 Unity 中表示材质的核心类,万物之源。它代表了一个渲染对象的材质,而这个材质可以具备各种属性。

显示屏:MaterialEditor

有 MaterialEditor 实例 editor:

MaterialEditor 方法
功能
样式

editor.TexturePropertySingleLine

材质选择器 (可附加滑块或颜色选择器)

editor.TextureScaleOffsetProperty

Scale & Offset 输入框

editor.ShaderProperty

滑块 / 数值控件

editor.ColorProperty

颜色选择器

数据库:MaterialProperty

显示一切的 MaterialEditor 需要 MaterialProperty 类来寻找和操作 shader 中的各个属性。

1.2 CODING

Shader "Code/P8_ComplexMaterials/UserInterface" {
	Properties {
		_Tint ("Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Albedo", 2D) = "white" {}
		[NoScaleOffset] _NormalMap ("Normals", 2D) = "bump" {}
		_BumpScale ("Bump Scale", Float) = 1
		[Gamma] _Metallic ("Metallic", Range(0, 1)) = 0
		_Smoothness ("Smoothness", Range(0, 1)) = 0.1
		_DetailTex ("Detail Albedo", 2D) = "gray" {}
		[NoScaleOffset] _DetailNormalMap ("Detail Normals", 2D) = "bump" {}
		_DetailBumpScale ("Detail Bump Scale", Float) = 1
	}
	
	CGINCLUDE
	#define BINORMAL_PER_FRAGMENT
	ENDCG

	SubShader {
		...
	}

	// to use a custom GUI, have to add the CustomEditor directive to a shader
	CustomEditor "UserInterface_GUI"
}
如何理解 OnGUI 方法?

在 Unity 编辑器中选中一个材质时,Unity 会检查材质的 Shader 是否有自定义的 ShaderGUI 类。如果有,Unity 会在渲染材质属性面板时调用 ShaderGUIOnGUI 方法。

ShaderGUI.OnGUI,是 Unity 和 ShaderGUI 之间的约定接口(一种 “钩子函数” 机制)。Unity 并不知道 OnGUI 内部写了什么逻辑,而是仅负责调用 OnGUI,然后交由其实现具体的渲染逻辑(比如 DoMain 等函数)。

因此可以说,OnGUI 是整个 inspector 撰写的核心。

如何理解 GUIContent 类?

在 Unity 中,GUIContent 是一个专门用来表示 GUI 控件内容的数据类型,主要作用是为 Unity 的 GUI 系统提供统一的内容表示方式,便于控件的渲染和交互。

其主要成员有,text、image(控件显示的图像,通常是一个 texture) 和 tooltip(鼠标悬停时的提示信息)。

为什么将 GUIContent 声明为 static?

声明为 static 的对象属于类本身,而不是方法的每次调用。这意味着无论调用多少次 MakeLabel,都会复用同一个 staticLabel 实例,而不是每次调用都创建新的 GUIContent 实例。

在 Unity 的编辑器扩展中,GUI 方法通常会被频繁调用,比如在 OnGUIOnInspectorGUI 中,每帧都会多次调用这些方法。

使用 static 对象避免了频繁的对象创建和销毁,通过复用同一个对象显著减少了性能开销。

如何理解 GUIGUILayoutEditorGUIEditorGUILayout 类?

通用 GUI 工具

  • GUI:通用的 GUI 布局工具;

  • GUILayout:GUI 的自动布局版本;

编辑器 GUI 工具

  • EditorGUI:专用于 Editor 界面的布局工具;

  • EditorGUILayoutEditorGUI 的自动布局版本。

其中,GUIGUILayoutEditorGUIEditorGUILayout 的方法高度重叠,功能基本一致。只是无 Layout 的版本因为需要手动布局,所以使用时往往需要手动指定 Rect


GUI

教程中暂时没有使用这个类。

GUILayout

在教程中,GUILayout 主要使用其 Label 方法生成标签控件。

GUILayout.Label("This is a Label");

EditorGUI

在教程中,EditorGUI 主要使用其 BeginChangeCheck / EndChangeCheck 方法和 indentLevel 方法。

// 触发检测
EditorGUI.BeginChangeCheck();
...
if(EditorGUI.EndChangeCheck()) {
    ...
}

// indentLevel 方法
EditorGUI.indentLevel += 2;

EditorGUILayout

在教程中,EditorGUILayout 是在制作 Smoothness 时使用了其 EnumPopup 方法。

source = (SmoothnessSource)EditorGUILayout.EnumPopup(MakeLabel(slider), source);

2 混合的另一种可能性:Metal and Nonmetal

现在,材质中金属与非金属程度的控制是全局的(依靠一条 slider)。如果想创建更复杂的纹理,可以通过添加一个 metallic map,实现一个材质中金属与非金属部分更精细的混合控制。

Drawing

2.1 复杂的想法 Sophisticated Idea

STEP 1: 添加 Metallic Map

  1. Main:Properties 添加材质

  2. Light:添加相应变量

STEP 2: 调整 metallic 逻辑

  1. Main:添加对于 Material 的 shader feature 指令

  2. Light:GetMetallic 函数取得 Metallic 值 -> Frag shader 中调用 GetMetallic 函数作为能量守恒函数 DiffuseAndSpecularFromMetallic 的参数

  3. GUI:添加 metallic map 材质选取窗口 -> 调整 map 和 slider 逻辑为 “OR”

STEP 3: 设置 GUI 切换 keyword

  1. 声明 Material 类型对象 target -> 将其赋值为 MaterialEditor.target 方法获取的材质

  2. SetKeyword 函数设置 keyword 启用状态 -> 在 DoMetallic 函数中使用 ChangeCheck 监控调用 SetKeyword 函数

2.2 CODING

Shader "Code/PA8_ComplexMaterials/MixMetalAndNonMetal" {
	Properties {
		_Tint ("Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Albedo", 2D) = "white" {}

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

		// STEP 1: 添加 Metallic Map
		[NoScaleOffset] _MetallicMap ("Metallic", 2D) = "white" {}
		[Gamma] _Metallic ("Metallic", Range(0, 1)) = 0
		_Smoothness ("Smoothness", Range(0, 1)) = 0.1

		_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
			// STEP2: 添加对于 Material 的 shader feature 指令
			#pragma shader_feature _METALLIC_MAP
			#pragma multi_compile _ SHADOWS_SCREEN
			#pragma multi_compile _ VERTEXLIGHT_ON
			#pragma vertex Vert
			#pragma fragment Frag
			#define FORWARD_BASE_PASS
			#include "MixMetalAndNonMetal_Light.cginc"
			ENDCG
		}

		Pass {
			Tags {
				"LightMode" = "ForwardAdd"
			}
			Blend One One
			ZWrite Off
			CGPROGRAM
			#pragma target 3.0
			#pragma shader_feature _METALLIC_MAP
			#pragma multi_compile_fwdadd_fullshadows
			#pragma vertex Vert
			#pragma fragment Frag
			#include "MixMetalAndNonMetal_Light.cginc"
			ENDCG
		}

		Pass {
			Tags {
				"LightMode" = "ShadowCaster"
			}
			CGPROGRAM
			#pragma target 3.0
			#pragma multi_compile_shadowcaster
			#pragma vertex ShadowVert
			#pragma fragment ShadowFrag
			#include "ComplexMaterials_Shadow.cginc"
			ENDCG
		}
	}

	CustomEditor "MixMetalAndNonMetal_GUI"
}
如何理解函数 OnGUI 中的语句 this.target = editor.target as Material

as 是一个类型转换运算符,将 editor.target 的类型转换为 Material。

因为 target 是从 Editor 基类继承的,所以它的类型是通用的 Object,因此,需要将它转换成具体的 Material 类型,才能操作材质特有的属性和方法。

如何 debug keywords?

可以通过 debug inspector 确认 keyword 是否在材质上添加或删除。

如果发现有任何意外着色器关键字,都是由于之前分配给材质的着色器而定义的。比如只要选择了新材质,standard shader GUI 就会添加 _EMISSION 关键字。这些关键字对毫无用处,从列表中删除即可。

如何理解语句 #pragma shader_feature _METALLIC_MAP

使用 multi-compile 指令时,Unity 会为所有可能的组合生成 shader variants。所有 variants 都会构建,有些可能会多余。而且如果关键字较多,编译所有排列组合会耗费大量时间。

有一种方法是定义 shader feature(而不是 multi-compile)。shader feature 只会在需要时编译。

此外,如果 shader feature 是单个关键字的 toggle,则可以省略单个下划线:

// #pragma shader_feature _ _METALLIC_MAP
#pragma shader_feature _METALLIC_MAP
如何理解 DoMetallic 中的 EditorGUI.BeginChangeCheckEditorGUI.EndChangeCheck 方法?

使用 EditorGUI.BeginChangeCheckEditorGUI.EndChangeCheck 方法检查是否有更改。第一个方法定义了开始跟踪更改的点,第二个方法标志着结束,并返回是否进行了更改。

通过将 TexturePropertySingleLine 夹在这两个方法之间,可以很容易地检测到 metallic 这行是否已被编辑。检测为 true,则设置关键字。

3 打包策略:Smoothness Maps

smoothness = (albedo.a || metallic.a) * _Smoothness

这一节实现的重点是复刻 Unity 对于 Smoothness 的打包策略。有三种主流存储方式:

由于很多情况下,材质的 metallic 属性和 smoothness 属性高度相关(金属区域比非金属区域更光滑),因此 Metallic Map 和 Smoothness Map 可以组合在一个纹理中,约定俗成是 Metallic 信息使用纹理的 R 通道,Smoothness 信息使用 A 通道;

此外,有些材质是完全非金属的(比如木材、塑料等)。这种情况下不需要 Metallic Map,而 Smoothness 对材质的表现依然重要,因此有两种方式,直接用一个纹理专门表示 Smoothness,或者将 Smoothness 存储在 Albedo Map 的 A 通道。

由于 DXT5 对 RGB 和 A 通道分别进行了压缩,因此将贴图合并到一个 DXT5 纹理中产生的质量与使用两个 DXT1 纹理产生的质量相同。虽然并不会减少内存需求,但这可以从一个 texture sample(而不是两个)中获取 metallic / albedo 和 smoothness。

此外,虽然 metallic 只需使用 R 通道,但是教程约定 RGB 通道都用于存储 metallic 值。

3.1 粗暴的想法 Rough Idea

Drawing

STEP 1: 添加 Smoothness map

  1. Main:添加 keywords

  2. Light:GetSmoothness 函数读取存储源中的 alpha 值 -> 在 BRDF 函数和 CreateIndirectLight 函数中将 GetSmoothness 返回值作为参数

STEP 2: 创建 smoothness 存储源选择

  1. 创建 enum 展示存储源选项 -> 使用 IsKeywordEnabled 函数检测 keyword -> DoSmoothness 函数使用当前 smoothness 源

  2. 使用 EditorGUILayout.EnumPopup 方法创建 popup list -> 追踪 GUI control 控制并切换 keyword

STEP 3: 支持 Undo 操作

创建 wrapper 函数调用 editor.RegisterPropertyChangeUndo 方法 -> 在 DoSmoothness 函数动作改变前录制原有动作

3.2 CODING

Shader "Code/PA8_ComplexMaterials/SmoothnessMaps" {
	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

		_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
			// STEP 1: 添加 keywords
			#pragma shader_feature _ _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC
			#pragma multi_compile _ SHADOWS_SCREEN
			#pragma multi_compile _ VERTEXLIGHT_ON
			#pragma vertex Vert
			#pragma fragment Frag
			#define FORWARD_BASE_PASS
			#include "SmoothnessMaps_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 multi_compile_fwdadd_fullshadows
			#pragma vertex Vert
			#pragma fragment Frag
			#include "SmoothnessMaps_Light.cginc"
			ENDCG
		}

		Pass {
			Tags {
				"LightMode" = "ShadowCaster"
			}
			CGPROGRAM
			#pragma target 3.0
			#pragma multi_compile_shadowcaster
			#pragma vertex ShadowVert
			#pragma fragment ShadowFrag
			#include "ComplexMaterials_Shadow.cginc"
			ENDCG
		}
	}

	CustomEditor "SmoothnessMaps_GUI"
}
如何理解 GetSmoothness 函数的 return 语句 return smoothness * _Smoothness

Unity 的 standard shader 提供了两种方式来控制 smoothness:

  1. 独立的 uniform:单一的浮点值,直接控制材质的整体光滑度(不依赖纹理)。在这里是 _Smoothness

  2. modulation scalar:一个比例因子,用来调节 smoothness map 的值(如果有的话)。在这里是 smoothness

因此代码逻辑为,没有 smoothness map 的情况下,只使用 _Smoothness ,否则可以使用 smoothness 调整贴图的效果,然后使用 _Smoothness 增强或减弱整体光滑度。

4 放射光源:Emissive

此前所有的光线都是通过反射 light 得到的,除此之外还有一些材质可以自发光,比如当它足够热的时候,不需要其他光源就可以看到它。Unity standard shader 将其实现为一张 emission map 和 color。

Shader "Code/PA8_ComplexMaterials/Emissive" {
	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] _EmissionMap ("Emission", 2D) = "black" {} // 默认为 black
		_Emission ("Emission", Color) = (0, 0, 0) // 仅需要 RGB 通道

		_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 _EMISSION_MAP // 仅在 Base Pass 中使用
			#pragma multi_compile _ SHADOWS_SCREEN
			#pragma multi_compile _ VERTEXLIGHT_ON
			#pragma vertex Vert
			#pragma fragment Frag
			#define FORWARD_BASE_PASS
			#include "Emissive_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 multi_compile_fwdadd_fullshadows
			#pragma vertex Vert
			#pragma fragment Frag
			#include "Emissive_Light.cginc"
			ENDCG
		}

		Pass {
			Tags {
				"LightMode" = "ShadowCaster"
			}
			CGPROGRAM
			#pragma target 3.0
			#pragma multi_compile_shadowcaster
			#pragma vertex ShadowVert
			#pragma fragment ShadowFrag
			#include "ComplexMaterials_Shadow.cginc"
			ENDCG
		}
	}

	CustomEditor "Emissive_GUI"
}

教程中在 editor.TexturePropertyWithHDRColor 方法中使用了 ColorPickerHDRConfig 类作为参数,这是一个标记为 Obsolete(过时)的类。在 Unity 的新版 HDR 系统中,ColorPickerHDRConfig 已经很少需要手动设置,Unity 内置的 HDR 功能可以自动适配颜色范围。因此该部分代码已删除。


APPENDIX

Anki

Reference

Catlike Coding - Rendering 9: Complex Materials

Last updated