Unity的着色器有以下三种:
Fixed Function Shader 固定片段着色器
Surface Shader 表面着色器
Vertex&Fragment Shader 顶点与片段着色器
顶点与片段着色器可以把物体整个渲染管线流程通过代码的方式表现出来,可以说是最接近显卡语言的着色器写法。这里提到渲染管线,其实他是3D图形学的一个非常重要的概念,即一个3D物体如何绘制到屏幕上的显卡渲染流水线的过程。
不要以为这些都是理所当然的事情,没有任何功能是计算机理所当然会的,包括一个3D物体如何显示在屏幕上,是要经过一连串复杂的计算得到的画面效果,而我们学Shader的目的,其实就算去创作,去改变这种效果。
Shader其实就算改变物体在屏幕上的形状和颜色的技术
下面我们来看看Shader的真面目
Shader "Unlit/NewUnlitShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 vert (appdata_base v):POSITION
{
return mul(UNITY_MATRIX_MVP,v.vertex);
}
fixed4 frag () : COLOR
{
return fixed4(1,0,0,1);
}
ENDCG
}
}
FallBack"Diffuse"
}
我们来一行一行解读一下每句的意思。首先这里可以看到他有四个关键字:
- Shader
- Properties
- SubShader
- Pass
我们来分开解读一下:
首先是Shader,后面双引号的内容是他在材质球中的位置,首先是根目录,然后是子目录。Unlit/NewUnlitShader就算去材质球找Unlit然后选NewUnlitShader就能选到本shader。
接下来轮到Properties,他记录的是Shader中的变量,这些变量可以表现在Unity中,也可以在Shader里面参与计算,也就是Unity和Shader进行交互的工具。具体变量会在下一篇文章中提到,这里给个截图,大家去体会体会。
然后是SubShader,他是Shader的子着色器,一个Shader可以有多个子着色器,我们会把画面效果写在子着色器里面,每一个子着色器写一种效果,但是他们不能同时执行,一次只有一种效果能实现。
根据上图我们可以看到,子着色器可以有很多个,但是我们具体用哪个呢?其实他们在这里是逻辑或的关系,也就是前面如果有能执行的,后面就不会执行。也就是多种画面效果,只要前面有一种能执行的了,后面的都不会执行。特别适用于选择显卡,比如说我有种画面效果,一个显卡显示不了,没关系,我后面还有很多备用的,一个不行用第二个,再不行用第三个。如果都不行,看到有个Fallback吗?他叫回滚,也就是说所有都不行的情况下调用回滚。通常我们喜欢把画面效果好的写在最上面,然后依次把要求高的SubShader写下来,最后写个回滚,如果全部执行不了就执行回滚,也是最后的保底,通常写最简单通用的着色器。
那SuaShader里面主要有什么呢?
他主要有标签和一些子着色器的关键字还有Pass通道。这里案例里面标签有渲染类型,Opaque表示不透明,就是说在这里把他和不透明物体归到同一类。LOD表示细节层次,这里表示着色器的LOD是100,那么系统的LOD低于这个值,他将不渲染。
接着我们讲到最后一个关键字,Pass通道。可以从上图看到,他和Subshader一样可以有很多个,但是他们却不是SubShader那样只有一个生效,他们会共同影响最后的画面效果。通常Pass通道写得越多,显示出来的画面效果越多。而我们的CG语法主要就是在Pass通道展开。
CG语法简介:
CG(Nvida公司开发的c for graphics计算机图形编程C语言超集):CG程序可以根据运行时的需要或者事先编译成GPU汇编代码;
HLSL(基于DirectX的High Level Shading Language):只能供微软的Direct3D以及XNA使用,移植性差;
GLSL(基于OpenGL的OpenGL Shading Language):供OpenGL使用,具有良好的跨平台和移植特性。
CG和HLSL的语法类似。GLSL语法体系自成一家。
CG是一个可以被OpenGL和Direct3D广泛支持的图形处理器编程语言。CG语言和OpenGL、DirectX并不是同一层次的语言,而是OpenGL和DirectX的上层,即CG程序是运行在OpenGL和 DirectX标准顶点和像素着色的基础上的。
简单说,CG就是C For Graphic,是Nvidia的一套显卡语言,他可以像雇佣军一样嵌套在DX和OpenGL里面使用,是移植性非常好的显卡语言。我们的ShaderLab是啥语言?他是隶属于DX的,也就是说他其实是在DX基础上的一个框架,那么他内部也是有CG这个雇佣军的。怎么看出来的?从Pass通道里面的CGPROGRAM开始,我们就看到CG语法的影子了。
注意从这里开始变成了CG语法,其实就是C语言,他嵌套在ShaderLab里面,那么上面的其实是ShaderLab的框架。细心的同学会发现,上面从来没有出现过分号,下面开始出现分号了,这也是他俩区别的标识。
那我们来看CG语法:
#pragma vertex vert
#pragma fragment frag
这两句主要是用你自己写的顶点函数和片段函数去替换系统自带的顶点函数和片段函数。
- 顶点函数负责处理物体形状(VS)
- 片段函数负责处理物体在屏幕的颜色(PS)
这里vert和frag是你自己定义的名字,可以改变,但是改变的话,下面必须声明两个同名的函数,否则编译器算你耍赖,要替换的函数找不到,就会报错。
float4 vert (appdata_base v):POSITION
{
return mul(UNITY_MATRIX_MVP,v.vertex);
}
这个就是你要实现的顶点函数,首先我们返回的顶点要是float4,4唯的向量,函数名必须和上面的一样,appdata_base 是CPU传过来的数据块,也就是一次Draw Call传过来的数据,里面有物体的法线,物体的本地顶点,物体的贴图信息等等,最后的是语义,这里表示返回值是用来显示形状的,不是用来显示颜色的。为啥要有语义呢?因为返回的float4可以做任何事情,可以用来处理形状,也可以表示颜色,你给他语义,他就知道他要用来做啥了。那么我们这个函数什么时候调用呢?不需要关心,我们上面写好替换了,程序会自动调用。
如何理解mul(UNITY_MATRIX_MVP,v.vertex)这个代码呢?这个后面渲染管线我们会讲,这里其实是把物体的顶点坐标通过3D矩阵计算换算成屏幕投影的坐标,显示在屏幕上。
接下来是:
fixed4 frag () : COLOR
{
return fixed4(1,0,0,1);
}
这个是我们实现的片段函数,返回的是fixed4,和上面float有点不同,fixed也是浮点,不过取值范围在-2-2,为啥要用fixed呢?首先我们知道这是片段函数,是用来处理颜色的。颜色有RGBA四个值,刚好对应fixed4?那为啥用fixed不用float呢?因为显卡的显存很珍贵,他发现颜色都是从0-1(其实把0~255做了个映射)的,那么就不需要用字节比较大的float.后面是COLOR语义,表示返回值是用来显示颜色的,不是处理形状的。
如何理解return fixed4(1,0,0,1);这个代码呢?其实就算返回一个颜色值,射回画笔工具里面的值就是,255,0,0,255,也就是红色,最后一个255表示透明度里面的不透明,与之相反的0就代表全透明。
最后最后有个ENDCG说明CG语言到底了,执行完了,回到ShaderLab的怀抱。
最后附一份实现水面效果的shader以供参考:
Shader "Game_XXX/whater"
{
Properties
{
_WaterTex("Normal Map (RGB), Foam (A)", 2D) = "white" {}
_WaterTex2("Normal Map (RGB), Foam (B)", 2D) = "white" {}
_Tiling("Wave Scale", Range(0.00025, 5)) = 0.25
_WaveSpeed("Wave Speed", Float) = 4.0
_SpecularRatio("Specular Ratio", Range(10,500)) = 200
_BottomColor("Bottom Color",Color) = (0,0,0,0)
_TopColor("Top Color",Color) = (0,0,0,0)
_Alpha("Alpha",Range(0,1)) = 1
_ReflectionTex("_ReflectionTex", 2D) = "black" {}
_ReflectionLight("ReflectionLight",Range(0,1)) = 0.3
_LightColorSelf("LightColorSelf",Color) = (1,1,1,1)
_LightDir("LightDir",vector) = (0,1,0,0)
}
SubShader
{
Tags
{
"Queue" = "Transparent-200"
"RenderType" = "Transparent"
"IgnoreProjector" = "True"
"LightMode" = "ForwardBase"
}
LOD 250
Pass
{
Lighting On
ZWrite On
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "UnityCG.cginc"
float _Tiling;
float _WaveSpeed;
float _SpecularRatio;
sampler2D _WaterTex;
sampler2D _WaterTex2;
sampler2D _ReflectionTex;
float4 _LightColorSelf;
float4 _LightDir;
float4 _BottomColor;
float4 _TopColor;
float _Alpha;
float _ReflectionLight;
struct v2f
{
float4 position : POSITION;
float3 worldPos : TEXCOORD0;
float3 tilingAndOffset:TEXCOORD2;
float4 screen:TEXCOORD3;
float4 VertColor :TEXCOORD4;
};
v2f Vert(appdata_full v)
{
v2f o;
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.position = UnityObjectToClipPos(v.vertex);
//uv动画
o.tilingAndOffset.z = frac(_Time.x * _WaveSpeed);
o.tilingAndOffset.xy = o.worldPos.xz*_Tiling;
o.screen = ComputeScreenPos(o.position);
o.VertColor = v.color;
return o;
}
float4 Frag(v2f i) :COLOR
{
float3 lightColor = _LightColorSelf.rgb * 2;
//世界视向量
float3 worldView = -normalize(i.worldPos - _WorldSpaceCameraPos);
float2 tiling = i.tilingAndOffset.xy;
//法线采样
float4 N1 = tex2D(_WaterTex, tiling.yx + float2(i.tilingAndOffset.z,0));
float4 N2 = tex2D(_WaterTex2, tiling.yx - float2(i.tilingAndOffset.z,0));
//两个法线相加,转世界空间,这里没有unpack,所以法线贴图不需要转normal
//法线贴图为0-1 两张加起来为0-2 将其x2-2,转换为-2 --2然后将其normalize,变成-1到1
//在遇到两张法线的情况下 ,一般将法线相加 再normalize
float3 worldNormal = normalize((N1.xyz + N2.xyz) * 2 - 2);
//以垂直的方向代替灯光 跟法线做点积 得到漫反射强度
float LdotN = dot(worldNormal, float3(0,1,0));
fixed2 uv = i.screen.xy / (i.screen.w + 0.0001);
uv.y = 1 - uv.y;
fixed4 refTex = tex2D(_ReflectionTex,uv + worldNormal.xy*0.02);
//这个变量一般在Forward渲染路径下使用,存储的是重要的pixel光源方向,
//没错,的确是使用w来判断这个光源的类型的,一般和_LightColor0配合使用
//float3 LView=_WorldSpaceLightPos0.xyz;
float3 LView = _LightDir.xyz;
//if(_WorldSpaceLightPos0.w == 0.0){
// L = normalize(_WorldSpaceLightPos0.xyz);
// }
// else{
// L = normalize(_WorldSpaceLightPos0.xyz - i.worldPos);
// }
//根据世界法线 ,世界视向量+光向量 得出高光 系数
float dotSpecular = dot(worldNormal, normalize(worldView + LView));
//控制高光的范围
float3 specularReflection = pow(saturate(dotSpecular), _SpecularRatio);
float4 col;
float fresnel = 0.5*LdotN + 0.5;
//根据法线的强度 来确定两种颜色之间的混合
col.rgb = lerp(_BottomColor.xyz, _TopColor.xyz, fresnel);
col.rgb = saturate(LdotN) *col.rgb;
//加上高光
col.rgb += specularReflection;
col.rgb = lerp(col.rgb,refTex.rgb*_ReflectionLight,0.7);
//col.rgb +=refTex.rgb*_ReflectionLight;
//加上灯光颜色
col.rgb *= lightColor;
col.rgb *= i.VertColor.rgb;
//控制透明度
col.a = i.VertColor.a * _Alpha;
return col;
}//float4 Frag(v2f i)
ENDCG
}//Pass
}//SubShader
FallBack "Diffuse"
注:该篇文章为转载,略有所改动
原篇作者:时过敬迁
链接:https://www.jianshu.com/p/d6f3e8d8bd15