C for Graphic:语言(逐句理解)
首先贴上CG shader代码,这一篇主要围绕这个代码进行学习,如下:
-
Shader "Unlit/TextureUnlitShader"
-
{
-
Properties
-
{
-
_MainTex ("Texture", 2D) = "white" {}
-
}
-
SubShader
-
{
-
Tags { "RenderType"="Opaque" }
-
LOD 100
-
-
Pass
-
{
-
CGPROGRAM
-
#pragma vertex vert
-
#pragma fragment frag
-
-
#include "UnityCG.cginc"
-
-
struct appdata
-
{
-
float4 vertex : POSITION;
-
float2 uv : TEXCOORD0;
-
};
-
-
struct v2f
-
{
-
float2 uv : TEXCOORD0;
-
float4 vertex : SV_POSITION;
-
};
-
-
sampler2D _MainTex;
-
-
v2f vert (appdata v)
-
{
-
v2f o;
-
o.vertex = UnityObjectToClipPos(v.vertex);
-
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
-
return o;
-
}
-
-
fixed4 frag (v2f i) : SV_Target
-
{
-
fixed4 col = tex2D(_MainTex, i.uv);
-
return col;
-
}
-
ENDCG
-
}
-
}
-
}
上面的CG shader只完成一个简单的图片渲染显示功能,但是细节很值得学习,接下来我就逐一讲解结合unity CG上下文来讲解shader每一句话都是什么意思。
首先说一下,nvidia是CG的缔造者,其他各个三维引擎或者工具都必须按照CG的基本规范去实现自己的CG编译器以及类似编程语法,比如我最爱的unity3d和rendermonkey(我不爱ue4的原因是ue4实在是太慢了,保存umap慢,编译构建慢等,之前用ue4的日子旁边都是放了一本书,凡是ue4在那操作阻塞着,我就看书玩,实在爱不起来)。那么我们现在用unity写CG shader,就得按照unity的那一套语法编译规范写代码了,上面的代码就是一个很标准的语法对照了,只要我们完全理解代码的每个词汇,就达到目标了。
so,我们开始咬文嚼字吧:
①.Shader{}包体。
-
Shader "Unlit/TextureUnlitShader"
-
{
-
//do something
-
}
类似c++或c#的class,将具体的shader代码封装,Shader让CG编译器识别这是一个shader代码的起始标签,“Unlit/TextureUnlitShader”就是shader名称的标识。
②.Properties{}属性字段
-
Properties
-
{
-
_MainTex ("Texture", 2D) = "white" {}
-
}
类似c++的public:修饰字段,如下:
-
class Texture
-
{
-
//这是一个贴图类
-
};
-
-
class MainTest
-
{
-
public:
-
float cg_float;
-
Texture* cg_MainTex;
-
-
};
这是一段模拟代码,和CG代码没有任何关联,我为什么用c++/c#作为演示代码呢,首先就是我们大概接触最早的就是c/c++了,因为学校里面最开始学习就是从c开始,其次我们作为unity开发,c#当然也是很熟悉啦。最后就算我用很详细的文字讲解代码意义,有的时候还是不如来一段演示代码让人更能理解其中的含义。
Properties{}就类似Public:,在其中定义我们需要的属性字段,可以供外部访问,比如我们定义一张texture2D。
_MainTex ("Texture", 2D) = "white" {},此时unity CG编译器在识别出Properties后,紧接着解析其中封装的属性字段,当解析到这段代码后,其中代码意义分解如下:
①._MainTex标识了该texture2D在shader中的字段名称。
②.("Texture", 2D)标识了该texture2D在unity 编辑面板中ui显示字符串"Texture",2D则标识类型为texture2D。
③.= "white" {}标识了该texture2D默认为白色,当然我们可以改成red红色。
ps:②中我说了unity编辑面板ui显示,这里可以通过官网下载的内置着色器中editor中的StandardShaderGUI.cs文件查看具体显示细节。
当然,Properties不仅仅只能定义一个texture2D,还能定义其他的,比如_MainFloat("Float",float) = 0.5,定义一个float值,当然还有其它的,我们以后用到的时候再看。
③.SubShader{}包体
-
SubShader
-
{
-
//do something
-
}
官方对SubShader的解释是封装一系列渲染pass和所有pass通用的标签和状态的包体,一个主Shader可以包含多个SubShader,为什么主Shader可以包含多个SubShader的原因是,前面我们说了Profile的概念,也就是说因为实际GPU硬件种类太多,为了保证该主Shader可以在多种多样的GPU上运行,所以可能需要写多个可以运行的SubShader,然后实际runtime运行时按照顺序查找到一个能运行SubShader去执行相应的渲染效果,假如都不能执行,就Fallback "Diffuse",意思就是回滚到最基本的Diffuse渲染,保证至少不出问题。
④.SubShader中标签和状态
-
Tags { "RenderType"="Opaque" }
-
LOD 100
-
Cull back
这些标签和状态键值对,被CG编译器识别,主要作用是标识后续渲染pass该什么时候并且怎么样被CG runtime渲染。
比如上面三句代码含义如下:
①.Tags { "RenderType"="Opaque" } 标识了渲染类型为Opaque,什么意思呢?就是代表这个渲染类型定义为不透明着色器,unity CG库提供了多种适用于不同环境的渲染类型,具体的后面会谈到。
②.LOD 100,LOD全称level of detail,,设置一个100是个什么意思呢?这里代表一种shader渲染细节技术,假如我们的主Shader包含多个SubShader,每个SubShader的LOD 为不同的值,假如为100、200、300,那么我们在c#代码中通过Shader.globalMaximumLOD = 100;控制全局LOD最大值,来显示不同LOD shader的渲染。有兴趣的小伙伴可以自己测试下,我就不贴图了。
③.Cull back,Cull也是渲染剔除的意思,设置back代表背面剔除渲染,那样的话实际上我们的shader只渲染了一个正面,节省一半的资源,当然我们也可以设置front剔除正面,或者off关闭,直接渲染双面。
ps:当然还有其他一些不同功能的tag和state,后面具体是用到再讲还是直接一次性列出待定,不过这里我们明白这些tag和state的语法意义就行了。
⑤.Pass{}渲染通道
从这里开始就是正式开始进入CG渲染了,Pass{}封装当前SubShader着色器具体的渲染代码,我们的顶点函数,片段函数,表面光照函数就是在其中具体实现,下面开始逐一分解每一句代码的具体意义:
①.CGPROGRAM开始和ENDCG结束,这个只是CG编译器的语法规范之一,代表开始编译或解析CG代码到结束,类似#region #endregion代码块。
②.#pragma vertex vert和#pragma fragment frag(当然还包含#pragma surface surf YangLightModel)这里就是预编译顶点函数和片段函数(表面光照函数),代表后面的vert和frag为我们要去具体开发实现的顶点和片段函数。
③.#include "UnityCG.cginc"和c包含头文件一样,这里引入了UnityCG.cginc这个文件(这个文件可以在下载的内置着色器CGIncludes文件夹中找到),因为我们需要用到unity CG runtime给我们提供的GPU硬件资源数据和各种图形处理函数,都在这个文件可以看到,这里我也不贴代码了,小伙伴们务必自己打开看一下。
④.appdata和v2f结构体,如果小伙伴们是按照顺序看我的博客,那么对这两个结构体及其包含的语义绑定字段有详细的理解。额外要说的另外一个语义TEXCOORD0,前面的TEXCOORD代表了纹理坐标,0则代表了第0通道纹理坐标,大部分情况下显卡支持4套纹理通道给我们开发使用,语义分别是TEXCOORD0,TEXCOORD1,TEXCOORD2,TEXCOORD3,我们可以对纹理坐标进行scale缩放或translate位移操作并储存到任意0-3的TEXCOORD。SV_POSITION则等价于POSITION,SV_前缀代表system value,我查到的解释是,当使用SV_POSITION绑定vertex顶点函数输出时,那么顶点坐标在vertex顶点函数return后就被固定了,直接进入光栅处理阶段,不可更改。
by the way,顺便画图描叙一下TEXCOORD作为纹理坐标语义输入时的uv:贴图坐标系,如下图:
可以认为,小猫贴图的uv就是从(0,0)到(1,1),(m,n)为其中一个采样颜色color值,绑定TEXCOORD语义输入时,float2 uv值就被传入(0,0)插值到(1,1)。
⑤.sampler2D _MainTex;,这句代码要和Properties{}对应,是定义获取_MainTex这个texture2D对象,当然CG代码中类型名为sampler2D,定义好后sampler2D对象后,提供后续函数使用。
⑥.float4 _MainTex_ST;,这句代码可能让小伙伴们迷惑,咱们又没在Properties{}中定义_MainTex_ST这个外部变量,怎么突然就蹦出来了这个东西?实际上这是和_MainTex相关的变量,代表了_MainTex的Scale和Translate,这是我从官方看到的解释,Scale缩放则对应unity编辑器ui面板上的Tiling,Translate位移则对应其Offset,属于unity CG编译器为我们提供的texture2D默认附带的变量字段。
⑦.vert顶点函数,我直接在代码中进行注释讲解 ,如下:
-
v2f vert (appdata v)
-
{
-
v2f o;
-
//将unity CG底层提供的模型顶点坐标源数据变换到裁剪空间
-
//UnityObjectToClipPos这个cg内置函数相当于mul(UNITY_MATRIX_MVP,v.vertex);
-
o.vertex = UnityObjectToClipPos(v.vertex);
-
// Transforms 2D UV by scale/bias property
-
// #define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)
-
//上面是UnityCG.cginc的TRANSFORM_TEX宏函数定义,实际上使用到了_MainTex_ST字段,意思就是将_MainTex_ST带入纹理uv的运算
-
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
-
return o;
-
}
⑧.frag片段函数,解释如下:
-
fixed4 frag (v2f i) : SV_Target
-
{
-
//tex2D为采样函数,具体意思就是根据uv值对一张纹理进行xy坐标点颜色color值获取,在片段函数返回并渲染
-
fixed4 col = tex2D(_MainTex, i.uv);
-
return col;
-
}
上面那张小猫图,其中以动点(m,n)进行tex2D采样的话,就是返回了不同坐标下小猫图的像素颜色值,并传递给col且return渲染。
⑨.Fallback "Diffuse",这就是unity CG编译器就行的容错处理了,假如我们的CG shader运行在一个很低端古老的的GPU而显示不出来,就回滚为Diffuse着色器,反正所有CPU都支持这个。
最后看一下这个CG shader的呈现效果,如下图:
以上就是对一个unity CG shader非常详细的讲解,目的就是为了大家能轻松理解后续的复杂的高级着色器。
最后还要重复罗嗦一句,学习CG shader必须按照图形流水线上下文来观察理解,比如流水线顶点到光栅到片段处理顺序流程,同时顶点函数和片段函数处理的输入输出的GPU硬件资源数据是动态变化的,比如模型顶点源坐标和纹理坐标等动态变化的数据,我们心里要有这些概念才能学好CG shader。
so,接下来继续深入。