URP学习笔记——解读Lighting.hlsl文件

填补去年学习URP时留下的坑

Lighting.hlsl是URP管线中一个重要的组成部分,它包含了URP渲染过程中光照相关的计算

1.概览

1.1 头文件

在正式学习这个文件之前,我们可以先看一看它的头文件,我认为这是学习一段代码如何去实现的一个重要前提

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/BRDF.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Debug/Debugging3D.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/GlobalIllumination.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/RealtimeLights.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/AmbientOcclusion.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DBuffer.hlsl"

通过观察它的头文件,我们就能大概了解这个文件做了什么工作。
可以看到,它使用了BRDF、GI,也就是我们通常用于PBR的计算方法,还有实时光照、AO(环境光遮蔽)这些。

1.2 大纲

看一段代码的大纲可以帮我们快速了解这段代码实现了哪些内容
在这里插入图片描述
使用vscode并将语言切换到hlsl会帮我们自动生成Lighting文件的大纲。
通过大纲我们可以看到,该文件主要是实现了 PBR 和 Blinn-Phong 两种光照计算的方式。
文件的上半部分封装了一系列 LightingXXX 的光照计算方程,而在最后的 UniversalXXX则是我们在Pass的片元着色器中真正调用的函数。

2. PBR Lighting

相比于从根源开始一点点分析,我个人更喜欢刨根问底式的解读方法,所以我会从最上层的封装开始慢慢往下梳理。

我们都知道URP将 PBR 和 Blinn-Phong 分成了两个shader处理,也就是 Lit.shader 和SimpleLit.shader,实际上这两个文件也是大同小异,它们最主要的区别还是在片元着色器中分别调用了 UniversalFragmentPBR() 和 UniversalFragmentBlinnPhong(),我们先来分析PBR的函数。

通过大纲我们可以看到Lighting.hlsl文件里有两个 UniversalFragmentPBR 方法,我们先来看第二个方法

half4 UniversalFragmentPBR(InputData inputData, half3 albedo, half metallic, half3 specular,
    half smoothness, half occlusion, half3 emission, half alpha)
{
    SurfaceData surfaceData;

    surfaceData.albedo = albedo;
    surfaceData.specular = specular;
    surfaceData.metallic = metallic;
    surfaceData.smoothness = smoothness;
    surfaceData.normalTS = half3(0, 0, 1);
    surfaceData.emission = emission;
    surfaceData.occlusion = occlusion;
    surfaceData.alpha = alpha;
    surfaceData.clearCoatMask = 0;
    surfaceData.clearCoatSmoothness = 1;

    return UniversalFragmentPBR(inputData, surfaceData);
}

稍微观察后就可以发现,第二个方法实际上也调用了第一个方法,也就是说,它的作用就是当我们直接传入 rmo 这些参数时,它会帮我们封装成 SurfaceData,不用我们手动封装。

再来看第一个 UniversalFragmentPBR 实现了什么,虽然这个方法有点长,但是宏定义占了相当一部分,也是比较容易理解的。

#if defined(_SPECULARHIGHLIGHTS_OFF)
    bool specularHighlightsOff = true;
    #else
    bool specularHighlightsOff = false;
    #endif

第一个宏,是否开启镜面高光,没啥好说的。

 BRDFData brdfData;
 // NOTE: can modify "surfaceData"...
 InitializeBRDFData(surfaceData, brdfData);

然后是对 BRDF 数据去做一个初始化,我们可以从 BRDF.hlsl 里看到 InitializeBRDFData 的实现

inline void InitializeBRDFData(half3 albedo, half metallic, half3 specular, half smoothness, inout half alpha, out BRDFData outBRDFData)
{
#ifdef _SPECULAR_SETUP
    half reflectivity = ReflectivitySpecular(specular);
    half oneMinusReflectivity = half(1.0) - reflectivity;
    half3 brdfDiffuse = albedo * oneMinusReflectivity;
    half3 brdfSpecular = specular;
#else
    half oneMinusReflectivity = OneMinusReflectivityMetallic(metallic);
    half reflectivity = half(1.0) - oneMinusReflectivity;
    half3 brdfDiffuse = albedo * oneMinusReflectivity;
    half3 brdfSpecular = lerp(kDieletricSpec.rgb, albedo, metallic);
#endif

    InitializeBRDFDataDirect(albedo, brdfDiffuse, brdfSpecular, reflectivity, oneMinusReflectivity, smoothness, alpha, outBRDFData);
}

这部分初始化做了镜面光照的区分,在启用 _SPECULAR_SETUP 的情况下会对漫反射系数 k 进行 specular 的影响,而未启用的情况下则是直接用金属度影响漫反射系数 k。
接下来调用了 InitializeBRDFDataDirect() 方法,也是在相同的文件里。

inline void InitializeBRDFDataDirect(half3 albedo, half3 diffuse, half3 specular, half reflectivity, half oneMinusReflectivity, half smoothness, inout half alpha, out BRDFData outBRDFData)
{
    outBRDFData = (BRDFData)0;
    outBRDFData.albedo = albedo;
    outBRDFData.diffuse = diffuse;
    outBRDFData.specular = specular;
    outBRDFData.reflectivity = reflectivity;

    outBRDFData.perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(smoothness);
    outBRDFData.roughness           = max(PerceptualRoughnessToRoughness(outBRDFData.perceptualRoughness), HALF_MIN_SQRT);
    outBRDFData.roughness2          = max(outBRDFData.roughness * outBRDFData.roughness, HALF_MIN);
    outBRDFData.grazingTerm         = saturate(smoothness + reflectivity);
    outBRDFData.normalizationTerm   = outBRDFData.roughness * half(4.0) + half(2.0);
    outBRDFData.roughness2MinusOne  = outBRDFData.roughness2 - half(1.0);

    // Input is expected to be non-alpha-premultiplied while ROP is set to pre-multiplied blend.
    // We use input color for specular, but (pre-)multiply the diffuse with alpha to complete the standard alpha blend equation.
    // In shader: Cs' = Cs * As, in ROP: Cs' + Cd(1-As);
    // i.e. we only alpha blend the diffuse part to background (transmittance).
    #if defined(_ALPHAPREMULTIPLY_ON)
        // TODO: would be clearer to multiply this once to accumulated diffuse lighting at end instead of the surface property.
        outBRDFData.diffuse *= alpha;
    #endif
}

这部分就是PBR相关计算需要的数据,这里粗糙度的计算如下:
p e r c e p t u a l R o u g h n e s s = 1 − s m o o t h perceptualRoughness = 1 - smooth perceptualRoughness=1smooth
r o u g h n e s s = p e r c e p t u a l R o u g h n e s s 2 roughness = perceptualRoughness^2 roughness=perceptualRoughness2
在做完 BRDF 的初始化工作后,我们将目光继续转回 UniversalFragmentPBR 方法

#if defined(DEBUG_DISPLAY)
    half4 debugColor;

    if (CanDebugOverrideOutputColor(inputData, surfaceData, debugColor))
    {
        return debugColor;
    }
    #endif

接下来是一个宏定义,可以看到这个就是debug的时候显示debugColor。

// Clear-coat calculation...
    BRDFData brdfDataClearCoat = CreateClearCoatBRDFData(surfaceData, brdfData);
    half4 shadowMask = CalculateShadowMask(inputData);
    AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor(inputData, surfaceData);
    uint meshRenderingLayers = GetMeshRenderingLayer();

之后是对透明物的初始化、计算shadowMask、ao,并获取渲染层级,继续往下看

 Light mainLight = GetMainLight(inputData, shadowMask, aoFactor);

这里通过之前初始化得到的全部信息去初始化主光源。

Light GetMainLight()
{
    Light light;
    light.direction = half3(_MainLightPosition.xyz);
#if USE_FORWARD_PLUS
#if defined(LIGHTMAP_ON) && defined(LIGHTMAP_SHADOW_MIXING)
    light.distanceAttenuation = _MainLightColor.a;
#else
    light.distanceAttenuation = 1.0;
#endif
#else
    light.distanceAttenuation = unity_LightData.z; // unity_LightData.z is 1 when not culled by the culling mask, otherwise 0.
#endif
    light.shadowAttenuation = 1.0;
    light.color = _MainLightColor.rgb;

    light.layerMask = _MainLightLayerMask;

    return light;
}

Light GetMainLight(float4 shadowCoord, float3 positionWS, half4 shadowMask)
{
    Light light = GetMainLight();
    light.shadowAttenuation = MainLightShadow(shadowCoord, positionWS, shadowMask, _MainLightOcclusionProbes);

    #if defined(_LIGHT_COOKIES)
        real3 cookieColor = SampleMainLightCookie(positionWS);
        light.color *= cookieColor;
    #endif

    return light;
}

Light GetMainLight(InputData inputData, half4 shadowMask, AmbientOcclusionFactor aoFactor)
{
    Light light = GetMainLight(inputData.shadowCoord, inputData.positionWS, shadowMask);

    #if defined(_SCREEN_SPACE_OCCLUSION) && !defined(_SURFACE_TYPE_TRANSPARENT)
    if (IsLightingFeatureEnabled(DEBUGLIGHTINGFEATUREFLAGS_AMBIENT_OCCLUSION))
    {
        light.color *= aoFactor.directAmbientOcclusion;
    }
    #endif

    return light;
}

从上到下分别获取了光源的位置、方向、距离衰减、阴影衰减、光源的颜色和遮罩等,同时还计算了ao对光源的影响。

MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI);

LightingData lightingData = CreateLightingData(inputData, surfaceData);

lightingData.giColor = GlobalIllumination(brdfData, brdfDataClearCoat, surfaceData.clearCoatMask,
                                              inputData.bakedGI, aoFactor.indirectAmbientOcclusion, inputData.positionWS,
                                              inputData.normalWS, inputData.viewDirectionWS, inputData.normalizedScreenSpaceUV);

这部分是关于 bake 和 GI 的计算,关于 GI 如何去计算可以看我之前写的文章:Unity实现PBR造轮子实践 的 IBL 光照部分

#ifdef _LIGHT_LAYERS
    if (IsMatchingLightLayer(mainLight.layerMask, meshRenderingLayers))
#endif
    {
        lightingData.mainLightColor = LightingPhysicallyBased(brdfData, brdfDataClearCoat,
                                                              mainLight,
                                                              inputData.normalWS, inputData.viewDirectionWS,
                                                              surfaceData.clearCoatMask, specularHighlightsOff);
    }

这部分是主光源颜色的计算,具体如下:

half3 LightingPhysicallyBased(BRDFData brdfData, BRDFData brdfDataClearCoat,
    half3 lightColor, half3 lightDirectionWS, half lightAttenuation,
    half3 normalWS, half3 viewDirectionWS,
    half clearCoatMask, bool specularHighlightsOff)
{
    half NdotL = saturate(dot(normalWS, lightDirectionWS));
    half3 radiance = lightColor * (lightAttenuation * NdotL);

    half3 brdf = brdfData.diffuse;
#ifndef _SPECULARHIGHLIGHTS_OFF
    [branch] if (!specularHighlightsOff)
    {
        brdf += brdfData.specular * DirectBRDFSpecular(brdfData, normalWS, lightDirectionWS, viewDirectionWS);

#if defined(_CLEARCOAT) || defined(_CLEARCOATMAP)
        // Clear coat evaluates the specular a second timw and has some common terms with the base specular.
        // We rely on the compiler to merge these and compute them only once.
        half brdfCoat = kDielectricSpec.r * DirectBRDFSpecular(brdfDataClearCoat, normalWS, lightDirectionWS, viewDirectionWS);

            // Mix clear coat and base layer using khronos glTF recommended formula
            // https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_materials_clearcoat/README.md
            // Use NoV for direct too instead of LoH as an optimization (NoV is light invariant).
            half NoV = saturate(dot(normalWS, viewDirectionWS));
            // Use slightly simpler fresnelTerm (Pow4 vs Pow5) as a small optimization.
            // It is matching fresnel used in the GI/Env, so should produce a consistent clear coat blend (env vs. direct)
            half coatFresnel = kDielectricSpec.x + kDielectricSpec.a * Pow4(1.0 - NoV);

        brdf = brdf * (1.0 - clearCoatMask * coatFresnel) + brdfCoat * clearCoatMask;
#endif // _CLEARCOAT
    }
#endif // _SPECULARHIGHLIGHTS_OFF

    return brdf * radiance;
}

具体实现的方法对应 Unity实现PBR造轮子实践 中的直接光实现部分

#if defined(_ADDITIONAL_LIGHTS)
    uint pixelLightCount = GetAdditionalLightsCount();

    #if USE_FORWARD_PLUS
    for (uint lightIndex = 0; lightIndex < min(URP_FP_DIRECTIONAL_LIGHTS_COUNT, MAX_VISIBLE_LIGHTS); lightIndex++)
    {
        FORWARD_PLUS_SUBTRACTIVE_LIGHT_CHECK

        Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor);

#ifdef _LIGHT_LAYERS
        if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
#endif
        {
            lightingData.additionalLightsColor += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light,
                                                                          inputData.normalWS, inputData.viewDirectionWS,
                                                                          surfaceData.clearCoatMask, specularHighlightsOff);
        }
    }
    #endif

    LIGHT_LOOP_BEGIN(pixelLightCount)
        Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor);

#ifdef _LIGHT_LAYERS
        if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
#endif
        {
            lightingData.additionalLightsColor += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light,
                                                                          inputData.normalWS, inputData.viewDirectionWS,
                                                                          surfaceData.clearCoatMask, specularHighlightsOff);
        }
    LIGHT_LOOP_END
    #endif

这一部分是对增强光影响的计算。

#if defined(_ADDITIONAL_LIGHTS_VERTEX)
    lightingData.vertexLightingColor += inputData.vertexLighting * brdfData.diffuse;
    #endif

对顶点光的计算。

#if REAL_IS_HALF
    // Clamp any half.inf+ to HALF_MAX
    return min(CalculateFinalColor(lightingData, surfaceData.alpha), HALF_MAX);
#else
    return CalculateFinalColor(lightingData, surfaceData.alpha);
#endif

最后会判断是否是半透物体,然后将之前得到的光照结果全部加起来。

3. Phong Lighting

Blinn Phong对光照的计算比PBR简单太多了,还是老规矩,先看 UniversalFragmentBlinnPhong 方法:

half4 UniversalFragmentBlinnPhong(InputData inputData, half3 diffuse, half4 specularGloss, half smoothness, half3 emission, half alpha, half3 normalTS)
{
    SurfaceData surfaceData;

    surfaceData.albedo = diffuse;
    surfaceData.alpha = alpha;
    surfaceData.emission = emission;
    surfaceData.metallic = 0;
    surfaceData.occlusion = 1;
    surfaceData.smoothness = smoothness;
    surfaceData.specular = specularGloss.rgb;
    surfaceData.clearCoatMask = 0;
    surfaceData.clearCoatSmoothness = 1;
    surfaceData.normalTS = normalTS;

    return UniversalFragmentBlinnPhong(inputData, surfaceData);
}

同样重写了 UniversalFragmentBlinnPhong 对 SurfaceData 进行封装。
直接看实现部分:

#if defined(DEBUG_DISPLAY)
    half4 debugColor;

    if (CanDebugOverrideOutputColor(inputData, surfaceData, debugColor))
    {
        return debugColor;
    }
    #endif

    uint meshRenderingLayers = GetMeshRenderingLayer();
    half4 shadowMask = CalculateShadowMask(inputData);
    AmbientOcclusionFactor aoFactor = CreateAmbientOcclusionFactor(inputData, surfaceData);
    Light mainLight = GetMainLight(inputData, shadowMask, aoFactor);

    MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI, aoFactor);

    inputData.bakedGI *= surfaceData.albedo;
	
	LightingData lightingData = CreateLightingData(inputData, surfaceData);

一大堆的初始化计算,和 PBR 几乎一样。

#ifdef _LIGHT_LAYERS
    if (IsMatchingLightLayer(mainLight.layerMask, meshRenderingLayers))
#endif
    {
        lightingData.mainLightColor += CalculateBlinnPhong(mainLight, inputData, surfaceData);
    }

计算Color部分,具体如下:

half3 CalculateBlinnPhong(Light light, InputData inputData, SurfaceData surfaceData)
{
    half3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);
    half3 lightDiffuseColor = LightingLambert(attenuatedLightColor, light.direction, inputData.normalWS);

    half3 lightSpecularColor = half3(0,0,0);
    #if defined(_SPECGLOSSMAP) || defined(_SPECULAR_COLOR)
    half smoothness = exp2(10 * surfaceData.smoothness + 1);

    lightSpecularColor += LightingSpecular(attenuatedLightColor, light.direction, inputData.normalWS, inputData.viewDirectionWS, half4(surfaceData.specular, 1), smoothness);
    #endif

#if _ALPHAPREMULTIPLY_ON
    return lightDiffuseColor * surfaceData.albedo * surfaceData.alpha + lightSpecularColor;
#else
    return lightDiffuseColor * surfaceData.albedo + lightSpecularColor;
#endif
}

分别计算了 Blinn-Phong 着色的两个部分——漫反射和高光。

后续的相关计算(和PBR类似):

 LIGHT_LOOP_BEGIN(pixelLightCount)
        Light light = GetAdditionalLight(lightIndex, inputData, shadowMask, aoFactor);
#ifdef _LIGHT_LAYERS
        if (IsMatchingLightLayer(light.layerMask, meshRenderingLayers))
#endif
        {
            lightingData.additionalLightsColor += CalculateBlinnPhong(light, inputData, surfaceData);
        }
    LIGHT_LOOP_END
    #endif

    #if defined(_ADDITIONAL_LIGHTS_VERTEX)
    lightingData.vertexLightingColor += inputData.vertexLighting * surfaceData.albedo;
    #endif

    return CalculateFinalColor(lightingData, surfaceData.alpha);

4. 后记

从去年年底我开始着手于学习 URP,但之后因工作变动的原因也不再喜欢更新文章了。这半年也是断断续续学了很多很杂的知识,以后还是不定期更新一些渲染方面的文章,希望给阅读我的文章的人一些启发吧。

  • 29
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这个报错的意思是这个着色器脚本中必须同时包含顶点着色器和片元着色器,但是它只找到了其中一个,所以编译失败。 为了解决这个问题,你需要在着色器脚本中同时包含顶点着色器和片元着色器的代码。你可以检查一下你的着色器脚本是否有缺失顶点着色器或片元着色器的部分。如果有,你需要将其添加到脚本中。 在这个着色器脚本中,你需要在Pass块中添加顶点着色器和片元着色器的代码。其中,顶点着色器的函数名为"vert",片元着色器的函数名为"frag"。你需要将这两个函数添加到Pass块中,并且在顶点着色器中对输入的顶点数据进行处理,并将处理后的数据传递给片元着色器进行渲染。 以下是修改后的代码: ``` Shader "Custom/URP Blinking" { Properties { _MainTex ("Texture", 2D) = "white" {} _Color ("Color", Color) = (1,1,1,1) _BlinkSpeed ("Blink Speed", Range(0.1, 10)) = 1 } SubShader { Tags {"Queue"="Transparent" "RenderType"="Opaque"} Pass { HLSLPROGRAM #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" CBUFFER_START(UnityPerMaterial) UNITY_DEFINE_PER_MATERIAL_MAIN_LIGHT_SHADOW(UnityPerMaterialShadow) float4 _Color; float _BlinkSpeed; float _Time; float4 _MainTex_ST; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); CBUFFER_END struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { float4 tex = UnitySampleTexture(sampler_MainTex, _MainTex, i.uv * _MainTex_ST.xy + _MainTex_ST.zw); float t = _Time * _BlinkSpeed; float blink = sin(t) * 0.5 + 0.5; fixed4 col = lerp(_Color, float4(1,1,1,1), blink); return tex * col; } ENDHLSL CGPROGRAM #pragma vertex vert #pragma fragment frag ENDCG } } FallBack "Diffuse" } ``` 请注意,在这里,我们将顶点着色器和片元着色器的代码分别放置在了HLSLPROGRAM和CGPROGRAM标签中,并使用#pragma vertex和#pragma fragment指令来告诉Unity哪一个是顶点着色器,哪一个是片元着色器。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值