在学完前面的线代和图形学后,终于可以来到正题了:《UnityShader入门精要》——冯乐乐
前四章是基础篇:
- 第一章 欢迎来到Shader的世界
- 第二章 渲染流水线
- 第三章 Unity Shader 基础
- 第四章 学习 Shader所需的数学基础
这里我是快速浏览了前四章的目录,然后直接进入第五章最简单的案例,毕竟开局都是HELLOWORLD嘛。遇到不懂的,再去看前四章,或者参考网上的链接进行补充。
书中的案例源码在:
https://github.com/candycat1992/Unity_Shaders_Book
切换到unity_2017_1那个分支后,使用unity2019版本可以打开。
书中错误可参考《Unity Shader入门精要》勘误
然后建议在开始之前,先给VS安装ShaderlabVS插件,这样写shader时就有代码提示功能了:
https://marketplace.visualstudio.com/items?itemName=ShaderlabVS2019.ShaderlabVS
下载到vsix文件后,双击安装即可,建议先关掉VS。
一、第五章的案例
这个案例可以当作HELLOWORLD吧,了解一下shader的基本流程。具体为什么这样做,就要回头看第三章的知识了。步骤如下:
1.为了得到更原始的效果,先去除天空盒
Window->Rendering->Lighting Setting,打开面板后,将Skybox Material那一项选为None即可
2.新建一个Shader
Create->Shader->Standard Surface Shader,命名为Chapter5-SimpleShader。选中该文件直接回车,会打开Visual Studio进行编辑。然后就能看到默认的代码,先改成这样:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Chapter5-SimpleShader"
{
SubShader
{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 v:POSITION):SV_POSITION{
return UnityObjectToClipPos(v);
}
fixed4 frag():SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
}
使用书中的代码后,Unity5.4之后的版本会自动升级,并有提示:
Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,)' with 'UnityObjectToClipPos()'
3.新建一个材质SimpleShaderMat
这里我没找到怎么为材质指定第2步的Chapter5-SimpleShader,只能直接把shader文件拖到材质的Inspector面板。
4.新建一个Sphere,在MeshRender中指定材质为SimpleShaderMat,就能看到大白球了。
二、回顾第三章 Unity Shader基础
1.create种类
- Standard Surface Shader 包含了标准光照模型的表面着色器
- Unlit Shader 不含光照(但包含雾效)的基本顶点/片元着色器
- Image Effect Shader 实现各种屏幕后处理效果(详见第12章)
- Compute Shader 利用GPU的并行性来进行一些与常规渲染流水线无关的计算
surface shaders,这是 Unity 主张鼓励使用的 Shader 编写方式,现在随意在 Unity 中创建的 Shader 文件其实就是 surface shader,那说白了 surface shaders 就是对 vertex and fragment shaders 的包装,最后的编译到底层的时候也是以 vertex and fragment shaders 的形式输入的。
2.ShaderLab
Unity Shader和图形学中提及的Shader有很大不同。
Unity Shader使用ShaderLab来编写。ShaderLab是Unity提供的编写Unity Shader的一种说明性语言。它使用了一些嵌套在花括号内部的语义(syntax)来描述一个Unity Shader文件的结构。这些结构包含了许多渲染所需的数据。例如Properties语句块中定义了着色器所需的各种属性,这些属性将会出现在材质面板中。从设计上来说,ShaderLab类似CgFX和Direct3D Effects(.FX)语言,他们都定义了要显示一个材质所需的所有东西,而不仅仅是着色器代码。
一个Unity Shader的基础结构如下所示:
Shader "ShaderName" {
Properties{
//属性
}
SubShader{
//显卡A使用的子着色器
}
SubShader{
//显卡B使用的子着色器
}
Fallback "VertexLit"
}
Unity会根据这个文件的结构来编译成真正的Shader文件,而开发者只需要和Unity Shader打交道即可。
3.Shader Name
以上面的案例参考
Shader "Custom/Chapter5-SimpleShader"
4.Shader Properties
格式是这样的:Name ("display name",PropertyType) = DefaultValue
,比如:
Shader "Custom/ShaderLab"
{
Properties{
//Numbers and Sliders
_Int("Int", Int) = 2
_Float("Float", Float) = 1.5
_Range("Range", Range(0.0,5.0) = 3.0)
// Colors and Vectors
_Color("Color", Color) = (1,1,1,1)
_Vector("Vector",Vector) = (2,3,6,1)
// Textures
_2D("2D", 2D) = ''"{}
_cube("Cube", Cube) = "White" {}
_3D("3D", 3D) = "black" {}
}
FallBack "Diffuse"
}
声明这些属性是为了在材质面板中能够方便的调整各材质属性。即使我们不在Properties语义块中声明这些属性,也可以直接在Cg代码片中定义变量。此时我们可以通过脚本向Shader中传递这些属性。因此,Properties语义块的作用仅仅是为了让这些属性可以出现在材质面板中。
5.SubShader
每个Unity Shader 文件可以包含多个SubShader语义块,但最少要有一个,当Unity需要加载这个Unity Shader时,Unity 会扫描所有的SubShader 语义块,然后选择第一个能够在目标平台上运行的SubShader。如果都不支持的话,Unity就会使用Fallback语义指定的Unity Shader。
Unity提供这种语义的原因在于,不同的显卡具有不同的能力。例如,一些旧的显卡仅能支持一定数目的操作指令,而一些更高级的显卡可以支持更多的指令数,那么我们希望在旧的显卡上计算复杂度较低的着色器,在高级的显卡上使用计算复杂度较高的着色器,以便提供更出色的画面。
SubShader语义块包含的定义通常如下:
SubShader{
// 可选的
[Tags]
// 可选的
[RenderSetup]
pass{
}
// other passes
}
SubShader中定义了一系列Pass可选的状态([RenderSetup])和标签([Tags])设置。
每个Pass定义了一次完整的渲染流程,但如果Pass的数量过多,往往会造成渲染性能的下降。因此我们应该尽量的使用最小数目的Pass。
状态和标签同样可以在Pass中声明。不同的是,SubShader中的一些标签设置是特定的。也就是说,这些标签设置和Pass中使用的标签是不一样的。
而对于状态设置来说,其使用的语法是相同的。但是如果我们在SubShader进行了这些设置,那么将会用于所有的Pass。
6.SubShader的RenderSetup
ShaderLab提供了一系列渲染状态的设置指令,而这些指令可以设置显卡的各种状态,例如是否是开启混合/深度测试等,下表给出了ShaderLab中常见的渲染状态设置选项。
当在SubShader块中设置了上述渲染状态时,就会应用到所有的Pass。如果我们不想这样(例如在双面渲染中,我们希望在第一个Pass中剔除正面来对背面进行渲染,在第二个Pass中剔除背面来对正面进行渲染),可以在Pass语义块中单独进行上面的设置。
7.SubShader的Tags
SubShader的标签(Tags)是一个键值对(Key/Value Pair),它的键和值都是字符串类型。这些键值对是SubShader和渲染引擎沟通的桥梁。它们用来告诉Unity的渲染引擎:我希望怎样以及何时渲染这个对象。标签的结构如下:
Tags {"TagName1" = "Value1" "TagName2" = "Value2"}
需要注意的是,上述标签仅可以在SubShader中使用,而不可以在Pass块中声明。Pass块中虽然也可以设置标签,但这些标签是不同于SubShader的标签类型。
8.Pass语义块
Pass语义块包含的语义如下
Pass{
[Name]
[Tags]
[RenderSetup]
// other code
}
首先我们可以在Pass块中定义该Pass的名称,例如:
Name "MyPassName"
通过这个名称,我们可以使用ShaderLab的UsePass命令来直接使用Unity Shader中的其他Pass。例如:
UsePass "MyShader/MYPASSNAME"
这样可以提高代码的复用性。需要注意的是,由于Unity内部会把所有的Pass的名字转换成大写字母的表示,因此,在使用UsePass命令时必须使用大写形式的名字。
其次,我们也可以对Pass设置渲染状态。SubShader的状态设置同样适用于Pass。除了上面提到的状态设置外,在Pass中我们还可以使用固定管线着色器的命令(详见3.4.3节)。
Pass同样可以设置标签,但它的标签不同于SubShader标签。这些标签也是用于告诉渲染引擎我们希望怎样来渲染该物体。下图给出了Pass中使用的标签类型。
9.留一条后路:Fallback
Fallback "name"
// 或者
Fallback Off
我们可以通过一个字符串来告诉Unity这个最低级的Unity Shader是谁。我们也可以任性的关闭Fallback功能,但一旦你这么做,你的意思大概就是:如果一个显卡跑不了上面的所有SubShader,那就不要管它了。
下面给出了一个使用Fallback语句的例子:
Fallback "VertexLit"
事实上,Fallback还会影响阴影的投射。在渲染阴影纹理时,Unity会在每个UnityShader中寻找一个阴影投射的Pass。通常情况下,我们不需要自己专门实现一个Pass,这是因为Fallback使用的内置Shader中包含了这样一个通用的Pass。因此,为每个UnityShader正确设置Fallback是非常重要的。更多关于Unity中阴影的实现,可以参见9.4节。
三、Unity的新宠:表面着色器(Surface Shader)
表面着色器(Surface Shader)是Unity自己创造的一种着色器代码类型。它需要的代码量很少,Unity在背后做了很多工作,但渲染的代价比较大。它的本质上和下面要讲的顶点/片元着色器是一样的。也就是说,当Unity提供一个表面着色器时,它在背后仍然把它转换成对应的顶点/片元着色器。我们可以理解为,表面着色器是Unity对顶点/片元着色器的更高一层的抽象。它存在的价值在于。Unity为我们处理了很多光照细节,使我们不必再操心这些烦人的事情。
一个非常简单的表面着色器代码如下:
Shader "Custom/Simple Surface Shader"{
SubShader{
Tags {"RenderType" = "Opaque"}
CGPROGRAM
#pragma surface surf Lambert
struct Input{
float 4 color : COLOR;
};
void surf (Input In, inout SurfaceOutput o){
o.Alebo = 1;
}
ENDCG
}
Fallback "Diffuse"
}
从上述程序中可看出,表面着色器被定义在SubShader语义块(而非Pass语义块)中的CGPROGRAM和ENDCG之间。原因是,表面着色器不需要开发者关心使用多少个Pass、每个Pass如何渲染等问题,Unity会在背后为我们做好这些事情。我们要做的就是告诉它使用这些纹理去填充颜色,使用这个法线纹理去填充法线,使用Lambert光照模型,其他的不要来烦我哦。
CGPROGRAM和ENDCG之间的代码是用Cg/HLSL编写的,也就是说。我们需要把Cg/HLSL语言嵌套在ShaderLab语言中。值得注意的是,这里的Cg/HLSL是Unity经封装后提供的,它的语法和标准的Cg/HLSL几乎一样,但还是有细微的不同,例如有些原生的函数和用法Unity没有提供支持。
四、最聪明的孩子:顶点/片元着色器
在Unity中我们可以使用Cg/HLSL语言来编写顶点/片元着色器(Vertex/Fragment Shader)。他们更加复杂,但灵活性也更高。
一个非常简单的顶点/片元着色器,就是第五章开头那个案例。
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Chapter5-SimpleShader"
{
SubShader
{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 v:POSITION):SV_POSITION{
return UnityObjectToClipPos(v);
}
fixed4 frag():SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
}
和表面着色器类似,顶点/片元着色器的代码也要定义在CGPROGRAM和ENDCG之间,但不同的是,顶点/片元着色器是写在Pass语义块内,而非SubShader内。原因是,我们需要自己定义每个Pass需要使用的Shader代码。虽然我们可能需要编写更多的代码,但带来的好处是灵活性很高。更重要的是。我们可以控制渲染的实现细节。同样,这里的CGPROGRAM和ENDCG之间的代码也是使用Cg/HLSL编写的。
五、被抛弃的角落:固定函数着色器
上面两种Unity Shader形式都使用了可编程管线。而对于一些较旧的设备,他们不支持可编程管线着色器,因此,这时候我们就需要使用固定函数着色器(Fixed Function Shader)来完成渲染。这些着色器往往只可以完成一些非常简单的效果。
一个非常简单的固定函数着色器示例代码如下:
Shader"Tutorial/Basic"{
Properties {
_Color ("Main Color", Color) = (1,0.5,0.5,1)
}
SubShader {
Pass{
Material{
Diffuse [_Color]
}
Lighting On
}
}
}
可以看出,固定函数着色器代码被定义在Pass语义块中,这些代码相当于Pass中的一些渲染设置。对于固定着色器来说。我们需要完全使用ShaderLab的语法(即使用ShaderLab的渲染设置命令)来编写,而非使用Cg/HLSL。
由于现在绝大多数GPU都支持可编程的渲染管线,这种固定渲染管线的编程方式已经逐渐被抛弃,实际上,在Unity5.2中,所有固定函数着色器都会在背后被Unity编译成对应的顶点/片元着色器,因此真正意义上的固定函数着色器已经不存在了。
六、选用哪种UnityShader形式
那么我们究竟选用哪一种Unity Shader的编写呢?在这里给出一些建议。
1.除非你有非常明确的需求必须要使用固定函数着色器,例如在非常旧的设备上运行你的游戏(这些设备非常少见),否则请使用可编程管线的着色器,即表面着色器或顶点/片元着色器
2.如果你想和各种光源打交道,你可能更喜欢使用表面着色器,但需小心它在移动平台的性能表现。
3.如果你需要使用的光照数目非常少,例如只有一个平行光,那么使用顶点/片元着色器是一个更好的选择。
4.最重要的是,如果你有很多自定义的渲染效果,那么请选择顶点/片元着色器。
本书的目的不仅在于教给读者如何使用Unity Shader, 更重要的是想要让读者掌握渲染背后的原理。仅仅了解高层抽象虽然可能会暂时使工作简化, 但从长久来看“知其然而不知其所以然”所带来的影响更加深远。因此, 在本书接下来的内容中,我们将着重使用顶点/片元着色器来进行Unity Shader 的编写。对于表面着色器来说,我们会在本书的第17 章中进行剖析, 读者可以在那里找到更多的学习内容。
七、Unity Shader !=真正的Shader
需要读者注意的是, Unity Shader 并不等同于第2 章中所讲的Shader,尽管Unity Shader 翻译过来就是Unity 着色器。在Unity 里, Unity Shader 实际上指的就是一个ShaderLab 文件一一硬盘上以.shader 作为文件后缀的一种文件。
在Unity Shader (或者说是ShaderLab 文件)里, 我们可以做的事情远多于一个传统意义上的Shader。
- 在传统的Shader 中, 我们仅可以编写特定类型的Shader , 例如顶点着色器、片元着色器等。而在Unity Shader 中,我们可以在同一个文件里同时包含需要的顶点着色器和片元着色器代码。
- 在传统的Shader 中,我们无法设置一些渲染设置, 例如是否开启混合、深度测试等,这些是开发者在另外的代码中自行设置的。而在Unity Shader 中, 我们通过一行特定的指令就可以完成这些设置。
- 在传统的Shader 中,我们需要编写冗长的代码来设置着色器的输入和输出, 要小心地处理这些输入输出的位置对应关系等。而在Unity Shader 中,我们只需要在特定语句块中声明一些属性,就可以依靠材质来方便地改变这些属性。而且对于模型自带的数据(如顶点位置、纹理坐标、法线等) , Unity Shader 也提供了直接访问的方法,不需要开发者自行编码来传给着色器。
当然, Unity Shader 除了上述这些优点外,也有一些缺点。由于Unity Shader 的高度封装性,我们可以编写的Shader 类型和语法都被限制了。对于一些类型的Shader,例如曲面细分着色器(Tessellation Shader)、几何着色器( Geometry Shader )等, Unity 的支持就相对差一些。例如,Unity 4.x 仅在DirectX 11 平台下提供曲面细分着色器、几何着色器的相关功能,而对于OpenGL平台则没有这些支持。除此之外, 一些高级的Shader 语法Unity Shader 也不支持。
可以说, Unity Shader 提供了一种让开发者同时控制渲染流水线中多个阶段的一种方式,不仅仅是提供Shader 代码。作为开发者而言,我们绝大部分时候只需要和Unity Shader 打交道,而不需要关心渲染引擎底层的实现细节。
八、Unity Shader 和 CG/HLSL 之间的关系
正如我们之前所讲, Unity Shader 是用ShaderLab 语言编写的,但对于表面着色器和顶点/片元着色器,我们可以在ShaderLab 内部嵌套CG/HLSL 语言来编写这些着色器代码。这些CG/HLSL代码是嵌套在CGPROGRAM 和ENDCG 之间的,正如我们之前看到的示例代码一样。由于CG 和DX9 风格的HLSL 从写法上来说几乎是同一种语言,因此在Unity 里CG 和HLSL 是等价的。我们可以说, CG/HLSL 代码是区别于ShaderLab 的另一个世界。
通常, CG 的代码片段是位于Pass 语义块内部的,如下所示:
Pass {
// Pass的标签和状态设置
CGPROGRAM
//编译指令,例如
#pragma vertex vert
#pragma fragment frag
//CG 代码
ENDCG
//其他一些设置
}
读者可能会有疑问:“之前不是说在表面着色器中, CG/HLSL 代码是写在SubShader 语义块内吗?而不是Pass 块内。” 的确,在表面着色器中, CG/HLSL 代码是写在SubShader 语义块内,但是读者应该还记得,表面着色器在本质上就是顶点/片元着色器,它们看起来很不像是因为表面着色器是Unity 在顶点/片元着色器上层为开发者提供的一层抽象封装,但在背后, Unity 还是会把它转化成一个包含多Pass 的顶点/片元着色器。我们可以在Unity Shader 的导入设置面板中单击Show generated code 按钮来查看生成的真正的顶点/片元着色器代码。可以说,从本质上来讲, Unity Shader只有两种形式:顶点/片元着色器和固定函数着色器(在Unity 5.2 以后的版本中,固定函数着色器也会在背后被转化成顶点/片元着色器,因此从本质上来说Unity 中只存在顶点/片元着色器)。
在提供给编程人员这些便利的背后, Unity 编辑器会把这些CG 片段编译成低级语言,如汇编语言等。通常,Unity 会自动把这些CG 片段编译到所有相关平台(这里的平台是指不同的渲染平台,例如Direct3D 9、OpenGL、Direct3D 11 、OpenGLES 等〉上。这些编译过程比较复杂, Unity会使用不同的编译器来把CG 转换成对应平台的代码。这样就不会在切换平台时再重新编译,而且如果代码在某些平台上发生错误就可以立刻得到错误信息。
正如在3.1.3 节中看到的一样,我们可以在Unity Shader 的导入设置面板上查看这些编译后的代码,查看这些代码有助于进行Debug 或优化等,如图3.9 所示。
但当发布游戏的时候,游戏数据文件中只包含目标平台需要的编译代码,而那些在目标平台上不需要的代码部分就会被移除。例如,当发布到Mac OS X 平台上时, DirectX 对应的代码部分就会被移除。
九、示例解析
现在可以回到第五章开头那个示例了,有了第三章的简介,应该很容易看懂。不过还有一些第三章没说的细节,在第五章中重新进行了详解:
1.pragma vertex vert和pragma fragment frag
vert、frag是函数名称,告诉Unity,哪个函数包含了顶点着色器的代码,哪个函数包含了片元着色器的代码。
2.vert
这是本例使用的顶点着色器代码,它是逐顶点执行的。
float4 vert(float4 v : POSITION) : SV_POSITION{
return UnityObjectToClipPos(v);
}
这个代码看升级之前的,更容易理解:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
POSITION 和SV_POSITION都是CG/HLSL中的语义,是不可忽略的,POSITION告诉Unity把模型的顶点坐标填充到输入参数v中,SV_POSITION告诉unity顶点着色器的输出是裁剪空间中的顶点坐标。UnityObjectToClipPos,是将顶点坐标从模型空间转换到剪裁空间中。
3.frag
fixed4 frag() : SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
在本例中frag 函数没有任何输入,它的输出是一个fixed4 类型的变量,并使用了SV_Target语义进行限定,它等于告诉渲染器,把用户的输出颜色存储到一个渲染 目标(render target)中,这里将输出到默认的帧缓存中。片元着色器输出的颜色的每个分量范围在[0,1],其中(0,0,0)表示黑色,(1,1,1)表示白色。
fixed4 低精度RGBA颜色
十、更多的模型数据
在第一个例子中,在顶点着色器中使用POSITION语义得到了模型的顶点位置。那么,如果我们想要得到更多的模型数据该怎么办呢?
现在,我们想要得到模型上每个顶点的纹理坐标和法线方向。这个需求是很常见的,我们需要使用纹理坐标来访问纹理,而法线可用于计算光照。因此,我们需要为顶点着色器定义一个新的输入参数,这个参数不再是一个简单的数据类型,而是一个结构体。修改后的代码如下:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Chapter5-SimpleShader"
{
SubShader
{
Pass{
CGPROGRAM
//vert、frag是函数名称,告诉Unity,那个函数包含了顶点着色器的代码,
//哪个函数包含了片元着色器的代码
#pragma vertex vert
#pragma fragment frag
//使用一个结构体来定义顶点着色器的输入
struct a2v {
//POSITION 语义告诉Unity,用模型空间的顶点坐标填充vertex变量
float4 vertex : POSITION;
//NORMAL 语义告诉Unity,用模型空间的法线方向填充normal变量
float3 normal : NORMAL;
//TEXCOORD0 语义告诉Unity,用模型的第一套纹理坐标填充texcoord变量
float4 texcoord : TEXCOORD0;
};
float4 vert(a2v v) : SV_POSITION{
//将顶点坐标从模型空间转换到剪裁空间中
//使用v.vertex来访问模型空间的顶点坐标
return UnityObjectToClipPos(v.vertex);
}
fixed4 frag():SV_Target{
//fixed4 低精度RGBA颜色
//片元着色器输出的颜色的每个分量范围在[0,1],其中(0,0,0)表示黑色,(1,1,1)表示白色
return fixed4(1.0,1.0,1.0,1.0);
}
ENDCG
}
}
}
1.a2v 有什么含义
a表示应用(application),v表示顶点着色器(vertex shader),a2v意思就是把数据从应用阶段传递到顶点着色器中。
2.填充到POSITON等语义中的这些数据究竟从哪里来的呢?
在Unity中,它们是由使用该材质的Mesh Render组件提供的。在每帧调用Draw Call时,Mesh Render组件会把它负责渲染的模型数据发送给Unity Shader。我们知道,一个模型通常包含一组三角面片,每个三角面片由3的顶点构成,而每个顶点又包含了一些数据,如顶点位置、法线、切线、纹理坐标、顶点颜色等。通过上面的方法,我们就可以在顶点着色器中访问顶点的这些模型数据。
十一、顶点着色器和片元着色器如何通信
在实践中,我们往往希望从顶点着色器输出的一些数据(模型的法线、纹理坐标等)传递给片元着色器,这就涉及到顶点着色器和片元着色器之间的通信。因此,我们需要再定义一个新的结构体,来储存顶点着色器的输出数据,并作为片元着色器的输入参数。修改后的代码如下:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Chapter5-SimpleShader"
{
SubShader
{
Pass{
CGPROGRAM
//vert、frag是函数名称,告诉Unity,那个函数包含了顶点着色器的代码,
//哪个函数包含了片元着色器的代码
#pragma vertex vert
#pragma fragment frag
//使用一个结构体来定义顶点着色器的输入
struct a2v {
//POSITION 语义告诉Unity,用模型空间的顶点坐标填充vertex变量
float4 vertex : POSITION;
//NORMAL 语义告诉Unity,用模型空间的发现方向填充normal变量
float3 normal : NORMAL;
//TEXCOORD0 语义告诉Unity,用模型的第一套纹理坐标填充texcoord变量
float4 texcoord : TEXCOORD0;
};
//使用一个结构体来定义顶点着色器的输出
struct v2f {
//SV_POSITION语义告诉Unity,pos里包含了顶点在裁剪空间中的位置信息
float4 pos : SV_POSITION;
//COLOR0语义可以用于存储颜色信息
fixed3 color : COLOR0;
};
//float4 vert(a2v v) : SV_POSITION{
//将顶点坐标从模型空间转换到剪裁空间中
//使用v.vertex来访问模型空间的顶点坐标
//return UnityObjectToClipPos(v.vertex);
//}
//fixed4 frag():SV_Target{
//fixed4 低精度RGBA颜色
//片元着色器输出的颜色的每个分量范围在[0,1],其中(0,0,0)表示黑色,(1,1,1)表示白色
//return fixed4(1.0,1.0,1.0,1.0);
//}
v2f vert(a2v v):SV_POSITION {
//声明输出结构
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//v.normal 包含了顶点的法线方向,其分量范围在[-1.0,1.0]
//下面的代码把分量范围映射到了[0.0,1.0]
//并存储到o.color中传递给片元着色器
o.color = v.normal * 0.5 + fixed3(0.5,0.5,0.5);
return o;
}
fixed4 frag(v2f i) : SV_Target{
//将插值后的i.color 显示在屏幕上
return fixed4(i.color,1.0);
}
ENDCG
}
}
}
1.vert的修改
之前返回的是一个float4类型的变量,即顶点在裁剪空间中的位置。现在返回的是v2f,v2f必须包含一个变量,它的语义是SV_POSITION。否则渲染器就无法得到剪裁空间中的顶点坐标,也就无法把顶点渲染到屏幕上。
COLOR0语义中的数据由用户自行定义,但一般都是存储颜色,例如漫反射颜色或逐顶点的高光反射颜色。
至此,我们完成了顶点着色器和片元着色器之间的通信。需要注意的是,顶点着色器是逐顶点调用的,而片元着色器是逐片元调用的。片元着色器中的输入实际是把顶点着色器的输出进行插值后得到的结果。(注:作者的这段话,可以根据闫令琪的图形学入门知识来理解。)
另外,vert当中计算color的公式是法线贴图的方式,那最后应该大部分是蓝色呀,为啥我看到的是个粉球呢???仔细检查了一下,发现是报错了,还提示了是45行。在Visual Studio设置中打开行号显示,发现是vert函数定义那一行:
打开https://github.com/candycat1992/Unity_Shaders_Book中unity_2017分支,vert的输出语义SV_POSITION被去掉了。原因还不清楚,不过改一下之后确实正常了:
v2f vert(a2v v) { //把这一行的:SV_POSITION去掉
十二、如何使用属性
材质提供给我们一个可以方便调节Unity Shader中参数的方式,而这些参数就需要写在Properties语义块中。
现在,我们有了一个新的需求,我们想要在材质面板上显示一个颜色拾取器,来直接控制模型在屏幕上显示的颜色,修改后的代码如下:
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Custom/Chapter5-SimpleShader"
{
Properties{
_Color("Color Tint",Color) = (1.0,1.0,1.0,1.0)
}
SubShader
{
Pass{
CGPROGRAM
//vert、frag是函数名称,告诉Unity,那个函数包含了顶点着色器的代码,
//哪个函数包含了片元着色器的代码
#pragma vertex vert
#pragma fragment frag
//在CG代码中,定义一个与属性名称和类型都匹配的变量
fixed4 _Color;
//使用一个结构体来定义顶点着色器的输入
struct a2v {
//POSITION 语义告诉Unity,用模型空间的顶点坐标填充vertex变量
float4 vertex : POSITION;
//NORMAL 语义告诉Unity,用模型空间的法线方向填充normal变量
float3 normal : NORMAL;
//TEXCOORD0 语义告诉Unity,用模型的第一套纹理坐标填充texcoord变量
float4 texcoord : TEXCOORD0;
};
//使用一个结构体来定义顶点着色器的输出
struct v2f {
//SV_POSITION语义告诉Unity,pos里包含了顶点在裁剪空间中的位置信息
float4 pos : SV_POSITION;
//COLOR0语义可以用于存储颜色信息
fixed3 color : COLOR0;
};
//float4 vert(a2v v) : SV_POSITION{
//将顶点坐标从模型空间转换到剪裁空间中
//使用v.vertex来访问模型空间的顶点坐标
//return UnityObjectToClipPos(v.vertex);
//}
//fixed4 frag():SV_Target{
//fixed4 低精度RGBA颜色
//片元着色器输出的颜色的每个分量范围在[0,1],其中(0,0,0)表示黑色,(1,1,1)表示白色
//return fixed4(1.0,1.0,1.0,1.0);
//}
v2f vert(a2v v) {
//声明输出结构
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//v.normal 包含了顶点的法线方向,其分量范围在[-1.0,1.0]
//下面的代码把分量范围映射到了[0.0,1.0]
//并存储到o.color中传递给片元着色器
o.color = v.normal * 0.5 + fixed3(0.5,0.5,0.5);
return o;
}
fixed4 frag(v2f i) : SV_Target{
//return fixed4(i.color,1.0);
fixed3 c = i.color;
c *= _Color.rgb;
//将插值后的i.color 显示在屏幕上
return fixed4(c,1.0);
}
ENDCG
}
}
}
上面的代码中,我们首先添加了Properties语义块,在这个语义块中声明了一个属性_Color,它的类型是Color,初始值是(1.0,1.0,1.0,1.0),对应白色。为了在CG代码中能访问到它,我们需要在CG代码片段中提前定义一个新的变量,这个变量的名称必须和Properties语义块中的属性定义一致。
有时CG变量前会有一个uniform关键字
uniform fixed4 _Color;
uniform 关键字是CG中修饰变量和参数的一种修饰词,它仅仅用于提供一些关于该变量的初始值是如何指定和存储的相关信息(这和其他一些图像编程接口中的uniform关键词的作用不太一样)。在UnityShader中,uniform关键词是可以省略的。
1.遇到的问题
说好的颜色选择器呢,我是不是哪里写错了……最后在这里找到了: