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.pswp.cn/web/93343.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/93343.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[Python]PTA:实验2-3-1-for 求1到100的和

本题要求编写程序&#xff0c;计算表达式 1 2 3 ... 100 的值。输入格式&#xff1a;本题无输入。输出格式&#xff1a;按照以下格式输出&#xff1a;sum 累加和代码如下&#xff1a;x0 for i in range(1,101,1):xi print("sum {}".format(x))

【解决笔记】MyBatis-Plus 中无 selectList 方法

MyBatis-Plus 中无 selectList 方法的解决笔记 核心前提 MyBatis-Plus 的 BaseMapper 接口内置了 selectList 等基础查询方法&#xff0c;继承该接口可直接使用&#xff0c;无需手动实现。 无 selectList 方法的两种情况及解决方式 1. 未继承 BaseMapper&#xff08;推荐方案&a…

一周学会Matplotlib3 Python 数据可视化-绘制箱线图(Box)

锋哥原创的Matplotlib3 Python数据可视化视频教程&#xff1a; 2026版 Matplotlib3 Python 数据可视化 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 课程介绍 本课程讲解利用python进行数据可视化 科研绘图-Matplotlib&#xff0c;学习Matplotlib图形参数基本设置&…

4.4 vue3生命周期函数

vue3生命周期函数生命周期钩子名称对比表阶段Vue 2 选项式 APIVue 3 组合式 API说明创建前beforeCreateonBeforeCreate&#xff08;已废弃&#xff09;Vue 3 中 setup() 替代创建完成createdsetup()&#xff08;替代&#xff09;setup 是入口&#xff0c;代替 beforeCreate 和 …

无脑整合springboot2.7+nacos2.2.3+dubbo3.2.9实现远程调用及配置中心

简介&#xff1a; 好久没有写博客了&#xff0c;最近辞职了有时间进行一次分享&#xff0c;今天我们主要是使用单体服务springboot整合nacos实现配置中心&#xff0c;然后整合dubbo来实现远程的rpc调用。如下是本地案例架构图&#xff0c;生产者和消费者的配置在nacos配置中心上…

腾讯位置商业授权微信小程序逆地址解析(坐标位置描述)

微信小程序JavaScript SDK 开发指南 逆地址解析(坐标位置描述) reverseGeocoder(options:Object) 本接口提供由坐标到坐标所在位置的文字描述的转换&#xff0c;输入坐标返回地理位置信息和附近poi列表。 注&#xff1a;坐标系采用gcj02坐标系 options属性说明 属性类型必填…

3D商品展示:技术狂欢下的普及困局

当微软推出Copilot 3D——仅需一张照片即可生成可编辑的3D模型时&#xff0c;业界曾欢呼“建模门槛彻底消失”。然而技术的美好愿景却撞上现实的铜墙铁壁&#xff1a;当前电商平台3D商品加载卡顿导致用户跳出率超60%&#xff0c;企业3D化渗透率仍不足34%。绚烂的技术烟花下&…

(Arxiv-2025)Stand-In:一种轻量化、即插即用的身份控制方法用于视频生成

Stand-In&#xff1a;一种轻量化、即插即用的身份控制方法用于视频生成 paper是WeChat发布在Arxiv 2025的工作 paper title:Stand-In: A Lightweight and Plug-and-Play Identity Control for Video Generation Code&#xff1a;链接 图1&#xff1a;给定一张参考图像&#xff…

数据科学与爬虫技术学习笔记

数据科学与爬虫技术学习笔记 一、数据科学基础库 1. NumPy&#xff1a;数值计算的基石 NumPy 是 Python 科学计算的核心库&#xff0c;专为数组和矩阵操作设计&#xff0c;能大幅简化循环操作&#xff0c;提供丰富的数学函数。 核心优势&#xff1a;高效处理同类型元素的多维…

学习嵌入式之硬件——I2C

一、I2C1.定义内部集成电路的简称&#xff0c;半双工串行同步通信&#xff0c;是芯片和芯片之间的通信方式&#xff1b;通常只有一个主机&#xff0c;多个从机&#xff0c;采用主从应答的方式上图所示是IIC的总线的使用场景&#xff0c;所有挂载在IIC总线上的设备都有两根信号线…

使用websockt

封装websocktHooksimport { ref, onMounted, onUnmounted } from vue;/*** webSocket的Hooks* param {string} websocket链接地址* */ export function useWebSocket(url: string) {// 核心状态 const data: Ref<any> ref(null);//收到websocket返回的数据const socke…

Jmeter自定义脚本

目录 log&#xff1a;输出类 Label&#xff1a;你自定义的组件的名称 FileName&#xff1a;添加的脚本文件的文件名 Parameters&#xff1a;你传入的参数&#xff0c;是一个字符串 args&#xff1a;你传入的参数&#xff0c;是一个数组 Parameters和args的异同&#xff1…

飞算 JavaAI 电商零售场景实践:从订单峰值到供应链协同的全链路技术革新

目录 一、电商核心场景的技术攻坚 1.1 分布式订单系统的事务一致性设计 1.1.1 TCC 模式下的订单创建流程 1.1.2 订单状态机的可靠流转 1.2 高并发秒杀系统的架构设计 1.2.1 多级限流与流量削峰 1.2.2 库存防超卖机制 1.3 智能推荐与用户行为分析 1.3.1 用户行为实时采…

51单片机-51单片机介绍

51单片机介绍单片机简介什么是单片机呢&#xff1f;单片机是一种集成电路芯片&#xff0c;采用超大规模集成电路技术将中央处理器&#xff08;CPU&#xff09;、随机存储器&#xff08;RAM&#xff09;、只读存储器&#xff08;ROM&#xff09;、多种I/O口、中断系统、定时器/计…

8月AI面试工具测评:破解规模化招聘难题

金秋校招临近&#xff0c;企业面临“百万简历涌入VS面试官团队告急”的典型困境。传统线下面试效率低下、标准参差&#xff0c;难以应对短时间内爆发式的人才筛选需求。AI面试工具凭借自动化与智能化特性成为破局关键&#xff0c;但市面上产品良莠不齐——究竟哪款能兼顾效率与…

Debian新一代的APT软件源配置文件格式DEB822详解

Debian 的 DEB822 格式详解&#xff1a;新一代 APT 源配置 DEB822 是一种基于 RFC 822 数据格式的配置文件语法&#xff0c;Debian 新一代的 APT 软件源配置文件格式就采用了 DEB822。DEB822 格式从 Debian 11 (Bullseye) 开始被引入&#xff0c;并在 Debian 12 (Bookworm) 中成…

实战 AI8051U 音视频播放:USART-SPI→DMA-P2P→SPI+I2S 例程详解

视频P2P播放&#xff0c;时间计算&#xff1a;fps20,50ms 周期刷屏时间&#xff1a;160*80 一帧刷屏时间28.2ms帧间隔&#xff1a;50ms-28.2ms21.8ms音频双缓冲区交叉播放&#xff0c;利用视频播放帧间隔加载下一个缓冲区音频数据&#xff0c;时间计算&#xff1a;16000采样率 …

解释器模式C++

解释器模式&#xff08;Interpreter Pattern&#xff09;是一种行为型设计模式&#xff0c;它用于定义一种语言的语法规则&#xff0c;并构建一个解释器来解释该语言中的句子。这种模式适用于需要处理固定语法规则的场景&#xff0c;如表达式解析、配置文件解析等。 解释器模式…

debian 13 显示中文字体 不再显示菱形块 终端显示中文

找了很多坑。。其它就安装一下中文字体即可 。 apt install ttf-wqy-zenhei 之后测试命令 fc-list &#xff1a;langzh 显示了刚字体的路径和中文字即成功了。 rootdebian:~# dpkg-reconfigure locales 以上命令配置中文语言。 debian 12.11 安装 MySQL 下载配置文件 &am…

51单片机-驱动蜂鸣器模块教程

本章概述思维导图&#xff1a; 51单片机驱动蜂鸣器模块教程 蜂鸣器简介 蜂鸣器是一种将电信号转换为声音信号的电子元件&#xff0c;广泛应用于报警、提示、通知等场景。其核心原理基于压电效应或电磁感应&#xff1a;因此可分为两种类型蜂鸣器&#xff1a;压电式蜂鸣器和电磁…