解读Unity中的CG编写Shader系列十 (光滑的镜面反射(冯氏着色))
前文完成了最基本的镜面反射着色器,单平行光源下的逐顶点着色(per-vertex lighting),又称为古罗着色(Gouraud shading)。这篇文章作为后续讨论更光滑的镜面反射方式,逐像素着色(per-pixcel lighting),又称为冯氏着色(Phong shading)
逐像素着色Per-Pixel Lighting (冯氏着色Phong Shading)
别把冯氏着色与冯氏反射模型搞混淆了,前问提到了冯氏反射模型,冯氏反射模型是为使计算机模拟接近真实的物体表面光泽提出的模型,即环境光(虚拟的)+漫反射光+镜面反射光=表面色彩。
逐顶点着色,故名思意跟顶点有关,也就是在我们的顶点着色器中根据每个顶点上的入射向量L、法向量N、观察向量V等直接计算出每个顶点该有的颜色,然后传递给后续环节进行着色,可想而知由于顶点是离散的,片段是连续的,所以引起着色效果的不光滑很容易理解。
那么逐像素着色与之对应的即是在片段着色器中,对法向量与坐标进行插值(如果不能理解插值,请百度一下),从而使离散的顶点计算出来的离散的颜色变得连续而光滑。这就是冯氏着色的要义。
将前文的古罗着色改为冯氏着色
说起来简单,做起来更简单了,我们直接把环境光、漫反射光、镜面反射光的计算拿到片段着色器中计算即可完成修改。为何不一开始就直接在片段着色器中写,是为了使看官对这两种不同的着色方式有一个印象。
在开始修改前我们只需要明白一个道理就行了:
我们之前在顶点着色器中获取到的法向量、观察向量、入射向量直接完成计算变成了颜色传递给片段着色器中,因此片段着色器的输入参数中基本上只有颜色。现在我们要把计算过程移到片段着色器中,那么法向量、观察向量、入射向量同理需要传递给片段着色器,而不再是直接传递一个颜色。
而观察向量与入射向量分别又是由世界相机坐标以及世界光源向量计算的,这两个信息并非meshRenderer传递给顶点着色器的,而是直接在cg中内置的uniform参数,因此在片段着色器中也可以直接获取,因此我们只需要传递顶点的坐标以及转换后的法向量。
因此我们的顶点着色器的输出结构体(又是片段着色器的输入结构体)应该修改为:
//定义顶点着色的输出结构体/片段着色的输入结构体 //去掉颜色 添加顶点的世界坐标以及法向量,这里的语义使用了TEXCOORD的两个集合,这里的TEXCOORD是我们自己使用的,与顶点着色器输入时使用该语义 已经有区别了 struct vertexOutput { float4 pos : SV_POSITION; float4 posWorld : TEXCOORD0; float3 normalDir : TEXCOORD1; };
片段着色器接受到法向量后进行一定的插值,其过程是再次单位化,为什么两次单位化就能完成插值,我线性代数也不是很好,大家可以自己研究下。
然后把计算过程搬至片段着色中,系列9的代码修改后的代码为:
Shader "Custom/PhoneShadingSpecular" { Properties { _Color ("Diffuse Material Color", Color) = (1,1,1,1) _SpecColor ("Specular Material Color", Color) = (1,1,1,1) //材料表面的光泽程度,根据前文所述,此参数无穷大时,材料完全不会产生镜面反射 _Shininess ("Shininess", Float) = 10 } SubShader { Pass{ Tags { "LightMode" = "ForwardBase" } CGPROGRAM //定义顶点着色器与片段着色器入口 #pragma vertex vert #pragma fragment frag //获取property中定义的材料颜色 uniform float4 _Color; uniform float4 _SpecColor; uniform float _Shininess; // 光源的位置或者方向 //uniform float4 _WorldSpaceLightPos0; // 光源的颜色 (from "Lighting.cginc") uniform float4 _LightColor0; //定义顶点着色器的输入参数结构体 //我们只需要每个顶点的位置与对应的法向量 struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; }; //定义顶点着色的输出结构体/片段着色的输入结构体 //去掉颜色 添加顶点的世界坐标以及法向量 struct vertexOutput { float4 pos : SV_POSITION; float4 posWorld : TEXCOORD0; float3 normalDir : TEXCOORD1; }; //顶点着色器 vertexOutput vert (vertexInput input) { vertexOutput output; //对象坐标系到世界坐标系的变换矩阵 //_Object2World与_World2Object均为unity提供的内置uniform参数 float4x4 modelMatrix = _Object2World; //世界坐标系到对象坐标系的变换矩阵 float4x4 modelMatrixInverse = _World2Object; //法向量N变化至对象坐标系 output.normalDir = normalize(float3(mul(float4(input.normal, 0.0), modelMatrixInverse))); //将顶点坐标向世界坐标系变换 output.posWorld=mul(modelMatrix,input.vertex); //国际惯例,顶点变化三步曲 output.pos = mul(UNITY_MATRIX_MVP, input.vertex); return output; } //片段着色器,老规矩,把顶点着色器的输出参数作为片段着色器的输入参数 float4 frag(vertexOutput input): COLOR { //接受顶点着色器传递的法向量与顶点世界坐标 //这里必须将法向量再normalize一次 //尽管在顶点着色器中已经normalize了一次 float3 normalDirection=normalize(input.normalDir); float3 worldPosition=input.posWorld; //观察向量V由摄像机坐标与顶点坐标矢量相减 //这里改顶点坐标为上面获取到的世界坐标 float3 viewDirection = normalize(float3(float4(_WorldSpaceCameraPos, 1.0) - worldPosition)); /*下面的部分直接招搬就好了*/ //平行光源的入射向量L直接由uniform_WorldSpaceLightPos0给出 float3 lightDirection =normalize(float3(_WorldSpaceLightPos0)); //镜面反射光的计算 float3 specularReflection=float3(_LightColor0)*float3(_SpecColor)*pow(max(0.0,dot(reflect(-lightDirection, normalDirection),viewDirection)),_Shininess); //前文计算好的漫反射光 float3 diffuseReflection=float3(_LightColor0) * float3(_Color)* max(0.0, dot(normalDirection, lightDirection)); //环境光直接获取 float3 ambientLighting = float3(UNITY_LIGHTMODEL_AMBIENT) * float3(_Color); //根据冯氏反射模型将上述3个RGB颜色向量相加,然后补充A: return float4(ambientLighting + diffuseReflection+ specularReflection, 1.0);; } ENDCG } } FallBack "Diffuse" }
最后的效果图,我们与上一个例子中的球体进行对比:
图左为冯氏着色下,图右为上一例的古罗着色,可见冯氏还挺厉害的啊,又是反射模型又是逐像素着色都是他的名字