Unity自定义SRP(七):LOD和反射

https://catlikecoding.com/unity/tutorials/custom-srp/lod-and-reflections/

1 LOD组

​ 离远时,细节过多的物体会因太小而模糊不清,最好的方式就是不渲染它们,让CPU内存占用减少,让GPU渲染更为重要的东西。我们也可以提前剔除这些物体,不过突然剔除可能会造成视觉问题,我们可以添加一些中间过渡的状态,Unity使用LOD(level of detail)组来完成。

1.1 LOD组组件

​ 我们可以创建一个空对象,然后添加一个LODGroup组件:

image-20210117155244455.png

​ 百分比即物体在窗口中的占比。我们可以点击任一LOD级别,然后赋予一个要显示的物体。

1.2 LOD过渡

​ LOD组的渐变模式有Cross FadeSpeed 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 RPCommonMaterial.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);
}

​ 然后在GetGILitPassFragment中应用:

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);
}

你可能感兴趣的:(Unity自定义SRP(七):LOD和反射)