内容会持续更新,有错误的地方欢迎指正,谢谢!
创建Shader
1.右键创建shader(如果想写顶点片元着色器就选Unlit Shader,如果想写表面着色器就选Standard Surface Shader,如果想写屏幕后处理着色器就选Image Effect Shader)
2.再创建材质Material,并将shader拖给材质
3.把材质拖给物体
简单的顶点/片元着色器
顶点/片元着色器的基本结构
Shader "Custom/Simple Shader" {
SubShader {
Pass {
//CGPROGRAM,ENDCG包围的为需要编辑的CG代码片段
CGPROGRAM
//下面两行告诉Unity,哪个函数包含了顶点着色器的代码,哪个函数包含了片元着色器的代码;
//编译指令:#pragma vertex name,name为指定的函数名,一般用vert,frag
#pragma vertex vert
#pragma fragment frag
//v包含了这个顶点的位置,这是通过POSITION语义指定。
//vert函数的返回值是一个float4类型的变量,它是该顶点在裁剪空间的位置。
//POSITION,SV_POSITION都是Cg/HLSL中的语义,用以限定输入输出参数
//如POSITION告诉Unity,把模型的顶点坐标填充到v中,SV_POSITION告诉Unity,输出是裁剪空间的顶点坐标
float4 vert(float4 v : POSITION) : SV_POSITION {
// MVP变换:从模型坐标变化到裁剪坐标
//Unity5.x版本(记不清具体版本了)以后的都改成了用UnityObjectToClipPos (v.vertex)代替
return mul(UNITY_MATRIX_MVP, v.vertex);
}
//SV_Target告诉渲染器,把用户的输出颜色存储到一个渲染目标中
fixed4 frag() : SV_Target {
return fixed4(1.0, 1.0, 1.0, 1.0);
}
ENDCG
}
}
}
POSITION和SV_POSITION是CG/HLSL的语义(用于限定输入、输出),在这里是不可以省略的,它们告诉系统需要哪种输入值,以及输出的是什么。例如POSITION说明输入的是顶点坐标,SV_POSITION说明输出的是裁剪空间的顶点坐标。如果没有语义,渲染器就不知道用户输入输出是什么。
定义模型数据:如何向顶点函数中输入更多的模型的顶点数据a2v
上面代码,我们用POSITION语义得到了模型的顶点位置。但我们时常还需要顶点的纹理坐标,法线,切线等数据,所以一个单一的输入参数肯定不行,就需要一个结构体a2v了。我们修改上面的代码如下:
Shader "Custom/Simple Shader" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//增加一个结构体,a表示应用(Application),v表示顶点着色器(vertex shader)
//a2v意思是把数据从应用阶段传递到顶点着色器,这样我们可以在顶点着色器中访问模型数据
struct a2v {
//POSITION、NORMAL、TEXCOORD0等来自Mesh Render组件
//POSITION语义:用模型空间顶点坐标填充vertex变量
float4 vertex : POSITION;
//NORMAL语义:模型空间的法线填充normal变量
float3 normal : NORMAL;
//TEXCOORD0语义:用模型的第一套纹理坐标填充texcoord变量
float4 texcoord : TEXCOORD0;
};
float4 vert(a2v v) : SV_POSITION {
//使用v.vertex来访问模型空间的顶点坐标
//并利用MVP矩阵将其从模型空间转换到裁剪空间里
return mul(UNITY_MATRIX_MVP, v.vertex);
}
fixed4 frag() : SV_Target {
return fixed4(1.0, 1.0, 1.0, 1.0);
}
ENDCG
}
}
}
顶点和片元着色器之间的通信v2f
在渲染流水线中,顶点数据经过顶点着色器的一系列变换,输出的数据要先光栅化,再由片元着色器进行上色。我们上面说的模型顶点坐标,法线,纹理坐标等数据是怎么传给片元着色器的呢,我们还需要定义一个结构体v2f,对上面的代码改动如下:
Shader "Custom/Simple Shader" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};
//使用一个结构体来定义顶点着色器的输出和片元着色器的输入
struct v2f {
//SV_POSITION语义:pos里包含了顶点在裁剪空间中的位置
float4 pos : SV_POSITION;
//COLOR0语义:可以用于存储颜色信息
fixed3 color : COLOR0;
};
v2f vert(a2v v) {
//声明输出结构v2f
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
//这是把v.normal(法线数据)转换成颜色信息存在o.color中,并传递给片元着色器
o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5); //v.normal包含法线方向,分量范围在[-1.0,1.0]
return o;
}
//v2f结构作为参数传递给片元着色器
fixed4 frag(v2f i) : SV_Target {
//将插值后的i.color显示到屏幕上
return fixed4(i.color, 1.0);
}
ENDCG
}
}
}
v2f中每个变量都需要语义,而且至少要包含SV_POSITION。否则渲染器得不到裁剪空间的坐标,也就无法显示在屏幕上。上例中我们把法线数据转化成颜色了,法线范围是[-1, 1],颜色范围是[0, 1],所以上面用先乘0.5,再加0.5的办法进行了转换。最后把转换后的颜色插值后显示在了屏幕上。
如何使用属性
例如:
添加Properties语义块,_Color属性就声明在其中,然后在CG中我们也要定义这个变量_Color,对应的类型是float4/half4/fixed4。
ShaderLab中属性的类型和CG中变量类型之间匹配关系如下:
ShaderLab中属性类型 | CG中变量类型 |
---|---|
Color, Vector | float4,half4,fixed4 |
Range, Float | float,half,fixed |
2D | sampler2D |
3D | sampler3D |
Cube | samplerCube |
挺简单哈~
Unity Shader内置文件、变量和语义
顶点/片元着色器的复杂之处在于很多事情都要我们亲力而为,例如:自己转换法线方向、自己处理光照等。但Unity提供了很多内置文件,这些文件包含了很多提前定义好了的函数、变量和宏。
使用#include指令可以把这些文件包含进来,这样,我们就可以使用Unity为我们提供的一些有用的变量和函数了~
例子:#include "UnityCG.cginc"
UnityCG.cginc是最常接触的包含文件。我们常用的结构体和函数基本都在里面。例如,我们可以直接使用其中预定义的结构体作为顶点着色器的输入和输出,如下:
另外,有一些文件是会被自动包含进来的,不需要使用#include指令,比如UnityShaderVariables.cginc和HLSLSupport.cginc。=>UnityShaderVariables.cginc里的UNITY_MATRIX_MVP,我们可以直接用。
其实,这些东西我们都可以自己实现的,但Unity封装好了,就直接用吧,不仅能提高我们的开发效率,还能提高代码的复用率。
Unity提供的Cg/HLSL语义
语义分为三类:
- 从应用阶段传递模型数据给顶点着色器时Unity支持的常用语义;
- 从顶点着色器传递数据给片元着色器时Unity使用的常用语义;
- 片元着色器输出时Unity支持的常用语义。
Shader整洁之道
- float、half还是fixed
根据实际情况(根据你游戏要跑的平台、美术效果的要求、性能限制等等因素)选择精度,最好制订一套变量类型的规则~ - 避免不必要的计算
原则1:尽量使用预计算:也就是把大量计算放在CPU中计算,再把计算结果从应用阶段传递到几何阶段。
原则2:在片元着色器中的计算越少越好:也就是能在几何阶段进行的运算就别放到光栅化阶段。 - 慎用分支和循环
能不用就别用~