渲染5——多盏灯

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wodownload2/article/details/82223412

每个物体受多盏灯影响
支持不同的灯光类型
使用灯光cookies
计算顶点灯
引入球协光

本节为渲染得第五节课程。之前的一节主要介绍一盏平行光的使用。本节将会介绍如何使用更多的灯。
本节课程使用unity的版本为5.4.0b21。
这里写图片描述
上图为使用了多盏灯照亮一个球体的图片。

1包含文件
为了支持多盏灯,我们必须新加一个通道。但是这些通道的代码有重复性,所以为了避免冗余,我们把这些重复的代码单独拎出来,成为一个可被incude的头文件。
unity不提供建头文件的命令,所以必须自己手动建立如MyLighting.cginc形式的头文件。
如下:
这里写图片描述
这里定义一个包含文件的宏,防止多次被引入。里面的内容为空,后面会逐渐添加公用的代码。

2 第二盏灯
我们的第二盏灯也是一个平行光。我们复制上节的平行光,然后调整旋转、强度、颜色,使其区别于第一盏灯,unity会根据强度来决定哪个是主灯。
单独的第一盏灯的时候效果为:
这里写图片描述

单独的第二盏灯效果:
这里写图片描述

而同时开两盏灯于第一盏的效果是一样的,究其原因是因为没有额外的通道。

2.1 第二个通道
forwardbase通道只能计算一盏主灯的效果。其他的灯需要在forwardadd中计算。
所以我们需要加一个名字叫forwardadd的通道,不区分大小写。

Pass 
{
        Tags 
        {
            "LightMode" = "ForwardAdd"
        }

        CGPROGRAM

        #pragma target 3.0

        #pragma vertex MyVertexProgram
        #pragma fragment MyFragmentProgram

        #include "My Lighting.cginc"

        ENDCG
        }

此时只能看到第二盏灯的效果:
这里写图片描述

原因是绘制第二盏灯的效果的时候,覆盖了第一盏灯的效果。所以此时要引入一个blend操作,我们这里使用
Blend One One
此时变为:

Pass {
            Tags {
                "LightMode" = "ForwardAdd"
            }

            Blend One One

            CGPROGRAM

            #pragma target 3.0

            #pragma vertex MyVertexProgram
            #pragma fragment MyFragmentProgram

            #include "My Lighting.cginc"

            ENDCG
        }

看到的效果为:
这里写图片描述

这里还要介绍下cpu画像素的机制,离相机近的像素有必要画出来,而同样位置离相机远的像素没有必要画出来,于是又一个深度缓存用来存储当前画过的像素的z大小。默认是写入,但是此时我们也可以使用ZWrite Off进行关闭写入,这里的第二个通道没有必要再写入深度信息,因为他是最后一个通道了,所以可以进行关闭。

Pass {
            Tags {
                "LightMode" = "ForwardAdd"
            }

            Blend One One
            ZWrite Off

            CGPROGRAM

            #pragma target 3.0

            #pragma vertex MyVertexProgram
            #pragma fragment MyFragmentProgram

            #include "My Lighting.cginc"

            ENDCG
        }

2.2 drawcall数量
首先是打开动态合批:
这里写图片描述

然后是去除环境材质:
这里写图片描述

再然后是关掉第二盏灯:
这里写图片描述
打开Stats和FrameDebug,可以看到有5个batch。
clear一个
立方体合批一个
三个球体三个,共五个。

然后我们打开第二盏灯:
这里写图片描述

clear一个
三个立方体每次渲染3个,两个灯攻击6个
三个球体每次渲染3个,两个灯共计6个,攻击13个。

动态合批只有在仅有一盏灯的时候才能有效,像我们这里两盏灯,就不能动态合批了。

3 点光源
设置光源类型的地方在Light组件上:
这里写图片描述

我们disable掉两盏平行灯,然后只用点光源,并且移动点光源,可以看到很奇怪的效果:
这里写图片描述
这里写图片描述

原因:我们的base pass依然执行,但是没有一个激活的平行光,渲染效果为黑色。而第二个pass只支持平行光的渲染,对顶点光源不支持,所以很奇怪。

3.1 灯光函数
我们创建一个独立的函数用来专门计算光照效果。

UnityLight CreateLight (Interpolators i) {
    UnityLight light;
    light.dir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos);
    UNITY_LIGHT_ATTENUATION(attenuation, 0, i.worldPos);
    light.color = _LightColor0.rgb * attenuation;
    light.ndotl = DotClamped(i.normal, light.dir);
    return light;
}

3.2 光源位置
_WorldSpaceLightPos0 在光源是平行光的时候为平行光的方向。如果是点光源的时候,此变量代表是光源的位置。所以我们的自己计算光源的方向了,方法为:

UnityLight light;
light.dir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos);

UnityLight为数据结构。

3.3 光源衰减
平行光的衰减是一个常数,我们不用关心。但是点光源的衰减是不同的,如下图所示:
这里写图片描述

光子数是固定的,随着传播,光子向各个方向行走,于是形成球体,随着半径的增大,平均在单位面积的光子就越来越少。而球体表面积为4 π r^2,我们可以把4 π 作为用光的强度来控制,也就是light的intensity来控制,这样点光源的衰减就与距离平方成反比。
计算衰减公式如下:

float3 lightVec = _WorldSpaceLightPos0.xyz - i.worldPos;
float attenuation = 1 / (dot(lightVec, lightVec));
light.color = _LightColor0.rgb * attenuation;

完整的计算光照代码为:

UnityLight CreateLight(Interpolators i) {
    UnityLight light;
    light.dir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos);
    float3 lightVec = _WorldSpaceLightPos0.xyz - i.worldPos;
    float attenuation = 1 / (dot(lightVec, lightVec));
    light.color = _LightColor0.rgb * attenuation;
    light.ndotl = DotClamped(i.normal, light.dir);
    return light;
}

效果为:
这里写图片描述

太亮了,为啥呢?因为当距离太近的时候,1 / (dot(lightVec, lightVec)); 无穷大,所以优化下:

float attenuation = 1 / (1 + dot(lightVec, lightVec));

效果为:
这里写图片描述

3.4 光的范围
在现实中,光子在没有撞到任何东西的时候,会保持向前移动。那意味着光是可以到达无限远的。但是随着距离的增大,光会越来越弱,直到我们看不到为止。但是我们不想去渲染看不到的光,所以我们我们的渲染将会有个范围,这就要给定灯光的范围。

点光源和聚光灯是有范围的。在光范围的物体会增加一个drawcall,在光范围之外的物体将不会增加drawcall。默认的范围是10。

当我们使用自己的衰减计算方法时,会发现当物体在进入范围和离开范围的时候,会突然的变亮和变暗。这种突变的原因是物体在超出范围的时候依然可以被照亮,这也是衰减和光的范围没有同步所导致。

理想状态,光是没有范围的,可以使无穷远的。这要求我们要做的是不要发生骤变,所以要在无穷远的是其衰减为0。
我们要用unity提供的衰减函数来替换我们自己的衰减函数。unity是这么做的呢?他是预先计算,让衰减早一点变为0。

这个方法在头文件:#include “AutoLight.cginc”
包含这个头文件,然后改变我们的CreateLight函数如下:

UnityLight CreateLight (Interpolators i) {
    UnityLight light;
    light.dir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos);
    UNITY_LIGHT_ATTENUATION(attenuation, 0, i.worldPos);
    light.color = _LightColor0.rgb * attenuation;
    light.ndotl = DotClamped(i.normal, light.dir);
    return light;
}

打开AutoLight.cginc文件,找到宏定义:
这里写图片描述

这个要求我们的通道中必须先定义这个宏才能使用这个函数,由于我们只在forward add中处理点光源,所以只要在forwardadd通道中加入这个宏即可。

Pass {
            Tags {
                "LightMode" = "ForwardAdd"
            }

            Blend One One
            ZWrite Off

            CGPROGRAM

            #pragma target 3.0

            #pragma vertex MyVertexProgram
            #pragma fragment MyFragmentProgram

            #define POINT

            #include "My Lighting.cginc"

            ENDCG
        }

这样物体在进入和离开区域及不会发生显示和不显示的突变了,原因是我们在到达最大距离之前就衰减完了。

4 混合灯
在改了上面的代码之后,我们把仅有的一个点光源关闭,然后打开两个平行光,看看其效果:
这里写图片描述

而正确两个盏灯是这样的:
这里写图片描述
原因是刚才我们的第二个光源被第二个通道当做点光源处理了。

4.1 shader变体
我们可以查看shader到底有几个变体,在如下图中:
这里写图片描述
可以看到这个My First Lighting Shader有两个变体,可以打开看看:
这里写图片描述

他们分别是forwarbase和forwardadd的一人一个变体。
针对上面的需求,我们需要为点光源和平行光源分别给一个变体,于是引入这个命令,multi_compile,即多重编译。

Pass {
            Tags {
                "LightMode" = "ForwardAdd"
            }

            Blend One One
            ZWrite Off

            CGPROGRAM

            #pragma target 3.0

            #pragma multi_compile DIRECTIONAL POINT

            #pragma vertex MyVertexProgram
            #pragma fragment MyFragmentProgram

            #include "My Lighting.cginc"

            ENDCG
        }

我们添加了这行:#pragma multi_compile DIRECTIONAL POINT
同时去除了#define POINT,这个霸道的宏,就因为它,不同类型的光源到这个通道都被视为点光源。
此时我们看到第二个通道有两个变体了:
这里写图片描述

这两个变体的名字是不是可以随便取得呢?不是的,这两个宏的是在AutoLight.cginc能用得到的。
然后我们要修改CreateLight函数,加入if判断宏:

UnityLight CreateLight (Interpolators i) {
    UnityLight light;

    #if defined(POINT)
        light.dir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos);
    #else
        light.dir = _WorldSpaceLightPos0.xyz;
    #endif

    UNITY_LIGHT_ATTENUATION(attenuation, 0, i.worldPos);
    light.color = _LightColor0.rgb * attenuation;
    light.ndotl = DotClamped(i.normal, light.dir);
    return light;
}

这个函数同样可以被base pass使用,因为bass pass没有定义POINT,所以执行的是#else代码段。unity会根据当前灯光的类型来决定使用哪个变体。如果是平行光那么使用DIRECTIONAL变体,如果是点光源则使用POINT变体。如果没有任何一个匹配的那么他会从变体列表中选择第一个变体。

5 聚光灯
为了加入对聚光灯的支持,我们再加入一个变体:SPOT

#pragma multi_compile DIRECTIONAL POINT SPOT

此时第二个通道的变体增加为3个了:
这里写图片描述

聚光灯也需要知道位置这和点光源类似:

UnityLight CreateLight (Interpolators i) {
    UnityLight light;

    #if defined(POINT) || defined(SPOT)
        light.dir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos);
    #else
        light.dir = _WorldSpaceLightPos0.xyz;
    #endif

    UNITY_LIGHT_ATTENUATION(attenuation, 0, i.worldPos);
    light.color = _LightColor0.rgb * attenuation;
    light.ndotl = DotClamped(i.normal, light.dir);
    return light;
}

此时这个shader已经能够支持聚光灯了,因为我们使用的是unity自己的衰减计算函数:UNITY_LIGHT_ATTENUATION
所以在AutoLight.cginc中包含了对聚光的衰减函数。
那么此时我们将点光源改为聚光类型之后,调整角度和范围,可以观察到如下的结果:
这里写图片描述

5.1 聚光的Cookies
剪影效果,就是聚光灯添加一个投射阴影的效果:
这里写图片描述
看到物体上有一个黑白斑纹的效果,好像是聚光灯投射下来的阴影。

要实现这个效果需要一张贴图,其格式和设置如下:
这里写图片描述

然后把这张图用到聚光灯上去:
这里写图片描述

6 更多的Cookies
平行光也可以有cookies,这些cookies是平铺的,所以他的边缘不需要渐变到0。

猜你喜欢

转载自blog.csdn.net/wodownload2/article/details/82223412