Unity SRP 8.全域性照明
翻譯彙總文章:
原文連結:
間接光照
烘焙和取樣光照貼圖
展示非直接光
建立自發光材質
透過探頭和lppv(
Light Probe Proxy Volume
) 取樣燈光
支援預計算實時全域性照明
這是Unity SRP系列的第8篇文章。主要關於怎麼支援靜態和動態全域性光照。
這篇教程使用 Unity 2018。3。0f2。
1 光照貼圖
實時光只處理了直射光。只有直接暴露在光線下的表面才會被照亮,這是因為缺少了從表面反射到另外的表面最終到攝像機的間接光。這也被稱為全域性照明。我們可以把間接光烘焙到光照貼圖來新增它。Rendering 16, Static Lighting 教程講解了再unity中烘焙燈光的基礎概念,但是對於老的渲染管線只能使用Enlighten lightmapper。
1。1 設定場景
當場景中只有一個直射光沒有間接光的時候,可以很容易的看到所有的陰影區域顏色是接近黑色的。
只有實時光的場景
大的陰影更加顯而易見
因為高光環境反射被新增到直射光裡,所以我們還能看到陰影裡面的物體。如果沒有反射探頭我們會看到天空盒的反射,這是明亮的。透過把
Intensity Multiplier
調整到0來排除天空盒的影響。我們就會看到陰影區域是全黑的。
全黑環境
1。2 烘焙光照
在場景燈光設定下的 Mixed Lighting 下勾選
Baked Global Illumination 和
把Lighting Mode調為Baked Indirect 來開啟烘焙間接光。這會讓Unity烘焙燈光,但是我們無法直接看到。
我會使用預設的Lightmapping 設定,在上面做一些小的改變。預設的使用先進的Lightmapper,我會保留它。因為我有一個小的場景,我會把光照貼圖的解析度從10增加到20。我還禁用了壓縮光照貼圖選項,為了得到最好的質量,跳過了貼圖壓縮這一步。改變Directional Model為 Non-Directional ,因為只有使用法線貼圖的場景才用到,這裡我們不需要。
光照貼圖設定
烘焙燈光是靜態的,所以在執行模式不能改變。只有被標記為lightmap-static的遊戲物件才會烘焙它們的間接光貢獻。把所有的幾何圖形都標記為完全靜態是最快的。
靜態遊戲物體
當烘焙時,有uv重疊的話,unity會產生警告。當一個物體的uv展開時在光照貼圖中太小,會引起光照資訊重疊,就會發生這種情況。你可以透過調整Scale in Lightmap的引數來調整物體的比例。另外對一些預設的物體,比如球體,可以透過開啟Stitch Seams 來提升烘焙光照的效果。
最後,為了烘焙最主要貢獻的燈光,設定它的模式為Mixed。這就意味著它被用於實時光照,它的間接光照也會被烘焙。
在烘焙完成之後,你可以透過Lighting window的Baked Lightmaps標籤頁來檢視貼圖。根據貼圖的大小和烘焙所有靜態物體所需空間的多少,你最終會得到多個貼圖。
1。3 取樣光照貼圖
為了取樣光照貼圖,我們需要通知Unity讓我們的貼圖對我們的shader可用並且在頂點資料裡面包含光照貼圖的uv座標。透過
MyPipeline
。Render
裡面的
RendererConfiguration
。PerObjectLightmaps 標誌來完成。就行我們開啟反射探頭一樣。
drawSettings。rendererConfiguration |=
RendererConfiguration。PerObjectReflectionProbes |
RendererConfiguration。PerObjectLightmaps;
當一個物體在光照貼圖中被渲染時,Unity會提供需要的資料並且選一個有LIGHTMAP
ON關鍵字shader變體。所以我們必須新增一個muti
-complie 在我們的shader中。
#pragma multi_compile _ _SHADOWS_SOFT
#pragma multi_compile _ LIGHTMAP_ON
透過在Lit。hlsl中新增 unity_Lightmap和和它配對使用的取樣器宣告來使光照貼圖可用
TEXTURE2D(unity_Lightmap);
SAMPLER(samplerunity_Lightmap);
光照貼圖的座標透過第二個uv通道提供,所以在VertexInput中新增
struct VertexInput {
float4 pos : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
float2 lightmapUV : TEXCOORD1;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
我們也必須將它們新增到VertexOutput中,但這僅在使用燈光貼圖時才需要。
struct VertexOutput {
float4 clipPos : SV_POSITION;
float3 normal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float3 vertexLighting : TEXCOORD2;
float2 uv : TEXCOORD3;
#if defined(LIGHTMAP_ON)
float2 lightmapUV : TEXCOORD4;
#endif
UNITY_VERTEX_INPUT_INSTANCE_ID
};
光照貼圖也有縮放和偏移,但是不應用在整個貼圖中。相反,他們用來指定一個物體展開的uv在光照貼圖上的位置。定義unity_LightmapST 作為UnityPerDraw buffer的一部分。因為它不符合TRANSFORM_TEX所期望的命名約定,如果需要,我們必須自己轉換LitPassVertex中的座標
CBUFFER_START(UnityPerDraw)
…
float4 unity_LightmapST;
CBUFFER_END
…
VertexOutput LitPassVertex (VertexInput input) {
…
output。uv = TRANSFORM_TEX(input。uv, _MainTex);
#if defined(LIGHTMAP_ON)
output。lightmapUV =
input。lightmapUV * unity_LightmapST。xy + unity_LightmapST。zw;
#endif
return output;
}
讓我們建立一個單獨的SampleLightmap函式,在給定一些UV座標的情況下對光照貼圖進行取樣。在裡面我們將呼叫Core EntityLighting檔案裡面的SampleSingleLightmap 函式。我們提供貼圖,取樣器宣告和uv座標,前兩個透過
TEXTURE2D_PARAM 宏傳遞。
float3 SampleLightmap (float2 uv) {
return SampleSingleLightmap(
TEXTURE2D_PARAM(unity_Lightmap, samplerunity_Lightmap), uv
);
}
SampleSingleLightmap需要更多的引數。下一個是UV座標的尺度偏移變換。但是我們已經在頂點程式中做過了,所以這裡我們要提供一個恆等變換。
return SampleSingleLightmap(
TEXTURE2D_PARAM(unity_Lightmap, samplerunity_Lightmap), uv,
float4(1, 1, 0, 0)
);
然後是一個布林值,指示是否需要對光照貼圖中的資料進行解碼。這依賴於目標平臺。如果unity使用HDR 光照貼圖那麼解碼就不是必須的。這是UNITY_LIGHTMAP_FULL_HDR定義時的情況。
return SampleSingleLightmap(
TEXTURE2D_PARAM(unity_Lightmap, samplerunity_Lightmap), uv,
float4(1, 1, 0, 0),
#if defined(UNITY_LIGHTMAP_FULL_HDR)
false
#else
true
#endif
);
最後,我們需要提供解碼指令,使照明在正確的範圍。我們需要使用float4(LIGHTMAP_HDR_MULTIPLIER, LIGHTMAP_HDR_EXPONENT, 0。0, 0。0)。
return SampleSingleLightmap(
TEXTURE2D_PARAM(unity_Lightmap, samplerunity_Lightmap), uv,
float4(1, 1, 0, 0),
#if defined(UNITY_LIGHTMAP_FULL_HDR)
false,
#else
true,
#endif
float4(LIGHTMAP_HDR_MULTIPLIER, LIGHTMAP_HDR_EXPONENT, 0。0, 0。0)
);
我們對光照貼圖進行取樣,因為我們想要新增全域性光照。我們為此建立一個全域性光照函式,在這個函數里面處理細節。給它一個VertexOutput引數,這意味著需要為他定義一個結構體。如果有光照貼圖,取樣它,否則返回0。
struct VertexOutput {
…
};
float3 GlobalIllumination (VertexOutput input) {
#if defined(LIGHTMAP_ON)
return SampleLightmap(input。lightmapUV);
#endif
return 0;
}
在LitPassFragment的末尾呼叫這個函式,最初替換所有其他的照明,這樣我們就可以單獨看到它了。
float4 LitPassFragment (
VertexOutput input, FRONT_FACE_TYPE isFrontFace : FRONT_FACE_SEMANTIC
) : SV_TARGET {
…
color += ReflectEnvironment(surface, SampleEnvironment(surface));
color = GlobalIllumination(input);
return float4(color, albedoAlpha。a);
}
只有全域性照明
1。4 透明表面
結果應該看起來很軟,但是不連續的偽影會出現在透明的表面附近,尤其是半透材質。progressive lightmapper使用材質渲染佇列去檢測透明,依賴_Cutoff屬性裁剪材質。所以它可以工作,但是當暴露背面的時候還是有問題。當雙面物體的前後重疊時也會有問題,這就是我們自己生成的雙面圖形的情況。
Transparency artifacts。
問題是光照貼圖值應用於前面。背面不包含資料。渲染背面是可行的,但它們最終會使用來自正面的光照資料。artifacts 出現因為取樣時lightmapper 命中了背面,它產生了無效的光照資訊。你可以減輕這個問題透過自定義物體的
Lightmap Parameters 。
並降低
Backface Tolerance
threshold,以便lightmapper接受更多丟失的資料並將其平滑。
Tolerant lightmapping。
1。5 合併直射光和間接光
現在我們知道全域性照明是有效的,把它加到直射光中。由於間接光只是漫射的,所以把它與表面的漫射特性相乘。
color += GlobalIllumination(input) * surface。diffuse;
直接光照和全域性照明
結果是比沒有全域性光照時候更亮,這是預料之中的。然而,現在的情況比以前光明多了。這是因為skybox是全球照明的一部分。讓我們只新增一個方向光的間接光,這樣我們可以更好地檢查它,我們把降低環境光的強度為零。
黑色的環境光
1。6 只烘焙燈光
也可以設定我們的光模式為Baked。這意味著不再需要實時光。相反,它的直接光和間接光都被烘焙到光照貼圖中。在我們的例子中,我們得到一個沒有任何實時光的場景。它也剔除了所有的高光和軟陰影。
Fully baked light。
1。7 Meta Pass (不知道怎麼翻譯)
光對映器必須知道場景中物體的表面屬性才能對光進行烘焙。它透過一個特殊的Meta Pass來獲取它們。我們的shader沒有這個pass,所以unity使用預設的meta pass。但是預設的pass在我們的shader中不是完全匹配的。所以我們要建立我們自己的。新增一個pass 設定mode為Meta,沒有culling,把它的程式碼放到單獨的Meta。hlsl檔案中。
Pass {
Tags {
“LightMode” = “Meta”
}
Cull Off
HLSLPROGRAM
#pragma vertex MetaPassVertex
#pragma fragment MetaPassFragment
#include “。。/ShaderLibrary/Meta。hlsl”
ENDHLSL
}
Meta。hlsl可以用一個精簡版的Lit。hlsl開始。我們只需要unity_MatrixVP矩陣,unity_LightmapST,主要紋理,和非例項化的材質屬性。不需要模型空間到世界空間的轉換,所以直接從物件空間到剪輯空間。開始,讓fragment程式返回零。
#ifndef MYRP_LIT_META_INCLUDED
#define MYRP_LIT_META_INCLUDED
#include
“Packages/com。unity。render-pipelines。core/ShaderLibrary/Common。hlsl”
#include
“Lighting。hlsl”
CBUFFER_START
(
UnityPerFrame
)
float4x4
unity_MatrixVP
;
CBUFFER_END
CBUFFER_START
(
UnityPerDraw
)
float4
unity_LightmapST
;
CBUFFER_END
CBUFFER_START
(
UnityPerMaterial
)
float4
_MainTex_ST
;
float4
_Color
;
float
_Metallic
;
float
_Smoothness
;
CBUFFER_END
TEXTURE2D
(
_MainTex
);
SAMPLER
(
sampler_MainTex
);
struct
VertexInput
{
float4
pos
:
POSITION
;
float2
uv
:
TEXCOORD0
;
float2
lightmapUV
:
TEXCOORD1
;
};
struct
VertexOutput
{
float4
clipPos
:
SV_POSITION
;
float2
uv
:
TEXCOORD0
;
};
VertexOutput
MetaPassVertex
(
VertexInput
input
)
{
VertexOutput
output
;
output
。
clipPos
=
mul
(
unity_MatrixVP
,
float4
(
input
。
pos
。
xyz
,
1。0
));
output
。
uv
=
TRANSFORM_TEX
(
input
。
uv
,
_MainTex
);
return
output
;
}
float4
MetaPassFragment
(
VertexOutput
input
)
:
SV_TARGET
{
float4
meta
=
0
;
return
meta
;
}
#endif // MYRP_LIT_META_INCLUDED
就像取樣光照貼圖一樣,在渲染光資料時,unity_LightmapST被用來得到正確的對映區域。在這種情況下,我們必須調整輸入位置XY座標。此外,還使用了一個技巧使OpenGL也能渲染,因為當Z位置沒有調整時,它會失敗。
VertexOutput MetaPassVertex (VertexInput input) {
VertexOutput output;
input。pos。xy =
input。lightmapUV * unity_LightmapST。xy + unity_LightmapST。zw;
input。pos。z = input。pos。z > 0 ? FLT_MIN : 0。0;
output。clipPos = mul(unity_MatrixVP, float4(input。pos。xyz, 1。0));
output。uv = TRANSFORM_TEX(input。uv, _MainTex);
return output;
}
我們需要初始化亮面,但是我們只有顏色、金屬和平滑資訊。新增一個方便的GetLitSurfaceMeta函式在Lighting。hlsl將所有其他值設定為零l。
LitSurface GetLitSurfaceMeta (float3 color, float metallic, float smoothness) {
return GetLitSurface(0, 0, 0, color, metallic, smoothness);
}
在fragment中獲取表面資料
float4 MetaPassFragment (VertexOutput input) : SV_TARGET {
float4 albedoAlpha = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input。uv);
albedoAlpha *= _Color;
LitSurface surface = GetLitSurfaceMeta(
albedoAlpha。rgb, _Metallic, _Smoothness
);
float4 meta = 0;
return meta;
}
我們現在使用合適的表面反照率,我們必須透過RGB通道輸出它,A通道設定為1。然而,他的強度是可以調整的,透過unity_OneOverOutputBoost 提供一個指數,與unity_MaxOutputValue一起定義最大亮度。透過PositivePow 應用他們得到最終的顏色,把它限定在0和最大值中間。
CBUFFER_START(UnityMetaPass)
float unity_OneOverOutputBoost;
float unity_MaxOutputValue;
CBUFFER_END
…
float4 MetaPassFragment (VertexOutput input) : SV_TARGET {
…
float4 meta = 0;
meta = float4(surface。diffuse, 1);
meta。rgb = clamp(
PositivePow(meta。rgb, unity_OneOverOutputBoost), 0, unity_MaxOutputValue
);
return meta;
}
我們現在輸出了應用於光照貼圖的反照率,但是meta pass 還要生成其他的資料。這個資料是否需要透過定義一個unity_MetaFragmentControl 布林標誌來知道。如果它的第一個分量為true,那麼我們輸出反照率,否則輸出0
CBUFFER_START(UnityMetaPass)
float unity_OneOverOutputBoost;
float unity_MaxOutputValue;
bool4 unity_MetaFragmentControl;
CBUFFER_END
…
float4 MetaPassFragment (VertexOutput input) : SV_TARGET {
…
float4 meta = 0;
if (unity_MetaFragmentControl。x) {
meta = float4(surface。diffuse, 1);
meta。rgb = clamp(
PositivePow(meta。rgb, unity_OneOverOutputBoost),
0,unity_MaxOutputValue
);
}
return meta;
}
到目前為止,我們得到了與預設meta pass相同的結果。然而,預設的meta pass也增加了高光顏色乘以粗糙度的一半加到反照率上。這背後的想法是,高度鏡面和粗糙的材料也透過一些間接的光。預設的陰影是這樣做的,但是它期望平滑值儲存在_Smoothness之外的其他地方。所以我們必須自己做。
meta = float4(surface。diffuse, 1);
meta。rgb += surface。specular * surface。roughness * 0。5;
最好的方法是用一個白色的金屬球,它的平滑度為0,然後只渲染間接光。
without boost
with boost
2 自發光
除了反射或吸收然後再發射光,物體也可以自己發射光。這就是真正的光源的工作原理,但是在渲染時沒有考慮到這一點。要建立自發光材質,只需在計算的照明中新增一種顏色。
2。1 自發光顏色
在shader中新增一個
_EmissionColor 屬性
,把黑色設定為預設值,因為散發的光可能是任意強度的,所以用HDR屬性把顏色標記為高動態範圍。
_Smoothness (“Smoothness”, Range(0, 1)) = 0。5
[HDR] _EmissionColor (“Emission Color”, Color) = (0, 0, 0, 0)
同樣也在
InstancedMaterialProperties
中新增一個自發光顏色欄位,用ColorUsage屬性並把第二個引數置為true標記它為HDR。它的第一個引數表示alpha通道是否顯示,這裡不需要,所以置為false。
static int emissionColorId = Shader。PropertyToID(“_EmissionColor”);
…
[SerializeField, ColorUsage(false, true)]
Color emissionColor = Color。black;
…
void OnValidate () {
…
propertyBlock。SetColor(emissionColorId, emissionColor);
GetComponent
}
在lit。hlsl中新增自發光作為另外一個例項屬性。然後在片元著色器中,用color值最後加上它的值。
UNITY_INSTANCING_BUFFER_START(PerInstance)
…
UNITY_DEFINE_INSTANCED_PROP(float4, _EmissionColor)
UNITY_INSTANCING_BUFFER_END(PerInstance)
…
float4 LitPassFragment (
VertexOutput input, FRONT_FACE_TYPE isFrontFace : FRONT_FACE_SEMANTIC
) : SV_TARGET {
…
color += GlobalIllumination(input) * surface。diffuse;
color += UNITY_ACCESS_INSTANCED_PROP(PerInstance, _EmissionColor)。rgb;
return float4(color, albedoAlpha。a);
}
2。2 間接自發光
自發光顏色可以使物體的表面更加明亮,但是不會影響到其他物體的表面,因為它不是一盞燈。我們能做的就是再渲染燈光貼圖的時候考慮它,高效的把它轉變成烘焙光。
lightmapper也使用meta通道來收集從表面發出的光線。在這種情況下,
unity_MetaFragmentControl
的第二個分量被設定。輸出自發光顏色,這種情況下alpha值被設定為1。
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
float4 _Color, _EmissionColor;
float _Metallic;
float _Smoothness;
CBUFFER_END
…
float4 MetaPassFragment (VertexOutput input) : SV_TARGET {
…
if (unity_MetaFragmentControl。x) {
…
}
if (unity_MetaFragmentControl。y) {
meta = float4(_EmissionColor。rgb, 1);
}
return meta;
}
這還不能夠使自發光影響到其他的表面,因為lightmapper預設情況下不會從其他的物體收集自發光,它還需要更多的工作,如果讓每個材質都開啟它,我們要在我們的shader GUI中新增一個全域性照明屬性,透過在
LitShaderGUI
。OnGUI
中呼叫 editor。
LightmapEmissionPropertry
()。 讓我們把它放在陰影投射的按鈕下面。
public override void OnGUI (
MaterialEditor materialEditor, MaterialProperty[] properties
) {
…
CastShadowsToggle();
editor。LightmapEmissionProperty();
…
}
把它設定為Baked還是不行,因為Unity 使用了另一個最佳化。如果一個材質的自發光最終是黑色的,它會跳過。這個是透過把材質globalIlluminationFlags 設定為
MaterialGlobalIlluminationFlags
。EmissiveIsBlack 來體現的。然而這個標記不是自動設定的,所以我們要自己設定。當全域性照明屬性改變時,我們將簡單地移除標記,這意味著自發光會被烘焙到所有使用這個材質的物體上。所以我們只在我們需要的時候使用這個材質。
EditorGUI。BeginChangeCheck();
editor。LightmapEmissionProperty();
if (EditorGUI。EndChangeCheck()) {
foreach (Material m in editor。targets) {
m。globalIlluminationFlags &=
~MaterialGlobalIlluminationFlags。EmissiveIsBlack;
}
}
3 光照探頭
光照貼圖之影響合併後的靜態幾何體。不能被用於動態物件。也不能很好的適用於小物體。但是,將lightmapping和非lightmapping物件組合在一起並不好,因為他們的不同是很明顯的。為了模擬這個現象,我標記所有不自動發光白色的球是動態的。(I‘ve made all white spheres that aren’t emissive dynamic。)
非自發光白球是動態的
當設定燈光為全烘焙時,不同變得更加明顯。這時候,動態物體沒有接受任何燈光,全部是黑色。
當光照貼圖不能用時,我們只有依賴光照探頭。光照探頭是在一個特定點上的簡單燈光,編碼為球諧函式。Rendering 5, Multiple Lights 中解釋了球諧函式是怎麼工作的。
3。1 取樣光照探頭
光照探頭資訊已經傳遞給shader,像光照貼圖資料一樣。在這裡我們必須開啟
RendererConfiguration
。PerObjectLightProbe
標誌。
drawSettings。rendererConfiguration |=
RendererConfiguration。PerObjectReflectionProbes |
RendererConfiguration。PerObjectLightmaps |
RendererConfiguration。PerObjectLightProbe;
球諧函式在shader中透過七個float4 向量表示,在UnityPerDraw 緩衝區。建立
SampleLightProbes
函式用法向量作為引數。 將係數放入陣列和法向量一起傳遞給
SampleSH9
。也定義在
EntityLighting中。
保證這個函式返回值是正值。
CBUFFER_START(UnityPerDraw)
…
float4 unity_SHAr, unity_SHAg, unity_SHAb;
float4 unity_SHBr, unity_SHBg, unity_SHBb;
float4 unity_SHC;
CBUFFER_END
…
float3 SampleLightProbes (LitSurface s) {
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, s。normal));
}
在
GlobalIllumination
新增一個surface引數,並且在光照貼圖不使用的時候用
SampleLightProbes
代替0作為返回值返回。
float3 GlobalIllumination (VertexOutput input, LitSurface surface) {
#if defined(LIGHTMAP_ON)
return SampleLightmap(input。lightmapUV);
#else
return SampleLightProbes(surface);
#endif
//return 0;
}
然後在LitPassFaragment中新增需要的引數。
color += GlobalIllumination(input, surface) * surface。diffuse;
3。2 放置光照探頭
動態物體現在使用光照探頭,但目前只有環境照明儲存在其中,我們將其設定為黑色。為了使烘焙光透過光探頭可用,我們必須在場景中新增光探頭組,透過GameObject / Light /
Light Probe Group
。這建立一個包含8個探頭的組, 你自己在場景中編輯它,在Rendering 16, Static Lighting。 中解釋了原因。
在光照探頭組新增之後,動態物體就會接收間接光。Unity內插了附近的光探頭,使其在每個物體的區域性原點處都有一個探頭值。這意味著動態物體不能例項化當他們在光照探頭組內部時。可以覆蓋每個物件用於插值的位置,這樣您就可以讓附近的物件使用相同的探測資料,這仍然允許它們被例項化。
3。3 Light Probe Proxy Volumes
因為光照探頭資料是基於物件的區域性原點,所以它只能影響相對小的物體。為了說明它,我在場景中放了一個瘦長的動態立方體。它會受到不同程度的烘焙光,但最終被均勻地照亮。
對於這樣的物體,如果我們取樣多個探頭,我們就可以得到合理的結果。我們可以透過使用LPPV來完成它。透過
Component / Rendering / Light Probe Proxy Volume
新增,我們在Rendering 18, Realtime GI, Probe Volumes, LOD Groups。 中解釋過它。
要開啟LPPV,物體上的Light Probes模式要設定為 Use Proxy Volume。
我們還需要告訴Unity傳送必要的資料導GPU,這裡我們使用
RendererConfiguration
。PerObjectLightProbeProxyVolume
標誌。
drawSettings。rendererConfiguration |=
RendererConfiguration。PerObjectReflectionProbes |
RendererConfiguration。PerObjectLightmaps |
RendererConfiguration。PerObjectLightProbe |
RendererConfiguration。PerObjectLightProbeProxyVolume;
LPPV配置要放入
UnityProbeVolume
緩衝區。包含一個變換矩陣,和尺寸資料以及一些引數。探頭體積資料被儲存在一個 浮點3D貼圖中,我們用
TEXTURE3D_FLOAT
(unity_ProbeVolumeSH)定義,和取樣器一起使用。
CBUFFER_START(UnityProbeVolume)
float4 unity_ProbeVolumeParams;
float4x4 unity_ProbeVolumeWorldToObject;
float3 unity_ProbeVolumeSizeInv;
float3 unity_ProbeVolumeMin;
CBUFFER_END
TEXTURE3D_FLOAT(unity_ProbeVolumeSH);
SAMPLER(samplerunity_ProbeVolumeSH);
在SampleLightProbes 函式中,檢查
unity_ProbeVolumeParams
的第一個分量是否被設定,如果設定了,我們必須取樣LPPV代替 普通的光照探頭。我們從
EntityLighting 中呼叫
SampleProbeVolumeSH4
函式,使用紋理、表面位置和法線、轉換矩陣、第二個和第三個引數值以及尺寸配置作為引數。
float3 SampleLightProbes (LitSurface s) {
if (unity_ProbeVolumeParams。x) {
return SampleProbeVolumeSH4(
TEXTURE3D_PARAM(unity_ProbeVolumeSH, samplerunity_ProbeVolumeSH),
s。position, s。normal, unity_ProbeVolumeWorldToObject,
unity_ProbeVolumeParams。y, unity_ProbeVolumeParams。z,
unity_ProbeVolumeMin, unity_ProbeVolumeSizeInv
);
}
else {
…
}
}
4 實時全域性照明
烘焙光照的缺點是不能執行時改變。正如 Rendering 18, Realtime GI, Probe Volumes, LOD Groups, 中解釋的一樣,Unity 使用預先計算全域性照明關係使執行時改變變成可能,而光的方向和強度也可以在執行時改變。這透過開啟Lighting 視窗下的
Realtime Global Illumination
under
Realtime Lighting 來完成。
讓我們來完成它,同時也禁用烘焙照明,將燈光模式設定為實時模式。
Unity將使用Enlighten引擎預先計算傳播間接光所需的所有資料,然後儲存這些資訊,以便以後完成烘焙過程。這使得更新全域性光照成為可能。最初,只有光探頭獲取實時全域性照明。靜態物件使用動態光照貼圖。
透過光照探頭實現的實時全域性照明
4。1 渲染實時全域性照明
為實時光照貼圖渲染表面資訊也是透過meta pass來完成的。但是實時光照貼圖的解析度將會很低,而且UV展開也是不同的。所以我們需要不同的uv座標和變換資訊,透過第三個頂點UV通道和unity_DynamicLightmapST 來實現
CBUFFER_START(UnityPerDraw)
float4 unity_LightmapST, unity_DynamicLightmapST;
CBUFFER_END
…
struct VertexInput {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float2 lightmapUV : TEXCOORD1;
float2 dynamicLightmapUV : TEXCOORD2;
};
烘焙和實時光照貼圖需要相同的輸出,唯一不同的是我們必須使用哪個座標。這是透過unity_MetaVertexControl顯示的,它的第一個標誌設定為烘焙,第二個為實時。
CBUFFER_START(UnityMetaPass)
float unity_OneOverOutputBoost;
float unity_MaxOutputValue;
bool4 unity_MetaVertexControl, unity_MetaFragmentControl;
CBUFFER_END
…
VertexOutput MetaPassVertex (VertexInput input) {
VertexOutput output;
if (unity_MetaVertexControl。x) {
input。pos。xy =
input。lightmapUV * unity_LightmapST。xy + unity_LightmapST。zw;
}
if (unity_MetaVertexControl。y) {
input。pos。xy =
input。dynamicLightmapUV * unity_DynamicLightmapST。xy +
unity_DynamicLightmapST。zw;
}
input。pos。z = input。pos。z > 0 ? FLT_MIN : 0。0;
…
}
4。2 取樣動態光照貼圖
現在我們在Lit。hlsl中取樣動態光照貼圖,和烘焙光照貼圖一樣,但是透過
unity_DynamicLightmap
貼圖以及它的取樣器 。複製
SampleLightmap
建立
SampleDynamicLightmap
函式,除了使用其他的貼圖和不對它進行編碼,其他的都一樣。
TEXTURE2D(unity_DynamicLightmap);
SAMPLER(samplerunity_DynamicLightmap);
…
float3 SampleDynamicLightmap (float2 uv) {
return SampleSingleLightmap(
TEXTURE2D_PARAM(unity_DynamicLightmap, samplerunity_DynamicLightmap), uv,
float4(1, 1, 0, 0), false,
float4(LIGHTMAP_HDR_MULTIPLIER, LIGHTMAP_HDR_EXPONENT, 0。0, 0。0)
);
}
當一個動態光照貼圖需要取樣Unity將選擇一個著色器變數與DYNAMICLIGHTMAP_ON關鍵字集,所以為它新增一個多編譯指令。
#pragma multi_compile _ LIGHTMAP_ON
#pragma multi_compile _ DYNAMICLIGHTMAP_ON
新增所需的UV座標和變換,就像烘焙光照貼圖一樣。
CBUFFER_START(UnityPerDraw)
…
float4 unity_LightmapST, unity_DynamicLightmapST;
…
CBUFFER_END
…
struct VertexInput {
…
float2 lightmapUV : TEXCOORD1;
float2 dynamicLightmapUV : TEXCOORD2;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct VertexOutput {
…
#if defined(LIGHTMAP_ON)
float2 lightmapUV : TEXCOORD4;
#endif
#if defined(DYNAMICLIGHTMAP_ON)
float2 dynamicLightmapUV : TEXCOORD5;
#endif
UNITY_VERTEX_INPUT_INSTANCE_ID
};
…
VertexOutput LitPassVertex (VertexInput input) {
…
#if defined(DYNAMICLIGHTMAP_ON)
output。dynamicLightmapUV =
input。dynamicLightmapUV * unity_DynamicLightmapST。xy +
unity_DynamicLightmapST。zw;
#endif
return output;
}
在GlobalIllumination中,如果有動態光照貼圖,對其進行取樣。烘焙光照貼圖和實時光照貼圖可以同時使用,所以在這種情況下新增它們。
float3 GlobalIllumination (VertexOutput input, LitSurface surface) {
#if defined(LIGHTMAP_ON)
float3 gi = SampleLightmap(input。lightmapUV);
#if defined(DYNAMICLIGHTMAP_ON)
gi += SampleDynamicLightmap(input。dynamicLightmapUV);
#endif
return gi;
#elif defined(DYNAMICLIGHTMAP_ON)
return SampleDynamicLightmap(input。dynamicLightmapUV);
#else
return SampleLightProbes(surface);
#endif
}
靜態物件現在也接受實時全域性照明
4。3 實時間接發光
我們的靜態自發光物體也會對實時全域性照明產生髮射光貢獻,代替被烘焙。
這樣做的好處是它可以調整發射顏色,並在播放模式下影響間接光,就像調整主光源的顏色和方向一樣。為了演示這一點,在InstancedMaterialProperties中新增一個脈衝發射頻率配置選項。在播放模式時,如果它大於零,使用餘弦振盪發射顏色之間的原始值和黑色。
[SerializeField]
float pulseEmissionFreqency;
void Awake () {
OnValidate();
if (pulseEmissionFreqency <= 0f) {
enabled = false;
}
}
void Update () {
Color originalEmissionColor = emissionColor;
emissionColor *= 0。5f +
0。5f * Mathf。Cos(2f * Mathf。PI * pulseEmissionFreqency * Time。time);
OnValidate();
emissionColor = originalEmissionColor;
}
僅僅改變發射顏色並不會自動更新全域性照明。我們必須告訴Unity照明情況已經改變,這可以透過呼叫改變物件的MeshRenderer元件上的UpdateGIMaterials來實現。
OnValidate();
GetComponent
emissionColor = originalEmissionColor;
這將觸發一個meta pass渲染我們的物件,當只改變一個統一的顏色這是多餘的。在這種情況下,我們可以透過呼叫DynamicGI。SetEmissive直接設定統一的顏色,使用渲染器和顏色作為引數,這樣計算速度更快。
//GetComponent
DynamicGI。SetEmissive(GetComponent
emissionColor = originalEmissionColor;
Updating global illumination。
https://www。zhihu。com/video/1186289539114577920
4。4 透明
Unity 使用Enlighten 生成實時全域性照明,它預設是不支援透明表面的。
透明的地方很黑
這種情況下,我們必須明確指出哪些物件是透明的,透過給他自定義 lightmapping 引數和開啟 Is Transparent選項。
透明表面不阻塞燈光,但它們仍然對間接光積累有充分的貢獻。結果是全域性光照在透明表面附近變得太強烈。這在使物件完全透明時變得非常明顯。
我們可以透過在meta pass中乘上反照率的透明度和發射色來彌補這一點。
float4 albedoAlpha = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input。uv);
albedoAlpha *= _Color;
albedoAlpha。rgb *= albedoAlpha。a;
…
if (unity_MetaFragmentControl。y) {
meta = float4(_EmissionColor。rgb * albedoAlpha。a, 1);
}
因為動態光照貼圖的解析度很低,紋理不會對最終的結果有很大的影響,但是均勻的不透明度值會。
請注意,即使在重新建立預先計算的資料時,Unity也不會總是獲取這些更改。它可能會在編輯模式下快取舊的照明結果,如果它認為它們仍然有效。我發現切換播放模式來顯示差異是最可靠的方法。
5 點光源和聚光燈
到目前為止我們只在方向光下生效,我們來檢查下這一切是否也在點光源和聚光燈下工作。
5。1 實時全域性照明
當啟用實時全域性照明時,兩個燈都按預期進行烘烤,但結果不正確。在計算陰影的間接照明時,沒有考慮陰影。它們也太亮了。
這證明了陰影實時間接照明不支援這些燈,Unity在inspector面板也提示了這一點。動態全域性照明主要是為了支援太陽的間接光,允許一個晝夜迴圈與全域性照明相結合。這隻需要完全支援定向光。
你仍然可以使用這些燈,但如果你想要它們有助於動態全域性照明,你必須繞過這個限制。或者,設定
Indirect Multiplier 為0
,防止他們影響全域性照明。
5。2 烘焙全域性照明
混合和全烘焙點光源和聚光燈不會有上面的問題,只是還是太亮了。
Baked only, too bright。
光的貢獻太多了,因為Unity假設使用預設管道的舊的光照衰減,而我們使用物理上正確的平方衰減。這對方向光來說沒有問題,因為它們沒有衰減。為了修正這個問題,在lightmapping的時候
MyPipeline 必須
告訴Unity使用拿一個衰減函式。 我們要使用Unity。Collections和UnityEngine。Experimental。GlobalIllumination的型別。但是,使用後者會導致LightType的型別衝突,所以應該將其顯式地使用為UnityEngine。LightType。
using Unity。Collections;
using UnityEngine;
using UnityEngine。Rendering;
using UnityEngine。Experimental。Rendering;
using UnityEngine。Experimental。GlobalIllumination;
using LightType = UnityEngine。LightType;
using Conditional = System。Diagnostics。ConditionalAttribute;
我們必須重寫lightmapper如何設定它的光線資料。這是透過向一個方法提供一個委託來完成的,該方法將資料從一個輸入光陣列傳輸到一個輸出
LightDataGI
陣列。委託型別是
Lightmapping
。
RequestLightsDelegate
我們用一個lamada表示式來定義它。
static Lightmapping。RequestLightsDelegate lightmappingLightsDelegate =
(Light[] inputLights, NativeArray
這個委託只在編輯器下能用,所以我們要加上編譯條件。
#if UNITY_EDITOR
static Lightmapping。RequestLightsDelegate lightmappingLightsDelegate =
(Light[] inputLights, NativeArray
#endif
我們必須遍歷所有的光,配置合適的
LightDataGI 結構體
,設定falloff型別為
FalloffType
。InverseSquared
,並且把它複製給輸出陣列。
static Lightmapping。RequestLightsDelegate lightmappingLightsDelegate =
(Light[] inputLights, NativeArray
LightDataGI lightData = new LightDataGI();
for (int i = 0; i < inputLights。Length; i++) {
Light light = inputLights[i];
lightData。falloff = FalloffType。InverseSquared;
outputLights[i] = lightData;
}
};
每一個燈都必須明確配置,即使我們不修改預設行為的任何行為除了衰減。我們可以使用
LightmapperUtils
。Extract
方法將適當的值放在特定於光的結構中,然後複製這些給光資料透過它的Init方法。如果我們最終得到一個未知的light型別,我們將呼叫InitNoBake函式,使用該light的例項識別符號當做引數。
Light light = inputLights[i];
switch (light。type) {
case LightType。Directional:
var directionalLight = new DirectionalLight();
LightmapperUtils。Extract(light, ref directionalLight);
lightData。Init(ref directionalLight);
break;
case LightType。Point:
var pointLight = new PointLight();
LightmapperUtils。Extract(light, ref pointLight);
lightData。Init(ref pointLight);
break;
case LightType。Spot:
var spotLight = new SpotLight();
LightmapperUtils。Extract(light, ref spotLight);
lightData。Init(ref spotLight);
break;
case LightType。Area:
var rectangleLight = new RectangleLight();
LightmapperUtils。Extract(light, ref rectangleLight);
lightData。Init(ref rectangleLight);
break;
default:
lightData。InitNoBake(light。GetInstanceID());
break;
}
lightData。falloff = FalloffType。InverseSquared;
outputLights[i] = lightData;
在委託中重寫預設的行為,透過把委託傳入
Lightmapping
。SetDelegate
方法,在我們構造方法的結尾呼叫。 我們還必須在管道物件被釋放時恢復到預設行為,我們透過覆蓋它的Dispose方法來呼叫基本實現,然後呼叫Lightmapping。ResetDelegate。
public MyPipeline (
…
) {
…
#if UNITY_EDITOR
Lightmapping。SetDelegate(lightmappingLightsDelegate);
#endif
}
#if UNITY_EDITOR
public override void Dispose () {
base。Dispose();
Lightmapping。ResetDelegate();
}
#endif
下一篇是烘焙陰影。