渲染5——多盏灯

每个物体受多盏灯影响
支持不同的灯光类型
使用灯光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。
首先设置图片的格式:
这里写图片描述
TextureType依然为Cookie
LightType改为Directional

把它用于灯光:
这里写图片描述

我注意到这个Cookie Size,直接给出两个图,可以看到其效果:
当Cookie Size为1的时候:
这里写图片描述

当Cookie Size为2的时候:
这里写图片描述
也就是缩放的意思。

unity认为使用了Cookie的Directional Light和不使用Cookie的Directional Light需要不同的shader变体,所以需要加入一个宏了,如下:

#pragma multi_compile DIRECTIONAL DIRECTIONAL_COOKIE POINT SPOT

但是计算光照颜色的地方不需要改,因为依然是平行光。
如果我们去除这个变体DIRECTIONAL_COOKIE 可以看到其效果为:
这里写图片描述

6.1 顶点光的Cookies
同样也可为顶点光设置cookie,首先是贴图:
点光是朝着各个方向发散的,所以会形成一个球体,那么在unity中使用的是立方体贴图来模拟。
这里需要设置两个参数:
一个是LightType指定为Point
一个是Mapping即映射方法,unity需要知道怎么去解析你的贴图,为了方便我们这里使用Auto方法。当然你可以采用其他的方法,可以自行观察下效果。
这里写图片描述
这里写图片描述

下面就是要再添加一个变体了,增加POINT_COOKIE

#pragma multi_compile DIRECTIONAL DIRECTIONAL_COOKIE POINT_COOKIE POINT SPOT

同时要改变下计算光照的方式:

UnityLight CreateLight (Interpolators i) {
    UnityLight light;

    #if defined(POINT) || defined(POINT_COOKIE) || 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;
}

然后其效果为:
这里写图片描述

这里有个简化:
在使用了这么多的变体之后,我们看看有几个变体了:
这里写图片描述

这里已经有5个变体,那么unity为我们提供了这样一种方式:

#pragma multi_compile_fwdadd
//#pragma multi_compile DIRECTIONAL DIRECTIONAL_COOKIE POINT_COOKIE POINT SPOT

这样就可以不用写那么多长串的代码了,可以查看使用这个定义方法之后,依然是产生了上面的五个变体。

7 顶点光照
每个可见的物体都会在base pass被渲染。base pass会渲染得是平行光,额外的灯光会在附加通道也就是forward add通道进行渲染。所以越多的灯光就会导致越多的draw call,同样的越多的物体出现的灯光范围内也会导致越多的drawcall。

我们可以通过设置像素灯的个数来控制drawcall的数量,设置的地方在:
这里写图片描述
默认的情况下,unity最多支持4盏像素灯。更高品质的渲染使用更多的像素灯。unity会根据灯光的强度来排序灯的重要程度。
因为不同的物体受到不同灯的影响,这样导致得到不一致的灯光效果,特别是当物体运动的时候,情况会更加糟糕,因为会导致灯光突然的巨变。导致巨变的原因是灯突然的关闭,unity提供了另外一种更加廉价但是性能更高的渲染方式,我们使用顶点着色器,而不是像素着色。

顶点着色的意思是,在vertex shader中计算光照。然后这个计算的结果会被传入fragment shader中进行差值。顶点个数比像素的个数要少的多,所以计算起来更加快速。unity是在base pass中处理这些灯,并且要相应的定义VERTELIGHT_ON关键字。

当使用顶点光照的时候,它只支持point light,对于directional light和spot light是不能被视为vertex light的。

为了使用顶点灯,我们必须在base pass中加入一个宏VERTEX_ON,其的变体只要使用_就可以了。

具体如下:
这里写图片描述

我们可以看看base pass 的变体个数:
这里写图片描述

7.1 一个顶点灯
为了把顶点灯的颜色传递给像素着色器,我们必须在顶点着色器的输出结构中增加一个变量颜色变量,并且仅当使用了定义了宏VERTEX_LIGHT_ON,如下:

struct Interpolators {
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD0;
    float3 normal : TEXCOORD1;
    float3 worldPos : TEXCOORD2;

    #if defined(VERTEXLIGHT_ON)
        float3 vertexLightColor : TEXCOORD3;
    #endif
};

为了计算vertexLightColor,我们这里创建一个单独的函数:ComputeVertexLightColor。

void ComputeVertexLightColor (inout Interpolators i) {
    ……
}

这里使用了inout关键字。

UnityShaderVariables定义了一个数组用来保存顶点灯的颜色信息,这些信息在RGBA,我们仅仅需要RGB三个通道。

void ComputeVertexLightColor(inout Interpolators i)
{
    #if defined(VERTEXLIGHT_ON)
        i.vertexLightColor = unity_LightColor[0].rgb;
    #endif
}

在像素着色器中,我们要把这个颜色累加到其他灯的颜色上。我们把顶点灯的颜色视为间接光照,所以在计算间接光照函数中把其颜色值付给diffuse属性,具体如下:

UnityIndirect CreateIndirectLight (Interpolators i) {
    UnityIndirect indirectLight;
    indirectLight.diffuse = 0;
    indirectLight.specular = 0;

    #if defined(VERTEXLIGHT_ON)
        indirectLight.diffuse = i.vertexLightColor;
    #endif
    return indirectLight;
}

为了看下
上面的函数只支持一盏顶点灯,同时我们把项目中的像素灯的个数设置为0,也就是说现在每个物体只受一个顶点灯的影响,forward add pass 是不会被执行的,其效果为:
这里写图片描述

如果把pixel light count设置为1,那么我们唯一的顶点灯被认为是像素灯,这是为啥呢?因为我们看看这个灯的设置模式为:
这里写图片描述
render mode为auto,而目前像素灯的个数为1,所以此时这个灯被认为是像素灯,那么此时的效果为:
这里写图片描述

而如果我们把render mode改为not important,此时的forward add pass又不会被执行了。效果为:
这里写图片描述

这里写图片描述

需要说明的是,render mode为important的时候,这个灯一定会作为像素灯处理,及时实际数量超过了设置中设置的像素灯的个数。render mode为not important的时候,这个灯一定不会作为像素灯处理。而在render mode为auto的时候,这个unity自己觉得它是否作为像素灯处理,在没有超过数量限制的时候,unity自己会把它当做像素灯处理,因为效果更好呀,你说是吧。

unity最多支持4盏顶点灯。所以有如下的计算函数:

void ComputeVertexLightColor (inout Interpolators i) {
    #if defined(VERTEXLIGHT_ON)
        i.vertexLightColor = Shade4PointLights(
            unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
            unity_LightColor[0].rgb, unity_LightColor[1].rgb,
            unity_LightColor[2].rgb, unity_LightColor[3].rgb,
            unity_4LightAtten0, i.worldPos, i.normal
        );
    #endif
}

这样就可以支持四盏顶点灯了,其效果如下:
这里写图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值