今天开始正式整合学习的shader内容。
Simple Shader
主要介绍了大概的shader格式。
Shader "Unity Sgaders Book/Chapter 5/Simple Shader" //shader名
{
Properties
{
//声明color类型的属性
_Color("Color Tint", Color) = (1.0,1.0,1.0,1.0)
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert //顶点着色器函数
#pragma fragment frag //片元着色器函数
fixed4 _Color; //定义color类型
//模型空间的输入顶点信息
struct a2v {
float4 vertex:POSITION; //模型空间的顶点坐标
float3 normal:NORMAL;//模型空间的法线方向
float4 texcoord:TEXCOORD0;//模型的第一套纹理坐标
};
//齐次裁剪空间的输出顶点信息
struct v2f {
float4 pos : SV_POSITION;//裁剪空间的顶点坐标
fixed3 color : COLOR0;//颜色信息
};
//输入模型空间的顶点信息,经过顶点着色器函数,输出齐次裁剪空间的顶点信息
v2f vert(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);
return o;
}
//输入齐次裁剪空间的顶点信息(经过插值之后),输出顶点颜色信息
fixed4 frag(v2f i) : SV_Target
{
fixed3 c = i.color;
c *= _Color.rgb;
return fixed4(c,1.0);
}
ENDCG
}
}
}
其主要意义在于通过模型法线获取不同参数,从而在材质面板中(材质面板默认显示球体,球体上每一个顶点的法线都不一样)显示一个颜色拾取器。效果如下图所示:
False Color
假彩色图像(false-color image)用于可视化一些数据,以方便对shader进行调试。即将想要的数据映射到[0-1]区间,作为颜色输出到屏幕上,然后通过屏幕上显示的像素颜色来判断这个值是否正确。通常用于debug。shader代码如下:
Shader "Unity Sgaders Book/Chapter 5/False Color"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float4 pos:SV_POSITION;
fixed4 color : COLOR0;
};
v2f vert(appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//可视化法线方向
o.color = fixed4(v.normal * 0.5 + fixed3(0.5,0.5,0.5),1);
//可视化切线方向
//o.color = fixed4(v.tangent * 0.5 + fixed3(0.5,0.5,0.5),1);
//可视化第一组纹理坐标
//o.color = fixed4(v.texcoord.xy, 0, 1);
//可视化顶点颜色
//o.color = v.color;
return o;
}
//不知道作者为何在这里只输出一个fixed参数,即只利用了第一个颜色参数,输出出来的只有红色
fixed frag(v2f i) :SV_Target
{
return i.color;
}
ENDCG
}
}
}
比如输出法线可视化,效果如下:
Diffuse Vertex Level/Diffuse Pixel Level
漫反射光照即是入射光线经法线后的反射光线的强度与“入射光线和发现之间的点积”有正比关系。颜色方面,需要叠加材质的漫反射颜色和入射光线的颜色。于是得出以下公式:
可以写下如下代码(逐顶点光照):
Shader "Unity Sgaders Book/Chapter 5/Diffuse Vertex Level"
{
Properties
{
//声明color类型的属性
_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;
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 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);
}
ENDCG
}
}
}
其中Diffuse为可调整的漫反射颜色。除了本身的漫反射计算以外,还需要考虑环境光的交互,所以需要加上环境光的颜色。
效果如下:
可以看到逐顶点光照的缺点就是有很明显的锯齿状,对于细分程度较低的模型会遇到。因此可以采用逐像素光照,即把color赋值的操作转移到frag函数中实现,在顶点着色器部分只处理和顶点有关的数据转换即可:
Shader "Unity Sgaders Book/Chapter 5/Diffuse Pixel Level"
{
Properties
{
//声明color类型的属性
_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:TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);//模型空间坐标转换为齐次裁剪空间
o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));//转换法线从模型空间转为世界空间
return o;
}
fixed4 frag(v2f i) :SV_Target
{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//环境光照信息
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);//标准化世界光照向量信息
//光照公式,等于自发光+漫反射+环境光
//这一步求漫反射
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(i.worldNormal, worldLight));
//这一步求漫反射和环境光交互
fixed3 color = ambient + diffuse;
return fixed4(color,1);
}
ENDCG
}
}
}
效果如下:
可以看到光滑了很多。
Half Lambert
以上介绍的是兰伯特光照模型,使用max(0,dot(n,I))来保证点积为非负数。我们也同样可以做α倍的缩放和β的偏移,来让dot(n,I)从[-1,1]的范围映射到[0,1]的范围,如下公式:
绝大部分情况,α=β=0.5。
因此稍微修改之前的逐像素光照模型即可完成任务:
Shader "Unity Sgaders Book/Chapter 5/Half Lambert"
{
Properties
{
//声明color类型的属性
_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:TEXCOORD0;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);//模型空间坐标转换为齐次裁剪空间
o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));//转换法线从模型空间转为世界空间
return o;
}
fixed4 frag(v2f i) :SV_Target
{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//环境光照信息
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);//标准化世界光照向量信息
//光照公式,等于自发光+漫反射+环境光
//这一步求漫反射
fixed halfLambert = saturate(dot(i.worldNormal, worldLight)) * 0.5 + 0.5;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
//这一步求漫反射和环境光交互
fixed3 color = ambient + diffuse;
return fixed4(color,1);
}
ENDCG
}
}
}
效果如下:
显而易见地观察到,它会比之前的光照模型要亮,是因为几乎没有diffuse=0的情况,几乎所有点都是亮的。
Specular Vertex Level/Specular Pixel Level
除了漫反射,还需要考虑高光反射,高光反射的计算公式如下:
即入射光的颜色和强度作用于带有高光属性的材质上,其与“视角方向和反射方向的点积”成正比关系。
因此延续漫反射光照模型继续写下去,只是添加了高光数据,如下逐顶点光照:
Shader "Unity Sgaders Book/Chapter 5/Specular Vertex Level"
{
Properties
{
_Diffuse("Diffuse", Color) = (1,1,1,1) //声明color类型的属性
_Specular("Specular",Color) = (1,1,1,1) //高光反射颜色
_Gloss("Gloss",Range(8,256)) = 20 //高光区域大小
}
SubShader
{
Pass
{
......
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
......
v2f vert(a2v v)
{
......
//光照反射
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;
}
......
}
}
}
同样有锯齿问题,于是引入逐像素光照,和diffuse pixel level类似,只需要在顶点着色器内部处理顶点相关信息,在片元着色器内部处理颜色信息即可:
Shader "Unity Sgaders Book/Chapter 5/Specular Pixel Level"
{
Properties
{
_Diffuse("Diffuse", Color) = (1,1,1,1) //声明color类型的属性
_Specular("Specular",Color) = (1,1,1,1) //高光反射颜色
_Gloss("Gloss",Range(8,256)) = 20 //高光区域大小
}
SubShader
{
Pass
{
......
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
......
fixed4 frag(v2f i) :SV_Target
{
......
//光照反射
fixed3 reflectDir = normalize(reflect(-worldLightDir, i.worldNormal));//计算入射光关于法线的反射方向
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);//视角方向
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
//这一步求漫反射和环境光和高光
fixed3 color = ambient + diffuse + specular;
return fixed4(color,1);
}
ENDCG
}
}
}
效果如下所示:
BlinnPhong
BlinnPhong光照模型和Phong模型的区别则是:Phong的高光计算是视角与“入射光相对法线反射之后的光”作点积运算,而BlinnPhong的高光计算则是法线与“视角和入射光相加后归一化”的向量作点积运算。
因此只需要修改部分Specular Pixel Level的片元着色器代码即可完成任务:
fixed4 frag(v2f i) :SV_Target
{
......
//光照反射
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));//视角方向
fixed3 halfDir = normalize(viewDir+worldLightDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, i.worldNormal)), _Gloss);
//这一步求漫反射和环境光和高光
fixed3 color = ambient + diffuse + specular;
return fixed4(color,1);
}
相比于Phong模型的高光反射部分看起来会更大更亮一些: