From 66e51f72c4de477535b24352c57d26f68c535c81 Mon Sep 17 00:00:00 2001
From: afeng 调试函数(Debugging Function)
目录
No Fog
diff --git a/docs/Unity_Addressabel_Group.html b/docs/Unity_Addressabel_Group.html
index c0b4cd5..8408ef7 100644
--- a/docs/Unity_Addressabel_Group.html
+++ b/docs/Unity_Addressabel_Group.html
@@ -41,6 +41,7 @@
AssetGroup
目录
Addressable Groups
diff --git a/docs/Unity_Advance_Texture.html b/docs/Unity_Advance_Texture.html
index 04d3207..ee12063 100644
--- a/docs/Unity_Advance_Texture.html
+++ b/docs/Unity_Advance_Texture.html
@@ -43,6 +43,7 @@
副切线在哪算合适
目录
RGB Splat贴图
目录
Skipping Shadows
目录
Deferred Pass下的反射
目录
光源结构 Light Structures
目录
LOD Instance
diff --git a/docs/Unity_Matrix_Transform.html b/docs/Unity_Matrix_Transform.html
index f6a1b0c..d4f7735 100644
--- a/docs/Unity_Matrix_Transform.html
+++ b/docs/Unity_Matrix_Transform.html
@@ -43,6 +43,7 @@
透视相机Perspective Camera
diff --git a/docs/Unity_Mix_Lighting.html b/docs/Unity_Mix_Lighting.html
index c95c600..96e2c09 100644
--- a/docs/Unity_Mix_Lighting.html
+++ b/docs/Unity_Mix_Lighting.html
@@ -43,6 +43,7 @@
为烘焙光打阴影
diff --git a/docs/Unity_Multi_Light.html b/docs/Unity_Multi_Light.html
index f7282ab..483f01b 100644
--- a/docs/Unity_Multi_Light.html
+++ b/docs/Unity_Multi_Light.html
@@ -43,6 +43,7 @@
球谐采样Skybox
目录
缩放对象和动态批处理
目录
支持淡入淡出
目录
模拟反射的反弹
diff --git a/docs/Unity_ShaderGUI_Extension_1.html b/docs/Unity_ShaderGUI_Extension_1.html
index 6483616..b495e04 100644
--- a/docs/Unity_ShaderGUI_Extension_1.html
+++ b/docs/Unity_ShaderGUI_Extension_1.html
@@ -43,6 +43,7 @@
HDR Emission
diff --git a/docs/Unity_ShaderGUI_Extension_2.html b/docs/Unity_ShaderGUI_Extension_2.html
index e2bf682..4f70c38 100644
--- a/docs/Unity_ShaderGUI_Extension_2.html
+++ b/docs/Unity_ShaderGUI_Extension_2.html
@@ -43,6 +43,7 @@
Normal Details
目录
Aniso Level
diff --git a/docs/Unity_Shader_Transparent_1.html b/docs/Unity_Shader_Transparent_1.html
index 9313dc1..3833182 100644
--- a/docs/Unity_Shader_Transparent_1.html
+++ b/docs/Unity_Shader_Transparent_1.html
@@ -43,6 +43,7 @@
调节Alpha
diff --git a/docs/Unity_Shader_Transparent_2.html b/docs/Unity_Shader_Transparent_2.html
index 03bb4d1..75ebcc5 100644
--- a/docs/Unity_Shader_Transparent_2.html
+++ b/docs/Unity_Shader_Transparent_2.html
@@ -43,6 +43,7 @@
3.2 合并显示
目录
采样阴影纹理
diff --git a/docs/Unity_Static_Lightting.html b/docs/Unity_Static_Lightting.html
index d4256ee..ac950bb 100644
--- a/docs/Unity_Static_Lightting.html
+++ b/docs/Unity_Static_Lightting.html
@@ -43,6 +43,7 @@
放置光照探针
目录
总结在使用Unity Addressable System过程中遇到的一部分问题,该篇主要记录Addressable在Unity编辑器的各种组织结构,本文Addressable后续简称AA。
+ +打开Unity Window - Package Manger,找到Addressable安装找一个合适的版本: +
+在Package Manager选择一个合适的Addressable版本还要结合Addressable更新log,打开Addressable更新日志
,版本列表展示了Address able对应的Unity版本:
+
Unity这里分了两个分支维护Addressable,其中一个分支是1.21版本对应的Unity版本是2019+,另一个分支是2.0版本对应的Unity版本是2023+: +
+2.0以上版本也可以用在非2023版的引擎,主要还是针对2023+ Unity引擎支持新的API什么的。如果想把2.0版本合到1.21,开一Beyond Compare合并差异也行。
+ +打开Addressable Package Json
,这里列明了每一个Addressable版本对应的Unity版本,结合Addressable更新日志
找到一个Bug最少又符合对Unity版本要求的Addressable版本。
找到合适的Addressable版本后,打开Packages/manifest.json,新增依赖包,比如:”com.unity.addressables”: “1.21.19”。
+ +Addressable最重要的依赖是ScriptableBuildPipeline,SBP这个包要更新到1.21.23或2.1.x以上,旧版SBP对SpriteAtlas图集打包会有冗余。 +打开Packages/manifest.json,强制指定SBP版本”com.unity.scriptablebuildpipeline”: “1.21.23”, +
+安装好了AA之后,会在Assets目录生成以下目录:
+ +目录 | +说明 | +
AddressableAssetsData | +根目录也包含几个重要文件 | +
Android | +打包后生成的平台目录 | +
AssetGroups | +给某资源设置寻址地址并指定Group后的Group描述文件 | +
Schemas | +Group组成的逻辑结构和特性描述文件 | +
AssetGroupTemplates | +生成Group的模版描述文件 | +
DataBuilders | +自定义AssetBundle打包脚本目录 | +
AddressableAssetsData根目录包含的重要文件:
+Profile In Use可以选择系统自带或自定义,其中自定义使用Manager按钮打开Profile Window: +
+Profile主要作用是指定打包地址和加载地址,我新增了BuildPath打包的保存目录和HotUpdate热更资源目录,这两个参数要在Catalog指定。见Catalog
+ +
指定addressables_content_state.bin的保存地址,该文件只在本地构建时有用,可以看看官方workflow
。简化描述我的理解就是打开Groups-Tools-Check Content Update Restrictions窗口,选择上一次保存的addressables_content_state去比对会生成一份catalog和一小撮热更bundle,就是增量更新这些bundle。但是如果玩家本地用户目录的bundle不小心损坏了,或则资源版本回退,直接使用这方案就不好办。
由于我开了手动更新catalog,同时自己维护了一套完整的更新列表,就禁用了这条属性。
+ +自定义证书、下载并发数量、超时等设置
+ +Play mode指定在编辑器是怎么加载资源,本地测试用
+Group是Addressable组织资源的一种容器,必须要把资源加入Group才能参与打包。
+ +++ + +直接说使用结论
+ ++
+- Group越少越好,在创建Group时非常耗时,成百上千个Group可能卡几十分钟甚至更久。
+- Group越少意味着Asset Entry越多,但是没关系,还有label标签可以用。
+- label标签可以做分类划分,具有相同label标签的资源打成一个bundle包。
+
Content Packing & Loading是BundleAssetGroupSchema,定义AssetBundle构建和加载时的一些信息
+ +如果采用创建N个Asset Group方式,然后对Group进行分组打包,这一个Group就要要包含热更Schema和加载Schema,和N个Asset Entry(这取决于该组有多少个address标记资源)。构建分析期间的耗时将会非常严重,截取一个增加Schema耗时图片 +
+每次增加Schema会触发save操作,每次修改Schema内部值后会触发Group Dirty Notify进一步触发遍历所有Group排查引用该schema的Group更新。
+ +Content Update Restriction是ContentUpdateGroupSchema,定义该Asset Group内的资源构建成bundle包时是否可以热更,如果要热更一定要添加
+这两个Schema均继承自AddressableAssetGroupSchema,应该是都要添加到Group内的,这两个文件描述了一个Group构建时所需要的属性。
+ +下一篇讲如何使用 Addressable构建Assetbundle打包。
]]>之前翻译使用过法线贴图将表面不规则感添加到平滑表面。 它会影响照明,但不会影响表面的实际形状。 因此,该效果视差不明显,通过实现法线贴图基于视野深度的幻觉有许多限制。这一篇的目的就是解决该限制。
+ +下面给出许多albedo map 和 normal map差异对比:
+ + +如果没有法线贴图,表面看起来很平坦。 添加法线贴图会使它看起来好像具有不规则的表面。 但是,高度海拔差异看起来不明显。 当从入射视角与表面的夹角越趋于0,高度差越不明显。如果高程差异较大,则表面特征的相对视觉位置应由于视差而发生很大变化,但不会发生变化。 我们看到的视差是平坦的表面。
+ + +虽然可以增加法线贴图的强度,但这不会改变视差。同样,当法线贴图变得太强时,它会看起来很奇怪。它影响了平坦表面的光线的明暗变换,而视差效果它们确实是平的。所以法线贴图只适用于小的变化,但不会表现出明显的视差。
+ + +要获得真正的深度视差感,首先需要确定深度应该是多少。法线贴图不包含这些信息。所以我们需要一个高度图。这样,我们就可以创建一个基于高度信息的假视差效果,就像法线贴图创建一个假斜率一样。下面的贴图也称它是灰度图,黑色代表最低点,白色代表最高点。因为我们将使用这个贴图来创建一个视差效果,也称为视差图。
+ + +确保在导入时禁用sRGB(颜色纹理),这样在使用线性空间渲染时数据就不会被弄乱
-### Shader参数 +为了能够使用视差贴图,我们必须为它添加一个属性到着色器。也会给它一个强度参数来缩放效果。因为视差效果相当强,我们将其范围设置为(0 , 0.1)。
-```hlsl -[NoScaleOffset] _ParallaxMap ("Parallax", 2D) = "black" {} -_ParallaxStrength ("Parallax Strength", Range(0, 0.1)) = 0 +[NoScaleOffset] _ParallaxMap ("Parallax", 2D) = "black" {}
+_ParallaxStrength ("Parallax Strength", Range(0, 0.1)) = 0
-[NoScaleOffset] _OcclusionMap ("Occlusion", 2D) = "white" {}
-_OcclusionStrength ("Occlusion Strength", Range(0, 1)) = 1
-```
+[NoScaleOffset] _OcclusionMap ("Occlusion", 2D) = "white" {}
+_OcclusionStrength ("Occlusion Strength", Range(0, 1)) = 1
+
视差贴图是一个着色器特性,我们将启用__PARALLAX_MAP_关键字。将必需的编译器指令添加到base pass、additive pass和deferred pass。
-```hlsl -#pragma shader_feature _NORMAL_MAP +#pragma shader_feature _NORMAL_MAP
#pragma shader_feature _PARALLAX_MAP
-```
+
为什么不在ShadowCaster增加视差贴图?
当使用albedo贴图的alpha通道的透明度时,视差贴图只会影响阴影。即使是这样,在阴影贴图中的视差效果也很难被注意到。所以它通常不值得额外的计算时间。但是如果愿意,也可以将它添加到阴影施法者通道中。
-```
-
-为了访问新的属性,给我的照明添加相应的变量
-
-```hlsl
-sampler2D _ParallaxMap;
-float _ParallaxStrength;
-
-sampler2D _OcclusionMap;
-float _OcclusionStrength;
-```
-
-为了能够自定义配置材质,在Extend ShaderGUI扩展中增加相应Enable与Disanble key的方法。
-
-```cs
-void DoParallax () {
- MaterialProperty map = FindProperty("_ParallaxMap");
- Texture tex = map.textureValue;
- EditorGUI.BeginChangeCheck();
- editor.TexturePropertySingleLine
- (
- MakeLabel(map, "Parallax (G)"), map,
- tex ? FindProperty("_ParallaxStrength") : null
- );
- if (EditorGUI.EndChangeCheck() && tex != map.textureValue) {
- SetKeyword("_PARALLAX_MAP", map.textureValue);
- }
-}
-```
-
-{% include img-picture.html
-url1="posts/2018/month1/catRender20/1692664-20200616191423089-347056836.png"
-width="250"
-%}
-
-### 坐标匹配
-
-通过在fragment程序中调整纹理坐标,让平坦表面的某些部分看起来高低交错。创建一个应用视差函数,给它一个inout插值器参数。
-
-```hlsl
-void ApplyParallax (inout Interpolators i) {
-}
-```
-
-在fragment程序使用插入的数据之前调用视差函数。_会有点异常是LOD衰落,_因为这取决于屏幕位置。先不调整这些坐标。
-
-```hlsl
-FragmentOutput MyFragmentProgram (Interpolators i) {
- UNITY_SETUP_INSTANCE_ID(i);
- #if defined(LOD_FADE_CROSSFADE)
- UnityApplyDitherCrossFade(i.vpos);
- #endif
-
- ApplyParallax(i);
-
- float alpha = GetAlpha(i);
- #if defined(_RENDERING_CUTOUT)
- clip(alpha - _Cutoff);
- #endif
-}
-```
-
-通过简单地向U坐标添加视差强度来调整纹理坐标。做一次偏移计算
-
-```hlsl
-void ApplyParallax (inout Interpolators i) {
- #if defined(_PARALLAX_MAP)
- i.uv.x += _ParallaxStrength;
- #endif
-}
-```
-
-改变视差强度会导致纹理偏移。增加U坐标会使纹理向负的U方向移动,V坐标同理。这看起来不是视差效果,因为这是一个与视角无关的均匀位移。
-
-{% include img-picture.html
-url1="posts/2018/month1/catRender20/1692664-20200618012938981-982216259.gif"
-des="u坐标移动"
-width="250"
-%}
-
-### 随视角方向移动
-
-视差是由相对于观察者的透视投影,所以必须改变纹理坐标。这意味着必须基于视图的方向来移动坐标,而视图的方向对于表面上每个片段都是不同的。
-
-{% include img-picture.html
-url1="posts/2018/month1/catRender20/1692664-20200617192759952-594530144.png"
-des="varies across a surface"
-width="250"
-%}
-
-纹理坐标存在于切线空间中。为了调整这些坐标,需要知道视图在切线空间中的方向。这需要矩阵乘法对空间进行转换。在fragment-程序已经有了一个切线空间矩阵,但是它是用于从切线空间到世界空间的转换。在本例中,需要从对象空间转到切线空间。
-
-视图方向向量定义为从表面到摄像机,需要归一化。我们可以在vertex程序中确定这个向量,转换它并将它传递给fragment程序。但是为了最终得到正确的方向,需要推迟归一化,直到插值完成后。添加切线空间视图方向作为一个新的插值成员变量。
-
-```hlsl
-struct InterpolatorsVertex {
- #if defined(_PARALLAX_MAP)
- float3 tangentViewDir : TEXCOORD8;
- #endif
-};
-
-struct Interpolators {
- #if defined(_PARALLAX_MAP)
- float3 tangentViewDir : TEXCOORD8;
- #endif
-};
-```
-
-```
-寄存器数量限制是多少?
-model 1与model 2都只支持8个Texture Coordinate Register ->Texcoord[0-7]。当使用model 3时,可以使用TEXCOORD8。若硬件不支持model 3其机能也就不是很强大,所以不要使用视差映射。
-```
-
-**首先**, 使用mesh网格数据中的原始顶点切向量和法向量,在顶点程序中创建一个从对象空间到切线空间的转换矩阵。因为我们只用它来变换一个向量而不是一个位置我们用一个3×3矩阵就足够了。
-
-```hlsl
-InterpolatorsVertex MyVertexProgram (VertexData v) {
- ComputeVertexLightColor(i);
-
- #if defined (_PARALLAX_MAP)
- float3x3 objectToTangent = float3x3(
- v.tangent.xyz,
- cross(v.normal, v.tangent.xyz) * v.tangent.w,
- v.normal
- );
- #endif
-
- return i;
-}
-```
-
-**然后**,可以使用`ObjSpaceViewDir`函数得到对象空间中顶点位置的视图方向,再用矩阵变换它我们就得到了我们需要的切线空间下视图方向。
-
-```hlsl
-#if defined (_PARALLAX_MAP)
- float3x3 objectToTangent = float3x3
- (
- v.tangent.xyz,
- cross(v.normal, v.tangent.xyz) * v.tangent.w,
- v.normal
- );
- i.tangentViewDir = mul(objectToTangent, ObjSpaceViewDir(v.vertex));
-#endif
-```
-
-```hlsl
-//ObjSpaceViewDir内部实现?
-//ObjSpaceViewDir函数是在UnityCG中定义的。它先将摄像机位置转换到对象空间,然后减去对象空间下顶点位置得到一个从顶点指向摄像机的向量,注意它还没有标准化.
-
-inline float3 ObjSpaceViewDir (float4 v)
-{
- float3 objSpaceCameraPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos.xyz, 1)).xyz;
- return objSpaceCameraPos - v.xyz;
-}
-```
-
-**最后**,我们可以在ApplyParallax函数使用切线空间视图方向了。首先,对它进行规格化normalize,把它变成一个合适的方向向量。然后,添加它的XY组件到纹理坐标,再由视差强度缩放。
-
-```hlsl
-void ApplyParallax (inout Interpolators i) {
- #if defined(_PARALLAX_MAP)
- i.tangentViewDir = normalize(i.tangentViewDir);
- i.uv.xy += i.tangentViewDir.xy * _ParallaxStrength;
- #endif
-}
-```
-
-这能有效地将视图方向投影到纹理表面上。当以90度角直视表面时,在切空间中的视图方向等于表面法线(0,0,1),这不会导致位移。视角越浅,投影越大,位移效果也越大。
-
-{% include img-picture.html
-url1="posts/2018/month1/catRender20/1692664-20200617192800432-1757806048.png"
-des="影视图方向用作UV偏移"
-width="250"
-%}
-
-所有这一切的影响是表面似乎被拉向上的切线空间,看起来比它实际上更高,基于视差强度。
+
为了访问新的属性,给我的照明添加相应的变量
+ +sampler2D _ParallaxMap;
+float _ParallaxStrength;
+
+sampler2D _OcclusionMap;
+float _OcclusionStrength;
+
为了能够自定义配置材质,在Extend ShaderGUI扩展中增加相应Enable与Disanble key的方法。
+ +void DoParallax () {
+ MaterialProperty map = FindProperty("_ParallaxMap");
+ Texture tex = map.textureValue;
+ EditorGUI.BeginChangeCheck();
+ editor.TexturePropertySingleLine
+ (
+ MakeLabel(map, "Parallax (G)"), map,
+ tex ? FindProperty("_ParallaxStrength") : null
+ );
+ if (EditorGUI.EndChangeCheck() && tex != map.textureValue) {
+ SetKeyword("_PARALLAX_MAP", map.textureValue);
+ }
+}
+
通过在fragment程序中调整纹理坐标,让平坦表面的某些部分看起来高低交错。创建一个应用视差函数,给它一个inout插值器参数。
+ +void ApplyParallax (inout Interpolators i) {
+}
+
在fragment程序使用插入的数据之前调用视差函数。_会有点异常是LOD衰落,_因为这取决于屏幕位置。先不调整这些坐标。
+ +FragmentOutput MyFragmentProgram (Interpolators i) {
+ UNITY_SETUP_INSTANCE_ID(i);
+ #if defined(LOD_FADE_CROSSFADE)
+ UnityApplyDitherCrossFade(i.vpos);
+ #endif
+
+ ApplyParallax(i);
+
+ float alpha = GetAlpha(i);
+ #if defined(_RENDERING_CUTOUT)
+ clip(alpha - _Cutoff);
+ #endif
+}
+
通过简单地向U坐标添加视差强度来调整纹理坐标。做一次偏移计算
+ +void ApplyParallax (inout Interpolators i) {
+ #if defined(_PARALLAX_MAP)
+ i.uv.x += _ParallaxStrength;
+ #endif
+}
+
改变视差强度会导致纹理偏移。增加U坐标会使纹理向负的U方向移动,V坐标同理。这看起来不是视差效果,因为这是一个与视角无关的均匀位移。
+ + +视差是由相对于观察者的透视投影,所以必须改变纹理坐标。这意味着必须基于视图的方向来移动坐标,而视图的方向对于表面上每个片段都是不同的。
+ + +纹理坐标存在于切线空间中。为了调整这些坐标,需要知道视图在切线空间中的方向。这需要矩阵乘法对空间进行转换。在fragment-程序已经有了一个切线空间矩阵,但是它是用于从切线空间到世界空间的转换。在本例中,需要从对象空间转到切线空间。
+ +视图方向向量定义为从表面到摄像机,需要归一化。我们可以在vertex程序中确定这个向量,转换它并将它传递给fragment程序。但是为了最终得到正确的方向,需要推迟归一化,直到插值完成后。添加切线空间视图方向作为一个新的插值成员变量。
+ +struct InterpolatorsVertex {
+ #if defined(_PARALLAX_MAP)
+ float3 tangentViewDir : TEXCOORD8;
+ #endif
+};
+
+struct Interpolators {
+ #if defined(_PARALLAX_MAP)
+ float3 tangentViewDir : TEXCOORD8;
+ #endif
+};
+
寄存器数量限制是多少?
+model 1与model 2都只支持8个Texture Coordinate Register ->Texcoord[0-7]。当使用model 3时,可以使用TEXCOORD8。若硬件不支持model 3其机能也就不是很强大,所以不要使用视差映射。
+
首先, 使用mesh网格数据中的原始顶点切向量和法向量,在顶点程序中创建一个从对象空间到切线空间的转换矩阵。因为我们只用它来变换一个向量而不是一个位置我们用一个3×3矩阵就足够了。
+ +InterpolatorsVertex MyVertexProgram (VertexData v) {
+ ComputeVertexLightColor(i);
+
+ #if defined (_PARALLAX_MAP)
+ float3x3 objectToTangent = float3x3(
+ v.tangent.xyz,
+ cross(v.normal, v.tangent.xyz) * v.tangent.w,
+ v.normal
+ );
+ #endif
+
+ return i;
+}
+
然后,可以使用ObjSpaceViewDir
函数得到对象空间中顶点位置的视图方向,再用矩阵变换它我们就得到了我们需要的切线空间下视图方向。
#if defined (_PARALLAX_MAP)
+ float3x3 objectToTangent = float3x3
+ (
+ v.tangent.xyz,
+ cross(v.normal, v.tangent.xyz) * v.tangent.w,
+ v.normal
+ );
+ i.tangentViewDir = mul(objectToTangent, ObjSpaceViewDir(v.vertex));
+#endif
+
//ObjSpaceViewDir内部实现?
+//ObjSpaceViewDir函数是在UnityCG中定义的。它先将摄像机位置转换到对象空间,然后减去对象空间下顶点位置得到一个从顶点指向摄像机的向量,注意它还没有标准化.
+
+inline float3 ObjSpaceViewDir (float4 v)
+{
+ float3 objSpaceCameraPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos.xyz, 1)).xyz;
+ return objSpaceCameraPos - v.xyz;
+}
+
最后,我们可以在ApplyParallax函数使用切线空间视图方向了。首先,对它进行规格化normalize,把它变成一个合适的方向向量。然后,添加它的XY组件到纹理坐标,再由视差强度缩放。
+ +void ApplyParallax (inout Interpolators i) {
+ #if defined(_PARALLAX_MAP)
+ i.tangentViewDir = normalize(i.tangentViewDir);
+ i.uv.xy += i.tangentViewDir.xy * _ParallaxStrength;
+ #endif
+}
+
这能有效地将视图方向投影到纹理表面上。当以90度角直视表面时,在切空间中的视图方向等于表面法线(0,0,1),这不会导致位移。视角越浅,投影越大,位移效果也越大。
+ + +所有这一切的影响是表面似乎被拉向上的切线空间,看起来比它实际上更高,基于视差强度。
+ + +在基于高度这一点上,我们可以让表面看起来更高,但它仍然是一个均匀位移。下一步是使用视差贴图来缩放位移。采样贴图,使用它的G通道作为高度,应用视差强度,并使用它来调节位移。
+ +i.tangentViewDir = normalize(i.tangentViewDir);
+float height = tex2D(_ParallaxMap, i.uv.xy).g;
+height *= _ParallaxStrength;
+i.uv.xy += i.tangentViewDir.xy * height;
+
低的区域现在保持不变,而高的区域被向上拉。standard shader抵消了这种效果,所以低的区域也向下移动,而在中间的区域保持他们原来的位置。这是通过从原始高度数据中减去差值来实现的。
+ +float height = tex2D(_ParallaxMap, i.uv.xy).g;
+height -= 0.5;
+height *= _ParallaxStrength;
+
这就产生了我们想要的视差效果,但它只在低强度下有效。不足的是位移位移变换的很快,会撕裂表面。
+ +我们目前使用的视差映射技术被称为带偏移限制的视差映射。我们只是使用了视图方向的XY部分,它的最大长度是1。因此,纹理偏移量是有限的。这种效果不错,但不能代表正确的透视投影。
+ +一个更精确的计算偏移量的物理方法是将高度场视为几何图形表面下的体积,并通过它拍摄一个视图射线。光线从相机发射到表面,从上面进入高度场体积,并持续发射直到它到达由场定义的表面。
+ +如果高度场均匀为零,那么射线就会一直持续到体积的底部。它与物体的距离取决于光线进入物体时的角度。它没有限制。角度越浅,越远。最极端的情况是当视角趋于0时,光线射向无穷大。
+ + +为了找到合适的偏移量,我们必须缩放视图方向向量,通过除以它自己的Z分量来使它的Z分量变成1。因为我们以后不需要用Z,我们只需要用X和Y除以Z。
+ +i.tangentViewDir = normalize(i.tangentViewDir);
+i.tangentViewDir.xy /= i.tangentViewDir.z;
+
虽然这样可以得到一个更正确的投影,但它确实会使浅视角的视差效果恶化。standard着色器通过增加0.42偏差到Z减轻浅视角的视差效果恶化,所以它永远不会接近零。这扭曲了透视图,但使工件更易于管理。我们再加上这个偏差.
+ +i.tangentViewDir.xy /= (i.tangentViewDir.z + 0.42);
+
通过上述多个步骤修正后, 现在我们的着色器与标准着色器支持同样的视差效果。视差映射可以应用于任何表面,投影假设切线空间是均匀的。曲面具有弯曲的切线空间,因此会产生物理上不正确的结果。只要视差强度和曲率很小,你就可以摆脱它。
-### 基于高度滑动 + +同样,阴影坐标不会受到这个效果的影响。因此,阴影在强烈的视差的组合下看起来很奇怪,好像漂浮在表面上。
-```hlsl -i.tangentViewDir = normalize(i.tangentViewDir); -float height = tex2D(_ParallaxMap, i.uv.xy).g; -height *= _ParallaxStrength; -i.uv.xy += i.tangentViewDir.xy * height; -``` + +你不同意Unity使用0.42的偏移值吗?或者你想使用一个不同的值,还是让它保持在0?或者你想用偏移限制代替吗?它是可以配置!
-```hlsl -float height = tex2D(_ParallaxMap, i.uv.xy).g; -height -= 0.5; -height *= _ParallaxStrength; -``` +当你想使用偏移限制,定义PARALLAX_OFFSET_LIMITING
在着色器。或者,通过定义PARALLAX_BIAS来设置要使用的偏差。
void ApplyParallax (inout Interpolators i) {
+ #if defined(_PARALLAX_MAP)
+ i.tangentViewDir = normalize(i.tangentViewDir);
+ #if !defined(PARALLAX_OFFSET_LIMITING)
+ i.tangentViewDir.xy /= (i.tangentViewDir.z + PARALLAX_BIAS);
+ #endif
+ #endif
+}
+
当没有定义时,假设偏差是0.42。在ApplyParallax
中定义它。注意,宏定义不关心函数作用域,它们总是全局的。
#if !defined(PARALLAX_OFFSET_LIMITING)
+ #if !defined(PARALLAX_BIAS)
+ #define PARALLAX_BIAS 0.42
+ #endif
+ i.tangentViewDir.xy /= (i.tangentViewDir.z + PARALLAX_BIAS);
+#endif
+
现在我们可以通过着色器的CGINCLUDE块来微调我们的视差效果。添加无偏差和限制偏移的选项,但将它们转换为注释,以坚持默认选项。
-一个更精确的计算偏移量的物理方法是将高度场视为几何图形表面下的体积,并通过它拍摄一个视图射线。光线从相机发射到表面,从上面进入高度场体积,并持续发射直到它到达由场定义的表面。 +CGINCLUDE
+ #define BINORMAL_PER_FRAGMENT
+ #define FOG_DISTANCE
+
+// #define PARALLAX_BIAS 0
+// #define PARALLAX_OFFSET_LIMITING
-如果高度场均匀为零,那么射线就会一直持续到体积的底部。它与物体的距离取决于光线进入物体时的角度。它没有限制。角度越浅,越远。最极端的情况是当视角趋于0时,光线射向无穷大。
+ENDCG
+
视差贴图可以在主贴图上工作,但是我们还没有注意到副贴图。我们必须应用纹理坐标偏移到细节UV上。
+ +首先,下面是一个包含网格模式的详细地图。它可以很容易地验证效果是否正确地应用于细节。
-```hlsl -i.tangentViewDir = normalize(i.tangentViewDir); -i.tangentViewDir.xy /= i.tangentViewDir.z; -``` + +使用这个纹理作为材质的细节albedo贴图。设置二级贴图的平铺为10×10。这表明,细节紫外线确实仍然不受影响。
+ + +Standard也简单地添加了UV偏移到细节UV,这是存储在UV插值器的ZW组件。
+ +float height = tex2D(_ParallaxMap, i.uv.xy).g;
+height -= 0.5;
+height *= _ParallaxStrength;
+float2 uvOffset = i.tangentViewDir.xy * height;
+i.uv.xy += uvOffset;
+i.uv.zw += uvOffset;
+
细节可能有所变化,但是它们肯定还不匹配视差效果。 那是因为我们平铺了二级纹理。 这样会将细节UV缩放10倍,使视差偏移量变弱十倍。 我们还必须将细节拼贴应用到偏移量。
+ +i.uv.zw += uvOffset * _DetailTex_ST.xy;
+
实际上,缩放应该相对于主UV平铺,以防它被设置为1×1以外的一些东西。
+ +i.uv.zw += uvOffset * (_DetailTex_ST.xy / _MainTex_ST.xy);
+
然而,除了上述的偏移视差映射还有另外的视差算法:发射射线与高度场体积相交,确定其交点在表面上的位置,然后对该位置采样。 它通过在射线进入体积时的交点,对高度图进行一次采样。 但是,当看向任意一个角度时,这并不能准确告诉射线实际上与高度场相交的高度。
+ + +先假设入口点的高度与交点的高度相同,但这实际上只有在入口点和交点具有相同的高度时才是正确的。当偏移量不大且高度场变化不大时,它的效果仍然很好。但是,当偏移量太大或高度变化太快时,该算法就会出现问题,而这很可能是错误的。这就会造成表面撕裂。
+ +如果我们能算出射线实际到达的高度场的位置,那么总能找到真正的可见表面点。这不能通过单个纹理样本来实现,我们必须沿着视图射线逐步移动,并每次都采样高度场,直到射线到达表面。该技术是RayMarching。
+ + +有各种不同的视差贴图使用raymarching。常见的是陡视差映射_Steep Parallax Mapping_、地形映射_Relief Mapping_和视差遮挡映射_Parallax Occlusion Mapping_。与使用单一纹理样本相比,它们能通过高度场来创建更好的视差效果。除此之外,它们还可以应用额外的阴影和技术来改进该算法。当我们做的匹配这些方法时,我会调用它。
+ +标准着色器仅支持简单的偏移视差映射。 现在,我们要在自己的着色器中添加对视差光线Ray marching的支持。 但是,我们还要继续支持这种简单方法。 两者都需要采样height字段,因此将采样代码行放在单独的GetParallaxHeight函数中。 而且,两种方法的投影视图方向和偏移量的最终应用都相同。 因此,将偏移量计算也单独为一个函数。 它仅需要原始UV坐标和已处理的视图方向作为参数,结果返回要应用的UV偏移。
+ +float GetParallaxHeight (float2 uv) {
+ return tex2D(_ParallaxMap, uv).g;
+}
+
+float2 ParallaxOffset (float2 uv, float2 viewDir) {
+ float height = GetParallaxHeight(uv);
+ height -= 0.5;
+ height *= _ParallaxStrength;
+ return viewDir * height;
+}
+
+void ApplyParallax (inout Interpolators i) {
+ #if defined(_PARALLAX_MAP)
+ i.tangentViewDir = normalize(i.tangentViewDir);
+ #if !defined(PARALLAX_OFFSET_LIMITING)
+ #if !defined(PARALLAX_BIAS)
+ #define PARALLAX_BIAS 0.42
+ #endif
+ i.tangentViewDir.xy /= (i.tangentViewDir.z + PARALLAX_BIAS);
+ #endif
+
+ float2 uvOffset = ParallaxOffset(i.uv.xy, i.tangentViewDir.xy);
+ i.uv.xy += uvOffset;
+ i.uv.zw += uvOffset * (_DetailTex_ST.xy / _MainTex_ST.xy);
+ #endif
+}
+
现在,我们将应用视差函数宏替换对视差偏移的硬编码调用,从而使视差方法更加灵活。如果没有定义它,我们将它设置为使用偏移量方法。
+ +void ApplyParallax (inout Interpolators i) {
+ #if defined(_PARALLAX_MAP)
+ //...
+ #if !defined(PARALLAX_FUNCTION)
+ #define PARALLAX_FUNCTION ParallaxOffset
+ #endif
+ float2 uvOffset = PARALLAX_FUNCTION(i.uv.xy, i.tangentViewDir.xy);
+ i.uv.xy += uvOffset;
+ i.uv.zw += uvOffset * (_DetailTex_ST.xy / _MainTex_ST.xy);
+ #endif
+}
+
为RayMarching方法创建一个新函数。与ParallaxOffset函数类似的参数和返回类型。
+ +float2 ParallaxOffset (float2 uv, float2 viewDir) {
+}
+
+float2 ParallaxRaymarching (float2 uv, float2 viewDir) {
+ float2 uvOffset = 0;
+ return uvOffset;
+}
+
现在可以通过定义_PARALLAX_FUNCTION_来改变着色器中的视差方法。
+ +#define PARALLAX_BIAS 0
+//#define PARALLAX_OFFSET_LIMITING
+#define PARALLAX_FUNCTION ParallaxRaymarching
+
为了找到视图射线到达高度场的点,我们需要对射线上的多个点进行采样并计算出在表面下方的位置。第一个采样点在顶部,我们在这里输入高度量,就像使用偏移方法一样。最后一个采样点就是射线到达体积底部的地方。我们会在这些端点之间均匀地添加额外的采样点。
+ +假设每条射线进行10次采样。这意味着我们将对高度图采样10次而不是一次,所以这不是一个便宜计算方法。因为我们用了10个样本,所以步长是0.1。这是我们沿着视图射线移动的因子,也就是UV偏移增量。
+ +float2 ParallaxRaymarching (float2 uv, float2 viewDir) {
+ float2 uvOffset = 0;
+ float stepSize = 0.1;
+ float2 uvDelta = viewDir * stepSize;
+ return uvOffset;
+}
+
为了应用视差强度,我们可以调整每一步采样的高度。但是缩放UV delta也有同样的效果,只需要计算一次。
+ +float2 uvDelta = viewDir * (stepSize * _ParallaxStrength);
+
通过这种方式,无论视差强度如何,我们都可以继续使用0–1作为高度场的范围。 因此,射线的第一步高度始终为1。低于或高于该高度的表面点的高度由高度场定义。
+ +float stepSize = 0.1;//步长
+float2 uvDelta = viewDir * (stepSize * _ParallaxStrength);
+float stepHeight = 1;//步高
+float surfaceHeight = GetParallaxHeight(uv);
+
现在我们要沿着射线迭代。首先,每一步我们都会增加UV偏移量。视图向量指向摄像机,但我们是在向表面移动,所以我们需要减去UV delta。然后我们用步高来减小步长。然后我们再次对高度图采样。使用while循环重复上述步骤,直到采样完毕。
+ +float stepHeight = 1;
+float surfaceHeight = GetParallaxHeight(uv);
+
+while (stepHeight > surfaceHeight)
+{
+ uvOffset -= uvDelta;
+ stepHeight -= stepSize;
+ surfaceHeight = GetParallaxHeight(uv + uvOffset);
+}
+
当编译时,会得到一个编译器警告和错误。这个警告告诉我们在循环中使用了梯度指令。这指的是循环中的纹理采样。GPU必须弄清楚使用哪个mipmap级别,它需要比较相邻片段使用的UV坐标。只有当所有片段执行相同的代码时,它才能对比。对于循环来说,这是不可能的,因为它可以提前终止,每个片段都可能不同。因此编译器将展开循环,这意味着它将一直执行所有9个步骤,而不管逻辑是否可以提前停止。相反,它随后使用确定性逻辑选择最终结果。
+ +编译失败是因为编译器无法确定循环的最大迭代次数。它不知道这个最多是9。通过将while循环转换为执行限制的for循环来明确这一点。
+ +for (int i = 1; i < 10 && stepHeight > surfaceHeight; i++)
+{
+ uvOffset -= uvDelta;
+ stepHeight -= stepSize;
+ surfaceHeight = GetParallaxHeight(uv + uvOffset);
+}
+
与简单的视差偏移方法相比,视差效果更加明显。较高的区域现在也正确地阻挡了我们后面较低区域的视野。我们还得到了明显的图层,总共10层。
+ +这个基本的光线行进方法最适合陡峭的视差贴图。效果的质量是由我们的样本分辨率决定的。一些方法根据视角使用可变的步骤。较浅的角度需要更多的步长,因为光线较长。但我们的样本量是固定的,所以我们不会这样做。
+ +提高质量的明显方法是增加采样的次数,因此让其可配置。使用_PARALLAX_RAYMARCHING_STEPS_,默认值为10,而不是固定的步长和迭代次数。
-虽然这样可以得到一个更正确的投影,但它确实会使浅视角的视差效果恶化。standard着色器通过增加0.42偏差到Z减轻浅视角的视差效果恶化,所以它永远不会接近零。这扭曲了透视图,但使工件更易于管理。我们再加上这个偏差. +float2 ParallaxRaymarching (float2 uv, float2 viewDir) {
+ #if !defined(PARALLAX_RAYMARCHING_STEPS)
+ #define PARALLAX_RAYMARCHING_STEPS 10
+ #endif
+ float2 uvOffset = 0;
+ float stepSize = 1.0 / PARALLAX_RAYMARCHING_STEPS;
+ float2 uvDelta = viewDir * (stepSize * _ParallaxStrength);
-```hlsl
-i.tangentViewDir.xy /= (i.tangentViewDir.z + 0.42);
-```
+ float stepHeight = 1;
+ float surfaceHeight = GetParallaxHeight(uv);
-{% include img-picture.html
-url1="posts/2018/month1/catRender20/1692664-20200617192802151-218582125.png"
-des="视差贴图像标准着色器"
-width="250"
-%}
+ for (
+ int i = 1;
+ i < PARALLAX_RAYMARCHING_STEPS && stepHeight > surfaceHeight;
+ i++
+ ) {
+ uvOffset -= uvDelta;
+ stepHeight -= stepSize;
+ surfaceHeight = GetParallaxHeight(uv + uvOffset);
+ }
-通过上述多个步骤修正后, 现在我们的着色器与标准着色器支持同样的视差效果。视差映射可以应用于任何表面,投影假设切线空间是均匀的。曲面具有弯曲的切线空间,因此会产生物理上不正确的结果。只要视差强度和曲率很小,你就可以摆脱它。
-
-{% include img-picture.html
-url1="posts/2018/month1/catRender20/1692664-20200617192802555-2049613835.png"
-des="球面视差贴图"
-width="250"
-%}
+ return uvOffset;
+}
+
现在我们可以在着色器中控制步数。对于真正的高质量,将PARALLAX_RAYMARCHING_STEPS定义为100。
-### Parallax Configuration - -你不同意Unity使用0.42的偏移值吗?或者你想使用一个不同的值,还是让它保持在0?或者你想用偏移限制代替吗?它是可以配置! - -当你想使用偏移限制,定义`PARALLAX_OFFSET_LIMITING`在着色器。或者,通过定义PARALLAX_BIAS来设置要使用的偏差。 - -```hlsl -void ApplyParallax (inout Interpolators i) { - #if defined(_PARALLAX_MAP) - i.tangentViewDir = normalize(i.tangentViewDir); - #if !defined(PARALLAX_OFFSET_LIMITING) - i.tangentViewDir.xy /= (i.tangentViewDir.z + PARALLAX_BIAS); - #endif - #endif -} -``` - -当没有定义时,假设偏差是0.42。在`ApplyParallax` 中定义它。注意,宏定义不关心函数作用域,它们总是全局的。 - -```hlsl -#if !defined(PARALLAX_OFFSET_LIMITING) - #if !defined(PARALLAX_BIAS) - #define PARALLAX_BIAS 0.42 - #endif - i.tangentViewDir.xy /= (i.tangentViewDir.z + PARALLAX_BIAS); -#endif -``` - -现在我们可以通过着色器的CGINCLUDE块来微调我们的视差效果。添加无偏差和限制偏移的选项,但将它们转换为注释,以坚持默认选项。 - -```hlsl -CGINCLUDE - #define BINORMAL_PER_FRAGMENT - #define FOG_DISTANCE - -// #define PARALLAX_BIAS 0 -// #define PARALLAX_OFFSET_LIMITING - -ENDCG -``` - -### Detail UV - -视差贴图可以在主贴图上工作,但是我们还没有注意到副贴图。我们必须应用纹理坐标偏移到细节UV上。 - -首先,下面是一个包含网格模式的详细地图。它可以很容易地验证效果是否正确地应用于细节。 - -{% include img-picture.html -url1="posts/2018/month1/catRender20/1692664-20200617192803893-1797850325.png" -des="细节网格纹理" -width="250" -%} - -使用这个纹理作为材质的细节albedo贴图。设置二级贴图的平铺为10×10。这表明,细节紫外线确实仍然不受影响。 - -{% include img-picture.html -url1="posts/2018/month1/catRender20/1692664-20200617192804491-290326603.png" -url2="posts/2018/month1/catRender20/1692664-20200617192805295-1360127209.png" -des="细节UV不受影响" -width="250" -%} - -Standard也简单地添加了UV偏移到细节UV,这是存储在UV插值器的ZW组件。 - -```hlsl -float height = tex2D(_ParallaxMap, i.uv.xy).g; -height -= 0.5; -height *= _ParallaxStrength; -float2 uvOffset = i.tangentViewDir.xy * height; -i.uv.xy += uvOffset; -i.uv.zw += uvOffset; -``` - -细节可能有所变化,但是它们肯定还不匹配视差效果。 那是因为我们平铺了二级纹理。 这样会将细节UV缩放10倍,使视差偏移量变弱十倍。 我们还必须将细节拼贴应用到偏移量。 - -```hlsl -i.uv.zw += uvOffset * _DetailTex_ST.xy; -``` - -实际上,缩放应该相对于主UV平铺,以防它被设置为1×1以外的一些东西。 - -```hlsl -i.uv.zw += uvOffset * (_DetailTex_ST.xy / _MainTex_ST.xy); -``` - -{% include img-picture.html -url1="posts/2018/month1/catRender20/1692664-20200617192806533-2106069113.png" -des="正确的UV" -width="250" -%} - -## Ray Marching-光线步进 - -然而,除了上述的偏移视差映射还有另外的视差算法:发射射线与高度场体积相交,确定其交点在表面上的位置,然后对该位置采样。 它通过在射线进入体积时的交点,对高度图进行一次采样。 但是,当看向任意一个角度时,这并不能准确告诉射线实际上与高度场相交的高度。 - -{% include img-picture.html -url1="posts/2018/month1/catRender20/1692664-20200617192807160-2024700857.png" -des="实际与预测的高度对比" -width="250" -%} - -先假设入口点的高度与交点的高度相同,但这实际上只有在入口点和交点具有相同的高度时才是正确的。当偏移量不大且高度场变化不大时,它的效果仍然很好。但是,当偏移量太大或高度变化太快时,该算法就会出现问题,而这很可能是错误的。这就会造成表面撕裂。 - -如果我们能算出射线实际到达的高度场的位置,那么总能找到真正的可见表面点。这不能通过单个纹理样本来实现,我们必须沿着视图射线逐步移动,并每次都采样高度场,直到射线到达表面。该技术是RayMarching。 - -{% include img-picture.html -url1="posts/2018/month1/catRender20/1692664-20200617192807679-1545265192.png" -des="随视图射线前进" -width="250" -%} - -有各种不同的视差贴图使用raymarching。常见的是陡视差映射_Steep Parallax Mapping_、地形映射_Relief Mapping_和视差遮挡映射_Parallax Occlusion Mapping_。与使用单一纹理样本相比,它们能通过高度场来创建更好的视差效果。除此之外,它们还可以应用额外的阴影和技术来改进该算法。当我们做的匹配这些方法时,我会调用它。 - -### 自定义视差函数 - -标准着色器仅支持简单的偏移视差映射。 现在,我们要在自己的着色器中添加对视差光线Ray marching的支持。 但是,我们还要继续支持这种简单方法。 两者都需要采样height字段,因此将采样代码行放在单独的GetParallaxHeight函数中。 而且,两种方法的投影视图方向和偏移量的最终应用都相同。 因此,将偏移量计算也单独为一个函数。 它仅需要原始UV坐标和已处理的视图方向作为参数,结果返回要应用的UV偏移。 - -```hlsl -float GetParallaxHeight (float2 uv) { - return tex2D(_ParallaxMap, uv).g; -} - -float2 ParallaxOffset (float2 uv, float2 viewDir) { - float height = GetParallaxHeight(uv); - height -= 0.5; - height *= _ParallaxStrength; - return viewDir * height; -} - -void ApplyParallax (inout Interpolators i) { - #if defined(_PARALLAX_MAP) - i.tangentViewDir = normalize(i.tangentViewDir); - #if !defined(PARALLAX_OFFSET_LIMITING) - #if !defined(PARALLAX_BIAS) - #define PARALLAX_BIAS 0.42 - #endif - i.tangentViewDir.xy /= (i.tangentViewDir.z + PARALLAX_BIAS); - #endif - - float2 uvOffset = ParallaxOffset(i.uv.xy, i.tangentViewDir.xy); - i.uv.xy += uvOffset; - i.uv.zw += uvOffset * (_DetailTex_ST.xy / _MainTex_ST.xy); - #endif -} -``` - -现在,我们将应用视差函数宏替换对视差偏移的硬编码调用,从而使视差方法更加灵活。如果没有定义它,我们将它设置为使用偏移量方法。 - -```hlsl -void ApplyParallax (inout Interpolators i) { - #if defined(_PARALLAX_MAP) - //... - #if !defined(PARALLAX_FUNCTION) - #define PARALLAX_FUNCTION ParallaxOffset - #endif - float2 uvOffset = PARALLAX_FUNCTION(i.uv.xy, i.tangentViewDir.xy); - i.uv.xy += uvOffset; - i.uv.zw += uvOffset * (_DetailTex_ST.xy / _MainTex_ST.xy); - #endif -} -``` - -为RayMarching方法创建一个新函数。与ParallaxOffset函数类似的参数和返回类型。 - -```hlsl -float2 ParallaxOffset (float2 uv, float2 viewDir) { -} - -float2 ParallaxRaymarching (float2 uv, float2 viewDir) { - float2 uvOffset = 0; - return uvOffset; -} -``` - -现在可以通过定义_PARALLAX_FUNCTION_来改变着色器中的视差方法。 - -```hlsl -#define PARALLAX_BIAS 0 -//#define PARALLAX_OFFSET_LIMITING -#define PARALLAX_FUNCTION ParallaxRaymarching -``` - -### 相交计算 - -为了找到视图射线到达高度场的点,我们需要对射线上的多个点进行采样并计算出在表面下方的位置。第一个采样点在顶部,我们在这里输入高度量,就像使用偏移方法一样。最后一个采样点就是射线到达体积底部的地方。我们会在这些端点之间均匀地添加额外的采样点。 - -假设每条射线进行10次采样。这意味着我们将对高度图采样10次而不是一次,所以这不是一个便宜计算方法。因为我们用了10个样本,所以步长是0.1。这是我们沿着视图射线移动的因子,也就是UV偏移增量。 - -```hlsl -float2 ParallaxRaymarching (float2 uv, float2 viewDir) { - float2 uvOffset = 0; - float stepSize = 0.1; - float2 uvDelta = viewDir * stepSize; - return uvOffset; -} -``` - -为了应用视差强度,我们可以调整每一步采样的高度。但是缩放UV delta也有同样的效果,只需要计算一次。 - -```hlsl -float2 uvDelta = viewDir * (stepSize * _ParallaxStrength); -``` - -通过这种方式,无论视差强度如何,我们都可以继续使用0–1作为高度场的范围。 因此,射线的第一步高度始终为1。低于或高于该高度的表面点的高度由高度场定义。 - -```hlsl -float stepSize = 0.1;//步长 -float2 uvDelta = viewDir * (stepSize * _ParallaxStrength); -float stepHeight = 1;//步高 -float surfaceHeight = GetParallaxHeight(uv); -``` - -现在我们要沿着射线迭代。**首先**,每一步我们都会增加UV偏移量。视图向量指向摄像机,但我们是在向表面移动,所以我们需要减去UV delta。**然后**我们用步高来减小步长。**然后**我们再次对高度图采样。使用while循环重复上述步骤,直到采样完毕。 - -```hlsl -float stepHeight = 1; -float surfaceHeight = GetParallaxHeight(uv); - -while (stepHeight > surfaceHeight) -{ - uvOffset -= uvDelta; - stepHeight -= stepSize; - surfaceHeight = GetParallaxHeight(uv + uvOffset); -} -``` - -当编译时,会得到一个编译器警告和错误。这个警告告诉我们在循环中使用了梯度指令。这指的是循环中的纹理采样。GPU必须弄清楚使用哪个mipmap级别,它需要比较相邻片段使用的UV坐标。只有当所有片段执行相同的代码时,它才能对比。对于循环来说,这是不可能的,因为它可以提前终止,每个片段都可能不同。因此编译器将展开循环,这意味着它将一直执行所有9个步骤,而不管逻辑是否可以提前停止。相反,它随后使用确定性逻辑选择最终结果。 - -编译失败是因为编译器无法确定循环的最大迭代次数。它不知道这个最多是9。通过将while循环转换为执行限制的for循环来明确这一点。 - -```hlsl -for (int i = 1; i < 10 && stepHeight > surfaceHeight; i++) -{ - uvOffset -= uvDelta; - stepHeight -= stepSize; - surfaceHeight = GetParallaxHeight(uv + uvOffset); -} -``` - -{% include img-picture.html -url1="posts/2018/month1/catRender20/1692664-20200618012945572-448280189.png" -des="Raymarching 步进10次 无偏差, 无限制" -width="250" -%} - -与简单的视差偏移方法相比,视差效果更加明显。较高的区域现在也正确地阻挡了我们后面较低区域的视野。我们还得到了明显的图层,总共10层。 - -### 更多步进 - -这个基本的光线行进方法最适合陡峭的视差贴图。效果的质量是由我们的样本分辨率决定的。一些方法根据视角使用可变的步骤。较浅的角度需要更多的步长,因为光线较长。但我们的样本量是固定的,所以我们不会这样做。 - -提高质量的明显方法是增加采样的次数,因此让其可配置。使用_PARALLAX_RAYMARCHING_STEPS_,默认值为10,而不是固定的步长和迭代次数。 - -```hlsl -float2 ParallaxRaymarching (float2 uv, float2 viewDir) { - #if !defined(PARALLAX_RAYMARCHING_STEPS) - #define PARALLAX_RAYMARCHING_STEPS 10 - #endif - float2 uvOffset = 0; - float stepSize = 1.0 / PARALLAX_RAYMARCHING_STEPS; - float2 uvDelta = viewDir * (stepSize * _ParallaxStrength); - - float stepHeight = 1; - float surfaceHeight = GetParallaxHeight(uv); - - for ( - int i = 1; - i < PARALLAX_RAYMARCHING_STEPS && stepHeight > surfaceHeight; - i++ - ) { - uvOffset -= uvDelta; - stepHeight -= stepSize; - surfaceHeight = GetParallaxHeight(uv + uvOffset); - } - - return uvOffset; -} -``` - -现在我们可以在着色器中控制步数。对于真正的高质量,将PARALLAX_RAYMARCHING_STEPS定义为100。 - -```hlsl -#define PARALLAX_BIAS 0 -//#define PARALLAX_OFFSET_LIMITING -#define PARALLAX_RAYMARCHING_STEPS 100 +#define PARALLAX_BIAS 0
+//#define PARALLAX_OFFSET_LIMITING
+#define PARALLAX_RAYMARCHING_STEPS 100
#define PARALLAX_FUNCTION ParallaxRaymarching
-```
+
这让我们知道了它的效果能有多好,但它计算量太大了,一般不适合手机。所以把样本数设为10后,我们仍然可以看到视差效果看起来连续和平滑。然而,由视差遮挡引起的轮廓总是锯齿状的,MSAA并不能消除这一点,因为它只适用于几何图形的边缘,而不是纹理效果。只要不依赖深度缓冲区,后处理抗锯齿技术能解决。
-``` -不能按片段写入深度缓冲区吗? +不能按片段写入深度缓冲区吗?
这在足够先进的硬件上确实是可能的,使它能够正确地与高度场相交并应用阴影。不过,它计算量太大。
-```
+
我们当前的方法是沿着射线步进,直到到达表面以下的点,或者到达射线末端可能的最低点。然后我们用UV偏移处理那个点。但隐藏在表面之下的这个点,很可能会出现错误。这就是导致表面撕裂的原因。
-增加步长数只会减少最大误差。使用足够的步骤,错误会变得更小,以至于我们无法再看到它。所以当一个表面总是从远处看,你可以用更少的步骤。距离越近,视角越小,需要的样本就越多。 +增加步长数只会减少最大误差。使用足够的步骤,错误会变得更小,以至于我们无法再看到它。所以当一个表面总是从远处看,你可以用更少的步骤。距离越近,视角越小,需要的样本就越多。
-{% include img-picture.html -url1="posts/2018/month1/catRender20/1692664-20200618012948690-1598196626.png" -des="Raymarching 100次采样" -width="250" -%} + +提高质量的一种方法是根据经验预测光线真正到达表面的位置。比如第一步在表面之上,下一步在表面之下。在这两步之间的某个点射线一定到达了表面。
-两个射线点、和两个射线点到表面最近的点,能定义两条线段。因为光线和表面碰撞,这两条线段会相交。所以如果我们跟踪前面的步骤,我们可以在循环之后执行直线交叉。我们可以用这个信息来近似出真正的交点。 +两个射线点、和两个射线点到表面最近的点,能定义两条线段。因为光线和表面碰撞,这两条线段会相交。所以如果我们跟踪前面的步骤,我们可以在循环之后执行直线交叉。我们可以用这个信息来近似出真正的交点。
-{% include img-picture.html -url1="posts/2018/month1/catRender20/1692664-20200618012949277-977122761.png" -des="执行直线交叉" -width="250" -%} + +在for循环内,我们必须跟踪之前的UV偏移量、步长高度和表面高度。一般来说,这些等于循环之前的第一个样本。
-```hlsl -float2 prevUVOffset = uvOffset; -float prevStepHeight = stepHeight; -float prevSurfaceHeight = surfaceHeight; -``` +float2 prevUVOffset = uvOffset;
+float prevStepHeight = stepHeight;
+float prevSurfaceHeight = surfaceHeight;
+
在循环之后,我们计算这些线的交点。我们可以使用这个插值之间的前点和后点的UV偏移。
-```hlsl -float prevDifference = prevStepHeight - prevSurfaceHeight; -float difference = surfaceHeight - stepHeight; -float t = prevDifference / (prevDifference + difference); -uvOffset = lerp(prevUVOffset, uvOffset, t); +float prevDifference = prevStepHeight - prevSurfaceHeight;
+float difference = surfaceHeight - stepHeight;
+float t = prevDifference / (prevDifference + difference);
+uvOffset = lerp(prevUVOffset, uvOffset, t);
-return uvOffset;
-```
+return uvOffset;
+
数学原理:
这两个线段定义在两个样本步骤之间的空间内。我们将这个空间的宽度设置为1。从前一步到最后一步的直线由点(0,a)和点(1,b)定义,其中a是前一步的高度,b是后一步的高度。因此,可以用线性函数'v(t) = a + (b - a)t'来定义视图线。同样地,面线由点(0,c)和(1,d)定义,函数's(t) = hlsl + (d - hlsl)t'。
交点存在于s(t) = v(t)'处。那么t的值是多少?
@@ -868,4473 +902,4511 @@ c + (d - c)t = a + (b - a)t
(a - c + d - b)t = a - c
t = (a - c) / (a - c + d - b)
注意:a - c是在t = 0处直线高度的绝对差。d - b是t = 1处的绝对高度差。
-```
+
实际上,在这种情况下,我们可以使用插值器来缩放我们要添加到上一点上的UV偏移量。它可以归结为相同的东西,只是用了更少的数学。
-```hlsl -float t = prevDifference / (prevDifference - difference); -uvOffset = prevUVOffset - uvDelta * t; -``` +float t = prevDifference / (prevDifference - difference);
+uvOffset = prevUVOffset - uvDelta * t;
+
效果看起来好多了。我们现在假设表面在样本点之间是线性的,这可以防止最明显的分层假象。然而,它不能帮助我们检测我们是否错过了步骤之间的交集。我们仍然需要很多的样本来处理小的特征,轮廓和浅角度。
-有了这个技巧,我们的方法类似于视差遮挡映射。虽然这是一个相对便宜的改进,但通过定义_PARALLAX_RAYMARCHING_INTERPOLATE_,我们让它成为可选的。 +有了这个技巧,我们的方法类似于视差遮挡映射。虽然这是一个相对便宜的改进,但通过定义_PARALLAX_RAYMARCHING_INTERPOLATE_,我们让它成为可选的。
-```hlsl -#if defined(PARALLAX_RAYMARCHING_INTERPOLATE) - float prevDifference = prevStepHeight - prevSurfaceHeight; - float difference = surfaceHeight - stepHeight; - float t = prevDifference / (prevDifference + difference); - uvOffset = prevUVOffset - uvDelta * t; -#endif -``` +#if defined(PARALLAX_RAYMARCHING_INTERPOLATE)
+ float prevDifference = prevStepHeight - prevSurfaceHeight;
+ float difference = surfaceHeight - stepHeight;
+ float t = prevDifference / (prevDifference + difference);
+ uvOffset = prevUVOffset - uvDelta * t;
+#endif
+
在shader内定义PARALLAX_RAYMARCHING_INTERPOLATE。
-```hlsl -#define PARALLAX_BIAS 0 -//#define PARALLAX_OFFSET_LIMITING -#define PARALLAX_RAYMARCHING_STEPS 10 +#define PARALLAX_BIAS 0
+//#define PARALLAX_OFFSET_LIMITING
+#define PARALLAX_RAYMARCHING_STEPS 10
#define PARALLAX_RAYMARCHING_INTERPOLATE
#define PARALLAX_FUNCTION ParallaxRaymarching
-```
+
通过在两个步长之间进行线性插值,我们假定表面在两个步长之间是笔直的。 但是,通常情况并非如此。 为了更好地处理不规则的高度场,我们必须在两个步长之间搜索实际的交点。 或至少接近它。
-完成循环后,不要使用最后的偏移量,而是将偏移量调整到最后两个步长的中间位置。对该点的高度进行采样。如果我们结束在表面以下,向表面之上方向移动四分之一,并再次采样。如果我们在表面上结束,向表面之下方向移动四分之,并再次采样。不断重复这个过程。 +完成循环后,不要使用最后的偏移量,而是将偏移量调整到最后两个步长的中间位置。对该点的高度进行采样。如果我们结束在表面以下,向表面之上方向移动四分之一,并再次采样。如果我们在表面上结束,向表面之下方向移动四分之,并再次采样。不断重复这个过程。
-{% include img-picture.html -url1="posts/2018/month1/catRender20/1692664-20200618012952340-10743193.png" -des="越来越接近交点" -width="250" -%} + +上述方法是二分查找的一个应用。它与地形测绘方法最匹配。每走一步,路程减半,直到到达目的地。在我们的例子中,我们将简单地做固定次数,以达到预期的解决方案。一步,得到0.5。两步,得到0.25、0.75。三步,是0.125、0.375、0.625、0.875。注意,从第二步开始,每次采样提升分的辨率将翻倍。
+为了控制是否使用此方法,我们定义_PARALLAX_RAYMARCHING_SEARCH_STEPS_。默认情况下将其设置为零,这意味着我们根本不进行搜索。如果它被定义为大于0,我们将不得不使用另一个循环。注意,这种方法与_PARALLAX_RAYMARCHING_INTERPOLATE_是不兼容的,因为我们不能再保证表面是交叉的最后两个步骤。当我们搜索的时候,禁用插值。
-为了控制是否使用此方法,我们定义_PARALLAX_RAYMARCHING_SEARCH_STEPS_。默认情况下将其设置为零,这意味着我们根本不进行搜索。如果它被定义为大于0,我们将不得不使用另一个循环。注意,这种方法与_PARALLAX_RAYMARCHING_INTERPOLATE_是**不兼容**的,因为我们不能再保证表面是交叉的最后两个步骤。当我们搜索的时候,禁用插值。 - -```hlsl -#if !defined(PARALLAX_RAYMARCHING_SEARCH_STEPS) - #define PARALLAX_RAYMARCHING_SEARCH_STEPS 0 -#endif -#if PARALLAX_RAYMARCHING_SEARCH_STEPS > 0 - for (int i = 0; i < PARALLAX_RAYMARCHING_SEARCH_STEPS; i++) { - } -#elif defined(PARALLAX_RAYMARCHING_INTERPOLATE) - float prevDifference = prevStepHeight - prevSurfaceHeight; - float difference = surfaceHeight - stepHeight; - float t = prevDifference / (prevDifference + difference); - uvOffset = prevUVOffset - uvDelta * t; +#if !defined(PARALLAX_RAYMARCHING_SEARCH_STEPS)
+ #define PARALLAX_RAYMARCHING_SEARCH_STEPS 0
#endif
-```
-
-此循环也执行与原始循环相同的基本工作。调整偏移量和步高,然后采样高度字段。
-
-```hlsl
-for (int i = 0; i < PARALLAX_RAYMARCHING_SEARCH_STEPS; i++) {
- uvOffset -= uvDelta;
- stepHeight -= stepSize;
- surfaceHeight = GetParallaxHeight(uv + uvOffset);
-}
-```
-
-但每次迭代,UV增量和步长减半。
-
-```hlsl
-for (int i = 0; i < PARALLAX_RAYMARCHING_SEARCH_STEPS; i++)
-{
- uvDelta *= 0.5;
- stepSize *= 0.5;
- uvOffset -= uvDelta;
- stepHeight -= stepSize;
- surfaceHeight = GetParallaxHeight(uv + uvOffset);
-}
-```
-
-同样,如果点在表面之下,我们必须朝相反的方向移动。
-
-```hlsl
-uvDelta *= 0.5;
-stepSize *= 0.5;
-if (stepHeight < surfaceHeight) {
- uvOffset += uvDelta;
- stepHeight += stepSize;
-}
-else {
- uvOffset -= uvDelta;
- stepHeight -= stepSize;
-}
-surfaceHeight = GetParallaxHeight(uv + uvOffset);
-```
-
-调整着色器,所以它使用三个搜索步骤
-
-```hlsl
-#define PARALLAX_BIAS 0
-//#define PARALLAX_OFFSET_LIMITING
-#define PARALLAX_RAYMARCHING_STEPS 10
+#if PARALLAX_RAYMARCHING_SEARCH_STEPS > 0
+ for (int i = 0; i < PARALLAX_RAYMARCHING_SEARCH_STEPS; i++) {
+ }
+#elif defined(PARALLAX_RAYMARCHING_INTERPOLATE)
+ float prevDifference = prevStepHeight - prevSurfaceHeight;
+ float difference = surfaceHeight - stepHeight;
+ float t = prevDifference / (prevDifference + difference);
+ uvOffset = prevUVOffset - uvDelta * t;
+#endif
+
此循环也执行与原始循环相同的基本工作。调整偏移量和步高,然后采样高度字段。
+ +for (int i = 0; i < PARALLAX_RAYMARCHING_SEARCH_STEPS; i++) {
+ uvOffset -= uvDelta;
+ stepHeight -= stepSize;
+ surfaceHeight = GetParallaxHeight(uv + uvOffset);
+}
+
但每次迭代,UV增量和步长减半。
+ +for (int i = 0; i < PARALLAX_RAYMARCHING_SEARCH_STEPS; i++)
+{
+ uvDelta *= 0.5;
+ stepSize *= 0.5;
+ uvOffset -= uvDelta;
+ stepHeight -= stepSize;
+ surfaceHeight = GetParallaxHeight(uv + uvOffset);
+}
+
同样,如果点在表面之下,我们必须朝相反的方向移动。
+ +uvDelta *= 0.5;
+stepSize *= 0.5;
+if (stepHeight < surfaceHeight) {
+ uvOffset += uvDelta;
+ stepHeight += stepSize;
+}
+else {
+ uvOffset -= uvDelta;
+ stepHeight -= stepSize;
+}
+surfaceHeight = GetParallaxHeight(uv + uvOffset);
+
调整着色器,所以它使用三个搜索步骤
+ +#define PARALLAX_BIAS 0
+//#define PARALLAX_OFFSET_LIMITING
+#define PARALLAX_RAYMARCHING_STEPS 10
#define PARALLAX_RAYMARCHING_INTERPOLATE
#define PARALLAX_RAYMARCHING_SEARCH_STEPS 3
#define PARALLAX_FUNCTION ParallaxRaymarching
-```
+
结果看起来相当不错,但仍不完美。二分法搜索可以比简单的插值处理较浅的角度,但仍然需要相当多的搜索步骤,以摆脱分层。所以这是一个试验的问题,找出哪种方法在特定情况下最有效,需要多少步骤。
-### 缩放对象和动态批处理 +尽管我们的视差映射方法似乎可行,但存在一个隐藏的错误。 而且还把错误显示出来了。它显示了何时使用动态批处理来组合已缩放的对象。 例如,给我们的四边形一个像\((10,10,10)\)的比例,然后复制它,将副本移到它下面一点。 假设在播放器设置中启用了此选项,这将触发Unity动态批处理四边形。
-批处理开始时,视差效果将扭曲。 旋转相机时,这一点非常明显。 但是,这仅发生在游戏视图和构建中,而不发生在场景视图中。 请注意,standard着色器也存在此问题,但是当使用弱偏移视差效果时,您可能不会立即注意到它。 +批处理开始时,视差效果将扭曲。 旋转相机时,这一点非常明显。 但是,这仅发生在游戏视图和构建中,而不发生在场景视图中。 请注意,standard着色器也存在此问题,但是当使用弱偏移视差效果时,您可能不会立即注意到它。
-{% include img-picture.html -url1="posts/2018/month1/catRender20/1692664-20200618012954391-1540920343.png" -des="动态批处理会产生奇怪的结果" -width="250" -%} + +在批处理将它们合并到一个单一的网格中之后,Unity不能标准化处理后的几何法向量和切向量。因此顶点数据正确的假设不再成立。
-顶点法向量和切向量没有规范化不是什么大的问题,因为我们在顶点程序中将视图向量转换到切线空间。对于其他所有内容,数据在使用之前都要标准化。 +顶点法向量和切向量没有规范化不是什么大的问题,因为我们在顶点程序中将视图向量转换到切线空间。对于其他所有内容,数据在使用之前都要标准化。
-解决方法是在构造对象转换到切线矩阵之前对向量进行归一化。 因为只有动态批处理的缩放几何才需要此选项,所以根据是否定义了PARALLAX_SUPPORT_SCALED_DYNAMIC_BATCHING,将其设为可选。 +解决方法是在构造对象转换到切线矩阵之前对向量进行归一化。 因为只有动态批处理的缩放几何才需要此选项,所以根据是否定义了PARALLAX_SUPPORT_SCALED_DYNAMIC_BATCHING,将其设为可选。
-```hlsl -#if defined (_PARALLAX_MAP) - #if defined(PARALLAX_SUPPORT_SCALED_DYNAMIC_BATCHING) - v.tangent.xyz = normalize(v.tangent.xyz); - v.normal = normalize(v.normal); - #endif - float3x3 objectToTangent = float3x3( - v.tangent.xyz, - cross(v.normal, v.tangent.xyz) * v.tangent.w, - v.normal - ); - i.tangentViewDir = mul(objectToTangent, ObjSpaceViewDir(v.vertex)); -#endif -``` +#if defined (_PARALLAX_MAP)
+ #if defined(PARALLAX_SUPPORT_SCALED_DYNAMIC_BATCHING)
+ v.tangent.xyz = normalize(v.tangent.xyz);
+ v.normal = normalize(v.normal);
+ #endif
+ float3x3 objectToTangent = float3x3(
+ v.tangent.xyz,
+ cross(v.normal, v.tangent.xyz) * v.tangent.w,
+ v.normal
+ );
+ i.tangentViewDir = mul(objectToTangent, ObjSpaceViewDir(v.vertex));
+#endif
+
#define PARALLAX_BIAS 0
+//#define PARALLAX_OFFSET_LIMITING
+#define PARALLAX_RAYMARCHING_STEPS 10
#define PARALLAX_RAYMARCHING_INTERPOLATE
#define PARALLAX_RAYMARCHING_SEARCH_STEPS 3
#define PARALLAX_FUNCTION ParallaxRaymarching
#define PARALLAX_SUPPORT_SCALED_DYNAMIC_BATCHING
-```
-{% include img-picture.html
-url1="posts/2018/month1/catRender20/batched-correct.png"
-des="动态批量与正确的结果"
-width="250"
-%}]]>catlikecoding Unity GPU Instance(翻译十九) 2018-01-24T20:00:00+08:00 2018-01-24T20:00:00+08:00 https://www.damonc.top/Unity_GPU_Instance
指示GPU绘制需要花时间;向其传递mesh和material属性也要花时间。现在已知两种节省Draw Call的方式:static和dynamic batching
-Unity可以将多个静态物体的网格合并为一个更大的静态网格,从而减少draw call。 **注意:** 只有使用相同材质的对象才能以这种方式组合。 这是以必须存储更多网格数据为代价的。 启用动态批处理后,Unity在运行时会对视图中的动态对象执行相同的操作。 这仅适用于小型网格物体,否则开销将变得太大。 +Unity可以将多个静态物体的网格合并为一个更大的静态网格,从而减少draw call。 注意: 只有使用相同材质的对象才能以这种方式组合。 这是以必须存储更多网格数据为代价的。 启用动态批处理后,Unity在运行时会对视图中的动态对象执行相同的操作。 这仅适用于小型网格物体,否则开销将变得太大。
-还有另一种组合draw call的方法:GPU instance或Geometry instance。与动态批处理一样,此操作在运行时针对可见对象。 它的目标是让GPU一次性渲染同一网格的多个副本。 因此,它不能组合不同的网格或材质。 +还有另一种组合draw call的方法:GPU instance或Geometry instance。与动态批处理一样,此操作在运行时针对可见对象。 它的目标是让GPU一次性渲染同一网格的多个副本。 因此,它不能组合不同的网格或材质。
-### 创建大量球体 +im title?
test test test.
test test test.
-test test test!!dfS:FDFH&*YER#.
-```
-{: .tip-info}
-
-```hlsl
-using UnityEngine;
-
-public class GPUInstancingTest : MonoBehaviour {
- public Transform prefab;
- public int instances = 5000;
- public float radius = 50f;
- //单位圆内随机一点并放大坐标50倍,生成5000个球体
- //然后查看statistics统计的draw Call信息
- void Start () {
- for (int i = 0; i < instances; i++) {
- Transform t = Instantiate(prefab);
- t.localPosition = Random.insideUnitSphere * radius;
- t.SetParent(transform);
- }
- }
-}
-```
-
-使用forward render path统计到的draw call,去掉背景和camera Effect两个draw call:
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender19/1692664-20200612010902826-821880737.png"
- des="5000 draw call"
- width="250"
-%}
-
-但是当使用cube代替球体
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender19/1692664-20200612010904552-338192039.png"
- des="6 draw call"
- width="250"
-%}
-
-### 支持Instance
-
-默认情况下,GPU Instance不会开启,必须设计shader以支持它。 即使这样,也必须为每种材料显式启用实例化。 Unity的standard着色器有一个开关。像标准着色器的GUI一样,我们将为shader扩展面板创建“高级选项”部分。 可以通过调用MaterialEditor.EnableInstancingField方法来添加切换。
-
-```hlsl
-void DoAdvanced () {
- GUILayout.Label("Advanced Options", EditorStyles.boldLabel);
- editor.EnableInstancingField();
-}
-```
-
-仅当shader实际支持instance时,才会显示该切换。 我们可以通过将#pragma multi_compile_instancing指令添加到着色器base-pass启用此支持。 这将为一些关键字启用着色器变体,自定义关键字_INSTANCING_ON_,其他关键字也可以。
-
-```hlsl
-#pragma multi_compile_fwdbase
+test test test!!dfS:FDFH&*YER#.
+
using UnityEngine;
+
+public class GPUInstancingTest : MonoBehaviour {
+ public Transform prefab;
+ public int instances = 5000;
+ public float radius = 50f;
+ //单位圆内随机一点并放大坐标50倍,生成5000个球体
+ //然后查看statistics统计的draw Call信息
+ void Start () {
+ for (int i = 0; i < instances; i++) {
+ Transform t = Instantiate(prefab);
+ t.localPosition = Random.insideUnitSphere * radius;
+ t.SetParent(transform);
+ }
+ }
+}
+
使用forward render path统计到的draw call,去掉背景和camera Effect两个draw call:
+ + +但是当使用cube代替球体
+ + +默认情况下,GPU Instance不会开启,必须设计shader以支持它。 即使这样,也必须为每种材料显式启用实例化。 Unity的standard着色器有一个开关。像标准着色器的GUI一样,我们将为shader扩展面板创建“高级选项”部分。 可以通过调用MaterialEditor.EnableInstancingField方法来添加切换。
+ +void DoAdvanced () {
+ GUILayout.Label("Advanced Options", EditorStyles.boldLabel);
+ editor.EnableInstancingField();
+}
+
仅当shader实际支持instance时,才会显示该切换。 我们可以通过将#pragma multi_compile_instancing指令添加到着色器base-pass启用此支持。 这将为一些关键字启用着色器变体,自定义关键字_INSTANCING_ON_,其他关键字也可以。
+ +#pragma multi_compile_fwdbase
#pragma multi_compile_fog
#pragma multi_compile_instancing
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender19/1692664-20200612010905493-1524350499.png"
- url2="posts/2018/month1/catRender19/1692664-20200612010906722-1492830859.png"
- des="合并了,但是显示有错误"
- width="250"
-%}
-
-批处理数量已减少到42,这意味着现在仅用40个批处理即可渲染所有5000个球体。帧速率也高达80 fps,但是只有几个球体可见。**错误原因**:虽然5000个球体仍在渲染,但是在合批中同一批次的所有球体的顶点转换时都使用了同一个位置:它们都使用同一批次中第一个球的转换矩阵。 发生这种情况是因为现在同一批中所有球体的矩阵都作为数组发送到GPU。 在不告知着色器要使用哪个数组索引的情况下,它始终使用第一个索引。
-
-### Instance IDs
-
-上述错误解决办法:每个Instance相对应的数组索引称为其Instance ID,GPU通过顶点数据将其传递到着色器的vertex程序。在大多数平台上,它是一个无符号整数,名为instanceID,具有SV_InstanceID语义。 我们可以简单地使用_UNITY_VERTEX_INPUT_INSTANCE_ID_宏将其包含在我们的VertexData结构中。 它在UnityCG中包含的_UnityInstancing.cginc_文件中定义。 它为我们提供了实例ID的正确定义,或者在未启用实例化时不提供任何内容。将其添加到VertexData结构。
-
-```hlsl
-struct VertexData {
- UNITY_VERTEX_INPUT_INSTANCE_ID
- float4 vertex : POSITION;
-};
-```
-
-启用instance后,我们现在可以在顶点程序中访问instanceID。 有了它,我们可以在变换顶点位置时使用正确的矩阵。 但是,UnityObjectToClipPos函数没有矩阵参数,它函数内部始终使用unity_ObjectToWorld矩阵。要解决此问题,UnityInstancing包含文件会使用矩阵数组的宏覆盖unity_ObjectToWorld。 _这可以被认为是肮脏的宏技巧,但无需更改现有着色器代码即可工作,从而确保了向后兼容性_。
-
-要使它工作,instance的数组索引必须对所有着色器代码全局可用。必须通过_UNITY_SETUP_INSTANCE_ID_宏进行手动设置,该宏必须在vertex程序最先计算,然后再执行其他的代码。
-
-```hlsl
-InterpolatorsVertex MyVertexProgram (VertexData v) {
- InterpolatorsVertex i;
- UNITY_INITIALIZE_OUTPUT(Interpolators, i);
- UNITY_SETUP_INSTANCE_ID(v);
- i.pos = UnityObjectToClipPos(v.vertex);
-}
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender19/1692664-20200612010908398-221325174.png"
- des="矩阵替换内部实现?"
- width="250"
-%}
-
-```hlsl
-//UnityInstancing中的实际代码要复杂得多。 它要处理平台差异,其他使用实例化的方法以及用于立
-//体声渲染的特殊代码,从而导致间接定义的多个步骤。 它还必须重新定义UnityObjectToClipPos,因
-//为UnityCG首先包含UnityShaderUtilities。
-//缓冲区宏将在后面说明。
-static uint unity_InstanceID;
-
-CBUFFER_START(UnityDrawCallInfo)
- // Where the current batch starts within the instanced arrays.
- int unity_BaseInstanceID;
-CBUFFER_END
-
-#define UNITY_VERTEX_INPUT_INSTANCE_ID uint instanceID : SV_InstanceID;
-
-#define UNITY_SETUP_INSTANCE_ID(input) \
+
批处理数量已减少到42,这意味着现在仅用40个批处理即可渲染所有5000个球体。帧速率也高达80 fps,但是只有几个球体可见。错误原因:虽然5000个球体仍在渲染,但是在合批中同一批次的所有球体的顶点转换时都使用了同一个位置:它们都使用同一批次中第一个球的转换矩阵。 发生这种情况是因为现在同一批中所有球体的矩阵都作为数组发送到GPU。 在不告知着色器要使用哪个数组索引的情况下,它始终使用第一个索引。
+ +上述错误解决办法:每个Instance相对应的数组索引称为其Instance ID,GPU通过顶点数据将其传递到着色器的vertex程序。在大多数平台上,它是一个无符号整数,名为instanceID,具有SV_InstanceID语义。 我们可以简单地使用_UNITY_VERTEX_INPUT_INSTANCE_ID_宏将其包含在我们的VertexData结构中。 它在UnityCG中包含的_UnityInstancing.cginc_文件中定义。 它为我们提供了实例ID的正确定义,或者在未启用实例化时不提供任何内容。将其添加到VertexData结构。
+ +struct VertexData {
+ UNITY_VERTEX_INPUT_INSTANCE_ID
+ float4 vertex : POSITION;
+};
+
启用instance后,我们现在可以在顶点程序中访问instanceID。 有了它,我们可以在变换顶点位置时使用正确的矩阵。 但是,UnityObjectToClipPos函数没有矩阵参数,它函数内部始终使用unity_ObjectToWorld矩阵。要解决此问题,UnityInstancing包含文件会使用矩阵数组的宏覆盖unity_ObjectToWorld。 这可以被认为是肮脏的宏技巧,但无需更改现有着色器代码即可工作,从而确保了向后兼容性。
+ +要使它工作,instance的数组索引必须对所有着色器代码全局可用。必须通过_UNITY_SETUP_INSTANCE_ID_宏进行手动设置,该宏必须在vertex程序最先计算,然后再执行其他的代码。
+ +InterpolatorsVertex MyVertexProgram (VertexData v) {
+ InterpolatorsVertex i;
+ UNITY_INITIALIZE_OUTPUT(Interpolators, i);
+ UNITY_SETUP_INSTANCE_ID(v);
+ i.pos = UnityObjectToClipPos(v.vertex);
+}
+
//UnityInstancing中的实际代码要复杂得多。 它要处理平台差异,其他使用实例化的方法以及用于立
+//体声渲染的特殊代码,从而导致间接定义的多个步骤。 它还必须重新定义UnityObjectToClipPos,因
+//为UnityCG首先包含UnityShaderUtilities。
+//缓冲区宏将在后面说明。
+static uint unity_InstanceID;
+
+CBUFFER_START(UnityDrawCallInfo)
+ // Where the current batch starts within the instanced arrays.
+ int unity_BaseInstanceID;
+CBUFFER_END
+
+#define UNITY_VERTEX_INPUT_INSTANCE_ID uint instanceID : SV_InstanceID;
+
+#define UNITY_SETUP_INSTANCE_ID(input) \
unity_InstanceID = input.instanceID + unity_BaseInstanceID;
-
-// Redefine some of the built-in variables
-// macros to make them work with instancing.
-UNITY_INSTANCING_CBUFFER_START(PerDraw0)
- float4x4 unity_ObjectToWorldArray[UNITY_INSTANCED_ARRAY_SIZE];
- float4x4 unity_WorldToObjectArray[UNITY_INSTANCED_ARRAY_SIZE];
-UNITY_INSTANCING_CBUFFER_END
-#define unity_ObjectToWorld unity_ObjectToWorldArray[unity_InstanceID]
+
+// Redefine some of the built-in variables
+// macros to make them work with instancing.
+UNITY_INSTANCING_CBUFFER_START(PerDraw0)
+ float4x4 unity_ObjectToWorldArray[UNITY_INSTANCED_ARRAY_SIZE];
+ float4x4 unity_WorldToObjectArray[UNITY_INSTANCED_ARRAY_SIZE];
+UNITY_INSTANCING_CBUFFER_END
+#define unity_ObjectToWorld unity_ObjectToWorldArray[unity_InstanceID]
#define unity_WorldToObject unity_WorldToObjectArray[unity_InstanceID]
-```
+
每台设备不一样,最终得到的批次数量可能与当前实验得到的数量不同。现在这情况下,以40批渲染5000个球体实例,这意味着每批125个球体。
-每个批次都需要自己的矩阵数组。 此数据发送到GPU并存储在内存缓冲区中,在Direct3D中称为常量缓冲区,在OpenGL中称为统一缓冲区。 这些**缓冲区具有最大大小**,这限制了一批中可以容纳多少个实例。 假设台式机GPU每个缓冲区的限制为64KB。 +每个批次都需要自己的矩阵数组。 此数据发送到GPU并存储在内存缓冲区中,在Direct3D中称为常量缓冲区,在OpenGL中称为统一缓冲区。 这些缓冲区具有最大大小,这限制了一批中可以容纳多少个实例。 假设台式机GPU每个缓冲区的限制为64KB。
-一个矩阵由16个浮点数组成,每个浮点数均为4个字节。 因此,每个矩阵64个字节。 每个实例都需要一个对象到世界的转换矩阵。 但是,我们还需要一个世界到对象的矩阵来转换法线向量。 因此,最终每个实例有128个字节。 这导致最大批处理大小为“ 64000/128 = 500”,这只能在10个批处理中渲染5000个球体。 +一个矩阵由16个浮点数组成,每个浮点数均为4个字节。 因此,每个矩阵64个字节。 每个实例都需要一个对象到世界的转换矩阵。 但是,我们还需要一个世界到对象的矩阵来转换法线向量。 因此,最终每个实例有128个字节。 这导致最大批处理大小为“ 64000/128 = 500”,这只能在10个批处理中渲染5000个球体。
-``` -内存单位是2进制,所以1KB表示1024字节,而不是1000。因此,'(64 * 1024)/ 128 = 512 '。UNITY_INSTANCED_ARRAY_SIZE默认定义为500,但您可以使用编译器指令覆盖它。例如,#pragma instancing_options maxcount:512将最大值设置为512。但是,这将导致断言失败错误,因此实际限制为511。到目前为止,500和512之间没有太大的差别。 -``` +内存单位是2进制,所以1KB表示1024字节,而不是1000。因此,'(64 * 1024)/ 128 = 512 '。UNITY_INSTANCED_ARRAY_SIZE默认定义为500,但您可以使用编译器指令覆盖它。例如,#pragma instancing_options maxcount:512将最大值设置为512。但是,这将导致断言失败错误,因此实际限制为511。到目前为止,500和512之间没有太大的差别。
+
即使假设台式机的最大容量为64KB成立,但是大多数移动设备的最大容量远远达不到64,可能仅为16KB。 Unity通过在针对OpenGL ES 3,OpenGL Core或Metal时将最大值除以四来解决此问题。 因为我在编辑器中使用的是OpenGL Core,所以最终的最大批处理大小为“ 500/4 = 125”。
+ +可以通过添加编译器指令#pragma instancing_options force_same_maxcount_for_gl来禁用此自动减少功能。 多个instance选项组合在同一指令中。 但是,这可能会导致在部署到移动设备上时发生故障,因此请小心使用。
-``` -那假设均等缩放选项呢? +那假设均等缩放选项呢?
可以使用#pragma instancing_options指示所有instance对象具有统一的缩放比例。 这消除了将世界到对象矩阵用于法线转换的需要(少存储一个矩阵)。 设置此选项后,虽然UnityObjectToWorldNormal函数确实会更改其行为,但它不会消除第二个矩阵数组。 因此,至少在Unity 2017.1.0中,此选项实际上没有任何作用。
-```
+
到目前为止,一直没有阴影。 重新打开主阴影的Soft shadow,并确保阴影距离足以包含所有球体
-{% include img-picture.html - url1="posts/2018/month1/catRender19/1692664-20200612010909964-776430193.png" - des="批处理爆炸" - width="250" -%} + +为大量物体渲染阴影会增加GPU耗能。但是我们也可以在渲染球体阴影时使用GPU instance。在shadow caster-pass中添加instance指令;同时也增加UNITY_VERTEX_INPUT_INSTANCE_ID
and UNITY_SETUP_INSTANCE_ID
#pragma multi_compile_shadowcaster
#pragma multi_compile_instancing
-struct VertexData {
- UNITY_VERTEX_INPUT_INSTANCE_ID
-};
+struct VertexData {
+ UNITY_VERTEX_INPUT_INSTANCE_ID
+};
-InterpolatorsVertex MyShadowVertexProgram (VertexData v) {
- InterpolatorsVertex i;
- UNITY_SETUP_INSTANCE_ID(v);
-}
-```
+InterpolatorsVertex MyShadowVertexProgram (VertexData v) {
+ InterpolatorsVertex i;
+ UNITY_SETUP_INSTANCE_ID(v);
+}
+
我们仅在base-pass和shadow caster-pass中添加了instance支持。 因此,批处理不适用于其他光源。 要验证这一点,停用主光源并添加一些会影响多个球体的聚光灯或点光源。 不要为它们打开阴影,因为那样会降低帧速率。
-{% include img-picture.html - url1="posts/2018/month1/catRender19/1692664-20200612010913312-1389275661.png" - des="批处理爆炸" - width="250" -%} + +上图,完全不支持多光源批处理。 要将instance与多个光源结合使用,只能切换到延迟渲染路径。 为此,请将所需的编译器指令添加到着色器的延迟传递中。
-```hlsl -#pragma multi_compile_prepassfinal +#pragma multi_compile_prepassfinal
#pragma multi_compile_instancing
-```
+
所有批处理都有一个限制:它们仅限于具有相同材料的对象。 当我们希望渲染的对象具有多样性时,此限制就会成为问题。
-### 随机着色 +随机改变球体的颜色
-```hlsl -void Start () { - for (int i = 0; i < instances; i++) { - Transform t = Instantiate(prefab); - t.localPosition = Random.insideUnitSphere * radius; - t.SetParent(transform); - t.GetComponentvoid Start () {
+ for (int i = 0; i < instances; i++) {
+ Transform t = Instantiate(prefab);
+ t.localPosition = Random.insideUnitSphere * radius;
+ t.SetParent(transform);
+ t.GetComponent<MeshRenderer>().material.color = new Color(Random.value, Random.value, Random.value);
+ }
+}
+
即使我们为物料启用了批处理,它也不再起作用。由于每个球体现在都有自己的材质,因此每个球体的着色器状态也必被更改。 这显示在统计面板中为SetPass call的数量。在这修改之前只有少量几个批次渲染,但是现在是5000加批次。
-### 材质属性块-Material Property Blocks +除了为每个球体创建新的材质实例外,我们还可以使用材质属性块。 这些是小的修改,设置属性块的颜色并将其传递给球体的渲染器,而不是直接分配材质的颜色。MaterialPropertyBlock官网介绍;
+渲染instance对象时,Unity通过数组形式将颜色数据传递到GPU内存并转换矩阵,Unity对存储在材料属性块中的属性执行相同的操作,但要使其起作用的话,我们必须在shader中定义一个instance的缓冲区。
-渲染instance对象时,Unity通过数组形式将颜色数据传递到GPU内存并转换矩阵,Unity对存储在材料属性块中的属性执行相同的操作,但要使其起作用的话,我们必须在shader中定义一个instance的缓冲区。 +声明instance缓冲区的工作类似于创建诸如插值器之类的结构,但是确切的语法因平台而异。 我们可以使用UNITY_INSTANCING_CBUFFER_START和UNITY_INSTANCING_CBUFFER_END宏来解决差异。 启用实例化后,它们将不执行任何操作。
-声明instance缓冲区的工作类似于创建诸如插值器之类的结构,但是确切的语法因平台而异。 我们可以使用UNITY_INSTANCING_CBUFFER_START和UNITY_INSTANCING_CBUFFER_END宏来解决差异。 启用实例化后,它们将不执行任何操作。 +将_Color变量的定义放在instance缓冲区中。 UNITY_INSTANCING_CBUFFER_START宏需要一个名称参数,实际名称无关紧要但要注意避免重名冲突。 宏以UNITY_INSTANCING_为其前缀。
-将_Color变量的定义放在instance缓冲区中。 UNITY_INSTANCING_CBUFFER_START宏需要一个名称参数,实际名称无关紧要但要注意避免重名冲突。 宏以UNITY_INSTANCING_为其前缀。 +UNITY_INSTANCING_CBUFFER_START(InstanceProperties)
+ float4 _Color;
+UNITY_INSTANCING_CBUFFER_END
+
像变换矩阵一样,启用instance后,颜色数据作为数组上传到GPU。UNITY_DEFINE_INSTANCED_PROP宏会为我们处理正确的声明语法。
-像变换矩阵一样,启用instance后,颜色数据作为数组上传到GPU。UNITY_DEFINE_INSTANCED_PROP宏会为我们处理正确的声明语法。 +UNITY_INSTANCING_CBUFFER_START(InstanceProperties)
+ //float4 _Color;
+ UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
+UNITY_INSTANCING_CBUFFER_END
+
最后要访问fragment程序中的数组,我们还需要在其中知道instanceID。 因此,将其添加到插值器结构中。
-最后要访问fragment程序中的数组,我们还需要在其中知道instanceID。 因此,将其添加到插值器结构中。 +struct InterpolatorsVertex {
+ UNITY_VERTEX_INPUT_INSTANCE_ID
+};
+struct Interpolators {
+ UNITY_VERTEX_INPUT_INSTANCE_ID
+};
+
在vertex顶点程序中,将ID从顶点数据复制到插值器。 启用实例化时,UNITY_TRANSFER_INSTANCE_ID宏定义此简单操作,否则不执行任何操作。
-在vertex顶点程序中,将ID从顶点数据复制到插值器。 启用实例化时,UNITY_TRANSFER_INSTANCE_ID宏定义此简单操作,否则不执行任何操作。 +InterpolatorsVertex MyVertexProgram (VertexData v) {
+ InterpolatorsVertex i;
+ UNITY_INITIALIZE_OUTPUT(Interpolators, i);
+ UNITY_SETUP_INSTANCE_ID(v);
+ UNITY_TRANSFER_INSTANCE_ID(v, i);
+}
+
在片段程序的开头,使ID全局可用,就像在顶点程序中一样。
-在片段程序的开头,使ID全局可用,就像在顶点程序中一样。 +FragmentOutput MyFragmentProgram (Interpolators i) {
+ UNITY_SETUP_INSTANCE_ID(i);
+}
+
现在,我们必须在不使用instance时以_Color的形式访问颜色,而在启用实例化时以_Color [unity_InstanceID]的形式访问颜色。 使用UNITY_ACCESS_INSTANCED_PROP宏可同时支持上述两种访问。
-现在,我们必须在不使用instance时以_Color的形式访问颜色,而在启用实例化时以_Color [unity_InstanceID]的形式访问颜色。 使用UNITY_ACCESS_INSTANCED_PROP宏可同时支持上述两种访问。 +float3 GetAlbedo (Interpolators i) {
+ float3 albedo =
+ tex2D(_MainTex, i.uv.xy).rgb * UNITY_ACCESS_INSTANCED_PROP(_Color).rgb;
+}
+float GetAlpha (Interpolators i) {
+ float alpha = UNITY_ACCESS_INSTANCED_PROP(_Color).a;
+}
+
新版本如果编译有错误:
从2017.3及以上版本, UNITY_ACCESS_INSTANCED_PROP macro改了它需要的两个参数:buffer名,颜色名使用UNITY_ACCESS_INSTANCED_PROP(InstanceProperties, _Color).
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender19/1692664-20200612010918398-38688989.png"
- width="250"
-%}
-
-现在,我们的颜色随机的球再次被批处理。 我们**可以用相同的方式使其他属性可变。 对于颜色,浮点数,矩阵和四分量浮点向量,这是可能的**。 **如果要改变纹理,可以使用单独的纹理数组**,并将索引添加到实例化缓冲区。其他属性修改类似。
-
-可以在同一个缓冲区中组合多个属性,但要牢记大小限制。 还应注意,缓冲区被划分为32位块,因此单个浮点数需要与向量相同的空间。 您也可以使用多个缓冲区,但是也有一个限制,它们不是免费提供的。 启用instance后,每个要缓冲的属性都将成为一个数组,因此仅对需要根据instance变化的属性执行此操作。
-
-### 阴影
-
-我们的阴影也取决于颜色。 调整shader阴影以便每个实例也可以支持唯一的颜色。
-
-```hlsl
-//float4 _Color;
-UNITY_INSTANCING_CBUFFER_START(InstanceProperties)
- UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
-UNITY_INSTANCING_CBUFFER_END
-
-struct InterpolatorsVertex {
- UNITY_VERTEX_INPUT_INSTANCE_ID
-};
-struct Interpolators {
- UNITY_VERTEX_INPUT_INSTANCE_ID
-};
-float GetAlpha (Interpolators i) {
- float alpha = UNITY_ACCESS_INSTANCED_PROP(_Color).a;
-}
-InterpolatorsVertex MyShadowVertexProgram (VertexData v) {
- InterpolatorsVertex i;
- UNITY_SETUP_INSTANCE_ID(v);
- UNITY_TRANSFER_INSTANCE_ID(v, i);
-}
-float4 MyShadowFragmentProgram (Interpolators i) : SV_TARGET {
- UNITY_SETUP_INSTANCE_ID(i);
-}
-```
-
-### LOD Instance
-
-```csharp
-void Start () {
- MaterialPropertyBlock properties = new MaterialPropertyBlock();
- for (int i = 0; i < instances; i++)
- {
- Transform t = Instantiate(prefab);
- t.localPosition = Random.insideUnitSphere * radius;
- t.SetParent(transform);
- //MaterialPropertyBlock properties = new MaterialPropertyBlock();
- properties.SetColor
- (
- "_Color", new Color(Random.value, Random.value, Random.value)
- );
- //t.GetComponent().SetPropertyBlock(properties);
- MeshRenderer r = t.GetComponent();
- if (r != null)
- {
- r.SetPropertyBlock(properties);
- }
- else
- {
- //对LOD子对象设置颜色
- for (int ci = 0; ci < t.childCount; ci++) {
- r = t.GetChild(ci).GetComponent();
- if (r) {
- r.SetPropertyBlock(properties);
- }
- }
- }
- }
-}
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender19/1692664-20200612010919954-2006515171.png"
- width="250"
-%}
-
-不幸的是没有有效的批处理。Unity能够对以相同的LOD颜色球体进行批处理,但是如果可以像往常一样进行批处理会更好。 我们可以通过用缓冲数组替换unity_LODFade来实现。可以通过为支持实例化的每个过程添加lodfade实例化选项来指示Unity的着色器代码执行此操作。
-
-```hlsl
-#pragma multi_compile_instancing
+
现在,我们的颜色随机的球再次被批处理。 我们可以用相同的方式使其他属性可变。 对于颜色,浮点数,矩阵和四分量浮点向量,这是可能的。 如果要改变纹理,可以使用单独的纹理数组,并将索引添加到实例化缓冲区。其他属性修改类似。
+ +可以在同一个缓冲区中组合多个属性,但要牢记大小限制。 还应注意,缓冲区被划分为32位块,因此单个浮点数需要与向量相同的空间。 您也可以使用多个缓冲区,但是也有一个限制,它们不是免费提供的。 启用instance后,每个要缓冲的属性都将成为一个数组,因此仅对需要根据instance变化的属性执行此操作。
+ +我们的阴影也取决于颜色。 调整shader阴影以便每个实例也可以支持唯一的颜色。
+ +//float4 _Color;
+UNITY_INSTANCING_CBUFFER_START(InstanceProperties)
+ UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
+UNITY_INSTANCING_CBUFFER_END
+
+struct InterpolatorsVertex {
+ UNITY_VERTEX_INPUT_INSTANCE_ID
+};
+struct Interpolators {
+ UNITY_VERTEX_INPUT_INSTANCE_ID
+};
+float GetAlpha (Interpolators i) {
+ float alpha = UNITY_ACCESS_INSTANCED_PROP(_Color).a;
+}
+InterpolatorsVertex MyShadowVertexProgram (VertexData v) {
+ InterpolatorsVertex i;
+ UNITY_SETUP_INSTANCE_ID(v);
+ UNITY_TRANSFER_INSTANCE_ID(v, i);
+}
+float4 MyShadowFragmentProgram (Interpolators i) : SV_TARGET {
+ UNITY_SETUP_INSTANCE_ID(i);
+}
+
void Start () {
+ MaterialPropertyBlock properties = new MaterialPropertyBlock();
+ for (int i = 0; i < instances; i++)
+ {
+ Transform t = Instantiate(prefab);
+ t.localPosition = Random.insideUnitSphere * radius;
+ t.SetParent(transform);
+ //MaterialPropertyBlock properties = new MaterialPropertyBlock();
+ properties.SetColor
+ (
+ "_Color", new Color(Random.value, Random.value, Random.value)
+ );
+ //t.GetComponent<MeshRenderer>().SetPropertyBlock(properties);
+ MeshRenderer r = t.GetComponent<MeshRenderer>();
+ if (r != null)
+ {
+ r.SetPropertyBlock(properties);
+ }
+ else
+ {
+ //对LOD子对象设置颜色
+ for (int ci = 0; ci < t.childCount; ci++) {
+ r = t.GetChild(ci).GetComponent<MeshRenderer>();
+ if (r) {
+ r.SetPropertyBlock(properties);
+ }
+ }
+ }
+ }
+}
+
不幸的是没有有效的批处理。Unity能够对以相同的LOD颜色球体进行批处理,但是如果可以像往常一样进行批处理会更好。 我们可以通过用缓冲数组替换unity_LODFade来实现。可以通过为支持实例化的每个过程添加lodfade实例化选项来指示Unity的着色器代码执行此操作。
+ +#pragma multi_compile_instancing
#pragma instancing_options lodfade
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender19/1692664-20200612010921540-656727216.png"
- des="instance LOD fading"
- width="250"
-%}]]>catlikecoding Unity 实时 GI & LPPV & LOD(翻译十八) 2018-01-23T20:00:00+08:00 2018-01-23T20:00:00+08:00 https://www.damonc.top/Unity_RealTime_GI_LOD 0 ? 0.0001 : 0;
-// i.pos = UnityObjectToClipPos(v.vertex);
- i.pos = UnityMetaVertexPosition(
- v.vertex, v.uv1, v.uv2, unity_LightmapST, unity_DynamicLightmapST
- );
-
- i.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
- i.uv.zw = TRANSFORM_TEX(v.uv, _DetailTex);
- return i;
-}
-```
-
-```hlsl
-UnityMetaVertexPosition是什么样子的?
-
-它除了通过unity_MetaVertexControl提供的标志来决定使用哪些坐标集和光照贴图之外,它还做了我们以前做的工作。
-float4 UnityMetaVertexPosition (
- float4 vertex, float2 uv1, float2 uv2,
- float4 lightmapST, float4 dynlightmapST
-) {
- if (unity_MetaVertexControl.x) {
- vertex.xy = uv1 * lightmapST.xy + lightmapST.zw;
- // OpenGL right now needs to actually use incoming vertex position,
- // so use it in a very dummy way
- vertex.z = vertex.z > 0 ? 1.0e-4f : 0.0f;
- }
- if (unity_MetaVertexControl.y) {
- vertex.xy = uv2 * dynlightmapST.xy + dynlightmapST.zw;
- // OpenGL right now needs to actually use incoming vertex position,
- // so use it in a very dummy way
- vertex.z = vertex.z > 0 ? 1.0e-4f : 0.0f;
- }
- return UnityObjectToClipPos(vertex);
-}
-```
-
-请注意,**meta渲染通道既用于烘焙光照贴图,也用于实时光照贴图**。所以当使用实时全局光照的时候,meta渲染通道也将被包含在构建中。
-
-### 对实时光照贴图进行采样
-
-为了对实时光照贴图进行采样,我们还必须将第三个UV坐标添加到My Lightmapping中的VertexData里面。
-
-```hlsl
-struct VertexData {
- float4 vertex : POSITION;
- float3 normal : NORMAL;
- float4 tangent : TANGENT;
- float2 uv : TEXCOORD0;
- float2 uv1 : TEXCOORD1;
- float2 uv2 : TEXCOORD2;
-};
-```
-
-当一张实时光照贴图被使用的时候,我们必须将这个光照贴图的坐标添加到我们的插值器中去。标准着色器在单个插值器中将两个光照贴图的坐标集合组合起来 - 与其他数据复用 - 但是我们可以为两者准备单独的插值器。当_DYNAMICLIGHTMAP_ON_关键字被定义的时候,我们知道有动态光照数据。它是multi_compile_fwdbase编译器指令的关键字列表的一部分。
-
-```hlsl
-struct Interpolators
-{
- #if defined(DYNAMICLIGHTMAP_ON)
- float2 dynamicLightmapUV : TEXCOORD7;
- #endif
-};
-```
-
-填充坐标就像对静态光照贴图的坐标所做的事情一样,除了动态光照图的缩放比例和偏移量的设置以外,这些可以通过unity_DynamicLightmapST变得可用。
-
-```hlsl
-Interpolators MyVertexProgram (VertexData v) {
- #if defined(LIGHTMAP_ON) || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
- i.lightmapUV = v.uv1 * unity_LightmapST.xy + unity_LightmapST.zw;
- #endif
-
- #if defined(DYNAMICLIGHTMAP_ON)
- i.dynamicLightmapUV = v.uv2 * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
- #endif
-}
-```
-
-对实时光照贴图的采样是在我们的CreateIndirectLight函数中完成的。复制 #if defined(LIGHTMAP_ON) 代码块并进行一些更改。 **首先**,新的部分是基于DYNAMICLIGHTMAP_ON关键字的。 **此外**,它应该使用DecodeRealtimeLightmap而不是DecodeLightmap,这是因为实时光照贴图使用不同的颜色格式。而且因为这些数据可能被添加到烘焙光照中,不要立即分配给indirectLight.diffuse,而是使用最后添加的中间变量。 **最后**,当不使用烘焙光照贴图和实时光照贴图的时候,我们只应该对球面谐波进行采样。
-
-```hlsl
-#if defined(LIGHTMAP_ON)
- indirectLight.diffuse = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV));
- #if defined(DIRLIGHTMAP_COMBINED)
- float4 lightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER(
- unity_LightmapInd, unity_Lightmap, i.lightmapUV
- );
- indirectLight.diffuse = DecodeDirectionalLightmap(
- indirectLight.diffuse, lightmapDirection, i.normal
- );
- #endif
-
- ApplySubtractiveLighting(i, indirectLight);
- // #else
- // indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
+
从这篇开始,这个系列教程将由Unity 2017.1.0f3来完成。后续的Shader新特性Unity的旧版本没有,因为我们要使用一个新的着色器函数。
+ +烘焙光照在静态物体上工作的非常好,对于动态几何体,由于有光照探针的缘故,烘焙光照这种方法也能工作的非常好。但是,烘焙光照不能处理动态光源。混合模式的光源可以通过一些实时的调节来消除,但调节的太多使得烘焙出来的间接光照不会改变。所以当你有一个户外场景的话,使用烘焙光照这种方法太阳的光照就不能有变化。太阳不能像在现实生活中一样在天空中移动,因为如果需要太阳在天空中移动的话,就需要逐渐变化的全局光照。所以场景必须一直不变。
+ +为了使间接光照能够在移动的太阳这样的情况发挥作用,Unity使用Enlighten系统来计算实时全局光照。除了在运行时计算光照和光照探针以外,它还采用烘焙间接光照一样的方式来工作。
+ +了解间接光需要知道光在静态表面之间如何反射。重点在于哪些表面可能会受到其他表面的影响,以及程度如何。弄清这些关系需要做很多的工作,不能实时完成。所以这个数据由编辑器处理并存储在运行时使用。然后 Enlighten系统会使用这个数据来计算实时光照贴图和探针数据。即使如此,只有低分辨率的光照贴图才可以在实时情况下运行。
+ +实时全局光照、烘焙全局光照都可以独立启用。你可以同时启用两个,或者启用其中的一个,或者两个都不启用。这两个选项都是通过“光照”窗口的“实时照光照”部分中的复选框启用。
+ + +要实际查看实时全局光照,请将测试场景中的主光源的模式设置为实时模式。 由于我们没有其他光源,即使启用了烘焙光照也能有效地关闭。
+ + +确保场景中的所有对象都使用我们的白色材质。 像上次一样,球体都是动态的,而其他的都是静态几何体。
+ + +事实证明,只有动态对象会受益于实时全局光照。静态物体会变的暗一点。这是因为光照探针自动并入实时全局光照。而静态对象必须对实时的光照贴图进行采样,而这些光照贴图与烘焙的光照贴图不同。我们的着色器还不支持。
+ +Unity在编辑模式下已经生成了实时的光照贴图,所以你可以随时查看实时的全局光照贴图。在编辑模式和播放模式之间进行切换的时候,这些贴图不会被保留,但是它们最终会得到相同的结果。你可以通过“光照”窗口的“对象贴图”选项来选择一个光照贴图静态对象对实时光照贴图进行检查。 选择“实时强度“可以可视化的查看实时光照贴图的数据。
+ + +虽然实时光照贴图已经被烘焙出来,并且它们还可能显示正确,但我们的meta渲染通道实际上使用的是错误的坐标。实时全局光照具有自己的光照贴图坐标,最终可能与静态光照贴图的坐标不同。Unity会根据光照贴图和对象的设置来自动生成这些坐标。这些数据存储在第三套UV中。所以将这些数据添加到My Lightmapping中的VertexData里面。
+ +struct VertexData {
+ float4 vertex : POSITION;
+ float2 uv : TEXCOORD0;
+ float2 uv1 : TEXCOORD1;
+ float2 uv2 : TEXCOORD2;
+};
+
现在,MyLightmappingVertexProgram必须使用第二个或是第三个UV坐标,以及静态或动态光照贴图的大小和偏移量。 我们可以依靠UnityMetaVertexPosition函数来使用正确的数据。
+ +Interpolators MyLightmappingVertexProgram (VertexData v) {
+ Interpolators i;
+// v.vertex.xy = v.uv1 * unity_LightmapST.xy + unity_LightmapST.zw;
+// v.vertex.z = v.vertex.z > 0 ? 0.0001 : 0;
+// i.pos = UnityObjectToClipPos(v.vertex);
+ i.pos = UnityMetaVertexPosition(
+ v.vertex, v.uv1, v.uv2, unity_LightmapST, unity_DynamicLightmapST
+ );
+
+ i.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
+ i.uv.zw = TRANSFORM_TEX(v.uv, _DetailTex);
+ return i;
+}
+
UnityMetaVertexPosition是什么样子的?
+
+它除了通过unity_MetaVertexControl提供的标志来决定使用哪些坐标集和光照贴图之外,它还做了我们以前做的工作。
+float4 UnityMetaVertexPosition (
+ float4 vertex, float2 uv1, float2 uv2,
+ float4 lightmapST, float4 dynlightmapST
+) {
+ if (unity_MetaVertexControl.x) {
+ vertex.xy = uv1 * lightmapST.xy + lightmapST.zw;
+ // OpenGL right now needs to actually use incoming vertex position,
+ // so use it in a very dummy way
+ vertex.z = vertex.z > 0 ? 1.0e-4f : 0.0f;
+ }
+ if (unity_MetaVertexControl.y) {
+ vertex.xy = uv2 * dynlightmapST.xy + dynlightmapST.zw;
+ // OpenGL right now needs to actually use incoming vertex position,
+ // so use it in a very dummy way
+ vertex.z = vertex.z > 0 ? 1.0e-4f : 0.0f;
+ }
+ return UnityObjectToClipPos(vertex);
+}
+
请注意,meta渲染通道既用于烘焙光照贴图,也用于实时光照贴图。所以当使用实时全局光照的时候,meta渲染通道也将被包含在构建中。
+ +为了对实时光照贴图进行采样,我们还必须将第三个UV坐标添加到My Lightmapping中的VertexData里面。
+ +struct VertexData {
+ float4 vertex : POSITION;
+ float3 normal : NORMAL;
+ float4 tangent : TANGENT;
+ float2 uv : TEXCOORD0;
+ float2 uv1 : TEXCOORD1;
+ float2 uv2 : TEXCOORD2;
+};
+
当一张实时光照贴图被使用的时候,我们必须将这个光照贴图的坐标添加到我们的插值器中去。标准着色器在单个插值器中将两个光照贴图的坐标集合组合起来 - 与其他数据复用 - 但是我们可以为两者准备单独的插值器。当_DYNAMICLIGHTMAP_ON_关键字被定义的时候,我们知道有动态光照数据。它是multi_compile_fwdbase编译器指令的关键字列表的一部分。
+ +struct Interpolators
+{
+ #if defined(DYNAMICLIGHTMAP_ON)
+ float2 dynamicLightmapUV : TEXCOORD7;
+ #endif
+};
+
填充坐标就像对静态光照贴图的坐标所做的事情一样,除了动态光照图的缩放比例和偏移量的设置以外,这些可以通过unity_DynamicLightmapST变得可用。
+ +Interpolators MyVertexProgram (VertexData v) {
+ #if defined(LIGHTMAP_ON) || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
+ i.lightmapUV = v.uv1 * unity_LightmapST.xy + unity_LightmapST.zw;
+ #endif
+
+ #if defined(DYNAMICLIGHTMAP_ON)
+ i.dynamicLightmapUV = v.uv2 * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
+ #endif
+}
+
对实时光照贴图的采样是在我们的CreateIndirectLight函数中完成的。复制 #if defined(LIGHTMAP_ON) 代码块并进行一些更改。 首先,新的部分是基于DYNAMICLIGHTMAP_ON关键字的。 此外,它应该使用DecodeRealtimeLightmap而不是DecodeLightmap,这是因为实时光照贴图使用不同的颜色格式。而且因为这些数据可能被添加到烘焙光照中,不要立即分配给indirectLight.diffuse,而是使用最后添加的中间变量。 最后,当不使用烘焙光照贴图和实时光照贴图的时候,我们只应该对球面谐波进行采样。
+ +#if defined(LIGHTMAP_ON)
+ indirectLight.diffuse = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV));
+ #if defined(DIRLIGHTMAP_COMBINED)
+ float4 lightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER(
+ unity_LightmapInd, unity_Lightmap, i.lightmapUV
+ );
+ indirectLight.diffuse = DecodeDirectionalLightmap(
+ indirectLight.diffuse, lightmapDirection, i.normal
+ );
+ #endif
+
+ ApplySubtractiveLighting(i, indirectLight);
+ // #else
+ // indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
+#endif
+
+#if defined(DYNAMICLIGHTMAP_ON)
+ float3 dynamicLightDiffuse = DecodeRealtimeLightmap(
+ UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, i.dynamicLightmapUV)
+ );
+ #if defined(DIRLIGHTMAP_COMBINED)
+ float4 dynamicLightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER(
+ unity_DynamicDirectionality, unity_DynamicLightmap,
+ i.dynamicLightmapUV
+ );
+ indirectLight.diffuse += DecodeDirectionalLightmap(
+ dynamicLightDiffuse, dynamicLightmapDirection, i.normal
+ );
+ #else
+ indirectLight.diffuse += dynamicLightDiffuse;
+ #endif
#endif
-
-#if defined(DYNAMICLIGHTMAP_ON)
- float3 dynamicLightDiffuse = DecodeRealtimeLightmap(
- UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, i.dynamicLightmapUV)
- );
- #if defined(DIRLIGHTMAP_COMBINED)
- float4 dynamicLightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER(
- unity_DynamicDirectionality, unity_DynamicLightmap,
- i.dynamicLightmapUV
- );
- indirectLight.diffuse += DecodeDirectionalLightmap(
- dynamicLightDiffuse, dynamicLightmapDirection, i.normal
- );
- #else
- indirectLight.diffuse += dynamicLightDiffuse;
- #endif
-#endif
-
-#if !defined(LIGHTMAP_ON) && !defined(DYNAMICLIGHTMAP_ON)
- indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
-#endif
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender18/1692664-20200605013216623-414124444.png"
- des="把实时全局光照应用于一切物体之上" width="250"
-%}
-
-现在我们的着色器使用的是实时光照贴图。最初,当使用Distance Shadowmask模式的时候,它的效果可能看起来与使用混合光源的烘焙光照的效果相同。当在播放模式下关闭光源的时候,差异就变得非常明显。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender18/1692664-20200605013217805-1029939389.png"
- des="禁用混合光源以后,间接光照仍然被保留" width="250"
-%}
-
-
-禁用混合光源以后,其间接光照将保持不变。相比之下,实时光照的间接贡献就会消失,并重新出现 - 这是应该出现的情况。 不过,新情况的完全烘焙好可能需要一段时间。 Enlighten系统会逐步调整光照贴图和光照探针。 这种情况发生的速度取决于场景的复杂性和实时全局光照CPU质量设置。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender18/1692664-20200605013219337-640958770.gif"
- des="切换实时光与实时GI"
- width="250"
-%}
-
-所有实时光源都对实时全局光照有贡献。 然而,它的典型用途是那些仅在主要方向上存在光线的光源,比如可以代表太阳,因为它在天空中移动。它适用于方向光源。点光源和聚光光源也能工作,但只是没有阴影。所以**当使用带有阴影的点光源或聚光光源的时候,你可能会遇到不正确的间接光照结果**。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender18/1692664-20200605013220298-1170450196.png"
- url2="posts/2018/month1/catRender18/1692664-20200605013221104-52400640.png"
- des="没有影响的间接光源和实时的聚光光源"
- width="250"
-%}
-
-如果要从实时全局光照里面去掉一个实时光源,可以通过设置它的Indirect Multiplier将它的光强度设置为零。
-
-### 自发光光源
-
-实时全局光照也可以用于自发光的静态物体。这使得可以匹配实时间接光照来改变物体的自发光变得可能。让我们来试试看吧。向场景中添加一个静态球体,并赋予它一个使用我们着色器的材质,这个材质具有黑色的反照率和白色的自发光颜色。最初,我们只能看到通过静态光照贴图实现的自发光的间接效果。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender18/1692664-20200605013222060-1153434303.png"
- des="用自发光球来烘焙全局光照"
- width="250"
-%}
-
-要将自发光光源烘焙到静态光照贴图中,我们必须在我们的着色器的GUI中设置材质的全局光照标志。因为我们总是将标志设置为BakedEmissive,光源最终将以烘焙好的光照贴图的形式出现。如果自发光光源是恒定的这个效果是很不错的,但这样就不允许我们做动画控制。
-
-为了同时对自发光光源支持烘焙和实时光照,我们必须使其可配置化。我们可以通过向MyLightingShaderGUI中添加一个选项来做到这一点,使用的是MaterialEditor.LightmapEmissionProperty方法。这个方法的单个参数是属性的缩进级别。
-
-```hlsl
-void DoEmission () {
- MaterialProperty map = FindProperty("_EmissionMap");
- Texture tex = map.textureValue;
- EditorGUI.BeginChangeCheck();
- editor.TexturePropertyWithHDRColor(
- MakeLabel(map, "Emission (RGB)"), map, FindProperty("_Emission"),
- emissionConfig, false
- );
- editor.LightmapEmissionProperty(2);
- if (EditorGUI.EndChangeCheck()) {
- if (tex != map.textureValue) {
- SetKeyword("_EMISSION_MAP", map.textureValue);
- }
-
- foreach (Material m in editor.targets) {
- m.globalIlluminationFlags =
- MaterialGlobalIlluminationFlags.BakedEmissive;
- }
- }
-}
-```
-
-每次当自发光属性发生改变的时候,我们也必须停止覆盖这个标志位。其实真正要做的事情比这更复杂一点。其中一个标志选项是EmissiveIsBlack,这个表示表示的是自发光计算可以跳过。这个标志总是会针对新材质进行设置。要让间接自发光能够工作,我们必须保证这个标志不被设置,无论我们选择实时光照还是烘焙。我们可以通过总是屏蔽标志值的EmissiveIsBlack位来做到这一点。
-
-```hlsl
-foreach (Material m in editor.targets) {
- m.globalIlluminationFlags &= ~MaterialGlobalIlluminationFlags.EmissiveIsBlack;
-}
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender18/1692664-20200605013222795-276798177.png"
- url2="posts/2018/month1/catRender18/1692664-20200605013223641-2073252267.png"
- des="带有自发光球的实时全局光照效果"
- width="250"
-%}
-
-烘焙全局光照和实时全局光照之间的视觉差异主要是因为实时光照贴图通常具有比烘焙全局光照更低的分辨率。所以当自发光不发生不变化的时候,你也可以使用烘焙全局光照,确保能够利用其更高的分辨率。
-
-```
-EmissiveIsBlack的目的是什么?
+
+#if !defined(LIGHTMAP_ON) && !defined(DYNAMICLIGHTMAP_ON)
+ indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
+#endif
+
现在我们的着色器使用的是实时光照贴图。最初,当使用Distance Shadowmask模式的时候,它的效果可能看起来与使用混合光源的烘焙光照的效果相同。当在播放模式下关闭光源的时候,差异就变得非常明显。
+ + +禁用混合光源以后,其间接光照将保持不变。相比之下,实时光照的间接贡献就会消失,并重新出现 - 这是应该出现的情况。 不过,新情况的完全烘焙好可能需要一段时间。 Enlighten系统会逐步调整光照贴图和光照探针。 这种情况发生的速度取决于场景的复杂性和实时全局光照CPU质量设置。
+ + +所有实时光源都对实时全局光照有贡献。 然而,它的典型用途是那些仅在主要方向上存在光线的光源,比如可以代表太阳,因为它在天空中移动。它适用于方向光源。点光源和聚光光源也能工作,但只是没有阴影。所以当使用带有阴影的点光源或聚光光源的时候,你可能会遇到不正确的间接光照结果。
+ + +如果要从实时全局光照里面去掉一个实时光源,可以通过设置它的Indirect Multiplier将它的光强度设置为零。
+ +实时全局光照也可以用于自发光的静态物体。这使得可以匹配实时间接光照来改变物体的自发光变得可能。让我们来试试看吧。向场景中添加一个静态球体,并赋予它一个使用我们着色器的材质,这个材质具有黑色的反照率和白色的自发光颜色。最初,我们只能看到通过静态光照贴图实现的自发光的间接效果。
+ + +要将自发光光源烘焙到静态光照贴图中,我们必须在我们的着色器的GUI中设置材质的全局光照标志。因为我们总是将标志设置为BakedEmissive,光源最终将以烘焙好的光照贴图的形式出现。如果自发光光源是恒定的这个效果是很不错的,但这样就不允许我们做动画控制。
+ +为了同时对自发光光源支持烘焙和实时光照,我们必须使其可配置化。我们可以通过向MyLightingShaderGUI中添加一个选项来做到这一点,使用的是MaterialEditor.LightmapEmissionProperty方法。这个方法的单个参数是属性的缩进级别。
+ +void DoEmission () {
+ MaterialProperty map = FindProperty("_EmissionMap");
+ Texture tex = map.textureValue;
+ EditorGUI.BeginChangeCheck();
+ editor.TexturePropertyWithHDRColor(
+ MakeLabel(map, "Emission (RGB)"), map, FindProperty("_Emission"),
+ emissionConfig, false
+ );
+ editor.LightmapEmissionProperty(2);
+ if (EditorGUI.EndChangeCheck()) {
+ if (tex != map.textureValue) {
+ SetKeyword("_EMISSION_MAP", map.textureValue);
+ }
+
+ foreach (Material m in editor.targets) {
+ m.globalIlluminationFlags =
+ MaterialGlobalIlluminationFlags.BakedEmissive;
+ }
+ }
+}
+
每次当自发光属性发生改变的时候,我们也必须停止覆盖这个标志位。其实真正要做的事情比这更复杂一点。其中一个标志选项是EmissiveIsBlack,这个表示表示的是自发光计算可以跳过。这个标志总是会针对新材质进行设置。要让间接自发光能够工作,我们必须保证这个标志不被设置,无论我们选择实时光照还是烘焙。我们可以通过总是屏蔽标志值的EmissiveIsBlack位来做到这一点。
+ +foreach (Material m in editor.targets) {
+ m.globalIlluminationFlags &= ~MaterialGlobalIlluminationFlags.EmissiveIsBlack;
+}
+
烘焙全局光照和实时全局光照之间的视觉差异主要是因为实时光照贴图通常具有比烘焙全局光照更低的分辨率。所以当自发光不发生不变化的时候,你也可以使用烘焙全局光照,确保能够利用其更高的分辨率。
+ +EmissiveIsBlack的目的是什么?
这是一个优化,使得计算可以跳过全局光照烘焙过程。然而,只有当自发光颜色确实是黑色的时候,它才依赖于标志。由于这个标志位由着色器的GUI进行设置,这是当材质在检视器里面进行编辑的时候确定的。或者至少,这是Unity的标准着色器的做法。因此,如果自发光颜色稍后被脚本或动画系统更改,则该标志位不会做相应的调整。这是许多人不理解为什么对自发光做动画不会影响到实时全局光照的原因。结果就是如果你想在运行时更改自发光颜色,那么就不要将自发光颜色设置为纯黑色。
我们没有使用这种方法,我们使用的是LightmapEmissionProperty,它还提供了对自发光完全关闭全局光照的选项。 所以这个选择对于用户来说是非常明确的,没有任何隐藏的行为。如果用户不要使用自发光? 那么只要确保它的全局光照被设置为None就可以了。
-```
-
-### 对自发光进行动画控制
+
用于自发光的实时全局光照只能用于静态对象。虽然物体是静态的,但其材质的自发光属性还是可以被动画化,并且将被全局光照系统所捕获到。让我们用一个在自发光颜色为白色和自发光颜色为黑色之间振荡的简单组件来尝试下这个事情。
-public class EmissiveOscillator : MonoBehaviour { - Material emissiveMaterial; - void Start () { - emissiveMaterial = GetComponentusing UnityEngine;
- void Update () {
- Color c = Color.Lerp(
- Color.white, Color.black,
- Mathf.Sin(Time.time * Mathf.PI) * 0.5f + 0.5f
- );
- emissiveMaterial.SetColor("_Emission", c);
- }
-}
-```
+public class EmissiveOscillator : MonoBehaviour {
+ Material emissiveMaterial;
+ void Start () {
+ emissiveMaterial = GetComponent<MeshRenderer>().material;
+ }
-将这个组件添加到我们的自发光球体。在播放模式下,自发光将会动画化,但间接光照不受影响。我们必须通知实时光照系统,它有工作要做。这可以通过调用适当网格渲染器的Renderer.UpdateGIMaterials方法来完成。
+ void Update () {
+ Color c = Color.Lerp(
+ Color.white, Color.black,
+ Mathf.Sin(Time.time * Mathf.PI) * 0.5f + 0.5f
+ );
+ emissiveMaterial.SetColor("_Emission", c);
+ }
+}
+
将这个组件添加到我们的自发光球体。在播放模式下,自发光将会动画化,但间接光照不受影响。我们必须通知实时光照系统,它有工作要做。这可以通过调用适当网格渲染器的Renderer.UpdateGIMaterials方法来完成。
-void Start () { - emissiveRenderer = GetComponentMeshRenderer emissiveRenderer;
+Material emissiveMaterial;
-void Update () {
- emissiveMaterial.SetColor("_Emission", c);
- emissiveRenderer.UpdateGIMaterials();
-}
-```
+void Start () {
+ emissiveRenderer = GetComponent<MeshRenderer>();
+ emissiveMaterial = emissiveRenderer.material;
+}
-{% include img-picture.html
- url1="posts/2018/month1/catRender18/1692664-20200605013226755-1065411582.gif"
- des="动画控制实时GI"
- width="250"
-%}
+void Update () {
+ emissiveMaterial.SetColor("_Emission", c);
+ emissiveRenderer.UpdateGIMaterials();
+}
+
调用UpdateGIMaterials方法会触发物体自发光的完整更新,并使用其meta渲染通道进行渲染。当自发光比纯色更复杂的时候,这是必要的,举个简单的例子来说,比如说我们使用纹理。如果一个纯色就足够了,那么我们可以通过使用渲染器和自发光颜色调用DynamicGI.SetEmissive方法来得到一个比较快捷的计算方式。这比使用meta渲染通道来渲染物体更快,所以在能够使用的时候可以利用这种方法。
-## 光照探针 +//emissiveRenderer.UpdateGIMaterials();
+DynamicGI.SetEmissive(emissiveRenderer, c);
+
烘焙全局光照和实时全局光照都通过光照探针应用于动态对象。物体的位置用于对光探针数据进行插值,然后将其用于全局光照。这对于相当小的物体来说下效果很好,但对于较大的物体来说就太粗糙了。
-{% include img-picture.html - url1="posts/2018/month1/catRender18/1692664-20200605013229006-1893115114.png" - des="对于大型动态物体来说,光照效果不好" - width="250" -%} +举个简单的例子来是说,将做了比较大拉伸的立方体添加到测试场景,以便它可以受到不同的光照条件的影响。它应该使用我们的白色材质。由于它是一个动态立方体,所以最终使用一个点来确定它的全局光照贡献。让我们移动这个点的位置,使得这一点最终处于一个被遮蔽的位置,那么整个立方体就会变黑,这显然是错误的。为了使这一点非常明显,让我们使用一个烘焙主光源,所以所有光照都来自烘焙全局光照和实时全局光照的数据。
-为了使光照探针器适用于这样的情况,我们可以使用光照探针代理体,或者简称为LPPV。这可以通过向着色器发送插值后的探针器数据网格而不是单个插值后的探针器数据来做到。这需要具有线性滤波的浮点数3D纹理,这就将这个方法限制到只能在现代显卡上使用。此外,还要确保在图形层设置中启用LPPV(光照探针代理体)支持。 + +为了使光照探针器适用于这样的情况,我们可以使用光照探针代理体,或者简称为LPPV。这可以通过向着色器发送插值后的探针器数据网格而不是单个插值后的探针器数据来做到。这需要具有线性滤波的浮点数3D纹理,这就将这个方法限制到只能在现代显卡上使用。此外,还要确保在图形层设置中启用LPPV(光照探针代理体)支持。
-### 向物体中添加一个光照探针代理体 + +光照探针代理体可以以各种方式设置,最直接的方法是在作为使用光照探针代理体的物体的一个组件。你可以通过Component / Rendering / Light Probe Proxy Volume来添加它。
-LPPV(光照探针代理体)通过在运行时在光照探针之间进行插值来工作,就好像它们是常规动态对象的网格一样。插值后得到的结果被缓存,刷新模式(Refresh Mode)控制在何时进行更新。默认值为“自动(Automatic)”,这意味着当动态全局光照更改和探针器组发生移动的时候会触发更新。包围盒模式(Bounding Box Mode)控制着代理体的定位。自动本地化(AutomaticLocal )意味着它会去匹配其附着的对象的包围盒。这些默认设置适用于我们的立方体,因此我们将保留这些设置。 + +LPPV(光照探针代理体)通过在运行时在光照探针之间进行插值来工作,就好像它们是常规动态对象的网格一样。插值后得到的结果被缓存,刷新模式(Refresh Mode)控制在何时进行更新。默认值为“自动(Automatic)”,这意味着当动态全局光照更改和探针器组发生移动的时候会触发更新。包围盒模式(Bounding Box Mode)控制着代理体的定位。自动本地化(AutomaticLocal )意味着它会去匹配其附着的对象的包围盒。这些默认设置适用于我们的立方体,因此我们将保留这些设置。
-{% include img-picture.html - url1="posts/2018/month1/catRender18/1692664-20200605013231203-999964017.png" - des="使用一个光照探针代理体而不是常规的探针器" - width="250" -%} +要使我们的立方体实际使用LPPV(光照探针代理体),我们必须将其网格渲染器的光照探针(Light Probes)模式设置为使用光照探针代理体(Use ProxyVolume)。默认行为是使用对象本身的LPPV(光照探针代理体)组件,但也可以强制使用另一个代理体。
-自动分辨率模式(automaticresolution mode)对于我们的拉伸立方体不起作用。 因此,将“分辨率模式(Resolution Mode )”设置为“自定义(Custom )”,并确保立方体的角上有采样点,并沿着其长边有多个样本点。当你选中这个对象的时候,可以看到这些采样点。 + +自动分辨率模式(automaticresolution mode)对于我们的拉伸立方体不起作用。 因此,将“分辨率模式(Resolution Mode )”设置为“自定义(Custom )”,并确保立方体的角上有采样点,并沿着其长边有多个样本点。当你选中这个对象的时候,可以看到这些采样点。
-### 对光照探针代理体进行采样 - -立方体已变黑,因为我们的着色器现在还不支持LPPV(光照探针代理体)采样。为了使其工作,我们必须在CreateIndirectLight函数内调整球面谐波代码。当使用LPPV(光照探针代理体)的时候,_UNITY_LIGHT_PROBE_PROXY_VOLUME_被定义为1。我们在这种情况下什么都不做,看看会发生什么。 - -```hlsl -#if !defined(LIGHTMAP_ON) && !defined(DYNAMICLIGHTMAP_ON) - #if UNITY_LIGHT_PROBE_PROXY_VOLUME - //... - #else - indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1))); - #endif -#endif -``` - -{% include img-picture.html - url1="posts/2018/month1/catRender18/1692664-20200605013233601-1847365803.png" - des="没有更多球面谐波的效果" - width="250" -%} - -得到的结果是所有的球面谐波被禁用,对于不使用LPPV(光照探针代理体)的动态对象也是如此。这是因为_UNITY_LIGHT_PROBE_PROXY_VOLUME_在项目范围内定义,而不是对每个对象实例进行定义。单个对象是否使用LPPV由UnityShaderVariables中定义的unity_ProbeVolumeParams的X分量指定。如果unity_ProbeVolumeParams的X分量设置为1,那么我们有一个LPPV(光照探针代理体),否则我们应该使用常规的球面谐波。 - -```hlsl -#if UNITY_LIGHT_PROBE_PROXY_VOLUME - if (unity_ProbeVolumeParams.x == 1) { - //... - } - else { - indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1))); - } -#else - indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1))); + +立方体已变黑,因为我们的着色器现在还不支持LPPV(光照探针代理体)采样。为了使其工作,我们必须在CreateIndirectLight函数内调整球面谐波代码。当使用LPPV(光照探针代理体)的时候,_UNITY_LIGHT_PROBE_PROXY_VOLUME_被定义为1。我们在这种情况下什么都不做,看看会发生什么。
+ +#if !defined(LIGHTMAP_ON) && !defined(DYNAMICLIGHTMAP_ON)
+ #if UNITY_LIGHT_PROBE_PROXY_VOLUME
+ //...
+ #else
+ indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
+ #endif
#endif
-```
-
-要对光照探针代理体进行采样,我们可以使用SHEvalLinearL0L1_SampleProbeVolume函数而不是ShadeSH9。这个函数在UnityCG中进行定义,并且需要世界空间中的位置作为额外的参数。
-
-```hlsl
-if (unity_ProbeVolumeParams.x == 1) {
- indirectLight.diffuse = SHEvalLinearL0L1_SampleProbeVolume
- (
- float4(i.normal, 1), i.worldPos
- );
- indirectLight.diffuse = max(0, indirectLight.diffuse);
-}
-```
-
-```
-SHEvalLinearL0L1_SampleProbeVolume如何工作?
+
得到的结果是所有的球面谐波被禁用,对于不使用LPPV(光照探针代理体)的动态对象也是如此。这是因为_UNITY_LIGHT_PROBE_PROXY_VOLUME_在项目范围内定义,而不是对每个对象实例进行定义。单个对象是否使用LPPV由UnityShaderVariables中定义的unity_ProbeVolumeParams的X分量指定。如果unity_ProbeVolumeParams的X分量设置为1,那么我们有一个LPPV(光照探针代理体),否则我们应该使用常规的球面谐波。
+ +#if UNITY_LIGHT_PROBE_PROXY_VOLUME
+ if (unity_ProbeVolumeParams.x == 1) {
+ //...
+ }
+ else {
+ indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
+ }
+#else
+ indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
+#endif
+
要对光照探针代理体进行采样,我们可以使用SHEvalLinearL0L1_SampleProbeVolume函数而不是ShadeSH9。这个函数在UnityCG中进行定义,并且需要世界空间中的位置作为额外的参数。
+ +if (unity_ProbeVolumeParams.x == 1) {
+ indirectLight.diffuse = SHEvalLinearL0L1_SampleProbeVolume
+ (
+ float4(i.normal, 1), i.worldPos
+ );
+ indirectLight.diffuse = max(0, indirectLight.diffuse);
+}
+
SHEvalLinearL0L1_SampleProbeVolume如何工作?
_顾名思义,该函数仅包括前两个球面谐波带L0和L1。 Unity不使用LPPV(光照探针代理体)的第三个波带。所以我们得到较低质量的光照近似值,但是我们在多个世界空间中的样本之间进行插值,而不是使用单个点。下面是这个函数的代码。_
-```
-
-```hlsl
-half3 SHEvalLinearL0L1_SampleProbeVolume (half4 normal, float3 worldPos) {
- const float transformToLocal = unity_ProbeVolumeParams.y;
- const float texelSizeX = unity_ProbeVolumeParams.z;
-
- //The SH coefficients textures and probe occlusion
- // are packed into 1 atlas.
- //-------------------------
- //| ShR | ShG | ShB | Occ |
- //-------------------------
-
- float3 position = (transformToLocal == 1.0f) ?
- mul(unity_ProbeVolumeWorldToObject, float4(worldPos, 1.0)).xyz :
- worldPos;
- float3 texCoord = (position - unity_ProbeVolumeMin.xyz) *
- unity_ProbeVolumeSizeInv.xyz;
- texCoord.x = texCoord.x * 0.25f;
-
- // We need to compute proper X coordinate to sample. Clamp the
- // coordinate otherwize we'll have leaking between RGB coefficients
- float texCoordX =
- clamp(texCoord.x, 0.5f * texelSizeX, 0.25f - 0.5f * texelSizeX);
-
- // sampler state comes from SHr (all SH textures share the same sampler)
- texCoord.x = texCoordX;
- half4 SHAr = UNITY_SAMPLE_TEX3D_SAMPLER(
- unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord
- );
- texCoord.x = texCoordX + 0.25f;
- half4 SHAg = UNITY_SAMPLE_TEX3D_SAMPLER(
- unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord
- );
- texCoord.x = texCoordX + 0.5f;
- half4 SHAb = UNITY_SAMPLE_TEX3D_SAMPLER(
- unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord
- );
- // Linear + constant polynomial terms
- half3 x1;
- x1.r = dot(SHAr, normal);
- x1.g = dot(SHAg, normal);
- x1.b = dot(SHAb, normal);
-
- return x1;
-}
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender18/1692664-20200605013235369-787618137.png"
- des="采样后的LPPV(光照探针代理体)的效果,在伽马空间中的效果太暗"
- width="250"
-%}
-
-我们的着色器现在在需要的时候对LPPV(光照探针代理体)进行采样,但结果太暗了。至少在伽马颜色空间中工作就是这样的结果。这是因为球面谐波数据存储在线性空间中。因此,可能需要进行颜色的转换。
-
-```hlsl
-if (unity_ProbeVolumeParams.x == 1) {
- indirectLight.diffuse = SHEvalLinearL0L1_SampleProbeVolume(
- float4(i.normal, 1), i.worldPos
- );
- indirectLight.diffuse = max(0, indirectLight.diffuse);
- #if defined(UNITY_COLORSPACE_GAMMA)
- indirectLight.diffuse = LinearToGammaSpace(indirectLight.diffuse);
- #endif
-}
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender18/1692664-20200605013236310-749557848.png"
- des="采样后的LPPV(光照探针代理体)的效果,带有正确的颜色"
- width="250"
-%}
-
-## LOD Groups
-
-当一个对象最终只覆盖应用程序窗口的一小部分的时候,你不需要高度详细的网格来渲染它。你可以根据对象在视图中的大小使用不同的网格。这被称为细节层次,或简称LOD。Unity允许我们通过组件LOD组来实现这样的功能。
-
-### 创建一个LOD层次结构
-
-这个想法是你在各种不同的LOD等级使用同一网格的多个版本。最高级 - LOD 0 - 具有最多的顶点、子对象、动画、复杂的材质等。随后的级别逐渐变得更简单,更容易计算。在理想情况下,相邻的LOD等级被设计为使得当Unity从一个LOD等级切换到另一个LOD等级的时候,你不能轻易地辨别出它们之间的区别。否则突然有LOD等级变化的时候就会让人很晕。但是在研究这种技术的时候,我们会使用明显的不同的网格。
-
-创建一个空的游戏对象并给它两个子对象。第一个子对象是标准球体,第二个子对象是标准立方体,其大小设置为0.75。 预期的结果看起来像是一个重叠的球体和立方体。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender18/1692664-20200605013237093-2016130218.png"
- url2="posts/2018/month1/catRender18/1692664-20200605013237695-361227834.png"
- des="球体和立方体作为一个对象"
- width="250"
-%}
+
half3 SHEvalLinearL0L1_SampleProbeVolume (half4 normal, float3 worldPos) {
+ const float transformToLocal = unity_ProbeVolumeParams.y;
+ const float texelSizeX = unity_ProbeVolumeParams.z;
+
+ //The SH coefficients textures and probe occlusion
+ // are packed into 1 atlas.
+ //-------------------------
+ //| ShR | ShG | ShB | Occ |
+ //-------------------------
+
+ float3 position = (transformToLocal == 1.0f) ?
+ mul(unity_ProbeVolumeWorldToObject, float4(worldPos, 1.0)).xyz :
+ worldPos;
+ float3 texCoord = (position - unity_ProbeVolumeMin.xyz) *
+ unity_ProbeVolumeSizeInv.xyz;
+ texCoord.x = texCoord.x * 0.25f;
+
+ // We need to compute proper X coordinate to sample. Clamp the
+ // coordinate otherwize we'll have leaking between RGB coefficients
+ float texCoordX =
+ clamp(texCoord.x, 0.5f * texelSizeX, 0.25f - 0.5f * texelSizeX);
+
+ // sampler state comes from SHr (all SH textures share the same sampler)
+ texCoord.x = texCoordX;
+ half4 SHAr = UNITY_SAMPLE_TEX3D_SAMPLER(
+ unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord
+ );
+ texCoord.x = texCoordX + 0.25f;
+ half4 SHAg = UNITY_SAMPLE_TEX3D_SAMPLER(
+ unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord
+ );
+ texCoord.x = texCoordX + 0.5f;
+ half4 SHAb = UNITY_SAMPLE_TEX3D_SAMPLER(
+ unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord
+ );
+ // Linear + constant polynomial terms
+ half3 x1;
+ x1.r = dot(SHAr, normal);
+ x1.g = dot(SHAg, normal);
+ x1.b = dot(SHAb, normal);
+
+ return x1;
+}
+
我们的着色器现在在需要的时候对LPPV(光照探针代理体)进行采样,但结果太暗了。至少在伽马颜色空间中工作就是这样的结果。这是因为球面谐波数据存储在线性空间中。因此,可能需要进行颜色的转换。
+ +if (unity_ProbeVolumeParams.x == 1) {
+ indirectLight.diffuse = SHEvalLinearL0L1_SampleProbeVolume(
+ float4(i.normal, 1), i.worldPos
+ );
+ indirectLight.diffuse = max(0, indirectLight.diffuse);
+ #if defined(UNITY_COLORSPACE_GAMMA)
+ indirectLight.diffuse = LinearToGammaSpace(indirectLight.diffuse);
+ #endif
+}
+
当一个对象最终只覆盖应用程序窗口的一小部分的时候,你不需要高度详细的网格来渲染它。你可以根据对象在视图中的大小使用不同的网格。这被称为细节层次,或简称LOD。Unity允许我们通过组件LOD组来实现这样的功能。
+ +这个想法是你在各种不同的LOD等级使用同一网格的多个版本。最高级 - LOD 0 - 具有最多的顶点、子对象、动画、复杂的材质等。随后的级别逐渐变得更简单,更容易计算。在理想情况下,相邻的LOD等级被设计为使得当Unity从一个LOD等级切换到另一个LOD等级的时候,你不能轻易地辨别出它们之间的区别。否则突然有LOD等级变化的时候就会让人很晕。但是在研究这种技术的时候,我们会使用明显的不同的网格。
+ +创建一个空的游戏对象并给它两个子对象。第一个子对象是标准球体,第二个子对象是标准立方体,其大小设置为0.75。 预期的结果看起来像是一个重叠的球体和立方体。
+ + +通过Component /Rendering / LOD Group将一个LOD组组件添加到父对象。你会得到一个具有默认设置的LOD组,它有三个LOD等级。 百分比是指由对象的包围盒覆盖的窗口的垂直部分。因此,当垂直尺寸下降到窗口高度的60%的时候,默认设置为切换到LOD 1,当垂直尺寸下降到窗口高度的30%的时候,默认设置为切换到LOD 2。当垂直尺寸下降到窗口高度的10%的时候,它根本不渲染。 你可以通过拖动LOD框的边来更改这些阈值。
+ + +这些阈值由LOD偏移(LOD Bias)进行修改,LOD偏移(LOD Bias)可以在组件检视器里面查看并修改。目前使用的是质量设置为2的默认值,这意味着阈值被减半。也可以设置为最大LOD等级,这将导致跳过最高级别。
+ +为了使其工作,你必须告诉组件每个LOD等级都会使用哪些对象。这是通过选择一个LOD块并将对象添加到其“渲染器”列表中完成的。你可以在场景中添加任何对象,但一定要确保添加其子对象到LOD块的“渲染器”列表。让LOD 0的“渲染器”使用球体,让LOD 1的“渲染器”使用立方体。我们将LOD 2的“渲染器”留空,所以我们只有两个LOD等级。如果需要的话,你可以通过右键单击上下文菜单删除并插入LOD等级。
+ + +一旦配置了LOD等级,你可以通过移动相机来查看它们的效果。如果物体足够大的话,它将使用球体,否则的话它将使用立方体,或根本不会渲染。
-通过Component /Rendering / LOD Group将一个LOD组组件添加到父对象。你会得到一个具有默认设置的LOD组,它有三个LOD等级。 百分比是指由对象的包围盒覆盖的窗口的垂直部分。因此,当垂直尺寸下降到窗口高度的60%的时候,默认设置为切换到LOD 1,当垂直尺寸下降到窗口高度的30%的时候,默认设置为切换到LOD 2。当垂直尺寸下降到窗口高度的10%的时候,它根本不渲染。 你可以通过拖动LOD框的边来更改这些阈值。 - -{% include img-picture.html - url1="posts/2018/month1/catRender18/1692664-20200605013238446-1711254101.png" - des="组件LOD组" - width="250" -%} - -这些阈值由LOD偏移(LOD Bias)进行修改,LOD偏移(LOD Bias)可以在组件检视器里面查看并修改。目前使用的是质量设置为2的默认值,这意味着阈值被减半。也可以设置为最大LOD等级,这将导致跳过最高级别。 - -为了使其工作,你必须告诉组件每个LOD等级都会使用哪些对象。这是通过选择一个LOD块并将对象添加到其“渲染器”列表中完成的。你可以在场景中添加任何对象,但一定要确保添加其子对象到LOD块的“渲染器”列表。让LOD 0的“渲染器”使用球体,让LOD 1的“渲染器”使用立方体。我们将LOD 2的“渲染器”留空,所以我们只有两个LOD等级。如果需要的话,你可以通过右键单击上下文菜单删除并插入LOD等级。 - -{% include img-picture.html - url1="posts/2018/month1/catRender18/1692664-20200605013239110-167141319.png" - des="让球这个子物体使用LOD 0等级" - width="250" -%} - -一旦配置了LOD等级,你可以通过移动相机来查看它们的效果。如果物体足够大的话,它将使用球体,否则的话它将使用立方体,或根本不会渲染。 - -{% include vedio-mini.html - _url="https://thumbs.gfycat.com/ShyAffectionateFairyfly-mobile.mp4" - _type="video/mp4" - des="LOD切换" width="250" -%} +因为LOD组是如何渲染的取决于它的视图大小,所以它们自然是动态的。但是,你仍然可以使其成为静态。对整个对象层次结构执行此操作,因此也包括了根节点和它的两个子节点。然后设置主光源为烘焙光源,看看会发生什么。
-{% include img-picture.html - url1="posts/2018/month1/catRender18/1692664-20200605013240110-283978428.png" - des="让球这个子物体使用LOD 0等级" - width="250" -%} + +使用烘焙光源得到的效果
-看起来在烘焙静态光照贴图的时候使用的是LOD 0。 我们最终总是能够看到球体的阴影和间接光照的贡献,即使LOD组切换到一个立方体或是对自身做了剔除。但请注意,立方体也是使用了静态光照贴图。 所以它不使用光照探针,对吧? 转动光照探针组就能发现这一点。 +看起来在烘焙静态光照贴图的时候使用的是LOD 0。 我们最终总是能够看到球体的阴影和间接光照的贡献,即使LOD组切换到一个立方体或是对自身做了剔除。但请注意,立方体也是使用了静态光照贴图。 所以它不使用光照探针,对吧? 转动光照探针组就能发现这一点。
-{% include img-picture.html - url1="posts/2018/month1/catRender18/1692664-20200605013241019-1435398902.png" - des="没有光照探针时候的烘焙光照" - width="250" -%} + +禁用光探针组会使得立方体变得更暗。这意味着他们不再接受间接光照。 这是因为在烘焙过程中确定间接光照的时候使用的是LOD 0。为了找到其他LOD等级下的间接光照, Unity可以做到的最好程度是依靠烘焙光照探针。 因此,即使在运行时我们不需要光照探针,我们也需要光照探针来为我们的立方体计算间接光照。
-### 实时全局光照和LOD组 +当只使用实时全局光照的时候,方法是类似的,除了我们的立方体现在在运行时使用的是光照探针。你可以通过选择球体或立方体来验证这一点。选择立方体后,你可以看到小工具显示了哪些光照探针被使用。 球体不显示它们,因为它使用的是动态光照贴图。
-{% include img-picture.html - url1="posts/2018/month1/catRender18/1692664-20200605013242971-183808454.png" - des="LOD 1使用光照探针来计算实时全局光照" - width="250" -%} + +当烘焙全局光照和实时全局光照同时使用的时候,它会变得更加复杂。 在这种情况下,立方体应该对烘焙全局光照使用光照贴图,对实时全局光照使用光照探针。不幸的是,这是不可能的,这是因为光照贴图和和球面谐波不能同时使用。这是一个非此即彼的问题。因为光照贴图数据对于立方体来说是可用的,所以Unity最终会使用它。因此,立方体不受实时全局光照的影响。
-{% include img-picture.html - url1="posts/2018/month1/catRender18/1692664-20200605013243831-1402316420.png" - des="仅对LOD 1等级使用烘焙光照,使用的是低强度的主光源" - width="250" -%} + +一个重要的细节是,烘焙的LOD等级和渲染的LOD等级是完全独立的。 他们不需要使用相同的设置。如果实时全局光照最终比烘焙全局光照更重要,你可以强制立方体使用光照探针,确保它对于光照贴图来说不是静态的,同时保持球体静止。
-{% include img-picture.html - url1="posts/2018/month1/catRender18/1692664-20200605013244794-1245713384.png" - des="LOD 1强制使用光照探针" - width="250" -%} + +LOD组这种方法的缺点是,当LOD等级发生变化的时候,它可以在视觉上很明显的表现出来。几何体会在视图中突然弹出、消失或改变形状。 这可以通过相邻LOD等级之间的淡入淡出来缓解,这通过将LOD组的渐变模式设置为淡入淡出来完成。还有另一种渐变模式,由Unity用于SpeedTree对象,我们不会使用这种模式。
-当启用淡入淡出的时候,每个LOD等级都会显示一个淡入变换宽度(Fade Transition Width )字段,用于控制其块的哪个部分用于衰落。举个简单的例子来说,当设置为0.5的时候,一半LOD范围将用于淡出到下一级。或者,淡入淡出过程可以是有动画的,在这种情况下,在LOD等级之间的切换需要大约半秒钟。 +当启用淡入淡出的时候,每个LOD等级都会显示一个淡入变换宽度(Fade Transition Width )字段,用于控制其块的哪个部分用于衰落。举个简单的例子来说,当设置为0.5的时候,一半LOD范围将用于淡出到下一级。或者,淡入淡出过程可以是有动画的,在这种情况下,在LOD等级之间的切换需要大约半秒钟。
-{% include img-picture.html - url1="posts/2018/month1/catRender18/1692664-20200605013245526-1175118666.png" - des="带有0.5变换宽度的淡入淡出" - width="250" -%} + +当启用淡入淡出的时候,在LOD组之间进行转换的时候会同时渲染两个LOD等级。
-### 支持淡入淡出 +Unity的标准着色器在默认情况下是不支持淡入淡出的。如果想要支持支持淡入淡出的话,你必须复制标准着色器并为LOD_FADE_CROSSFADE关键字添加一个多编译指令。添加这条指令还有一个原因是为了在My First Lighting着色器里面支持淡入淡出功能。让我们将这条指令添加到除了meta渲染通道以外的所有渲染通道。
-```hlsl -#pragma multi_compile _ LOD_FADE_CROSSFADE -``` +#pragma multi_compile _ LOD_FADE_CROSSFADE
+
我们将使用抖动来在LOD等级之间进行转换。这种方法适用于前向渲染和延迟渲染,也适用于有阴影的情况。
-在创建半透明阴影的时候,我们已经使用了抖动这种方法。它需要片段的屏幕空间坐标,这迫使我们为顶点程序和片段程序使用不同的插值器结构。所以让我们复制My Lighting 中的Interpolators结构,将其重命名为InterpolatorsVertex。 +在创建半透明阴影的时候,我们已经使用了抖动这种方法。它需要片段的屏幕空间坐标,这迫使我们为顶点程序和片段程序使用不同的插值器结构。所以让我们复制My Lighting 中的Interpolators结构,将其重命名为InterpolatorsVertex。
-```hlsl -struct InterpolatorsVertex { +struct InterpolatorsVertex {
-};
-struct Interpolators {
+};
+struct Interpolators {
-};
+};
-InterpolatorsVertex MyVertexProgram (VertexData v) {
- InterpolatorsVertex i;
-}
-```
+InterpolatorsVertex MyVertexProgram (VertexData v) {
+ InterpolatorsVertex i;
+}
+
当我们必须进行淡入淡出处理的时候,片段程序的插值器里面必须包含vpos,否则我们可以使用同样的位置信息。
-```hlsl -struct Interpolators { - #if defined(LOD_FADE_CROSSFADE) - UNITY_VPOS_TYPE vpos : VPOS; - #else - float4 pos : SV_POSITION; - #endif -}; -``` +struct Interpolators {
+ #if defined(LOD_FADE_CROSSFADE)
+ UNITY_VPOS_TYPE vpos : VPOS;
+ #else
+ float4 pos : SV_POSITION;
+ #endif
+};
+
我们可以在我们片段程序中开始的位置使用UnityApplyDitherCrossFade函数来执行淡入淡出操作。
-```hlsl -FragmentOutput MyFragmentProgram (Interpolators i) { - #if defined(LOD_FADE_CROSSFADE) - UnityApplyDitherCrossFade(i.vpos); - #endif -} -``` +FragmentOutput MyFragmentProgram (Interpolators i) {
+ #if defined(LOD_FADE_CROSSFADE)
+ UnityApplyDitherCrossFade(i.vpos);
+ #endif
+}
+
UnityApplyDitherCrossFade是如何工作的?
这个函数在UnityCG中进行定义。它的方法类似于我们在《渲染12:半透明阴影》中使用的抖动方法,区别只是整个对象的抖动级别是均匀的。 因此,不需要混合抖动级别。 它使用存储在4×64大小的二维纹理中的16个抖动级别,而不是4×4×16大小的三维纹理。
-```
-
-```hlsl
-FragmentOutput MyFragmentProgram (Interpolators i) {
- #if defined(LOD_FADE_CROSSFADE)
- UnityApplyDitherCrossFade(i.vpos);
- #endif
-}
-```
-
-unity_LODFade变量在UnityShaderVariables中进行定义。它的Y分量包含的是对象的渐变量,共有十六步。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender18/1692664-20200605013246331-642394563.png"
- des="通过抖动方法得到的淡入淡出几何体"
- width="250"
-%}
-
-淡入淡出现在可以在几何体上正常工作了。为了使其适用于阴影,我们必须调整My Shadows着色器。 首先,当我们进行淡入淡出处理的时候,必须使用vpos。其次,我们还必须在片段程序开始的位置使用UnityApplyDitherCrossFade函数。
-
-```hlsl
-struct Interpolators {
- #if SHADOWS_SEMITRANSPARENT || defined(LOD_FADE_CROSSFADE)
- UNITY_VPOS_TYPE vpos : VPOS;
- #else
- float4 positions : SV_POSITION;
- #endif
-};
-
-float4 MyShadowFragmentProgram (Interpolators i) : SV_TARGET {
- #if defined(LOD_FADE_CROSSFADE)
- UnityApplyDitherCrossFade(i.vpos);
- #endif
-}
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender18/1692664-20200605013247225-2138491287.png"
- url2="posts/2018/month1/catRender18/1692664-20200605013249358-581600268.gif"
- des="对几何体和阴影都做了淡入淡出处理"
- width="250"
-%}
-
-因为立方体和球体相互交叉,所以我们在对它们做淡入淡出处理的时候,得到一些奇怪的自阴影效果。这对于看到淡入淡出处理能在阴影上起作用是很方便的,但是当你为实际游戏创建LOD几何体的时候,需要注意这些瑕疵。]]>catlikecoding Unity 混合光照(翻译十七) 2018-01-21T20:00:00+08:00 2018-01-21T20:00:00+08:00 https://www.damonc.top/Unity_Mix_Lighting
FragmentOutput MyFragmentProgram (Interpolators i) {
+ #if defined(LOD_FADE_CROSSFADE)
+ UnityApplyDitherCrossFade(i.vpos);
+ #endif
+}
+
unity_LODFade变量在UnityShaderVariables中进行定义。它的Y分量包含的是对象的渐变量,共有十六步。
+ + +淡入淡出现在可以在几何体上正常工作了。为了使其适用于阴影,我们必须调整My Shadows着色器。 首先,当我们进行淡入淡出处理的时候,必须使用vpos。其次,我们还必须在片段程序开始的位置使用UnityApplyDitherCrossFade函数。
+ +struct Interpolators {
+ #if SHADOWS_SEMITRANSPARENT || defined(LOD_FADE_CROSSFADE)
+ UNITY_VPOS_TYPE vpos : VPOS;
+ #else
+ float4 positions : SV_POSITION;
+ #endif
+};
+
+float4 MyShadowFragmentProgram (Interpolators i) : SV_TARGET {
+ #if defined(LOD_FADE_CROSSFADE)
+ UnityApplyDitherCrossFade(i.vpos);
+ #endif
+}
+
因为立方体和球体相互交叉,所以我们在对它们做淡入淡出处理的时候,得到一些奇怪的自阴影效果。这对于看到淡入淡出处理能在阴影上起作用是很方便的,但是当你为实际游戏创建LOD几何体的时候,需要注意这些瑕疵。
]]>光照贴图可以提供预计算光照:以纹理内存为代价减少了GPU在实时中的工作量;还加入了间接光。
+ +烘焙光的限制
1.高光不能被烘焙
2.烘焙光只通过光照探头影响动态物体
3.烘焙光不产生实时阴影
-```
+
你可以在下面的截图中看到完全实时光照和完全烘焙光照之间的区别。前一篇教程中的一个场景,唯一的不同是我将所有的球体都设置为动态并重新改变了一些球体的位置。其它一切都是静态的。这是使用前向渲染的方法。
-{% include img-picture.html - url1="posts/2018/month1/catRender17/1692664-20200603002309942-1932595350.png" - url2="posts/2018/month1/catRender17/1692664-20200603002311276-1707091650.png" - des="完全实时和完全烘焙光照" width="250" -%} + +烘焙光照有间接光而没有实时光照,因为间接光需要光照贴图。由于间接光可以为场景加入很大的真实感,如果我们可以将它和实时光照融合在一起就再好不过了。这是可以的,但也意味着着色的开销会增加。我们需要将混合光(Mixed Lighting)的光照模式(Lighting Mode)设置为烘焙间接(Baked Indirect)。
-{% include img-picture.html - url1="posts/2018/month1/catRender17/1692664-20200603002312394-324532807.png" - des="混合光照,烘焙间接" width="250" -%} + +我们已经在前一篇教程中切换到这个模式了,但是之前我们只使用了完全烘焙光照。虽然表现结果与完全烘焙光照相同,混合光照模式没有任何区别。为了使用混合光照,光源的模式必须要设置为混合。
-{% include img-picture.html - url1="posts/2018/month1/catRender17/1692664-20200603002313374-471117722.png" - des="混合模式的主光源" width="250" -%} + +在将主定向光改为混合光后,两件事会发生:
-第一,Unity会再次烘焙光照贴图。这一次光照贴图只会存储间接光,所以它会比之前的暗很多。 +第一,Unity会再次烘焙光照贴图。这一次光照贴图只会存储间接光,所以它会比之前的暗很多。
-{% include img-picture.html - url1="posts/2018/month1/catRender17/1692664-20200603002314496-2053721233.png" - url2="posts/2018/month1/catRender17/1692664-20200603002315595-1655913714.png" - des="完全烘焙的光照贴图 vs 只有间接光的光照贴图" width="250" -%} + +第二,所有物体都会像主光源被设置为实时那样被照亮。只有一点不同:光照贴图被用来为静态物体添加间接光,而不是球谐光或探头。动态物体的间接光仍要使用光照探头。
-{% include img-picture.html - url1="posts/2018/month1/catRender17/1692664-20200603002316908-68636833.png" - des="混合光照,实时直接光照烘焙间接光" width="250" -%} + +我们不需要改变我们的着色器来支持这点,因为前向基础通道(forward base pass)已经融合了光照贴图数据和主定向光源。和往常一样,额外的光照会得到附加通道(additive pass)。当使用延迟渲染通道时,主光源也会得到一个通道。
-**混合光可以在运行时调整吗?** -是的,因为它们被用于实时光照。但是,它们的烘焙数据时静态的。所以在运行时你只能稍微调整光照,比如稍微调整它的强度。更大的变化会使人明显看出烘焙光照和实时光照之间的不同步。_ +混合光可以在运行时调整吗?
+是的,因为它们被用于实时光照。但是,它们的烘焙数据时静态的。所以在运行时你只能稍微调整光照,比如稍微调整它的强度。更大的变化会使人明显看出烘焙光照和实时光照之间的不同步。_
刚开始一切似乎正常运行。但是,定向光的阴影衰减发生了错误。我们通过极大降低阴影距离观察到阴影被剪掉了。
-{% include img-picture.html - url1="posts/2018/month1/catRender17/1692664-20200603002318408-928334072.png" - url2="posts/2018/month1/catRender17/1692664-20200603002319957-145579115.png" - des="阴影衰减,标准着色器vs我们的着色器" width="250" -%} + +虽然Unity很长一段时间都有混合光照模式,但实际上它在Unity5中就不起作用了。Unity5.6中新加入了一个混合光照模式,即我们现在使用的这个。当该新模式被加入时,_UNITY_LIGHT_ATTENUATION_宏下面的代码发生了变化。我们在使用完全烘焙光照或者实时光照时没有注意到这一点,但是我们必须更新我们的代码以适应混合光照的新方法。由于这是最近的一个巨大的变化,我们必须要注意它所带来的问题。
-我们要改变的第一点是不再使用_SHADOW_COORDS_宏来定义阴影坐标的插值(interpolater)。我们必须使用新的_UNITY_SHADOW_COORDS_宏来代替它。 +我们要改变的第一点是不再使用_SHADOW_COORDS_宏来定义阴影坐标的插值(interpolater)。我们必须使用新的_UNITY_SHADOW_COORDS_宏来代替它。
-```hlsl -struct Interpolators -{ - //SHADOW_COORDS(5) - UNITY_SHADOW_COORDS(5) -}; -``` +struct Interpolators
+{
+ //SHADOW_COORDS(5)
+ UNITY_SHADOW_COORDS(5)
+};
+
同样,TRANSFER_SHADOW_应该替换为_UNITY_TRANSFER_SHADOW
-```hlsl -Interpolators MyVertexProgram (VertexData v) -{ - //TRANSFER_SHADOW(i); - UNITY_TRANSFER_SHADOW(i); -} -``` +Interpolators MyVertexProgram (VertexData v)
+{
+ //TRANSFER_SHADOW(i);
+ UNITY_TRANSFER_SHADOW(i);
+}
+
然而,这会产生一个编译错误,因为该宏需要一个额外的参数。从Unity 5.6开始,只有定向阴影的屏幕空间坐标中被放入一个插值。点光源和聚光源的阴影坐标现在在片段程序(fragment program)中进行计算。有个新变化:在一些情况中光照贴图的坐标被用在阴影蒙版(shadow mask)中,我们会在后面讲解这一点。为了该宏能正常工作,我们必须为它提供第二个UV通道中的数据,其中包含光照贴图的坐标。
-```hlsl -UNITY_TRANSFER_SHADOW(i, v.uv1); +UNITY_TRANSFER_SHADOW(i, v.uv1);
-```
+
这样会再次产生一个编译错误。这是因为在一些情况下_UNITY_SHADOW_COORDS_错误地创建了一个插值,尽管实际上并不需要。在这种情况下,_TRANSFER_SHADOW_不会初始化它,因而导致错误。这个问题出现在5.6.0中,一直到5.6.2和2017.1.0beta版本中都有。
-人们通常不会注意到这个问题,因为Unity的标准着色器使用_UNITY_INITIALIZE_OUTPUT_宏来完全地初始化它的插值结构体。因为我们不使用这个宏,所以出现了问题。为了解决它,我们使用_UNITY_INITIALIZE_OUTPUT_宏来初始化我们的插值。 +人们通常不会注意到这个问题,因为Unity的标准着色器使用_UNITY_INITIALIZE_OUTPUT_宏来完全地初始化它的插值结构体。因为我们不使用这个宏,所以出现了问题。为了解决它,我们使用_UNITY_INITIALIZE_OUTPUT_宏来初始化我们的插值。
-```hlsl -Interpolators MyVertexProgram (VertexData v) -{ - Interpolators i; - UNITY_INITIALIZE_OUTPUT(Interpolators, i); -} -``` +Interpolators MyVertexProgram (VertexData v)
+{
+ Interpolators i;
+ UNITY_INITIALIZE_OUTPUT(Interpolators, i);
+}
+
_UNITY_INITIALIZE_OUTPUT_有什么作用?
-它只是为变量分配数值0,将其转换为正确的类型。至少是当程序支持该宏时会这样,否则它不会做任何事。 +它只是为变量分配数值0,将其转换为正确的类型。至少是当程序支持该宏时会这样,否则它不会做任何事。
-```hlsl -// Initialize arbitrary structure with zero values. -// Not supported on some backends -// (e.g. Cg-based particularly with nested structs). -// hlsl2glsl would almost support it, except with structs that have -arrays -// -- so treat as not supported there either :( -#if defined(UNITY_COMPILER_HLSL) || defined(SHADER_API_PSSL) || \\ -defined(UNITY_COMPILER_HLSLCC) - #define UNITY_INITIALIZE_OUTPUT(type,name) name = (type)0; +// Initialize arbitrary structure with zero values.
+// Not supported on some backends
+// (e.g. Cg-based particularly with nested structs).
+// hlsl2glsl would almost support it, except with structs that have
+arrays
+// -- so treat as not supported there either :(
+#if defined(UNITY_COMPILER_HLSL) || defined(SHADER_API_PSSL) || \\
+defined(UNITY_COMPILER_HLSLCC)
+ #define UNITY_INITIALIZE_OUTPUT(type,name) name = (type)0;
#else
- #define UNITY_INITIALIZE_OUTPUT(type,name)
+ #define UNITY_INITIALIZE_OUTPUT(type,name)
#endif
-```
+
通常我们倾向于只使用显式赋值,很少使用这个初始化插值宏。
-### 手动衰减阴影 +现在我们正确地使用了新的宏定义,但是主光源的阴影仍然没有按照它们应该的那样衰减。结果我们发现当同时使用定向阴影和光照贴图时,UNITY_LIGHT_ATTENUATION 不会对光源进行衰减。使用混合模式的主定向光源就会产生这个问题。所以我们必须手动设置。
-``` -为什么在这个例子中阴影没有衰减? +为什么在这个例子中阴影没有衰减?
1、UNITY_LIGHT_ATTENUATION宏之前是独立使用的,但是自从Unity5. 6它开始和Unity的标准全局光照函数一同使用。我们没有采用同样的方法,因此它不能正常工作。
2、至于为什么要做这个改动,唯一的线索就是AutoLight中的一段注释:“为了性能的原因以GI函数的深度处理阴影”。由于着色器编译器会随意地移动代码。
-```
+
对于我们的延迟光照着色器,我们已经有了进行阴影衰减的代码。将相关代码片段从MyDeferredShading中复制到My Lighting中的一个新函数中。唯一实际的区别在于我们必须使用视图向量和视图矩阵构建viewZ。我们只需要Z分量,所以无需进行一次完整的矩阵乘法。
-```hlsl -float FadeShadows (Interpolators i, float attenuation) { - float viewZ = dot(_WorldSpaceCameraPos - i.worldPos, UNITY_MATRIX_V[2].xyz); - float shadowFadeDistance = UnityComputeShadowFadeDistance(i.worldPos, viewZ); - float shadowFade = UnityComputeShadowFade(shadowFadeDistance); - attenuation = saturate(attenuation + shadowFade); - return attenuation; -} -``` +float FadeShadows (Interpolators i, float attenuation) {
+ float viewZ = dot(_WorldSpaceCameraPos - i.worldPos, UNITY_MATRIX_V[2].xyz);
+ float shadowFadeDistance = UnityComputeShadowFadeDistance(i.worldPos, viewZ);
+ float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
+ attenuation = saturate(attenuation + shadowFade);
+ return attenuation;
+}
+
该手动衰减必须在使用了_UNITY_LIGHT_ATTENUATION初始化完成_之后。
-```hlsl -UnityLight CreateLight (Interpolators i) { +UnityLight CreateLight (Interpolators i) {
- UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos.xyz);
- attenuation = FadeShadows(i, attenuation);
-}
-```
-
-只有当 _HANDLE_SHADOW_BLENDING_IN_GI_ 在UnityShadowLibrary.cginc文件中有定义时,FadeShadows才会开始计算。
-
-```hlsl
-float FadeShadows (Interpolators i, float attenuation) {
- #if HANDLE_SHADOWS_BLENDING_IN_GI
- // UNITY_LIGHT_ATTENUATION doesn't fade shadows for us.
- float viewZ = dot(_WorldSpaceCameraPos - i.worldPos, UNITY_MATRIX_V[2].xyz);
- float shadowFadeDistance = UnityComputeShadowFadeDistance(i.worldPos, viewZ);
- float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
- attenuation = saturate(attenuation + shadowFade);
- #endif
- return attenuation;
-}
-```
-
-最后,我们的阴影如它们应该的那样正常衰减了。
-
-## 使用阴影蒙版
-
-烘焙间接光的混合模式成本很高。它们需要实时光照外加间接光的光照贴图那么大的工作量。它和完全烘焙光照相比最重要的是加入了实时阴影。幸运的是,有一个方法仍可以将阴影烘焙到光照贴图中,将其和实时阴影综合起来。为了开启这个功能,我们将混合光照模式改为Shadowmask。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender17/1692664-20200603002321275-2134383138.png"
- des="Shadowmask模式" width="250"
-%}
-
-在这个模式中,混合光照的间接光和阴影衰减都存储在了光照贴图中。阴影被存储在一张额外的贴图(即阴影蒙版)。当只有主定向光源时,红色的阴影蒙版决定是否过滤被照亮的物体。红色是因为阴影信息被存储在纹理的R通道中。事实上,贴图中至多可以储存四个光照的阴影,因为它只有四个通道。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender17/1692664-20200603002322323-849349388.png"
- des="烘焙的强度以及阴影蒙版" width="250"
-%}
-
-在Unity创建了阴影蒙版后,静态物体的阴影投射会消失。只有光照探头仍会处理它们。动态物体的阴影不受影响。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender17/1692664-20200603002323536-1775535799.png"
- des="没有烘焙阴影" width="250"
-%}
-
-### 对阴影蒙版采样
-
-为了重新得到烘焙阴影,我们必须对阴影蒙版采样样。Unity的宏已经对点光源和聚光源进行了取样,不过我们必须也要将它包含在我们的FadeShadows函数中。为此我们可以使用UnityShadowLibrary中的UnitySampleBakedOcclusions函数。它需要光照贴图的UV坐标和世界位置作为输入参数。
-
-```hlsl
-float FadeShadows (Interpolators i, float attenuation)
-{
- #if HANDLE_SHADOWS_BLENDING_IN_GI
-
- float bakedAttenuation = UnitySampleBakedOcclusion(i.lightmapUV, i.worldPos);
- attenuation = saturate(attenuation + shadowFade);
- #endif
- return attenuation;
-}
-```
-
-```
-UnitySampleBakedOcclusion是什么样子的?
+ UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos.xyz);
+ attenuation = FadeShadows(i, attenuation);
+}
+
只有当 HANDLE_SHADOW_BLENDING_IN_GI 在UnityShadowLibrary.cginc文件中有定义时,FadeShadows才会开始计算。
+ +float FadeShadows (Interpolators i, float attenuation) {
+ #if HANDLE_SHADOWS_BLENDING_IN_GI
+ // UNITY_LIGHT_ATTENUATION doesn't fade shadows for us.
+ float viewZ = dot(_WorldSpaceCameraPos - i.worldPos, UNITY_MATRIX_V[2].xyz);
+ float shadowFadeDistance = UnityComputeShadowFadeDistance(i.worldPos, viewZ);
+ float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
+ attenuation = saturate(attenuation + shadowFade);
+ #endif
+ return attenuation;
+}
+
最后,我们的阴影如它们应该的那样正常衰减了。
+ +烘焙间接光的混合模式成本很高。它们需要实时光照外加间接光的光照贴图那么大的工作量。它和完全烘焙光照相比最重要的是加入了实时阴影。幸运的是,有一个方法仍可以将阴影烘焙到光照贴图中,将其和实时阴影综合起来。为了开启这个功能,我们将混合光照模式改为Shadowmask。
+ + +在这个模式中,混合光照的间接光和阴影衰减都存储在了光照贴图中。阴影被存储在一张额外的贴图(即阴影蒙版)。当只有主定向光源时,红色的阴影蒙版决定是否过滤被照亮的物体。红色是因为阴影信息被存储在纹理的R通道中。事实上,贴图中至多可以储存四个光照的阴影,因为它只有四个通道。
+ + +在Unity创建了阴影蒙版后,静态物体的阴影投射会消失。只有光照探头仍会处理它们。动态物体的阴影不受影响。
+ + +为了重新得到烘焙阴影,我们必须对阴影蒙版采样样。Unity的宏已经对点光源和聚光源进行了取样,不过我们必须也要将它包含在我们的FadeShadows函数中。为此我们可以使用UnityShadowLibrary中的UnitySampleBakedOcclusions函数。它需要光照贴图的UV坐标和世界位置作为输入参数。
+ +float FadeShadows (Interpolators i, float attenuation)
+{
+ #if HANDLE_SHADOWS_BLENDING_IN_GI
+
+ float bakedAttenuation = UnitySampleBakedOcclusion(i.lightmapUV, i.worldPos);
+ attenuation = saturate(attenuation + shadowFade);
+ #endif
+ return attenuation;
+}
+
UnitySampleBakedOcclusion是什么样子的?
它使用光照贴图坐标对阴影蒙版取样,然后选择适当的通道。unity_OcclusionMaskSelector变量是一个含有一个分量的向量,该分量被设置为1以匹配当前正在被着色的光源。
-```
-
-```hlsl
-fixed UnitySampleBakedOcclusion (float2 lightmapUV, float3 worldPos) {
- #if defined (SHADOWS_SHADOWMASK)
- #if defined(LIGHTMAP_ON)
- fixed4 rawOcclusionMask = UNITY_SAMPLE_TEX2D_SAMPLER(
- unity_ShadowMask, unity_Lightmap, lightmapUV.xy
- );
- #else
- fixed4 rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
- #endif
- return saturate(dot(rawOcclusionMask, unity_OcclusionMaskSelector));
- #else
- return 1.0;
- #endif
-}
-```
-
-_该函数还处理了光照探头代理体积的衰减,但是我们还没有支持这点所以我去掉了那部分的代码。这就是为什么该函数有一个世界位置的参数。_
-
-当使用阴影蒙版时,_UnitySampleBakedOcclusions_提供给我们烘焙阴影衰减,在其他情况下它的值都为1。现在我们必须将它和我们已经有的衰减综合起来然后对阴影进行衰减。UnityMixRealtimeAndBakedShadows函数为我们实现了这些。
-
-```hlsl
-float bakedAttenuation = UnitySampleBakedOcclusion(i.lightmapUV, i.worldPos);
-//attenuation = saturate(attenuation shadowFade);
-attenuation = UnityMixRealtimeAndBakedShadows
-(
- attenuation, bakedAttenuation, shadowFade
-);
-```
-
-```
-UnityMixRealtimeAndBakedShadows是如何工作的?
+
fixed UnitySampleBakedOcclusion (float2 lightmapUV, float3 worldPos) {
+ #if defined (SHADOWS_SHADOWMASK)
+ #if defined(LIGHTMAP_ON)
+ fixed4 rawOcclusionMask = UNITY_SAMPLE_TEX2D_SAMPLER(
+ unity_ShadowMask, unity_Lightmap, lightmapUV.xy
+ );
+ #else
+ fixed4 rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
+ #endif
+ return saturate(dot(rawOcclusionMask, unity_OcclusionMaskSelector));
+ #else
+ return 1.0;
+ #endif
+}
+
该函数还处理了光照探头代理体积的衰减,但是我们还没有支持这点所以我去掉了那部分的代码。这就是为什么该函数有一个世界位置的参数。
+ +当使用阴影蒙版时,_UnitySampleBakedOcclusions_提供给我们烘焙阴影衰减,在其他情况下它的值都为1。现在我们必须将它和我们已经有的衰减综合起来然后对阴影进行衰减。UnityMixRealtimeAndBakedShadows函数为我们实现了这些。
+ +float bakedAttenuation = UnitySampleBakedOcclusion(i.lightmapUV, i.worldPos);
+//attenuation = saturate(attenuation shadowFade);
+attenuation = UnityMixRealtimeAndBakedShadows
+(
+ attenuation, bakedAttenuation, shadowFade
+);
+
UnityMixRealtimeAndBakedShadows是如何工作的?
它也是UnityShadowLibrary中的一个函数。它还处理光照探头代理体积以及一些其他极端情况。那些情况和我们无关,所以我删除了一些内容。
-```
-
-```hlsl
-inline half UnityMixRealtimeAndBakedShadows (
- half realtimeShadowAttenuation, half bakedShadowAttenuation, half fade
-) {
- #if !defined(SHADOWS_DEPTH) && !defined(SHADOWS_SCREEN) && \\
- !defined(SHADOWS_CUBE)
- return bakedShadowAttenuation;
- #endif
-
- #if defined (SHADOWS_SHADOWMASK)
- #if defined (LIGHTMAP_SHADOW_MIXING)
- realtimeShadowAttenuation = saturate(realtimeShadowAttenuation + fade);
- return min(realtimeShadowAttenuation, bakedShadowAttenuation);
- #else
- return lerp(
- realtimeShadowAttenuation, bakedShadowAttenuation, fade
- );
- #endif
- #else //no shadowmask
- return saturate(realtimeShadowAttenuation + fade);
- #endif
-}
-```
-
-**如果没有动态阴影,那么结果将得到烘焙的衰减。这意味着动态物体没有阴影,以及被映射到光照贴图上的物体没有烘焙阴影。**
-
-当没有使用阴影蒙版时,它会进行原来的衰减。否则,它会根据我们是否做了阴影混合进行表现,我们后面再讲。现在,它只是在实时衰减和烘焙衰减之间进行一个插值。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender17/1692664-20200603002325094-888038969.png"
- des="实时阴影和阴影蒙版阴影" width="250"
-%}
-
-现在静态物体有了实时阴影和烘焙阴影,且它们正确地混合。实时阴影的衰减仍然超过了阴影距离,但是烘焙阴影没有。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender17/1692664-20200603002326606-1972314102.png"
- des="只有实时阴影衰减了" width="250"
-%}
-
-### 添加一个阴影蒙版G-Buffer
-
-现在阴影蒙版可用于前向渲染路径,但是我们需要使它也可用于延迟渲染:添加阴影蒙版信息作为一个额外的G-缓存。所以当_SHADOWS_SHADOWMASK_被定义时,在_FragmentOutput_结构体中添加一个缓存。
-
-```hlsl
-struct FragmentOutput {
- #if defined(DEFERRED_PASS)
- float4 gBuffer0 : SV_Target0;
- float4 gBuffer1 : SV_Target1;
- float4 gBuffer2 : SV_Target2;
- float4 gBuffer3 : SV_Target3;
-
- #if defined(SHADOWS_SHADOWMASK)
- float4 gBuffer4 : SV_Target4;
- #endif
- #else
- float4 color : SV_Target;
- #endif
-};
-```
-
-添加的第五个G-缓存,会使显存增大,并不是所有的平台(mobile)都支持它。Unity只在有足够多的渲染目标可用时才支持阴影蒙版,因此我们也应该这样做。
-
-```hlsl
-#if defined(SHADOWS_SHADOWMASK) && (UNITY_ALLOWED_MRT_COUNT > 4)
- float4 gBuffer4 : SV_Target4;
-#endif
-
-```
-
-我们只需在G-缓存中存储采样得到的阴影蒙版数据,而且没有一个确切的光照,为此我们可以使用_UnityGetRawBakedOcclusions_函数,它与_UnitySampleBakedOcclusion_相似,唯一不同在于它没有选择某个纹理通道。
-
-```hlsl
-FragmentOutput output;
-#if defined(DEFERRED_PASS)
- #if !defined(UNITY_HDR_ON)
- color.rgb = exp2(-color.rgb);
- #endif
- output.gBuffer0.rgb = albedo;
- output.gBuffer0.a = GetOcclusion(i);
- output.gBuffer1.rgb = specularTint;
- output.gBuffer1.a = GetSmoothness(i);
- output.gBuffer2 = float4(i.normal * 0.5 + 0.5, 1);
- output.gBuffer3 = color;
- #if defined(SHADOWS_SHADOWMASK) && (UNITY_ALLOWED_MRT_COUNT > 4)
- output.gBuffer4 = UnityGetRawBakedOcclusions(i.lightmapUV, i.worldPos.xyz);
- #endif
+
inline half UnityMixRealtimeAndBakedShadows (
+ half realtimeShadowAttenuation, half bakedShadowAttenuation, half fade
+) {
+ #if !defined(SHADOWS_DEPTH) && !defined(SHADOWS_SCREEN) && \\
+ !defined(SHADOWS_CUBE)
+ return bakedShadowAttenuation;
+ #endif
+
+ #if defined (SHADOWS_SHADOWMASK)
+ #if defined (LIGHTMAP_SHADOW_MIXING)
+ realtimeShadowAttenuation = saturate(realtimeShadowAttenuation + fade);
+ return min(realtimeShadowAttenuation, bakedShadowAttenuation);
+ #else
+ return lerp(
+ realtimeShadowAttenuation, bakedShadowAttenuation, fade
+ );
+ #endif
+ #else //no shadowmask
+ return saturate(realtimeShadowAttenuation + fade);
+ #endif
+}
+
如果没有动态阴影,那么结果将得到烘焙的衰减。这意味着动态物体没有阴影,以及被映射到光照贴图上的物体没有烘焙阴影。
+ +当没有使用阴影蒙版时,它会进行原来的衰减。否则,它会根据我们是否做了阴影混合进行表现,我们后面再讲。现在,它只是在实时衰减和烘焙衰减之间进行一个插值。
+ + +现在静态物体有了实时阴影和烘焙阴影,且它们正确地混合。实时阴影的衰减仍然超过了阴影距离,但是烘焙阴影没有。
+ + +现在阴影蒙版可用于前向渲染路径,但是我们需要使它也可用于延迟渲染:添加阴影蒙版信息作为一个额外的G-缓存。所以当_SHADOWS_SHADOWMASK_被定义时,在_FragmentOutput_结构体中添加一个缓存。
+ +struct FragmentOutput {
+ #if defined(DEFERRED_PASS)
+ float4 gBuffer0 : SV_Target0;
+ float4 gBuffer1 : SV_Target1;
+ float4 gBuffer2 : SV_Target2;
+ float4 gBuffer3 : SV_Target3;
+
+ #if defined(SHADOWS_SHADOWMASK)
+ float4 gBuffer4 : SV_Target4;
+ #endif
+ #else
+ float4 color : SV_Target;
+ #endif
+};
+
添加的第五个G-缓存,会使显存增大,并不是所有的平台(mobile)都支持它。Unity只在有足够多的渲染目标可用时才支持阴影蒙版,因此我们也应该这样做。
+ +#if defined(SHADOWS_SHADOWMASK) && (UNITY_ALLOWED_MRT_COUNT > 4)
+ float4 gBuffer4 : SV_Target4;
+#endif
+
+
我们只需在G-缓存中存储采样得到的阴影蒙版数据,而且没有一个确切的光照,为此我们可以使用_UnityGetRawBakedOcclusions_函数,它与_UnitySampleBakedOcclusion_相似,唯一不同在于它没有选择某个纹理通道。
+ +FragmentOutput output;
+#if defined(DEFERRED_PASS)
+ #if !defined(UNITY_HDR_ON)
+ color.rgb = exp2(-color.rgb);
+ #endif
+ output.gBuffer0.rgb = albedo;
+ output.gBuffer0.a = GetOcclusion(i);
+ output.gBuffer1.rgb = specularTint;
+ output.gBuffer1.a = GetSmoothness(i);
+ output.gBuffer2 = float4(i.normal * 0.5 + 0.5, 1);
+ output.gBuffer3 = color;
+ #if defined(SHADOWS_SHADOWMASK) && (UNITY_ALLOWED_MRT_COUNT > 4)
+ output.gBuffer4 = UnityGetRawBakedOcclusions(i.lightmapUV, i.worldPos.xyz);
+ #endif
#else
- output.color = ApplyFog(color, i);
-#endif
-```
-
-为了可以在没有光照贴图的时候也能成功编译,当光照贴图坐标不可用时我们使用0代替它。
-
-```hlsl
-#if defined(SHADOWS_SHADOWMASK) && (UNITY_ALLOWED_MRT_COUNT > 4)
- float2 shadowUV = 0;
- #if defined(LIGHTMAP_ON)
- shadowUV = i.lightmapUV;
- #endif
- output.gBuffer4 = UnityGetRawBakedOcclusions(shadowUV, i.worldPos.xyz);
-#endif
-
-```
-
-### 使用阴影蒙版G-缓存
-
-调整MyDeferredShading延迟渲染着色器。
-
-第一步先添加额外的一个G-buffer变量。
-
-```hlsl
-sampler2D _CameraGBufferTexture0;
-sampler2D _CameraGBufferTexture1;
-sampler2D _CameraGBufferTexture2;
-sampler2D _CameraGBufferTexture4;
-```
-
-第二步,创建一个函数来得到适当的阴影衰减。如果有了阴影蒙版,可通过对纹理采样然后和_unity_OcclusionMaskSelector_进行一次颜色饱和点乘。这个变量是在_UnityShaderVariables.cginc_中定义的,包含了一个用于选择当前正在被渲染的光照通道的向量。
-
-```hlsl
-float GetShadowMaskAttenuation (float2 uv)
-{
- float attenuation = 1;
- #if defined (SHADOWS_SHADOWMASK)
- float4 mask = tex2D(_CameraGBufferTexture4, uv);
- attenuation = saturate(dot(mask, unity_OcclusionMaskSelector));
- #endif
- return attenuation;
-}
-```
-
-在CreateLight中,即使当前光照没有实时阴影,我们在有阴影蒙版时也要衰减阴影。
-
-```hlsl
-UnityLight CreateLight (float2 uv, float3 worldPos, float viewZ) {
- #if defined(SHADOWS_SHADOWMASK)
- shadowed = true;
- #endif
- if (shadowed)
- {
- }
-}
-```
-
-为了正确地包含烘焙阴影,再次使用UnityMixRealtimeAndBakedShadows代替之前的衰减计算。
-
-```hlsl
-if (shadowed)
-{
- float shadowFadeDistance = UnityComputeShadowFadeDistance(worldPos, viewZ);
- float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
-// shadowAttenuation = saturate(shadowAttenuation + shadowFade);
- shadowAttenuation = UnityMixRealtimeAndBakedShadows(
- shadowAttenuation, GetShadowMaskAttenuation(uv), shadowFade
- );
-}
-```
-
-现在也可以使用自定义的延迟光照着色器得到正确的烘焙阴影了。例外,即当我们的优化分支被使用时会跳过阴影混合。该捷径在阴影蒙版被使用时不可用。
-
-```hlsl
-if (shadowed) {
+ output.color = ApplyFog(color, i);
+#endif
+
为了可以在没有光照贴图的时候也能成功编译,当光照贴图坐标不可用时我们使用0代替它。
+ +#if defined(SHADOWS_SHADOWMASK) && (UNITY_ALLOWED_MRT_COUNT > 4)
+ float2 shadowUV = 0;
+ #if defined(LIGHTMAP_ON)
+ shadowUV = i.lightmapUV;
+ #endif
+ output.gBuffer4 = UnityGetRawBakedOcclusions(shadowUV, i.worldPos.xyz);
+#endif
+
+
调整MyDeferredShading延迟渲染着色器。
+ +第一步先添加额外的一个G-buffer变量。
+ +sampler2D _CameraGBufferTexture0;
+sampler2D _CameraGBufferTexture1;
+sampler2D _CameraGBufferTexture2;
+sampler2D _CameraGBufferTexture4;
+
第二步,创建一个函数来得到适当的阴影衰减。如果有了阴影蒙版,可通过对纹理采样然后和_unity_OcclusionMaskSelector_进行一次颜色饱和点乘。这个变量是在_UnityShaderVariables.cginc_中定义的,包含了一个用于选择当前正在被渲染的光照通道的向量。
+ +float GetShadowMaskAttenuation (float2 uv)
+{
+ float attenuation = 1;
+ #if defined (SHADOWS_SHADOWMASK)
+ float4 mask = tex2D(_CameraGBufferTexture4, uv);
+ attenuation = saturate(dot(mask, unity_OcclusionMaskSelector));
+ #endif
+ return attenuation;
+}
+
在CreateLight中,即使当前光照没有实时阴影,我们在有阴影蒙版时也要衰减阴影。
+ +UnityLight CreateLight (float2 uv, float3 worldPos, float viewZ) {
+ #if defined(SHADOWS_SHADOWMASK)
+ shadowed = true;
+ #endif
+ if (shadowed)
+ {
+ }
+}
+
为了正确地包含烘焙阴影,再次使用UnityMixRealtimeAndBakedShadows代替之前的衰减计算。
+ +if (shadowed)
+{
+ float shadowFadeDistance = UnityComputeShadowFadeDistance(worldPos, viewZ);
+ float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
+// shadowAttenuation = saturate(shadowAttenuation + shadowFade);
+ shadowAttenuation = UnityMixRealtimeAndBakedShadows(
+ shadowAttenuation, GetShadowMaskAttenuation(uv), shadowFade
+ );
+}
+
现在也可以使用自定义的延迟光照着色器得到正确的烘焙阴影了。例外,即当我们的优化分支被使用时会跳过阴影混合。该捷径在阴影蒙版被使用时不可用。
+ +if (shadowed) {
- #if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT)
- #if !defined(SHADOWS_SHADOWMASK)
- UNITY_BRANCH
- if (shadowFade > 0.99) {
- shadowAttenuation = 1;
- }
- #endif
- #endif
-}
-```
+ #if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT)
+ #if !defined(SHADOWS_SHADOWMASK)
+ UNITY_BRANCH
+ if (shadowFade > 0.99) {
+ shadowAttenuation = 1;
+ }
+ #endif
+ #endif
+}
+
虽然使用阴影蒙版模式我们可以得到不错的静态物体的烘焙阴影,动态物体却不能从中获利。动态物体只能接收到实时阴影以及光照探头数据。如果我们希望得到动态物体的阴影,那么静态物体必须也要投射实时阴影。这里的混合光照模式我们要用到距离阴影蒙版(Distance Shadowmask)了。
-{% include img-picture.html - url1="posts/2018/month1/catRender17/1692664-20200603002327719-433885054.png" - des="距离阴影蒙版模式" width="250" -%} + +在2017及以上,使用哪个阴影蒙版模式是通过质量设置进行控制。
-当使用DistanceShadowmask模式时,所有物体都使用实时阴影。第一眼看去,好像和Baked Indirect模式完全一样。 +当使用DistanceShadowmask模式时,所有物体都使用实时阴影。第一眼看去,好像和Baked Indirect模式完全一样。
-{% include img-picture.html - url1="posts/2018/month1/catRender17/1692664-20200603002329008-1483902900.png" - des="所有物体都有实时阴影" width="250" -%} + +不过这里仍有一个阴影蒙版。在这个模式中,烘焙阴影和光照探头的使用超出了阴影距离。因此该模式是成本最高的模式,在阴影距离范围内等价于烘焙间接模式,超出该范围则等价于阴影蒙版模式。
-前面已经支持这个模式了,因为我们正在使用UnityMixRealtimeAndBakedShadows。为了正确地混合完全实时阴影和烘焙阴影,它像往常那样衰减实时阴影,然后取其和烘焙阴影的最小值。 +前面已经支持这个模式了,因为我们正在使用UnityMixRealtimeAndBakedShadows。为了正确地混合完全实时阴影和烘焙阴影,它像往常那样衰减实时阴影,然后取其和烘焙阴影的最小值。
-### 多重光照 +因为阴影蒙版有四个通道,它可以最多同时支持4个光照体积重叠在一起
-{% include img-picture.html - url1="posts/2018/month1/catRender17/1692664-20200603002330331-1413395037.png" - des="四个光源,都是混合光" width="250" -%} + +主方向光源的阴影仍存储在R通道中。你还能够看到存储在G通道和B通道中的聚光源的阴影,最后一个聚光源的阴影存储在A通道中。
-当光照体积不重叠时,它们使用相同的通道来存储它们的阴影数据。所以你可以有任意多个混合光照。但是你必须**确保至多四个光照体积彼此重叠**。如果有太多个混合光影响同一篇区域,那么一些就会改回到完全烘焙模式。为了说明这一点,下面这张截图显示的是在多加入一个聚光源以后的光照贴图。你可以在强度贴图中清楚地看到其中一个已经变成了烘焙光。 +当光照体积不重叠时,它们使用相同的通道来存储它们的阴影数据。所以你可以有任意多个混合光照。但是你必须确保至多四个光照体积彼此重叠。如果有太多个混合光影响同一篇区域,那么一些就会改回到完全烘焙模式。为了说明这一点,下面这张截图显示的是在多加入一个聚光源以后的光照贴图。你可以在强度贴图中清楚地看到其中一个已经变成了烘焙光。
-{% include img-picture.html - url1="posts/2018/month1/catRender17/1692664-20200603002332758-586177298.png" - url2="posts/2018/month1/catRender17/1692664-20200603002333778-925731185.png" - des="5个重叠的光照,其中一个为完全烘焙光" width="250" -%} + +不幸的是,阴影蒙版只有当包含至多一个混合模式的方向光源存在时才能正常工作。对于额外的方向光,阴影衰减会发生错误,至少是在使用前向渲染通道时。延迟渲染倒没有问题。
-{% include img-picture.html - url1="posts/2018/month1/catRender17/1692664-20200603002335110-966052747.png" - des="两个方向光源产生错误的衰减" width="250" -%} + +这是使用UNITY_LIGHT_ATTENUATION的新方法中的一个漏洞:Unity使用通过UNITY_SHADOW_COORDS定义的阴影插值来存储方向阴影的屏幕空间坐标,或者其它拥有阴影蒙版的光源的光照贴图坐标。
-使用阴影蒙版的方向光还需要光照贴图坐标。在forward-render中,这些坐标会被包含,因为LIGHTMAP_ON会在需要的时候被定义。然而,LIGHTMAP_ON在additional-pass中永远不会被定义。这意味着多余的方向光没有可用的光照贴图坐标。结果UNITY_LIGHT_ATTENUATION在这种情况下只会使用0,导致错误的光照贴图采样 +使用阴影蒙版的方向光还需要光照贴图坐标。在forward-render中,这些坐标会被包含,因为LIGHTMAP_ON会在需要的时候被定义。然而,LIGHTMAP_ON在additional-pass中永远不会被定义。这意味着多余的方向光没有可用的光照贴图坐标。结果UNITY_LIGHT_ATTENUATION在这种情况下只会使用0,导致错误的光照贴图采样
-所以我们不能依靠UNITY_LIGHT_ATTENUATION额外获得使用阴影蒙版的方向光源。用屏幕空间的方向阴影 +所以我们不能依靠UNITY_LIGHT_ATTENUATION额外获得使用阴影蒙版的方向光源。用屏幕空间的方向阴影
-```hlsl -#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2) - +#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
+
+#endif
+#if !defined(LIGHTMAP_ON) && defined(SHADOWS_SCREEN)
+ #if defined(SHADOWS_SHADOWMASK) && !defined(UNITY_NO_SCREENSPACE_SHADOWS)
+ #define ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS 1
+ #endif
#endif
-#if !defined(LIGHTMAP_ON) && defined(SHADOWS_SCREEN)
- #if defined(SHADOWS_SHADOWMASK) && !defined(UNITY_NO_SCREENSPACE_SHADOWS)
- #define ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS 1
- #endif
-#endif
-```
+
接下来,对那些额外有蒙版的定向阴影,我们也要包含光照贴图坐标。
-```hlsl -struct Interpolators { +struct Interpolators {
- #if defined(LIGHTMAP_ON) || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
- float2 lightmapUV : TEXCOORD6;
- #endif
-};
+ #if defined(LIGHTMAP_ON) || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
+ float2 lightmapUV : TEXCOORD6;
+ #endif
+};
-Interpolators MyVertexProgram (VertexData v) {
-
- #if defined(LIGHTMAP_ON) || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
- i.lightmapUV = v.uv1 * unity_LightmapST.xy + unity_LightmapST.zw;
- #endif
+Interpolators MyVertexProgram (VertexData v) {
-}
-```
-
-当光照贴图坐标可用时,我们可以再次使用FadeShadows函数进行我们自己控制的衰减。
-
-```hlsl
-float FadeShadows (Interpolators i, float attenuation)
-{
- #if HANDLE_SHADOWS_BLENDING_IN_GI || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
-
- #endif
- return attenuation;
-}
-```
-
-但是,这仍然不正确,因为我们为其输入了错误的衰减数据。我们必须绕开UNITY_LIGHT_ATTENUATION,只得到烘焙后的衰减,在这个情况中我们可以使用SHADOW_ATTENUATION宏。
-
-```hlsl
-float FadeShadows (Interpolators i, float attenuation)
-{
- #if HANDLE_SHADOWS_BLENDING_IN_GI || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
- //UNITY_LIGHT_ATTENUATION doesn't fade shadows for us.
- #if ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
- attenuation = SHADOW_ATTENUATION(i);
- #endif
-
- #endif
- return attenuation;
-}
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender17/1692664-20200603002336571-896877423.png"
- des="两个定向光源正确的衰减" width="250"
-%}
-
-## 消减阴影-Subtractive Shadows
-
-混合光照很好,但是它不像完全烘焙光照那样成本低廉。如果以低性能硬件为目标,那么混合光照不太可行。烘焙光照会管用,但是事实上你**也许需要动态物体对静态物体投射阴影。那样的话,你可以使用消减混合光照模式**。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender17/1692664-20200603002337701-173459592.png"
- des="消减模式" width="250"
-%}
-
-在切换到消减模式后,场景会亮很多。这是由于静态物体现在同时使用完全烘焙的光照贴图和方向光源。这是因为动态物体仍然会同时使用光照探头和方向光源。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender17/1692664-20200603002338965-1025220581.png"
- des="静态物体受到两次光照" width="250"
-%}
-
-**消减模式只可用于前向渲染**。当使用延迟渲染路径时,相关的物体会回到前向渲染路径,就像透明物体那样。
-
-### 消减光照
-
-在消减模式中,静态物体通过光照贴图被照亮,同时还将动态阴影考虑在内。这是通过降低光照贴图在阴影区域的强度来实现的。为此,着色器需要使用光照贴图和实时阴影。它还需要使用实时光照来计算出要将光照贴图调暗多少。这就是为什么我们在切换到这个模式后得到了双重光照。
-
-**消减光照是一个近似,只在一个单一定向光下起作用,因此它只支持主方向光的阴影**。另外,我们必须以某种方式了解在动态着色区域内间接光的环境是什么。由于我们使用的是一个完全烘焙的光照贴图,我们没有这个信息。Unity没有包含一个额外的只有间接光的光照贴图,而是使用了一个统一的颜色对环境光取近似值。即实时阴影颜色(Realtime Shadow Color),你可以在混合光照选项中调整它。
-
-在着色器中,我们知道_当LIGHTMAP_ON_,_SHADOWS_SCREEN_,和_LIGHTMAP_SHADOW_MIXING_关键词被定义而_SHADOWS_SHADOWMASK_没有被定义时我们应该使用消减光照。如果这样的话我们定义_SUBTRACTIVE_LIGHTING_,以便更容易使用它。
-
-```hlsl
-#if !defined(LIGHTMAP_ON) && defined(SHADOWS_SCREEN)
- #if defined(SHADOWS_SHADOWMASK) && !defined(UNITY_NO_SCREENSPACE_SHADOWS)
- #define ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS 1
- #endif
-#endif
-
-#if defined(LIGHTMAP_ON) && defined(SHADOWS_SCREEN)
- #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK)
- #define SUBTRACTIVE_LIGHTING 1
- #endif
+ #if defined(LIGHTMAP_ON) || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
+ i.lightmapUV = v.uv1 * unity_LightmapST.xy + unity_LightmapST.zw;
+ #endif
+
+}
+
当光照贴图坐标可用时,我们可以再次使用FadeShadows函数进行我们自己控制的衰减。
+ +float FadeShadows (Interpolators i, float attenuation)
+{
+ #if HANDLE_SHADOWS_BLENDING_IN_GI || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
+
+ #endif
+ return attenuation;
+}
+
但是,这仍然不正确,因为我们为其输入了错误的衰减数据。我们必须绕开UNITY_LIGHT_ATTENUATION,只得到烘焙后的衰减,在这个情况中我们可以使用SHADOW_ATTENUATION宏。
+ +float FadeShadows (Interpolators i, float attenuation)
+{
+ #if HANDLE_SHADOWS_BLENDING_IN_GI || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
+ //UNITY_LIGHT_ATTENUATION doesn't fade shadows for us.
+ #if ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
+ attenuation = SHADOW_ATTENUATION(i);
+ #endif
+
+ #endif
+ return attenuation;
+}
+
混合光照很好,但是它不像完全烘焙光照那样成本低廉。如果以低性能硬件为目标,那么混合光照不太可行。烘焙光照会管用,但是事实上你也许需要动态物体对静态物体投射阴影。那样的话,你可以使用消减混合光照模式。
+ + +在切换到消减模式后,场景会亮很多。这是由于静态物体现在同时使用完全烘焙的光照贴图和方向光源。这是因为动态物体仍然会同时使用光照探头和方向光源。
+ + +消减模式只可用于前向渲染。当使用延迟渲染路径时,相关的物体会回到前向渲染路径,就像透明物体那样。
+ +在消减模式中,静态物体通过光照贴图被照亮,同时还将动态阴影考虑在内。这是通过降低光照贴图在阴影区域的强度来实现的。为此,着色器需要使用光照贴图和实时阴影。它还需要使用实时光照来计算出要将光照贴图调暗多少。这就是为什么我们在切换到这个模式后得到了双重光照。
+ +消减光照是一个近似,只在一个单一定向光下起作用,因此它只支持主方向光的阴影。另外,我们必须以某种方式了解在动态着色区域内间接光的环境是什么。由于我们使用的是一个完全烘焙的光照贴图,我们没有这个信息。Unity没有包含一个额外的只有间接光的光照贴图,而是使用了一个统一的颜色对环境光取近似值。即实时阴影颜色(Realtime Shadow Color),你可以在混合光照选项中调整它。
+ +在着色器中,我们知道_当LIGHTMAP_ON_,SHADOWS_SCREEN,和_LIGHTMAP_SHADOW_MIXING_关键词被定义而_SHADOWS_SHADOWMASK_没有被定义时我们应该使用消减光照。如果这样的话我们定义_SUBTRACTIVE_LIGHTING_,以便更容易使用它。
+ +#if !defined(LIGHTMAP_ON) && defined(SHADOWS_SCREEN)
+ #if defined(SHADOWS_SHADOWMASK) && !defined(UNITY_NO_SCREENSPACE_SHADOWS)
+ #define ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS 1
+ #endif
#endif
-```
-
-在做其他事情之前,我们必须去除掉双重阴影。为此我们可以关闭动态光照,就像我们对延迟通道所做的那样。
-
-```hlsl
-UnityLight CreateLight (Interpolators i)
-{
- UnityLight light;
-
- #if defined(DEFERRED_PASS) || SUBTRACTIVE_LIGHTING
- light.dir = float3(0, 1, 0);
- light.color = 0;
- #else
-
- #endif
-
- return light;
-}
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender17/1692664-20200603002340399-1575340233.png"
- des="静态物体只有烘焙光" width="250"
-%}
-
-### 为烘焙光打阴影
-
-为了应用消减阴影,我们创建一个函数以在需要的时候调整间接光。通常它不会做任何事。
-
-```hlsl
-void ApplySubtractiveLighting (
- Interpolators i, inout UnityIndirect indirectLight
-) {
-
-}
-```
-
-我们在获取光照贴图数据后要调用该函数。
-
-```hlsl
-UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) {
- #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS)
- #if defined(LIGHTMAP_ON)
- indirectLight.diffuse = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV));
-
- #if defined(DIRLIGHTMAP_COMBINED)
-
- #endif
-
- ApplySubtractiveLighting(i, indirectLight);
- #else
- indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
- #endif
-
- #endif
-
- return indirectLight;
-}
-```
-
-如果有消减光照,那么我们必须获取阴影衰减。我们可以简单地从CreateLight中将代码复制过来。
-
-```hlsl
-void ApplySubtractiveLighting (
- Interpolators i, inout UnityIndirect indirectLight
-) {
- #if SUBTRACTIVE_LIGHTING
- UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos.xyz);
- attenuation = FadeShadows(i, attenuation);
- #endif
-}
-```
-
-下一步,我们要计算出如果使用实时光照的话我们可以接收到多少光。我们假设该信息和烘焙在光照贴图中的信息相吻合。由于光照贴图只包含漫射光,我们只需计算定向光的Lambert。
-
-```hlsl
-#if SUBTRACTIVE_LIGHTING
- UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos.xyz);
- attenuation = FadeShadows(i, attenuation);
- float ndotl = saturate(dot(i.normal, _WorldSpaceLightPos0.xyz));
+
+#if defined(LIGHTMAP_ON) && defined(SHADOWS_SCREEN)
+ #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK)
+ #define SUBTRACTIVE_LIGHTING 1
+ #endif
#endif
-```
-
-为了达到阴影光照的强度,我们必须将兰伯特项乘以衰减。但是我们已经有了完全不含阴影的烘焙光照。因此我们估算一下有多少光被阴影挡住了。
-
-```hlsl
-float ndotl = saturate(dot(i.normal, _WorldSpaceLightPos0.xyz));
-float3 shadowedLightEstimate = ndotl * (1 - attenuation) * _LightColor0.rgb;
-```
-
-通过从烘焙光中减去该估值,我们最终得到了调整好的光照。
-
-```hlsl
-float3 shadowedLightEstimate = ndotl * (1 - attenuation) * _LightColor0.rgb;
-float3 subtractedLight = indirectLight.diffuse – shadowedLightEstimate;
-indirectLight.diffuse = subtractedLight;
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender17/1692664-20200603002341952-1577870051.png"
- des="减去后得到的光照" width="250"
-%}
-
-无论在什么环境光场景中,这总会产生纯黑色阴影。为了更好地符合场景的需要,我们可以使用我们的消减阴影颜色,可以通过unity_ShadowColor实现。阴影区域不应比这个颜色更暗,不过它们可以更亮些。所以我们取计算出的光照和阴影颜色的最大值。
-
-```hlsl
-float3 subtractedLight = indirectLight.diffuse - shadowedLightEstimate;
-subtractedLight = max(subtractedLight, unity_ShadowColor.rgb);
-indirectLight.diffuse = subtractedLight;
-```
-
-我们还要考虑到阴影强度被设置为小于1这个情况。为了应用阴影强度,在有阴影和无阴影光照之间基于_LightShadowData的X分量做插值。
-
-```hlsl
-subtractedLight = max(subtractedLight, unity_ShadowColor.rgb);
-subtractedLight = lerp(subtractedLight, indirectLight.diffuse, _LightShadowData.x);
-indirectLight.diffuse = subtractedLight;
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender17/1692664-20200603002343602-223725225.png"
- des="有颜色的阴影" width="250"
-%}
-
-因为我们的场景的环境强度(ambient intensity)被设置为0,所以默认的阴影颜色和场景不太搭配。但是可以很轻松地发现消减阴影,因此我没有调整它。还有一点非常明显,即阴影颜色现在覆盖了所有的烘焙阴影,而实际不应该这样。它应该只影响那些接收动态阴影的区域,不应该使烘焙阴影变亮。为此,使用消减光照和烘焙光照的最小值。
-
-```hlsl
-//indirectLight.diffuse = subtractedLight;
-indirectLight.diffuse = min(subtractedLight, indirectLight.diffuse);
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender17/1692664-20200603002345136-2052798411.png"
- des="正确的消减阴影" width="250"
-%}
-
-现在只要我们使用适当的阴影颜色,我们就会得到正确的消减阴影。但是记住这只是一个近似,而且它不太适用于多重光照。例如,其它的烘焙光会产生错误的阴影。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender17/1692664-20200603002346714-785287043.png"
- des="多重光照错误的消减" width="250"
-%}]]>catlikecoding Unity 光照烘焙(翻译十六) 2018-01-19T20:00:00+08:00 2018-01-19T20:00:00+08:00 https://www.damonc.top/Unity_Static_Lightting
在做其他事情之前,我们必须去除掉双重阴影。为此我们可以关闭动态光照,就像我们对延迟通道所做的那样。
+ +UnityLight CreateLight (Interpolators i)
+{
+ UnityLight light;
+
+ #if defined(DEFERRED_PASS) || SUBTRACTIVE_LIGHTING
+ light.dir = float3(0, 1, 0);
+ light.color = 0;
+ #else
+
+ #endif
+
+ return light;
+}
+
为了应用消减阴影,我们创建一个函数以在需要的时候调整间接光。通常它不会做任何事。
+ +void ApplySubtractiveLighting (
+ Interpolators i, inout UnityIndirect indirectLight
+) {
+
+}
+
我们在获取光照贴图数据后要调用该函数。
+ +UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) {
+ #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS)
+ #if defined(LIGHTMAP_ON)
+ indirectLight.diffuse = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV));
+
+ #if defined(DIRLIGHTMAP_COMBINED)
+
+ #endif
+
+ ApplySubtractiveLighting(i, indirectLight);
+ #else
+ indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
+ #endif
+
+ #endif
+
+ return indirectLight;
+}
+
如果有消减光照,那么我们必须获取阴影衰减。我们可以简单地从CreateLight中将代码复制过来。
+ +void ApplySubtractiveLighting (
+ Interpolators i, inout UnityIndirect indirectLight
+) {
+ #if SUBTRACTIVE_LIGHTING
+ UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos.xyz);
+ attenuation = FadeShadows(i, attenuation);
+ #endif
+}
+
下一步,我们要计算出如果使用实时光照的话我们可以接收到多少光。我们假设该信息和烘焙在光照贴图中的信息相吻合。由于光照贴图只包含漫射光,我们只需计算定向光的Lambert。
+ +#if SUBTRACTIVE_LIGHTING
+ UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos.xyz);
+ attenuation = FadeShadows(i, attenuation);
+ float ndotl = saturate(dot(i.normal, _WorldSpaceLightPos0.xyz));
+#endif
+
为了达到阴影光照的强度,我们必须将兰伯特项乘以衰减。但是我们已经有了完全不含阴影的烘焙光照。因此我们估算一下有多少光被阴影挡住了。
+ +float ndotl = saturate(dot(i.normal, _WorldSpaceLightPos0.xyz));
+float3 shadowedLightEstimate = ndotl * (1 - attenuation) * _LightColor0.rgb;
+
通过从烘焙光中减去该估值,我们最终得到了调整好的光照。
+ +float3 shadowedLightEstimate = ndotl * (1 - attenuation) * _LightColor0.rgb;
+float3 subtractedLight = indirectLight.diffuse – shadowedLightEstimate;
+indirectLight.diffuse = subtractedLight;
+
无论在什么环境光场景中,这总会产生纯黑色阴影。为了更好地符合场景的需要,我们可以使用我们的消减阴影颜色,可以通过unity_ShadowColor实现。阴影区域不应比这个颜色更暗,不过它们可以更亮些。所以我们取计算出的光照和阴影颜色的最大值。
+ +float3 subtractedLight = indirectLight.diffuse - shadowedLightEstimate;
+subtractedLight = max(subtractedLight, unity_ShadowColor.rgb);
+indirectLight.diffuse = subtractedLight;
+
我们还要考虑到阴影强度被设置为小于1这个情况。为了应用阴影强度,在有阴影和无阴影光照之间基于_LightShadowData的X分量做插值。
+ +subtractedLight = max(subtractedLight, unity_ShadowColor.rgb);
+subtractedLight = lerp(subtractedLight, indirectLight.diffuse, _LightShadowData.x);
+indirectLight.diffuse = subtractedLight;
+
因为我们的场景的环境强度(ambient intensity)被设置为0,所以默认的阴影颜色和场景不太搭配。但是可以很轻松地发现消减阴影,因此我没有调整它。还有一点非常明显,即阴影颜色现在覆盖了所有的烘焙阴影,而实际不应该这样。它应该只影响那些接收动态阴影的区域,不应该使烘焙阴影变亮。为此,使用消减光照和烘焙光照的最小值。
+ +//indirectLight.diffuse = subtractedLight;
+indirectLight.diffuse = min(subtractedLight, indirectLight.diffuse);
+
现在只要我们使用适当的阴影颜色,我们就会得到正确的消减阴影。但是记住这只是一个近似,而且它不太适用于多重光照。例如,其它的烘焙光会产生错误的阴影。
+ + +为了尝试光照贴图,我创建了一个简单的测试场景,它具有一个简单的结构,可以提供阴影,还有一些放置在其内部的球体。一切物体都使用默认的Unity材质。 +
+要开始使用光照贴图,将唯一的光源对象的模式改为“Baked(烘焙)”而不是“Realtime(实时)”。
+ + +将主方向光源变成烘培光源后,就不被纳入动态光照计算。从动态对象的角度来看,光源是不存在的。 唯一仍然不变的是环境光照,它仍然是基于主方向光源的。
+ + +要实际启用光照贴图,请在lighting窗口的“混合光照(Mixed Lighting)”中打开“烘培全局光照(BakedGlobal Illumination)”。 然后将光照模式设置为“烘培间接光照(BakedIndirect)”。 尽管它的名字说的是烘培间接光照,但是它也包括了直接光照。 它通常用于向场景添加间接光照。另外,确保实时全局光照(Realtime Global Illumination)被禁用,因为我们还没有支持到这一点。
+ + +场景的对象都应该是固定的:它们位置永远不会移动。要将这一个信息传达给Unity,请将这些对象标记为静态。你可以通过启用检视器窗口右上角的“静态”切换键来做到这一点。
+ +光源也必须被标记为静态吗?
+有各种子系统关心物体是否是静态的。“静态(static)”还有一个下拉菜单,你可以使用它来微调哪些系统会将这个对象视为静态的。现在我们只关心光照贴图,但最简单的做法是使一切都完全是静态的。
+ + +一个物体对于光照贴图来说是否是静态的,也可以通过其网格渲染器的检视器来进行查看和编辑。
+ + +现在,所有的物体都是静态的,它们将被包含在光照贴图的处理过程中。
+ + +必须注意,使用光照贴图得到的结果不如使用实时照明得到的结果亮度那么高。这是因为缺失了镜面高光,只剩下了漫反射光照。镜面高光取决于视角,因此取决于相机的角度。正是由于相机是移动的,因此它不能包含在光照贴图中。(使用场景推荐)这种限制意味着光照贴图可以用于微弱的光线和暗淡的表面,但不能用于强直射光或有光泽的表面。如果你想要镜面高光,你将不得不使用实时光源。所以你经常会使用烘烤光源和实时光源的混合。
-有各种子系统关心物体是否是静态的。“静态(static)”还有一个下拉菜单,你可以使用它来微调哪些系统会将这个对象视为静态的。现在我们只关心光照贴图,但最简单的做法是使一切都完全是静态的。 - -{% include img-picture.html - url1="posts/2018/month1/catRender16/1692664-20200601224546220-2118005287.png" - des="静态标签设定" width="250" -%} - -一个物体对于光照贴图来说是否是静态的,也可以通过其网格渲染器的检视器来进行查看和编辑。 - -{% include img-picture.html - url1="posts/2018/month1/catRender16/1692664-20200601224546912-1870383195.png" - des="对于光照贴图来说是静态的物体" width="250" -%} - - -现在,所有的物体都是静态的,它们将被包含在光照贴图的处理过程中。 - -{% include img-picture.html - url1="posts/2018/month1/catRender16/1692664-20200601224547954-1770311194.png" - des="使用烘焙光照的场景" width="250" -%} - -**必须注意**,使用光照贴图得到的结果不如使用实时照明得到的结果亮度那么高。这是因为缺失了镜面高光,只剩下了漫反射光照。镜面高光取决于视角,因此取决于相机的角度。正是由于相机是移动的,因此它不能包含在光照贴图中。(使用场景推荐)这种限制意味着光照贴图可以用于微弱的光线和暗淡的表面,但不能用于强直射光或有光泽的表面。如果你想要镜面高光,你将不得不使用实时光源。所以你经常会使用烘烤光源和实时光源的混合。 - -``` -为什么没有立即得到烘焙光源? +为什么没有立即得到烘焙光源?
为了确保在需要的时候光照贴图可以实际生成和更新,请在光照窗口的底部启用“自动生成(Auto Generate)”。 否则,你必须手动生成新的光照贴图。
-```
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender16/1692664-20200601224548501-1138122298.png"
- des="自动烘焙" width="250"
-%}
-
-### 光照贴图设置-Lightingmapping Setting
-
-光照烘焙窗口包含专门用于光照贴图设置的部分。在这里,你可以在**质量**、**尺寸**和**烘烤时间**之间取得平衡。你还可以在光照贴图烘焙算法引擎:Enlighten和Progressive lightmapper之间进行切换。后者会增量地生成光照贴图,优先考虑场景视图中可见的内容,这在编辑的时候很方便。本教程中使用的是Enlighten光照贴图引擎。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender16/1692664-20200601224549324-867042323.png"
- des="默认的光照贴图设置" width="250"
-%}
-
-在做任何事情之前,请将“DirectionalMode“设置为”Non-Direction“。 稍后我们会处理其他模式。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender16/1692664-20200601224549936-119016943.png"
- des="使用“Non-directional”模式的光照贴图" width="250"
-%}
-
-烘烤的光照存储在纹理中。 你可以通过将光照窗口从“场景(Scene)“切换到”全局地图(_Global Maps_)“模式来进行查看。 使用默认设置,我的测试场景很容易与一张1024×1024贴图相匹配。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender16/1692664-20200601224550735-1332475026.png"
- des="得到光照贴图" width="250"
-%}
-
-
-Unity自带的Objects物体都有用于光照贴图的UV坐标。对于手动导入的模型,可以自己提供UV坐标,也可以让Unity生成。烘烤后可以在光照贴图中看到展开的纹理。它们需要多少空间取决于场景中物体的大小和光照贴图的分辨率设置。 如果质量要求高分辨率太大,一张贴图涨不下,Unity会创建额外的贴图存储,直至完成。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender16/1692664-20200601224551453-953456979.png"
- url2="posts/2018/month1/catRender16/1692664-20200601224552186-1643111793.png"
- des="光照贴图的分辨率的不同会带来很大的差异" width="250"
-%}
-
-
-对于每个项目来说,最佳设置都是不同。 你必须不断的调整烘焙参数,直到达成很好的效果及平衡。需要注意的是,视觉质量也很大程度上取决于用于光照贴图的纹理展开的质量。不存在纹理接缝可能会产生明显的瑕疵。Unity的默认球体就是一个很好的例子。它不适用于光照贴图。
-
-### 间接光源
-
-烘焙光照会失去镜面高光,只能获得的是间接光照,它是在到达人眼之前会在多个表面反射的光。烘焙光会在拐角周围区域反射,那些本来会被遮挡的区域仍然会被照亮。我们不能实时计算镜面高光这个信息(本节1.2有说明),但是我们可以在烘焙的时候包括反射光。
-
-要清楚地看到实时光照和烘培光照之间的差异:将环境光照的强度设置为零,去掉天空盒的影响,所有的光都只是来自方向光。比对
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender16/1692664-20200601224552903-1416086887.png"
- url2="posts/2018/month1/catRender16/1692664-20200601224553660-737254829.png"
- des="没有环境光照,realtime vs. lightmapped" width="250"
-%}
-
-每次光子反射的时候,它都会失去一些能量,它会被一些需要的材质采样着色。Unity在烘焙间接光照的时候,物体会根据附近的颜色进行着色。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender16/1692664-20200601224554555-1690717767.png"
- url2="posts/2018/month1/catRender16/1692664-20200601224555273-1136371525.png"
- des="绿色的地面,realtime vs. lightmapped" width="250"
-%}
-
-自发光表面也会影响烘焙光照。它们会成为间接光源。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender16/1692664-20200601224556002-270644308.png"
- url2="posts/2018/month1/catRender16/1692664-20200601224556791-2131078685.png"
- des="自发光的地面,realtime vs. lightmapped" width="250"
-%}
-
-间接光照的一个特殊设置是AO环境遮挡:这是指在角落和转折中发生的间接光照造成的阴影。这是一种人为的提升,可以增强深度方面的视觉。
-
-{% include img-picture.html
- url1="posts/2018/month1/catRender16/1692664-20200601224557440-355583450.png"
- url2="posts/2018/month1/catRender16/1692664-20200601224558173-2138402443.png"
- des="使用环境遮挡的效果" width="250"
-%}
-
-环境遮挡效果完全基于物体表面。它不考虑光线实际来自哪里。烘焙时并不总是正确,举个简单的例子:当与自发光表面组合的时候就会产生一些错误的结果。
+
光照烘焙窗口包含专门用于光照贴图设置的部分。在这里,你可以在质量、尺寸和烘烤时间之间取得平衡。你还可以在光照贴图烘焙算法引擎:Enlighten和Progressive lightmapper之间进行切换。后者会增量地生成光照贴图,优先考虑场景视图中可见的内容,这在编辑的时候很方便。本教程中使用的是Enlighten光照贴图引擎。
+ + +在做任何事情之前,请将“DirectionalMode“设置为”Non-Direction“。 稍后我们会处理其他模式。
+ + +烘烤的光照存储在纹理中。 你可以通过将光照窗口从“场景(Scene)“切换到”全局地图(Global Maps)“模式来进行查看。 使用默认设置,我的测试场景很容易与一张1024×1024贴图相匹配。
+ + +Unity自带的Objects物体都有用于光照贴图的UV坐标。对于手动导入的模型,可以自己提供UV坐标,也可以让Unity生成。烘烤后可以在光照贴图中看到展开的纹理。它们需要多少空间取决于场景中物体的大小和光照贴图的分辨率设置。 如果质量要求高分辨率太大,一张贴图涨不下,Unity会创建额外的贴图存储,直至完成。
+ + +对于每个项目来说,最佳设置都是不同。 你必须不断的调整烘焙参数,直到达成很好的效果及平衡。需要注意的是,视觉质量也很大程度上取决于用于光照贴图的纹理展开的质量。不存在纹理接缝可能会产生明显的瑕疵。Unity的默认球体就是一个很好的例子。它不适用于光照贴图。
+ +烘焙光照会失去镜面高光,只能获得的是间接光照,它是在到达人眼之前会在多个表面反射的光。烘焙光会在拐角周围区域反射,那些本来会被遮挡的区域仍然会被照亮。我们不能实时计算镜面高光这个信息(本节1.2有说明),但是我们可以在烘焙的时候包括反射光。
+ +要清楚地看到实时光照和烘培光照之间的差异:将环境光照的强度设置为零,去掉天空盒的影响,所有的光都只是来自方向光。比对
+ + +每次光子反射的时候,它都会失去一些能量,它会被一些需要的材质采样着色。Unity在烘焙间接光照的时候,物体会根据附近的颜色进行着色。
+ + +自发光表面也会影响烘焙光照。它们会成为间接光源。
+ + +间接光照的一个特殊设置是AO环境遮挡:这是指在角落和转折中发生的间接光照造成的阴影。这是一种人为的提升,可以增强深度方面的视觉。
+ + +环境遮挡效果完全基于物体表面。它不考虑光线实际来自哪里。烘焙时并不总是正确,举个简单的例子:当与自发光表面组合的时候就会产生一些错误的结果。
+ + +光照贴图在一定程度上可以处理半透明表面。 光将通过它们,尽管光的颜色不会被它们所过滤。
+ + +镂空材质也可以在光照贴图中正常工作。
+ + +但 这仅在使用封闭曲面的时候有效。当使用像是quad这样的单面几何,光线将在不存在的一面损坏。当另外一面没有任何东西的时候,这是很好的,但是当使用单面透明表面的时候会导致问题。
+ + +为了处理这个问题,必须告诉光照贴图系统将这些表面视为透明的。 这可以通过自定义光照贴图设置完成
+ +将物体标记为透明也会改变它对间接光照的贡献。透明物体让间接光通过,而不透明物体则会阻挡间接光。
+ +现在我们知道光照贴图是如何工作的,我们可以为Shader着色器添加对光照贴图的支持。第一步是对光照贴图进行采样。调整场景中的球体,以便我们的着色器使用白色材质。
+ + +当一个着色器被认为应该使用光照贴图的时候,Unity会寻找与LIGHTMAP_ON关键字关联的变体。 所以我们必须为这个关键字添加一个多编译指令。 当使用forward-render-path的时候,仅在base-pass中采样光照贴图。
-{% include img-picture.html - url1="posts/2018/month1/catRender16/1692664-20200601224559715-1180880317.png" - des="半透明的屋顶" width="250" -%} - -镂空材质也可以在光照贴图中正常工作。 - -{% include img-picture.html - url1="posts/2018/month1/catRender16/1692664-20200601224600513-791335188.png" - des="镂空的屋顶" width="250" -%} - -但 这仅在使用封闭曲面的时候有效。当使用像是quad这样的单面几何,光线将在不存在的一面损坏。_当另外一面没有任何东西的时候,这是很好的,但是当使用单面透明表面的时候会导致问题_。 - -{% include img-picture.html - url1="posts/2018/month1/catRender16/1692664-20200601224601275-358793307.png" - des="四边形上有一个错误" width="250" -%} - -为了处理这个问题,必须告诉光照贴图系统将这些表面视为透明的。 这可以通过自定义光照贴图设置完成 - -1. 通过Asset / Create / Lightmap参数来创建这些数据。这些资源允许你自定义每个对象的光照贴图计算。在这种情况下,我们只想表明我们正在处理一个透明的对象。所以启用“它是透明的(_Is Transparent_)“。 下面它是一个全局作用预计算实时全局光照(_Precomputed Realtime GI_)部分中的一部分,它会影响所有烘烤光照。 - -{% include img-picture.html - url1="posts/2018/month1/catRender16/1692664-20200601224602678-564328235.png" - des="指示这是透明的" width="250" -%} - -2. 单独设置:通过物体的网格渲染器检视器来选择它们。你的资源名字将显示在Lightmap参数的下拉列表中。 - -{% include img-picture.html - url1="posts/2018/month1/catRender16/1692664-20200601224603354-539689204.png" - url2="posts/2018/month1/catRender16/1692664-20200601224604232-1056111478.png" - des="为透明四边形使用自定义参数" width="250" -%} - - -将物体标记为透明也会改变它对间接光照的贡献。透明物体让间接光通过,而不透明物体则会阻挡间接光。 - -## 使用光照贴图 - -现在我们知道光照贴图是如何工作的,我们可以为Shader着色器添加对光照贴图的支持。第一步是对光照贴图进行采样。调整场景中的球体,以便我们的着色器使用白色材质。 - -{% include img-picture.html - url1="posts/2018/month1/catRender16/1692664-20200601224605139-922193658.png" - des="使用我们的白色材质的球体" width="250" -%} - -### 光照贴图的着色器变体 - -当一个着色器被认为应该使用光照贴图的时候,Unity会寻找与LIGHTMAP_ON关键字关联的变体。 所以我们必须为这个关键字添加一个多编译指令。 当使用forward-render-path的时候,仅在base-pass中采样光照贴图。 - -```hlsl -#pragma multi_compile _ SHADOWS_SCREEN +#pragma multi_compile _ SHADOWS_SCREEN
#pragma multi_compile _ VERTEXLIGHT_ON
#pragma multi_compile _ LIGHTMAP_ON
#pragma multi_compile_fog
-```
+
当使用光照贴图的时候,Unity不会包含顶点光源。他们的关键字是_相互排斥_的。所以我们不需要一个会同时使用_VERTEXLIGHT_ON_和_LIGHTMAP_ON_的变体。(互斥)
-```hlsl -#pragma multi_compile _ SHADOWS_SCREEN -//#pragma multi_compile _ VERTEXLIGHT_ON -//#pragma multi_compile _ LIGHTMAP_ON -#pragma multi_compile _ LIGHTMAP_ON VERTEXLIGHT_ON +#pragma multi_compile _ SHADOWS_SCREEN
+//#pragma multi_compile _ VERTEXLIGHT_ON
+//#pragma multi_compile _ LIGHTMAP_ON
+#pragma multi_compile _ LIGHTMAP_ON VERTEXLIGHT_ON
#pragma multi_compile_fog
-```
+
延迟渲染路径中也支持光照贴图,因此也可以将这个关键字添加到延迟渲染通道中。
-```hlsl -#pragma multi_compile _ UNITY_HDR_ON +#pragma multi_compile _ UNITY_HDR_ON
#pragma multi_compile _ LIGHTMAP_ON
-```
-
-### 光照贴图的坐标
-
-用于采样光照贴图的坐标存储在TEXCOORD1。 所以将此通道添加到shader中的VertexData结构体中。Unity给出了uv使用说明表:Shader中是uv0、uv1、uv2、uv3;C#中是UV、UV2、UV3、UV4
-
-```hlsl
-struct VertexData
-{
- float4 vertex : POSITION;
- float3 normal : NORMAL;
- float4 tangent : TANGENT;
- float2 uv : TEXCOORD0;
- float2 uv1 : TEXCOORD1;
-};
-```
-
-光照贴图坐标也必须进行插值。因为它们与顶点光源互斥,所以都可以使用TEXCOORD6。
-
-```hlsl
-struct Interpolators
-{
- …
- #if defined(VERTEXLIGHT_ON)
- float3 vertexLightColor : TEXCOORD6;
- #endif
-
- #if defined(LIGHTMAP_ON)
- float2 lightmapUV : TEXCOORD6;
- #endif
-};
-```
-
-来自模型顶点数据的坐标定义了用于光照贴图的纹理展开(第二套uv)。但是它并没有告诉我们这个展开位置在哪里,展开尺寸大小。我们必须缩放和偏移坐标才能得到最终的光照贴图坐标。这种方法类似于常规纹理坐标的转换,除了转换是特定于对象的,而这里的方法是特定于材质的。在_UnityShaderVariables_中将光照贴图的纹理定义为_unity_Lightmap_。
-
-```hlsl
-Interpolators MyVertexProgram (VertexData v)
-{
- i.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
- i.uv.zw = TRANSFORM_TEX(v.uv, _DetailTex);
- #if defined(LIGHTMAP_ON)
- i.lightmapUV = TRANSFORM_TEX(v.uv1, unity_Lightmap);
- #endif
-}
-```
-
-不幸的是,我们不能使用方便的_TRANSFORM_TEX_宏,因为它假定光照贴图的变换被被定义为_unity_Lightmap_ST_,而实际上是被定义为_unity_LightmapST_。由于这种不一致,我们必须手动进行这个变换。
+
用于采样光照贴图的坐标存储在TEXCOORD1。 所以将此通道添加到shader中的VertexData结构体中。Unity给出了uv使用说明表:Shader中是uv0、uv1、uv2、uv3;C#中是UV、UV2、UV3、UV4
+ +struct VertexData
+{
+ float4 vertex : POSITION;
+ float3 normal : NORMAL;
+ float4 tangent : TANGENT;
+ float2 uv : TEXCOORD0;
+ float2 uv1 : TEXCOORD1;
+};
+
光照贴图坐标也必须进行插值。因为它们与顶点光源互斥,所以都可以使用TEXCOORD6。
+ +struct Interpolators
+{
+ …
+ #if defined(VERTEXLIGHT_ON)
+ float3 vertexLightColor : TEXCOORD6;
+ #endif
+
+ #if defined(LIGHTMAP_ON)
+ float2 lightmapUV : TEXCOORD6;
+ #endif
+};
+
来自模型顶点数据的坐标定义了用于光照贴图的纹理展开(第二套uv)。但是它并没有告诉我们这个展开位置在哪里,展开尺寸大小。我们必须缩放和偏移坐标才能得到最终的光照贴图坐标。这种方法类似于常规纹理坐标的转换,除了转换是特定于对象的,而这里的方法是特定于材质的。在_UnityShaderVariables_中将光照贴图的纹理定义为_unity_Lightmap_。
+ +Interpolators MyVertexProgram (VertexData v)
+{
+ i.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
+ i.uv.zw = TRANSFORM_TEX(v.uv, _DetailTex);
+ #if defined(LIGHTMAP_ON)
+ i.lightmapUV = TRANSFORM_TEX(v.uv1, unity_Lightmap);
+ #endif
+}
+
不幸的是,我们不能使用方便的_TRANSFORM_TEX_宏,因为它假定光照贴图的变换被被定义为_unity_Lightmap_ST_,而实际上是被定义为_unity_LightmapST_。由于这种不一致,我们必须手动进行这个变换。
+ +i.lightmapUV = v.uv1 *unity_LightmapST.xy + unity_LightmapST.zw;
+
因为光照贴图的数据被认为是间接光照,我们将在CreateIndirectLight函数中进行采样。当光照贴图可用的时候,必须将它们用作间接光而不是球面谐波。
+ +UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) {
+ #if defined(VERTEXLIGHT_ON)
+ indirectLight.diffuse = i.vertexLightColor;
+ #endif
+
+ #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS)
+ #if defined(LIGHTMAP_ON)
+ indirectLight.diffuse = 0;
+ #else
+ indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
+ #endif
+ float3 reflectionDir = reflect(-viewDir, i.normal);
+
+ #endif
+
+ return indirectLight;
+}
+
为什么indirectLight.diffuse的值被赋值而不是加起来?光照贴图从来没有与顶点光源组合起来。
+ +unity_Lightmap的确切形式取决于目标平台。 它被定义为UNITY_DECLARE_TEX2D(unity_Lightmap)。要对它进行采样,我们将使用UNITY_SAMPLE_TEX2D宏而不是tex2D。这是根据不同平台决定。
+ +indirectLight.diffuse = UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV);
+
我们现在得到了烘焙的间接光照,但效果看起来不对。这是因为光照贴图数据已被编码。颜色以RGBM格式或是半强度格式进行存储,以支持高强度的光。UnityCG的DecodeLightmap函数负责为我们解码。
+ +indirectLight.diffuse = DecodeLightmap
+(
+ UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV)
+);
+
目前,光照贴图会将场景对象总是视为不透明和纯白色的物体。我们必须对我们的着色器进行一些调整,添加一个渲染通道来完全支持光照贴图。
+ +从现在开始,对场景中的所有对象使用我们自己的着色器。也不再使用默认的材质。
+ +光照贴图不使用实时渲染管道,因此现有自写的shader不能支持。 当尝试使用半透明阴影的时候,这是最明显的。通过设置屋顶立方体材质的色调alpha分量小于1来赋予屋顶立方体半透明度。
+ + +光照贴图仍然把屋顶看成是实心物体,这是不正确的。它使用材质的渲染类型来确定如何处理表面,这应该告诉光照贴图我们的对象是半透明的。事实上,它确实知道屋顶是半透明的,它只是把它看作是完全不透明的而已。这是因为它采用Unity的命名约定_Color材质属性的alpha组件以及主纹理来设置不透明度。
+ +用_Color替换_Tint。
+ +Properties
+{
+ // _Tint ("Tint", Color) = (1, 1, 1, 1)
+ _Color ("Tint", Color) = (1, 1, 1, 1)
+}
+
然后,为了保证我们的着色器的功能,我们还必须在shader文件、cg文件替换,而且我们还要调整GUI拓展。
-### 对光照贴图进行采样-Sampling Lightmap + +镂空部分的阴影也有类似的问题。光照贴图程序期望透明度的阈值存储在_Cutoff属性中,但是我们使用的是_AlphaCutoff。 因此,它使用默认阈值1。
- #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS) - #if defined(LIGHTMAP_ON) - indirectLight.diffuse = 0; - #else - indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1))); - #endif - float3 reflectionDir = reflect(-viewDir, i.normal); + +解决方案是再次采用Unity的命名约定_Cutoff材质属性。所以替换shader、cg文件、GUI拓展。
- return indirectLight; -} -``` + +渲染光照贴图正确的表面反照率和自发光。
-```hlsl -indirectLight.diffuse = UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV); -``` + +要采样物体的表面颜色,光照贴图程序会将它的光照模式设置为Meta来寻找一个着色器渲染通道。这个渲染通道仅由光照贴图程序使用,不使用剔除。所以让我们在我们的着色器上添加一个渲染通道。
-我们现在得到了烘焙的间接光照,但效果看起来不对。这是因为光照贴图数据已被编码。颜色以RGBM格式或是半强度格式进行存储,以支持高强度的光。UnityCG的DecodeLightmap函数负责为我们解码。 +Pass {
+ Tags {
+ "LightMode" = "Meta"
+ }
+ Cull Off
-```hlsl
-indirectLight.diffuse = DecodeLightmap
-(
- UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV)
-);
-```
+ CGPROGRAM
-{% include img-picture.html
- url1="posts/2018/month1/catRender16/1692664-20200601224606764-748268587.png"
- des="使用解码后光照图数据的效果" width="250"
-%}
+ #pragma vertex MyLightmappingVertexProgram
+ #pragma fragment MyLightmappingFragmentProgram
+ #include "My Lightmapping.cginc"
+
+ ENDCG
+}
+
现在我们需要确定反照率、镜面高光颜色、平滑度、自发光。只需要顶点的位置和uv坐标,以及需要vertexProgram中的光照贴图坐标。不使用法线和切线。
-## 创建光照贴图 - -_目前,光照贴图会将场景对象总是视为不透明和纯白色的物体。我们必须对我们的着色器进行一些调整,添加一个渲染通道来完全支持光照贴图。_ - -_从现在开始,对场景中的所有对象使用我们自己的着色器。也不再使用默认的材质。_ - -### 半透明的阴影-Semitransparent Shadow +#if !defined(MY_LIGHTMAPPING_INCLUDED)
+#define MY_LIGHTMAPPING_INCLUDED
+
+#include "UnityPBSLighting.cginc"
+
+float4 _Color;
+sampler2D _MainTex, _DetailTex, _DetailMask;
+float4 _MainTex_ST, _DetailTex_ST;
+
+sampler2D _MetallicMap;
+float _Metallic;
+float _Smoothness;
+
+sampler2D _EmissionMap;
+float3 _Emission;
+
+struct VertexData {
+ float4 vertex : POSITION;
+ float2 uv : TEXCOORD0;
+ float2 uv1 : TEXCOORD1;
+};
+
+struct Interpolators {
+ float4 pos : SV_POSITION;
+ float4 uv : TEXCOORD0;
+};
+
+float GetDetailMask (Interpolators i) {
+ …
+}
+
+float3 GetAlbedo (Interpolators i) {
+ …
+}
+
+float GetMetallic (Interpolators i) {
+ …
+}
+
+float GetSmoothness (Interpolators i) {
+ …
+}
+
+float3 GetEmission (Interpolators i) {
+ …
+}
+
+#endif
+
GetEmission函数去除_FORWARD_BASE_PASS_和_DEFERRED_PASS_限制。
+ +float3 GetEmission (Interpolators i) {
+// #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS)
+ #if defined(_EMISSION_MAP)
+ return tex2D(_EmissionMap, i.uv.xy) *_Emission;
+ #else
+ return _Emission;
+ #endif
+// #else
+// return 0;
+// #endif
+}
+
这些函数只有在定义了适当的关键字时才会起作用,因此可以在渲染通道中为其添加着色功能。
+ +#pragma vertex MyLightmappingVertexProgram
+#pragma fragment MyLightmappingFragmentProgram
+
+#pragma shader_feature _METALLIC_MAP
+#pragma shader_feature _ _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC
+#pragma shader_feature _EMISSION_MAP
+#pragma shader_feature _DETAIL_MASK
+#pragma shader_feature _DETAIL_ALBEDO_MAP
+
+#include "My Lightmapping.cginc"
+
这个pass的vertex 程序很简单。只是转换位置、转换纹理坐标。
+ +Interpolators MyLightmappingVertexProgram (VertexData v) {
+ Interpolators i;
+ i.pos = UnityObjectToClipPos(v.vertex);
+
+ i.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
+ i.uv.zw = TRANSFORM_TEX(v.uv, _DetailTex);
+ return i;
+}
+
计算2.2提到的映射偏移,我们必须使用光照贴图uv坐标而不是顶点位置,然后进行适当的转换把纹理uv坐标作为模型顶点的屏幕位置,模型的UV映射必须要正确:纹理上的每个点必须映射为模型上的唯一点。
+ +Interpolators i;
+v.vertex.xy = v.uv1 *unity_LightmapST.xy + unity_LightmapST.zw;
+v.vertex.z = 0;
+i.pos = UnityObjectToClipPos(v.vertex);
+
v.vertex.z = 0,不是所有机器上都能支持,顶点位置的Z坐标必须以某种方式使用,即使我们不使用它也是如此。Unity的着色器为此使用虚拟值,所以我们将简单地做同样的事情。
+ +Interpolators i;
+v.vertex.xy = v.uv1 *unity_LightmapST.xy + unity_LightmapST.zw;
+v.vertex.z = v.vertex.z > 0 ? 0.0001 : 0;
+i.pos = UnityObjectToClipPos(v.vertex);
+
在片段程序中,计算输出反照率和自发光颜色。光照贴图程序将通过执行两次渲染来做到这一点,每次执行有一个输出。为了使这个过程更容易,我们可以使用_UnityMetaPass.cginc文_件中定义的_UnityMetaFragment_函数。它使用_UnityMetaInput_结构作为参数,其中包含反照率和自发光颜色。 该函数将决定要输出反照率和自发光颜色中的哪一个以及如何编码输出结果。
+ +UnityMetaInput也包含镜面高光颜色,即使它不存储在光照贴图中。它用于一些编辑器可视化,我们先忽略它。
+ +#include "UnityPBSLighting.cginc"
+#include "UnityMetaPass.cginc"
+…
+float4 MyLightmappingFragmentProgram (Interpolators i) : SV_TARGET {
+ UnityMetaInput surfaceData;
+ surfaceData.Emission = 0;
+ surfaceData.Albedo = 0;
+ surfaceData.SpecularColor = 0;
+ return UnityMetaFragment(surfaceData);
+}
+
UnityMetaFragment是什么样子的?
+ +//unity_MetaFragmentControl变量包含一个标记,这个标记会告诉函数是否输出反照率或是自发光颜色。还有一段有关
+//编辑器可视化变体的代码,但是我把它删掉了,因为与这里的内容不相关。
+half4 UnityMetaFragment (UnityMetaInput IN) {
+ half4 res = 0;
+ if (unity_MetaFragmentControl.x) {
+ res = half4(IN.Albedo,1);
+
+ // d3d9 shader compiler doesn't like NaNs and infinity.
+ unity_OneOverOutputBoost = saturate(unity_OneOverOutputBoost);
+
+ // Apply Albedo Boost from LightmapSettings.
+ res.rgb = clamp(
+ pow(res.rgb, unity_OneOverOutputBoost), 0, unity_MaxOutputValue
+ );
+ }
+ if (unity_MetaFragmentControl.y) {
+ half3 emission;
+ if (unity_UseLinearSpace)
+ emission = IN.Emission;
+ else
+ emission = GammaToLinearSpace (IN.Emission);
+
+ res = UnityEncodeRGBM(emission, EMISSIVE_RGBM_SCALE);
+ }
+ return res;
+}
+
+
要获得自发光颜色,我们可以简单的使用GetEmission函数。要获得反照率,我们必须再次使用_DiffuseAndSpecularFromMetallic_函数。 该函数具有镜面高光颜色和反射率作为输出参数,即使我们现在不使用它们,我们也必须提供这些参数。我们可以使用surfaceData.SpecularColor来捕获镜面高光颜色。
+ +float4 MyLightmappingFragmentProgram (Interpolators i) : SV_TARGET
+{
+ UnityMetaInput surfaceData;
+ surfaceData.Emission = GetEmission(i);
+ float oneMinusReflectivity;
+ surfaceData.Albedo = DiffuseAndSpecularFromMetallic
+ (
+ GetAlbedo(i), GetMetallic(i),
+ surfaceData.SpecularColor, oneMinusReflectivity
+ );
+ //surfaceData.SpecularColor = 0;
+ return UnityMetaFragment(surfaceData);
+}
+
但自发光光照可能还没有出现在光照贴图中。这是因为光照贴图程序并不总是包含一个自发光光照的渲染通道。材质必须表明它们具有自发光光照属性,以对烘焙过程做出贡献。这是通过Material.globalIlluminationFlags属性完成的。扩展GUI设置:当自发光光照编辑的时候,它应该被烘焙进光照贴图。
+ +void DoEmission () {
+ …
+ if (EditorGUI.EndChangeCheck()) {
+ if (tex != map.textureValue) {
+ SetKeyword("_EMISSION_MAP", map.textureValue);
+ }
+ foreach (Material m in editor.targets) {
+ m.globalIlluminationFlags = MaterialGlobalIlluminationFlags.BakedEmissive;
+ }
+ }
+}
+
我们的shader现在看起来可以正常工作了,但它与标准着色器的结果不完全匹配。 当使用平滑度非常低的有色金属的时候,物体表面不太明亮。
+ + +标准着色器通过将反射率的一部分加到镜面高光颜色进行补偿(高亮)。它使用_UnityStandardBRDF.cginc_的_SmoothnessToRoughness函数_来确定基于平滑度的粗糙度值,将其缩小一半,并使用它来缩放镜面高光颜色。
+ +float roughness = SmoothnessToRoughness(GetSmoothness(i)) *0.5;
+surfaceData.Albedo += surfaceData.SpecularColor *roughness;
+
+return UnityMetaFragment(surfaceData);
+
SmoothnessToRoughness计算了什么东西?
+ +//转换:减去平滑度值,然后平方。 从平滑度到粗糙度的平方映射最终会产生比仅仅做线性转换更好的结果。
+// Smoothness is the user facing name
+// it should be perceptualSmoothness
+// but we don't want the user to have to deal with this name
+half SmoothnessToRoughness(half smoothness) {
+ return(1 - smoothness) *(1 - smoothness);
+}
+
光照贴图程序只使用物体的顶点数据,不考虑物体的法线贴图。光照贴图的分辨率太低,无法捕获由典型法线贴图提供的细节。这意味着静态光照将是平坦的。当使用具有法线贴图的材质的时候,这变得非常明显。
+ + +当从实时光照切换到烘焙光时,法线贴图的影响几乎完全消失。这是因为它要求环境反射才能看到它们。
+ +通过将“DirectionalMode”改回“Directional”,可以让法线贴图与烘焙光照一起工作。
+ + +当使用方向光照贴图的时候,Unity将创建两个贴图。第一张贴图包含通常的光照信息,称为强度图。 第二张贴图被称为方向图。 它包含大部分烘烤光来自的方向。
+ + +当方向图可用的时候,用它来对烘焙光进行简单的漫反射阴影计算。这使得它可用于法线贴图之上。注意,只有一个光方向是已知的,所以阴影将是一个近似。至少有一个主方向光照的时候,结果就会很好。
+ +当方向光照贴图可用的时候,Unity将使用_LIGHTMAP_ON_和_DIRLIGHTMAP_COMBINED_关键字查找着色器变体。我们可以在forward-base-pass通道中使用#pragma multi_compile_fwdbase,而不是为手动添加多编译指令。它会负责解决所有的光照贴图关键字,以及VERTEXLIGHT_ON关键字。
-{% include img-picture.html - url1="posts/2018/month1/catRender16/1692664-20200601224607620-2057696906.png" - des="半透明的屋顶,效果不正确" width="250" -%} +//#pragma multi_compile _ SHADOWS_SCREEN
+//#pragma multi_compile _ LIGHTMAP_ON VERTEXLIGHT_ON
-**光照贴图**仍然把屋顶看成是实心物体,这是不正确的。它**使用材质的渲染类型来确定如何处理表面**,这应该告诉光照贴图我们的对象是半透明的。事实上,它确实知道屋顶是半透明的,它只是把它看作是完全不透明的而已。这是因为**它**采用Unity的命名约定**_Color材质属性的alpha组件以及主纹理来设置不透明度**。
+#pragma multi_compile_fwdbase
+#pragma multi_compile_fog
+
我们可以为deferred-pass必须使用#pragma multi_compile_prepassfinal指令。 它解决了光照贴图和高动态光照渲染的关键字。
-```hlsl -Properties -{ - // _Tint ("Tint", Color) = (1, 1, 1, 1) - _Color ("Tint", Color) = (1, 1, 1, 1) -} -``` +//#pragma multi_compile _ UNITY_HDR_ON
+//#pragma multi_compile _ LIGHTMAP_ON
+#pragma multi_compile_prepassfinal
+
prepassfinal是什么东西?
+Unity 4使用了一种与以后的版本不同的延迟渲染管线。 在Unity 5中,它被称为传统延迟光照。 这种方法有更多的渲染通道。Prepass决定是当时的术语。不需要引入新的指令,#pragma multi_compile_prepassfinal也用于当前的延迟渲染通道。
在CreateIndirectLight函数中,在检索烘焙光源本身后,需要直接获得烘焙光的方向。方向贴图可以通过unity_LightmapInd获得。
-### 镂空部分的阴影-Cutout Shadow +#if defined(LIGHTMAP_ON)
+ indirectLight.diffuse = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV));
+ #if defined(DIRLIGHTMAP_COMBINED)
+ float4 lightmapDirection = UNITY_SAMPLE_TEX2D
+ (
+ unity_LightmapInd, i.lightmapUV
+ );
+ #endif
+#else
+ indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
+#endif
+
但是,这将导致编译错误。这是因为一个纹理变量实际上由两部分组成。 有纹理资源,还有采样器状态。采样器状态决定纹理的采样方式,包括滤波器和截取模式。 通常,每个纹理都定义了这两个部分,但这并不是所有平台都需要的。 也可以将这两个部分分开,这允许我们为多个纹理定义单个采样器状态。
-{% include img-picture.html - url1="posts/2018/month1/catRender16/1692664-20200601224609483-570537720.png" - des="镂空的屋顶,效果不正确" width="250" -%} +因为强度和方向贴图总是以相同的方式进行采样,所以在可能的情况下,Unity使用单个采样器状态。 这就是为什么我们在采样强度贴图的时候必须使用UNITY_SAMPLE_TEX2D宏。方向贴图已经定义,没有采样器。 要对其进行采样,我们必须使用UNITY_SAMPLE_TEX2D_SAMPLER宏来明确地告诉它要使用哪个采样器。
-解决方案是再次**采用Unity的命名约定_Cutoff材质属性**。所以替换shader、cg文件、GUI拓展。 +float4 lightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER
+(
+ unity_LightmapInd, unity_Lightmap, i.lightmapUV
+);
+
要使用方向:1、解码 2、对法向量执行点积,找到漫反射因子并将其应用于颜色。
-**渲染光照贴图正确的表面反照率和自发光**。 +但是方向贴图并没有包含单位长度的方向,而是比单位长度的方向会大一些。 幸运的是可以使用UnityCG的DecodeDirectionLightmap函数来解码方向数据。
-{% include img-picture.html - url1="posts/2018/month1/catRender16/1692664-20200601224612151-595484578.png" - des="绿色的地板,效果不正确" width="250" -%} +float4 lightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER
+(
+ unity_LightmapInd, unity_Lightmap, i.lightmapUV
+);
-要采样物体的表面颜色,光照贴图程序会将它的光照模式设置为Meta来寻找一个着色器渲染通道。这个渲染通道仅由光照贴图程序使用,不使用剔除。所以让我们在我们的着色器上添加一个渲染通道。
+indirectLight.diffuse = DecodeDirectionalLightmap
+(
+ indirectLight.diffuse, lightmapDirection, i.normal
+);
-```hlsl
-Pass {
- Tags {
- "LightMode" = "Meta"
- }
- Cull Off
+
DecodeDirectionLightmap内部做了什么?
- ENDCG -} -``` +DecodeDirectionLightmap实际上并不计算正确的漫射照明因子。 相反,它使用的是半Lambert。 这种方法可以有效地将光照射在表面周围,照亮阴影的区域会更多。这么做是有必要的,这是因为烘烤的光照不是来自于单个方向.
inline half3 DecodeDirectionalLightmap (
+ half3 color, fixed4 dirTex, half3 normalWorld
+) {
+ // In directional (non-specular) mode Enlighten bakes dominant light
+ // direction in a way, that using it for half Lambert and then dividing
+ // by a "rebalancing coefficient" gives a result close to plain diffuse
+ // response lightmaps, but normalmapped.
-```hlsl
-#if !defined(MY_LIGHTMAPPING_INCLUDED)
-#define MY_LIGHTMAPPING_INCLUDED
+ // Note that dir is not unit length on purpose. Its length is
+ // "directionality", like for the directional specular lightmaps.
-#include "UnityPBSLighting.cginc"
+ half halfLambert = dot(normalWorld, dirTex.xyz - 0.5) + 0.5;
-float4 _Color;
-sampler2D _MainTex, _DetailTex, _DetailMask;
-float4 _MainTex_ST, _DetailTex_ST;
+ return color *halfLambert / max(1e-4h, dirTex.w);
+}
+
代码的注释中提到镜面高光。 这些是支持镜面高光的光照贴图,但需要更多的纹理,使用起来也更昂贵,并且在大多数情况下没有产生良好的效果。自Unity 5.6起,它们已被删除了。
-sampler2D _EmissionMap; -float3 _Emission; +光照贴图仅适用于静态对象,而不适用于动态对象。 因此,动态对象不适合带有烘烤光照的场景。当没有实时光源的时候,这是非常明显的。
-struct Interpolators { - float4 pos : SV_POSITION; - float4 uv : TEXCOORD0; -}; + +为了更好地混合静态和动态对象,我们必须以某种方式将烘焙的光照应用于动态对象。为了解决这个问题,Unity有光照探针。 光照探针是对空间中的一个点包含该位置的光照信息。 它是用球面谐波来存储这些信息而不是用纹理。 如果可用的话,这些光照探针将用于动态对象,而不是全局环境数据。所以我们要做的就是创建一些探针,等到烘焙的时候,我们的着色器就会自动使用它们。
-float3 GetAlbedo (Interpolators i) { - … -} +通过GameObject / Light /Light Probe Group将一组光探测器添加到场景中。 这将创建一个新的游戏对象,在立方体的形状中共有八个光探测器。 它们将在渲染动态对象的时候立即使用。
-float GetSmoothness (Interpolators i) { - … -} + +通过检视器,可以在启用“编辑探针”模式后编辑光探测器组。
-#endif -``` - -GetEmission函数去除_FORWARD_BASE_PASS_和_DEFERRED_PASS_限制。 - -```hlsl -float3 GetEmission (Interpolators i) { -// #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS) - #if defined(_EMISSION_MAP) - return tex2D(_EmissionMap, i.uv.xy) *_Emission; - #else - return _Emission; - #endif -// #else -// return 0; -// #endif -} -``` - -这些函数只有在定义了适当的关键字时才会起作用,因此可以在渲染通道中为其添加着色功能。 - -```hlsl -#pragma vertex MyLightmappingVertexProgram -#pragma fragment MyLightmappingFragmentProgram + +光照探针组将其包围的体积分成四个区域。四个探测器定义了四面体的角。 这些探测器被进行插值以确定用于动态物体的最终球谐函数,这取决于其在四面体内的位置。这意味着动态对象被视为一个单一的点,因此这种方法只对相当小的对象有效。在编辑探测器的时候,会自动生成四面体。 你不需要知道他们的配置,但它们的可视化信息可以帮助你查看探测器的相对位置。放置光照探针需要你去调整他们的位置,直到你得到一个你可以接受的结果,就像光照贴图的设置一样。首先封装将要包含动态对象的区域。
-```hlsl -//#pragma multi_compile _ SHADOWS_SCREEN -//#pragma multi_compile _ LIGHTMAP_ON VERTEXLIGHT_ON + +然后根据光照条件如何变化来添加更多的探针。你不必将它们放置在静态几何中。 也不要把它们放在不透明的单面几何体错误的那一面。
-但是,这将导致编译错误。这是因为一个纹理变量实际上由两部分组成。 有纹理资源,还有采样器状态。采样器状态决定纹理的采样方式,包括滤波器和截取模式。 通常,每个纹理都定义了这两个部分,但这并不是所有平台都需要的。 也可以将这两个部分分开,这允许我们为多个纹理定义单个采样器状态。 + +继续添加和移动探测器,直到你在所有区域都有了合理的光照条件,并且在它们之间发生的转换是可以接受的。
-```hlsl -float4 lightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER -( - unity_LightmapInd, unity_Lightmap, i.lightmapUV -); -``` + +可以通过移动动态对象来测试探针。当选择一个动态对象的时候,也会显示当前正在发挥作用的探针。探针将显示其光照,而不仅仅是黄色球体。你还可以看到用于动态对象的内插数据。
-要使用方向:1、解码 2、对法向量执行点积,找到漫反射因子并将其应用于颜色。 + +通过不同的光照探头,物体的明暗变化明显。
]]>在G-Buffers填充完毕后,然后渲染光。本篇先介绍Unity是如何渲染光,以及实现自己的渲染光的Shader。在Edit / Project Settings / Graphics 去掉默认的Shader。
-``` +每个deferred光都是在一个独立的Pass修改屏幕图像(后处理Image)完成渲染。创建一个Shader然后指定到Built-In shader settings
-_DecodeDirectionLightmap内部做了什么?_ + +修改之后,编辑器大量报错.
- // Note that dir is not unit length on purpose. Its length is - // "directionality", like for the directional specular lightmaps. + +先简单复制第一个Pass解决错误,结果是屏幕内除了天空盒外所有物体被渲染成黑色了。这是因为使用了stencil-buffer。
- return color *halfLambert / max(1e-4h, dirTex.w); -} -``` +报错的原因:为什么需要第二个Pass?
+当在LDR(HDR禁用)模式,天空变黑了。这是因为转换过程中没有正确使用stencil-buffer模板掩码。在第二个Pass中配置:应该只渲染不属于背景的片段,可通过_StencilNonBackground提供适当的模板值。
-光照贴图仅适用于静态对象,而不适用于动态对象。 因此,动态对象不适合带有烘烤光照的场景。当没有实时光源的时候,这是非常明显的。 +Pass
+{
+ Stencil
+ {
+ Ref[_StencilNonBackground]
+ ReadMask[_StencilNonBackground]
+ CompBack Equal
+ CompFront Equal
+ }
+}
+
在第二个Pass的light-buffer转换光照数据,方法就似Fog shader:用输入源的Image UV坐标采样buffer来绘制一个覆盖全屏的quad
-为了更好地混合静态和动态对象,我们必须以某种方式将烘焙的光照应用于动态对象。为了解决这个问题,Unity有光照探针。 光照探针是对空间中的一个点包含该位置的光照信息。 它是用球面谐波来存储这些信息而不是用纹理。 如果可用的话,这些光照探针将用于动态对象,而不是全局环境数据。所以我们要做的就是创建一些探针,等到烘焙的时候,我们的着色器就会自动使用它们。 +struct VertexData {
+ float4 vertex : POSITION;
+ float2 uv : TEXCOORD0;
+};
+struct Interpolators {
+ float4 pos : SV_POSITION;
+ float2 uv : TEXCOORD0;
+};
+Interpolators VertexProgram (VertexData v) {
+ Interpolators i;
+ i.pos = UnityObjectToClipPos(v.vertex);
+ i.uv = v.uv;
+ return i;
+}
+
该light buffer通过名为_LightBuffer变量提供给Shader
-### 创建光照探针组 +sampler2D _LightBuffer;
+//...
+float4 FragmentProgram (Interpolators i) : SV_Target {
+ return tex2D(_LightBuffer, i.uv);
+}
+
LDR颜色使用指数编码:\(2^{-C}\),使用对数解码\(-log2^C\)
-通过GameObject / Light /Light Probe Group将一组光探测器添加到场景中。 这将创建一个新的游戏对象,在立方体的形状中共有八个光探测器。 它们将在渲染动态对象的时候立即使用。 +return -log2(tex2D(_LightBuffer, i.uv));
+
新增一个cginc文件,引入第一个pass。要把渲染的光照增加到图像上,必须确保不能擦除已渲染的图像,因此改变混合模式要完全合并源颜色和目标颜色。
-通过检视器,可以在启用“编辑探针”模式后编辑光探测器组。 +Blend One One
+
也需要所有可能的光照配置shader variants变体,该编译指令:multi_compile_lightpass会创建所有包含的变体。然后再增加一个HDR_ON的指令。
-### 放置光照探针 +#pragma multi_compile_lightpass
+#pragma multi_compile _ UNITY_HDR_ON
+
需要用UV坐标从G-buffers采样,不幸的是,该light pass通道unity不支持提供该坐标。解决办法:从clip-space传递过来,使用ComputeScreenPos函数计算,返回一个float4的齐次坐标。
+ +v2f VertexProgram(appdata v)
+{
+ v2f o;
+ o.pos = UnityObjectToClipPos(v.vertex);
+ o.uv = ComputeScreenPos(o.pos);
+ return o;
+}
+
然后在fragment就能计算最终的2D坐标。必须在fragment计算。见翻译7
-然后根据光照条件如何变化来添加更多的探针。你不必将它们放置在静态几何中。 也不要把它们放在不透明的单面几何体错误的那一面。 +fixed4 FragmentProgram(v2f i) : SV_Target
+{
+ float2 uv = i.uv.xy / i.uv.w;
+ return 0;
+}
+
与上篇deferred fog中相似,需要计算从相机到片元的距离:从相机原点发射射线通过片元(给定方向)到达far-plane,然后再用fragment深度缩放射线。用该方法重建片元的世界坐标。
-{% include img-picture.html - url1="posts/2018/month1/catRender16/1692664-20200601224624739-928043347.png" - des="调整探测器的位置" width="250" -%} +struct VertexData {
+ float4 vertex : POSITION;
+ float3 normal : NORMAL;
+};
-{% include img-picture.html
- url1="posts/2018/month1/catRender16/1692664-20200601224702732-797999005.gif"
- des="移动动态对象" width="250"
-%}
+struct Interpolators {
+ float4 pos : SV_POSITION;
+ float4 uv : TEXCOORD0;
+ float3 ray : TEXCOORD1;
+};
-通过不同的光照探头,物体的明暗变化明显。]]>catlikecoding Unity Deferred Lights-延迟光照(翻译十五) 2018-01-17T20:00:00+08:00 2018-01-17T20:00:00+08:00 https://www.damonc.top/Unity_Deferred_Lights Interpolators VertexProgram (VertexData v) {
+ Interpolators i;
+ i.pos = UnityObjectToClipPos(v.vertex);
+ i.uv = ComputeScreenPos(i.pos);
+ i.ray = v.normal;
+ return i;
+}
+
//Unity提供的声明函数,等于 sampler2D _CameraDepthTexture; 定义在UnityCG
+UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
-在G-Buffers填充完毕后,然后渲染光。本篇先介绍Unity是如何渲染光,以及实现自己的渲染光的Shader。在Edit / Project Settings / Graphics 去掉默认的Shader。
+float4 FragmentProgram (Interpolators i) : SV_Target {
+ float2 uv = i.uv.xy / i.uv.w;
+ float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
+ depth = Linear01Depth(depth);
+ return 0;
+}
+
depth = Linear01Depth(depth);
+float3 rayToFarPlane = i.ray * _ProjectionParams.z / i.ray.z;
+
float3 rayToFarPlane = i.ray * _ProjectionParams.z / i.ray.z;
+float3 viewPos = rayToFarPlane * depth;
+
float3 viewPos = rayToFarPlane * depth;
+float3 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1)).xyz;
+
获取World Pos后。通过访问G-buffer检索properties,该buffer可从内置的_CamearGBufferTexture变量获取
-### Avoiding the Sky +sampler2D _CameraGBufferTexture0;
+sampler2D _CameraGBufferTexture1;
+sampler2D _CameraGBufferTexture2;
+
在上一篇Defferred Shading中也手动计算过G-buffer,这次直接读取_CameraGBufferTexture现成的albedo、specular、smoothness、normal
-```hlsl -Pass -{ - Stencil - { - Ref[_StencilNonBackground] - ReadMask[_StencilNonBackground] - CompBack Equal - CompFront Equal - } -} -``` +float3 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1)).xyz;
+float3 albedo = tex2D(_CameraGBufferTexture0, uv).rgb;
+float3 specularTint = tex2D(_CameraGBufferTexture1, uv).rgb;//合并
+float3 smoothness = tex2D(_CameraGBufferTexture1, uv).a;//合并
+float3 normal = tex2D(_CameraGBufferTexture2, uv).rgb * 2 - 1;
+
引入BRDF函数,定义在UnityPBSLighting.cginc中
-在第二个Pass的light-buffer转换光照数据,方法就似Fog shader:用输入源的Image UV坐标采样buffer来绘制一个覆盖全屏的quad +首先计算视野方向
-```hlsl -struct VertexData { - float4 vertex : POSITION; - float2 uv : TEXCOORD0; -}; -struct Interpolators { - float4 pos : SV_POSITION; - float2 uv : TEXCOORD0; -}; -Interpolators VertexProgram (VertexData v) { - Interpolators i; - i.pos = UnityObjectToClipPos(v.vertex); - i.uv = v.uv; - return i; -} -``` +float3 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1)).xyz;
+float3 viewDir = normalize(_WorldSpaceCameraPos - worldPos);
+
其次是表面反射,这可从specular颜色获取,使用SpecularStrength函数提取。
-该light buffer通过名为_LightBuffer变量提供给Shader +//...
+float oneMinusReflectivity = 1 - SpecularStrength(specularTint);
+
然后传递光照数据,初始化直接光和间接光
+ +float oneMinusReflectivity = 1 - SpecularStrength(specularTint);
+//...
+UnityLight light;
+light.color = 0;
+light.dir = 0;
-LDR颜色使用指数编码:$$2^{-C}$$,使用对数解码$$-log2^C$$
+UnityIndirect indirectLight;
+indirectLight.diffuse = 0;
+indirectLight.specular = 0;
+
最后计算最终的颜色
-```hlsl -return -log2(tex2D(_LightBuffer, i.uv)); -``` +indirectLight.specular = 0;
+float4 color = UNITY_BRDF_PBS
+(
+ albedo, specularTint, oneMinusReflectivity, smoothness,
+ normal, viewDir, light, indirectLight
+);
+return color;
+
因为间接光呈现的是黑色的,在这里不适用。但是直接光必须被配置成与当前渲染的光相匹配。对于方向光,需要它的颜色和方向。这两个变量可以通过_LightColor和_LightDir变量获得。
-```hlsl -Blend One One -``` +float4 _LightColor, _LightDir;
-也需要所有可能的光照配置shader variants变体,该编译指令:multi_compile_lightpass会创建所有包含的变体。然后再增加一个HDR_ON的指令。
+UnityLight CreateLight () {
+ UnityLight light;
+ light.dir = _LightDir;
+ light.color = _LightColor.rgb;
+ return light;
+}
-```hlsl
-#pragma multi_compile_lightpass
-#pragma multi_compile _ UNITY_HDR_ON
-```
+ UnityLight light = CreateLight();
+// light.color = 0;
+// light.dir = 0;
+
计算得到最终的光照,但光的方向错误了。原因:_LightDir是光到表面的方向。在CreateLight计算中需要表面到光的方向
-```hlsl -fixed4 FragmentProgram(v2f i) : SV_Target -{ - float2 uv = i.uv.xy / i.uv.w; - return 0; -} -``` +light.dir = -_LightDir;
+
在自己的cginc文件中,我们依靠AutoLight中的宏来确定由阴影引起的光衰减。 不幸的是,该文件在编写时并没有考虑到延迟的光线。 现在将自己进行阴影采样,可通过_ShadowMapTexture变量访问阴影贴图。
-```hlsl -struct VertexData { - float4 vertex : POSITION; - float3 normal : NORMAL; -}; +sampler2D _ShadowMapTexture;
+
但是,我们不能随意声明此变量。 它已经在UnityShadowLibrary中为点和聚光灯阴影定义了它。 因此,我们不应该自己定义它,除非使用方向光阴影。
-Interpolators VertexProgram (VertexData v) { - Interpolators i; - i.pos = UnityObjectToClipPos(v.vertex); - i.uv = ComputeScreenPos(i.pos); - i.ray = v.normal; - return i; -} -``` - -2. 其次。在fragment函数通过采样_CameraDepthTexture纹理和线性化计算可以得到depth值,类似于deferred fog计算 +#if defined (SHADOWS_SCREEN)
+ sampler2D _ShadowMapTexture;
+#endif
+
要应用方向光阴影,需要采样阴影纹理并使用它来减弱光色即可。 在CreateLight中计算就需要把UV坐标参数。
-float4 FragmentProgram (Interpolators i) : SV_Target { - float2 uv = i.uv.xy / i.uv.w; - float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv); - depth = Linear01Depth(depth); - return 0; -} -``` +UnityLight CreateLight (float2 uv) {
+ UnityLight light;
+ light.dir = -_LightDir;
+ float shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
+ light.color = _LightColor.rgb * shadowAttenuation;
+ return light;
+}
-3. 然后。与deferred fog最大的不同:fog shader需要射线到达far plane;而本shader的射线只能到达near plane。所以必须要缩放射线以便它能达到far-plane:缩放射线使Z坐标变为1,并与远平面距离相乘。
-
-```hlsl
-depth = Linear01Depth(depth);
-float3 rayToFarPlane = i.ray * _ProjectionParams.z / i.ray.z;
-```
-
-4. 再接着。按深度值缩放射线一次得到一个坐标。该射线被定义在视图空间,它是camera的本地空间。因此,射线也以片段在视图空间中的坐标结束。
-
-```hlsl
-float3 rayToFarPlane = i.ray * _ProjectionParams.z / i.ray.z;
-float3 viewPos = rayToFarPlane * depth;
-```
-
-5. 最后。再使用unity_CameraToWorld内置矩阵从view视图空间转换到world世界坐标,该矩阵定义在ShaderVariables.cginc
+UnityLight light = CreateLight(uv);
+
当然,这仅在定向光启用了阴影时才有效。 如果不是,则阴影衰减始终为1。
+ +float shadowAttenuation = 1;
+#if defined(SHADOWS_SCREEN)
+ shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
+#endif
+light.color = _LightColor.rgb * shadowAttenuation;
+
阴影贴图应该是有限的,它覆盖的面积越大,阴影的分辨率越低。 Unity提供了绘制阴影的最大距离,此距离可以通过_Edit / Project Settings / Quality_进行调整。
+ + +当阴影几乎快达到了该限定距离就会淡出,Unity内置的shader是这样设定并计算。由于我将手动采样该阴影纹理,当到达纹理的边缘时阴影会被截取,结果是阴影虽然消失了,但有被急剧切割的生硬画面。
+ + +要渐隐阴影,首先要知道的是阴影完全消失的距离。该距离又依赖于阴影投射方向。在Stable Fit模式下,以map的中心点呈球面形开始渐隐消失阴影;在Close Fit模式它是依赖于视野深度。
+ +UnityComputeShadowFadeDistance函数能计算出正确距离,它需要两个参数:world pos 和 view depth;然后返回距离A。 注意:该距离A是从阴影纹理的中心点位置或者未更改的视野深度开始计算的。
+ +UnityLight CreateLight (float2 uv, float3 worldPos, float viewZ) {
+ UnityLight light;
+ light.dir = -_LightDir;
+ float shadowAttenuation = 1;
+ #if defined(SHADOWS_SCREEN)
+ shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
+ float shadowFadeDistance = UnityComputeShadowFadeDistance(worldPos, viewZ);
+ #endif
+ light.color = _LightColor.rgb * shadowAttenuation;
+ return light;
+}
+
阴影应该是快要接近渐隐距离时开始消失,一旦到达就完全消失。UnityComputeShadowFade函数计算合适的消失因子。
+ +float shadowFadeDistance = UnityComputeShadowFadeDistance(worldPos, viewZ);
+float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
+
UnityComputeShadowFade 定义在UnityShadowLibrary.cginc,见下:
-获取World Pos后。通过访问G-buffer检索properties,该buffer可从内置的_CamearGBufferTexture变量获取 +float UnityComputeShadowFadeDistance (float3 wpos, float z) {
+ float sphereDist = distance(wpos, unity_ShadowFadeCenterAndType.xyz);
+ return lerp(z, sphereDist, unity_ShadowFadeCenterAndType.w);
+}
-```hlsl
-sampler2D _CameraGBufferTexture0;
-sampler2D _CameraGBufferTexture1;
-sampler2D _CameraGBufferTexture2;
-```
+half UnityComputeShadowFade(float fadeDist) {
+ return saturate(fadeDist * _LightShadowData.z + _LightShadowData.w);
+}
+
阴影渐隐值范围是[0, 1],该值决定了阴影要消失多少。实际的消失值可以加到阴影衰减之上并限定在[0, 1]之内
-```hlsl -float3 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1)).xyz; -float3 albedo = tex2D(_CameraGBufferTexture0, uv).rgb; -float3 specularTint = tex2D(_CameraGBufferTexture1, uv).rgb;//合并 -float3 smoothness = tex2D(_CameraGBufferTexture1, uv).a;//合并 -float3 normal = tex2D(_CameraGBufferTexture2, uv).rgb * 2 - 1; -``` +float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
+shadowAttenuation = saturate(shadowAttenuation + shadowFade);
+
最后,提供世界坐标和视图深度在片元程序中创建光照。视图深度是片元在视图空间中的位置的Z分量。
+ +UnityLight light = CreateLight(uv, worldPos, viewPos.z);
+
支持Cookies纹理,使用变量__LightTexture0_访问;同时还要从world-space转换到light-space,最后采样。转换矩阵使用_unity_WorldToLight_矩阵变量
+ +sampler2D _LightTexture0;
+float4x4 unity_WorldToLight;
+
在_CreateLight_,使用上述矩阵变量转换world-space到light-space;然后使用转换后的坐标采样cookie纹理。cookie也要衰减,需要单独定义并使用。
+ +light.dir = -_LightDir;
+float attenuation = 1;
+float shadowAttenuation = 1;
+#if defined(DIRECTIONAL_COOKIE)
+ float2 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1)).xy;
+ attenuation *= tex2D(_LightTexture0, uvCookie).w;
+#endif
+ //...
+light.color = _LightColor.rgb * (attenuation * shadowAttenuation);
+
整体结果似乎可以,但是观察边缘似乎有硬边
+ + +相邻片元的cookie坐标的巨大差异就会导致该问题出现。在这种情况下,GPU选择的mipmap级别对于最近的表面是low level。解决办法之一就是:在采样mip映射时应用偏移。大v的总结
+ +attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie, 0, -8)).w;
+
上述只支持HDR,现在来支持LDR。步骤如下:
+ +首先,编码后的LDR颜色要乘如light-buffer,而不是加法。这可以用:Blend DstColor Zero实现。注意只用该Blend mode会引起HDR的错误。所以需要灵活配置:Blend [_SrcBlend] [_DstBlend]
+ +然后,使用\(2^{-c}\)函数解码
+ +float4 color = UNITY_BRDF_PBS(
+ albedo, specularTint, oneMinusReflectivity, smoothness,
+ normal, viewDir, light, indirectLight
+);
+ #if !defined(UNITY_HDR_ON)
+ color = exp2(-color);
+ #endif
+return color;
+
因为方向光会影响到场景内所有物体,所以被画成全屏quad。相比之下,聚光灯只会影响位于圆锥体内的部分物体。通常不需要计算整个图像的聚光灯光照,将绘制一个与聚光灯的影响范围相匹配的金字塔体。
+ +禁用方向灯,改用聚光灯。因为着色器只对方向光正确工作,那么现在的结果会出现错误。但是它仍可以让你看到金字塔的哪些部分被渲染了。 +
+根据上图,金字塔是作为一个普通的3D对象呈现的。它的背面被剔除,所以我们可以看到金字塔的正面。只有当它前面没有东西的时候,它才会被画出来。除此之外,还添加了一个pass,用于设置模板缓冲区,以将绘图限制为位于金字塔卷内的片段。您可以通过frame-debugger来验证。
+ + +这意味着我们的着色器的culling和z-test设置被否弃了。 因此将其从着色器中删除。
+ +Blend [_SrcBlend] [_DstBlend]
+//Cull Off
+//ZTest Always
+ZWrite Off
+
当聚光灯的体积距离相机足够远时,此方法适用。 但是,当聚光灯离摄像机太近时,它会失败。 发生这种情况时,相机可能会进入了该体积内。 甚至有可能将近平面的一部分置于其内部,而将其余部分置于其外部,与近平面相交了。 在这些情况下,模板缓冲区不能用于限制渲染。
+ +仍然渲染光照的技巧是绘制金字塔的内表面,而不是金字塔的外表面。 这是通过渲染其背面而不是其正面来完成的。 而且,仅当这些表面最终位于已渲染的表面之后时才渲染它们。 这种方法还涵盖了聚光灯体积内的所有片段。 但这最终导致渲染了太多的碎片,因为通常金字塔的通常隐藏部分也将被渲染。 因此,仅在必要时执行。
+ + +目前,CreateLight只能用于方向光。让我们确保特定于方向灯的代码只在适当的时候使用。
+ +UnityLight CreateLight (float2 uv, float3 worldPos, float viewZ) {
+ UnityLight light;
+ //light.dir = -_LightDir;
+ float attenuation = 1;
+ float shadowAttenuation = 1;
+
+ #if defined(DIRECTIONAL) || defined(DIRECTIONAL_COOKIE)
+ light.dir = -_LightDir;
+
+ #if defined(DIRECTIONAL_COOKIE)
+ float2 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1)).xy;
+ attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie, 0, -8)).w;
+ #endif
+
+ #if defined(SHADOWS_SCREEN)
+ shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
+ float shadowFadeDistance = UnityComputeShadowFadeDistance(worldPos, viewZ);
+ float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
+ shadowAttenuation = saturate(shadowAttenuation + shadowFade);
+ #endif
+ #else
+ light.dir = 1;
+ #endif
+
+ light.color = _LightColor.rgb * (attenuation * shadowAttenuation);
+ return light;
+}
+
尽管阴影衰落基于方向阴影贴图,但是其他类型的阴影也应该会被渐隐。 这样可以确保所有阴影都以相同的方式渐隐,而不仅仅是某些阴影。 因此,只要有阴影,阴影淡入淡出代码便适用于所有灯光。 因此,让我们将该代码移到特定于光源的块之外。
+ +我们可以使用布尔值来控制是否使用阴影淡出代码。由于布尔值是一个常数值,如果它仍然为假,代码将被删除。
+ +UnityLight CreateLight (float2 uv, float3 worldPos, float viewZ) {
+ UnityLight light;
+ float attenuation = 1;
+ float shadowAttenuation = 1;
+ bool shadowed = false;
+ #if defined(DIRECTIONAL) || defined(DIRECTIONAL_COOKIE)
+ //省略代码
+ #if defined(SHADOWS_SCREEN)
+ shadowed = true;
+ shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
+ // float shadowFadeDistance = UnityComputeShadowFadeDistance(worldPos, viewZ);
+ // float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
+ // shadowAttenuation = saturate(shadowAttenuation + shadowFade);
+ #endif
+ #else
+ light.dir = 1;
+ #endif
+
+ if (shadowed) {
+ float shadowFadeDistance = UnityComputeShadowFadeDistance(worldPos, viewZ);
+ float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
+ shadowAttenuation = saturate(shadowAttenuation + shadowFade);
+ }
+
+ light.color = _LightColor.rgb * (attenuation * shadowAttenuation);
+ return light;
+}
+
非方向灯光都有一个position变量。它通过内置的_LightPos提供。
+ +float4 _LightColor, _LightDir, _LightPos;
+
现在可以确定聚光灯的光向量得出光方向。
+ +#else
+ float3 lightVec = _LightPos.xyz - worldPos;
+ light.dir = normalize(lightVec);
+#endif
+
结果为黑色,似乎光线方向不正确。 发生这种情况是因为聚光灯的世界位置计算不正确。 当我们在场景中的某个地方渲染金字塔时,不像方向光那样渲染全屏quad将光线存储在normal通道中。 而必须是经由Vertex-Program从顶点的位置发射射线,通过将顶点的pos转换到view-space完成计算,为此,我们可以使用UnityObjectToViewPos函数。
+ +i.ray = UnityObjectToViewPos(v.vertex);
+
然而,这会产生方向错误的光线。我们要消去它们的X和Y坐标。
+ +i.ray = UnityObjectToViewPos(v.vertex) * float3(-1, -1, 1);
+
再次看看UnityObjectToViewPos内部实现
+ +inline float3 UnityObjectToViewPos (in float3 pos) {
+ return mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, float4(pos, 1.0))).xyz;
+}
+
当渲染方向光时,应该只使用顶点法线。当渲染非方向灯以外的光几何时,需要把顶点pos转到view-space计算。Unity通过__LightAsQuad_变量告诉我们正在处理哪种情况。
+ +如果__LightAsQuad_被设为1,则处理的是方向光quad并且可以使用法线。否则,我们必须使用UnityObjectToViewPos。插值好过if ==> from + (to – from) * t, t为1直接使用法线,为0直接计算到view-space
+ +i.ray = lerp
+(
+ UnityObjectToViewPos(v.vertex) * float3(-1, -1, 1),
+ v.normal,
+ _LightAsQuad
+);
+
聚光灯的锥形衰减是通过cookie纹理创建的,无论是默认的圆形还是定制的cookie。我们可以从复制定向光的cookie代码,仿照着写。也是存储在_LightTexture0
+ +float3 lightVec = _LightPos.xyz - worldPos;
+light.dir = normalize(lightVec);
+float2 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1)).xy;
+attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie, 0, -8)).w;
+
但是,聚光灯Cookie越远离灯光位置,它就会变得越大。 这是由于通过透视变换造成的。 因此,矩阵乘法会产生4D齐次坐标。 为了得到规则的2D坐标,我们必须将X和Y除以W。
+ +float4 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1));
+uvCookie.xy /= uvCookie.w;
+attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie.xy, 0, -8)).w;
+
上图实际上产生了两个光锥,一个向前一个向后。 后向圆锥通常在渲染区域之外结束,但这并不能保证。我们只需要前向锥,它对应于负的W坐标。
+ +attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie.xy, 0, -8)).w;
+attenuation *= uvCookie.w < 0;
+
聚光灯发出的光也会根据距离衰减。此衰减存储在查找纹理中,可通过__LightTextureB0_使用该纹理。
+ +sampler2D _LightTexture0, _LightTextureB0;
+
纹理被设计成必须使用光的距离的平方,并按光的范围进行缩放,作为UV进行采样。范围存储在_LightPos的第四个分量中。采样得到的纹理应该使用哪个通道在不同的平台,由_UNITY_ATTEN_CHANNEL_宏定义。
-引入BRDF函数,定义在UnityPBSLighting.cginc中 +light.dir = normalize(lightVec);
+attenuation *= tex2D
+(
+ _LightTextureB0,
+ (dot(lightVec, lightVec) * _LightPos.w).rr
+).UNITY_ATTEN_CHANNEL;
+float4 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1));
+
当聚光灯有阴影时,定义SHADOWS_DEPTH关键字。
-```hlsl -//... -float oneMinusReflectivity = 1 - SpecularStrength(specularTint); -``` +//在CreateLight中
+float4 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1));
+uvCookie.xy /= uvCookie.w;
+attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie.xy, 0, -8)).w;
-然后传递光照数据,初始化直接光和间接光
+#if defined(SHADOWS_DEPTH)
+ shadowed = true;
+#endif
+
聚光灯和方向灯使用相同的变量来采样阴影贴图。在聚光灯的情况下,可以使用内置UnitySampleShadowmap来处理采样硬阴影或软阴影的细节。参数:阴影空间中的片元位置。unity_WorldToShadow_(4x4)_矩阵中第一个数组可以用来将世界空间转换为阴影空间。
+ +shadowed = true;
+shadowAttenuation = UnitySampleShadowmap(
+ mul(unity_WorldToShadow[0], float4(worldPos, 1))
+);
+
点光源使用与聚光灯相同的光向量、方向和距离衰减。这样他们就可以共享代码。应该只在定义SPOT关键字时使用spotlight代码的其余部分。
-UnityIndirect indirectLight; -indirectLight.diffuse = 0; -indirectLight.specular = 0; -``` +#if defined(DIRECTIONAL) || defined(DIRECTIONAL_COOKIE)
+ //...
+#else
+ float3 lightVec = _LightPos.xyz - worldPos;
+ light.dir = normalize(lightVec);
+
+ attenuation *= tex2D(
+ _LightTextureB0,
+ (dot(lightVec, lightVec) * _LightPos.w).rr
+ ).UNITY_ATTEN_CHANNEL;
+
+ #if defined(SPOT)
+ float4 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1));
+ uvCookie.xy /= uvCookie.w;
+ attenuation *=
+ tex2Dbias(_LightTexture0, float4(uvCookie.xy, 0, -8)).w;
+ attenuation *= uvCookie.w < 0;
+
+ #if defined(SHADOWS_DEPTH)
+ shadowed = true;
+ shadowAttenuation = UnitySampleShadowmap(
+ mul(unity_WorldToShadow[0], float4(worldPos, 1))
+ );
+ #endif
+ #endif
+#endif
+
这已经足够让点光源工作了。它们被渲染成和聚光灯一样的效果,除了渲染范围使用的是球形而不是锥形。
-```hlsl -indirectLight.specular = 0; -float4 color = UNITY_BRDF_PBS -( - albedo, specularTint, oneMinusReflectivity, smoothness, - normal, viewDir, light, indirectLight -); -return color; -``` + +点光源的阴影存储在一个CubeMap。内置UnitySampleShadowmap可采样。参数:光的方向。一个从光到表面的向量。它是光的相反方向。
-```hlsl -float4 _LightColor, _LightDir; +#if defined(SPOT)
+//...
+#else
+ #if defined(SHADOWS_CUBE)
+ shadowed = true;
+ shadowAttenuation = UnitySampleShadowmap(-lightVec);
+ #endif
+#endif
+
Point light cookie也可以通过_LightTexture0获得。需要的是一个cubeMap映射,而不是常规的纹理。
+ +//sampler2D _LightTexture0, _LightTextureB0;
+#if defined(POINT_COOKIE)
+ samplerCUBE _LightTexture0;
+#else
+ sampler2D _LightTexture0;
+#endif
+
+sampler2D _LightTextureB0;
+float4x4 unity_WorldToLight;
+
要对cookie进行采样,请将片段的world-space转换为light-space,并使用光照空间对立方体映射进行采样。
+ +#else
+ #if defined(POINT_COOKIE)
+ float3 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1)).xyz;
+ attenuation *= texCUBEbias(_LightTexture0, float4(uvCookie, -8)).w;
+ #endif
+
+ #if defined(SHADOWS_CUBE)
+ shadowed = true;
+ shadowAttenuation = UnitySampleShadowmap(-lightVec);
+ #endif
+#endif
+
现在,我们可以使用自己的着色器渲染所有动态光源。 尽管我们目前并未对优化进行太多关注,但仍有一项潜在的大型优化值得考虑:最终超出阴影渐隐距离的片元将不会被阴影化。 但是现在仍在采样它们的阴影,这可能很昂贵。 我们可以通过基于阴影衰落因子进行_UNITY_BRANCH_分支来避免这种情况。 它接近1,那么我们可以完全跳过阴影衰减。
+ +if (shadowed) {
+ float shadowFadeDistance = UnityComputeShadowFadeDistance(worldPos, viewZ);
+ float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
+ shadowAttenuation = saturate(shadowAttenuation + shadowFade);
+ UNITY_BRANCH
+ if (shadowFade > 0.99) {
+ shadowAttenuation = 1;
+ }
+}
+
但是,即使用了_UNITY_BRANCH_分支它本身也很昂贵。除了靠近阴影区域的边缘,所有碎片都落在阴影区域的内部或外部。 但这仅在GPU可以利用这一点的情况下才重要。 在这种情况下,使用HLSLSupport.cginc定义UNITY_FAST_COHERENT_DYNAMIC_BRANCHING宏。
+ +#if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING)
+ UNITY_BRANCH
+ if (shadowFade > 0.99) {
+ shadowAttenuation = 1;
+ }
+#endif
+
即使这样,仅当阴影需要多个纹理样本时才值得使用。 对于柔和的聚光灯和点光源阴影,进一步使用用SHADOWS_SOFT关键字指示。 而方向光阴影始终只需要单个纹理,因此它性能很便宜。
+ +#if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT)
+ UNITY_BRANCH
+ if (shadowFade > 0.99) {
+ shadowAttenuation = 1;
+ }
+#endif
+
在14之前,一直假定着光线在真空中传播,在真空中可能是精确的。但是当光线穿过大气或水就不一样了,光线在击中物体表面时会发生被吸收、散射和反射。
+ +一个精确的大气干扰光线渲染将需要及其昂贵的体积测量方法,那是大多数现代GPU负担不起的。相反,勉强采用一些常量雾参数近似模拟。
+ +Unity光照设置包含了场景雾设置选项,默认是不启用。启用后,默认是灰色雾。Unity自带的雾只适用于使用了Forward渲染路径的物体。若激活Deferred path,提示: +
+图2不明显,是因为Fog color灰色雾将散射和反射更多的光线,吸收较少。把Fog Color改为纯黑色试试 +
+雾的浓度是随视距线性增长的,在视距开头正常显示,超过这个距离就只有雾的颜色可见。
+ +线性雾公式: +\(f = { {E-c} \over {E-s} }\)
+ +c 是雾坐标;
+s 是视距起始距离;
+E 是视距终止距离;
+f值 被限定在[0, 1]范围,被用在雾和物体着色之间插值。
最终计算在fragment color着色到物体对象上,雾不会影响到skybox
+ +更接近真实感的雾 +
+指数雾公式: +\(f = {1 \over {2^{cd}}}\) +
+d 是fog的密度因子;
+c 是距离因子。
指数平方雾公式: +\(f = {1 \over {2^{(cd)^2}}}\) +
+增加Fog到自己的shader中,增加Fog需要使用内置关键字:multi_compile_fog_指令。该指令的变体会额外增加:_FOG_LINEAR、FOG_EXP、FOG_EXP2
+ +#pragma multi_compile_fog
+
新增ApplyFog()函数,用于在Fragment计算最终着色:获取当前颜色和插值数据作为参数,返回最终颜色。
+ +计算步骤:
+任何雾公式都是基于视距的,首先计算出视距值备用;
+然后使用UnityCG.cginc宏_UNITY_CALC_FOG_FACTOR_RAW_根据具体雾公式计算出雾因子。
+最后根据雾因子,在fog_color和当前color取插值返回。
float4 ApplyFOG(float4 color, Interpolators i)
+{
+ float viewDistance = length(_WorldSpaceCameraPos - i.worldPos);
+ UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
+ return learp(unity_FogColor, color, unityFogFactor);
+}
+
//宏UNITY_CALC_FOG_FACTOR_RAW
+#if defined(FOG_LINEAR)
+ // factor = (end-z)/(end-start) = z \* (-1/(end-start)) + (end/(end-start))
+ #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = (coord) \* unity_FogParams.z + unity_FogParams.w
+#elif defined(FOG_EXP)
+ // factor = exp(-density\*z)
+ #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.y \* (coord); unityFogFactor = exp2(-unityFogFactor)
+#elif defined(FOG_EXP2)
+ // factor = exp(-(density\*z)^2)
+ #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.x \* (coord); unityFogFactor = exp2(-unityFogFactor\*unityFogFactor)
+#else
+ #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = 0.0
+#endif
+
+//宏UNITY_CALC_FOG_FACTOR
+#define UNITY_CALC_FOG_FACTOR(coord) UNITY_CALC_FOG_FACTOR_RAW(UNITY_Z_0_FAR_FROM_CLIPSPACE(coord))
+
+//unity_FogParams 定义在ShaderVariables
+// x = density / sqrt(ln(2)), useful for Exp2 mode
+// y = density / ln(2), useful for Exp mode
+// z = -1/(end-start), useful for Linear mode
+// w = end/(end-start), useful for Linear mode
+float4 unity_FogParams;
+
注意雾因子必须限定在[0,1]
+ +return learp(unity_FogColor, color, saturate(unityFogFactor));
+
同时雾也不能影响Alpha值
+ +color.rgb = learp(unity_FogColor.rgb, color.rgb, saturate(unityFogFactor));
+
增加深度雾支持。与Standard Shader不同的原因是计算fog坐标方法不同。虽然使用world-space视图距离是有意义的,但标准着色器使用裁剪空间深度值。因此视角不影响雾坐标。此外,在某些情况下,距离是受相机的近裁切面距离的影响,这将把雾推开一点。 +
+基于深度代替距离的优点是:不必计算平方根,计算速度更快,适用于非真实渲染。缺点是:忽略视角,也即相机以原点旋转会影响雾密度,因为旋转时密度会改变。 +
+支持depth-based深度雾 ,必须把clip-pass裁剪空间深度值传递到片元函数。定义一个关键字:FOG_DEPTH.
+ +#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
+ #define FOG_DEPTH 1
+#endif
+
由于需要多存储一个z值,但又不能新增一个独立的变量,就把worldPos改为float4
+ +#if defined(FOG_DEPTH)
+ float4 worldPos : TEXCOORD4;
+#else
+ float3 worldPos : TEXCOORD4;
+#endif
+
然后要替换i.worldPos的所有用法为i.worldPos.xyz。将剪贴空间深度值赋给i.worldPos.w,在fragment传递给viewDistance。它只是齐次剪贴空间位置的Z坐标,所以在它被转换为0-1范围内的值之前。 +
+不正确的原因:可能会有反向裁剪空间Z的情况,需要转换。
+ +#if defined(UNITY_REVERSED_Z)
+ //D3d with reversed Z =>
+ //z clip range is [near, 0] -> remapping to [0, far]
+ //max is required to protect ourselves from near plane not being
+ //correct/meaningfull in case of oblique matrices.
+ #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) \\
+ max(((1.0-(coord)/_ProjectionParams.y)\*_ProjectionParams.z),0)
+#elif UNITY_UV_STARTS_AT_TOP
+ //D3d without reversed z => z clip range is [0, far] -> nothing to do
+ #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord)
+#else
+ //Opengl => z clip range is [-near, far] -> should remap in theory
+ //but dont do it in practice to save some perf (range is close enought)
+ #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord)
+#endif
+
+#define UNITY_CALC_FOG_FACTOR(coord) \\
+ UNITY_CALC_FOG_FACTOR_RAW(UNITY_Z_0_FAR_FROM_CLIPSPACE(coord))
+
增加双支持!FOG_DISTANCE 和 FOG_DEPTH。用宏代替feature指令,仿照_BINORMAL_PER_FRAGMENT_定义_FOG_DISTANCE,_默认就是它。
+ +CGINCLUDE
+ #define BINORMAL_PER_FRAGMENT
+ #define FOG_DISTANCE
+ENDCG
+
//在shader中,要切换到基于距离的雾,如果FOG_DISTANCE已经被定义,我们要做的就是去掉FOG_DEPTH的定义。
+#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
+ #if !defined(FOG_DISTANCE)
+ #define FOG_DEPTH 1
+ #endif
+#endif
+
增加支持禁用。只在需要时使用雾,增加FOG_ON宏
+ +#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
+ #if !defined(FOG_DISTANCE)
+ #define FOG_DEPTH 1
+ #endif
+ #define FOG_ON 1
+#endif
+
+float4 ApplyFog (float4 color, Interpolators i) {
+ #if FOG_ON
+ float viewDistance = length(_WorldSpaceCameraPos - i.worldPos.xyz);
+ #if FOG_DEPTH
+ viewDistance = UNITY_Z_0_FAR_FROM_CLIPSPACE(i.worldPos.w);
+ #endif
+ UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
+ color.rgb = lerp(unity_FogColor.rgb, color.rgb, saturate(unityFogFactor));
+ #endif
+ return color;
+}
+
增加支持多光源。但是变得更亮了,这是因为每个光的颜色都叠加到了雾色之上,所以黑色雾是没问题。
+ + +解决办法就是:对additive pass使用黑色雾,这样就会淡化一部分颜色。
+ +float3 fogColor = 0;
+#if defined(FORWARD_BASE_PASS)
+ fogColor = unity_FogColor.rgb;
+#endif
+color.rgb = lerp(fogColor, color.rgb, saturate(unityFogFactor));
+
deferred路径没有雾,这是因为所有的光照计算完成后,才会计算雾。为了能够在deferred渲染雾,见2.1
+ +要增加雾渲染,需要等所有光照计算直到它们完成后,在其他pass再次渲染雾。该pass不在shader内部,属于屏幕ImageEffects(后处理)阶段。
+ +[ExecuteInEditMode]
+public class DeferredFogRender : MonoBehaviour
+{
+ private void OnRenderImage(RenderTexture source, RenderTexture destination)
+ {
+ }
+}
+
这是增加了一个全屏后处理pass。如果有多个这样实现了OnRenderImage脚本,将会按顺序依次执行。
+ +OnRenderImage(RenderTexture source, RenderTexture destination) 两个参数:
+source 是已计算好最终颜色
+destination 输出雾。若为空直接进入帧缓冲区
方法内部必须调用Graphics.Blit函数,它会画一个全屏面片输出到Destination
+ +void OnRenderImage (RenderTexture source, RenderTexture destination) {
+ Graphics.Blit(source, destination);
+}
+
2.1只是做了简单的拷贝,没什么用。必须要新建一个处理sourceTexture的shader来渲染雾。基本框架:
+ +Shader "Custom/MyDeferredFog"
+{
+ Properties
+ {
+ _MainTex ("Texture", 2D) = "white" {}
+ }
+ SubShader
+ {
+ // No culling or depth
+ Cull Off
+ ZWrite Off
+ ZTest Always
+ Pass
+ {
+ }
+ }
+}
+
然后用后处理脚本需要引用该Shader
+ +Pass
+{
+ CGPROGRAM
+ #pragma vertex VertexProgram
+ #pragma fragment FragmentProgram
+ #pragma multi_compile_fog
+ #include "UnityCG.cginc"
+ sampler2D _MainTex;
+ struct modelData
+ {
+ float4 vertex : POSITION;
+ float2 uv : TEXCOORD0;
+ };
+ struct Interpolarters
+ {
+ float4 position : SV_POSITION;
+ float2 uv : TEXCOORD0;
+ };
+ Interpolarters VertexProgram(modelData m) {
+ Interpolarters i;
+ i.position = UnityObjectToClipPos(m.vertex);
+ i.uv = m.uv;
+ return i;
+ }
+ float4 FragmentProgram(Interpolarters i) :SV_Target
+ {
+ float3 sourceColor = tex2D(_MainTex, i.uv).rgb;
+ return float4(sourceColor, 1);
+ }
+ ENDCG
+}
+
增加深度雾。Unity自带深度buffer变量__CameraDepthTexture_, 然后使用指令_SAMPLE_DEPTH_TEXTURE_采样深度:
+ +UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
+
+float4 FragmentProgram(Interpolarters i) :SV_Target
+{
+ float depth = SAMPLE_DEPTHE_TEXTURE(_CameraDepthTexture, i.uv);
+ float3 sourceColor = tex2D(_MainTex, i.uv).rgb;
+ return float4(sourceColor, 1);
+}
+
!首先。可以使用在UnityCG中定义的Linear01Depth函数将其转换为一个线性范围。这是因为从深度缓冲区得到原始数据后,需要从齐次坐标转换为[0,1]范围的clip-space坐标。我们必须转换这个值,使它成为世界空间中的一个线性深度值。
+ +float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
+depth = Linear01Depth(depth);
+
Linear01Depth内部实现:
+ +// Z buffer to linear 0..1 depth
+inline float Linear01Depth( float z )
+{
+ return 1.0 / (_ZBufferParams.x \* z + _ZBufferParams.y);
+}
+
+// Values used to linearize the Z buffer
+// (http://www.humus.name/temp/Linearize%20depth.txt)
+// x = 1-far/near
+// y = far/near
+// z = x/far
+// w = y/far
+float4 _ZBufferParams;
+
!然后。需要使用far_clip平面距离缩放该depth值,得到真实的深度视距。clip_space裁剪空间可通过float4 ProjectionParams变量获得, 定义在_UnityShaderVariables.cginc中。其中Z分量就是远平面far_clip距离。。
+ +depth = Linear01Depth(depth);
+float distance = depth \* _ProjectionParams.z;
+
!最后,计算实际的fog。
+ +float viewDistance = depth \* _ProjectionParams.z;
+UNITY_CALC_FOG_FACTOR_RAW(viewDistance);
+unityFogFactor = saturate(unityFogFactor);
+float3 sourceColor = tex2D(_MainTex, i.uv).rgb;
+float3 color = lerp(unity_FogColor.rgb, sourceColor, unityFogFactor);
+return float4(color, 1);
+
对比图15,就像把雾蒙在了物体上方。解决办法就是在绘制物体之前,绘制雾。使用_ImageEffectOpaque_属性绘制
[ImageEffectOpaque]
+void OnRenderImage (RenderTexture source, RenderTexture destination) {
+ Graphics.Blit(source, destination, fogMate);
+}
+
处理近平面(非精确处理),near plane存储在Y值中
+ +float viewDistance = depth \* _ProjectionParams.z -_ProjectionParams.y;
+
deferred灯光的着色,从depth-buffer中重建世界空间位置,以便计算灯光。我们也可以这样仿照这样计算雾。
+ +透视相机的clip-space空间定义了一个梯形区域,如果忽略near-plane就得到的是一个以相机world-pos为顶点的三角形区域。它的高是far-plaen距离,那么线性化后的depth范围:顶点为0,底边为1。
+ + +对于渲染的后处理图形Image的每个像素,都能通过从顶点到底边发射一条射线(从屏幕射向3D空间),检测是否击中任何物体,击中渲染,未击中不渲染。
+ + +若击中某个物体,那么对应像素的深度就要小于1. 如果,该射线在半道击中了物体,射线对应的像素深度值就是1/2.这就意味着 $射线对应的Z值 ={射线击中物体时的长度\over射线总长度}$。范围[0, 1]。又由于射线的方向都是一致的,X和Y坐标也应该减半。
+ + +一旦得到该射线,就能从相机的位置出发,寻找可能会被渲染的物体表面的世界坐标(若击中)。同时,也要得到该射线的长度。
+ +要使用上述方法,必须知道从相机到平面的每一个像素的射线。但实际上,只需要4条射线,金字塔的每个角都需要一条射线。用插值可给出中间所有像素的光线。
+ +基于相机远平面和视角计算光线,同时相机的方向和位置与距离无关,所以可以忽略变换。Camera提供了一个函数:CalculateFrustumCorners
,四个参数
+ 矩形面积(image rect)
+ 光线投射距离(相机far-plane)
+ 立体渲染(相机自带)
+ 4个元素的3D向量组
deferredCamera.CalculateFrustumCorners
+(
+ rectArea,
+ deferredCamera.farClipPlane,
+ deferredCamera.stereoActiveEye,
+ corners
+);
+
下一步传递该数据至Shader,同时也得改变索引顺序。相机提供的是:左下、左上、右上、右下。shader需要:左下、右下、左上、右上
+ +//corners vectex index: b-l, u-l, u-r, b-r
+//shader vectex index : b-l, b-r, u-l, u-r
+frustumCorners[0] = corners[0];
+frustumCorners[1] = corners[3];
+frustumCorners[2] = corners[1];
+frustumCorners[3] = corners[2];
+
+fogMate.SetVectorArray("__FustumCorners", frustumCorners);
+
Shader需要一个接收变量,同时定义一个FOG_DISTANCE宏,当需要使用距离时再计算光线。
+ +#define FOG_DISTANCE
+
+struct Interpolators
+{
+ float4 position : SV_POSITION;
+ float2 uv : TEXCOORD0;
+#if FOG_DISTANCE
+ float3 ray : TEXCOORD1;
+#endif
+};
+
根据UV坐标计算获取数组中对应的光线,传进shader的数组排列:(0,0) (1,0) (0,1) (1,1),使用U+2V可得
+ +#if FOG_DISTANCE
+ i.ray = _FustumCorners[i.uv.x + 2 \* i.uv.y];
+#endif
+
最后在Fragment函数替换基于深度计算的雾,使用基于距离计算
+ +float viewDistance = 0;
+#if defined(FOG_DISTANCE)
+ viewDistance = length(i.ray \* depth);
+#else
+ viewDistance = depth \* _ProjectionParams.z - _ProjectionParams.y;
+#endif
+
解放天空盒。两个不同渲染路径渲染的雾会有显著差异。延迟雾也会影响天空盒。它的作用就像far-plane是一个固体屏障,受到雾的影响。当深度值接近1时,表明已经到达了远平面。如果不想给天空盒蒙上雾,可以通过将雾因子设置为1来防止。
+ +if (depth > 0.999)
+{
+ unityFogFactor = 1;
+}
+
最后考虑如何停止渲染雾。解决方案是当没有设置任何雾关键字,通过设置雾因子为1即可。
+ +#if !defined(FOG_LINEAR) || !defined(FOG_EXP) || !defined(FOG_EXP2)
+ unityFogFactor = 1;
+#endif
+
到目前为止一直使用了Unity的Forward Render Path,现在开始学习Deferred Path,以及对比这两者间的差异
+ +一共有64个Object可见物体组成一个Prefab。
+ +通过对比这个prefab有和没有阴影,分别计算处于ForwardPath和DeferredPath下的Draw Call数。
+1. No Shadows
+ +没有阴影下,128次几何绘制加1次Clear;1次
天空盒绘制;2次
屏幕处理绘制,总共132次Draw Call。(如果是使用一个方向光,动态批处理就会生效,就可以少于64个批次绘制)。然而由于有一个额外的方向光,dynamicBatching就不会生效,所以总共绘制两遍。
2. Enable Shadows
+
启用阴影后,需要更多的Draw Calls去生成阴影纹理,分析如下:
+ +首先,填充depth-buffer,需要47次Draw Call,47少于64得益于dynamicBatching;
+其次,创建Cascading阴影纹理。第一个光提交了110次Drall Call,同时第二个光提交了115次Drall Call,这些纹理渲染在屏幕空间screen-space buffer,执行过滤。
+最后,每个光绘制一次几何物体,用了128=$(64*2)$次DC。
总共408次Draw Calls。 +
+上面是分析前向渲染,下面是分析延迟渲染。
+1. No Shadows +
+首先,45次Draw Call渲染GBuffer,这得益于dynamic Batching;
+其次,1次Draw Call复制深度纹理;
+接着,1次绘制反射和1次自发光反射;
+最后,2次光照着色(两个方向光)。
总共52次 = 49次几何绘制; 1次天空盒绘制;2次屏幕处理绘制
-{% include img-picture.html - url1="posts/2018/month1/catRender15/1692664-20200527011133845-1557873589.png" - des="正确,没有阴影" width="250" -%} +2. Enable Shadows
+与上图的lighting着色不同,用231次Draw Calls绘制。但是其阴影绘制方法与Forward模式是一样的。 +
+Deferred不支持MSAA,如果启用Camera组件会有Warning
-在自己的cginc文件中,我们依靠AutoLight中的宏来确定由阴影引起的光衰减。 不幸的是,该文件在编写时并没有考虑到延迟的光线。 现在将自己进行阴影采样,可通过_ShadowMapTexture变量访问阴影贴图。
+延迟着色依赖于每个片段存储的数据,这是通过纹理完成的。 这与MSAA不兼容,因为该抗锯
+齿技术依赖于子像素数据。 尽管
+三角形边缘仍然可以从MSAA中受益,但延迟的数据仍会混叠。 您必须依靠一个后处理过滤器来进行抗锯齿。
+
缘由以及deferred着色解释:
+ +要渲染物体,需要抓取它的Mesh数据;转换到正确的空间;插值传递数据;检索properties数据;计算光照。对于Forward shaders:对要着色的物体的每个像素重复上述步骤;additivePass要比basePass节省,是因为depth-buffer已经提前准备好,同时它不需要关心间接光;但它任然会重复在basePase已经完成的有大量工作。 +
+既然每次计算的几何性质都是一样的,可以让basePass计算后将它们存储在一个缓冲区中。然后,additivePass可以重用数据,消除重复的工作。要存储这些片段的数据,就需要一个适合的缓冲区,就像深度和帧缓冲区一样。 +
+现在,缓冲区中提供了照明所需的所有几何数据。 唯一缺少的是灯光本身。 但这意味着我们不再需要渲染几何体,仅需渲染光就足够了。 此外,basePass只需要填充缓冲区,然后推迟所有直接光照着色计算。这就是延迟着色。 +
+当使用一个光源,deferred着色本身没有多大好处。但是当使用很多光源时,每额外增加一个光只会少量增加一点工作,前提是该光源不投射阴影。
+ +因此,当几何物体和光源分开渲染,光的数量对物体的影响是没有限制的,所有的光对它们范围内的物体都是逐像素着色,这个_Pixel Light Count_也就没用了。 +
+多个实时光源,也可以用bake替代。
+ +光本身是如何渲染的?
+ +1、directional方向光,它被渲染成一个面片(Quad)覆盖整个屏幕,使用_Internal-DeferredShading_ shader完成渲染. +
+该shader使用了_UnityDeferredLibrary.cginc_的UnityDeferredCalculateLightParams函数计算光照。
+ + **对于SpotLight、PointLight类似,不同在于它有自身的照明范围。**
+
2、SpotLight首先要渲染成一个类似金字塔体Mesh。
+ +首先,使用Internal-StencilWrite shader渲染该金字塔Mesh并写入模板缓冲区;
+然后,用该缓冲区与稍后将渲染的片元比对,是否要屏蔽体积范围外的片元光照计算。
+目的:处于体积范围内的片元将被计算光照、阴影,体积范围外的不需要计算,如果这个金字塔内一个片元被渲染,它会执行光照计算。防止那些不必要的光照计算降低开销。
+原因:光线无法到达那里。
+注意,当光的体积与相机的近平面相交则该方法失效。
+
3、PointLight使用类似方法,区别在于它被渲染成球体Mesh。 +
+缓存数据的缺点就是要存储。deferred渲染使用了multiple render texture实现(MRT),这些纹理就是G-buffers。
+ +deferred要求4个G-Buffers,合并后每个像素总位数:LDR160位,HDR192位。这比起32位大多了,所以对于移动GPU有限制,桌面则没问题。
+ +是哪四个纹理?
+ +打开frame Debugger或点击Scene/top left下拉菜单选择Deferred选项 +
+Albedo-对应RT0;典型代表diffuse color
+Specular–对应RT1;典型代表specular + roughness
+Normal-对应RT2;典型代表normals
+Emission–对应RT3;典型代表emission + reflection
首先,执行deferred渲染G-buffer,数据存到G-buffer缓冲区。在这渲染期间,forward模式下的物体不参与渲染,不可见。 +
+接着,渲染forward depth,同样存到G-buffer缓冲区。由于是forward所以输出黑色轮廓,覆盖了之前的片元。 +
+增加一个Pass,指定光照标签Light Mode = “Deferred”,pass的顺序不重要。
+ +Pass {
+ Tags {
+ "LightMode" = "Deferred"
+ }
+}
+
完整版的着色
+Pass {
+ Tags {
+ "LightMode" = "Deferred"
+ }
+
+ CGPROGRAM
+
+ #pragma target 3.0
+ #pragma exclude_renderers nomrt//(对于不支持MRT设备禁用该pass)
+
+ #pragma shader_feature _ _RENDERING_CUTOUT
+ #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 vertex MyVertexProgram
+ #pragma fragment MyFragmentProgram
+
+ #define DEFERRED_PASS
+
+ #include "MyLighting_SemitransparencyShadow.cginc"
+
+ ENDCG
+}
+
要填充4个缓冲区需要为Fragment支持两种输出格式;然后修改Fragment函数返回值。
+ +struct FragmentOutput {
+#ifdef DEFERRED_PASS
+ float4 gBuffer0 : SV_TARGET0;
+ float4 gBuffer1 : SV_TARGET1;
+ float4 gBuffer2 : SV_TARGET2;
+ float4 gBuffer3 : SV_TARGET3;
+#else
+ float4 color : SV_TARGET;
+#endif
+};
+
+FragmentOutput MyFragmentProgram(Interpolators i){
+ FragmentOutput output;
+ UNITY_INITIALIZE_OUTPUT(FragmentOutput, output);//不加这句会有warning
+ #ifdef DEFERRED_PASS
+ #else
+ output.color = color;
+ #endif
+ return output;
+}
+
该g-Buffer一般用来计算diffuse albedo和surface occlusion,这是ARGB32格式纹理。
+ +diffuse存在RGB通道,occlusion存在A通道。
+ +#if defined(DEFERRED_PASS)
+ output.gBuffer0.rgb = albedo;
+ output.gBuffer0.a = GetOcclusion(i);
+#else
+
该g-Buffer一般用来计算specular albedo和smoothness,也是ARGB32格式纹理。
+ +specular存在RGB通道,smoothness值存在A通道。
+ +output.gBuffer1.rgb = specularTint;
+output.gBuffer1.a = GetSmoothness(i);
+
该g-Buffer包含了世界空间法线向量,存在RGB通道,是一个ARGB2101010格式纹理。Alpha占2位,RGB各占10位。这意味着该向量的每个标量占10bits而不是之前的8位,也就有更精确。alpha通道不用,默认位1.
+ +output.gBuffer2 = float4(i.normal * 0.5 + 0.5, 1);
+
该g-Buffer被用来计算场景光照,纹理格式依赖于相机设置的HDR或LDR。LDR是ARGB2101010格式。HDR是ARGBHalf格式,每个通道存储16位单精度浮点数,共64位。因此HDR版本比其他buffers大两倍内存。只用到了RGB,Alpha默认为1。
+ +output.gBuffer3 = color;
+
上面使用的color颜色已计算过阴影,需要使用DEFRRED_PASS重新计算;同时关闭light.ndotl计算;在GetEmission也要使用DEFRRED_PASS标签
output.gBuffer3 = color;
+
+UnityLight CreateLight (Interpolators i) {
+ UnityLight light;
+ #if defined(DEFERRED_PASS)
+ light.dir = float3(0, 1, 0);
+ light.color = 0;
+ #else
+ //...
+ #endif
+ ~light.ndotl = DotClamped(i.normal, light.dir);~
+ return light;
+}
+
+float3 GetEmission (Interpolators i) {
+ #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS)
+ …
+ #else
+ return 0;
+ #endif
+}
+
输出四个Buffers后结果看起来较好,但是还不够完整,还缺少环境光。环境光与自发光都没有单独的pass计算,都需要在g-buffers填充后附加到最终颜色值上。环境光是间接光,在间接光函数计算DEFERRED_PASS
+ + +对于Internal-DeferredShading shader,Camera组件启用或关闭HDR:
+ +启用HDR,则不执行pass 1,pass 0 fragment返回half4精度,纹理格式ARGBHalf。每个通道存储16位单精度浮点数,总共64位。
+ +关闭HDR后,使用LDR计算策略。在pass 0 fragment会返回fixed精度,使用exp2(x)_[exp2(x)=2-x]_对数编码lightBuffer,获取到更大的颜色范围;pass 1则使用log对数解码buffer数据到主纹理上,纹理格式为ARGB2101010。Alpha占2位,RGB各占10位。
+ +开启HDR模式下,Deferred与Forward着色效果几乎相似。但是在LDR模式下,Deferred着色就是错误的。 +
+在上图描述中,Unity会使用exp2(x)编码LDR数据,所以对自发光和环境光也要使用exp2(x)编码。 +
+#pragma multi_compile _ UNITY_HDR_ON
+
+#if defined(DEFERRED_PASS)
+ #if !defined(UNITY_HDR_ON)
+ color.rgb = exp2(-color.rgb);
+ #endif
+ …
+#else
+ output.color = color;
+#endif
+
展示不同渲染模式下的,探针照射区域过渡对比。下图显示了每个反射可混合多个反射探针。 +
+延迟模式的不同之处在于:探测不会针对每个对象进行混合。而是按像素混合的。这是由internal-deferred shader完成计算。图3右很明显,大地板镜子,延伸到结构之外后,它的渲染范围很小。
+ +Forward模式下:
+ 地板被迫在整个表面使用reflection探头。结果,boxProjection探射到的投影在box外面也显示。也能看见它和其他探针混在一起。图3左。
Deferred模式下:
+ reflection只渲染box size范围,探射的范围被投射到与boxProjection相交的面积。所以reflection的反射不会超出给定BoxSize范围。实际上,边缘消失的时候也会延伸一点。其他两个探头也是如此。
渲染reflection所用方法,与渲染lights类似:
+首先,使用Internal-StencilWrite shader渲染box size大小的Mesh并写入模板缓冲区;
+然后,用该缓冲区与稍后将渲染的片元比对,是否要屏蔽体积范围外的片元。
Importance决定各reflection的渲染先后顺序;Intensity决定了反射强度:默认为1,最小为0,大于1过曝。Forward下是整个接收面积;Deferred下是相交面积。下面谈Blend Distance。
+ +Deferred下探针体积边缘有过渡混合,由Blend Distance决定。该值默认为1,只有Deferred模式可调。
+ +多个probes相交时,边缘过渡混合非常有效。也可以用来增大探射体积范围。 +
+虽然deferred很有效,并且每个物体可以混合两个以上的探针。
+ +也有缺点:不能使用锚重写来强制对象使用特定的反射探测。有时这是确保得到正确反射的唯一方法。可以先禁用内置Deferred Refelection,
+ +打开frame Debugger查看G-Buffers RT3,包含了Emission和Refelection +
+采样黑色探针是浪费。要确保deferred pass只在有需要时采样,用UNITY_ENABLE_REFLECTION_BUFFERS来检查。
+ +UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) {
+ #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS)
+ #if defined(DEFERRED_PASS) && UNITY_ENABLE_REFLECTION_BUFFERS
+ indirectLight.specular = 0;
+ #endif
+ #endif
+ return indirectLight;
+}
+