最近在研究Unity 的Shader编写,冯乐乐《UnityShader 入门精要》发现还是挺有意思的。
这里就来实现一下基础的Shader。笔者使用的Unity版本是2019.4.19f1。相比于《UnityShader 入门精要》中的某些写法和函数进行了更新。
在游戏引擎中光照模型有很多种,但在早期的游戏引擎中往往只使用一个光照模型,这个模型被称为标准光照模型。
它的基本方法是,把进入到摄像机内的光线分为4个部分,每个部分使用一种方法来计算它的贡献度。这4个部分如下
在标准光照模型中,我们使用了一种被称为环境光的部分来近似模拟间接光照(可以理解为整体环境的光照)。环境光的计算非常简单,它通常是一个全局变量,即场景中的所有物体都使用这个环境光。
在Unity 中 环境光使用如下代码求得:
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
漫反射的介绍见 漫反射_百度百科。
在漫反射中,视角的位置是不重要的,因为反射是完全随机的,因此可以认为在任何反射方向上的分布都是一样的。但是,入射光线的角度很重要。
漫反射公式如下:
C diffuse 为 漫反射颜色
C light 为 光源颜色
M diffuse 为 材质漫反射系数
向量 n 为法线方向
向量 l 为光源方向
为了防止法线和光源方向点积结果为负值,所以使用取最大值的函数来将其截取到0。
这里的公式没看懂也没关系。熟能生巧。随着研究的深入,就会慢慢理解了。
上代码
Shader "Custom/DiffuseShader"
{
Properties
{
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
}
SubShader
{
Pass
{
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Diffuse;
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 worldNormal : TEXCOORD0;
};
v2f vert(appdata v)
{
v2f o;
//从模型空间变换到裁剪空间
o.vertex = UnityObjectToClipPos(v.vertex);
//从模型空间变换到世界空间
o.worldNormal = UnityObjectToWorldNormal(v.normal);
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
}
}
// 上述SubShader都失败后用于回调的Unity Shader
FallBack "Diffuse"
}
新建一个 Image Effect Shader,打开将此代码覆盖。新建一个材质 选择 Custom/DiffuseShader ,将材质拖到物体上即可。
代码解析:
Shader “Custom/DiffuseShader” 指定了Shader的路径。
在Properties 中规定了可以在Inspector中编辑的属性。这里我们指定了材质的默认漫反射系数(公式中的M diffuse)。
在SubShader的Pass中,我们指定了Pass中LightMode为ForwardBase。该Pass会计算环境光、主要的平行光、顶点/SH光源和Lightmaps光照贴图。
同时在Pass中规定了顶点和片段着色器的输入。
在顶点着色器中,获取了模型空间下的顶点坐标和顶点法线。之后将顶点坐标变换到裁剪空间中。将法线变换到世界坐标下。法线即为公式中的向量 n 。
在片段着色器中 ambient 为 环境光
worldLight 为公式中的向量 l
_LightColor0.rgb 为公式中的 C light
最终得到了该片段颜色的结果。
最终指定了 Fallback),如果所有SubShader都无法在该硬件上运行,会使用默认的Diffuse。
高光反射用于计算那些沿着完全镜面反射方向被反射的光线,这可以让物体看起来是有光泽的,例如金属材质。
但是这里的高光反射是一种经验模型,也就是说,它并不完全符合真实世界中的高光反射现象。
公式为
C specular为 高光反射颜色
C light 为 光源颜色
M specular为 材质高光反射系数
向量 v 为视角方向
向量 l 为反射方向
上代码
Shader "Custom/SpecularShader"
{
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 "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
v2f vert(a2v v)
{
v2f o;
//从模型空间变换到裁剪空间
o.vertex = UnityObjectToClipPos(v.vertex);
//法线从模型空间变换到世界空间
o.worldNormal = UnityObjectToWorldNormal(v.normal);
//顶点从模型空间变换到世界空间
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"
}
在Pass的顶点着色器中计算得到了世界空间下的顶点坐标,在片段着色器中新增了对于高光反射的计算,最终将环境光 漫反射 以及高光反射的结果相加,得到最终结果。
同时指定了FallBack为默认的Specular
新建一个 Image Effect Shader,打开将此代码覆盖。新建一个材质 选择 Custom/SpecularShader ,将材质拖到物体上即可。
得到的结果为。
可以看到高光反射的Shader更带一些金属性,同时在材质的Inspector面板调整Gloss的值,Gloss值越大,高光反射的光点越小。