翻译15 Unity Deferred Lights - 延迟光照

自定义灯光渲染
解码LDR颜色
增长独立Pass渲染光
支持方向光、点光源、聚光灯
手动采样阴影纹理
Unity 5.6.6f1
数组

1 Light Shader

在G-Buffers填充完毕后,而后渲染光。本篇先介绍Unity是如何渲染光,以及实现本身Shader的光渲染。在Edit / Project Settings / Graphics 去掉默认的Shader。cookie

1.1 Using a Custom Shader

每一个deferred光都是在一个独立的Pass修改屏幕图像(后处理Image)完成渲染。建立一个Shader而后指定到Built-In shader settingsapp

image

图1 修改内置的Shader编辑器

1.2 A Second Pass

修改以后,编辑器大量报错.ide

image

图2 least 2 passes函数

先简单复制第一个Pass解决错误,结果是屏幕内除了天空盒外全部物体被渲染成黑色了。这是由于使用了stencil-buffer。性能

报错的缘由:为何须要第二个Pass?
    当HDR禁用时,光照数据会被使用对数编码计算,而后在(第二个)最终的pass解码该数据。因此必需要增长Pass。当禁用HDR时就能调用第二个Pass,但此时天空也变黑了。优化

1.3 Avoiding the Sky

当在LDR(HDR禁用)模式,天空变黑了。这是由于转换过程当中没有正确使用stencil-buffer模板掩码。在第二个Pass中配置:应该只渲染不属于背景的片断,可经过_StencilNonBackground提供适当的模板值。ui

Pass
{
    Stencil
    {
        Ref[_StencilNonBackground]
        ReadMask[_StencilNonBackground]
        CompBack Equal
        CompFront Equal
    }
}

1.4 Converting Colors

在第二个Pass的light-buffer转换光照数据,方法就似Fog shader:用输入源的Image UV坐标采样buffer来绘制一个覆盖全屏的quad编码

struct VertexData {
    float4 vertex : POSITION;
   float2 uv : TEXCOORD0;
};
struct Interpolators {
    float4 pos : SV_POSITION;
 float2 uv : TEXCOORD0;
};
Interpolators VertexProgram (VertexData v) {
    Interpolators i;
    i.pos = UnityObjectToClipPos(v.vertex);
    i.uv = v.uv;
    return i;
}

该light buffer经过名为_LightBuffer变量提供给Shader

sampler2D _LightBuffer;
…
float4 FragmentProgram (Interpolators i) : SV_Target {
    return tex2D(_LightBuffer, i.uv);
}

LDR颜色使用指数编码:2-C,使用对数解码-log2C

return -log2(tex2D(_LightBuffer, i.uv));

2 Directional Lights

新增一个cginc文件,引入第一个pass。要把渲染的光照增长到图像上,必须确保不能擦除已渲染的图像,所以改变混合模式要彻底合并源颜色和目标颜色。

Blend One One

也须要全部可能的光照配置shader variants变体,该编译指令:multi_compile_lightpass会建立全部包含的变体。而后再增长一个HDR_ON的指令。

#pragma multi_compile_lightpass
#pragma multi_compile _ UNITY_HDR_ON

2.1 G-Buffer UV Coordinates

须要用UV坐标从G-buffers采样,不幸的是,该light pass通道unity不支持提供该坐标。解决办法:从clip-space传递过来,使用ComputeScreenPos函数计算,返回一个float4的齐次坐标。

v2f VertexProgram(appdata v)
{
     v2f o;
     o.pos = UnityObjectToClipPos(v.vertex);
     o.uv = ComputeScreenPos(o.pos);
     return o;
}

而后在fragment就能计算最终的2D坐标。必须在fragment计算。见翻译7

fixed4 FragmentProgram(v2f i) : SV_Target
{
    float2 uv = i.uv.xy / i.uv.w;
    return 0;
}

2.2 World Position

与上篇deferred fog中类似,须要计算从相机到片元的距离:从相机原点发射射线经过片元(给定方向)到达far-plane,而后再用fragment深度缩放射线。用该方法重建片元的世界坐标

1首先。对于方向光,从quad的四顶点发出的射线做为法向量提供。因此能够经过顶点程序对射线进行插值。

struct VertexData {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
};

struct Interpolators {
    float4 pos : SV_POSITION;
    float4 uv : TEXCOORD0;
    float3 ray : TEXCOORD1;
};

Interpolators VertexProgram (VertexData v) {
    Interpolators i;
    i.pos = UnityObjectToClipPos(v.vertex);
    i.uv = ComputeScreenPos(i.pos);
    i.ray = v.normal;
    return i;
}

2其次。在fragment函数经过采样_CameraDepthTexture纹理和线性化计算能够获得depth值,相似于deferred fog计算

//Unity提供的声明函数,等于 sampler2D _CameraDepthTexture; 定义在UnityCG
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);

float4 FragmentProgram (Interpolators i) : SV_Target {
    float2 uv = i.uv.xy / i.uv.w;
 float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv); depth = Linear01Depth(depth);
    return 0;
}

3而后。与deferred fog最大的不一样:fog shader须要射线到达far plane;而本shader的射线只能到达near plane。因此必需要缩放射线以便它能达到far-plane:缩放射线使Z坐标变为1,并与远平面距离相乘。

depth = Linear01Depth(depth);
float3 rayToFarPlane = i.ray * _ProjectionParams.z / i.ray.z;

4再接着。按深度值缩放射线一次获得一个坐标。该射线被定义在视图空间,它是camera的本地空间。所以,射线也以片断在视图空间中的坐标结束。

float3 rayToFarPlane = i.ray * _ProjectionParams.z / i.ray.z;
float3 viewPos = rayToFarPlane * depth;

5最后。再使用unity_CameraToWorld内置矩阵从view视图空间转换到world世界坐标,该矩阵定义在ShaderVariables.cginc

float3 viewPos = rayToFarPlane * depth;
float3 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1)).xyz;

2.3 Reading G-Buffer Data

获取World Pos后。经过访问G-buffer检索properties,该buffer可从内置的_CamearGBufferTexture变量获取

sampler2D _CameraGBufferTexture0;
sampler2D _CameraGBufferTexture1;
sampler2D _CameraGBufferTexture2;

在上一篇Defferred Shading中也手动计算过G-buffer,此次直接读取_CameraGBufferTexture现成的albedo、specular、smoothness、normal

float3 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1)).xyz;
float3 albedo = tex2D(_CameraGBufferTexture0, uv).rgb;
float3 specularTint = tex2D(_CameraGBufferTexture1, uv).rgb;//合并
float3 smoothness = tex2D(_CameraGBufferTexture1, uv).a;//合并
float3 normal = tex2D(_CameraGBufferTexture2, uv).rgb * 2 - 1;

2.4 Computing BRDF

引入BRDF函数,定义在UnityPBSLighting.cginc中

首先计算视野方向

float3 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1)).xyz;
float3 viewDir = normalize(_WorldSpaceCameraPos - worldPos);
其次是表面反射,这可从specular颜色获取,使用SpecularStrength函数提取。
//。。。
float oneMinusReflectivity = 1 - SpecularStrength(specularTint);

而后传递光照数据,初始化直接光和间接光

float oneMinusReflectivity = 1 - SpecularStrength(specularTint);
//。。。
UnityLight light;
light.color = 0;
light.dir = 0;

UnityIndirect indirectLight;
indirectLight.diffuse = 0;
indirectLight.specular = 0;

最后计算最终的颜色

indirectLight.specular = 0;
float4 color = UNITY_BRDF_PBS
(
    albedo, specularTint, oneMinusReflectivity, smoothness,
    normal, viewDir, light, indirectLight
);
return color;

2.5 Configuring the Light

由于间接光呈现的是黑色的,在这里不适用。可是直接光必须被配置成与当前渲染的光相匹配。对于方向光,须要它的颜色和方向。这两个变量能够经过_LightColor和_LightDir变量得到。

float4 _LightColor, _LightDir;

UnityLight CreateLight () {
    UnityLight light;
    light.dir = _LightDir;
    light.color = _LightColor.rgb;
    return light;
}

    UnityLight light = CreateLight();
//    light.color = 0;
//    light.dir = 0;

image

光照方向错误

计算获得最终的光照,但光的方向错误了。缘由:_LightDir是光到表面的方向。在CreateLight计算中须要表面到光的方向

light.dir = -_LightDir;

image

正确,没有阴影

2.6 Shadows

在本身的cginc文件中,咱们依靠AutoLight中的宏来肯定由阴影引发的光衰减。 不幸的是,该文件在编写时并无考虑到延迟的光线。 如今将本身进行阴影采样,可经过_ShadowMapTexture变量访问阴影贴图。

sampler2D _ShadowMapTexture;

可是,咱们不能随意声明此变量。 它已经在UnityShadowLibrary中为点和聚光灯阴影定义了它。 所以,咱们不该该本身定义它,除非使用方向光阴影。

#if defined (SHADOWS_SCREEN)
    sampler2D _ShadowMapTexture;
#endif

要应用方向光阴影,须要采样阴影纹理并使用它来减弱光色便可。 在CreateLight中计算就须要把UV坐标参数。

UnityLight CreateLight (float2 uv) {
    UnityLight light;
    light.dir = -_LightDir;
    float shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
    light.color = _LightColor.rgb * shadowAttenuation;
    return light;
}

UnityLight light = CreateLight(uv);

image

有阴影的方向光

固然,这仅在定向光启用了阴影时才有效。 若是不是,则阴影衰减始终为1。

float shadowAttenuation = 1;
#if defined(SHADOWS_SCREEN)
    shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
#endif
light.color = _LightColor.rgb * shadowAttenuation;

2.7 Fading Shadows

阴影贴图应该是有限的,它覆盖的面积越大,阴影的分辨率越低。 Unity提供了绘制阴影的最大距离,此距离能够经过Edit / Project Settings / Quality进行调整。

image

阴影距离配置

当阴影几乎快达到了该限定距离就会淡出,Unity内置的shader是这样设定并计算。因为我将手动采样该阴影纹理,当到达纹理的边缘时阴影会被截取,结果是阴影虽然消失了,但有被急剧切割的生硬画面。

image

长、短距离阴影对比

要渐隐阴影,首先要知道的是阴影彻底消失的距离。该距离又依赖于阴影投射方向。在Stable Fit模式下,以map的中心点呈球面形开始渐隐消失阴影;在Close Fit模式它是依赖于视野深度。

UnityComputeShadowFadeDistance函数能计算出正确距离,它须要两个参数:world pos 和 view depth;而后返回距离A。 注意:该距离A是从阴影纹理的中心点位置或者未更改的视野深度开始计算的。

UnityLight CreateLight (float2 uv, float3 worldPos, float viewZ) {
    UnityLight light;
    light.dir = -_LightDir;
    float shadowAttenuation = 1;
    #if defined(SHADOWS_SCREEN)
        shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
        float shadowFadeDistance = UnityComputeShadowFadeDistance(worldPos, viewZ);
    #endif
    light.color = _LightColor.rgb * shadowAttenuation;
    return light;
}

阴影应该是快要接近渐隐距离时开始消失,一旦到达就彻底消失。UnityComputeShadowFade函数计算合适的消失因子。

float shadowFadeDistance =    UnityComputeShadowFadeDistance(worldPos, viewZ);
float shadowFade = UnityComputeShadowFade(shadowFadeDistance);

UnityComputeShadowFade定义在UnityShadowLibrary.cginc,见下:

float UnityComputeShadowFadeDistance (float3 wpos, float z) {
    float sphereDist = distance(wpos, unity_ShadowFadeCenterAndType.xyz);
    return lerp(z, sphereDist, unity_ShadowFadeCenterAndType.w);
}

half UnityComputeShadowFade(float fadeDist) {
    return saturate(fadeDist * _LightShadowData.z + _LightShadowData.w);
}
View Code

阴影渐隐值范围是[0, 1],该值决定了阴影要消失多少。实际的消失值能够加到阴影衰减之上并限定在[0, 1]以内

float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
shadowAttenuation = saturate(shadowAttenuation + shadowFade);

最后,提供世界坐标和视图深度在片元程序中建立光照。视图深度是片元在视图空间中的位置的Z份量。

UnityLight light = CreateLight(uv, worldPos, viewPos.z);

image

阴影渐隐

2.8 Light Cookies

支持Cookies纹理,使用变量_LightTexture0访问;同时还要从world-space转换到light-space,最后采样。转换矩阵使用unity_WorldToLight矩阵变量

sampler2D _LightTexture0;
float4x4 unity_WorldToLight;

CreateLight,使用上述矩阵变量转换world-space到light-space;而后使用转换后的坐标采样cookie纹理。cookie也要衰减,须要单独定义并使用。

light.dir = -_LightDir;
float attenuation = 1;
float shadowAttenuation = 1;
#if defined(DIRECTIONAL_COOKIE)
    float2 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1)).xy;
    attenuation *= tex2D(_LightTexture0, uvCookie).w;
#endif
    …
light.color = _LightColor.rgb * (attenuation * shadowAttenuation);

image

带有cookie的方向光

总体结果彷佛能够,可是观察边缘彷佛有硬边

image

硬边过渡

相邻片元的cookie坐标的巨大差别就会致使该问题出现。在这种状况下,GPU选择的mipmap级别对于最近的表面是low level。解决办法之一就是:在采样mip映射时应用偏移。大神的总结

attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie, 0, -8)).w;

image

偏移采样

2.9 Supporting LDR

上述只支持HDR,如今来支持LDR。步骤以下:

首先,编码后的LDR颜色要乘如light-buffer,而不是加法。这能够用:Blend DstColor Zero实现。注意只用该Blend mode会引发HDR的错误。因此须要灵活配置:Blend [_SrcBlend] [_DstBlend]

而后,使用2-c函数解码

float4 color = UNITY_BRDF_PBS(
        albedo, specularTint, oneMinusReflectivity, smoothness,
        normal, viewDir, light, indirectLight
);
    #if !defined(UNITY_HDR_ON)
        color = exp2(-color);
    #endif
return color;

3 Spotlights

由于方向光会影响到场景内全部物体,因此被画成全屏quad。相比之下,聚光灯只会影响位于圆锥体内的部分物体。一般不须要计算整个图像的聚光灯光照,将绘制一个与聚光灯的影响范围相匹配的金字塔体。

3.1 Drawing a Pyramid

禁用方向灯,改用聚光灯。由于着色器只对方向光正确工做,那么如今的结果会出现错误。可是它仍可让你看到金字塔的哪些部分被渲染了。

image

渲染范围

根据上图,金字塔是做为一个普通的3D对象呈现的。它的背面被剔除,因此咱们能够看到金字塔的正面。只有当它前面没有东西的时候,它才会被画出来。除此以外,还添加了一个pass,用于设置模板缓冲区,以将绘图限制为位于金字塔卷内的片断。您能够经过frame-debugger来验证。

image

剔除方式

这意味着咱们的着色器的culling和z-test设置被否弃了。 所以将其从着色器中删除。

Blend [_SrcBlend] [_DstBlend]
//Cull Off
//ZTest Always
ZWrite Off

当聚光灯的体积距离相机足够远时,此方法适用。 可是,当聚光灯离摄像机太近时,它会失败。 发生这种状况时,相机可能会进入了该体积内。 甚至有可能将近平面的一部分置于其内部,而将其他部分置于其外部,与近平面相交了。 在这些状况下,模板缓冲区不能用于限制渲染。

仍然渲染光照的技巧是绘制金字塔的内表面,而不是金字塔的外表面。 这是经过渲染其背面而不是其正面来完成的。 并且,仅当这些表面最终位于已渲染的表面以后时才渲染它们。 这种方法还涵盖了聚光灯体积内的全部片断。 但这最终致使渲染了太多的碎片,由于一般金字塔的一般隐藏部分也将被渲染。 所以,仅在必要时执行。

image image

当靠近相机时,要绘制背面才正确

3.2 Supporting Multiple Light Types

目前,CreateLight只能用于方向光。让咱们确保特定于方向灯的代码只在适当的时候使用。

UnityLight CreateLight (float2 uv, float3 worldPos, float viewZ) {
    UnityLight light;

   
   
   
   
     // light.dir = -_LightDir; 
   
    float attenuation = 1;
    float shadowAttenuation = 1;

    #if defined(DIRECTIONAL) || defined(DIRECTIONAL_COOKIE)
        light.dir = -_LightDir;

        #if defined(DIRECTIONAL_COOKIE)
            float2 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1)).xy;
            attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie, 0, -8)).w;
        #endif

        #if defined(SHADOWS_SCREEN)
            shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
            float shadowFadeDistance = UnityComputeShadowFadeDistance(worldPos, viewZ);
            float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
            shadowAttenuation = saturate(shadowAttenuation + shadowFade);
        #endif
    #else
        light.dir = 1;
    #endif

    light.color = _LightColor.rgb * (attenuation * shadowAttenuation);
    return light;
}

尽管阴影衰落基于方向阴影贴图,可是其余类型的阴影也应该会被渐隐。 这样能够确保全部阴影都以相同的方式渐隐,而不只仅是某些阴影。 所以,只要有阴影,阴影淡入淡出代码便适用于全部灯光。 所以,让咱们将该代码移到特定于光源的块以外。

咱们可使用布尔值来控制是否使用阴影淡出代码。因为布尔值是一个常数值,若是它仍然为假,代码将被删除。

UnityLight CreateLight (float2 uv, float3 worldPos, float viewZ) {
    UnityLight light;
    float attenuation = 1;
    float shadowAttenuation = 1;
    bool shadowed = false;
    #if defined(DIRECTIONAL) || defined(DIRECTIONAL_COOKIE)
        …省略代码
        #if defined(SHADOWS_SCREEN)
            shadowed = true;
            shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
//          float shadowFadeDistance = UnityComputeShadowFadeDistance(worldPos, viewZ);
//          float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
//          shadowAttenuation = saturate(shadowAttenuation + shadowFade);
        #endif
    #else
        light.dir = 1;
    #endif

    if (shadowed) {
        float shadowFadeDistance = UnityComputeShadowFadeDistance(worldPos, viewZ);
        float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
        shadowAttenuation = saturate(shadowAttenuation + shadowFade);
    }

    light.color = _LightColor.rgb * (attenuation * shadowAttenuation);
    return light;
}

非方向灯光都有一个position变量。它经过内置的_LightPos提供。

float4 _LightColor, _LightDir, _LightPos;

如今能够肯定聚光灯的光向量得出光方向。

#else
    float3 lightVec = _LightPos.xyz - worldPos;
    light.dir = normalize(lightVec);
#endif

3.3 World Position Agin

结果为黑色,彷佛光线方向不正确。 发生这种状况是由于聚光灯的世界位置计算不正确。 当咱们在场景中的某个地方渲染金字塔时,不像方向光那样渲染全屏quad将光线存储在normal通道中。 而必须是经由Vertex-Program从顶点的位置发射射线,经过将顶点的pos转换到view-space完成计算,为此,咱们可使用UnityObjectToViewPos函数。

i.ray = UnityObjectToViewPos(v.vertex);

然而,这会产生方向错误的光线。咱们要消去它们的X和Y坐标。

i.ray = UnityObjectToViewPos(v.vertex) * float3(-1, -1, 1);

image

正确的世界位置

再次看看UnityObjectToViewPos内部实现

inline float3 UnityObjectToViewPos (in float3 pos) {
    return mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, float4(pos, 1.0))).xyz;
}

当渲染方向光时,应该只使用顶点法线。当渲染非方向灯之外的光几什么时候,须要把顶点pos转到view-space计算。Unity经过_LightAsQuad变量告诉咱们正在处理哪一种状况。

若是_LightAsQuad被设为1,则处理的是方向光quad而且可使用法线。不然,咱们必须使用UnityObjectToViewPos。插值好过if ==> from + (to – from)*t,t为1直接使用法线,为0直接计算到view-space

i.ray = lerp
(
    UnityObjectToViewPos(v.vertex) * float3(-1, -1, 1),
    v.normal,
    _LightAsQuad
);

3.4 Cookie Attenuation

聚光灯的锥形衰减是经过cookie纹理建立的,不管是默认的圆形仍是定制的cookie。咱们能够从复制定向光的cookie代码,仿照着写。也是存储在_LightTexture0

float3 lightVec = _LightPos.xyz - worldPos;
light.dir = normalize(lightVec);
float2 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1)).xy;
attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie, 0, -8)).w;

可是,聚光灯Cookie越远离灯光位置,它就会变得越大。 这是因为经过透视变换形成的。 所以,矩阵乘法会产生4D齐次坐标。 为了获得规则的2D坐标,咱们必须将X和Y除以W。

float4 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1));
uvCookie.xy /= uvCookie.w;
attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie.xy, 0, -8)).w;

image

cookie衰减

上图实际上产生了两个光锥,一个向前一个向后。 后向圆锥一般在渲染区域以外结束,但这并不能保证。咱们只须要前向锥,它对应于负的W坐标。

attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie.xy, 0, -8)).w;
attenuation *= uvCookie.w < 0;

3.5 Distance Attenuation

聚光灯发出的光也会根据距离衰减。此衰减存储在查找纹理中,可经过_LightTextureB0使用该纹理。

sampler2D _LightTexture0, _LightTextureB0;

纹理被设计成必须使用光的距离的平方,并按光的范围进行缩放,做为UV进行采样。范围存储在_LightPos的第四个份量中。采样获得的纹理应该使用哪一个通道在不一样的平台,由UNITY_ATTEN_CHANNEL宏定义。

light.dir = normalize(lightVec);
attenuation *= tex2D
(
    _LightTextureB0,
    (dot(lightVec, lightVec) * _LightPos.w).rr
).UNITY_ATTEN_CHANNEL;
float4 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1));

image

cookie 和 distance衰减

3.6 Shadows

当聚光灯有阴影时,定义SHADOWS_DEPTH关键字。

//在CreateLight中
float4 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1));
uvCookie.xy /= uvCookie.w;
attenuation *= tex2Dbias(_LightTexture0, float4(uvCookie.xy, 0, -8)).w;

#if defined(SHADOWS_DEPTH)
    shadowed = true;
#endif

聚光灯和方向灯使用相同的变量来采样阴影贴图。在聚光灯的状况下,可使用内置UnitySampleShadowmap来处理采样硬阴影或软阴影的细节。参数:阴影空间中的片元位置。unity_WorldToShadow(4x4)矩阵中第一个数组能够用来将世界空间转换为阴影空间。

shadowed = true;
shadowAttenuation = UnitySampleShadowmap(
    mul(unity_WorldToShadow[0], float4(worldPos, 1))
);

4 Point Lights

点光源使用与聚光灯相同的光向量、方向和距离衰减。这样他们就能够共享代码。应该只在定义SPOT关键字时使用spotlight代码的其他部分。

    #if defined(DIRECTIONAL) || defined(DIRECTIONAL_COOKIE)
        …
    #else
        float3 lightVec = _LightPos.xyz - worldPos;
        light.dir = normalize(lightVec);

        attenuation *= tex2D(
            _LightTextureB0,
            (dot(lightVec, lightVec) * _LightPos.w).rr
        ).UNITY_ATTEN_CHANNEL;

        #if defined(SPOT)
            float4 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1));
            uvCookie.xy /= uvCookie.w;
            attenuation *=
                tex2Dbias(_LightTexture0, float4(uvCookie.xy, 0, -8)).w;
            attenuation *= uvCookie.w < 0;

            #if defined(SHADOWS_DEPTH)
                shadowed = true;
                shadowAttenuation = UnitySampleShadowmap(
                    mul(unity_WorldToShadow[0], float4(worldPos, 1))
                );
            #endif
        #endif
    #endif

这已经足够让点光源工做了。它们被渲染成和聚光灯同样的效果,除了渲染范围使用的是球形而不是锥形。

image

高亮

4.1 Shadows

点光源的阴影存储在一个CubeMap。内置UnitySampleShadowmap可采样。参数:光的方向。一个从光到表面的向量。它是光的相反方向。

#if defined(SPOT)
…
#else
    #if defined(SHADOWS_CUBE)
        shadowed = true;
        shadowAttenuation = UnitySampleShadowmap(-lightVec);
    #endif
#endif

image

点光源阴影

4.2 Cookies

Point light cookie也能够经过_LightTexture0得到。须要的是一个cubeMap映射,而不是常规的纹理。

//sampler2D _LightTexture0, _LightTextureB0;
#if defined(POINT_COOKIE)
    samplerCUBE _LightTexture0;
#else
    sampler2D _LightTexture0;
#endif

sampler2D _LightTextureB0;
float4x4 unity_WorldToLight;

要对cookie进行采样,请将片断的world-space转换为light-space,并使用光照空间对立方体映射进行采样。

#else
    #if defined(POINT_COOKIE)
        float3 uvCookie = mul(unity_WorldToLight, float4(worldPos, 1)).xyz;
        attenuation *= texCUBEbias(_LightTexture0, float4(uvCookie, -8)).w;
    #endif

    #if defined(SHADOWS_CUBE)
        shadowed = true;
        shadowAttenuation = UnitySampleShadowmap(-lightVec);
    #endif
#endif

image

点光源cookie

4.3 Skipping Shadows

如今,咱们可使用本身的着色器渲染全部动态光源。 尽管咱们目前并未对优化进行太多关注,但仍有一项潜在的大型优化值得考虑最终超出阴影渐隐距离的片元将不会被阴影化。 可是如今仍在采样它们的阴影,这可能很昂贵。 咱们能够经过基于阴影衰落因子进行UNITY_BRANCH分支来避免这种状况。 它接近1,那么咱们能够彻底跳过阴影衰减。

if (shadowed) {
    float shadowFadeDistance = UnityComputeShadowFadeDistance(worldPos, viewZ);
    float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
    shadowAttenuation = saturate(shadowAttenuation + shadowFade);
    UNITY_BRANCH
    if (shadowFade > 0.99) {
        shadowAttenuation = 1;
    }
}

可是,即便用了UNITY_BRANCH分支它自己也很昂贵。除了靠近阴影区域的边缘,全部碎片都落在阴影区域的内部或外部。 但这仅在GPU能够利用这一点的状况下才重要。 在这种状况下,使用HLSLSupport.cginc定义UNITY_FAST_COHERENT_DYNAMIC_BRANCHING宏。

#if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING)
    UNITY_BRANCH
    if (shadowFade > 0.99) {
        shadowAttenuation = 1;
    }
#endif

即便这样,仅当阴影须要多个纹理样本时才值得使用。 对于柔和的聚光灯和点光源阴影,进一步使用用SHADOWS_SOFT关键字指示。 而方向光阴影始终只须要单个纹理,所以它性能很便宜。

#if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT)
    UNITY_BRANCH
    if (shadowFade > 0.99) {
        shadowAttenuation = 1;
    }
#endif