unity shader development[1]

什么是着色器?

直奔问题的核心,着色器两者兼而有之:

  • 在代码中对表面微观层面上发生的事情进行模拟,这使得最终的图像在我们的眼睛看来是真实的。
  • 在 GPU 上运行的一段代码
作为光线模拟的着色器

  为了解释第一个定义,请看图1-1中的三张图片。它们向你展示了由不同材料制成的三个表面。你的大脑可以立即理解一个物体是由什么材料制成的,只需看着它。 这是因为每一种材料与光的相互作用模式对人脑来说都是非常有特点的,是可以识别的。照明着色器模拟了与光的互动,或者利用我们对光的物理学的了解,或者通过大量的试验和错误以及艺术家的努力。
在这里插入图片描述
  在物理世界中,表面是由原子构成的,而光既是波又是粒子。光、表面和我们的眼睛之间的相互作用决定了表面的外观。当来自某个方向的光照射到表面时,它可以被吸收、反射(在另一个方向)、折射(在稍微不同的方向)或散射(在许多不同的方向)。当光线与表面接触时,光线的行为会产生材料的特定外观。参见图 1-2。

在这里插入图片描述
  即使表面在宏观层面上看起来很光滑,就像皮肤一样,在微观层面上,它也可以具有向不同方向散射光的微观面。
  在计算机内部,我们没有将现实模拟到这种详细程度所需的计算能力。如果我们必须模拟整个事物,原子等等,渲染任何东西都需要数年时间。在大多数渲染器中,表面被表示为 3D 模型,这些模型基本上是 3D 空间(顶点)中某个位置的点,然后将它们分组为三角形,然后再次分组以形成 3D 形状。即使是一个简单的模型也可以有数千个顶点。

  我们的 3D 场景由模型、纹理和着色器组成,被渲染为由像素组成的 2D 图像。这是通过将这些顶点位置投影到最终图像中正确的 2D 位置来完成的,同时将任何纹理应用于相应的表面并为 3D 模型的每个顶点和最终图像的每个潜在像素执行着色器。参见图 1-3。
在这里插入图片描述
  无论您在建模时愿意在细节上走多远,都不可能与现实世界中的细节水平相匹配。我们可以使用我们在着色器中照明如何工作的数学知识,使我们的 3D 模型看起来尽可能逼真,弥补无法在更高的细节层次上模拟表面的问题。我们还可以使用它来足够快地渲染我们的场景,以便我们的游戏每秒可以绘制相当数量的帧。适合游戏的每秒帧数范围从 30 fps 到 60 fps,虚拟现实游戏需要超过 60 fps。
  这就是基于物理的渲染的全部内容。它基本上是表面中各种类型的照明行为以及我们用来近似它们的数学模型的目录。

渲染为透视画法

  渲染在概念上(和数学上)与画家使用透视从生活中绘制到画布中的过程非常相似。
  透视图技术起源于 500 多年前的意大利文艺复兴时期,即使它的数学基础早在欧几里德时代奠定。在我们的例子中,画布是我们的最终图像,场景和 3D 模型是现实,而画家是我们的渲染器。
  在计算机图形学中,渲染场景的方法有很多种,有些计算成本更高,有些则更少。快速类型(基于光栅化器)是实时渲染(包括游戏)一直在使用的类型。慢速类型(光线追踪等)是 3D 动画电影通常使用的类型,因为渲染时间可以达到每帧数小时。
  快速渲染器的渲染过程可以这样简化:首先将场景中模型的形状投影到最终的 2D 图像中;从我们隐喻画家的角度来看,我们称之为“勾画轮廓”阶段。然后使用着色器中实现的光照计算填充每个轮廓中包含的像素;我们称之为“绘画”阶段。
  您可以在不使用着色器的情况下渲染图像,我们曾经这样做过。在可编程图形管线出现之前,渲染是通过 API 调用(OpenGL 和 DirectX3D 等 API)进行的。为了获得更好的速度,API 将为您提供预制函数,您可以向其传递参数。它们是在硬件中实现的,因此无法修改它们。它们被称为固定功能渲染管道。
  为了使渲染器更加灵活,引入了可编程图形管道。有了它,您可以编写称为着色器的小程序,这些程序将在 GPU 上执行,以代替大部分固定功能的功能。

渲染过程

  如前所述,这种类型的渲染在概念上可以分为两个阶段:

  • 轮廓阶段
  • 绘画阶段

  轮廓阶段确定最终图像中的哪些像素将属于某个三角形,方法是将模型的顶点投影到最终图像中,并从相机的角度检查是否有另一个模型在前面。绘画阶段根据场景数据(灯光、纹理和灯光计算)计算每个像素的颜色。
  第一阶段处理顶点,第二阶段处理从第一阶段获得的信息并输出像素颜色。

着色器作为在 GPU 上运行的代码

  如前所述,模型中可能有数千个顶点,渲染图像可能有数百万个像素。游戏场景的复杂程度因它们将要运行的平台而异。在PlayStation 4 Pro上,最终图像分辨率达到3840×2160像素(俗称4k分辨率),一个场景可以有几十万个以上的顶点。通常,着色器将在场景中的每个顶点以及最终图像中的每个像素上运行。为了实现这种实时渲染速度,我们需要一个特殊的处理器,它能够在几毫秒内运行数百万次非常短的程序。这种处理器通常称为图形处理单元,或 GPU。
  着色是一个方向的数据流过程,这意味着顶点、纹理和着色器进入,然后在另一端,颜色退出,并被放入渲染目标,这意味着基本上是 2D 图像。我们不需要知道我们正在处理的顶点附近的顶点,或者我们正在计算的顶点附近的像素(至少大部分时间),因此所有这些着色器都可以独立执行,同时时间,在大量顶点/像素上。

着色器的执行

图 1-4 显示了如何渲染一个简单的场景。
在这里插入图片描述
  该场景有八个顶点,并已渲染为 1920x1080 图像(全高清分辨率)。渲染过程中到底发生了什么?

  1. 场景的顶点和它们各自的数据被传递到顶点着色器。
  2. 顶点着色器在它们中的每一个上执行。
  3. 顶点着色器从每个顶点产生一个输出数据结构,包含最终图像上顶点的颜色和位置等信息。
  4. 顶点序列被组装成图元,例如三角形、线、点等。出于本书的目的,我们将假设三角形。
  5. 光栅化器接受一个图元并将其转换为一个像素列表。对于该三角形内的每个潜在像素,该结构的值被内插并传递给像素着色器。 (例如,如果一个顶点为绿色,相邻顶点为红色,则它们之间的像素会形成绿色到红色的渐变。)光栅化器是 GPU 的一部分;我们无法定制它。
  6. 片段着色器针对任何潜在像素运行。这是对我们来说更有趣的阶段,因为大多数光照计算发生在片段着色器中。
  7. 如果渲染器是前向渲染,对于第一个之后的每个灯光,片段着色器将再次运行,并使用该灯光的数据。
  8. 每个潜在的像素(又称片段)都会被检查是否有另一个靠近摄像机的潜在像素,因此在当前像素的前面。如果有,该片段将被拒绝。
  9. 所有片段着色器光通道都混合在一起。
  10. 所有像素颜色都写入渲染目标(可以是屏幕、纹理或文件等)

  正如你在图 1-5 中看到的,这个立方体有上色的顶点。阴影立方体中从黑色到灰色的渐变是由于步骤 4 中发生的插值。
在这里插入图片描述
  这是渲染场景时如何执行着色器的概述。现在我们将更准确地定义一些我之前提到的技术术语。

不同类型的着色器

我们已经提到了几种类型的着色器。他们在这里,还有一些其他的:

  • 顶点着色器:在每个顶点上执行。
  • 片段着色器:针对每个可能的最终像素(称为片段)执行。
  • Unlit 着色器:仅限 Unity,一种将顶点着色器和像素着色器合并到一个文件中的着色器。
  • 表面着色器:仅限于 Unity,包含顶点着色器和片段着色器功能,但利用 ShaderLab 对 Cg 着色语言的扩展来自动化照明着色器中常用的一些代码。
  • 图像效果着色器:仅限 Unity,用于应用模糊、泛光、景深、颜色分级等效果。它通常是渲染上运行的最后一个着色器,因为它应用于场景几何体的渲染。
  • 计算着色器:计算任意计算,不一定是渲染,例如物理模拟、图像处理、光线跟踪,以及一般可以轻松分解为许多独立任务的任何任务。在本书中,我们在 Unity 表面着色器上花费了大量时间,但不会涉及计算着色器。还有更多类型的着色器,但由于它们不经常使用,我们不会提及它们。
坐标系

  着色器中的每个计算都存在于特定的坐标系中。想想笛卡尔坐标系——几乎每个人都曾经处理过它。那是一个 2D 坐标系,而许多用于渲染计算的坐标系是 3D 渲染系统。这是一个列表:

  • 局部(或对象)空间:相对于正在渲染的模型的 3D 坐标系
  • 世界空间:相对于正在渲染的整个场景的 3D 坐标系
  • 视图(或眼睛)空间:相对于观察者视角(您正在渲染的相机)的 3D 坐标系
  • 剪辑空间:范围为 -1.0 到 1.0 的 3D 坐标系
  • 屏幕空间:相对于渲染目标(屏幕等)的 2D 坐标系
  • 切线空间:用于法线贴图

  我们偶尔会提到坐标空间。了解它们很重要,尽管大多数时候,您不需要直接与它们打交道。很容易混淆哪个空间用于哪个计算,因此学习识别每个空间以及每个空间何时有用是值得的。
  渲染管线的各个阶段在两个空间之间进行转换,以便在最合适的空间中执行计算。选择正确的坐标系可以使计算更简单,计算成本更低。

光的类型

  在自然界中,每束光都是从 3D 表面发出的。没有现实生活中的像素这样的东西。在渲染中,我们使用近似值来降低所需的计算能力,但这些近似值会限制渲染的保真度。在 Unity 中,我们使用三种不同的真实灯光近似值(见图 1-6):

  • 点光源
  • 定向光
  • 区域光(仅用于烘焙光照贴图)

在这里插入图片描述

点光源

  想想一盏夜灯,它是一种向四周发射光线的小灯,但这些光线不会到达很远的地方。点光源有衰减,这意味着在一定距离处,光完全消失。

定向光

  想想太阳;它离我们太远了,即使是点光源,到达我们的所有光线都是平行的。如果您要放大非常近的点光源的光,您将到达可见光线全部平行的点。因此,我们不会达到衰减,因此定向光会无限点亮。

区域光

  对于物理现实,区域光是三者的最佳近似,但计算成本更高。任何发光的真实物体很可能是立体的,因此有一个区域。但这会使渲染计算复杂化,从而使它们更加昂贵。 Unity 没有可用于实时照明的区域光;它只能在烘焙光照贴图时使用。

渲染方程

  我们想要在光照着色器中实现的计算可以用渲染方程表示:在这里插入图片描述
  不要惊慌!这只是一个无害的等式。此时您无需了解任何相关信息。但它会帮助你习惯看到它而不会颤抖。这实际上是一种非常有用的方法,可以将计算照明所需的一切放在一行中。
  后面的章节更详细地介绍了这个等式。在这里,我们将概述它所代表的含义,即光在表面上的行为。

光的行为

  我们所看到的一切,我们之所以能看到,是因为有一些光线击中了那个物体,并从它身上反射出来,朝着我们眼睛的方向。弹跳发生的确切方式和原因对于渲染非常重要。多少光会从表面反射,以及向哪个方向反射,取决于许多因素:

  • 光线来自的角度(也称为反射)。它越平行,反弹越少。参见图 1-7。

在这里插入图片描述

  • 表面的颜色(又名,吸收)。光谱包括所有可见颜色。红色表面会吸收光谱中的所有其他颜色,只反射红色部分。参见图 1-8。

在这里插入图片描述

  • 表面的光滑度或粗糙度。在微观层面,表面可能比看起来更粗糙,而微表面可以向不同方向反射光。参见图 1-9。
    在这里插入图片描述
  • 半透明层。想想一个水坑。它下面的地面看起来比干燥的地面更暗,因为水面反射了一些光线,因此到达地面的光线较少。请参见图 1-10。
    在这里插入图片描述
反射光

  正如您可能想象的那样,从一个表面反射的光通常最终会照射到另一个表面,而后者又会反射其中的一部分。反弹的光会一直反弹,直到能量完全耗尽。这就是全局照明所模拟的。
  直接照射到表面的光称为直射光;从另一个表面反射后照射到物体上的光称为间接光。为了更清楚,图 1-11 和图 1-12 两次显示了相同的场景。在图 1-11 中,只有直接照明,而图 1-12 也使用间接照明进行渲染。如您所见,差异非常明显。

在这里插入图片描述
  在 2010 年之前,游戏通常使用非常粗略的全局照明近似值,例如环境光,它只包含整个场景的一个值。球谐函数也用于近似 GI。它们是比环境更可靠的近似值,但更昂贵且数学上也更复杂。

渲染器类型

  渲染器有几种类型,它们之间还有很多混合体。根据游戏的艺术方向,场景的某些部分会比其他部分花费更多的时间来渲染。因此,就需要改变渲染器,以优化更耗时的部分。

Forward

  这是第一种类型的实时渲染器。它曾经在图形 API(OpenGL 或 Directx3D)中实现。这是我们之前讨论过的类型。场景信息被输入其中,每个三角形都被光栅化,对于每个灯光,都有一个着色通道。

Deferred

  如果没有全局照明,渲染更自然场景的唯一方法是使用大量灯光。这在 PS3/Xbox360 一代开始成为普遍做法。但是,如您所知,前向渲染器中的每一个附加光都意味着对所有像素进行额外的着色器传递。
  为了获得更好的性能,发明了这种新型渲染器,它将场景的着色推迟到最后一刻。这允许它忽略当前未到达正在着色的模型的灯光,从而获得更好的性能。
  延迟渲染器有一些问题点,例如无法正确渲染透明对象。它们也不太灵活,因为在开发渲染器时必须事先决定传递到着色阶段的信息。

Forward+(平铺正向着色)

  在 PS4/Xbox One 一代中,可以使用各种近似的全局照明,这使得 Deferred 的吸引力降低。 Forward 和 Deferred 的混合,这种渲染器类型将图像分解成瓦片,这些瓦片用 Forward 方法着色,但只考虑影响当前瓦片的灯光。此渲染器类型目前在 Unity 中不可用。

未来的渲染器

  该行业似乎正在开发更灵活的渲染器,可以更好地针对每个游戏的需求进行定制。 Unity 已经在开发可编写脚本的渲染循环,这将允许您自己编写渲染代码,而目前您只能在 Forward 和 Deferred 之间进行选择。此新功能已在当前的 Unity 测试版中可用。

Shader Visual Graphs

  您之前的着色器开发经验可能是通过节点编辑器获得的。许多游戏引擎和 3D 建模软件程序都可以通过可视化节点界面进行着色器开发。图 1-13 以 Unreal 的着色器编辑器为例。
在这里插入图片描述
  它们是开发着色器的一种直观的方式。Unity不包括这样的工具(尽管你可以从资产商店下载ShaderForge和其他工具),虽然它们在很多方面都很好,但很难用节点编辑器来开发自定义的照明着色器。由于这些工具无论如何都会生成代码,而且有时不是好的代码,所以非常值得学习手工编写着色器代码。即使你最终用ShaderForge生成了你的着色器,通过学习手工编码着色器,你将能够编辑生成的代码以提高性能或修复错误和编译器错误。

总结

  本章介绍了着色器,使程序员能够进一步自定义图形管道,这在过去是固定的。图形程序员会将场景数据传递给它,并且只能为其提供预定参数。如今,我们正朝着越来越多的可定制性迈进,因为它让有能力的人有可能编写更快的渲染代码。
  着色器是在 GPU 上运行的代码,可以模拟不同表面的光照。它们还可用于图像效果(例如,模糊)、曲面细分和一般计算。我们将在本书中坚持照明和图像效果。
  如果您只使用内置着色器,那么了解如何编写着色器将使您能够更好地控制游戏的外观。

  现在您已经了解了着色器开发的基础知识,您将立即将它们付诸实践。下一章将向您展示如何创建您的第一个 Unity 着色器,同时熟悉着色器开发工作流程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值