原文 译文

已翻译 Unity 渲染教程(十六):静态光照

2017-08-21 3 1873

翻译作者

版权所有,禁止匿名转载;禁止商业使用;禁止个人使用。

 译者: 崔嘉艺(milan21   审校:王磊(未来的未来)

·         进行采样并渲染到光照贴图。

·         让烘焙的光照能与法线贴图一起工作。

·         使用一个光照探测器组。

这是关于渲染基础的系列教程的第十六部分。在前面的部分里我们提供了自己的延迟光源。而在系列的这个部分,我们将转而去研究光照贴图。

 Unity 渲染教程(十六):静态光照

对一些光源进行烘培的效果。

光照贴图

执行光照计算的开销是非常昂贵的。 延迟渲染允许我们使用很多光源,但阴影的开销仍然是一个限制因素。如果我们的场景是动态的,那么我们没有办法来避免执行这些计算。但是如果光源和几何体都是不变的,那么我们可以只计算一次光照并重复使用。这使得我们可以在场景中放置许多光源,而不必在运行的时候再渲染它们。这种方法也可以使用那些不能用作实时光源的区域光源。

到底有多少光照要预先计算可能会有所不同。在本教程中,我们将一路走下去,将所有内容都放在光照贴图中。所以根本不会有任何动态光照。

为了尝试光照贴图,我创建了一个简单的测试场景,它具有一个简单的结构,可以提供阴影,还有一些放置在其内部的球体。一切物体都使用默认的Unity材质。

 Unity 渲染教程(十六):静态光照

针对光照贴图的一个测试场景。

烘焙光源

要开始使用光照贴图,将唯一的光源对象的模式改为“Baked(烘焙)”而不是“Realtime(实时)”。

 Unity 渲染教程(十六):静态光照

使用烘焙模式的主方向光源。

将主方向光源变成烘培光源后,它将不再包含在动态光照中。从动态对象的角度来看,光源是不存在的。 唯一仍然不变的是环境光照,它仍然是基于主方向光源的。

 Unity 渲染教程(十六):静态光照

没有直接光照的效果。

要实际启用光照贴图,请在光照窗口的“混合光照(Mixed Lighting)”部分中打开“烘培全局光照BakedGlobal Illumination)”。 然后将光照模式设置为“烘培间接光照(BakedIndirect)”。 尽管名字里面说的是烘培间接光照,但是它也包括了直接光照。 它通常用于向场景添加间接光照。另外,确保实时全局光照(Realtime Global Illumination )被禁用,因为我们还没有支持到这一点。

 Unity 渲染教程(十六):静态光照

烘培间接光照模式。

静态几何体

场景的对象都应该是固定的。它们永远不会移动位置。要将这一个信息传达给Unity,请将这些对象标记为静态。你可以通过启用检视器窗口右上角的静态切换键来做到这一点。

光源也必须被标记为静态吗?

不,这不是必需的。光源只需要设置为适当的模式。

有各种子系统关心物体是否是静态的。“静态(static)”还有一个下拉菜单,你可以使用它来微调哪些系统会将这个对象视为静态的。现在我们只关心光照贴图,但最简单的做法是使一切都完全是静态的。

 Unity 渲染教程(十六):静态光照

静态的物体。

一个物体对于光照贴图来说是否是静态的,也可以通过其网格渲染器的检视器来进行查看和编辑。

 Unity 渲染教程(十六):静态光照

对于光照贴图来说是静态的物体。

现在,所有的物体都是静态的,它们将被包含在光照贴图的处理过程中。

 Unity 渲染教程(十六):静态光照

使用烘焙光照的场景。

请注意,使用光照贴图得到的结果不如使用实时照明得到的结果亮度那么高。这是因为缺失了镜面高光,只剩下了漫反射光照。镜面高光取决于视角,因此取决于相机的角度。通常,相机是移动的,因此它不能包含在光照贴图中。这种限制意味着光照贴图可以用于微弱的光线和暗淡的表面,但不能用于强直射光或有光泽的表面。如果你想要镜面高光,你将不得不使用实时光源。所以你经常会使用烘烤光源和实时光源的混合。

为什么我没有得到烘焙光源?

为了确保在需要的时候光照贴图可以实际生成和更新,请在光照窗口的底部启用“自动生成(Auto Generate。 否则,你必须手动生成新的光照贴图。

 Unity 渲染教程(十六):静态光照

自动生成的光照贴图。

 

光照贴图设置

光照窗口包含专门用于光照贴图设置的部分。在这里,你可以在质量、尺寸和烘烤时间之间取得平衡。你还可以在Enlighten的光照贴图引擎和Progressive lightmapper之间进行切换。后者会增量地生成光照贴图,优先考虑场景视图中可见的内容,这在编辑的时候很方便。我在本教程中使用的是Enlighten光照贴图引擎。

 Unity 渲染教程(十六):静态光照

默认的光照贴图设置。

在做任何事情之前,请将“DirectionalMode“设置为”Non-Direction“。 稍后我们会处理其他模式。

 Unity 渲染教程(十六):静态光照

使用“Non-directional”模式的光照贴图。

烘烤的光照存储在纹理中。 你可以通过将光照窗口从“场景(Scene)“切换到”全局地图(Global Maps )“模式来进行查看。 使用默认设置,我的测试场景很容易与一张1024×1024贴图相匹配。

 Unity 渲染教程(十六):静态光照

得到的光照贴图。

Unity的默认对象都具有用于光照贴图的UV坐标。对于导入的网格,你可以提供自己的坐标,也可以让Unity为你生成。烘烤后,可以在光照贴图中看到纹理展开。它们需要多少空间取决于场景中物体的大小和光照贴图的分辨率设置。 如果它们不适合单一贴图,Unity将会创建其他贴图。

 Unity 渲染教程(十六):静态光照 Unity 渲染教程(十六):静态光照

光照贴图的分辨率的不同会带来很大的差异。

对于每个项目来说,最佳设置都是不同。 你必须调整设置,直到达成良好的平衡。需要注意的是,视觉质量也很大程度上取决于用于光照贴图的纹理展开的质量。不存在纹理接缝可能会产生明显的瑕疵。Unity的默认球体就是一个很好的例子。它不适用于光照贴图。

间接光源

烘烤光照意味着我们失去了镜面高光,我们获得的是间接光照。这是在到达我们的眼睛之前会在多个表面反射的光。因为光会在角的周围反射,那些本来会被遮挡的区域仍然会被照亮。我们不能实时计算这个信息,但是我们可以在烘焙的时候包括反射光。

要清楚地看到实时光照和烘培光照之间的差异,将环境光照的强度设置为零。 这就去掉了天空盒的影响,所以所有的光都只是来自方向光源。

 Unity 渲染教程(十六):静态光照 Unity 渲染教程(十六):静态光照

没有环境光照,在实时光照与光照贴图下的效果对比。

每次光子反射的时候,它都会失去一些能量,它被与它相互作用的材质着色。 Unity在烘烤间接光照的时候考虑到了这一点。 因此,物体会根据附近的颜色进行着色。

 Unity 渲染教程(十六):静态光照 Unity 渲染教程(十六):静态光照

绿色的地面,在实时光照与光照贴图下的效果对比。

自发光表面也会影响烘焙光照。它们会成为间接光源。

 Unity 渲染教程(十六):静态光照 Unity 渲染教程(十六):静态光照

自发光的地面,在实时光照与光照贴图下的效果对比。

间接光照的一个特殊设置是环境遮挡。这是指在角落和折痕中发生的间接光照造成的阴影。这是一种人为的提升,可以增强深度方面的感觉。

 Unity 渲染教程(十六):静态光照 Unity 渲染教程(十六):静态光照

使用环境遮挡的效果。

环境遮挡效果完全基于表面是如何隐藏的。它不考虑光线实际来自哪里。这并不总是正确的,举个简单的例子来说,当与自发光表面组合的时候就会产生一些错误的结果。

 Unity 渲染教程(十六):静态光照

显然是错误的环境遮挡效果。

透明度

光照贴图在一定程度上可以处理半透明表面。 光将通过它们,尽管光的颜色不会被它们所过滤。

 Unity 渲染教程(十六):静态光照

半透明的屋顶。

镂空材质也可以在光照贴图中正常工作。

 Unity 渲染教程(十六):静态光照

镂空的屋顶。

但是,这仅在使用封闭曲面的时候有效。当使用单面几何像是四边形的时候,光线将在不存在的一面损坏。当另外一面没有任何东西的时候,这是很好的,但是当使用单面透明表面的时候会导致问题。

 Unity 渲染教程(十六):静态光照

四边形上有一个错误。

为了处理这个问题,你必须告诉光照贴图系统将这些表面视为透明的。 这可以通过自定义光照贴图设置完成。你可以通过Asset / Create / Lightmap参数来创建这些数据。这些资源允许你自定义每个对象的光照贴图计算。在这种情况下,我们只想表明我们正在处理一个透明的对象。所以启用“它是透明的(Is Transparent)“。 虽然它是预计算实时全局光照(Precomputed Realtime GI )部分中的一部分,但会影响所有烘烤光照。

 Unity 渲染教程(十六):静态光照

指示这是透明的。

要使用这些设置,请通过物体的网格渲染器检视器来选择它们。你的资源名字将显示在Lightmap参数的下拉列表中。

 Unity 渲染教程(十六):静态光照  Unity 渲染教程(十六):静态光照

为透明四边形使用自定义参数。

将物体标记为透明也会改变它对间接光照的贡献。透明物体让间接光通过,而不透明物体则会阻挡间接光。

项目文件下载地址:unitypackage

使用光照贴图

现在我们知道光照贴图是如何工作的,我们可以为My First Lighting着色器添加对光照贴图的支持。这个过程的第一步是对光照贴图进行采样。调整场景中的球体,以便我们的着色器使用白色材质。

 Unity 渲染教程(十六):静态光照

使用我们的白色材质的球体。

光照贴图的着色器变体

当一个着色器被认为应该使用光照贴图的时候,Unity会寻找与LIGHTMAP_ON关键字关联的变体。 所以我们必须为这个关键字添加一个多编译指令。 当使用前向渲染路径的时候,仅在基本渲染通道中采样光照贴图。

1
2
3
4
5
6
7
#pragma multi_compile _ SHADOWS_SCREEN
 
#pragma multi_compile _ VERTEXLIGHT_ON
 
#pragma multi_compile _ LIGHTMAP_ON
 
#pragma multi_compile_fog

当使用光照贴图的时候,Unity不会包含顶点光源。他们的关键字是相互排斥的。所以我们不需要一个会同时使用VERTEXLIGHT_ONLIGHTMAP_ON的变体。

1
2
3
4
5
6
7
8
9
                                              #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

延迟渲染路径中也支持光照贴图,因此也可以将这个关键字添加到延迟渲染通道中。

1
2
3
#pragma multi_compile _ UNITY_HDR_ON
 
#pragma multi_compile _ LIGHTMAP_ON


光照贴图的坐标

用于采样光照贴图的坐标存储在第二个纹理坐标通道uv1里面。 所以将此通道添加到My Lighting中的VertexData结构体中。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct VertexData {
 
               float4 vertex : POSITION;
 
               float3 normal : NORMAL;
 
               float4 tangent : TANGENT;
 
               float2 uv : TEXCOORD0;
 
               float2 uv1 : TEXCOORD1;
 
};

光照贴图坐标也必须进行插值。因为它们与顶点光源互斥,所以都可以使用TEXCOORD6

1
2
3
4
5
6
7
8
9
10
11
struct Interpolators {
    
 
    #if defined(VERTEXLIGHT_ON)
        float3 vertexLightColor : TEXCOORD6;
    #endif
 
    #if defined(LIGHTMAP_ON)
        float2 lightmapUV : TEXCOORD6;
    #endif
};

来自顶点数据的坐标定义了用于光照贴图的网格的纹理展开。但是它并没有告诉我们这个展开位置在哪里,也没有告诉我们它的大小。我们必须缩放和偏移坐标才能得到最终的光照贴图坐标。这种方法类似于应用于常规纹理坐标的转换,除了转换是特定于对象的,而这里的方法是特定于材质的。在UnityShaderVariables中将光照贴图的纹理定义为unity_Lightmap

1
2
3
4
5
6
7
8
9
10
11
12
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。由于这种不一致,我们必须手动进行这个变换。

1
i.lightmapUV = v.uv1 * unity_LightmapST.xy + unity_LightmapST.zw;


对光照贴图进行采样

因为光照贴图的数据被认为是间接光照,我们将在CreateIndirectLight函数中进行采样。当光照贴图可用的时候,我们必须将它们用作间接光,而不是球面谐波。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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_TEX2Dunity_Lightmap)。要对它进行采样,我们将使用UNITY_SAMPLE_TEX2D宏而不是tex2D。 我们稍后会看到这么做的原因。

1
2
indirectLight.diffuse =
    UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV);

 Unity 渲染教程(十六):静态光照

使用原始光照图数据的效果。

我们现在得到了烘烤的间接光照,但效果看起来不对。这是因为光照贴图数据已被编码。颜色以RGBM格式或是半强度格式进行存储,以支持高强度的光。 UnityCGDecodeLightmap函数负责为我们解码。

1
2
3
indirectLight.diffuse = DecodeLightmap(
    UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV)
);

 Unity 渲染教程(十六):静态光照

使用解码后光照图数据的效果。

项目文件下载地址:unitypackage

 

创建光照贴图

虽然光照贴图看上去可以与我们的着色器一起使用,但这只适用于我们简单的测试场景。目前,光照贴图总是将我们的对象视为不透明和纯白色的物体,即使事实不是这样。我们必须对我们的着色器进行一些调整,甚至添加另一个渲染通道来完全支持光照贴图。

从现在开始,对场景中的所有对象使用我们自己的着色器。也不再使用默认的材质。

半透明的阴影

光照贴图不使用实时渲染管道,因此它不使用我们的着色器来完成它的工作。 当尝试使用半透明阴影的时候,这是最明显的。通过设置屋顶立方体材质的色调alpha分量小于1来赋予屋顶立方体半透明度。

 Unity 渲染教程(十六):静态光照

半透明的屋顶,效果不正确。

光照贴图仍然把屋顶看成是实心物体,这是不正确的。它使用材质的渲染类型来确定如何处理表面,这应该告诉光照贴图我们的对象是半透明的。事实上,它确实知道屋顶是半透明的,它只是把它看作是完全不透明的而已。这是因为它使用_Color材质属性的alpha组件以及主纹理来设置不透明度。但是我们没有这个属性,我们用_Tint进行代替!

不幸的是,没有办法告诉光照贴图程序要使用哪个属性。 所以为了使光照贴图能够工作,我们别无选择,只能用_Color替换_Tint的使用。 首先,更新我们的着色器的属性。

1
2
3
4
5
    Properties {
//      _Tint ("Tint", Color) = (1, 1, 1, 1)
        _Color ("Tint", Color) = (1, 1, 1, 1)
        
    }

然后,为了保证我们的着色器的功能,我们还必须更换My Lighting.中的相应变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
//float4 _Tint;
float4 _Color;
 
float3 GetAlbedo (Interpolators i) {
    float3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb;
    
}
 
float GetAlpha (Interpolators i) {
    float alpha = _Color.a;
    
}

需要对My Shadows做同样的事情。

1
2
3
4
5
6
7
8
//float4 _Tint;
float4 _Color;
 
float GetAlpha (Interpolators i) {
    float alpha = _Color.a;
    
}

而且我们还要调整MyLightingShaderGUI

1
2
3
4
5
6
7
8
9
10
void DoMain () {
    GUILayout.Label("Main Maps", EditorStyles.boldLabel);
 
    MaterialProperty mainTex = FindProperty("_MainTex");
    editor.TexturePropertySingleLine(
        MakeLabel(mainTex, "Albedo (RGB)"), mainTex, FindProperty("_Color")
    );
 
    
}

 Unity 渲染教程(十六):静态光照

半透明的屋顶,正确的效果。

 

镂空部分的阴影

镂空部分的阴影也有类似的问题。光照贴图程序期望透明度的阈值存储在_Cutoff属性中,但是我们使用的是_AlphaCutoff。 因此,它使用默认阈值1

 Unity 渲染教程(十六):静态光照

镂空的屋顶,效果不正确。

解决方案是再次采用Unity的命名约定。所以更换资源。

1
2
3
4
5
6
7
8
    Properties {
        
 
//      _AlphaCutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
        _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
 
        
    }

调整My Lighting 以匹配新的名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
//float _AlphaCutoff;
float _Cutoff;
 
 
FragmentOutput MyFragmentProgram (Interpolators i) {
    float alpha = GetAlpha(i);
    #if defined(_RENDERING_CUTOUT)
        clip(alpha - _Cutoff);
    #endif
 
    
}

要对My Shadows做同样的更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
//float _AlphaCutoff;
float _Cutoff;
 
 
float4 MyShadowFragmentProgram (Interpolators i) : SV_TARGET {
    float alpha = GetAlpha(i);
    #if defined(_RENDERING_CUTOUT)
        clip(alpha - _Cutoff);
    #endif
 
    
}

同样要对MyLightingShaderGUI 做类似的修改。

1
2
3
4
5
6
void DoAlphaCutoff () {
    MaterialProperty slider = FindProperty("_Cutoff");
    EditorGUI.indentLevel += 2;
    editor.ShaderProperty(slider, MakeLabel(slider));
    EditorGUI.indentLevel -= 2;
}

 Unity 渲染教程(十六):静态光照

镂空的屋顶,正确的效果。

添加一个Meta渲染通道

下一步是确保光照贴图程序使用正确的表面反照率和自发光。 现在,一切都是白色的。你可以通过让地板称为绿色来看到这一点。 它应该导致绿色的间接光,但它自己保持白色。

 Unity 渲染教程(十六):静态光照

绿色的地板,效果不正确。

为了找出对象的表面颜色,光照贴图程序会将它的光照模式设置为Meta来寻找一个着色器渲染通道。这个渲染通道仅由光照贴图程序使用,不包含在构建中。所以让我们在我们的着色器上添加一个渲染通道。这是一个不应该使用剔除的基础渲染通道。 把这个渲染通道的代码放在一个新的名为My Lightmapping 的导入文件之中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Pass {
    Tags {
        "LightMode" = "Meta"
    }
 
    Cull Off
 
    CGPROGRAM
 
    #pragma vertex MyLightmappingVertexProgram
    #pragma fragment MyLightmappingFragmentProgram
 
    #include "My Lightmapping.cginc"
 
    ENDCG
}

现在我们需要确定反照率,镜面高光颜色,平滑度和自发光。所以将所需的变量和函数从My Lighting复制到My Lighting。我们只需要顶点位置和uv坐标。不使用法线和切线,但我们会需要顶点着色器中的光照贴图坐标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#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之外。GetEmission函数只有在用于前向基础渲染通道或是延迟渲染通道中的时候才会做一些事情。 在My Lightmapping,中,我们可以简单的删除这个限制。

1
2
3
4
5
6
7
8
9
10
11
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
}

这些函数只有在定义了适当的关键字时才会起作用,因此可以在渲染通道中为其添加着色功能。

1
2
3
4
5
6
7
8
9
10
#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"


顶点程序

这个渲染通道的顶点程序很简单。只是转换位置并转换纹理坐标。

1
2
3
4
5
6
7
8
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;
}

但是,我们并没有为相机做一个实际的渲染,我们是在为光照贴图程序进行渲染。我们将颜色与光照贴图中的对象的纹理展开相关联。要执行此映射,我们必须使用光照贴图坐标而不是顶点位置,并进行适当的转换。

1
2
3
4
Interpolators i;
v.vertex.xy = v.uv1 * unity_LightmapST.xy + unity_LightmapST.zw;
v.vertex.z = 0;
i.pos = UnityObjectToClipPos(v.vertex);

事实证明,为了让这个方法能在所有的机器上都工作,顶点位置的Z坐标必须以某种方式使用,即使我们不使用它也是如此。 Unity的着色器为此使用虚拟值,所以我们将简单地做同样的事情。

1
2
3
4
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导入文件中定义的UnityMetaFragment函数。它使用UnityMetaInput结构作为参数,其中包含反照率和自发光颜色。 该函数将决定要输出反照率和自发光颜色中的哪一个以及如何编码输出结果。

UnityMetaInput也包含镜面高光颜色,即使它不存储在光照贴图中。它用于一些编辑器可视化,我们现在将忽略它。

1
2
3
4
5
6
7
8
9
10
11
12
#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变量包含一个标记,这个标记会告诉函数是否输出反照率或是自发光颜色。还有一段有关编辑器可视化变体的代码,但是我把它删掉了,因为与这里的内容不相关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}

 Unity 渲染教程(十六):静态光照

间接光照设置为0的效果。

要获得自发光颜色,我们可以简单的使用GetEmission函数。要获得反照率,我们必须再次使用DiffuseAndSpecularFromMetallic函数。 该函数具有镜面高光颜色和反射率作为输出参数,因此即使我们不在函数外使用它们,我们也必须提供这些参数。我们可以使用surfaceData.SpecularColor来捕获镜面高光颜色。

1
2
3
4
5
6
7
8
9
10
11
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);
}

 Unity 渲染教程(十六):静态光照

间接光照着色的效果。

这适用于间接光照,但自发光光照可能还没有出现在光照贴图中。这是因为光照贴图程序并不总是包含一个自发光光照的渲染通道。材质必须表明它们具有自发光光照以对烘烤过程做出贡献。这是通过Material.globalIlluminationFlags属性完成的。 现在,让我们总是这么设置:当自发光光照编辑的时候,它应该被烘烤进光照贴图。

1
2
3
4
5
6
7
8
9
10
11
12
13
void DoEmission () {
    
    if (EditorGUI.EndChangeCheck()) {
        if (tex != map.textureValue) {
            SetKeyword("_EMISSION_MAP", map.textureValue);
        }
 
        foreach (Material m in editor.targets) {
            m.globalIlluminationFlags =
                MaterialGlobalIlluminationFlags.BakedEmissive;
        }
    }
}


粗糙的金属

我们的着色器现在看起来可以正常工作了,但它与标准着色器的结果不完全匹配。 当使用平滑度非常低的有色金属的时候,这一点是最明显的。

 Unity 渲染教程(十六):静态光照 Unity 渲染教程(十六):静态光照

粗糙的绿色金属,标准着色器与我们的着色器的效果对比。

这个想法是,非常粗糙的金属应该产生比我们目前的计算结果更多的间接光。 标准着色器通过将反射率的一部分加到镜面高光颜色进行补偿。它使用UnityStandardBRDFSmoothnessToRoughness函数来确定基于平滑度的粗糙度值,将其缩小一半,并使用它来缩放镜面高光颜色。

1
2
3
4
float roughness = SmoothnessToRoughness(GetSmoothness(i)) * 0.5;
surfaceData.Albedo += surfaceData.SpecularColor * roughness;
 
return UnityMetaFragment(surfaceData); 

SmoothnessToRoughness计算了什么东西?

转换是一减去平滑度值,然后平方。 从平滑度到粗糙度的平方映射最终会产生比仅仅做线性转换更好的结果。

1
2
3
4
5
6
// 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);
}

 Unity 渲染教程(十六):静态光照

调整反照率后的效果。

项目文件下载地址:unitypackage

定向光照贴图

光照贴图程序只使用几何体的顶点数据,不考虑几何体的法线贴图。光照贴图的分辨率太低,无法捕获由典型法线贴图提供的细节。这意味着静态光照将是平坦的。当使用具有法线贴图的材质的时候,这变得非常明显。

 Unity 渲染教程(十六):静态光照 Unity 渲染教程(十六):静态光照

使用了法线贴图,标准着色器与我们的着色器的效果对比。

当从实时光照切换到烘烤光照时,法线贴图的影响几乎完全消失。只是因为它们仍然用于环境反射才能看到它们。

方向性

通过将DirectionalMode ”改回“Directional,可以让法线贴图与烘焙光照一起工作。

 Unity 渲染教程(十六):静态光照

再次启用定向光照贴图。

当使用定向光照贴图的时候,Unity将创建两个贴图,而不是仅仅一个。第一张贴图包含通常的光照信息,称为强度图。 第二张贴图被称为定向图。 它包含大部分烘烤光来自的方向。

 Unity 渲染教程(十六):静态光照I

强度图和定向图。

当定向图可用的时候,我们可以使用它来对烘烤光进行简单的漫反射阴影计算。 这使得它可以应用于法线贴图之上。注意,只有一个光方向是已知的,所以阴影将是一个近似。 只要光照至少有一个主导的光线方向的时候,结果就会很好。

对方向进行采样

当定向光照贴图可用的时候,Unity将使用LIGHTMAP_ONDIRLIGHTMAP_COMBINED关键字查找着色器变体。我们可以在前向基础渲染通道中使用#pragma multi_compile_fwdbase,而不是为手动添加多编译指令。它会负责解决所有的光照贴图关键字,以及VERTEXLIGHT_ON关键字。

1
2
3
4
//          #pragma multi_compile _ SHADOWS_SCREEN
//          #pragma multi_compile _ LIGHTMAP_ON VERTEXLIGHT_ON
            #pragma multi_compile_fwdbase
            #pragma multi_compile_fog

我们可以为延迟渲染通道做同样的事情,但是我们必须使用#pragma multi_compile_prepassfinal指令。 它解决了光照贴图和高动态光照渲染的关键字。

prepassfinal是什么东西?

Unity 4使用了一种与以后的版本不同的延迟渲染管线。 在Unity 5中,它被称为传统延迟光照。 这种方法有更多的渲染通道。 Prepass决定是当时的术语。不需要引入新的指令,#pragma multi_compile_prepassfinal也用于当前的延迟渲染通道。

1
2
3
//          #pragma multi_compile _ UNITY_HDR_ON
//          #pragma multi_compile _ LIGHTMAP_ON
            #pragma multi_compile_prepassfinal

CreateIndirectLight函数中,在检索烘焙光源本身后,需要直接烘烤光的方向。方向贴图可以通过unity_LightmapInd获得。

1
2
3
4
5
6
7
8
9
10
11
12
#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

但是,这将导致编译错误。这是因为一个纹理变量实际上由两部分组成。 有纹理资源,还有采样器状态。采样器状态决定纹理的采样方式,包括滤波器和截取模式。 通常,每个纹理都定义了这两个部分,但这并不是所有平台都需要的。 也可以将这两个部分分开,这允许我们为多个纹理定义单个采样器状态。

因为强度和方向贴图总是以相同的方式进行采样,所以在可能的情况下,Unity使用单个采样器状态。 这就是为什么我们在采样强度贴图的时候必须使用UNITY_SAMPLE_TEX2D宏。方向贴图已经定义,没有采样器。 要对其进行采样,我们必须使用UNITY_SAMPLE_TEX2D_SAMPLER宏来明确地告诉它要使用哪个采样器。

1
2
3
float4 lightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER(
    unity_LightmapInd, unity_Lightmap, i.lightmapUV
);


使用方向贴图

要使用方向,我们首先要解码它。 然后,我们可以对法向量执行点积,找到漫反射因子并将其应用于颜色。但是方向贴图并没有包含单位长度的方向,而是比单位长度的方向会大一些。 幸运的是,我们可以使用UnityCGDecodeDirectionLightmap函数来解码方向数据并为我们执行渲染。

1
2
3
4
5
6
float4 lightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER(
    unity_LightmapInd, unity_Lightmap, i.lightmapUV
);
indirectLight.diffuse = DecodeDirectionalLightmap(
    indirectLight.diffuse, lightmapDirection, i.normal
);

 Unity 渲染教程(十六):静态光照

使用带有方向的光照贴图的效果。

DecodeDirectionLightmap做了什么?

DecodeDirectionLightmap实际上并不计算正确的漫射照明因子。 相反,它使用的是半Lambert。 这种方法可以有效地将光照射在表面周围,照亮阴影的区域会比它们本该照亮的更多。这么做是有必要的,这是因为烘烤的光照不是来自于单个方向。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.
 
    // Note that dir is not unit length on purpose. Its length is
    // "directionality", like for the directional specular lightmaps.
 
    half halfLambert = dot(normalWorld, dirTex.xyz - 0.5) + 0.5;
 
    return color * halfLambert / max(1e-4h, dirTex.w);
}

代码的注释中提到镜面高光。 这些是支持镜面高光的光照贴图,但需要更多的纹理,使用起来也更昂贵,并且在大多数情况下没有产生良好的效果。自Unity 5.6起,它们已被删除了。

项目文件下载地址:unitypackage

光探测器

光照贴图仅适用于静态对象,而不适用于动态对象。 因此,动态对象不适合带有烘烤光照的场景。当没有实时光源的时候,这是非常明显的。

 Unity 渲染教程(十六):静态光照

动态对象是显而易见的。为了更好地混合静态和动态对象,我们必须以某种方式将烘焙的光照应用于动态对象。为了解决这个问题,Unity有光探测器。 光探测器是对空间中的一个点包含该位置的光照信息。 不是用纹理,它是用球面谐波来存储这些信息。 如果可用的话,这些光探测器将用于动态对象,而不是全局环境数据。所以我们要做的就是创建一些光探测器,等到烘烤的时候,我们的着色器就会自动使用它们。

创建光探测器组

通过GameObject / Light /Light Probe Group将一组光探测器添加到场景中。 这将创建一个新的游戏对象,在立方体的形状中共有八个光探测器。 它们将在渲染动态对象的时候立即使用。

 Unity 渲染教程(十六):静态光照

一个新的光探测器组。

通过它的检视器,你可以在启用“编辑光探测器”模式后编辑光探测器组。 启用以后,你可以选择单个探针,并在场景视图中移动它们,或通过检视器进行调整。你可以操作,复制和删除单个探测器,就像它们是游戏对象一样。

 

 Unity 渲染教程(十六):静态光照

光探测器组的检视器。

你不必显式的启用编辑模式。 在场景视图中选择组就足以开始编辑探测器。 要停止编辑它们,请取消选择该组。

放置光探测器

光探测器组将其包围的体积分成四个区域。四个探测器定义了四面体的角。 这些探测器被进行插值以确定用于动态物体的最终球谐函数,这取决于其在四面体内的位置。这意味着动态对象被视为一个单一的点,因此这种方法只对相当小的对象有效。

在编辑探测器的时候,会自动生成四面体。 你不需要知道他们的配置,但它们的可视化信息可以帮助你查看探测器的相对位置。

放置光探测器需要你去调整他们的位置,直到你得到一个你可以接受的结果,就像光照贴图的设置一样。首先封装将要包含动态对象的区域。

 Unity 渲染教程(十六):静态光照

封装区域。

然后根据光照条件如何变化来添加更多的探测器。你不必将它们放置在静态几何中。 也不要把它们放在不透明的单面几何体错误的那一面。

 Unity 渲染教程(十六):静态光照

放置更多的探测器。

继续添加和移动探测器,直到你在所有区域都有了合理的光照条件,并且在它们之间发生的转换是可以接受的。

 Unity 渲染教程(十六):静态光照

调整探测器的位置。

你可以通过移动动态对象来测试探测器。当选择一个动态对象的时候,也会显示当前正在发挥作用的探测器。探测器将显示其光照,而不仅仅是黄色球体。你还可以看到用于动态对象的内插数据。

通过光探测器组来移动动态对象。

下一个教程将转向更加先进的混合烘培和动态光照的方法。。它将于20176月发布。

工程文件下载地址:unitypackage

PDF下载地址:PDF


【版权声明】

原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。

分类: 标签: Renderin
举报 分享

想免费获取内部独家PPT资料库? 观看行业大牛直播?

立即扫码加群

评论(3)

3个评论

GAD译馆

更多