ChuanXin 发表于 2021-11-19 14:37

《UnityShader入门精要》学习笔记——第三章 ...

声明:

本文章的学习内容来源全部出自《UnityShader入门精要》——冯乐乐
该文章只是本人我的学习笔记,里面对《UnityShader入门精要》进行了些许概括且加了自己的些许理解
如果想更加具体地了解其内容,建议购买原著进行学习
<hr/>Shader是渲染流水线的某些特定阶段,如顶点着色器、片元着色器......
Unity Shader让开发者能够轻松管理着色器代码和渲染设置(如开启/关混合、深度测试、设置渲染顺序等)
3.1Unity Shader概述

3.1.1一对好兄弟:材质(Material)和Unity Shader

材质(Material)和Unity Shader的配合,常见流程:
创建一个材质创建一个 Unity Shader,并把它赋给上一步中创建的材质把材质赋给要渲染的对象在材质面板中调整 Unity Shader的属性,以得到满意的效果。
Unity Shader定义了渲染所需的各种代码(如顶点着色器和片元着色器)、属性(如使用哪些纹理等)和指令(渲染和标签设置等),而材质则允许我们调节这些属性,并将其最终赋给相应的模型。

3.1.2Unity中的材质

Unity 的材质和许多建模软件(如 Cinema 4D、 Maya 等)中提供的材质功能类似,它们都提供了一个面板来调整材质的各个参数。这种可视化 的方法使得开发者不再需要自行在代码中设置和改变渲染所需的各种参数



3.1.3Unity中的Shader

Unity Shader本质上就是一个文本文件
Unity Shader也有 导入设置(Import Settings)面板
在该面板上,我们可以在Default Maps中指定该Unity Shader使用的默认纹理。 当任何材质 第一次使用该Unity Shader时,这些纹理就会自动被赋予到相应的属性上
在下方的面板中,Unity会显示出和该Unity Shader相关的信息,例如它是否是一个表面着色器(Surface Shader)、是否是 一个固定函数着色器(Fixed Function Shader)等, 还有一 些信息是和我们在Unity Shader中的标签设置(详见3.3.3节)有关, 例如是否会投射阴影、使用的渲染队列、LOD值等



Unity Shader 的导入面板还可以方便地查看其使用的渲染队列(Render queue)、 是否关闭批处理(Disable batching) 、属性列表 (Properties) 等信息。
对于表面着色器(详见3.4.1节)来说,我们可以通过单击 Show generated code按钮米打开个新的文件,在该文件里将显示 Unity在背后为该表面着色器生成的顶点/片元着色器。这可以方便我们对这些生成的代码进行修改(需要复制到一个新的 Unity Shader中才可保存)和研究。 同样地,如果该 Unity Shader是一个固定函数着色器,在 Fixed function的后面也会出现一个Showgenerated code按钮,来让我们查看该固定函数着色器生成的顶点片元着色器。 Compile and show code下拉列表可以让开发者检查该 Unity Shader针对不同图像编程接口(例如 OpenGL、D3D9、D3D11等)最终编译成的 Shader代码,如图3.5所示。直接单击该按钮可以查看生成的底层的汇编指令。我们可以利用这些代码来分析和优化着色器。

3.2Unity Shader的基础: Shaderlab

Unity Shader 是Unity为开发者提供的高层级的渲染抽象层



在Unity中,所有的 Unity Shader 都是使用 ShaderLab 来编写的。
ShaderLab 是Unity提供的编写 Unity Shader 的一种说明性语言
它使用了一些嵌套在花括号内部的语义(syntax)来描述一个 Unity Shader 文件的结构。这些结构包含了许多渲染所需的数据,例如 Properties语句块中定义了着色器所需的各种属性,这些属性将会出现在材质面板中。 从设计上来说, Shaderlab类似于CgFX和 Direct3 D Efects(.FX)语言,它们都定义了要显示一个材质所需的所有东西,而不仅仅是着色器代码。

一个Unity Shader的基础结构如下图所示:
Shader "ShaderName"{
   Properties{
         //属性
   }
   SUbShader{
         //显卡A使用的子着色器
   }
   SubShader{
         //显卡B使用的子着色器
   }
   Fallback"VertexLit"
}Unity 在背后会根据使用的平台来把这些结构编译成真正的代码和 Shader 文件,而开发者只需要和 Unity Shader 打交道即可。
<hr/>3.3Unity Shader的结构

3.3.1 给Unity Shader起名字

每个 Unity Shader 文件的第一行都需要通过 Shader 语义来指定该 Unity Shader 的名字
当为材质选择使用的 Unity Shader 时,这些名称就会出现在材质面板的下拉列表里。通过在字符串中添加斜杠("/") 可以控制 Unity Shader 在材质面板中出现的位置。例如:
Shader "Custom/MyShader " {        }
那么这个 Unity Shader 在材质面板中的位置就是 Shader-> Custom-> MyShader



3.3.2 材质和Unity Shader的桥梁:Properties

Properties 语义块中包含了 一系列属性 (property), 这些属性将会出现在材质面板中。
Properties 语义块的定义通常如下:
Properties{
   //_名字(”显示的名称“,属性类型(有无范围)) = 默认值
   _Name("display name",PropertyType) = DefaultValue
   _Name("display name",PropertyType) = DefaultValue
   //更多属性
}
Properties语义块支持的属性类型:
属性类型默认值的定义语法例子Intnumber_Int ("Int", Int) = 2Floatnumber_Float ("Float", Float)= 1.5Range(min,max)number_Range("Range", Range(0.0, 5.0)) = 3.0Color(number,number,number,number)_Color("Color", Color) = (1.0, 1.0, 1.0, 1.0)Vector(number,number,number,number)_Vector("Vector", Vector) = (2, 3, 6, 1)2D"DefautTexture" {}_2D ("2D", 2D) = "" {}3D"DefautTexture" {}_3D ("3D", 3D) = "black" {}Cube"DefautTexture" {}_Cube ("Cube", Cube)= "white" {}所有属性类型例子代码:
Shader "Custom/ShaderLabProperties"{
   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.0, 1.0, 1.0, 1.0)
   _Vector("Vector", Vector) = (2, 3, 6, 1)
   
   //Textures                纹理类型""默认值可以为“white","black","red","bump"......
   _2D("2D", 2D) = "" {}
   _3D("3D", 3D) = "black" {}
   _Cube("Cube", Cube) = "white" {}
   }
   
   FallBack ”Diffuse"
}

为了在 Shader中可以访问到这些属性,我们需要在CG代码片中定义和这些属性类型相匹配的变量。需要说明的是,即使我们不在 Properties语义块中声明这些属性,也可以直接在CG代码片中定义变量。此时,我们可以通过脚本向 Shader中传递这些属性。 因此,Properties 语义块的作用仅仅是为了让这些属性可以出现在材质面板中

3.3.3 重量级成员:SubShader

一个Unity Shader 文件可以包含多个SubShader,但至少一个。
Unity会扫描所有的 SubShader 语义块,然后选择第一个能够在目标平台上运行的 SubShader(因为不同的显卡的能力不同),如果都不支持的话,Unity会使用 FallBack 语句指定的Unity Shader
SubShader 语义块中包含的定义通常如下:
SubShader{
   //标签——可选的
   
   
   //渲染状态设置——可选的
   
   
   Pass{
   }
   // Other Passes
}SubShader中定义了 一系列Pass以及可选的状态()和标签()设置。 每个 Pass定义了一次完整的渲染流程, 但如果Pass的数目过多, 往往会造成渲染性能的下降
Pass里面可以同样声明标签和渲染设置
SubShader中的一些标签设置是特定的(这些标签的设置和pass中的不一样)
渲染状态设置的语法是一样的,但是在Subshader 进行这些设置,将会应用于所有Pass

渲染状态设置
ShaderLab提供了一 系列渲染状态的设置指令, 这些指令可以设置显卡的各种状态, 例如:是否开启混合/深度测试等
ShaderLab常见的渲染状态设置选项:
状态名称设置指令解释CullCull   Back\|Front\|Off设置**剔除模式**:剔除背面/正面/ 关闭剔除ZTestZTestLess Greater\|LEqual \|GEqual \|Equal\|NotEqual \|Always设置**深度测试**时使用的函数ZWriteZWrite   On\|Off开启/关闭**深度写入**BlendBlend SrcFactor DstFactor开启并设置**混合模式**在SubShader块中设置了上述渲染状态时,将会应用到所有的Pass。如果我们不想这样(例如在双面渲染中,我们希望在第一个Pass中剔除正面来对背面进行渲染, 在第二个Pass中剔除背面来对正面进行渲染), 可以在Pass语义块中单独进行上面的设置。

SubShader的标签
SubShader的标签(Tags)是一个键值对(Key/Value Pair), 它的键和值都是字符串类型。 这 些键值对是SubShader和渲染引擎之间的沟通桥梁。 它们用来告诉Unity的渲染引擎:我希望怎样以及何时渲染这个对象
标签的结构:
Tags{
   "TagName1" = "Value1"
   "TagName2" = "Value2"
}SubShader的标签类型
标签类型说明例子Queue**控制渲染顺序**,指定该物体属于哪一个渲染队列
通过这种方式可以保证所有的**透明物体**可以在所有不透明物体后面被渲染(详见第8章)我们也可以自定义使用的渲染队列来控制物体的渲染顺序Tags{"Queue" = "Transparent"}RenderType**对着色器进行分类**,例如这是一个不透明的着色器,或是一个透明的着色器等
这可以被用于着色器替换(Shader Replacement)功能Tags{"RenderType" = "Opaque"}DisableBatching**指定是否对该SubShader使用批处理**(因为有些SubShader例如模型空间下的坐标进行顶点动画,在使用Unity的批处理功能时会出现问题)Tags{"DisableBatching" = "True"}ForceNoShadowCasting使用该SubShader的物体是否会有**投影**Tags{"ForceNoShadowCasting" = "True"}IgnoreProjector若该标签值为”True“,则使用该SubShader的物体不会受Projector的影响
通常用于**半透明物体**Tags{"IgnoreProjector" = "True"}CanUseSpriteAtlas当该Sub Shader是**用于精灵( sprites)时**,该标签设为“ False"Tags{"CanUseSpriteAtlas" = "False"}PreviewType当该Sub Shader是**用于精灵( sprites)时**,该标签设为“ False"Tags{"PreviewType" = "Plane"}PreviewType指明材质面板将**如何预览该材质**。默认情况下,材质将显示为一个球形,我们可以通过把该标签的值设为“ Plane"“ Sky Box”来改变预览类型Tags{"PreviewType" = "Plane"}注意:上述标签仅可在SubShader中声明

Pass语句块
Pass语义块包含的语义:
Pass{
             //Name ”MyPassName"
             //标签
      //渲染状态设置
   //Other Code
}在Pass中定义Pass的名称
Name ”MyPassName"
通过这个名称,我们可以使用 ShaderLab的UsePass 命令来直接使用其他 Unity Shader 中的Pass,这样可以提高代码的复用性,例如:
UsePass "MyShader /MYPASSNAME "
注意:由于 Unity 内部会把所有 Pass 的名称转换成大写字母的表示;因此在使用 UsePass命令时必须使用大写形式的名字。
可以对Pass 设置渲染状态,语法和SubShader一样,SubShader设置的渲染状态同样适用于Pass

可以对Pass 设置标签,但其不同于SubShader
Pass的标签类型
标签类型说明例子LightMode定义该*Pass*在Unity的渲染流水线中的角色Tags{"LightMode" = "ForwardBase"}RequireOptions用于指定**当满足某些条件时才渲染该*Pass***
它的值是一个由空格分隔的字符串目前, Unity支持的选项有SoftVegetation。在后面的版本中,可能会增加更多的选项Tags{"RequireOptions" = "SoftVegetation"}Unity支持的特殊的Pass,以便进行代码复用或实现更复杂的效果
Usepass:如我们之前提到的一样,可以使用该命令来复用其他 Unity Shader中的PassGrabpass:该Pass负责抓取屏幕并将结果存储在一张纹理中,以用于后续的Pass处理(详见10.2.2节)

3.3.4 留一条后路:Fallback

紧跟在各个SubShader语义块后面,如果上边所有的SubShader在这块显卡上都不能运行,就使用这个最低级的Shader
语义如下:
Fallback "name"
//或者
Fallback Off例子:FallBack "VertexLit"
事实上, Fallback还会影响阴影的投射。在渲染阴影纹理时, Unity会在每个 Unity Shader 中寻找一个阴影投射的Pass。通常情况下,我们不需要自己专门实现一个Pass,这是因为 Fallback使用的内置 Shader中包含了这样一个通用的Pass 因此,为每个 Unity Shader正确设置 Fallback是非常重要的

3.3.5 Shaderlab还有其他语义

CustomEditor语义:自定义材质面板的编辑界面
Category语义:对Unity Shader中的命令分组
<hr/>3.4 Unity Shader 的形式

尽管Unity Shader可以作的事情很多(例如设置渲染状态等),但是最重要的任务还是指定各种着色器所需的代码
这些着色器代码可以写在SubShader语义块中(表面着色器),也可以写在Pass语义块中(顶点/片元着色器 和 固定函数着色器)

3.4.1 Unity的宠儿:表面着色器(Surface Shader)

表面着色器(Surface Shader)是Unity自创的一种着色器代码类型。
它的代码量很少,但是代价很大,Unity在背后把它转化成顶点/片元着色器
表面着色器被定义在SubShader语义块中(而非Pass语义块)的CGPROGRAM和ENDCG之间,原因是,表面着色器不需要开发者自己关系使用多少个Pass、每个Pass如何渲染等问题,Unity会在背后帮我们做
CGPROGRAM和ENDCG 之间的代码是使用 CG/HLSL 编写的,这里的CG/HLSL是Unity经封装后提供的,语法和标准的CG/HLSL几乎一样,但是还有细微的不同,例如有些原生的函数和用法Unity并没有提供支持。
一个简单的表面着色器示例代码:
Shader"Custom/Simple Surface Shader"{
SubShader{
         Tags {
             "RenderType" = "Opaque"
         }
         
         CGPROGRAM
         #pragma surface surf Lambert//使用Lambert
         struct Input{
             float4 color : COLOR;
         };
         void surf(Input IN, inout SurfaceOutput o){
             o.Albedo = 1;
         }
         ENDCG
   }
   Fallback "Diffuse"
}上述Simple Surface Shader得到的材质效果



3.4.2 最聪明的孩子:顶点/片元着色器(Vertext/Fragment Shader)

在Unity中我们可以使用CG/HLSL语言来编写 顶点/片元着色器(Vertext/Fragment Shader),
顶点/片元着色器也是定义在CGPROGRAM和ENDCG之间的,但是它写在Pass语义块内
我们需要自己定义每个Pass需要使用的Shader代码,这也灵活性更高,且我们能够控制渲染的实时细节。
这里的CGPROGRAM和ENDCG之间的代码也是用 CG/HLSL编写的
一个特别简单的顶点片元着色器示例代码:
Shader"Custom/Simple VertexFragment Shader"{
   SubShader{
         Pass{
             CGPROGRAM
            
             #pragma vertex vert
             #pragma fragment frag

             //顶点着色器
             float4 vert(float4 v : POSITION) : SV_POSITION{
               returnmul (UNITY_MATRIX_MVP, v);
             }
             //片元着色器
             fixed4 frag() : SV_Target{
               return fixed4(1.0,1.0,1.0,1.0);
             }
            
             ENDCG
         }
   }
}
3.4.3 被抛弃的角落:固定函数着色器(Fixed Function)

写在Pass语义块中,不定义在CGPROGRAM和ENDCG之间,不使用CG/HLSL语言,而是完全使用ShaderLab语法。不支持可编程管线着色器,所以只能完成一些简单的效果,在Unity5.2中,真正意义上的固定函数着色器已经不存在了。

3.4.4 选择哪种Unity Shader

除非有明确的需求一定要在非常旧的设备上运行游戏,才选择固定函数着色器;若不是请使用可编程管线的着色器表面着色器的性能开销很大需要很多自定义效果,请选择顶点/片元着色器
<hr/>3.6 答疑解惑

3.6.1 Unity Shader != 真正的Shader

Unity Shader 并不等同于第二章所讲的Shader
在Unity里,Unity Shader实际上就是一个ShaderLab文件(硬盘上后缀为.Shader的文件)
在Unity Shader(或者说ShaderLab文件)里,我们可以做的事情远多于一个传统意义上的Shader
传统Shader和Unity Shader的区别:(Unity Shader的优点)
在传统Shader中,我们仅可以编写特定类型的某个Shader,例如顶点着色器、片元着色器。而在Unity Shader中,我们可以在同一个文件里同时包含需要的顶点着色器和片元着色器代码在传统Shader中,我们无法设置一些渲染设置,如是否开启混合、深度测试等,这是开发者在另外的代码中自行设置的。而在Unity Shader中,我们通过一行特定的指令就可以完成这些设置在传统Shader中,我们需要编写冗长的代码来设置着色器的输入和输出,且要小心地处理输入输出的位置对应关系等。而在Unity Shader中,我们只需要在特定语句块中声明一些属性,就可以依靠材质方便地改变这些属性。而对于模型自带的数据(如顶点位置、纹理坐标、法线等),Unity Shader也提供了直接访问的方法,不需要开发者自行编码来传给着色器。
Unity Shader的缺点:
由于 Unity Shader 的高度封装性, 我们可以编写的 Shader 类型和语法都被限制了。对于一些类型的Shader,如曲面细分着色器 (Tessellation Shader) 、几何着色器 (Geometry Shader) 等,Unity的支持就差些。除此之外,一些高级的Shader语法Unity Shader也不支持

可以说, Unity Shader 提供了一种让开发者同时控制渲染流水线中多个阶段的一种方式,不仅仅是提供 Shader 代码。
作为开发者而言,我们绝大部分时候只需要和 Unity Shader 打交道,而 不需要关心渲染引擎底层的实现细节。

3.6.2 Unity Shader 和 CG/HLSL 之间的关系

Unity Shader是用 ShaderLab语言编写的,但对于表面着色器和顶点/片元着色器,我们可以在ShaderLab内部嵌套CG/HLSL语言来编写着色器代码。(只有被淘汰的固定函数着色器不可以嵌套)
这些CG/HLSL代码是嵌套在 CGPROGRAM 和 ENDCG之间 ,由于CG和HLSL从写法上来说是同一种语言,在Unity里,CG和HLSL是等价的,所以我们这里说CG语言
因为表面着色器会被Unity在背后转化成一个包含多 Pass 的顶点/片元着色器,(我们可以在 Unity Shader 的导入设置面板中单击 Show generated code 按钮来查看生成的真正的顶点/片元着色器代码。),所以从本质上来说,表面着色器也是顶点/片元着色器
因此我们说CG的代码片段是位于Pass语义块内部的
Pass{
   //Pass 的标签和状态设置
   
   CGPROGRAM
   //编译指令,例如
   #pragma vertex vert
   #pragma fragment frag
   
   //CG代码
   
   ENDCG
   //其他一些设置

}Unity编译CG片段:
Unity 编译器会把CG片段编译成低级语言,如汇编语言。
Unity会用不同的编译器来把CG转换成对于平台的代码,这样就不会在切换平台时再重新编译 ,而且如果代码在某些平台上发生错误就可以立刻得到错误信息。


当发布游戏的时候, 游戏数据文件中只包含目标平台需要的编译代码, 而那些在目标平台上不需要的代码部分就会被移除。 例如, 当发布到Mac OS X平台上时, DirectX对应的代码部分 就会被移除

3.6.3我可以使用GLSL来写吗

答案是可以
和CG/HLSL需要嵌套在 CGPROGRAM 和 ENDCG 之间类似,GLSL的代码需要嵌套在 GLSLPROGRAM 和 ENDGLSL 之间
但是用GLSL写的话,就意味着你可以发布的目标平台只有:Mac OS X、 OpenGL ES
2.0 或者Linux,而对于PC、Xbox360这样的仅支持DirectX的平台来说,就不可以使用了
<hr/>再次声明:

本文章的学习内容来源全部出自《UnityShader入门精要》——冯乐乐
该文章只是本人我的学习笔记,里面对《UnityShader入门精要》进行了些许概括且加了自己的些许理解
如果想更加具体地了解其内容,建议购买原著进行学习
页: [1]
查看完整版本: 《UnityShader入门精要》学习笔记——第三章 ...