《Unity Shader入门精要》读书笔记1

图例多来自书中

渲染流水线

在这里插入图片描述

  • 应用阶段:CPU,准备工作通过CPU设置渲染状态,用以后续指导GPU如何渲染
  • 几何阶段:GPU,决定需要绘制的图元是什么,以及怎么绘制
  • 光栅化阶段:GPU,产生最终显示在屏幕上的像素

应用阶段

应用阶段是渲染流水线的起点,主要分为三个子阶段:

  1. 把数据加载到显存(VRAM)中
  2. 设置渲染状态
  3. 调用Draw Call

数据的加载:HDD->RAM->VRAM
加载的数据:顶点的位置信息、法线方向、顶点颜色、纹理坐标等

设置渲染状态:定义网格怎样被渲染,包括使用哪个顶点着色器/片元着色器、光源属性、材质等

做好上述准备工作CPU就需要调用一个渲染命令来告诉GPU去渲染

Draw Call:CPU作为发起方、GPU作为接收方的一个命令,该命令仅会包含一个指向需要被渲染的图元的列表(材质信息等渲染的细节上面的工作已经设置好了)

接收到Draw Call后,GPU根据渲染状态和输入的顶点数据进行计算,最终产生屏幕上的像素。该过程就是GPU流水线

几何阶段

应用阶段之后的两个阶段,即几何阶段和光栅化阶段,开发者没有绝对的控制权,只是GPU向开发者开放了很多控制权

在这里插入图片描述
上图颜色表示了不同阶段的可配置性或可编程性:绿色表示该流水线阶段是完全可编程控制的,黄色表示该流水线阶段可以配置但不是可编程的,蓝色表示该流水线阶段是由 GPU 固定实现的,开发者没有任何控制权。实线表示该 Shader 必须由开发者编程实现,虚线表示该 Shader 是可选的。

齐次裁剪空间,参考这篇文章:计算机图形学补充2:齐次空间裁剪(Homogeneous Space Clipping)

在这里插入图片描述

归一化的设备坐标:Normalized Device Coordinates, NDC

注:OpenGL和Unity的NDC中Z分量范围 [ − 1 , 1 ] [-1,1] [1,1],而DirectX的NDC的Z分量范围 [ 0 , 1 ] [0,1] [0,1]

裁剪

裁剪有三种情况:

  • 完全在视野外
  • 完全在视野内
  • 部分再视野内

例如一条线段(两个顶点构成)如果一个在视野外,一个在视野内,则视野外的顶点应该由线和视野边界的交界点作为新顶点代替

在这里插入图片描述

屏幕映射

把每个图元的x和y(不包括z)转化到屏幕坐标系。

屏幕坐标系和z坐标构成了窗口坐标系,这些值会传递到光栅化阶段使用。

在这里插入图片描述

光栅化阶段

两个最重要的目标:

  • 计算图元覆盖了哪些像素
  • 为这些像素计算它们的颜色

三角形遍历

检查每个像素是否被一个三角网格所覆盖,如果覆盖的话就会生成一个片元。这个找到哪些像素被三角形网格覆盖的过程就是三角形遍历,这个阶段也被称之为扫描变换

片元着色器

在DirectX中也被称为像素着色器。

最重要的是进行纹理采样

局限是仅可影响单个片元。例外是片元着色器可以访问到导数信息。

逐片元操作

是渲染流水线的最后一步,是高度可配置的

在这里插入图片描述

逐片元操作的主要工作:

  • 决定每个片元的可见性,这涉及到很多测试工作,如深度测试和模板测试
  • 如果一个片元通过了所有测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色值进行合并,或者说是混合

下图是两种测试的流程

在这里插入图片描述

逐片元操作可能因为不通过测试而舍弃某个片元,即为这个片元所做的工作白费

模板测试的高级用法:渲染阴影、轮廓渲染

深度测试:类似于Zbuffer的功能。将深度测试提前执行的技术叫做Early-Z技术

不透明的物体不需要混合,但是半透明的物体需要混合。混合很像PS对图层的操作,如正片叠底等。

在这里插入图片描述

提前进行测试(如果条件允许)是可以避免在要舍弃的片元上花功夫的,这样可以不用片元着色器计算它们的颜色,节省资源。即“尽早可能地知道哪些片元要被舍弃”

使用双缓冲策略来避免看到正在光栅化的图元。

关于OpenGL和DirectX

都是图像应用编程接口,需要显卡驱动翻译成GPU能听懂的语言,显卡驱动还负责把纹理等数据转化为GPU支持的格式,可以说是GPU的操作系统。

整个过程就是:应用程序向接口发送渲染命令,接口向显卡驱动发送渲染命令,显卡驱动再翻译给GPU

先把配置好的数据传到显存中,然后再调用Draw Call。

在这里插入图片描述

着色器语言

DirectX用HLSL(High Level Shading Language)
OpenGL用GLSL(OpenGL Shading Language)
NVIDIA用CG(C for Graphic)

这些语言会被编译成汇编语言,也称为中间语言(Intermediate Language, IL),然后经由显卡驱动翻译成真正的GPU可以理解的机器语言

GLSL优点在于它的跨平台性,但是由于OpenGL只是接口,其实现各不相同,这也意味着 GLSL 的编译结果将取决于硬件供应商。

HLSL是微软的,即使硬件不同,同一个着色器的编译结果也是一样的(前提是版本相同)

CG 语言可以无缝移植成 HLSL 代码。但缺点是可能无法完全发挥出 OpenGL 的最新特性。

简言之:

  • GLSL跨平台但是依赖硬件实现
  • HLSL不依赖硬件实现,但支持它的平台多是微软自家产品
  • CG是真正意义的跨平台

Draw Call

  • OpenGL 中的 glDrawElements 命令
  • DirectX 中的 DrawIndexedPrimitive 命令

一个常见的误区是,Draw Call 中造成性能问题的元凶是 GPU,认为 GPU 上的状态切换是耗时的,其实不是的,真正「拖后腿」其实的是 CPU。

提交一次Draw Call对于CPU要进行很多准备工作,例如检查渲染状态等。而一旦 CPU 完成了这些准备工作,GPU 就可以开始本次的渲染。如果 Draw Call 的数量太多,CPU 就会把大量时间花费在提交 Draw Call 上,造成 CPU 的过载。10次Draw Call就有10次准备工作,但是合并为一次Draw Call就只用进行1次准备工作(处理的总量是不变的)

所以减少Draw Call方法之一就是合并Draw Call,这是一种批处理的思想,且更适合静态物体。

合并网格就是其中要做的一种事情,前提是这些网格都有相同的渲染状态

在游戏开发过程中,为了减少 Draw Call 的开销,有两点需要注意:

  1. 避免使用大量很小的网格。当不可避免地需要使用很小的网格结构时,考虑是否可以合并它们。
  2. 避免使用过多的材质。尽量在不同的网格之间共用同一个材质。

CPU和GPU的并行

使用命令缓冲区(Command Buffer)避免因双方速度差异造成的等待,这在计算机组成原理中讲的也有所体现。

在这里插入图片描述
命令缓冲区中的命令有很多种类,而 Draw Call 是其中一种,其他命令还有改变渲染状态等(例如改变使用的着色器,使用不同的纹理等)

关于固定渲染流水线

固定函数的流水线(Fixed-Function Pipeline),也简称为固定管线,通常是指在较旧的 GPU 上实现的渲染流水线。这种流水线只给开发者提供一些配置操作,但开发者没有对流水线阶段的完全控制权。

一个形象的比喻是,我们在使用固定管线进行渲染时,就好像在控制电路上的多个开关,我们可以选择打开或者关闭一个开关,但永远无法控制整个电路的排布。

3D API 最后支持固定管线的版本 第一个支持可编程管线的版本
OpenGL 1.5 2.0
OpenGL ES 1.1 2.0
DirectX 7.0 8.0

更多细节

书籍:Real Time Rendering

在这里插入图片描述

OpenGL:Rendering Pipeline Overview

DirectX:[MSDN]图形管道

Unity Shader

Unity Shader 本质上就是一个文本文件

Shader所在的阶段就是渲染流水线的一部分,也是渲染流水线的特色。依靠着色器我们可以控制流水线中的渲染细节。

Unity 作为一个出色的编辑工具,为我们提供了一个既可以方便地编写着色器 ,同时又可设置渲染状态的地方:Unity Shader

Unity Shader和上面说的渲染管线的Shader有很大不同

材质和Shader相辅相成,材质要和Shader配套结合使用,一个最常见的流程是:

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

在这里插入图片描述

Unity 一共提供了 4 种 Unity Shader 模板供我们选择:

  • Standard Surface Shader 会产生一个包含了标准光照模型(使用了 Unity 5 中新添加的基于物理的渲染方法)的表面着色器模板
  • Unlit Shader 则会产生一个不包含光照(但包含雾效)的基本的顶点/片元着色器
  • Image Effect Shader 则为我们实现各种屏幕后处理效果提供了一个基本模板
  • Compute Shader 会产生一种特殊的 Shader 文件,这类 Shader 旨在利用 GPU 的并行性来进行一些与常规渲染流水线无关的计算

Compute Shader不在这本书的讨论范围之内,但是可以参考官方文档的介绍:计算着色器

由于本书的重点在于如何在 Unity 中编写顶点/片元着色器,因此在后续的学习中,我们通常会使用 Unlit Shader 来生成一个基本的顶点/片元着色器模板。

在这里插入图片描述
在 Project 视图中选中某个 Unity Shader 即可看到Unity Shader的导入设置 (Import Settings)面板

可以在 Default Maps 中指定该 Unity Shader 使用的默认纹理。当任何材质第一次使用该 Unity Shader 时,这些纹理就会自动被赋予到相应的属性上。

在下方的面板中,Unity 会显示出和该 Unity Shader 相关的信息,例如它是否是一个表面着色器(Surface Shader)、是否是一个固定函数着色器(Fixed Function Shader)等,还有一些信息是和我们在 Unity Shader 中的标签设置有关,例如是否会投射阴影、使用的渲染队列、LOD 值等。

Shader Lab

在 Unity 中,所有的 Unity Shader 都是使用 ShaderLab 来编写的。

Shader Lab 是 Unity 提供的编写 Unity Shader 的一种说明性语言。它使用了一些嵌套在花括号内部的语义(syntax)来描述一个 Unity Shader 文件的结构。这些结构包含了许多渲染所需的数据,例如Properties语句块中定义了着色器所需的各种属性,这些属性将会出现在材质面板中。

从设计上来说,ShaderLab 类似于 CgFX 和 Direct3D Effects(.FX)语言,它们都定义了要显示一个材质所需的所有东西,而不仅仅是着色器代码。

Shader "ShaderName" {
    
    
    Properties {
    
    
        // 属性 
    }
    SubShader {
    
    
        // 显卡 A 使用的子着色器 
    }
    SubShader {
    
    
        // 显卡 B 使用的子着色器 
    }    
    Fallback "VertexLi"
}

Shader命名

上面的ShaderName所在位置就可以换成自己指定的名称,例如

Shader "Unlit/partice1"{
    
    }

这样在一个材质配置面板,为它选择Shader时,就可以在下拉菜单的Unlit子菜单中找到。

在这里插入图片描述

Shader属性

属性是材质和 Unity Shader 的桥梁

Properties 语义块中包含了一系列 属性 (property),这些属性将会出现在材质面板中。

属性的基础定义方式是:

Properties {
    
    
    Name ("display name", PropertyType) = DefaultValue
    Name ("display name", PropertyType) = DefaultValue
    // 更多属性 
}

开发者们声明这些属性是为了在材质面板中能够方便地调整各种材质属性。

如果我们需要在 Shader 中访问它们,就需要使用每个属性的名字

在 Unity 中,这些属性的名字通常由一个下划线开始。显示的名称 (display name)则是出现在材质面板上的名字。

我们需要为每个属性指定它的类型(PropertyType),常见的属性类型下表所示。除此之外,我们还需要为每个属性指定一个默认值,在我们第一次把该 Unity Shader 赋给某个材质时,材质面板上显示的就是这些默认值。

属性类型 默认值的定义语法 例子
Int number _Int (“Int”, Int) = 2
Float number _Float (“Float”, Float) = 1.5
Range(min, max) number _Range(“Range”, Range(0.0, 5.0)) = 3.0
Color (number,number,number,number) _Color (“Color”, Color) = (1,1,1,1)
Vector (number,number,number,number) _Vector (“Vector”, Vector) = (2, 3, 6, 1)
2D “defaulttexture” {} _2D (“2D”, 2D) = “” {}
Cube “defaulttexture” {} _Cube (“Cube”, Cube) = “white” {}
3D “defaulttexture” {} _3D (“3D”, 3D) = “black” {}

对于 Int 、Float 、Range 这些数字类型的属性,其默认值就是一个单独的数字

对于 Color 和 Vector 这类属性,默认值是用圆括号包围的一个四维向量

对于 2D 、Cube 、3D 这 3 种纹理类型,默认值的定义稍微复杂,它们的默认值是通过一个字符串后跟一个花括号来指定的,其中,字符串要么是空的,要么是内置的纹理名称,如"white"、“black”、“gray"或者"bump”。

花括号的用处原本是用于指定一些纹理属性的,例如在 Unity 5.0 以前的版本中,我们可以通过 TexGen CubeReflectTexGen CubeNormal等选项来控制固定管线的纹理坐标的生成。但在 Unity 5.0 以后的版本中,这些选项被移除了,如果我们需要类似的功能,就需要自己在顶点着色器中编写计算相应纹理坐标的代码。

这是一个展示所有属性定义方式的例子

Shader "Custom/ShaderLabProperties" {
    
    
    Properties {
    
    
    	_MainTex ("Texture", 2D) = "white" {
    
    }
        // 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"
}

在shader中添加了上面例子的属性后,可以在使用了该shader的材质中直接看到:

在这里插入图片描述

Properties 语义块的作用仅仅是为了让这些属性可以出现在材质面板中。

有时,我们想要在材质面板上显示更多类型的变量,例如使用布尔变量来控制 Shader 中使用哪种计算。Unity 允许我们重载默认的材质编辑面板,以提供更多自定义的数据类型。

可参阅官方手册:ShaderLab:指定自定义编辑器

SubShader

每一个 Unity Shader 文件可以包含多个 SubShader 语义块,但最少要有一个。当 Unity 需要加载这个 Unity Shader 时,Unity 会扫描所有的 SubShader 语义块,然后选择第一个能够在目标平台上运行的 SubShader 。如果都不支持的话,Unity 就会使用 Fallback 语义指定的 Unity Shader。

Unity 提供这种语义的原因在于,不同的显卡具有不同的能力。例如,一些旧的显卡仅能支持一定数目的操作指令,而一些更高级的显卡可以支持更多的指令数,那么我们希望在旧的显卡上使用计算复杂度较低的着色器,而在高级的显卡上使用计算复杂度较高的着色器,以便提供更出色的画面。出色的画面。

SubShader 语义块中包含的定义通常如下:

SubShader {
    
    
    // 可选的标签
    [tags]
    
    // 可选的状态
    [RenderSetup]

    Pass {
    
    
    }
    // Other Passes
}

SubShader中定义了一系列Pass以及可选的状态和标签设置。

Tags

Tags是键值对,它的键和值都是字符串类型。这些键值对是 SubShader 和渲染引擎之间的沟通桥梁。它们用来告诉 Unity 的渲染引擎:我希望怎样以及何时渲染这个对象。

格式如下:

Tags {
    
     "TagName1" = "Value1" "TagName2" = "Value2" }

标签类型如下表:

标签类型 说明 例 子
Queue 控制渲染顺序,指定该物体属于哪一个渲染队列,通过这种方式可以保证所有的透明物体可以在所有不透明物体后面被渲染(详见第 8 章),我们也可以自定义使用的渲染队列来控制物体的渲染顺序 Tags { “Queue” = “Transparent” }
RenderType 对着色器进行分类,例如这是一个不透明的着色器,或是一个透明的着色器等。这可以被用于着色器替换(Shader Replacement)功能 Tags { “RenderType” = “Opaque” }
DisableBatching 一些 SubShader 在使用 Unity 的批处理功能时会出现问题,例如使用了模型空间下的坐标进行顶点动画(详见 11.3 节)。这时可以通过该标签来直接指明是否对该 SubShader 使用批处理 Tags { “DisableBatching” = “True” }
ForceNoShadowCasting 控制使用该 SubShader 的物体是否会投射阴影(详见 8.4 节) Tags { “ForceNoShadowCasting” = “True” }
IgnoreProjector 如果该标签值为「True」,那么使用该 SubShader 的物体将不会受 Projector 的影响。通常用于半透明物体 Tags { “IgnoreProjector” = “True” }
CanUseSpriteAtlas 当该 SubShader 是用于精灵(sprites)时,将该标签设为「False」 Tags { “CanUseSpriteAtlas” = “False” }
PreviewType 指明材质面板将如何预览该材质。默认情况下,材质将显示为一个球形,我们可以通过把该标签的值设为「Plane」「SkyBox」来改变预览类型 Tags { “PreviewType” = “Plane” }

需要注意的是,上述标签仅可以在 SubShader 中声明,而不可以在 Pass 块中声明。Pass 块虽然也可以定义标签,但这些标签是不同于 SubShader 的标签类型。

状态设置

状态设置用来设置显卡的各种状态,常见选项如下表

在这里插入图片描述

Pass

对于Pass,每个 Pass 定义了一次完整的渲染流程,但如果 Pass 的数目过多,往往会造成渲染性能的下降。因此,我们应尽量使用最小数目的 Pass 。

状态和标签同样可以在 Pass 声明。不同的是,SubShader 中的一些标签设置是特定的。也就是说,这些标签设置和 Pass 中使用的标签是不一样的。而对于状态设置来说,其使用的语法是相同的。但是,如果我们在 SubShader 进行了这些设置,那么将会用于所有的 Pass

Pass语义块长这样

Pass {
    
     
    [Name]
    [Tags] 
    [RenderSetup] 
    // Other code
}

定义Pass名称方法如下:

Name "MyPassName"

可以通过UsePass "MyShader/MYPASSNAME"这种格式的代码来使用别的Shader的Pass

使用 UsePass 命令时必须使用大写形式的名字,因为 Unity 内部会把所有 Pass 的名称转换成大写字母的表示

Tags和RenderSetup的设置同SubShader。此外在 Pass 中我们还可以使用固定管线的着色器命令。

Tags使用的标签如下表:

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

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

Fallback

类似C语言的switch语句里的default。

在有Fallback的情况下,如果上面所有的SubShader在这块显卡上都不能运行,那么只能运行Fallback里的内容。

如果不写Fallback,则如果上面所有的SubShader在这块显卡上都不能运行,那就不再管它了。

事实上,Fallback 还会影响阴影的投射。在渲染阴影纹理时,Unity 会在每个 Unity Shader 中寻找一个阴影投射的 Pass。通常情况下,我们不需要自己专门实现一个 Pass,这是因为 Fallback 使用的内置 Shader 中包含了这样一个通用的 Pass。因此,为每个 Unity Shader 正确设置 Fallback 是非常重要的。

其他

还有其他一些语义,如:

  • 使用 CustomEditor 语义来扩展编辑界面
  • 使用 Category 语义来对 Unity Shader 中的命令进行分组

猜你喜欢

转载自blog.csdn.net/qq_39377889/article/details/128487266