Shader 学习笔记:光照

Shader以及背后的图形学我一直很感兴趣(比C#要感兴趣),而且《Shader 入门精要》也是我最喜欢的书之一。这篇文章开始写一写Shader,当然并不是作为一个教学的博客,而是当做学习的沉淀(所以不会写Shader语法基础),先从最基础的开始,光照。

我们在这篇文章中讲标准光照模型,一般来说标准光照模型中,射入摄像机的光线有以下四个部分:

  • 自发光Emissive:描述当给定一个方向时,一个表面会向该方向发射多少的辐射量。在Unity中如果没用全局光照(Global-ilumination)的话自发光看起来只是它自己比较亮而已,旁边的物体并不会受到光照。
  • 漫反射Diffuse:描述当一个光源照向模型表面时,该表面会向四周散射多少辐射量。
  • 高光Specular:描述当一个光源照向模型表面时,该表面会在完全镜面方向反射多少辐射量。
  • 环境光Ambient:描述间接光照。

这篇博客并不会涉及到自发光,但是会认真写一写我对其他光照算法的理解。先写简单光照:


漫反射:光照方向、法线方向

我们首先假设我们此时照向一个球体的光是平行光,那么有如下的图示:

Shader 学习笔记:光照_第1张图片

其中

  • 蓝色的线代表这个球体上的法线方向,一般不经过修饰的话一个球上的点的法线方向都是由球心指向该点的方向的射线(我们图中只花了无根,实际上可能有亿万根)。
  • 橘色的线代表光照方向,由于我们这里使用的平行光,所以每一束光都平行。

每个光照方向都与法线形成一个夹角θ,我们可以清晰的看到当θ等于90度的时候光照最弱,当θ等于0度的时候光照最强,当θ大于90度的时候没有光照。θ角的描述正好和θ的余弦函数类似,我们由此就可以推导出:

漫反射的强度与法线和光照方向的夹角的余弦成正比(这也符合兰伯特定律),并且由于只需要计算在0-90°中的夹角存在,所以不能让cosθ小于0:

Diffuse=(lightColor.rgb*DiffuseColor.rgb)max(0,normalDir*lightDir)

我们可以知道漫反射中的计算的两个关键元素,光照方向和法线方向,当然为了完整的求出它们的夹角余弦值,需要对它们进行归一化(让向量长度为1)。


高光反射:法线、光照、视角方向、反射方向

相较于漫反射的计算,高光反射的计算则是一个经验模型,它并不符合现实生活中的光照现象,高光反射可以用于计算沿着完全镜面反射方向反射的光线。高光的计算一般分为基本的Phong模型和Bilin-Phong模型,二者的区别仅仅在于计算方式的不同,Bilin-Phong模型是对Phong模型的简化。但是它们都需要法线、光照、视角方向这三个要素:

Phong模型:

Shader 学习笔记:光照_第2张图片

基本的Phong模型中,高光反射只需要前三个向量:法线、光照、视角方向,反射方向可以由法线方向和光照方向计算出来:

反射方向ReflectionDIr = 2 ( nomralDir * lightDir ) * normalDir - lightDir

同时,Shader中也提供了reflect函数来计算一个点对应的反射方向,我们只需要输入参数就行。

然后使用Phong模型对视角方向和反射方向进行计算:

高光反射Specular = (lightColor.rgb*DiffuseColor.rgb)  pow ( max ( 0 , viewDir * reflectionDir ) , 材质光泽度 )

在Shader中,材质光泽度我们可以完全自己控制,作为外部引入的系数之一。

Blinn-Phong模型:

Shader 学习笔记:光照_第3张图片

Blinn-Phong模型相较于Phong模型来说只是进行了数学上简单的修改,它避免了直接计算反射方向,而是引入了一个新的矢量h来简便计算,h实际上就是视角方向和光照方向的和。即为:

矢量h = normalize(viewDir + lightDir)

那么我们在计算高光强度的时候也进行了相应的简化:

 高光反射Specular = (lightColor.rgb*DiffuseColor.rgb)  pow ( max ( 0 , normalDir * h) , 材质光泽度 ) 

在硬件中美摄像机和光源距离足够远的话,Blinn-Phong计算起来要比Phong快一些,但是二者都只是经验模型,所以二者并不能判定谁更接近于真实的光照。


一般来说,我们计算这样的光照模型可以选择逐像素计算或者逐顶点计算,我们下面的两个Shader:FragmentCombine和VertexShader就对应了这两种计算方式,当然代码上的表面区别只有计算位置的不同,但是我们能在图片中看到它们的区别,首先先放代码:

逐像素计算光照

在顶点着色器中:计算每个顶点的裁剪空间坐标、每个顶点的世界空间坐标、每个顶点法线的世界空间方向。

在片元着色器中:获得归一化后的法线方向,并获得世界空间中的光照方向和视角方向,带入我们上面的公式进行计算得到fixed3Diffuse值和Specular值(在这个例子中使用的Blinn-Phong计算方法),将它们叠加然后输出:

Shader "Hidden/FragmentCombine"
{
    Properties
    {
        _DiffuseColor("Diffuse",Color)=(1,1,1,1)
        _SpecularColor("SpecularColor",Color)=(1,1,1,1)
        _Gloss("Gloss",Range(8.0,256))=20
    }

    SubShader
    {
        pass
        {
            Tags
            {
                "LightMode"="ForwardBase"
            }

            CGPROGRAM 
            #pragma vertex myVert
            #pragma fragment myFrag

            #include "Lighting.cginc"

            fixed4 _DiffuseColor;
            float _Gloss;
            fixed4 _SpecularColor;

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

            };
            struct VertexToFragment
            {
                float4 pos:SV_POSITION;
                float3 Normal:TEXCOORD0;
                float3 worldPos:TEXCOORD2;
            };
            
            VertexToFragment myVert(VertexData data)
            {
                VertexToFragment VToF;
                VToF.pos=UnityObjectToClipPos(data.vertex);
                VToF.Normal=UnityObjectToWorldNormal(data.normal).xyz;
                VToF.worldPos=mul(unity_ObjectToWorld,data.vertex);
                return VToF;
            }

            fixed4 myFrag(VertexToFragment VToF):SV_TARGET
            {
                float3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
                float3 worldNormal=normalize(VToF.Normal.xyz);
                float3 worldviewDir=normalize(_WorldSpaceCameraPos.xyz- VToF.worldPos.xyz);


                float3 Specular=_SpecularColor.xyz*pow(max(0,dot(worldNormal,normalize(worldviewDir+worldLight))),_Gloss);

                float3 Diffuse=_DiffuseColor.xyz*saturate(dot(worldNormal,worldLight));
                //float3 Diffuse=_DiffuseColor.xyz*(0.5*dot(worldNormal,worldLight)+0.5);
                float3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
                return fixed4(ambient+Diffuse+Specular,1.0);
            }
            ENDCG
        }
    }
    Fallback "Diffyse"
}

逐顶点计算光照 :

在顶点着色器中我们就会完成顶点片元着色器的所有功能,在这个例子中我们使用Phong光照模型,计算每个顶点的反射方向,对于片元着色器我们只输出颜色,然后在片元着色器中进行原封不动的输出即可:

Shader "Custom/VertexCombine"
{
    Properties
    {
        _DiffuseColor("Diffuse",Color)=(1,1,1,1)
        _SpecularColor("SpecularColor",Color)=(1,1,1,1)
        _Gloss("Gloss",Range(8.0,256))=20
    }

    SubShader
    {
        pass
        {
            Tags
            {
                "LightMode"="ForwardBase"
            }

            CGPROGRAM 
            #pragma vertex myVert
            #pragma fragment myFrag

            #include "Lighting.cginc"

            fixed4 _DiffuseColor;
            fixed4 _SpecularColor;
            float _Gloss;

            struct VertexData
            {
                float4 vertex:POSITION;
                float3 normal:NORMAL;
            };
            struct VertexToFragment
            {
                float4 pos:SV_POSITION;
                float3 color:COLOR;
            };
            
            VertexToFragment myVert(VertexData data)
            {
                VertexToFragment VToF;
                VToF.pos=UnityObjectToClipPos(data.vertex);

                float3 worldNormal=UnityObjectToWorldNormal(data.normal).xyz;
                float3 worldPos=mul(unity_ObjectToWorld,data.vertex);
                float3 worldLight=normalize(UnityWorldSpaceLightDir(worldPos));
                float3 Diffuse=_DiffuseColor.xyz*saturate(dot(worldNormal,worldLight));

                float3 worldView=normalize(UnityWorldSpaceViewDir(worldPos));
                float3 worldReflect=normalize(reflect(-worldLight,worldNormal));

                float3 Specular=_SpecularColor.xyz*pow(max(0,dot(worldView,worldReflect)),_Gloss);
                float3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;

                VToF.color=ambient+Diffuse+Specular;

                return VToF;
            }

            fixed4 myFrag(VertexToFragment VToF):SV_TARGET
            {
                return fixed4(VToF.color,1.0);
            }

            ENDCG
        }
    }
    Fallback "Diffuse"
}

 这里的球的模型并不是使用的Unity自带的模型,而是使用的自制的meshProfile,我在第二篇博客就贴出它的代码了。然后我们放个对照组,左边是逐像素计算,右边是逐顶点计算:可以清楚的看到,逐像素的情况下漫反射和高光反射都保持比较正常的(比较圆),但是逐顶点的高光则按照顶点来分布,图2中我们看到两个球的高光一个是圆的一个是菱形:

Shader 学习笔记:光照_第4张图片

Shader 学习笔记:光照_第5张图片

我们归纳一下上面的两个段代码,写这样的基础光照代码中需要注意:

1.逐顶点计算也称为高洛德着色Gouraud Shading),我们算好的顶点颜色将会在渲染图元内部进行线性插值,然后输出每个面片对应的像素颜色。我们的像素颜色完全依赖顶点的线性插值,所以当计算存在非线性的计算(例如我们的高光反射)时,会根据顶点的分布产生明显的棱角(当然我们如果顶点非常密集那么棱角现象也会消除)。

2.逐像素计算也称为Phong着色Phong Shading),我们需要将它和高光中的Phong模型区别开来。这种着色主要是对每个像素的法线根据它们的三个顶点进行插值,所以Phong着色也称为法线插值着色。很明显的是,虽然逐像素计算可以无视面片的疏密获得比较精确的光照结果,但是计算的次数要比逐顶点计算要多得多,所以二者如何选择还是要根据图形的面片分布情况来决定如何使用。

3.我们在获得顶点的相关参数时,获得光照的方向的方式有两种:变量_WorldSpaceLightPos0UnityWorldSpaceLightDir函数或者WorldSpaceLightDir函数(这个函数的内部使用的还是前面那个Unity开头的函数)来获得。但是我们要注意:这两个函数都只能在前向渲染中使用。

4.视角方向我们可以通过当前世界空间中摄像机坐标_WorldSpaceCameraPos减去当前计算世界空间中顶点的坐标获得视角方向,也可以直接通过UnityWorldSpaceViewDir函数得到。但是我们要注意以上的函数获得的向量都没有被归一化,所以我们还需要进行归一化之后再进行光照的计算。在上面的例子中,两种计算方式分别都有用到。

5.我们获得的光源都是由交点指向光源的,但是反射函数中的光照方向必须是光源指向交点的,所以我们传入reflect函数的光源向量需要取反然后传入。

6.漫反射除了简单的求角度的余弦值,还存在改进版,称为半兰伯特光照模型:

Diffuse=(lightColor.rgb*DiffuseColor.rgb) * (α * dot ( worldNormal * worldLight )  + β )

这样即使是球形的背面也能获得一定的漫反射强度,整个模型整体看起来要明亮许多(因为无论是哪个像素的夹角余弦值都存在,而原先的漫反射模型由于max的存在,只要是处于背面,余弦值都被框死在了0,所以完全是黑的),一般默认的αβ都是0.5。不过它相较于原来的漫反射模型来说,它不存在任何的物理依据,仅仅是数学上的视觉加强效果。在我们的逐像素光照代码中有一行注释的代码就是半兰伯特模型。


前向渲染的多光照

上面的光照只涉及到了平行光的问题,对于平行光来说,我们只需要在意光的方向和强度。但是对于Unity中的点光源、聚光灯这样的光源来说,我们还需要获得光源的位置、强度、衰减、颜色、方向。在Unity中,如何处理这些参数与我们的渲染路径有关:

Unity中的渲染路径一般分为前向渲染(Forward Rendering Path)延迟渲染(Defferred Rendering Path)两种方式,前向渲染比较传统和常用,这篇博客的多光照实现使用前向渲染来完成。我们上面的例子已经使用到了前向渲染路径了:

Shader 学习笔记:光照_第6张图片

前向渲染实际上是将每个光源都计算一次,当计算一个片元时,先使用深度缓冲来判断片元是否可见,如果可见就渲染一次它的光照,然后更新帧缓冲。

如果一个物体处于多个光照的环境下, 那么就需要执行多次pass,每个pass计算一个逐像素光源的结果。然后在帧缓冲中将每个光照的结果合并起来得到最终颜色值。例如场景中有N个物体,每个物体受到M个光源的影响,那么整个场景需要进行N*M次pass

 前向渲染有三种方式处理光照:逐顶点处理逐像素处理球谐函数(Spherical Harmonics)处理。在Unity中有如下的法则来判断使用哪种处理方式:

  • 最亮的平行光总是在BasePass中处理
  • 其他的平行光会在BasePass中按照逐顶点的方式处理或者在AdditionalPass中处理。如果场景中不存在平行光,则会按照全黑的光源处理。
  • 渲染模式设置成Not Important的光源,前4个会在BasePass中按照逐顶点的方式处理,剩下的按照球谐函数来处理
  • 渲染模式设置成Important的光源会在AdditionalPass中处理
  • 如果以上规则得到的逐像素光源数量小于Unity的Quality Setting中设置的逐像素光源数量,那么会有更多的光源按照逐像素来处理。

前向渲染的光照计算规则存在于两个Pass中,一个ForwardBase和ForwardAdd中,二者有如下的规则:

Shader 学习笔记:光照_第7张图片

关于前向渲染的两个Pass我们需要注意: 

  1.在BasePass中我们除了设置标签外,还使用#pragma multi_compile_fwdbase指令来标注这是BasePass。而在AdditionalPass中我们使用#pragma multi_compile_fwdadd来标注为AdditionalPass。这些编译指令会保证Unity为相应的Pass生成所有需要的Shader变种和相应的光照变量。例如光照衰减值等。

  2.默认来说除了BasePass中主要的平行光会渲染阴影,AdditionalPass不会渲染其他光源的阴影的。如果需要计算AdditionalPass中的光源阴影,我们可以使用#pragma multi_compile_fwdadd_fullshadows来替代之前的AdditionalPass指令。来开启点光源和聚光灯的阴影。

  3.对于环境光和自发光来说,我们都只希望它只渲染一次,所以应该只能放在BasePass中渲染。

  4.为了每次Additional Pass渲染获得的光照结果都能与上次的光照结果在帧缓存中进行叠加,所以设置混合模式为Blend One One。即源颜色(SrcColor)与目标颜色(DstColor)相加时,两边的权重比都是1。

  5.通常的前向渲染模型中的BasePass和AdditionalPass都各只有一个,但BasePass也可以定义多次来面对需要双面渲染的情况。BasePass仅仅执行一次,而AdditionalPass的执行次数取决于物体上其他重要光源的数目。

虽然两个Pass都是逐像素来分别处理各种光源,但实际上这些指令只是告诉渲染引擎这个Pass在前向渲染中的位置,然后渲染引擎会进行相关计算并填充一些内置变量(例如_LightColor0)。但具体在Pass中如何计算完全取决于我们自己的选择,我们可以只进行逐顶点的计算,而不进行逐像素的计算,下面两个例子一个是所有光源逐顶点的计算,一个是所有光源逐像素的计算。

前向渲染Pass中光源的处理:

Unity的Shader在处理光源时,最重要的五个要素为:位置,方向,颜色,强度,衰减。

BasePass:对于BasePass来说,由于只需要处理场景中最亮的平行光,所以写法还是和上面一样正常的计算单光源的光照:

  1. 通过变量_WorldSpaceLightPos0或者函数UnityWorldSpaceLightDir来获得平行光的方向(平行光没有位置)。
  2. 然后通过_LightColor0来获得光源的强度和颜色。
  3. 由于平行光没有衰减,所以衰减值恒为1.0。

AdditionalPass:AdditionalPass中处理的是多种不同的光源,它们区别于那个最强的平行光,可能是点光源、聚光灯、强度不是最强的平行光,所以也需要不同的方式计算:

     1.光源的颜色和强度仍然可以使用_LightColor0来获得。

     2.光源的方向需要判定此时的Pass中的光源是否是平行光来得到具体的方向,在ShaderLab中,存在一个宏来判断是否是平行光:

                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
                #else
                    fixed3 worldLight=normalize(_WorldSpaceLightPos0-VToF.worldPos.xyz);
                #endif

我们使用USING_DIRECTIONAL_LIGHT来判定当前是否为平行光,如果是平行光,那么 _WorldSpaceLightPos0表示世界空间中光源的方向。如果是其他的光源,_WorldSpaceLightPos0表示世界空间下光源的位置,那么光照方向自然可以通过两点相减来获得。

但是,在前向渲染中我们可以直接根据UnityWorldSpaceLightDir函数来获得光源的方向,不需要判断是否是平行光。

      3. 同时,不同光源的衰减也需要判断光源是否为平行光:

                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed atten=1.0;
                #else
                    float3 lightCoord=mul(unity_WorldToLight,fixed4(VToF.worldPos)).xyz;
                    fixed atten=tex2D(_LightTexture0,dot(lightCoord,lightCoord)).UNITY_ATTEN_CHANNEL;
                #endif

如果是平行光,那么光照衰减的系数当然就是1.0。如果不是,那么将会使用一张名为_LightTexture0的的光照贴图来计算光照衰减。

我们通常只需要关心_LightTexture0对角线上的纹理颜色值。这些值表明了在光源空间中不同位置的点的衰减值。点(0,0)表示光源位置重合的点的衰减值,而点(1,1)表明光源空间中距离最远的点的衰减值。

为了纹理采样,我们需要得到当前像素点在光照空间中的位置,所以使用unity_WorldToLight来获得像素点在光源空间中坐标。

然后使用这个纹理坐标的模的平方,而不是该像素点距离光照中心原点的距离(避免开方)来对_LightTexture0采样。然后使用宏UNITY_ATTEN_CHANNEL来获得衰减纹理中衰减值所在的分量。进而得到最终衰减值atten

我们写出在BasePass中和AdditionalPass中逐像素计算和逐顶点计算的完整代码,二者没有太大区别,两个的代码都采用了Blinn-Phong的计算方式,只是计算的位置发生了改变:

前向渲染的逐像素计算:BasePass中和上文中的计算方式一致,AdditionalPass在片元着色器添加中判断是否为平行光,然后根据像素的光照空间坐标的模平方来对光照贴图采样来得到衰减值。

Shader "Custom/FragmentCombine"
{
    Properties
    {
        _DiffuseColor("Diffuse",Color)=(1,1,1,1)
        _SpecularColor("SpecularColor",Color)=(1,1,1,1)
        _Gloss("Gloss",Range(8.0,256))=20
    }

    SubShader
    {
        pass
        {
            Tags
            {
                "LightMode"="ForwardBase"
            }

            CGPROGRAM 

            #pragma multi_compile_fwdbase

            #pragma vertex myVert
            #pragma fragment myFrag

            #include "Lighting.cginc"

            fixed4 _DiffuseColor;
            float _Gloss;
            fixed4 _SpecularColor;

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

            };
            struct VertexToFragment
            {
                float4 pos:SV_POSITION;
                float3 Normal:TEXCOORD0;
                float3 worldPos:TEXCOORD2;
            };
            
            VertexToFragment myVert(VertexData data)
            {
                VertexToFragment VToF;
                VToF.pos=UnityObjectToClipPos(data.vertex);
                VToF.Normal=UnityObjectToWorldNormal(data.normal).xyz;
                VToF.worldPos=mul(unity_ObjectToWorld,data.vertex);
                return VToF;
            }

            fixed4 myFrag(VertexToFragment VToF):SV_TARGET
            {
                float3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
                float3 worldNormal=normalize(VToF.Normal.xyz);
                float3 worldviewDir=normalize(_WorldSpaceCameraPos.xyz- VToF.worldPos.xyz);

                float3 Specular=_LightColor0.xyz*_SpecularColor.xyz*pow(max(0,dot(worldviewDir,normalize(worldLight+worldviewDir))),_Gloss);

                float3 Diffuse=_LightColor0.xyz*_DiffuseColor.xyz*saturate(dot(worldNormal,worldLight));
                float3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
                return fixed4(ambient+Diffuse+Specular,1.0);
            }
            ENDCG
        }
        pass
        {
            Tags
            {
                "LightMode"="ForwardAdd"
            }

            Blend One One
            CGPROGRAM 

            #pragma multi_compile_fwdadd
            #pragma vertex myVertex
            #pragma fragment myFragment

            #include "Lighting.cginc"
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"

            fixed4 _DiffuseColor;
            float _Gloss;
            fixed4 _SpecularColor;


            struct VertexData
            {
                float4 vertex:POSITION;
                float3 normal:NORMAL;
            };
            struct VertexToFragment
            {
                float4 pos:SV_POSITION;
                float3 Normal:TEXCOORD0;
                float4 worldPos:TEXCOORD2;
            };
            
            VertexToFragment myVertex(VertexData data)
            {
                VertexToFragment VToF;
                VToF.pos=UnityObjectToClipPos(data.vertex);
                VToF.Normal=UnityObjectToWorldNormal(data.normal);
                VToF.worldPos=mul(unity_ObjectToWorld,data.vertex);
                return VToF;
            }

            fixed4 myFragment(VertexToFragment VToF):SV_TARGET
            {
                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
                #else
                    fixed3 worldLight=normalize(_WorldSpaceLightPos0-VToF.worldPos.xyz);
                #endif

                float3 worldNormal=normalize(VToF.Normal.xyz);
                float3 worldviewDir=normalize(_WorldSpaceCameraPos.xyz- VToF.worldPos.xyz);

                float3 Specular=_LightColor0*_SpecularColor.xyz*pow(max(0,dot(worldviewDir,normalize(worldLight+worldviewDir))),_Gloss);

                float3 Diffuse=_LightColor0.xyz*_DiffuseColor.xyz*saturate(dot(worldNormal,worldLight));

                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed atten=1.0;
                #else
                    float3 lightCoord=mul(unity_WorldToLight,fixed4(VToF.worldPos)).xyz;
                    fixed atten=tex2D(_LightTexture0,dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL;
                    
                #endif

                return fixed4((Diffuse+Specular)*atten,1.0);
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

前向渲染逐顶点计算:在BasePass中顶点着色器计算光照需要的内容(和上文中逐顶点光照一致),在AdditionalPass中顶点着色器中判断光照方向,由于光照贴图的采样必须在片元着色器中完成, 所以除了光照贴图的采样代码,其他的代码都迁移至顶点着色器即可。 

Shader "Custom/VertexCombine"
{
    Properties
    {
        _DiffuseColor("Diffuse",Color)=(1,1,1,1)
        _SpecularColor("SpecularColor",Color)=(1,1,1,1)
        _Gloss("Gloss",Range(8.0,256))=20
    }

    SubShader
    {
        pass
        {
            Tags
            {
                "LightMode"="ForwardBase"
            }

            CGPROGRAM 
            #pragma vertex myVert
            #pragma fragment myFrag

            #include "Lighting.cginc"

            fixed4 _DiffuseColor;
            fixed4 _SpecularColor;
            float _Gloss;

            struct VertexData
            {
                float4 vertex:POSITION;
                float3 normal:NORMAL;
            };
            struct VertexToFragment
            {
                float4 pos:SV_POSITION;
                float3 color:COLOR;
            };
            
            VertexToFragment myVert(VertexData data)
            {
                VertexToFragment VToF;
                VToF.pos=UnityObjectToClipPos(data.vertex);

                float3 worldNormal=UnityObjectToWorldNormal(data.normal).xyz;
                float3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
                float3 Diffuse=_DiffuseColor.xyz*saturate(dot(worldNormal,worldLight));

                float3 worldView=normalize(_WorldSpaceCameraPos-mul(unity_ObjectToWorld,data.vertex));

                float3 Specular=_SpecularColor.xyz*pow(max(0,dot(worldView,normalize(worldView+worldLight))),_Gloss);
                float3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;

                VToF.color=ambient+Diffuse+Specular;

                return VToF;
            }

            fixed4 myFrag(VertexToFragment VToF):SV_TARGET
            {
                return fixed4(VToF.color,1.0);
            }

            ENDCG
        }
        pass
        {
            Tags
            {
                "LightMode"="ForwardAdd"
            }

            Blend One One
            CGPROGRAM 

            #pragma multi_compile_fwdadd
            #pragma vertex myVertex
            #pragma fragment myFragment

            #include "Lighting.cginc"
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"

            fixed4 _DiffuseColor;
            float _Gloss;
            fixed4 _SpecularColor;


            struct VertexData
            {
                float4 vertex:POSITION;
                float3 normal:NORMAL;
            };
            struct VertexToFragment
            {
                float4 pos:SV_POSITION;
                float3 color:COLOR;
                float4 worldPos:TEXCOORD0;
            };
            
            VertexToFragment myVertex(VertexData data)
            {
                VertexToFragment VToF;
                VToF.pos=UnityObjectToClipPos(data.vertex);
                float3 worldNormal=normalize(UnityObjectToWorldNormal(data.normal));
                float4 worldPos=mul(unity_ObjectToWorld,data.vertex);

                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
                #else
                    fixed3 worldLight=normalize(_WorldSpaceLightPos0-worldPos.xyz);
                #endif

                float3 Specular=_LightColor0*_SpecularColor.xyz*pow(max(0,dot(worldviewDir,normalize(worldLight+worldviewDir))),_Gloss);

                float3 Diffuse=_LightColor0.xyz*_DiffuseColor.xyz*saturate(dot(worldNormal,worldLight));

                VToF.worldPos=worldPos;
                
                VToF.color=(Diffuse+Specular);
                return VToF;
            }

            fixed4 myFragment(VertexToFragment VToF):SV_TARGET
            {
                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed atten=1.0;
                #else
                    float3 lightCoord=mul(unity_WorldToLight,fixed4(VToF.worldPos)).xyz;
                    fixed atten=tex2D(_LightTexture0,dot(lightCoord,lightCoord).xx).UNITY_ATTEN_CHANNEL;
                #endif

                return fixed4(VToF.color*atten,1.0);
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

我们两个多光照的计算方式的对照如下图:左边是逐顶点计算的前向渲染,右边是逐像素计算的前向渲染。球体的割面为12面。

可以看到逐顶点计算的方式受到面片分布的影响很大:

Shader 学习笔记:光照_第8张图片

我们此时添加一个另外一个平行光,设置成蓝色,那么当前两个球的光照效果如下:

Shader 学习笔记:光照_第9张图片

然后在FrameDebugger中可以看到:

第一个平行光在BasePass中计算得来,并且混合系数为One Zero,即完全替换目标中的颜色:

Shader 学习笔记:光照_第10张图片

第二个蓝色平行光在AdditionalPass中计算得来,混合系数为One One,即与目标中的颜色相叠加:

Shader 学习笔记:光照_第11张图片

 


前向渲染中的阴影:

我们希望能将一些我们的着色器能投射阴影或者接受阴影,Unity使用名为ShadowMap的技术来实现这一点。

ShadowMap:将摄像机放置在与光源重合的地方,那么该光源的阴影区域即为摄像机所看不到的位置,Unity将会为该光源计算它的阴影映射纹理(ShadowMap)。

阴影映射纹理本质上是一张深度图。它记录了从该光源位置出发能看到的场景中距离它最近的表面位置。即为这个位置对应模型的深度信息。

在Unity中,阴影映射纹理的计算并不在BasePass和AdditionalPass中,而是处于一个设置标签为ShadowCaster的Pass中来更新光源的阴影映射纹理。

  • unity首先将摄像机放置在光源的位置上,然后调用名为ShadowCaster的Pass。
  • 底层渲染引擎会在该模型对应的Shader中寻找ShadowCaster的Pass。如果不存在该Pass,则将会在FallBack指定的Shader的Pass中继续寻找。如果仍然没有,则该物体不会向其他物体接受阴影。
  • 如果寻找到了该pass,Unity会调用该Pass获得可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理,根据这两种纹理来得到屏幕空间的阴影图。

如果深度纹理中记录的表面深度大于转换到阴影映射纹理中的深度值,那么说明该模型表面可见,但是处于该光源的阴影中。通过这样的方式,阴影图记录了屏幕空间中所有存在阴影的区域。

所以,如果我们想要让模型接受来自其他光源的阴影,只需要在Shader中对屏幕空间下的阴影图进行采样就行,由于阴影纹理处于屏幕空间中,所以需要将模型坐标由模型空间转入屏幕空间中。这种计算方式也称为屏幕空间的阴影映射技术。

  • 如果一个物体需要接受其他物体投射的阴影,就必须在Shader中对阴影纹理和屏幕空间的阴影图进行采样,然后将采样结果与光照结果相乘来得到阴影。
  • 如果一个物体需要向其他物体投射阴影,就必须将该物体加入到光源的阴影映射纹理的计算中。从而让其他物体在对阴影纹理采样时能够得到这个投射阴影物体的相关信息,这个过程通过设置“LightMode”为“ShadowCaster”的pass来实现。

物体投射阴影: 一个物体向其他物体投射阴影很简单,甚至不需要自己定义ShadowCaster的shader,只需要对定义FallBack然后让Unity自己去寻找就好了,例如上文中的FallBack "Diffuse"语句中,Unity内置的漫反射Shader“Diffuse虽然本身也没有定义这样一个语句,但是Diffuse自身也FallBack了一个Shader“VertexLit”,内部最终会找到标签为ShadowCaster的Shader。

例如下图中的球使用逐顶点光照的Shader,它在更新深度纹理(UpdateDepthTexture)时调用了Shader:VertexCombine中名为ShadowCaster的Shader来更新纹理信息,但是我们上文的那个shader只有两个Pass并且不存在标签为ShadowCaster的pass。这个pass是Unity在Fallback中找到的

Shader 学习笔记:光照_第12张图片

并且,在获得平行光的阴影映射纹理的步骤(RenderShadowMap)中,我们看到这个类似于上帝视角的位置就是摄像机在光源位置投射下来的角度,它也同样调用了Pass:ShadowCaster来得到光源的阴影映射纹理:

Shader 学习笔记:光照_第13张图片

 

物体接受阴影:Unity中,一个物体接受其他物体的阴影基本上都是基于宏的操作,在BasePass中需要有如下的操作:

1.在顶点着色器传入片元着色器的结构体中使用宏SHADOW_COORDS来声明一个阴影坐标,这个宏的参数是我们下一个可以用的插值寄存器的索引值,例如我们这里已经定义了Texcoord0和Texcoord1了,所以宏中的值为2:

Shader 学习笔记:光照_第14张图片

2.在顶点着色器返回VToF结构体前,使用内置宏TRANSFER_SHADOW来计算结构体中声明的阴影纹理坐标:

3.片元着色器中计算阴影的衰减值,这同样也是一个内置宏SHADOW_ATTENUATION:

在《Shader 入门精要》里面,这三个内置宏被称为阴影三剑客,他们都在内置文件AutoLight.cginc中可以找到。

  • SHADOW_COORDS实际上是声明了一个名为ShadowCoord的阴影坐标纹理变量。
  • TRNASFER_SHADOW则会调用内置的ComputeScreenPos来计算阴影坐标纹理ShadowCoord,然后将顶点坐标从模型空间变换到光源空间中并存储在ShadowCoord中。
  • SHADOW_ATTENUATION负责使用ShadowCoord采样来得到阴影信息。

 UNITY_LIGHT_ATTENUATION——光照衰减和阴影的统一处理:

在前面的前向渲染中,Base处理最亮的平行光和不重要的光源,Additional处理剩下的平行光和不重要的光源,但实际上,多光照和阴影实际上都是一致的:通过计算光照衰减因子和阴影值与最终输出的颜色相乘。所以二者可以等而视之。

在Unity中提供了一个非常好用的内置宏UNITY_LIGHT_ATTENUATION来统一计算光照和衰减因子。我们可以用它来代替使用光照贴图的光照衰减因子计算以及UNITY_LIGHT_ATTENUATION的阴影衰减因子计算。

它的三个参数有如下规则:

  • atten接收光照衰减和阴影值的乘积,这个值并不是我们声明的,而是在UNITY_LIGHT_ATTENUATION中自动声明,它有些许类似于C#函数参数中的out语法。
  • VToF中定义了阴影坐标SHADOW_COORDS,它被传递给SHADOW_ATTENUATION来计算阴影值。
  • 第三个参数是世界空间坐标worldPos,这个参数会转入到光源空间下然后与光源空间采样来得到光源衰减值。

这样我们两个Pass的代码就变得统一了起来,两个Pass除了一些指令和标签以外几乎可以变成一模一样,我们在下文中给出这样的代码,其中,被省略的代码为不使用UNITY_LIGHT_ATTENUATION时的计算方式:

Shader "Custom/FragmentCombine"
{
    Properties
    {
        _DiffuseColor("Diffuse",Color)=(1,1,1,1)
        _SpecularColor("SpecularColor",Color)=(1,1,1,1)
        _Gloss("Gloss",Range(8.0,256))=20
    }

    SubShader
    {
        pass
        {
            Tags
            {
                "LightMode"="ForwardBase"
            }

            CGPROGRAM 

            #pragma multi_compile_fwdbase

            #pragma vertex myVert
            #pragma fragment myFrag

            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            fixed4 _DiffuseColor;
            float _Gloss;
            fixed4 _SpecularColor;

             struct VertexData
            {
                float4 vertex:POSITION;
                float3 normal:NORMAL;
            };
            struct VertexToFragment
            {
                float4 pos:SV_POSITION;
                float3 Normal:TEXCOORD0;
                float4 worldPos:TEXCOORD1;
                SHADOW_COORDS(2)
            };
            
            VertexToFragment myVert(VertexData v)
            {
                VertexToFragment VToF;
                VToF.pos=UnityObjectToClipPos(v.vertex);
                VToF.Normal=UnityObjectToWorldNormal(v.normal);
                VToF.worldPos=mul(unity_ObjectToWorld,v.vertex);
                TRANSFER_SHADOW(VToF);
                return VToF;
            }

            fixed4 myFrag(VertexToFragment VToF):SV_TARGET
            {
                fixed3 worldLight=normalize(UnityWorldSpaceLightDir(VToF.worldPos));
                float3 worldNormal=normalize(VToF.Normal.xyz);
                float3 worldviewDir=normalize(_WorldSpaceCameraPos.xyz- VToF.worldPos.xyz);

                float3 Specular=_LightColor0*_SpecularColor.xyz*pow(max(0,dot(worldNormal,normalize(worldviewDir+worldLight))),_Gloss);

                float3 Diffuse=_LightColor0.xyz*_DiffuseColor.xyz*saturate(dot(worldNormal,worldLight));

                float3 Ambiet=UNITY_LIGHTMODEL_AMBIENT;

                //fixed shaow=SHADOW_ATTENUATION(VToF);
                //return fixed4((Ambiet+Diffuse+Specular)*shaow,1.0);
                UNITY_LIGHT_ATTENUATION(atten,VToF,VToF.worldPos.xyz);
                return fixed4(Ambiet+(Diffuse+Specular)*atten,1.0);
            }
            ENDCG
        }
        pass
        {
            Tags
            {
                "LightMode"="ForwardAdd"
            }

            Blend One One
            CGPROGRAM 

            #pragma multi_compile_fwdadd_fullshadows
            #pragma vertex myVertex
            #pragma fragment myFragment

            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            fixed4 _DiffuseColor;
            float _Gloss;
            fixed4 _SpecularColor;


            struct VertexData
            {
                float4 vertex:POSITION;
                float3 normal:NORMAL;
            };
            struct VertexToFragment
            {
                float4 pos:SV_POSITION;
                float3 Normal:TEXCOORD0;
                float4 worldPos:TEXCOORD1;
                SHADOW_COORDS(2)
            };
            
            VertexToFragment myVertex(VertexData v)
            {
                VertexToFragment VToF;
                VToF.pos=UnityObjectToClipPos(v.vertex);
                VToF.Normal=UnityObjectToWorldNormal(v.normal);
                VToF.worldPos=mul(unity_ObjectToWorld,v.vertex);
                TRANSFER_SHADOW(VToF);
                return VToF;
            }

            fixed4 myFragment(VertexToFragment VToF):SV_TARGET
            {
                fixed3 worldLight=normalize(UnityWorldSpaceLightDir(VToF.worldPos));
                float3 worldNormal=normalize(VToF.Normal.xyz);
                float3 worldviewDir=normalize(_WorldSpaceCameraPos.xyz- VToF.worldPos.xyz);

                float3 reflectDir=normalize(reflect(-worldLight,worldNormal));

                float3 Specular=_LightColor0*_SpecularColor.xyz*pow(max(0,dot(worldviewDir,reflectDir)),_Gloss);

                float3 Diffuse=_LightColor0.xyz*_DiffuseColor.xyz*saturate(dot(worldNormal,worldLight));

                //#ifdef USING_DIRECTIONAL_LIGHT
                //    fixed atten=1.0;
                //#else
                //    float3 lightCoord=mul(unity_WorldToLight,fixed4(VToF.worldPos)).xyz;
                //    fixed atten=tex2D(_LightTexture0,dot(lightCoord,lightCoord)).UNITY_ATTEN_CHANNEL;
                //#endif
                //fixed shaow=SHADOW_ATTENUATION(VToF);
                //return fixed4((Diffuse+Specular)*atten*shaow,1.0);
                UNITY_LIGHT_ATTENUATION(atten,VToF,VToF.worldPos.xyz);
                return fixed4((Diffuse+Specular)*atten,1.0);
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}

我们将两个球设置成前文的两个Shader(逐顶点和逐像素),将平面使用上文的Shader,它们输出的效果如下:

Shader 学习笔记:光照_第15张图片

我们可以看到,使用了上文Shader的平面接收了阴影,并且与光照产生了混合。 

你可能感兴趣的:(Shader以及相关的,Shader基础)