zoukankan      html  css  js  c++  java
  • Unity中的多光源

    在Unity中,如果想要使用多光源,比如2个平行光,或者1个平行光+1个点光源,需要在额外的shader pass中进行处理:

    		Pass {
    			Tags {
    				"LightMode" = "ForwardAdd"
    			}
    
    			Blend One One
    			ZWrite Off
    
    			CGPROGRAM
    
    			...
    
    			ENDCG
    		}
    

    这里设置了blend mode,表示add pass渲染其他光源所得到的颜色会叠加到base pass上,而关闭ZWrite则是个优化,因为这里只是用来渲染其他光源,objects本身没有特殊处理,所以没必要进行深度写入。

    放在base pass渲染的一定是平行光源,如果有多个平行光源,那Unity就会去选择intensity属性最大的那个,把其他平行光源放到add pass中渲染。需要注意的一点是,如果我们的scene里只有一个点光源,那么还是会渲染两次pass,其中点光源的渲染还是放在add pass中,base pass就仿佛是没有光源的情况下渲染:

    可以看到截图中,场景中有6个object,只有一个点光源active,status里一共12个batches

    可以看出,base pass是没有光源的,点光源的渲染是在add pass中完成的

    另外,Unity的点光源,有个range的属性,这个属性控制了点光源有效的范围,超出这个范围的object,是接受不到该光源的光照的,也就会省掉这个光源的渲染pass。例如,我们将场景中唯一的点光源的range设置为0:

    range设置为0时,status里一共6个batches

    为了配合使用range,unity提供了API来控制点光源强度的衰减。这样,随着物体离光源的距离增加,光照强度逐渐减弱,到range边界时衰减为0,使得表现不会突兀。

    unity提供了一个名为UNITY_LIGHT_ATTENUATION的API,它在点光源的情况下定义如下:

    #ifdef POINT
    sampler2D_float _LightTexture0;
    unityShadowCoord4x4 unity_WorldToLight;
    #   define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) 
            unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz; 
            fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); 
            fixed destName = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).r * shadow;
    #endif
    

    API首先将物体变换到光源坐标系,然后计算物体与光源之间的距离,使用这个距离作为采样衰减贴图_LightTexture0的uv。首先,让我们好奇一下这个unity_WorldToLight长啥样:

    图中点光源的世界坐标为(0.1, 0.145, 0, 1),对应的unity_WorldToLight矩阵为:

    [egin{bmatrix} 0.1 & 0 & 0 & -0.01 \ 0 & 0.1 & 0 & -0.015 \ 0 & 0 & 0.1 & 0 \ 0 & 0 & 0 & 1 end{bmatrix} ]

    注意到,图中点光源设置的range为10,可以推断得到这个矩阵其实就是一个先平移再缩放的变换矩阵:

    [M = S cdot T \ M = egin{bmatrix} 1/r & 0 & 0 & 0 \ 0 & 1/r & 0 & 0 \ 0 & 0 & 1/r & 0 \ 0 & 0 & 0 & 1 end{bmatrix} cdot egin{bmatrix} 1 & 0 & 0 & -x \ 0 & 1 & 0 & -y \ 0 & 0 & 1 & -z \ 0 & 0 & 0 & 1 end{bmatrix} \ M = egin{bmatrix} 1/r & 0 & 0 & -x/r \ 0 & 1/r & 0 & -y/r \ 0 & 0 & 1/r & -z/r \ 0 & 0 & 0 & 1 end{bmatrix} ]

    实际意义上,就是控制光源坐标系下的坐标范围都在[0,1]之间,这样方便直接sample后面的衰减纹理。那么,这个衰减纹理又长啥样呢?同样地,我们使用frame debugger查看:

    很不幸的是,它是个1024*1的纹理,我们没法直接预览查看。那就手写一个shader,把它画出来:

    Shader "Custom/LightTextureShader"
    {
        Properties
        {
        }
        SubShader
        {
            Pass
            {
                Tags {
    				"LightMode" = "ForwardBase"
    			}
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
    
                #include "UnityCG.cginc"
    
                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 = v.uv;
                    return o;
                }
    
                fixed4 frag (v2f i) : SV_Target
                {
                    return 0;
                }
                ENDCG
            }
    
            Pass {
    			Tags {
    				"LightMode" = "ForwardAdd"
    			}
    
    			Blend One One
    			ZWrite Off
    
    			CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #pragma multi_compile_fwdadd
    
                #include "AutoLight.cginc"
    
                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 = v.uv;
                    return o;
                }
    
                fixed4 frag (v2f i) : SV_Target
                {
                #if defined(POINT)
                    return tex2D(_LightTexture0, i.uv);
                #else
                    return 0;
                #endif
                }
                ENDCG
    		}
        }
    }
    
    

    然后让场景中只有一个点光源,创建一个quad,让它处于光源的range内,设置为该材质:

    这就是LightTexture0的庐山真面目了,UNITY_LIGHT_ATTENUATION实际上就是对纹理的对角线区域进行采样,使用其r通道,这也是看上去纹理偏红的原因,从0-1越来越暗也是符合衰减的规律。

    顺便一提的是,上面的shader使用了multi_compile_fwdadd,其含义就是unity会使用不同关键字为我们编译不同版本的shader。我们可以手动查看variant的总数:

    点击show,还可以看到使用到的keywords:

    Builtin keywords used: POINT DIRECTIONAL SPOT POINT_COOKIE DIRECTIONAL_COOKIE
    
    5 keyword variants used in scene:
    
    POINT
    DIRECTIONAL
    SPOT
    POINT_COOKIE
    DIRECTIONAL_COOKIE
    
    

    unity会自动挑选合适的版本使用。

    除了点光源外,还有一种叫做聚光灯的光源。SpotLight情况下UNITY_LIGHT_ATTENUATION的定义如下:

    #ifdef SPOT
    sampler2D_float _LightTexture0;
    unityShadowCoord4x4 unity_WorldToLight;
    sampler2D_float _LightTextureB0;
    inline fixed UnitySpotCookie(unityShadowCoord4 LightCoord)
    {
        return tex2D(_LightTexture0, LightCoord.xy / LightCoord.w + 0.5).w;
    }
    inline fixed UnitySpotAttenuate(unityShadowCoord3 LightCoord)
    {
        return tex2D(_LightTextureB0, dot(LightCoord, LightCoord).xx).r;
    }
    #if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
    #define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord4 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1))
    #else
    #define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord4 lightCoord = input._LightCoord
    #endif
    #   define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) 
            DECLARE_LIGHT_COORD(input, worldPos); 
            fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); 
            fixed destName = (lightCoord.z > 0) * UnitySpotCookie(lightCoord) * UnitySpotAttenuate(lightCoord.xyz) * shadow;
    #endif
    

    可以看到,计算聚光灯光源的衰减使用了两张贴图,_LightTexture0_LightTextureB0。先从使用到_LightTexture0的函数UnitySpotCookie看起:

    可以发现这里有个齐次坐标系转换的过程。为啥这次需要除w?老样子,用frame debugger查看一下:

    注意到,这次unity_WorldToLight矩阵和之前完全不一样了。那这个矩阵又是怎么得来的呢?首先可以想到,聚光灯光源是有位置的,而且有方向,即transform的position和rotation对它都有影响;其次,聚光灯的覆盖范围是一个圆锥,那么这就需要进行一个透视投影变换,处于覆盖范围内的点,都会被投射到一个平面上,以便对_LightTexture0进行采样。覆盖范围由range和spot angle两个参数共同决定。

    position和rotation的用途就和相机变换一样,将物体变换到光源空间。接下来的投影也和相机中的透视投影类似,这里的投影平面其实就是对应采样的纹理,其长宽均为1。那么对应光源空间中的任一点(x,y,z,1),其投影坐标有:

    [dfrac{x'}{x} = dfrac{d}{z} \ dfrac{y'}{x} = dfrac{d}{z} ]

    这里的d是投影平面到光源的距离,因为投影平面的长宽是1,所以可以得到距离d为:

    [tandfrac{ heta}{2} = dfrac{1}{2d} \ d = dfrac{1}{2tandfrac{ heta}{2}} ]

    得到:

    [x' = dfrac{x}{2ztandfrac{ heta}{2}} \ y' = dfrac{y}{2ztandfrac{ heta}{2}} ]

    而对于z'来说,它本身取什么样的值并不影响投影采样_LightTexture0,这里可以设置和x',y'格式一致的常量,即

    [z' = dfrac{1}{2tandfrac{ heta}{2}} ]

    由于z在分母位置了,需要利用齐次坐标的性质,即有:

    [M cdot (x, y, z, 1)^T = (x', y', z', 1) = (x, y, z, 2ztandfrac{ heta}{2}) ]

    这样就结束了吗?还没有,让我们看一下UnitySpotAttenuate函数,可以发现它用到了变换后的齐次坐标的点积进行纹理采样,齐次坐标xyz的点积代表物体距离光源的距离。由于聚光灯光源是有距离范围的,所以需要做下归一化,方便纹理采样:

    [(x, y, z, 2ztandfrac{ heta}{2}) = (dfrac{x}{r}, dfrac{y}{r}, dfrac{z}{r}, dfrac{2ztandfrac{ heta}{2}}{r}) ]

    投影矩阵M的最终形式为

    [ M = egin{bmatrix} 1/r & 0 & 0 & 0 \ 0 & 1/r & 0 & 0 \ 0 & 0 & 1/r & 0 \ 0 & 0 & 2tandfrac{ heta}{2} /r & 1 end{bmatrix} ]

    使用frame debugger看看这两张贴图长啥样:

    配合前面的推导,就很容易理解这个函数所做的事情了。lightCoord.z > 0很好理解,只有位于光源前方的物体才能接收到光照,UnitySpotCookie函数中LightCoord.xy / LightCoord.w的值域是[-0.5, 0.5],所以要额外+0.5进行归一化方便采样。UnitySpotAttenuate函数和之前点光源做的事情类似,根据距离在衰减纹理的对角线上进行采样,这里用到的衰减纹理也和点光源相同。

    另外,对于这里的_LightTexture0,我们还可以使用自己的贴图进行替换,对应Light Component中的Cookie属性:

    我们尝试使用自己的一张cookie贴图试试,Unity在texture的导入设置中专门有一个cookie的导入选项:

    设置到聚光灯上之后,使用frame debugger查看:

    的确_LightTexture0变成了我们设置的贴图了。

    当然,除了聚光灯可以使用cookie以外,点光源和平行光也都支持使用cookie贴图。和聚光灯有区别的地方在于,点光源和平行光源一旦使用cookie,则相当于使用了POINT_COOKIEDIRECTIONAL_COOKIE这两个关键字,原先的POINTDIRECTIONAL关键字则不再生效。这会导致,原本在base pass渲染的平行光源,会全部挪到add pass中渲染;而且,UNITY_LIGHT_ATTENUATION函数定义发生变化,先看DIRECTIONAL_COOKIE下的版本:

    #ifdef DIRECTIONAL_COOKIE
    sampler2D_float _LightTexture0;
    unityShadowCoord4x4 unity_WorldToLight;
    #   if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
    #       define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord2 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xy
    #   else
    #       define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord2 lightCoord = input._LightCoord
    #   endif
    #   define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) 
            DECLARE_LIGHT_COORD(input, worldPos); 
            fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); 
            fixed destName = tex2D(_LightTexture0, lightCoord).w * shadow;
    #endif
    

    注意到这里也有个unity_worldToLight矩阵。老规矩,用frame debugger看一眼:

    这里的推导比较简单,首先cookie是有个size大小的参数设置,控制了采样纹理的区域;其次那两个0.5的偏移,是为了让位于光源空间中原点位置的点,采样的纹理坐标是(0.5,0.5),即纹理的中心点。

    再来看下POINT_COOKIE下的版本:

    #ifdef POINT_COOKIE
    samplerCUBE_float _LightTexture0;
    unityShadowCoord4x4 unity_WorldToLight;
    sampler2D_float _LightTextureB0;
    #   if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
    #       define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz
    #   else
    #       define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord3 lightCoord = input._LightCoord
    #   endif
    #   define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) 
            DECLARE_LIGHT_COORD(input, worldPos); 
            fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); 
            fixed destName = tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).r * texCUBE(_LightTexture0, lightCoord).w * shadow;
    #endif
    

    继续用frame debugger查看:

    我们发现点光源的cookie导入时变成了cube map。同时还有一点,unity_worldToLight矩阵和不使用cookie的点光源版本是一样的。

    通常来说,每新增一个光源,unity都会为之新增一个add pass。相应地,性能开销也会越来越大。

    图中有4个点光源,6个物体,总共需要6个base pass + 6*4个add pass = 30个pass

    Unity在quality setting中提供了像素光源的设置来控制add pass的数量,例如我们将其设置为2:

    发现总共pass的数量也随之下降了:

    图中有4个点光源,6个物体,但因为像素光源数量设置为2,所以总共需要6个base pass + 6*2个add pass = 18个pass

    unity会根据光源的重要程度自动筛选出属于像素光源的光源,那么没被选上的光源去哪儿了呢?答案是挪到顶点光源去了。顾名思义,Unity希望我们在顶点着色器阶段就把光源的颜色计算完毕。那么要如何计算呢?

    Unity为属于顶点光源的点光源(注意平行光源是会被忽略掉的),在base pass中定义VERTEXLIGHT_ON关键字,并且会保存最多4个点光源的位置和颜色信息。这些内容依旧可以从frame debugger中一探究竟:

    图中像素光照数量设置为0,有4个点光源

    4个点光源的颜色和位置信息

    可以看出,顶点光照是在base pass中计算完成的,并且unity_4LightPosX0unity_4LightPosY0unity_4LightPosZ0这三个向量共同组成了4个光源的位置,unity_LightColor数组保存了每个光源的颜色信息,unity_4LightAtten0则是保存了每个光源的衰减信息,这个值与光源的range有关。Unity为我们提供了API来计算顶点光照的颜色:

    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
    }
    

    Shade4PointLights的内部实现如下:

    // Used in ForwardBase pass: Calculates diffuse lighting from 4 point lights, with data packed in a special way.
    float3 Shade4PointLights (
        float4 lightPosX, float4 lightPosY, float4 lightPosZ,
        float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
        float4 lightAttenSq,
        float3 pos, float3 normal)
    {
        // to light vectors
        float4 toLightX = lightPosX - pos.x;
        float4 toLightY = lightPosY - pos.y;
        float4 toLightZ = lightPosZ - pos.z;
        // squared lengths
        float4 lengthSq = 0;
        lengthSq += toLightX * toLightX;
        lengthSq += toLightY * toLightY;
        lengthSq += toLightZ * toLightZ;
        // don't produce NaNs if some vertex position overlaps with the light
        lengthSq = max(lengthSq, 0.000001);
    
        // NdotL
        float4 ndotl = 0;
        ndotl += toLightX * normal.x;
        ndotl += toLightY * normal.y;
        ndotl += toLightZ * normal.z;
        // correct NdotL
        float4 corr = rsqrt(lengthSq);
        ndotl = max (float4(0,0,0,0), ndotl * corr);
        // attenuation
        float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
        float4 diff = ndotl * atten;
        // final color
        float3 col = 0;
        col += lightColor0 * diff.x;
        col += lightColor1 * diff.y;
        col += lightColor2 * diff.z;
        col += lightColor3 * diff.w;
        return col;
    }
    

    最后,对于其他剩下的光源,我们可以使用球谐光照计算其颜色:

    float4 diffuse = max(0, ShadeSH9(float4(normal, 1)));
    

    如果你觉得我的文章有帮助,欢迎关注我的微信公众号(大龄社畜的游戏开发之路-

  • 相关阅读:
    socket是什么
    0,1,2 代表标准输入、标准输出、标准错误
    认识程序的执行:从高级语言到二进制,以java为例
    97 条 Linux 运维工程师常用命令总结[转]
    rsync 参数配置说明[转]
    shell 脚本学习之内部变量
    ansible 入门学习(一)
    python 管理多版本之pyenv
    CentOS6 克 隆
    yum 本地仓库搭建
  • 原文地址:https://www.cnblogs.com/back-to-the-past/p/14619708.html
Copyright © 2011-2022 走看看