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"
}
#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;
sampler2D _OcclusionMap;
float _OcclusionStrength;
sampler2D _EmissionMap;
float3 _Emission;
...
float GetOcclusion(Interpolators i) {
#if defined(_OCCLUSION_MAP)
//返回 1 和 map 之间的插值(系数由 slider _OcclusionStrength 设定)
return lerp(1, tex2D(_OcclusionMap, i.uv.xy).g, _OcclusionStrength);
#else
return 1;
#endif
}
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)
...
float occlusion = GetOcclusion(i);
indirectLight.diffuse *= occlusion;
indirectLight.specular *= occlusion;
#endif
return indirectLight;
}
...
#endif
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();
DoOcclusion();
DoEmission();
editor.TextureScaleOffsetProperty(mainTex);
}
...
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);
}
}
...
}
为什么 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"
}
#if !defined(MORECOMPLEXITY_MASKINGDETAILS_LIGHT)
#define MORECOMPLEXITY_MASKINGDETAILS_LIGHT
#include "UnityPBSLighting.cginc"
#include "AutoLight.cginc"
float4 _Tint;
sampler2D _MainTex, _DetailTex, _DetailMask;
float4 _MainTex_ST, _DetailTex_ST;
...
float GetDetailMask(Interpolators i) {
#if defined (_DETAIL_MASK)
return tex2D(_DetailMask, i.uv.xy).a;
#else
return 1;
#endif
}
float3 GetAlbedo(Interpolators i) {
float3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Tint.rgb;
float3 details = tex2D(_DetailTex, i.uv.zw) * unity_ColorSpaceDouble;
albedo = lerp(albedo, albedo * details, GetDetailMask(i));
return albedo;
}
...
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);
#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;
= DiffuseAndSpecularFromMetallic(
, GetMetallic(i), specularTint, oneMinusReflectivity
);
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
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();
DoDetailMask();
editor.TextureScaleOffsetProperty(mainTex);
}
...
void DoDetailMask() {
MaterialProperty mask = FindProperty("_DetailMask");
EditorGUI.BeginChangeCheck();
editor.TexturePropertySingleLine(
MakeLabel(mask, "Detail Mask (A)"), mask
);
if (EditorGUI.EndChangeCheck()) {
SetKeyword("_DETAIL_MASK", mask.textureValue);
}
}
...
}
3 更多操控:More Keywords
没啥好说的。
动态管理关键字(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"
}
#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;
#if defined (_DETAIL_ALBEDO_MAP)
float3 details = tex2D(_DetailTex, i.uv.zw) * unity_ColorSpaceDouble;
albedo = lerp(albedo, albedo * details, GetDetailMask(i));
#endif
return albedo;
}
float3 GetTangentSpaceNormal(Interpolators i) {
float3 normal = float3(0, 0, 1);
#if defined(_NORMAL_MAP)
normal = UnpackScaleNormal(tex2D(_NormalMap, i.uv.xy), _BumpScale);
#endif
#if defined(_DETAIL_NORMAL_MAP)
float3 detailNormal = UnpackScaleNormal(tex2D(_DetailNormalMap, i.uv.zw), _DetailBumpScale);
detailNormal = lerp(float3(0, 0, 1), detailNormal, GetDetailMask(i));
normal = BlendNormals(normal, detailNormal);
#endif
return normal;
}
...
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);
float3 tangentSpaceNormal = GetTangentSpaceNormal(i);
#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
using UnityEngine;
using UnityEditor;
public class MoreKeywords_GUI : ShaderGUI {
...
void DoNormals() {
MaterialProperty map = FindProperty("_NormalMap");
EditorGUI.BeginChangeCheck();
editor.TexturePropertySingleLine(MakeLabel(map), map, map.textureValue ? FindProperty("_BumpScale") : null);
if (EditorGUI.EndChangeCheck()) {
SetKeyword("_NORMAL_MAP", map.textureValue);
}
}
...
void DoSecondary() {
GUILayout.Label("Secondary Maps", EditorStyles.boldLabel);
MaterialProperty detailTex = FindProperty("_DetailTex");
EditorGUI.BeginChangeCheck();
editor.TexturePropertySingleLine(MakeLabel(detailTex, "Albedo(RGB) multiplied by 2"), detailTex);
if (EditorGUI.EndChangeCheck()) {
SetKeyword("_DETAIL_ALBEDO_MAP", detailTex.textureValue);
}
DoSecondaryNormals();
editor.TextureScaleOffsetProperty(detailTex);
}
void DoSecondaryNormals() {
MaterialProperty map = FindProperty("_DetailNormalMap");
EditorGUI.BeginChangeCheck();
editor.TexturePropertySingleLine(MakeLabel(map), map, map.textureValue ? FindProperty("_DetailBumpScale") : null);
if (EditorGUI.EndChangeCheck()) {
SetKeyword("_DETAIL_NORMAL_MAP", map.textureValue);
}
}
...
}
4 一 shader 多用:Editing Multiple Materials
在使用一个 shader 编辑多个材质时,主要考虑解决两个冲突问题。
关键字过少:“选不到” 的问题
在编辑多个材质时,如果材质 GUI 仅设置了选中材质中的第一个材质的关键字(editor.target
),其他材质的关键字可能无法正确更新。
因此在 SetKeyword 的函数了使用 foreach 循环,遍历以确保所有材质应用更新后的关键字。
关键字过多:“连坐” 问题
当选中的多个材质之间的属性不一致,可能会导致错误。比如第一个材质使用了 normal map,但第二个材质没有。如果通过 GUI 修改了 normal map 相关的属性,可能会错误地为第二个材质启用了 normal map 的关键字。
为解决这个问题,需要在修改属性之前记录贴图的当前引用值。此时仅在贴图值真正发生改变时,才更新关键字。
这里仅以 DoNormals
为例。需将同样的方法用在 DoMetallic
,DoOcclusion
,DoEmission
和 DoSecondaryNormals
函数中。
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
Last updated