写在前面
看了许多Shader教程,也学到了一些常见的Shader效果,但还是有一些迷糊,不能灵活的运用。所以打算一步步从零开始,把Shader彻底搞明白。以此记录一下学习过程。
Shader效果
- 单个对象
- 正确渲染出对象
- 将对象设置成黄色(为啥是黄色,随便选的颜色:-))
思路
- 这里选择Quad作为渲染对象,顶点组成比较简单
- 要正确显示出Quad,需要将顶点位置告诉GPU
- 要正确显示黄色,则要告诉GPU一个黄色的色值
以下是具体细节实现的过程,我们一步步来:
先建一个新的项目
目录结构如下:
先从最简单的开始,后期可能会整理,Materials主要存放材质,Prefabs存放实验用的prefab,Textures主要是贴图资源,Loading是测试用的场景,Shaders里放实验用的Shader
新建测试对象
- 选择GameObject->3D Object->Quad,新建一个Quad对象
- 新建一个Material,替换掉默认的Material
- 新建一个Shader,替换掉默认的Shader
- 将Shader中自动生成的内容清空,然后一步步的添加需要的内容,清空后的Shader内容如下:
Shader "Unlit/first"{
}
这时的场景显示如下:
为什么会显示一个粉色呢?
选中Shader后,查看Inspector面板发现,原来有一个错误,Unity发现Shader是一个空的,所以不能编译,就给Quad对象一个默认的粉色来提醒(实际上是用了一个默认的Shader)
选中shader后在Inspector面板查看如下:
会出现一个Errors,提示没有subshaders或默认的fallbacks支持GPU,也没有生成编译后的Shader代码。
试试加上属性会是什么结果:
Shader "Unlit/first"{
Properties{
_MainTex("Texture 2D",2D) = "white"{}
}
}
结果还是会出现如上警告
如果加上SubShader不加属性呢:
Shader "Unlit/first"{
SubShader{
}
}
这时会出现一个“==Shader error in ‘first’: Parse error: syntax error, unexpected ‘}’ at line 4==”error,说明只有一个空的SubShader是不行的。
加上Pass试试:
Shader "Unlit/first"{
SubShader{
Pass{
}
}
}
此时编译通过了,并且Quad对象也不是显示粉色了。而是显示了一个默认的白色,Shader的Inspector也显示了Show generated code和Compile and show code两项:
说明Unity中的Shader最少需要一个SubShader加一个Pass
**如果Pass中没有CGPROGRAM … ENDCG段,则Unity会自动生成一段默认的Shader,点击”Show generated code”,可以查看其中的内容
如果有CGPROGRAM … ENDCG段,Unity就不自动生成Shader代码了,需要自己指定vertex和fragment函数,如果没有指定会生成一个编译错误。”Compile and show code”是Unity中的Shader编译成glsl等图形api的shader,可以查看其中生成的最底层的内容,以方便查错和探究其中的细节。这里先不展开说这个。**
经过多次实验,Shader中应该有如下内容才能开始实现自己的Vertex Shader和Fragment Shader:
Shader "Unlit/first"{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct appdata{
};
struct v2f{
};
v2f vert(appdata IN){
}
fixed4 frag(v2f IN):SV_TARGET{
return fixed(1,1,0,1);
}
ENDCG
}
}
}
- #pragma vertex vert和#pragma fragment frag指定vertex和fragment要调用哪个函数,这里是vert和frag函数
- struct appdata是vert函数的输入
- struct v2f是vert函数的输出也是frag函数的输入
- SV_TARGET是绑定语义,绑定颜色缓存
上面这段代码中,虽然vert是空的,实际测试是可以编译通过,不报错的,而frag中必须要return 一个颜色值,并且SV_TARGET也是要指定的,否则都会报错
从上面代码的逻辑来看是给了一个黄颜色的值,是不是我们的Shader效果算完成了呢,到场景里看一下
结果,这段shader作用到Quad对象后,Quad对象消失了,场景里空空如也,为什么呢?
原因很简单,没有指定位置嘛,你不告诉位置,GPU怎么知道把对象放哪里呢,虽然不给位置代码能编译通过,但至少要给一个位置信息,才能把对象正确的显示出来。修改后的Shader代码如下:
Shader "Unlit/first"{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct appdata{
float4 pos:POSITION;
};
struct v2f{
float4 pos:SV_POSITION;
};
v2f vert(appdata IN){
v2f o;
o.pos = IN.pos;
return o;
}
fixed4 frag(v2f IN):SV_TARGET{
return fixed4(1,1,0,1);
}
ENDCG
}
}
}
这次会不会显示正确了呢?
显示效果如下:
这次显示出来了,但总感觉哪里不对劲,不是应该是正方形的吗?怎么变长方形了,不是应该和红线重合吗?怎么对不上了。而且移动相机,改变Quad对象的transform的值,这个黄色块块位置也不会动。
这个显示结果显然不是我们想要的,预期的结果应该是一个正方形,而且能随着相机和物体的坐标改变位置。
是哪里出问题了呢?
- 这就要熟悉渲染流水线了,由于在vertex shader中没有对坐标做任何处理,之后的光栅化阶段中做视口变换时,会认为拿到的坐标已经是标准化设备的坐标了(标准化设备坐标就是一个x,y,z值在-1.0到1.0的一小段空间。落在范围外的坐标都会被丢弃/裁剪),所以直接对顶点值做了透视除法,视口变换,和剔除,因为Quad对象的顶点是由{-0.5,-0.5},{0.5,0.5},{0.5,-0.5},{-0.5,0.5}四个顶点组成,是固定不变的,所以如果不在vertex shader中做处理,改变guad的位置,移动相机,等变换操作都不会改变显示结果,只有相机的Viewport Rect的值会影响视口的变化,视口变换时也是读取的这个值
- Unity中对象的mesh顶点信息,实际上是相对于gameobject的局部坐标系来组织的,所以顶点信息必须要转成世界坐标才能显示正确。
- 如果跟随相机变化,那就需要转成相机空间。
结论:是因为顶点没有在vertex Shader中做任何处理,流入到光栅阶段后,被当成了标准化设备坐标进行了透视除法,视口变换和剔除操作,所以导致了错误的显示结果
通过上面的分析,得到,要想在Unity中显示正确的结果,就需要将顶点坐标转成世界坐标,世界坐标转成相机空间坐标,将相机空间坐标通过投影变换裁剪,变成标准化设备坐标(NDC),这个过程就是常说的流水线中的模型变换,相机变换,和投影裁剪变换,这些是通过矩阵表示来计算的,叫做MVP矩阵,这些矩阵不在这里展开说,可以查其他相关资料,计算后的结果再流入下一阶段就可以了。
Unity将一些常用操作,封装到了”UnityCG.cginc”中,上述变换操作可以用UnityObjectToClipPos()实现,里面其实就是MVP矩阵,修改后的vertex shader如下:
Shader "Unlit/first"{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata{
float4 pos:POSITION;
};
struct v2f{
float4 pos:SV_POSITION;
};
v2f vert(appdata IN){
v2f o;
o.pos = UnityObjectToClipPos(IN.pos);
return o;
}
fixed4 frag(v2f IN):SV_TARGET{
return fixed4(1,1,0,1);
}
ENDCG
}
}
}
效果如下:
这次得到了想要的效果,也可以随相机和物体的变化而变化 了。
总结
今天实现了一个最基本的Shader,显示位置并设置颜色。有了这个基础,接下来,一步步向更高级Shader,如需配套代码,可以从作者github获取