此文及专栏系是以Shader入门精要为基础整理的Unity Shader学习笔记,尽量以初学者视角还原(其实半年前我就是初学者),错误还需指正。
本篇是实操部分的第三个Shader,即高光反射Shader,文章选取顶点着色器生成的高光反射Shader作为说明,具体名词可能不再解释。
本篇实现的是高光反射这一渲染中的重要主题,这种效果通常出现在金属、镜面反射等场合。按照图形学中基础的光照模型,高光反射的光强及色彩由以下公式决定:
从左到右的参数分别是:最终结果,即高光反射的光强和色彩;入射光线的颜色和强度;高光反射系数;max函数的运算结果,是对观察视角向量v和反射方向向量r的取正。
是材质的光泽度,这可能是大多数人不曾了解过的名词,事实上我尝试如下理解,请看下面这个光球,如果无视右下角的光泽,那么大亮点就是我们的高光部分。想象一下,亮点的中心那里反射光线正好与观察视角向量重合,也即我们的“眼睛”正好接收到那里的反射光线,v和r单位向量乘积为1;而偏移了那里,反射光线就和我们的视角有夹角了,v和r这两个单位向量的乘积逐渐减小,因为光泽度作为指数存在,这个结果逐渐接近0,也就是光线减弱趋向于消失。
当然,反射方向我们还需要计算,其公式为:
这一效果可以由CG语言的相关函数得到。其中n和I向量分别为法线方向和光源入射方向。
接下来正式撰写我们的Shader,仍然基于2020版本的Unity,实测可以渲染成功,环境配置不再赘述,全部代码如下:
Shader "Unlit/HighLightShader"
{
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;
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));
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed halfLambert = dot(worldNormal, worldLightDir)*0.8+0.2;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
//接下来正式计算高光部分
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(reflectDir, viewDir)),_Gloss);
o.color = ambient + diffuse +specular;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 片元着色器,简单传参
return fixed4(i.color, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}
区别于简单的漫反射,我们的高光Shader不能再使用基础的Color属性参数了,这里提供给材质方面的自定义接口还包括反射颜色和光泽度。
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1) //这是反射色彩纹理
_Gloss("Gloss", Range(8.0, 256)) = 20 //这是光泽度
}
我们传入一个Color属性参数,用于反射色彩(为什么要专门新建一个变量,区别于漫反射色彩?我的理解是这玩意和漫反射还是不一样的,比如玻璃和一些镀膜金属就有和表面不同的反射色彩,而且这也方便了游戏美术的运用),至于光泽度Gloss采用float类型,我们这里用Range函数定义,这个函数一看就懂,表示数值范围是8到256(我们前面对光泽度有解释,这个参数放在公式指数位置,因此必然是一个远大于1的值)
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;
fixed3 color : COLOR;
};
这里不再多解释了,tags表示Pass块在渲染管线中的位置,#pragma用法指定着色器函数,#include包含内置库以使用参数,然后再把properties中的参数重新声明;a2v和v2f分别是我们顶点着色器的输入和输出结构体,区别于上一篇漫反射Shader中计算大部分在片元着色器,我们这里计算主要在顶点着色器完成,所以需要把色彩信息color也传出。
v2f vert (a2v v)
{
//这是反射部分计算,参考上一篇
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
fixed3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed halfLambert = dot(worldNormal, worldLightDir)*0.8+0.2;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
//接下来正式计算高光部分
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(reflectDir, viewDir)),_Gloss);
o.color = ambient + diffuse +specular;
return o;
}
顶点着色器的内容分两部分,反射部分和高光部分。
反射部分参考上一篇文章,其原理相近。首先拿到Unity计算好的输入结构体,使用UnityObjectToClipPos裁剪vertex(实际上是POSITION顶点位置)(裁剪可以理解为过滤掉不会被渲染的点),然后通过内置的变换矩阵unity_WorldToObject,将这一坐标转换成世界坐标系,最后用normalize将其单位化。
UNITY_LIGHTMODEL_AMBIENT是内置变量,代表系统接收到的环境光部分(封装相当好,不必关心实现)。_LightColor0和_WorldSpaceLightPos0分别是光照的光量(色彩和光强)和方向向量信息,我们将这些参数引用,并对光照方向向量进行单位化。(xyz自然就是坐标啦)
接下来使用了修正过的半兰伯特光照模型,我们上次提到,纯粹按照漫反射模型计算,因为没有考虑到反射,物体的阴影部分是纯黑的。半兰伯特模型本来是对光照和物体法线两个向量的运算结果,做一个线性变换,也就是dot(worldNormal, worldLightDir)*0.5+0.5,保证不会出现纯黑,但是这样会导致我们最后渲染出的高光不明显,不利于使用,因此修正为dot(worldNormal, worldLightDir)*0.8+0.2。最后_LightColor0.rgb * _Diffuse.rgb * halfLambert这个式子计算出漫反射结果。
接下来就是高光部分了,根据公式
我们用reflect(-worldLightDir, worldNormal) 计算出反射向量(我们不关心封装好的函数,但可以知道原理,不是吗),这里的worldLightDir加了个负号取反了,因为reflect函数的入射光线要求是光照点指向光源,而unity提供的_WorldSpaceLightPos0是反向的,需要反着来。
我们来看公式,此时,我们已经拿到了(_LightColor0,内置参数)、(_Specular.rgb,properties引入的参数)、反射向量和观察视角向量,_LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)),_Gloss)这个式子计算出最后的高光反射结果(pow是指数运算函数),然后与漫反射结果相加,输出即可。
fixed4 frag (v2f i) : SV_Target
{
// 片元着色器,简单传参
return fixed4(i.color, 1.0);
}
ENDCG
}
}
Fallback "Specular"
前面提过,这个Shader没用片元着色器进行计算,因此frag函数的内容非常简单,加一个默认值1.0的分量即可。最后Fallback里面用Specular,因为我们这是高光反射Shader嘛,应该拿unity的反射Shader作为备份。
最后的渲染效果是右边这样,左边则是上一篇的漫反射。你看,是不是很像unity默认的那种,感觉有点油腻的反射。事实上不必嫌弃它,我们已经实现了基本的高光反射,预设的一些目标也已经完成,比如我们修正的半兰伯特模型也保证了背后不至于全黑。