最近断断续续学习了一些Unity Shader的内容,总结一下,主要学习资料是siki学院的课程。
Unity Shader基本结构
Shader "Unlit/002" //Shader路径
{
Properties //Shader属性 可在面板修改
{
_Int("Int",Int) = 2
_Float("Float",float) = 1.5
_Range("Range",range(0.0,2.0))= 1.0
_Color("Color",Color) = (1,1,1,1)
_Vector("Vector",Vector) = (1,4,3,8)
_MainTex("Texture", 2D) = "white" {}
_Cube("Cube",Cube) = "white"{}
_3D("3D",3D) = "black"{}
}
SubShader //子着色器,可以有多个,如果第一个SubShader无法运行就执行第二个,以此类推
{
Tags { "RenderType"="Opaque" } //标签,可选
LOD 100 //LOD
Pass //Pass通道,可以有多个
{
CGPROGRAM //CG语言开始
#pragma vertex vert //定义顶点着色器的函数名为vert
#pragma fragment frag //定义片元着色器的函数名为frag
#include "UnityCG.cginc" //包含Unity提供的头文件
//POSITION表示将模型顶点坐标传入v,SV_POSITION表示顶点着色器的输出是裁剪空间顶点坐标
float4 vert(float4 v:POSITION):SV_POSITION
{
return UnityObjectToClipPos(v); //将模型坐标转为裁剪空间坐标
}
//SV_TARGET表示片元着色器输出颜色到帧缓存中
fixed4 frag():SV_TARGET
{
return fixed4(1,1,1,1); //直接返回白色
}
ENDCG //CG语言结束
}
}
Fallback "VertexLit" //如果以上SubShader都无法使用,则使用VertexLit这个Shader
}
这个最简单的Shader将顶点坐标传入顶点着色器,转换成裁剪空间坐标后输出给片元着色器,片元着色器不做任何处理,直接将每个像素置为白色,效果如图:
结构体的使用
Shader "Unlit/003"
{
Properties
{
_Color("Color" , Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//只有在CGPROGRAM内再次定义一个与属性块内名字与类型相同的变量,属性块对应的变量才能起作用
fixed4 _Color;
struct a2v//传入顶点着色器的结构体
{
//用模型顶点填充v变量
float4 vertex:POSITION;
//用模型的法线填充n变量
float3 normal:NORMAL;
//用模型的第一套uv填充texcoord
float4 texcoord: TEXCOORD0;
};
struct v2f//顶点着色器输出给片元着色器的结构体
{
//SV_POSITION语义告诉unity : pos为裁剪空间中的位置信息
float4 pos:SV_POSITION;
//COLOR0 语义可以储存颜色信息
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);
}
ENDCG
}
}
}
Shader中使用结构体和C语言类似,然后我们就可以使用结构体来进行顶点着色器和片元着色器的数据传输,上面这个Shader中,在顶点着色器里将法线向量映射到[0,1]区间,然后保存在v2f的color变量里传给片元着色器,片元着色器将color与Shader属性里的_Color相叠加输出。
可以看到右边颜色偏蓝,左边偏红,上边偏绿,这是将法线向量作为颜色输出的结果,x分量较大就会偏红,其他两个分量同理。
漫反射Shader
Shader "Unlit/005"
{
Properties
{
_Diffuse("Diffuse", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc" //引入灯光相关头文件
fixed4 _Diffuse;
struct v2f
{
float4 vertex : SV_POSITION;
fixed3 color: Color;
};
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; //获得环境光颜色
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); //将法线转到世界空间
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz); //获得第一个平行光的方向
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal,worldLight)); //lambert光照模型计算
o.color = diffuse + ambient;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return fixed4(i.color,1);
}
ENDCG
}
}
FallBack "Diffuse"
}
appdata_base是Unity在UnityCG.cginc中提供的结构体,包含顶点、法线和UV信息。
在顶点着色器中使用Lambert光照模型(关于光照模型的原理这篇先不提)计算漫反射光照,_LightColor0表示场景第一个灯光的颜色,灯光颜色乘以物体固有色乘以世界空间法线和世界空间灯光位置的点积再加上环境光即得到最终的顶点颜色。
也可以在片元着色器中计算光照,如何取舍取决于追求性能还是质量,一般来说顶点数量是小于像素数量的,所以如果追求更好的性能就在顶点着色器中计算,如果想要更好的质量就在片元着色器中计算。
用片元着色器计算漫反射:
Shader "Unlit/006"
{
Properties
{
_Diffuse("Diffuse", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Diffuse;
struct v2f
{
float4 vertex : SV_POSITION;
fixed3 worldNormal: TEXCOORD0;
};
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
fixed3 worldNormal = UnityObjectToWorldNormal( v.normal);
o.worldNormal = worldNormal;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0,dot(worldLightDir,i.worldNormal));
fixed3 color = ambient + diffuse;
return fixed4(color,1);
}
ENDCG
}
}
FallBack "Diffuse"
}
高光反射Shader
Shader "Unlit/008"
{
Properties
{
_Diffuse("Diffuse", Color) = (1,1,1,1)
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss", Range(1,256)) = 5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct v2f
{
float4 vertex : SV_POSITION;
fixed3 color: Color;
};
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
fixed3 worldPos = mul(unity_ObjectToWorld, v.vertex);
//漫反射
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldLight = UnityWorldSpaceLightDir(worldPos);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal,worldLight));
//高光反射
fixed3 reflectDir = normalize(reflect(-worldLight,worldNormal));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(reflectDir,viewDir)),_Gloss);//Phong光照模型计算
o.color = diffuse + ambient + specular;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return fixed4(i.color,1);
}
ENDCG
}
}
FallBack "Diffuse"
}
高光反射计算使用Phong光照模型,计算反射向量reflectDir时需要注意将WorldLight取反才是正确的入射光方向,观察向量viewDir使用UnityWorldSpaceViewDir得到即可。
Blinn-Phong高光反射
Shader "Unlit/010"
{
Properties
{
_Diffuse("Diffuse", Color) = (1,1,1,1)
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss", Range(1,256)) = 5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct v2f
{
float4 vertex : SV_POSITION;
fixed3 worldNormal: TEXCOORD0;
float3 worldPos: TEXCOORD1;
};
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
fixed3 worldNormal = UnityObjectToWorldNormal( v.normal);
o.worldNormal = worldNormal;
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//漫反射
fixed3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0,dot(worldLightDir,i.worldNormal));
//高光反射
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(i.worldNormal,halfDir)),_Gloss);
fixed3 color = ambient + diffuse + specular;
return fixed4(color,1);
}
ENDCG
}
}
}
另一种高光反射计算方法,使用灯光向量和观察向量的和halfDir与世界法线进行计算,saturate用于将值限定在0-1之间,这种计算得出的结果高光会更亮一些。
纹理采样
Shader "Unlit/011"
{
Properties
{
_MainTex("MainTex", 2D) = "white" {}
_Diffuse("Diffuse", Color) = (1,1,1,1)
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss", Range(1,256)) = 5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
//用一个纹理加上_ST表示该纹理的缩放和偏移
//_MainTex_ST.xy存储的是缩放值,而_MainTex_ST.zw存储的是偏移值
float4 _MainTex_ST;
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct v2f
{
float4 vertex : SV_POSITION;
fixed3 worldNormal: TEXCOORD0;
float3 worldPos: TEXCOORD1;
float2 uv : TEXCOORD2;
};
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
fixed3 worldNormal = UnityObjectToWorldNormal( v.normal);
o.worldNormal = worldNormal;
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
//用Unity内置的TRANSFORM_TEX得到纹理坐标
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);//v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//纹理颜色,用tex2D进行采样
fixed3 albedo = tex2D(_MainTex, i.uv).rgb;
//漫反射
fixed3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);
fixed3 diffuse = _LightColor0.rgb * albedo * _Diffuse.rgb * (dot(worldLightDir,i.worldNormal)*0.5+0.5);
//高光反射
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(i.worldNormal,halfDir)),_Gloss);
fixed3 color = ambient + diffuse + specular;
return fixed4(color,1);
}
ENDCG
}
}
}
纹理采样Unity提供了相关的方法,TRANSFORM_TEX和tex2D用于计算纹理坐标和纹理颜色,注意必须要声明_MainTex_ST。
法线纹理映射
1.切线空间法线纹理映射
Shader "Unlit/012"
{
Properties
{
_MainTex("MainTex", 2D) = "white" {}
_BumpMap("Normal Map", 2D) = "bump" {}
_BumpScale("Bump Scale", float) = 1
_Diffuse("Diffuse", Color) = (1,1,1,1)
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss", Range(1,256)) = 5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct v2f
{
float4 vertex : SV_POSITION;
fixed3 lightDir: TEXCOORD0;
float3 viewDir: TEXCOORD1;
float2 uv : TEXCOORD2;
float2 normalUv : TEXCOORD3;
};
v2f vert (appdata_tan v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.normalUv = TRANSFORM_TEX(v.texcoord, _BumpMap);
//求副切线向量
//float3 binormal = cross(v.normal,v.tangent.xyz) * v.tangent.w;
//float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
TANGENT_SPACE_ROTATION;
//求切线空间光源方向及视角方向
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex)).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentviewDir = normalize(i.viewDir);
fixed4 packedNormal = tex2D(_BumpMap,i.normalUv);
//需要将法线贴图设置成normal map
fixed3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
//环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//纹理颜色
fixed3 albedo = tex2D(_MainTex, i.uv).rgb;
//漫反射
fixed3 diffuse = _LightColor0.rgb * albedo * _Diffuse.rgb * (dot(tangentLightDir,tangentNormal)*0.5+0.5);
//高光反射
fixed3 halfDir = normalize(tangentLightDir + tangentviewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(tangentNormal,halfDir)),_Gloss);
fixed3 color = ambient + diffuse + specular;
return fixed4(color,1);
}
ENDCG
}
}
}
2.世界空间法线纹理映射
Shader "Unlit/013"
{
Properties
{
_MainTex("MainTex", 2D) = "white" {}
_BumpMap("Normal Map", 2D) = "bump" {}
_BumpScale("Bump Scale", float) = 1
_Diffuse("Diffuse", Color) = (1,1,1,1)
_Specular("Specular", Color) = (1,1,1,1)
_Gloss("Gloss", Range(1,256)) = 5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct v2f
{
float4 vertex : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtiW0 : TEXCOORD1;
float4 TtiW1 : TEXCOORD2;
float4 TtiW2 : TEXCOORD3;
};
v2f vert (appdata_tan v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
//计算世界坐标下的顶点位置,法线,切线,副法线
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal,worldTangent) * v.tangent.w;
//按列摆放得到从切线空间到世界空间的变换矩阵
o.TtiW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtiW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtiW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//求世界坐标
float3 worldPos = float3(i.TtiW0.w,i.TtiW1.w,i.TtiW2.w);
//计算世界空间下的光照和视角
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
//获得法线纹理
fixed4 packedNormal = tex2D(_BumpMap,i.uv.zw);
fixed3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
//切线空间法线转换到世界空间
fixed3 worldNormal = normalize(float3(dot(i.TtiW0.xyz, tangentNormal),dot(i.TtiW1.xyz, tangentNormal),dot(i.TtiW2.xyz, tangentNormal)));
//环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//纹理颜色
fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb;
//漫反射
fixed3 diffuse = _LightColor0.rgb * albedo * _Diffuse.rgb * (dot(lightDir,worldNormal)*0.5+0.5);
//高光反射
fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal,halfDir)),_Gloss);
fixed3 color = ambient + diffuse + specular;
return fixed4(color,1);
}
ENDCG
}
}
}
法线贴图采样之后还需要调用UnpackNormal函数获得正确的切线空间法线,光照计算时比较常见的是在切线空间或者世界空间进行计算,如果在切线空间计算就将光照和视角向量转到切线空间计算就好了,如果在世界空间计算则需要手动计算从切线空间到世界空间的转换矩阵传入片元着色器计算。