https://catlikecoding.com/unity/tutorials/custom-srp/lod-and-reflections/
1 LOD组
离远时,细节过多的物体会因太小而模糊不清,最好的方式就是不渲染它们,让CPU内存占用减少,让GPU渲染更为重要的东西。我们也可以提前剔除这些物体,不过突然剔除可能会造成视觉问题,我们可以添加一些中间过渡的状态,Unity使用LOD(level of detail)组来完成。
1.1 LOD组组件
我们可以创建一个空对象,然后添加一个LODGroup
组件:
百分比即物体在窗口中的占比。我们可以点击任一LOD级别,然后赋予一个要显示的物体。
1.2 LOD过渡
LOD组的渐变模式有Cross Fade
和Speed Tree
,Corss Fade即交叉渐变,会提前显示下一LOD级别的物体,交叉过渡,Speed Tree用于Speed Tree
的树,可在网格和公告板间过渡。
切换为Cross Fade
后,我们可以点击任一LOD级别,调节Fade Transition Width
滑条,数值越低越早混合,0表示不混合,1表示立即切换。
开启交叉渐变后,会同时渲染两个LOD级别,我们需要在对应的shader中添加相应关键字生成shader变体:
#pragma multi_compile _ LOD_FADE_CROSSFADE
物体的渐变程度可由UnityPerDraw
缓冲中的unity_LODFade
获得,X组件是渐变因数,比如可以这样观察LOD渐变因数:
float4 LitPassFragment (Varyings input) : SV_TARGET
{
UNITY_SETUP_INSTANCE_ID(input);
#if defined(LOD_FADE_CROSSFADE)
return unity_LODFade.x;
#endif
…
}
1.3 平滑交叉渐变
我们可以开启Animate Cross-fading
选项,这样的渐变符合平滑动画曲线。默认的动画时间是0.5s,我们可以通过LODGroup.corssFadeAnimationDuration
属性修改长度。
2 反射
向场景中添加细节的另一种方式是添加环境高光反射,这对于金属度高的表面来说非常重要。
2.1 间接BRDF
我们已经支持了漫反射全局光照,取决于BRDF的漫反射颜色,现在添加高光全局光照。添加一个IndirectBRDF
方法,一开始只返回漫反射光:
float3 IndirectBRDF (
Surface surface, BRDF brdf, float3 diffuse, float3 specular
)
{
return diffuse * brdf.diffuse;
}
在一开始添加反射光颜色,包括GI高光和BRDF高光颜色:
float3 reflection = specular * brdf.specular;
return diffuse * brdf.diffuse + reflection;
使用粗糙度散射这些反射,我们除以粗糙度的二次方,防止分母为0加1:
float3 reflection = specular * brdf.specular;
reflection /= brdf.roughness * brdf.roughness + 1.0;
在GetLighting
中替换直接计算的间接光漫反射:
float3 GetLighting (Surface surfaceWS, BRDF brdf, GI gi)
{
ShadowData shadowData = GetShadowData(surfaceWS);
shadowData.shadowMask = gi.shadowMask;
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 采样环境
最常见的环境就是天空盒,可通过unity_SpecCube0
获得相应立方体贴图纹理:
TEXTURECUBE(unity_SpecCube0);
SAMPLER(samplerunity_SpecCube0);
添加一个采样方法SampleEnvironment
,方法中使用SAMPLE_TEXTURECUBE_LOD
来采样,最后一个参数是mipmap级别,这里先设为最大mipmap级别:
float3 SampleEnvironment (Surface surfaceWS)
{
float3 uvw = 0.0;
float4 environment = SAMPLE_TEXTURECUBE_LOD(
unity_SpecCube0, samplerunity_SpecCube0, uvw, 0.0
);
return environment.rgb;
}
我们需要通过反射光方向来采样,而这个我们可看见的反射光方向也就是观察方向的反射:
float3 uvw = reflect(-surfaceWS.viewDirection, surfaceWS.normal);
然后,在GI中添加高光属性,在GetGI
中采样获得:
struct GI
{
float3 diffuse;
float3 specular;
ShadowMask shadowMask;
};
…
GI GetGI (float2 lightMapUV, Surface surfaceWS)
{
GI gi;
gi.diffuse = SampleLightMap(lightMapUV) + SampleLightProbe(surfaceWS);
gi.specular = SampleEnvironment(surfaceWS);
…
}
之后在GetLighting
中的IndirectBRDF
中传入正确的GI高光颜色:
float3 color = IndirectBRDF(surfaceWS, brdf, gi.diffuse, gi.specular);
想起作用的话,需要配置逐物体数据标志:
perObjectData =
PerObjectData.ReflectionProbes |
PerObjectData.Lightmaps | PerObjectData.ShadowMask |
PerObjectData.LightProbe | PerObjectData.OcclusionProbe |
PerObjectData.LightProbeProxyVolume |
PerObjectData.OcclusionProbeProxyVolume
2.3 粗糙反射
粗糙表面不仅会散射高光的强度,还会为高光添加一定的混乱度。Unity通过使用较低级别的mipmap模糊环境贴图来模拟这些效果。为得到正确的mipmap等级,我们需要知道人所感知的粗糙度。将其添加到BRDF结构体中:
struct BRDF
{
…
float perceptualRoughness;
};
…
BRDF GetBRDF (Surface surface, bool applyAlphaToDiffuse = false)
{
…
brdf.perceptualRoughness =
PerceptualSmoothnessToPerceptualRoughness(surface.smoothness);
brdf.roughness = PerceptualRoughnessToRoughness(brdf.perceptualRoughness);
return brdf;
}
我们使用PerceptualSmoothnessToPerceptualRoughness
来获得感知粗糙度,然后获得我们想要的表面粗糙度,这些方法定义在Core RP
的CommonMaterial.hlsl
中:
real PerceptualSmoothnessToPerceptualRoughness(real perceptualSmoothness)
{
return (1.0 - perceptualSmoothness);
}
real PerceptualRoughnessToRoughness(real perceptualRoughness)
{
return perceptualRoughness * perceptualRoughness;
}
接着我们通过定义在ImageBasedLighting.hlsl
中的PerceptualRoughnessToMipmapLevel
来获得正确的mipmap级别:
float3 SampleEnvironment (Surface surfaceWS, BRDF brdf)
{
float3 uvw = reflect(-surfaceWS.viewDirection, surfaceWS.normal);
float mip = PerceptualRoughnessToMipmapLevel(brdf.perceptualRoughness);
float4 environment = SAMPLE_TEXTURECUBE_LOD(
unity_SpecCube0, samplerunity_SpecCube0, uvw, mip
);
return environment.rgb;
}
PerceptualRoughnessToMipmapLevel
:
#ifndef UNITY_SPECCUBE_LOD_STEPS
// This is actuall the last mip index, we generate 7 mips of convolution
#define UNITY_SPECCUBE_LOD_STEPS 6
#endif
real PerceptualRoughnessToMipmapLevel(real perceptualRoughness, uint mipMapCount)
{
perceptualRoughness = perceptualRoughness * (1.7 - 0.7 * perceptualRoughness);
return perceptualRoughness * mipMapCount;
}
real PerceptualRoughnessToMipmapLevel(real perceptualRoughness)
{
return PerceptualRoughnessToMipmapLevel(perceptualRoughness, UNITY_SPECCUBE_LOD_STEPS);
}
然后在GetGI
和LitPassFragment
中应用:
GI GetGI (float2 lightMapUV, Surface surfaceWS, BRDF brdf)
{
GI gi;
gi.diffuse = SampleLightMap(lightMapUV) + SampleLightProbe(surfaceWS);
gi.specular = SampleEnvironment(surfaceWS, brdf);
…
}
GI gi = GetGI(GI_FRAGMENT_DATA(input), surface, brdf);
2.4 菲涅尔反射
表面的一种属性是当沿着掠射角观察时,会看起来完全是镜面,这种现象称之为菲涅尔反射。真实的菲涅尔非常复杂,我们用一种模拟边界的方式来模拟菲涅尔效应。
这里使用Schlick的模拟的变体:
struct BRDF
{
…
float fresnel;
};
…
BRDF GetBRDF (Surface surface, bool applyAlphaToDiffuse = false)
{
…
brdf.fresnel = saturate(surface.smoothness + 1.0 - oneMinusReflectivity);
return brdf;
}
在IndirectBRDF
中,我们用1-法线与观察防线的点积来影响菲涅尔的强度,并进行4次幂运算:
float fresnelStrength =
Pow4(1.0 - saturate(dot(surface.normal, surface.viewDirection)));
然后使用菲涅尔强度在菲涅尔颜色和高光间插值:
float3 reflection =
specular * lerp(brdf.specular, brdf.fresnel, fresnelStrength);
2.5 控制菲涅尔
在shader中添加相应的属性:
_Metallic ("Metallic", Range(0, 1)) = 0
_Smoothness ("Smoothness", Range(0, 1)) = 0.5
_Fresnel ("Fresnel", Range(0, 1)) = 1
在LitInput
中声明相应的变量:
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
…
UNITY_DEFINE_INSTANCED_PROP(float, _Fresnel)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
…
float GetFresnel (float2 baseUV)
{
return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Fresnel);
}
在Surface
结构体中添加菲涅尔强度的属性:
struct Surface
{
…
float smoothness;
float fresnelStrength;
float dither;
};
在IndirectBRDF
中应用:
float fresnelStrength = surface.fresnelStrength *
Pow4(1.0 - saturate(dot(surface.normal, surface.viewDirection)));
2.6 反射探针
默认的环境立方体贴图只包含天空盒,不包含场景中的其它物体。为了反射所有东西,我们可以添加反射探针,GameObject/Light/Refelection Probe
。反射探针从它所在位置将场景渲染到一个立方体贴图中,因为与位置有关,往往需要在场景中防止多个反射探针:
反射探针的类型默认设置为Baked
,即只渲染一次,我们还可以设置为Realtime
。
场景中根据物体的摆放,我们设置多个反射探针,但这也意味着可能会破坏GPU批处理。
物体的MeshRenderer
中的Anchor Override
可以用来调整物体使用哪个探针:
反射探针也有多种混合模式可以选择,默认是Blend Probes
,即可以在最佳的两个反射探针间混合,但该模式不支持SRP批处理。因此,目前暂时仅支持关闭Off
,以及Simple
,选择最重要的探针。
2.7 解码探针
立方体贴图数据可以是LDR或HDR的,要HDR的话,可以这么声明:
CBUFFER_START(UnityPerDraw)
…
float4 unity_SpecCube0_HDR;
…
CBUFFER_END
然后使用DecodeHDREnvironment
来解码:
float3 SampleEnvironment (Surface surfaceWS, BRDF brdf)
{
…
return DecodeHDREnvironment(environment, unity_SpecCube0_HDR);
}