前言

这个并不是技术分享的教程类博客,只是我记录的笔记而已。不,不要误会了!(虽然溜一遍下来法线好像也能看

Shader基础

顶点着色器和片元着色器

顶点着色器

顶点着色器是流水线的第一个阶段。它负责坐标变换以及逐顶点光照。比如把顶点坐标从模型空间转换到齐次裁剪空间,逐顶点光照在后面会说。

片元着色器

片元着色器又称为像素着色器,它的输入是上一个阶段对定点信息插值得到的结果,输出是一个或者多个颜色值。在这个阶段可以完成很多重要渲染技术(纹理采样)。但是它只能影响单个片元,无法将任何结果发给其他人。

Shader的结构

Shader"MyShaderName" //Shader的名字,里面添加/可以创建文件夹
{
Properties{
	//属性,声明一些着色需要的变量
	_Color("Color Tint",Color)=(1,1,1,1)
	//代码内叫啥("窗口里叫啥",啥类型)=赋值
}
SubShader{
//针对显卡A的SubShader
Pass{
//设置渲染状态和标签,此时声明Properties的变量
Tags{"LightMode"="ForwardBase"}

CGPROGRAM //开始CG代码的片段

//该代码片段的编译指令:
#pragma vertex vert //vert函数为顶点着色方式
#pragma fragment frag //frag函数为片元着色方式

#include "Lighting.cginc" //声明引用的头文件

struct a2v{
	//可以在这里声明结构体
};

struct v2f{

};

v2f vert(a2v v){
//进行顶点着色器的相关操作
}

fixed4 frag(v2f i):SV_Target{
//进行片元着色器的相关操作
}

ENDCG //结束CG代码的片段
}
//其它需要的Pass
}
FallBack "Diffuse"//如果上面SubShader全寄了,就回调双引号内的Unity Shader
}

变量声明形式

fixed4 :什么精度的数字+多少个连一起

精度: fixed<half<float

标准光照模型

在标准光照模型中,把进入摄像机的模型分成四部分,每个部分分别计算贡献度。四个部分分别为自发光、高光反射、漫反射、环境光

环境光(ambient)

用来模拟间接光照。简介光照指在多个物体反射后进入摄像机的光线。环境光为一个全局变量,计算等式为:

$$ c_{ambient}=g_{ambient} $$

在Shader中,我们通过Unity内置变量UNITY_LIGHTMODEL_AMBIENT直接获得环境光颜色和强度信息。

自发光(emissive)

直接从光源进入摄像机的光线,计算等式为:

$$ c_{emissive}=m_{emissive} $$

由于大部分物体没有自发光特性,所以不存在Unity变量。对于存在自发光特性的物体,我们要先把材质的自发光颜色添加到输出颜色上再输出就行了。

漫反射(diffuse)

用于对那些被物体表面随机散射到各个方向的辐射度进行建模。该光照符合兰伯特定律:

$$ c_{diffuse}=(c_{light}*m_{diffuse})max(0,\hat{n}*\hat{l}) $$

其中clight是光源颜色,mdiffuse是材质的漫反射颜色,n为表面法线,l为指向光源的单位矢量。当两者夹角为0时,说明光源直射,为最大值1.当两者垂直时值为0,说明光源射不到该表面。而大于90度已经是负值(没有光线反射),所以也算作是0.

高光反射(specular)

指完全沿着镜面反射方向被反射的光线,让物体看起来是有光泽的。

Phong模型

计算高光反射所需信息有表面法线,视角方向,光源方向和反射方向等。其中反射方向可以通过前两个计算而得出:

$$ \hat{r}=2*(\hat(n)*\hat(l))*\hat(n)-\hat(l)$$

(知道法线方向和入射方向求反射方向属于是初中知识了。。。)

这样可以通过上述信息,使用Phong模型计算高光反射的部分:

$$ c_{specular}=(c_{light}*m_{specular})max(0,\hat{v}*\hat{r})^m_{gloss}$$

其中mgloss为光泽度,用于控制高光区域“亮点”的宽度。光泽度越大亮点越小。(毕竟下面范围是[0,1],肯定越大是越小的)mspecular是材料的高光反射,控制强度和颜色。clight是光源颜色和强度。

Blinn模型

另一种高光反射模型为Blinn模型。它与Phong模型的区别在于避免计算反射方向,而是在对光源方向和视角方向取平均后再归一化后,引入新的矢量得到的。即

$$\hat(h)={\frac {\hat(v)+\hat(l)}{\lvert \hat(v)+\hat(l) \rvert}}$$

后面就和Phong模型差不多,但是是拿n和h之间的夹角计算:

$$ c_{specular}=(c_{light}*m_{specular})max(0,\hat{n}*\hat{h})^m_{gloss}$$

逐像素还是逐顶点

计算光照可以在两个着色器中计算。其中在片元着色器计算叫做逐像素光照(Phong着色),在顶点着色器中计算叫做逐顶点光照(高洛德着色)。

由于逐顶点光照只需在每个顶点上计算光照,然后再渲染图元内部线性插值,所以计算量小于逐像素光照。但由于存在非线性计算时,过度依赖线性插值的逐顶点光照就会产生问题。(特别是在高光反射上)

漫反射光照模型实践

逐顶点光照计算

Shader "Custom/Diffuse Vertex-Level"
{
    Properties
    {
        _Diffuse("Diffuse",Color)=(1,1,1,1)
        //控制材质的漫反射颜色,可以在hierachy窗口调整
    }
    SubShader
    {
        pass{
        //定义正确的LightMode才能得到Unity的内置光照变量(如_LightColor0)
        Tags { "LightMode"="ForwardBase" }
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        
        //需要使用下面的内置文件
        #include "Lighting.cginc"
		//只有定义一个和Properties类型匹配的变量才能使用Properties内变量
        fixed4 _Diffuse;

        struct a2v{
            float4 vertex:POSITION;
            float3 normal:NORMAL; //告诉Unity将模型顶点法线信息存储到normal变量内
        };

        struct v2f{
            float4 pos:SV_POSITION;
            fixed3 color:COLOR;
        };

        v2f vert(a2v v){
            v2f o;
            o.pos=UnityObjectToClipPos(v.vertex);
            //将顶点位置从模型空间转换到裁剪空间
            fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
            //获取环境光部分
            fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));
            //我们选用世界坐标下计算,而v.normal为模型空间下的,所以需要转化为世界坐标
            //转化流程为得到两空间的变换矩阵的逆矩阵unity_WorldToObject,然后和法线相乘
            //由于法线为一个三维向量,所以只需要截取前三行前三列
            fixed3 worldLight=normalize(_WorldSpaceLightPos0.xyz);
            //将光源方向归一化处理
            fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLight));
            //计算漫反射光照
            o.color=ambient+diffuse;
            //将漫反射光和环境光部分相加,得到最终光照结果。
            return o;
        }
        fixed4 frag(v2f i):SV_TARGET{
            return fixed4(i.color,1.0);
            //由于所有计算在顶点着色器完成了,所以这里直接摸大鱼(
        }
        ENDCG
        }
    }
    FallBack "Diffuse"
}

saturate(x):把x锁定在[0,1]范围内;

normalize(x):将x归一化,只留下方向;

**UnityObjectToClipPos(v)**:将顶点位置从模型空间转换到裁剪空间;

mul(x,y):计算两向量/一向量一矩阵/两矩阵相乘;

dot(x,y):计算x,y的点积。

逐像素光照计算

Shader"Custom/Diffuse Pixel-Level{
Properties
    {
        _Diffuse("Diffuse",Color)=(1,1,1,1)
    }
    SubShader
    {
        pass{
        Tags { "LightMode"="ForwardBase" }
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        
        #include "Lighting.cginc"

        fixed4 _Diffuse;

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

        struct v2f{
            float4 pos:SV_POSITION;
            float3 worldNormal:TEXTCOORD0;
        };

        v2f vert(a2v v){
            v2f o;
            o.pos=UnityObjectToClipPos(v.vertex);
            o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
            //将法线从世界空间转换为模型空间
            //由于计算全给了片元着色器,所以这里摸大鱼(
            return o;
        }
        fixed4 frag(v2f i):SV_TARGET{
            fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT;
            fixed3 worldNormal=normalize(i.worldNormal);
            fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
            fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));
            fixed3 color=ambient+diffuse;
            return fixed4(color,1.0);
        }
        ENDCG
        }
    }
    FallBack "Diffuse"
    }

完 全 相 同

半兰伯特模型

由于兰伯特光照模型没照到的地方全是0,莫得光暗变化,所以万能的v社在开发《半条命》的时候整了个半兰伯特光照模型,公式如下:

$$c_{diffuse}=(c_{light}*m_{diffuse})(0.5(\hat(n)*\hat(l))+0.5)$$

这样背光面也有明暗变化,视觉上加强了不少。

也亮堂了(

代码方面和逐像素计算差不多,但是修改的地方有:

fixed4 frag(v2f i):SV_TARGET{
            fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT;
            fixed3 worldNormal=normalize(i.worldNormal);
            fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
            fixed halfLambert=saturate(dot(worldNormal,worldLightDir))*0.5+0.5;
            //这里单独拎出来算了
            fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*halfLambert;
            fixed3 color=ambient+diffuse;
            return fixed4(color,1.0);
        }

高光反射光照模型实践

之前提到过反射方向的计算公式:

$$ \hat{r}=2*(\hat(n)*\hat(l))*\hat(n)-\hat(l)$$

在Unity里,你只需要**reflect(i,n)**就可以(i入射方向,n法线方向)

逐顶点光照计算

Shader "Custom/Specular Vertex-Level"
{
    Properties
    {
        _Diffuse ("Diffuse",Color)=(1,1,1,1)
        _Specular ("Specular",Color)=(1,1,1,1)
        //控制高光反射的颜色
        _Gloss ("Gloss", Range(8.0,256))=20
        //控制高光反射区域大小
    }
    SubShader
    {
        pass{
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

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

            struct v2f
            {
                float4 pos:SV_POSITION;
                float3 color:COLOR;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                //将顶点位置从模型空间转换到裁剪空间
                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
                //拿环境光
                fixed3 worldNormal=normalize(mul(v.normal,(float3x3)unity_WorldToObject));
                fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
                fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));
                //算漫反射光照
                fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
                //计算反射方向,由于reflect函数入射方向要求从光源指向交点,所以worldLightDir要取反
                fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-mul(unity_ObjectToWorld,v.vertex).xyz);
                //_WorldSpaceCameraPos为摄像机位置,将顶点位置先转换为世界空间,然后与摄像机相减就是视角位置
                fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
                //最后进行Phong模型的计算,得出高光反射的贡献度
                o.color=ambient+diffuse+specular;
                //与环境光、漫反射光相加,存储到最后的颜色里
                return o;
            }

            fixed4 frag(v2f i):SV_Target{
            	//由于在顶点着色器都算完了,所以这里摸大鱼
                return fixed4(i.color,1.0);
            }
            ENDCG
            }
    }
    FallBack "Specular"
}

这时逐顶点计算高光反射的弊端就出现了:阴处都是一片片的。

逐顶点

这是因为高光反射的计算是非线性的,而在顶点着色器里计算光照再插值是线性的。这样破坏了原来计算的非线性关系。

逐像素光照计算

Shader "Unity Shader Book/Chapter 6/Specular Pixel-Level"
{
    Properties
    {
        _Diffuse ("Diffuse",Color)=(1,1,1,1)
        _Specular ("Specular",Color)=(1,1,1,1)
        _Gloss ("Gloss", Range(8.0,256))=20
    }
    SubShader
    {
        pass{
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

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

            struct v2f
            {
                float4 pos:SV_POSITION;
                float3 worldNormal:TEXCOORD0;
                float3 worldPos:TEXCOORD1;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
                o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
                return o;
            }

            fixed4 frag(v2f i):SV_Target{
                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
                fixed3 worldNormal=normalize(i.worldNormal);
                fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
                fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));
                fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
                fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
                fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(saturate(dot(reflectDir,viewDir)),_Gloss);
                return fixed4(ambient+diffuse+specular,1.0);
            }
            ENDCG
            }
    }
    FallBack "Specular"
}

虽然计算过程基本相同,但是得出的效果更令人满意:

逐像素

Blinn-Phong模型计算

由于逐顶点计算高光反射的拉跨程度,所以我们只用逐像素的方法来实践

Shader "Custom/BlinnPhong"
{
    Properties
    {
        _Diffuse ("Diffuse",Color)=(1,1,1,1)
        _Specular ("Specular",Color)=(1,1,1,1)
        _Gloss ("Gloss", Range(8.0,256))=20
    }
    SubShader
    {
        pass{
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

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

            struct v2f
            {
                float4 pos:SV_POSITION;
                float3 worldNormal:TEXCOORD0;
                float3 worldPos:TEXCOORD1;
            };

            v2f vert(a2v v)
            {
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.worldNormal=mul(v.normal,(float3x3)unity_WorldToObject);
                o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
                return o;
            }

            fixed4 frag(v2f i):SV_Target{
                fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
                fixed3 worldNormal=normalize(i.worldNormal);
                fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
                fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));
                fixed3 reflectDir=normalize(reflect(-worldLightDir,worldNormal));
                fixed3 viewDir=normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
                fixed3 halfDir=normalize(worldLightDir+viewDir);
                //扣扣大,计算新矢量h
                fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(worldNormal,halfDir)),_Gloss);
                //然后改成h和n点乘
                return fixed4(ambient+diffuse+specular,1.0);
            }
            ENDCG
            }
    }
    FallBack "Specular"
}

使用Blinn模型的高光反射部分看起来更大、更亮一些,所以大多数情况都会采用Blinn模型。