找回密码
 立即注册
查看: 166|回复: 0

第三章 Unity Shader 基础

[复制链接]
发表于 2022-9-2 21:28 | 显示全部楼层 |阅读模式
Unity提供了一个地方能够让开发者更加轻松地管理着色器代码以及渲染设置(如开启/关闭混合、深度测试、设置渲染顺序等),而不需要像上面的Opengl伪代码一样,管理多个文件和函数等。Unity提供的这个“方便的地方”,就是Unity Shader。
1、Unity Shader概述

1.1 材质和UnityShader

在Unity中我们需要配合使用材质(Material)和Unity Shader才能达到需要的效果。一个最常见的流程是:

  • 创建一个材质;
  • 创建一个Unity Shader,并把它赋给上一步中创建的材质;
  • 把材质赋给要渲染的对象;
  • 在材质面板中调整Unity Shader的属性,以得到满意的效果。


Unity Shader定义了渲染所需的各种代码(如顶点着色器和片元着色器)、属性(如使用哪些纹理等)和指令(渲染和标签设置等),而材质则允许我们调节这些属性,并将其最终赋给相应的模型。
1.2  Unity中的材质

Unity中的材质需要结合一个GameObject的Mesh或者Particle Systems组件来工作。它决定了我们的游戏对象看起来是什么样子的。
材质面板最上方的下拉菜单中选择需要使用的Unity Shader。当选择完毕后,材质面板中就会出现该Unity Shader可用的各种属性。这些属性可以是颜色、纹理、浮点数、滑动条(限制了范围的浮点数)、向量等。当我们把材质赋给场景中的一个对象时,就可以看到调整属性所发生的视觉变化。
Unity Shader本质上就是一个文本文件。和Unity中的很多外部文件类似,Unity Shader也有导入设置(Import Settings)面板,在Project视图中选中某个Unity Shader即可看到。
可以在Default Maps中指定该Unity Shader使用的默认纹理。当任何材质第一次使用该Unity Shader时,这些纹理就会自动被赋予到相应的属性上。
2、Unity Shader的基础:ShaderLab



Unity Shader为控制渲染过程提供了一层抽象。在其帮助下,开发者只需要使用ShaderLab来编写Unity Shader文件就可以完成所有的工作。
在Unity中,所有的Unity Shader都是使用ShaderLab来编写的。ShaderLab是Unity提供的编写Unity Shader的一种说明性语言。它使用了一些嵌套在花括号内部的语义(syntax)来描述一个Unity Shader文件的结构。
ShaderLab定义了要显示一个材质所需的所有东西,而不仅仅是着色器代码
一个Unity Shader的基础结构如下所示:
Shader "Shader Name"{
    Properties{
    //属性
    }
    SubShader{
     //显卡A使用的子着色器
    }
    SubShader {
     //显卡B使用的子着色器
    }
    Fallback "VertexLit"
   }Unity在背后会根据使用的平台来把这些结构编译成真正的代码和Shader文件,而开发者只需要和Unity Shader打交道即可。
3、Unity Shader的结构

讲解UnityShader 的基本语义和用法。
3.1 起名

每个Unity Shader文件的第一行都需要通过Shader语义来指定该UnityShader的名字。字符串表示,/可以表示层级关系。
Shader "Custom/MyShader"{}这个Unity Shader名字叫MyShader,在材质面板的位置是Shader -> Custom ->MyShader


3.2 Properties

Properties是材质和Unity Shader的桥梁。Properties语义块中包含了一系列属性(property),这些属性将会出现在材质面板中。
其实意思就是,Properties语义块为材质定义有哪些属性。
定义一般是:
Properties{
   Name("display name", PropertyType) = DefaultValue
   Name("display name", PropertyType) = DefaultValue
   //更多属性
}开发者们声明这些属性是为了在材质面板中能够方便地调整各种材质属性。就是我们可以在材质面板直接改动这些材质属性,而不一定非要来代码里修改。

  • Name是Shader中访问这些属性的名字
  • displayname是出现在材质面板上的名字
  • PropertyType是每个属性的类型,就像编程里的变量的double,int类型这样
  • DefaultValue是默认值,当把这个Unity Shader赋值给某个材质的时候,最初就显示这些默认值。



Properties语义块支持的属性类型

代码和材质面板示例如下:




为了在Shader中可以访问到这些属性,我们还需要在CG代码片中定义和这些属性类型相匹配的变量。(所以CG代码片和shader啥关系?不是一个东西吗。那CG代码片里的定义,应该也需要跟这些类型的名称一样吗)
就是这里的Name,不能直接在pass 里使用,需要在pass里定义和Name一样的名字,类型对应的类型,才能将属性里的变量与pass里的绑定起来。本质上是属性的作用域与pass的作用域不是一个,需要通讯,所以要绑定。
Properties语义块的作用仅仅是为了让这些属性可以出现在材质面板中。一些属性可以不在Properties语义块声明,而是在代码中直接定义,然后也可以用脚本将其传递给shader。
3.3 SubShader

每一个Unity Shader文件可以包含多个SubShader语义块,但最少要有一个。当Unity需要加载这个Unity Shader时,Unity会扫描所有的SubShader语义块,然后选择第一个能够在目标平台上运行的SubShader。如果都不支持的话,Unity就会使用Fallback语义指定的Unity Shader。
Unity提供这种语义的原因在于,不同的显卡具有不同的能力
SubShader语义块中包含的定义通常如下:
SubShader {
    //可选的
    [Tags]
   
    //可选的
    [RenderSetup]

    Pass{
    }
    // 其它Pass
}每个Pass定义了一次完整的渲染流程,但如果Pass的数目过多,往往会造成渲染性能的下降。因此,我们应尽量使用最小数目的Pass。状态和标签同样可以在Pass声明。
Pass中使用的标签只应用这个Pass,而SubShader进行了这些设置,那么将会用于所有的Pass。
状态设置:
ShaderLab提供了一系列渲染状态的设置指令,这些指令可以设置显卡的各种状态,例如是否开启混合/深度测试等。



常见的渲染状态设置选项

当在SubShader块中设置了上述渲染状态时,将会应用到所有的Pass。在Pass语义块中也可以单独进行上面的设置。
标签:
SubShader的标签(Tags)是一个键值对(Key/Value Pair),它的键和值都是字符串类型。这些键值对是SubShader和渲染引擎之间的沟通桥梁。它们用来告诉Unity的渲染引擎:SubShader我希望怎样以及何时渲染这个对象。
标签的结构如下:
Tags{ "TagName1" = "Value1", "TagName2" = "Value2"}


SubShader的标签类型

上述标签仅可以在SubShader中声明,而不可以在Pass块中声明。Pass块虽然也可以定义标签,但这些标签是不同于SubShader的标签类型。这是我们下面将要讲到的。
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也有自己的标签,但它的标签不同于SubShader的标签。这些标签也是用于告诉渲染引擎我们希望怎样来渲染该物体。


除了上面普通的Pass定义外,Unity Shader还支持一些特殊的Pass,以便进行代码复用或实现更复杂的效果。

  • UsePass:如我们之前提到的一样,可以使用该命令来复用其他UnityShader中的Pass;
  • GrabPass:该Pass负责抓取屏幕并将结果存储在一张纹理中,以用于后续的Pass处理。
3.4 Fallback

紧跟在各个SubShader语义块后面的,可以是一个Fallback指令。它用于告诉Unity, “如果上面所有的SubShader在这块显卡上都不能运行,那么就使用这个最低级的Shader吧!”
语义如下:
Fallback "name"
//或者关掉
Fallback off总结来说,就是一个unity Shader里先是包含一个属性,其中让一些属性声明后,可以显示在材质面板中,然后有许多SubShader,其中有渲染设置和标签,还有pass,pass里面也有渲染设置和标签,还有代码。
4、Unity Shader的形式

Unity Shader最重要的任务还是指定各种着色器所需的代码。这些着色器代码可以写在SubShader语义块中(表面着色器的做法),也可以写在Pass语义块中(顶点/片元着色器和固定函数着色器的做法)。


4.1 表面着色器

表面着色器(Surface Shader)是Unity自己创造的一种着色器代码类型。它需要的代码量很少,Unity在背后做了很多工作,但渲染的代价比较大。当给Unity提供一个表面着色器的时候,它在背后仍旧把它转换成对应的顶点/片元着色器。
表面着色器是Unity对顶点/片元着色器的更高一层的抽象。它存在的价值在于,Unity为我们处理了很多光照细节,使得我们不需要再操心这些“烦人的事情。
一个非常简单的表面着色器示例代码如下:
Shader "Custom/Simple Surface Shader" {
    SubShader {
        Tags {"RenderType" = "Opaque"}
        
        CGPROGRAM
        
        #pragma surface surf Lambert
        
        struct Input {
            float4 color : COLOR;
        };
        
        void surf(Input IN, input SurfaceOutput o) {
            o.Albedo = 1;
        }

        ENDCG
    }
   
    Fallback "Diffuse"
}
表面着色器被定义在SubShader语义块(而非Pass语义块)中的CGPROGRAM和ENDCG之间。表面着色器不需要开发者关心使用多少个Pass、每个Pass如何渲染等问题,Unity会在背后为我们做好这些事情。
我们只需要指定使用这些纹理去填充颜色,使用这个法线纹理去填充法线,使用Lambert光照模型。
CGPROGRAM和ENDCG之间的代码是使用CG/HLSL编写的。
4.2 顶点/片元着色器

在Unity中我们可以使用CG/HLSL语言来编写顶点/片元着色器(Vertex/Fragment Shader)。
一个非常简单的顶点/片元着色器示例代码如下:
Shader "Custom/Simple VertexFragment Shader" {
    SubShader {
        Pass {
            CGPROGRAM
               
            #pragma vertex vert
            #pragma fragment frag
            
            float4 vert(float4 v : POSITION) : SV_POSITION {
                return mul(UNITY_MATRIX_MVP, v);
            }
            
            fixed4 frag() : SV_Target {
                return fixed4(1.0, 0.0, 0.0, 1.0);
            }
               
            ENDCG
        }
    }
}顶点/片元着色器的代码也需要定义在CGPROGRAM和ENDCG之间,但不同的是,顶点/片元着色器是写在Pass语义块内,而非SubShader内的。
原因是,我们需要自己定义每个Pass需要使用的Shader代码。虽然我们可能需要编写更多的代码,但带来的好处是灵活性很高。更重要的是,我们可以控制渲染的实现细节。同样,这里的CGPROGRAM和ENDCG之间的代码也是使用CG/HLSL编写的。
4.3 建议


  • 如果你想和各种光源打交道,你可能更喜欢使用表面着色器,但需要小心它在移动平台的性能表现。
  • 如果你需要使用的光照数目非常少,例如只有一个平行光,那么使用顶点/片元着色器是一个更好的选择。
  • 最重要的是,如果你有很多自定义的渲染效果,那么请选择顶点/片元着色器。
5、杂谈

Unity Shader并不等同于第2章中所讲的Shader。在Unity里,Unity Shader实际上指的就是一个ShaderLab文件——硬盘上以.shader作为文件后缀的一种文件。
在Unity Shader(或者说是ShaderLab文件)里,我们可以做的事情远多于一个传统意义上的Shader。

  • 在传统的Shader中,我们仅可以编写特定类型的Shader,例如顶点着色器、片元着色器等。而在Unity Shader中,我们可以在同一个文件里同时包含需要的顶点着色器和片元着色器代码。
  • 在传统的Shader中,我们无法设置一些渲染设置,例如是否开启混合、深度测试等,这些是开发者在另外的代码中自行设置的。而在UnityShader中,我们通过一行特定的指令就可以完成这些设置。
  • 在传统的Shader中,我们需要编写冗长的代码来设置着色器的输入和输出,要小心地处理这些输入输出的位置对应关系等。而在Unity Shader中,我们只需要在特定语句块中声明一些属性,就可以依靠材质来方便地改变这些属性。而且对于模型自带的数据(如顶点位置、纹理坐标、法线等), Unity Shader也提供了直接访问的方法,不需要开发者自行编码来传给着色器。
Unity Shader是用ShaderLab语言编写的,但对于表面着色器和顶点/片元着色器,我们可以在ShaderLab内部嵌套CG/HLSL语言来编写这些着色器代码。
因此在Unity里CG和HLSL是等价的。我们可以说,CG/HLSL代码是区别于ShaderLab的另一个世界。
通常,CG的代码片段是位于Pass语义块内部的。而在表面着色器中,CG/HLSL代码是写在SubShader语义块内。这是因为,表面着色器就是顶点着色器和片元着色器的抽象。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-6-30 08:20 , Processed in 0.100739 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表