1 LOD Groups
場景中對象越多,場景就越豐富,但是過多的對象,也會增加 CPU 和 GPU 的負擔.同時如果對象最終渲染在屏幕上后覆蓋的像素太少,就會產生模糊不清的像素點/噪點.如果能夠不渲染這些過小的對象,就能解決噪點問題,同時釋放 CPU GPU,去處理更重要的對象.
裁剪掉這些對象,可能會導致對象突然消失/出現的問題,因此,可以基于對象在屏幕上的大小,定義一系列子對象,根據對象到攝像機的距離,選擇一個子對象進行渲染
這些邏輯,都可以有 LOD Group 組件來實現.
1.1 LOD Group Component
組件創建后,默認有4個LOD級別, LOD 0,1,2 和culled(裁剪掉,不渲染).
組件上的百分數,代表對象在屏幕上,渲染的高度與屏幕高度的比值(一般都是這樣),叫做對象的屏占比.如上圖,它表示如果屏占比大于60%則用 LOD 0 渲染,以此類推,知道屏占比小于10% 時就會被裁剪掉
在 Quality Settings 中,可以配置 LOD Bias,默認是 2, 會將對象屏占比乘以2.在上面的配置中,意味著屏占比大于30%時渲染 LOD 0.
對于一個 LOD 對象,我們一般會創建一個對象,添加 LOD Group 組件,然后為其創建子對象,這些子對象被LOD驅動,渲染當前 LOD 級別配置的 Renderer.
選擇一個 LOD 級別,點擊 + 號,創建一個 Renderer 項, 然后就可以將 LOD 子對象拖動到 Renderers 列表中,表示該LOD會渲染它.
一個對象可以配置到多個 LOD 中.比如有A,B,C三個子對象,LOD0時,ABC都渲染,LOD1時渲染AB,LOD2時渲染A.這樣隨著距離增加,按照重要程度(ABC),三個子對象會一次消失.
1.2 LOD Transition
LOD之間的切換可能會過于突兀, 在LOD Group 的 Fade Mode 選項中,選擇 cross fade,則切換時舊的 LOD 會有淡出過程,新的也會有淡入.但是目前該選項不會帶來任何效果,因為這需要我們的 shader 的支持.
切換時兩個LOD 的 Renderer 都會渲染, shader 中需要對他們進行混合.
Unity 用 LOD_FADE_CROSSFADE 來定義支持混合的 shader 變體.需要在 CustomLit 和 ShadowCaster 兩個 pass 中定義 shader keyword:
#pragma multi_compile _ LOD_FADE_CROSSFADE
Fade 控制參數由 per draw buffer 中的 unity_LODFade 提供.其 x 時淡出參數.該參數在淡出和淡入時的取值是不同的:
- 淡出時,1 ~ 0
- 淡入時,-1 ~ 0 (一定是負的,但是是不是 0 ~ -1 ?)
后面實現 fade 效果時,會針對性處理.
1.3 Dithering fade 效果
我們通過 clip 來實現淡入淡出效果.
定義 ClipLOD 方法,并在像素著色器開始時調用
// 執行 lod fade 裁剪
void ClipLOD(float2 positionCS, float fade)
{
#if defined(LOD_FADE_CROSSFADE)//// 在垂直方向上劃分條紋的效果//float dither = (positionCS.y % 32)/32;// unity dither 生成函數float dither = InterleavedGradientNoise(positionCS.xy, 0);// 淡入時,fade 時負的,因此需要 + ditherclip(fade + (fade < 0 ? dither : -dither));
#endif
}float4 LitPassFragment(Varyings input) : SV_TARGET
{UNITY_SETUP_INSTANCE_ID(input);ClipLOD(input.positionCS, unity_LODFade.x);...
}
可以看到我們自定義的 dither 的 fade 效果
lightmap 過度時,我們用到了 unity 提供的 dither 生成函數?InterleavedGradientNoise,這里可以換成同樣的函數看效果
1.4 Animated cross-fading
開啟后, cross fade 會在一定時間內完成,默認時 0.5 秒.這個值可以在代碼中,修改?LODGroup
.crossFadeAnimationDuration
?的值來改變.注意這個值是靜態變量,會影響所有 LOD Group
2 Reflections
目前我們的材質還不支持高光反射,導致我們的 metallic 材質是黑色的.
首先向 baked scene 中,創建幾個球,配置其材質為 metallic > 0.8,可以發現是黑色的
2.1 Indirect BRDF
首先增加 IndirectBRDF 函數
// BRDF.hlsl
float3 IndirectBRDF(Surface surface, BRDF brdf, float3 diffuse, float3 specular)
{float3 reflection = brdf.specular * specular;// 粗糙表面會散射光線,因此除以粗糙度的平方+1reflection /= (brdf.roughness * brdf.roughness + 1.0);float3 diff = brdf.diffuse * diffuse;return diff + reflection;
}
粗糙度除了降低反射強度,還會讓反射變得模糊.我們可以通過采樣 cubemap 的低級別 mipmap 來實現這樣的效果.Unity 在 Core RP Library 中的 ImageBasedLighting.hlsl 中定義了基于 perceptual roughness(感知粗糙度)來獲取mipmap level,因此需要在 BRDF 結構體中定義該數據
// BRDF.hlsl
struct BRDF
{float3 specular;float3 diffuse;float roughness;float perceptualRoughness;
};
...BRDF GetBRDF(Surface surface, bool premultiplyAlpha)
{BRDF brdf;float oneMinusReflectivity = OneMinusReflectivity(surface.metallic);oneMinusReflectivity = 1.0 - surface.metallic;brdf.diffuse = surface.color * oneMinusReflectivity;if (premultiplyAlpha)brdf.diffuse *= surface.alpha;brdf.specular = lerp(MIN_REFLECTIVITY, surface.color, surface.metallic);brdf.perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(surface.smoothness);brdf.roughness = PerceptualRoughnessToRoughness(brdf.perceptualRoughness);return brdf;
}
unity 通過 unity_SpecCube0 提供環境貼圖,在GI.hlsl 中聲明相應的貼圖和采樣器,然后定義采樣函數.在GI結構體中增加 specular 用來傳遞反射
// GI.hlsl#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/EntityLighting.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/ImageBasedLighting.hlsl"
...
TEXTURECUBE(unity_SpecCube0);
SAMPLER(samplerunity_SpecCube0);
...
struct GI
{float3 diffuse;ShadowMask shadowMask;float3 specular;
};
...
// 采樣環境貼圖
float3 SampleEnvironment(Surface surfaceWS, BRDF brdf)
{// 采樣環境貼圖需要一個方向,由表面法線和表面的觀察方向計算反射方向.float3 uvw = reflect(-surfaceWS.viewDirection, surfaceWS.normal);// 基于“感知粗糙度”,計算采樣時的 mipmap levelfloat mip = PerceptualRoughnessToMipmapLevel(brdf.perceptualRoughness);float4 environment = SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, uvw, 0.0);return environment.rgb;
}
..
GI GetGI (float2 lightMapUV, Surface surfaceWS)
{GI gi;gi.diffuse = SampleLightMap(lightMapUV) + SampleLightProbe(surfaceWS);// 采樣環境圖gi.specular = SampleEnvironment(surfaceWS);...return gi;
}
修改Lighting 中的 GetLighting 函數,調用 IndirectBRDF
float3 GetLighting(Surface surfaceWS, BRDF brdf, GI gi)
{ShadowData shadowData = GetShadowData(surfaceWS);shadowData.shadowMask = gi.shadowMask;// 臨時返回以查看數據//return gi.shadowMask.shadows.rgb;//float3 color = gi.diffuse * brdf.diffuse;float3 color = IndirectBRDF(surfaceWS, brdf, gi.diffuse, 1.0);for(int i = 0; i < GetDirectionalLightCount(); ++i){Light light = GetDirectionalLight(i, surfaceWS, shadowData);color += GetLighting(surfaceWS, brdf, light);}return color;
}
如下圖,反射了天空盒
2.2 Fresnel Reflection
當視線掠過表面(觀察方向與表面法線接近90度)時,表面更像鏡子,會反射更多,這種現象就是 菲涅爾反射.模擬這種現象非常復雜,我們參考 URP 中采用的 Schlick approximation(石里克近似) 法.
我們將菲涅爾反射定義為白色,而粗糙表面會降低菲涅爾效應,因此我們基于粗糙度計算菲涅爾顏色(本質上灰度).然后基于觀察方向和表面法線計算菲涅爾強度,并在菲涅爾顏色和反射之間插值.
// BRDF.hlslstruct BRDF
{float3 specular; // 高光反射float3 diffuse; // 漫反射float roughness; // 粗糙度float perceptualRoughness; // 感知粗糙度float fresnel; // 菲涅爾顏色,因為是灰度,rgb都相等,因此只需要一個浮點數
};...BRDF GetBRDF(Surface surface, bool premultiplyAlpha)
{BRDF brdf;float oneMinusReflectivity = OneMinusReflectivity(surface.metallic);oneMinusReflectivity = 1.0 - surface.metallic;brdf.diffuse = surface.color * oneMinusReflectivity;if (premultiplyAlpha)brdf.diffuse *= surface.alpha;brdf.specular = lerp(MIN_REFLECTIVITY, surface.color, surface.metallic);brdf.perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(surface.smoothness);brdf.roughness = PerceptualRoughnessToRoughness(brdf.perceptualRoughness);// 計算菲涅爾灰度顏色brdf.fresnel = saturate(surface.smoothness + 1.0 - oneMinusReflectivity);return brdf;
}float3 IndirectBRDF(Surface surface, BRDF brdf, float3 diffuse, float3 specular)
{// 計算菲涅爾強度float fresnelStrength = Pow4(1.0 - saturate(dot(surface.normal, surface.viewDirection)));// 反射是根據菲尼爾強度,在高光反射和菲涅爾反射之間插值float3 reflection = specular + lerp(brdf.specular, brdf.fresnel, fresnelStrength);// 粗糙表面會散射光線,因此除以粗糙度的平方+1reflection /= (brdf.roughness * brdf.roughness + 1.0);float3 diff = brdf.diffuse * diffuse;return diff + reflection;
}
2.3 Fresnel Slider
菲涅爾反射主要出現在幾何體的邊緣.當環境圖與物體背后的顏色匹配時,效果很好.但是如果顏色不匹配,就會顯得怪異.
降低光滑度可以降低菲涅爾反射,但會讓整個表面變暗.同時,在某些情況下,近似的菲涅爾反射可能不合適,如水下.因此,需要加一個參數來控制菲涅爾強度.
// Lit.shader 中定義材質參數
_Smoothness("Smoothness", Range(0,1)) = 0.51
_Fresnel("Fresnel", Range(0,1)) = 1// LitInput.hlsl 中,定義 per material 變量,并定義查詢函數
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
...
float _Smoothness;
float _Fresnel;
...
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)...float GetFresnel (float2 baseUV)
{return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Fresnel);
}// UnlitInput.hlsl 中,定義返回0的函數
float GetFresnel (float2 baseUV)
{return 0.0;
}// Surface.hlsl 中,為Surface結構體增加 fresnelStrength
struct Surface
{...float smoothness;float fresnelStrength;...
};// LitPass.hlsl 中,為 Surface.fresnelStrength 賦值
float4 LitPassFragment(Varyings input) : SV_TARGET
{...surface.smoothness = GetSmoothness(input.uv);surface.fresnelStrength = GetFresnel(input.uv);...
}// BRDF.hlsl 中,應用該強度
// 間接 BRDF 光
float3 IndirectBRDF(Surface surface, BRDF brdf, float3 giDiffuse, float3 giSpecular)
{//return gi.diffuse * brdf.diffuse;//float3 reflection = giSpecular * brdf.specular;// 計算菲涅爾強度 float fresnelStrength = surface.fresnelStrength * Pow4(1.0 - saturate(dot(surface.normal, surface.viewDirection)));// 反射是根據菲尼爾強度,在高光反射和菲涅爾反射之間插值,在與 giSpecular(來自環境圖) 相乘float3 reflection = giSpecular * lerp(brdf.specular, brdf.fresnel, fresnelStrength);// 粗糙表面會散射光線,因此除以粗糙度的平方+1reflection /= (brdf.roughness * brdf.roughness + 1.0);// 計算間接 diffusefloat3 diffuse = brdf.diffuse * giDiffuse;// 累加 diffuse 和 reflectionreturn diffuse + reflection;
}
2.4 Reflection Probes
除了反射天空,還可以創建當前場景.通過GameObject / Light / Reflection Probe創建 Reflection Probe,該組件會在其位置拍攝6方向并生成立方體貼圖.屬性 Box Size 用來確定影響范圍,配合 Importance 重要性,該范圍內的對象會使用該 Reflection Probe.
Cube map 可以離線生成,也可以實時生成.實時生成需要渲染6個畫面,因此消耗比較大,不建議.
通過 Anchor Override, Renderer 可以調整 Reflection Probe 選擇,即使對象本身超出了范圍,但是可以指定該屬性的位置在 Reflection Probe 范圍內,來優化選擇. 使用場景中的 Light Probe 會打斷合批.同時,DrawMeshInstanced 接口渲染時,不支持指定 Reflection Probe.
Renderer 的 Reflection Probe 選項中:
- 默認是 Blend Probes, Unity會選擇兩個 Reflection Probe 并進行插值,該模式與 SRP Batcher 不兼容,因此我們不能用.
- off 表示使用天空盒的 cube map
- Simple 表示使用最近,最重要的 Reflection Probe
Cube map 可能是 HDR 或 LDR 的,我們需要正確解碼采樣結果.該解碼參數以 unity_SpecCube0_HDR 變量提供.
// UnityInput.hlsl 中
CBUFFER_START(UnityPerDraw)
...
float4 unity_ProbeVolumeMin;
float4 unity_SpecCube0_HDR; // 如何解碼 reflection cube map
CBUFFER_END// GI.hlsl 中
// 采樣環境貼圖
float3 SampleEnvironment(Surface surfaceWS, BRDF brdf)
{// 采樣環境貼圖需要一個方向,由表面法線和表面的觀察方向計算反射方向.float3 uvw = reflect(-surfaceWS.viewDirection, surfaceWS.normal);// 基于“感知粗糙度”,計算采樣時的 mipmap levelfloat mip = PerceptualRoughnessToMipmapLevel(brdf.perceptualRoughness);float4 environment = SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, uvw, 0.0);// unity_SpecCube0_HDR 參數可以確定 cube map 是 HDR/LDR的,通過下面的方法正確解碼return DecodeHDREnvironment(environment, unity_SpecCube0_HDR);
}