当前位置: 首页 > news >正文

Custom SRP - Baked Light

https://catlikecoding.com/unity/tutorials/custom-srp/baked-light/

本篇教程介绍将静态光照烘焙到 light map 和 light prob 中.

首先贴上我遇到的问题,希望遇到的同学帮忙解答:

实践本教程过程中,定义的 MetaPass 没有效果, Unity 始终在使用默认的 meta pass,我使用的是 unity2022.3.14

1 Baking Satic Light

现在的光照都是在运行时实时计算的,除此以外,还可以离线计算光照并存储到 light map / probe 中.有两个原因使用离线方式:1.降低运行时到计算量,2.支持不能实时计算的间接光照.后者也是大家熟知的全局光照:光不是直接来自光源,而是来自间接反射,环境,或自发光表面.

烘焙光照的缺点是运行时不可以改变,而且增加了包体大小和内存占用.

1.1 Scene Lighting Settings

全局光照通过 Lighting 窗口的 Scene 面板,对每个场景进行配置.通过 Mixed Lighting/Baked Global Illumination 选项开关启用烘焙光照,并将 Lighting Mode 选项配置为 Baked Indirect,烘焙静态间接光.

再下面是 Lightmapping Settings 区域,来配置烘焙过程,大部分用默认值,除了

  • Lightmap Resolution 降低到 20

  • Compress Lightmaps 禁用

  • Directional Mode 设置为 Non-Directional

  • 启用 Progressive GPU lightmapper

1.2 Static Object

为了展示烘焙光照,需要创建一个场景,包含了一个车库,以及一些简单几何体.这些集合体有的在车库内,有的在外.

同时该场景只有一个方向光,并且将 Mode 设置为 Mixed, 即混合模式, Unity 会将该光源的间接光烘焙的 light map 中,直接光照依然是实时计算的.

将包括构成车库在内的所有的立方体/长方体,配置为参与到烘焙过程中,光线将会从这些对象的表面反弹,形成间接光.通过启用 MeshRenderer 上的 Contribute Global Illumination 选项开关,来完成配置.开启该选项会自动切换 Receive Gobal Illumination 模式为 Light Map,意味着到达该表面的间接光会被烘焙到 Light Map 中.也可以通过对象 Static 下拉列表中,启用 Contribute GI 来启用 bake.

如果 Lighting 面板的 Auto Generate 选项开启,开启对象的烘焙后,场景会马上开始烘焙,否则就需要按下 Generate Lighting 按钮启用烘焙.Lightmapping 设置在 MeshRenderer 上也会显示,也会显示包含该对象的 lightmap.

烘焙的光偏蓝,是受到了天空盒的影响.天空盒代表了环境的天空的间接光照.靠近建筑的地方更亮一些,是因为这里有很多几何体,光在地面和这些几何体之间发生反弹,产生了间接光.

1.3 Fully-Baked Light

也可以将方向光的光照模式,从 Mixed 改为 Baked,那该光源不再参与实时光照,直接光,间接光全都烘焙到 light map 上了.我们的材质现在还没有采样 light map,因此上全黑的.

实际上,直接光照也被当作间接光照一样处理,因此 light map 更亮了.

2 Sampling Baked Light

2.1 Global Illumination

创建 ShaderLibrary/GI.hlsl,GI相关的代码都会放到该文件.定义 GI 结构体,并定义 GetGI 函数,接受一个 light map UV坐标,返回该结构体.间接光是从所有方向来的光,所以是 diffuse ,没有 specular,因此为 GI 增加 diffuse 成员.目前我们先把 UV 作为颜色返回.

#ifndef GI_INCLUDED
#define GI_INCLUDEDstruct GI
{float3 diffuse;
};GI GetGI(float2 lightmapUV)
{GI gi;gi.diffuse = float3(lightmapUV.x, lightmapUV.y, 0);return gi;
}#endif

specular GI 通常是通过 reflection probes 来提供的,后面会介绍

给 GetLighting 函数增加 GI 参数,在color累加光照前,用 GI.diffuse 来初始化 color.现在还不需要用 GI.diffuse * surface.diffuse,这样可以观察到接受到的 light map 光照.

float3 GetLighting(Surface surfaceWS, BRDF brdf, GI gi)
{ShadowData shadowData = GetShadowData(surfaceWS);float3 color = gi.diffuse;...
}

2.2 Light map coordinates

通过 DrawSettings 的参数,通知 unity 将 light map UV 作为顶点数据传到 GPU. 后面我们需要 light probe 等数据时,可以通过为 perObjectData 或上更多选项标记来实现

drawingSettings.perObjectData = PerObjectData.Lightmaps;

Unity 将多个物体的 lightmap 烘焙到同一张 lightmap 上,而mesh的lightmap uv 是模型独自空间里的,因此其顶点传过来的UV,需要根据偏移和缩放,计算在 lightmap 上的 UV.这些参数通过 UnityPerDraw 来接收

CBUFFER_START(UnityPerDraw)
...
float4 unity_LightmapST;           // 当前版本使用该参数来变换 lightmap uv
float4 unity_DynamicLightmapST;    // 该参数被废弃,但是必须保留否则出错
CBUFFER_END

材质需要支持 lightmap 和 realtime lighting,因此在 Lit.shader 中需要为其定义 keywords:

#pragma multi_compile _ _LIGHTMAP_ON

Lightmpa uv 是顶点数据,并且需要被传递到像素着色器,因此需要为 Attributes 和 Varyings 增加 UV 参数.由于同时要支持实时光照,因此我们定义一些宏来区分.GI.hlsl

#if defined(LIGHTMAP_ON)#define GI_ATTRIBUTE_DATA float2 lightMapUV : TEXCOORD1;#define GI_VARYINGS_DATA float2 lightMapUV : VAR_LIGHT_MAP_UV;#define TRANSFER_GI_DATA(input, output) output.lightMapUV = input.lightMapUV;#define TRANSFER_GI_DATA(input, output) \output.lightMapUV = input.lightMapUV * \unity_LightmapST.xy + unity_LightmapST.zw;
#else#define GI_ATTRIBUTE_DATA#define GI_VARYINGS_DATA#define TRANSFER_GI_DATA(input, output)#define GI_FRAGMENT_DATA(input) 0.0
#endif

然后修改 Attributes Varyings,并在像素着色器中,取出 lightmap uv 作为参数获取 GI 数据

...
#include "../ShaderLibrary/GI.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"
...
struct Attributes
{...GI_ATTRIBUTE_DATAUNITY_VERTEX_INPUT_INSTANCE_ID
};struct Varyings
{...GI_VARYINGS_DATAUNITY_VERTEX_INPUT_INSTANCE_ID
};float4 LitPassFragment(Varyings input) : SV_TARGET
{...GI gi = GetGI(GI_FRAGMENT_DATA(input));float3 color = GetLighting(surface, brdf, gi);return float4(color, surface.alpha);
}...

如图可以看到,参与gi的立方体对象显示了它们的LightMap up,其它的都是黑色。

2.3 Sampling LightMap

unity 中lightmap 及其采样器为 unity_Lightmap 和 samplerunity_Lightmap。

同时包含 Core RP library 中的 EntityLighting.hlsl,我们需要利用其返回 light data。

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/EntityLighting.hlsl"TEXTURE2D(unity_Lightmap);
SAMPLER(samplerunity_Lightmap);

通过SampleSingleLightmap来采样Lightmap

  • 第一二两个参数是贴图及其采样器,我们用TEXTURE2D_ARGS来传递

  • 第三个参数是 Lightmap uv

  • 第四个参数是 uv 变换参数,我们已经处理了变换,因此,实参为1,1,0,0

  • 第五个参数是 lightmap 是否压缩了.如果没有定义 UNITY_LIGHTMAP_FULL_HDR,则会压缩

  • 最后一个参数是 float4 包含了解码指令. 指定 x 分量为 LIGHTMAP_HDR_MULTIPLIER, y 分量为 LIGHTMAP_HDR_EXPONENT,其它设置为 0.

定义SampleLightMap函数,如果启用Lightmap就采样,否则返回0

float3 SampleLightMap (float2 lightMapUV) 
{#if defined(LIGHTMAP_ON)#if defined(UNITY_LIGHTMAP_FULL_HDR)bool compressed = false;#elsebool compressed = true;#endifreturn SampleSingleLightmap(TEXTURE2D_ARGS(unity_Lightmap, samplerunity_Lightmap),lightMapUv, float4(1.0,1.0,0.0,0.0),compressed,float4(LIGHTMAP_HDR_MULTIPLIER, LIGHTMAP_HDR_EXPONENT, 0.0, 0.0));
#elsereturn 0.0;
#endif
}GI GetGI (float2 lightMapUV) 
{GI gi;gi.diffuse = SampleLightMap(lightMapUV);return gi;
}

可以看到场景被 lightmap 照亮了

2.5 Disabling Environment Lighting

烘焙的光照很亮,这是因为光照包含了天空盒提供的天光.在 Lighting 窗口的 Environment 页签,可以把天空盒强度设置为0,去掉天光的影响.

可以看到,车库内部也被主要来自地面的间接光照亮了.

3 Light Probes

动态对象不参与烘焙GI,但是可以通过 light probes 使其受GI的影响。light probe 是场景中的一个点,通过三阶多项式,尤其是球面谐波近似,将入射光烘焙下来。light probes 摆放到场景中,unity针对每个对象选择light probe并进行插值,近似像素位置的光照。

3.1 light probe group

通过game object/light/ light probe group 向场景中添加 light probe。将会创建一个拥有 LightProbeGroup组件的game object,并且默认包含了6个 probes。由于它们也是 game object,所以当开启 Edit Light Probe Positions时,可以像普通对象一样移动,新建,删除.

场景中可以有多个 Light Probe Group,Unity会把所有的 Probe 收集起来,并创建连接他们的四面体,每个动态对象最终会在一个四面体中.基于顶点上的4个 probe 进行差值,最终得到物体的光照.如果对象在所有四面体之外,那么就会照一个距离最近的三角面来插值,因此效果会有点怪.

默认情况下,选中一个动态对象后,其 Gizmos 会显示影响它的 Probes,以及在对象位置处的插值结果.

通过 Lighting 窗口 Scene 页签下的 Light Probe Visualization 来改变默认行为

Light Probe 的放置依赖场景.

  • 首先,只需要放到动态物体会去的地方

  • 第二,放到光线发生变化的地方.每个 Probe 都是插值点,因此放到要放到光照变化的地方.

  • 第三,不要放到被烘焙的对象内部,否则它就是黑色的

  • 最后,对象都会进行插值,因此如果光照在墙的两侧不同,那么在墙的两侧放置 Probe.这样,对象不会在两侧进行插值(要么在墙外的四面体内,要么在墙内的四面体内).

3.2 Sampling Probes

每个对象插值的 light probe 数据需要提交给 GPU,也是通过 drawingSettings 参数来完成的.

drawingSettings.perObjectData = PerObjectData.Lightmaps | PerObjectData.LightProbe;

UnityPerDraw 需要7个 float4,用来表示四面体的红,绿,蓝光.

CBUFFER_START(UnityPerDraw)
...
float4 unity_SHAr;
float4 unity_SHAg;
float4 unity_SHAb;
float4 unity_SHBr;
float4 unity_SHBg;
float4 unity_SHBb;
float4 unity_SHC;
CBUFFER_END

在 GI.hlsl 中,创建一个新的函数 SampleLightProbe 来完成 Probe 的采样.该函数需要一个方向,因此为其声明世界空间的 surface 参数.

如果这个对象启用了 lightmap,就返回0.否则返回 SampleSH9,同时保证大于等于0.该函数需要 probe data 和 法线作为参数. probe data 需要以系数数组的形式提供.

同时为 GetGI 也添加 surface 参数,并调用 SampleLightProbe,将结果累加到 gi.diffuse 上.

float3 SampleLightProbe(Surface surfaceWS)
{
#if defined(LIGHTMAP_ON)return 0.0;
#elsefloat4 coefficients[7];coefficients[0] = unity_SHAr;coefficients[1] = unity_SHAg;coefficients[2] = unity_SHAb;coefficients[3] = unity_SHBr;coefficients[4] = unity_SHBg;coefficients[5] = unity_SHBb;coefficients[6] = unity_SHC;return max(0.0, SampleSH9(coefficients, surfaceWS.normal));
#endif
}GI GetGI (float2 lightMapUV, Surface surfaceWS) 
{GI gi;gi.diffuse = SampleLightMap(lightMapUV) + SampleLightProbe(surfaceWS);return gi;
}

像素着色器中调用 GetGI 的地方也要修改:

float4 LitPassFragment(Varyings input) : SV_TARGET
{...GI gi = GetGI(GI_FRAGMENT_DATA(input), surface);float3 color = GetLighting(surface, brdf, gi);return float4(color, surface.alpha);
}

可以看到,动态物体(所有的小球),都被照亮了,同时车库内的更暗些.

3.3 Light Probe Proxy Volumes

Light Probe 对于那些比较小的物体比较适合.对于那些比较大的物体,由于 Light Probe 是基于单一位置计算 Light Probe Data 的,因此效果会不正确,比如上图中的长条物体,其车库外面的部分同车库内,使用了同样的光照.

通过使用 light probe proxy volume 即 LPPV 来解决该问题,为该对象添加一个 LPPV 组件,并将其 Renderer 组件的 Probe Mode 改为 Use Proxy Volume

有多种方式可以配置 Volume,这里我们用 custom resolution mode 来沿着边缘放置 sub-probes

3.4 Sampling LPPV

采样 LPPV 同样需要向GPU提交数据,还是 DrawingSettings,这次是 PerObjectData.LightProbeProxyVolume

drawingSettings.perObjectData = PerObjectData.Lightmaps | PerObjectData.LightProbe| PerObjectData.LightProbeProxyVolume;

同样,向 UnityPerDraw 中添加成员来接收数据

CBUFFER_START(UnityPerDraw)
...
float4 unity_ProbeVolumeParams;            // light probe proxy volume 参数
float4x4 unity_ProbeVolumeWorldToObject;
float4 unity_ProbeVolumeSizeInv;
float4 unity_ProbeVolumeMin;
CBUFFER_END

Volumes 数据存储在一张 3D float 贴图中: unity_ProbeVolumeSH, 在 GI.hlsl 中,通过 TEXTURE3D_FLOAT 声明贴图,同时声明采样器

TEXTURE3D_FLOAT(unity_ProbeVolumeSH);
SAMPLER(samplerunity_ProbeVolumeSH);

使用 light probe 还是 LPPV 由 unity_ProbeVolumeParams 的 x 分量确定.如果不是0,就利用 SampleProbeVolumeSH4 来采样 LPPV:

  • 首先传入贴图和采样器,还是用一个宏来包装:TEXTURE3D_ARGS

  • 然后是像素的世界空间位置及法线

  • 变换矩阵,即 UnityPerDraw 的 unity_ProbeVolumeWorldToObject 

  • 其它 UnityPerDraw 传入的参数,使用方式见下面的代码

    float3 SampleLightProbe(Surface surfaceWS)
    {
    #if defined(LIGHTMAP_ON)return 0.0;
    #elseif (unity_ProbeVolumeParams.x) {return SampleProbeVolumeSH4(TEXTURE3D_ARGS(unity_ProbeVolumeSH, samplerunity_ProbeVolumeSH),surfaceWS.position, surfaceWS.normal,unity_ProbeVolumeWorldToObject,unity_ProbeVolumeParams.y, unity_ProbeVolumeParams.z,unity_ProbeVolumeMin.xyz, unity_ProbeVolumeSizeInv.xyz);}else{float4 coefficients[7];coefficients[0] = unity_SHAr;coefficients[1] = unity_SHAg;coefficients[2] = unity_SHAb;coefficients[3] = unity_SHBr;coefficients[4] = unity_SHBg;coefficients[5] = unity_SHBb;coefficients[6] = unity_SHC;return max(0.0, SampleSH9(coefficients, surfaceWS.normal));}
    #endif
    }

    现在,可以看到长条对象,在车库内的部分是暗的,外面是亮的(不明显是因为我的场景布置不合理)

采样 LPPV 需要变换到 Volume 空间,还需要一些其它的计算,需要采样 volume texture,以及应用球协.该例子中应用了 L1 球协,因此精度没有那么高,但是已经在一个对象上体现出了光照的差异.

4 Meta Pass

间接漫反射光是从表面反弹出来的,因此应该受到表面反射率的影响,目前还没有处理.unity 使用特殊的 meta pass 来确定烘焙的反射光.由于我们没有定义,因此 unity 使用默认的 meta pass,其返回白色.

4.1 Unified Input

MetaPass 也需要和 LitPass 一样的UnityPerMaterial,以及基础贴图和采样器,因此从 LitPass.hlsl 中提取出来,放到 LitInput.hlsl 中,让这两个 pass 共享.

同时我们定义了一系列函数,将 instance 相关的代码隐藏起来,这些函数都以 float2 uv 作为参数,但是有些函数可能不会用到.

#ifndef CUSTOM_LIT_INPUT_INCLUDED
#define CUSTOM_LIT_INPUT_INCLUDEDUNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float4 _BaseMap_ST;
float _Cutoff;
float _Metallic;
float _Smoothness;
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);float2 TransformBaseUV(float2 baseUV)
{float4 baseST = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseMap_ST);return baseUV * baseST.xy + baseST.zw;
}float4 GetBase(float2 baseUV)
{float4 map = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap);float4 color = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);return map * color;
}float GetCutoff (float2 baseUV) 
{return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff);
}float GetMetallic (float2 baseUV) 
{return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Metallic);
}float GetSmoothness (float2 baseUV) 
{return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Smoothness);
}#endif

在.shader subpass 开始的地方,在HLSLINCLUDE ENDHLSL 之间,包含 common.hlsl , lit input.hlsl ,这样所有pass都会首先包含这两个文件。

HLSLINCLUDE
#include "../ShaderLibrary/Common.hlsl"
#include "LitInput.hlsl"
ENDHLSL

移除litpass.hlsl 中重复的包含和 buffer texture sampler 定义,并将相关代码替换成在 lit input.hlsl中定义的函数。

对ShadowCasterPass做同样的处理。

4.2 Unlit

unlit 相关的材质文件也做同样的处理。

复制lit input 并改名为 unlit input.hlsl,移除金属度和光滑度,修改对应函数返回0,然后在unlit.shader中包含文件。

4.3 Meta Light Mode

向 Lit Unlit shader 中添加新 pass,设置其 light mode 为 meta。cull mode 为 off。不需要 multi compile 指令。

Pass
{Name "META"Tags { "LightMode" = "Meta" }Cull OffHLSLPROGRAM#pragam target 3.5#pragam vertex MetaPassVertex#pragma fragment MetaPassFragment#include "MetaPass.hlsl"ENDHLSL
}

下面是 MetaPass.hlsl 代码

#ifndef CUSTOM_META_PASS_INCLUDED
#define CUSTOM_META_PASS_INCLUDED// BRDF 及其依赖的头文件
#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Shadow.hlsl"
#include "../ShaderLibrary/Light.hlsl"
#include "../ShaderLibrary/BRDF.hlsl"struct Attributes {float3 positionOS : POSITION;float2 baseUV : TEXCOORD0;float2 lightMapUV : TEXCOORD1;    // 需要 light map uv
};struct Varyings {float4 positionCS : SV_POSITION;float2 baseUV : VAR_BASE_UV;
};// meta pass 控制参数,用来告诉 shader 要生成什么数据
// x :生成漫反射反射率
// y :
bool4 unity_MetaFragmentControl;
// unity 对计算结果进行放大的参数
float unity_OneOverOutputBoost;
// 限制最大值
float unity_MaxOutputValue;Varyings MetaPassVertex(Attributes input) 
{Varyings output;// 对象空间位置的 XY 坐标,由 light map uv 计算得来input.positionOS.xy =input.lightMapUV * unity_LightmapST.xy + unity_LightmapST.zw;// 必须在代码中显式访问 Z,否则 OpenGL 不渲染input.positionOS.z = input.positionOS.z > 0.0 ? FLT_MIN : 0.0;output.positionCS = TransformWorldToHClip(input.positionOS);output.baseUV = TransformBaseUV(input.baseUV);return output;
}float4 MetaPassFragment(Varyings input) : SV_TARGET {float4 base = GetBase(input.baseUV);Surface surface;ZERO_INITIALIZE(Surface, surface);surface.color = base.rgb;surface.metallic = GetMetallic(input.baseUV);surface.smoothness = GetSmoothness(input.baseUV);BRDF brdf = GetBRDF(surface);float4 meta = 0.0;if (unity_MetaFragmentControl.x) {// 返回 BRDF 的漫反射meta = float4(brdf.diffuse, 1.0);// 配合 unity boost,对结果预处理一下meta.rgb += brdf.specular * brdf.roughness * 0.5;// unity boostmeta.rgb = min(PositivePow(meta.rgb, unity_OneOverOutputBoost), unity_MaxOutputValue);}return meta;
}#endif

在 Lighting.hlsl 的 GetLighting 中,累加间接和实时漫反射

float3 GetLighting(Surface surfaceWS, BRDF brdf, GI gi)
{ShadowData shadowData = GetShadowData(surfaceWS);float3 color = gi.diffuse * brdf.diffuse;for(int i = 0; i < GetDirectionalLightCount(); ++i){Light light = GetDirectionalLight(i, surfaceWS, shadowData);color += GetLighting(surfaceWS, brdf, light);}return color;
}

备注:在我自己的实践中, MetaPass 始终没生效, Unity 总是使用默认的返回白色的漫反射,如果谁知道怎么解决该问题,还请留言.

最后,设置光的模式为混合模式

5 Emissive Surfaces 

自发光可以简单的在光照计算的最后,叠加上一个颜色来实现.因为不是真正的光源,因此不会影响其它表面,但是他们可以在 lightmap 中贡献烘焙光.

5.1 Emitted Light

自发光需要增加两个材质属性:自发光贴图和颜色.

自发光贴图上 ST,使用与 base map 相同的值,因此增加 [NoScaleOffset] 来告诉 unity 不为其定义/上传数据.

为来支持更亮的自发光,需要定义 HDR 颜色,通过 [HDR] 修饰属性来实现,这允许设置超过1的颜色值.

emissive map 我们使用默认的粒子贴图

_BaseMap("Texture", 2D) = "white" {}
[NoScaleOffset]_EmissionMap("Emission", 2D) = "white"{}
[HDR]_EmissionColor("Emission", Color) = (0.0,0.0,0.0,0.0)

在 LitInput.hlsl 中,增加自发光贴图变量,并为材质数据增加自发光颜色.同时实现 GetEmission 函数:

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
...
float4 _EmissionColor;
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)TEXTURE2D(_BaseMap);
TEXTURE2D(_EmissionMap);
SAMPLER(sampler_BaseMap);float3 GetEmission(float2 baseUV)
{float4 map = SAMPLE_TEXTURE2D(_EmissionMap, sampler_BaseMap, baseUV);float4 color = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _EmissionColor);return map.rgb * color.rgb;
}

在 LitPass.hlsl 中,像素着色器最后,返回之间,调用 GetEmission 并将颜色叠加上去

float4 LitPassFragment(Varyings input) : SV_TARGET
{float3 color = GetLighting(surface, brdf, gi);color += GetEmission(input.uv);return float4(color, surface.alpha);
}

对于 Unlit.hlsl 我们的 GetEmission 只是简单的调用 GetBase.

最后,不要忘记我们的 PerObjectMaterialProperties 脚本,缓存 shader property id,定义自发光颜色.由于要支持HDR,因此用到了 ColorUsage 语义,有两个参数:第一个指示是否显示 alpha 通道,我们不需要因此是 false,第二个是否支持 HDR,因此是 true.

static int emissionColorId = Shader.PropertyToID("_EmissionColor");
// ColorUsage(show alpha channel, use hdr)
[SerializeField, ColorUsage(false, true)]
private Color emissioncolor = Color.black;private void OnValidate()
{...matPropBlock.SetColor(emissionColorId, emissioncolor);GetComponent<Renderer>().SetPropertyBlock(matPropBlock);
}

5.2 Baked Emission

自发光通过单独的 pass 烘焙,即 unity_MetaFragmentControl.y 是 true 时烘焙自发光

if (unity_MetaFragmentControl.x) 
{...
}
else if(unity_MetaFragmentControl.y)
{meta = float4(GetEmission(input.uv), 1.0);
}

要烘焙自发光,需要为材质显示指定烘焙自发光.通过调用 MaterialEditor.LightmapEmissionProperty().因此需要在OnGUI 中调用.

MaterialEditor.LightmapEmissionProperty() 仅仅是在材质面板上显示选项,并初始设置为 None.尽管名字跟 Emission 无关(Global Illumination) ,但选择 Baked 确实是告诉 unity 用独立的 pass 来烘焙自发光.另外的 Realtime 选项已经废弃了,不用管它.

这些依然不够,Unity 在烘焙时倾向于避免单独的 emission pass.如果材质的 emission 是 0, 那么就会忽略,但这没有考虑每个对象的材质属相.我们可以在 emission mode 改变时,去掉选择的材质的 globalIlluminationFlags 的 MaterialGlobalIlluminationFlags.EmissiveIsBlack 来解决

public override void OnGUI(MaterialEditor matEditor, MaterialProperty[] props)
{...properties = props;BakedEmission();...
}void BakedEmission()
{EditorGUI.BeginChangeCheck();materialEditor.LightmapEmissionProperty();if (EditorGUI.EndChangeCheck()){foreach (Material material in materialEditor.targets)material.globalIlluminationFlags &= ~MaterialGlobalIlluminationFlags.EmissiveIsBlack;}
}

6 Baked Transparency

半透明对象也可以烘焙,不过现在的效果还不对,需要增加些内容.如下图,半透明的屋顶,被当作不透明来烘焙的

Unity 是通过一种硬编码的方式来处理半透明材质的,通过 Queue 队列来判断材质是 opaque, clipped, transparent 的,然后乘上贴图以及颜色的 alpha 的到透明度, clipping 则使用 _Cutoff.目前我们的 shader 处理了第三种情况,前两种还没有处理.

在材质上增加 _MainText _MainColor 两个属性,并用[HideInInspector]修饰,这两个属性不需要在材质面板上编辑,我们在材质编辑器代码中复制 _BaseMap _BaseColor 的属性.

在 Lit.shader 中

_Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5
[HideInInspector]_MainTex("Testure for lightmap", 2D) = "white"{}
[HideInInspector]_Color("Color for lightmap", Color) = (1.0,1.0,1.0,1.0)

在 CustomShaderGUI.cs 中,复制材质属性

public override void OnGUI(MaterialEditor matEditor, MaterialProperty[] props)
{...if (EditorGUI.EndChangeCheck()){// 如果有修改,则更新阴影通道SetShadowCasterPass();CopyLightmappingProperties();}
}void CopyLightmappingProperties()
{MaterialProperty propMainTex = FindProperty("_MainTex", properties, false);MaterialProperty propBaseTex  = FindProperty("_BaseMap", properties, false);if (propMainTex != null && propBaseTex != null){propMainTex.textureValue = propBaseTex.textureValue;propMainTex.textureScaleAndOffset  = propBaseTex.textureScaleAndOffset;}MaterialProperty propColor = FindProperty("_Color", properties, false);MaterialProperty propBaseColor = FindProperty("_BaseColor", properties, false);if (propColor != null && propBaseColor != null){propColor.colorValue = propBaseColor.colorValue;}
}

如下图,半透明已经烘焙正确了

7 Mesh Ball

接下来我们要让由 MeshBall 脚本生成的 instance ball 支持全局光照.由于 balls 是在运行时生成的,不能参与到烘焙中,但是可以让他们受到 light probes 的影响.

7.1 Light Probes

通过另一个 DrawMeshInstanced 接口来指示使用 light probes,这个接口需要5个额外的参数:

  • 第一个(第六个)是阴影投射模式,我们设置成 On

  • 然后是 layer 使用默认值 0

  • 第三个是指定渲染的摄像机,传入 null 表示他们需要被所有摄像机渲染

  • 最后设置 light probe mode,只能使用 LightProbeUsage.CustomProvided,因为light probes 是通过材质数据上传的,而不是实例自身.

void Update()
{Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 1023, matPropBlock,ShadowCastingMode.On, true, 0, null, LightProbeUsage.CustomProvided);
}

然后,需要为所有实例手动生成插值 light probes,并添加到 material property block 中

private void Awake()
{...// 生成 interpolated light probes 数据并拷贝到 material property block// 首先获得实例的位置var positions = new Vector3[1023];for (int i = 0; i < 1023; i++){positions[i] = matrices[i].GetColumn(3);}// 生成 interpolated light probesvar lightProbes = new SphericalHarmonicsL2[1023];// 参数1: 位置熟知// 参数2: 返回 SphericalHarmonicsL2 数据// 参数3: 返回 Occlusion 数据,这里不需要,传 nullLightProbes.CalculateInterpolatedLightAndOcclusionProbes(positions, lightProbes, null);// 将数据拷贝到 material property blockmatPropBlock.CopySHCoefficientArraysFrom(lightProbes);
}

7.2 LPPV

另一种方式是通过 LPPV 实现 instance 的 GI, 这需要将场景中的 instance 放置到一个紧密的空间.这种方式让我们不必进行上面那些计算,同时可以实时改变 instance 的位置,而不需要每帧计算 interpolated light probes 并上传,前提是他们始终在我们定义的 LPPV 内部.

为 MeshBall.cs 增加一个 LightProbeProxyVolume 成员,如果该成员被使用了,就不用计算 light probes 数据了.然后调整 DrawMeshInstanced 参数:

[SerializeField] LightProbeProxyVolume lightProbeProxyVolume = null;private void Awake()
{...if (lightProbeProxyVolume == null){// 生成 interpolated light probes 数据并拷贝到 material property block...}}void Update()
{LightProbeUsage probeUsage = lightProbeProxyVolume ? LightProbeUsage.UseProxyVolume : LightProbeUsage.CustomProvided;Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 1023, matPropBlock,ShadowCastingMode.On, true, 0, null, probeUsage, lightProbeProxyVolume);
}

然后,为 MeshBall 所在的对象添加 LPPV 组件,并配置 Bounding Box Mode 为 Custom,以定义世界空间的区域.并将其赋值给 MeshBall 组件.

http://www.lryc.cn/news/622876.html

相关文章:

  • 用Pygame开发桌面小游戏:从入门到发布
  • 搜索 AI 搜索 概率论基础教程第3章条件概率与独立性(二)
  • 概率论基础教程第3章条件概率与独立性(一)
  • 《P4180 [BJWC2010] 严格次小生成树》
  • [极客时间]LangChain 实战课 ----- 代理(上)|(12)ReAct框架,推理与行动的协同
  • Manus AI与多语言手写识别的技术突破与行业变革
  • 《Python学习之字典(一):基础操作与核心用法》
  • 【每日一题】Day5
  • 电路设计——复位电路
  • 设计模式之静态代理
  • Java 10 新特性及具体应用
  • ABB焊接机器人弧焊省气
  • 多机编队——(6)解决机器人跟踪过程中mpc控制转圈问题
  • 【轨物方案】预防性运维:轨物科技用AI+机器人重塑光伏电站价值链
  • MyBatis极速通关中篇:核心配置精讲与复杂查询实战
  • 大模型教机器人叠衣服:2025年”语言理解+多模态融合“的智能新篇
  • Tomcat架构深度解析:从Server到Servlet的全流程揭秘
  • blender制作动画导入unity两种方式
  • ENSP的简单动态路由rip协议配置
  • 广东省省考备考(第七十八天8.16)——资料分析、判断推理(强化训练)
  • Docker目录的迁移
  • GaussDB 数据库架构师修炼(十三)安全管理(3)-行级访问控制
  • 6JSON格式转python并实现数据可视化
  • 在ubuntu系统上离线安装jenkins的做法
  • 零基础学习人工智能的完整路线规划
  • Flink Stream API 源码走读 - window 和 sum
  • (第十七期)HTML图像标签详解:从入门到精通
  • 【完整源码+数据集+部署教程】高尔夫球追踪与识别系统源码和数据集:改进yolo11-LAWDS
  • 【基础-判断】可以通过ohpm uninstall 指令下载指定的三方库
  • 力扣(接雨水)——基于最高柱分割的双指针