转载自http://blog.csdn.net/lzhq1982/article/details/74941687
上一篇学习了基础漫反射的实现,这一篇我们看看基础高光反射的实现。
基本光照模型中高光反射的计算公式:
从上面公式可以看出,我们需要4个变量,入射光线的颜色和强度c(light),材质的高光反射系数m(specular),视角方向v及反射方向r。其中,反射方向r可以由法线 n 和光源方向 I 计算计算得到:
然而CG早就给我们提供了计算反射方向的函数reflect(i, n)。
参数:i,入射方向;n,法线方向。
描述:当给定入射方向i和法线方向n时,reflect函数可以返回反射方向。
1、逐顶点光照
还是像上篇一样,准备一个胶囊体,放上新建的材质,绑好Shader模板,然后我们改写Shader,直接上代码
Shader "CustomShader/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 appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; fixed3 color : COLOR; }; v2f vert (appdata v) { v2f o; o.vertex = mul(UNITY_MATRIX_MVP, 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)); fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(viewDir, reflectDir)), _Gloss); o.color = ambient + diffuse + specular; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = fixed4(i.color, 1.0); return col; } ENDCG } } FallBack "Specular"}
上面代码也很简单,我们抛去这里的漫反射部分,上一篇已经介绍过了,这里只看高光反射部分。
首先属性里,我们加入了_Specular,控制材质高光反射颜色,_Gloss控制高光区域大小。
关于标签,头文件上一篇介绍过了,这里不再说,颜色属性范围是[0, 1]。所以直接用fixed精度就可以了。_Gloss范围大,用float。这里是逐顶点光照,核心逻辑都在顶点着色器里了。前面是环境光和漫反射,我们重点看高光反射部分,高光反射需要的4个变量,光线强度和颜色我们用_LightColor0,高光反射颜色我们用传入的_Specular,然后我们看v和r,注意,我们要用世界空间的v和r,v是视角方向,就是眼睛看到该点的方向,眼睛是摄像机,所以摄像机减改点就可以得到视角方向:
fixed3 viewDir =
normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);
v.vertex是模型空间的点,所以先转化到世界空间,最后不要忘了归一化。
反射方向我们用reflect函数,CG的reflect函数的入射方向要求是由光源指向交点处,所以我们对worldLightDir取反:
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
最后全部代入公式,得到高光反射颜色:
fixed3 specular =
_LightColor0.rgb * _Specular.rgb * pow(saturate(dot(viewDir, reflectDir)), _Gloss);
最后把环境光,漫反射和高光反射相加即可。
片元着色器啥也没做,直接输出颜色。Fallback返回Shader内置的Specular。
最终效果如下图:
我们可以看出高光部分很不平滑,因为高光反射部分是非线性的,而顶点着色器到片元着色器的插值是线性的,这样得出的效果会不准确。那我们来看逐像素的。
2、逐像素光照
直接上代码吧:
Shader "CustomShader/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 appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; }; v2f vert (appdata v) { v2f o; o.vertex = mul(UNITY_MATRIX_MVP, 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 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); fixed3 worldNormal = normalize(i.worldNormal); fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * (saturate(dot(worldNormal, worldLightDir))); fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal)); fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(viewDir, reflectDir)), _Gloss); fixed3 color = ambient + diffuse + specular; return fixed4(color, 1.0); } ENDCG } } FallBack "Specular"}
核心代码和逐顶点区别不大,只是放在片元着色器中计算了,当然为了片元着色器中计算用到的法线和顶点的世界坐标,我们把它们放在顶点输出结构体中,并在顶点着色器中计算出世界法线和世界坐标,经插值传给片元着色器进行计算,后面就一样了,不解释,最后看效果:
是不是平滑多了,至此,我们实现了一个完整的Phong光照模型。
3、Blinn-Phong光照模型
在基础光照的概念和理论这篇里我们还提到了另一种高光反射的实现方法——Blinn光照模型。它没用反射方向r,而是用了一个新的矢量h,它是通过视角方向v和光照方向I相加后再归一化得到的,即:
那么Blinn的公式如下:
上代码:
Shader "CustomShader/Specular 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 appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; }; v2f vert (appdata v) { v2f o; o.vertex = mul(UNITY_MATRIX_MVP, 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 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); fixed3 worldNormal = normalize(i.worldNormal); fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * (saturate(dot(worldNormal, worldLightDir))); fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos); fixed3 blinn = normalize(viewDir + worldLightDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, blinn)), _Gloss); fixed3 color = ambient + diffuse + specular; return fixed4(color, 1.0); } ENDCG } } FallBack "Specular"}
还是逐像素的光照,和之前的写法没啥区别,只是公式部分用了Blinn的公式,我们注意到这两行就行了:
fixed3 blinn = normalize(viewDir + worldLightDir);
fixed3 specular =
_LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, blinn)), _Gloss);
然后我们看对比效果:
可以看出,Blinn-Phong模型的高光反射部分看起来更大、更亮一些。在实际渲染中,我们绝大多数情况会用Blinn-Phong模型。
4、使用Unity内置的函数
从上面代码可以看出,我们经常需要光源方向,视角方向等信息。而上面我们用代码实现的它们,其实只适用于平行光,简单的光照效果,不适用于点光源、聚光灯等复杂的光照模型。而手动计算这些信息很麻烦。Unity为我们提供了内置函数。
有了它们我们就方便多了,今后我们会用它们代替计算,有一点需要注意,它们不保证归一化,所以我们要手动归一化。基础光照就介绍到这里,下面我们看基础纹理。