作者:王子饼干 ——镇江虹视游戏科技有限公司联合创始人、《多洛可小镇》制作人
原文链接:UnityShader入门精要笔记4:Shader基础
原文发布时间:2020年3月1日
文章类型: 授权转载
只有经历过地狱般的磨砺,才能练就创造天堂的力量;只有流过血的手指,才能弹出世间的绝响。——泰戈尔
笔记当前使用的Unity版本:“2019.3.3”
笔记当前Unity最新的版本:“2020.1.0.Alpha 25”
从本篇开始的话,我们可能就要开始具体的编写着色器了。关于本系列的文章所使用的所有代码和未来将要增加的代码,我都会放到我的Github上,并且贴在每一篇的末尾。关于笔记有任何的纰漏,或者有任何问题,欢迎大家在评论区指出。另外,我想问一下如何能在知乎编辑器上写笔记的时候关闭拼写检查,谢谢~
该系列的文章会使用我个人的讲述风格,并不是一味的把《UnityShader入门精要》原书中的内容复制过来,所以可能大部分的内容都经过了我自己的加工,剔除了旧版本的Shader中已经不能再使用的代码。只不过整体的脉络还是沿着《UnityShader入门精要》一步步的前进。
前面我们讲过了ShaderLab的简单语法,在前面的大多数的笔记当中,我还是会补充充足的注释到代码当中,并且我们会做一些简单的回顾。首先,我们先回忆一下一个着色器最基本的结构
Shader "Custom/MyShader"
//此处用于指定你的着色器的名称以及在Shader的下栏菜单中的位置
//注意不要和其他的着色器同名。
{
Properties{
//着色器的输入,说白了,这里缩写的内容都会显示到材质面板上
//如果有一些参数要留给用户去调整,都要写在这里,比如材质的纹理等
}
SubShader{
/*子着色器,每个子着色器都是一个完整的功能模块,
只不过他们应对不同的显卡,有任何一个子着色器可用
该着色器就是可用的,如果所有的着色器都不可用,
则Unity会使用Fallback指定的系统着色器来进行着色器
如果你不指定任何Fallback且所有的子着色器都失效了
那么该物体的渲染就会失效*/
pass{
/*每个子着色器中的pass块都要执行一遍
拿锻造一柄剑来说,一个pass就像是一道加工工艺
加工工艺越多,该着色器要处理的任务也就越多
当所有的pass都完成了,我们就可以得到一柄剑了。
*/
CGPROGRAM
//这里是真正的着色器代码所需要编写的地方,
//ShaderLab支持CG语法和GLSL语法,当然,仅仅是“语法”
//其本质还是ShaderLab,如果你要使用GLSL来编写着色器
//则使用GLSLPROGRAM和ENDGLSL语法块来定义
ENDCG
}
}
SubShader{
//针对显卡B的着色器
}
Fallback"Diffuse"
//如果上述所有的显卡都不管用,那我们就
//使用UnityShader自带的Diffuse来进行渲染
}
最后还是要补充一些题外话,就是该系列所有的教程都会采用“编写代码”的方式来进行着色器的开发,而不会使用ShaderGraph或者任何其他的节点编辑器,一来是因为有一些特殊的效果节点编辑器无法实现,二来是因为,如果可以实现着色器的代码,那么使用节点编辑器肯定也没有任何问题。
我们先尝试编写一个最简单的着色器,我们先在Unity中创建一个Cube
然后创建一个标准表面着色器,然后删除所有的代码,自己键入下列代码,注意一定要自己编写。如果你是一个初学者,你可以给自己一个基础的目标,那就是把这些代码写至少10遍,写到熟练为止。不断的练习会让你逐步的对每个语义,每一行代码开始产生自己的思考。而不是像你刚刚见到这些代码的时候的迷茫和无措。再配合书上对每一行代码的讲解开始深入理解它们。
Shader "UShaderMagicBook/SimpleTest"{
/*一个最基础的着色器*/
Properties{
_BaseColor("MyColor",Color) = (1.0,1.0,1.0,1.0) //后面不能加分号
}
SubShader{
//子着色器A
pass{
//第一个pass块
CGPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
/*定义你的顶点着色器的函数名称和片元着色器的函数名称,该步是必要的
起什么名字无所谓,命名规则同C类语言命名规则
1.区分大小写
2.不以数字开头
3.不能和系统关键字冲突*/
fixed4 _BaseColor;
/*光在Properties语块中定义变量是没用的,你必须在CGPROGRAM块中再次声明,Unity
才会把变量材质面板的值填充过来*/
float4 Vertex(float4 v:POSITION):SV_POSITION{
/*定义顶点着色器,处理和模型顶点有关的内容
一个顶点着色器至少要完成一件事,那就是把模型的顶点转换到
裁剪空间,至于为什么要转换到裁剪空间,详见笔记的5.2节*/
return UnityObjectToClipPos(v);
//把模型转换到裁剪空间
}
fixed4 Pixel():SV_TARGET{
//计算模型表面每个像素值的颜色
return _BaseColor;
}
ENDCG
}
}
}
接着,我们创建一个新的材质,然后把它的着色器换成我们刚才编写的那个着色器
然后将该材质拖拽到模型的表面,它就可以将该模型以我们的着色器代码渲染出来。
我们可以在属性面板找到,_BaseColor(也就是我们一开始在着色器的Properties中编写的一个输入值)
然后尝试改变这个输入的颜色,会发现模型的表面颜色也发生了改变。
前面已经说到了,属性也就是材质面板的输入,该部分可以通过在Properties语块中定义输入值来实现。
定义方式如下:
Properties{
_BaseColor("MyColor",Color) = (1.0,1.0,1.0,1.0)
}
其中BaseColor是属性的变量名,你在CGPROGRAM语块中引用的名称也是该名称,只不过在你引用之前,需要在CGPROGRAM语块中重新声明一下。否则Unity会提示“未定义的名称:_BaseColor”
而MyColor大家也猜到了,这个是一个字符串类型,在材质面板上的这个输入值的标签就是由该字符串定义的。
而后面的Color则是一个属性的类型,表面它是一个颜色值,UnityShader支持的属性类型有如下这些。(在笔记2中也出现过)
最后,我们要给它设定一个缺省值(默认值),也就是材质面板上最初的赋值,由于我们是Color类型的变量,所以我们给它赋值为了(1.0,1.0,1.0,1.0)
此处大家可能会疑问,颜色值为什么不是0-255而是0-1,我们经常需要把颜色进行混合(混合也就是采用指定的混合因子让它们乘起来,暂时理解不了没关系,知道有乘法这个操作就ok),如果两个比较大的数相乘,所得到的数字会更大,更别提我们可能会使用远超两种颜色的值进行混合。为了减轻GPU的运算压力,把颜色值的量映射到0-1的范围内更为合适。由于颜色值的范围始终是0-1,所以如果是PC上,所有值的颜色都可以使用fixed4类型来定义。
这也是下面为什么我们采用了fixed4来重新声明_BaseColor;
这带来了一个新的问题,也就是Unity的材质面板输入和CGPROGRAM中使用的属性类型并不是一样的,他们可能会存在一个对应关系,事实上,CG变量和Unity材质面板输入确实有类型对应关系,我们可以在 《UnityShader入门精要》的106页找到
当然,我在自己的笔记中也抄写了一份。以加深印象,方便后续的时候复习和回顾。当然啦,一个单纯的数字类型也分float,half,fixed三种基础类型,这三种类型都是浮点值类型,它们唯一的不同是数字的大小不同。我们可以在《UnityShader入门精要》的第117页找到它们
有两行代码比较关键
#pragma vertex Vertex
#pragma fragment Pixel
它就类似于主函数一样,只不过主函数是唯一的,我们不需要自己手动指定。而着色器中也有两个核心函数,顶点着色器函数和片元着色器函数,我们需要手动指定一下,这步是必须的。也不一定要定义成Vertex和Pixel,任何合法的函数名都是可以的。你的顶点着色器和片元着色器必须定义成#pragma编译指令中所指定的这个名字。
其实相对于是语义,我更喜欢把它称为是一个命令。在本节代码中,我们使用了三个语义,POSITION,SV_POSITION,SV_TARGET。
我们先阅读一下标准定义,尝试理解他们的作用。
上表中,POSITION的描述是“模型空间中顶点的位置,通常是float4类型”,其实仔细思考一下我们就很容易明白了,因为我们要计算一个模型的顶点,还是计算模型表面的颜色,我们都需要获得一定的输入,就像是Properties中定义的输入那样。但是这些输入并不是Properties一样是用户定义的,可有可无,根据具体需要来选择的那种。而是必须的,固有的,不论用户是否使用,都必须要有的输入。
其中模型的顶点就是必须要有的,因而我们定义了POSITION语义,告诉Unity,我们希望把模型的顶点赋值给哪个变量。(我其实更喜欢填充这个词,更有感觉一点。后面如果我说到填充,其实就是赋值的意思。)
我们还能看到一些其他的输入,比如TEXCOORD,COLOR,NORMAL等,不过暂时我们用不到。
下面我们说的很重要,大家务必记住。
相同的语义命令在不同的环境下作用可能不同,环境包括“顶点着色器的输入”,“顶点着色器的输出”,“片元着色器的输出”(要是有人问,为什么片元着色器没有输入的话,这里回答一下,片元着色器的输入就是顶点着色器的输出。)。就拿TEXCOORDn来说,作为顶点着色器的输入,它表示的是纹理坐标,而作为顶点着色器的输出来说,它表示可以表示很多内容。至于这个很多内容具体是什么,等我们遇到的时候就会说到的。
在顶点着色器的输出中,我们始终要输出一个内容,就是SV_POSITION,如果没有这个输出,那我们甚至可以认为你的着色器没有定义“顶点着色器”这个部分。它在渲染流水线中要处理的空间转换就是从模型空间转换到齐次裁剪空间,剩下的部分则由Unity自动完成。此处我们要用SV_POSITION来指定顶点的输出,该步是基础且必须的。
片元着色器的输出由SV_TARGET来指定,它一般是会放到片元着色器函数的后面。因为片元着色器的输出内容不需要再传递给其他的颜色了(或者说,片元着色器的输出就是我们最终需要的结果)
此处可以看到,SV_TARGET语义是定义在Pixel函数的后面的,注意要有冒号,以表明它是一个语义
通常来说,一个顶点着色器的输入和输出内容比较多,不仅仅是只有一个POSITION和一个SV_POSITION,此时我们就需要额外的定一个结构体来标记出所有的输入和输出。
struct vertexInput{
float4 vertex : POSITION; //先定义类型和变量名,然后注明语义块
float3 normal : NORMAL; //同上
}; //注意结构体后面一定要加一个分号
struct vertexOutput{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
// texcoord0在此处表示的已经不再是纹理坐标了,只是一个用于储存值的地方
};
这边再强调一下,为什么我们只定义顶点着色器的输入和输出。第一,顶点着色器的输出就是片元着色器的输入,第二,片元着色器的输出只有一个语义,它就是我们最终需要的结果,即SV_TARGET。因而我们只定义顶点着色器的输入和输出。
本篇所讲的内容都是基础且非常重要的,如果它们能够给任何人提供了一点帮助,我觉得就很有意义。在王爽的《汇编语言》中有一句话说的很好。即学习不应该是一股脑的把一个章节的知识都记下来然后再往前走,而是需要什么得时候就学什么,这样目标更为明确,对知识点的作用印象会更加深刻,其实《UnityShader入门精要》原书中第五章节内容要更多,但很多东西是现阶段不需要的,后续需要的时候,原书则直接拿过来用了,使得很多新手忘记了一些知识点的存在。当然我并不是说它是错误的,从编排性上来说,这样更加专业。只是教学或者教育应该是想方设法的降低知识点理解的难度,而非一味为了专业性而故弄玄虚(此处是泛指某些博客)。觉得该笔记有帮助的可以点个赞,刚开始写笔记,讲了很多和主题无关的东西,希望大家多多包涵。
以上就是本篇所有的内容了,给耐心看到这里的你点个赞,另外,本篇只用到了一个着色器代码,SimpleTest,虽然内容简单,大家可以自己编写出来,但是我还是上传到了我的github上。