追根溯源

聊了这么多,Shader 究竟是什么?它一直在特定领域被特定人群所使用(游戏、艺术、可视化等)。如你所见,下面并非一个视频或动图(点击左下角播放按钮试试),它正是由 Shader 所绘制,没有引用任何图片资源,仅仅通过数学公式就构建出一个近乎真实的自然世界。

《Snail》from Inigo Quilez

从前端开发视角来看,Shader 不一定是个大众的名词,不过你也许听过 OpenGL 或 WebGL,Shader 就是它们的着色器语言。不妨让我为你们理清一些概念:

OpenGL:英语全称 Open Graphics Library,译为开放图形库或者开放式图形库。是用于渲染 2D、3D 矢量图形的跨语言、跨平台的应用程序编程接口(API)。这个接口由近 350 个不同的函数调用组成,用来从简单的图形比特绘制复杂的三维景象。OpenGL 常用于 CAD、虚拟现实、科学可视化程序和电子游戏开发。与其并存的图形接口还有 Microsoft 的 Direct3D 以及 Khronos Group 的 Vulkan。

—— OpenGL - Wikipedia

OpenGL ES:英文全称 OpenGL for Embedded Systems,是 OpenGL 三维图形 API 的子集,针对手机、PDA 和游戏主机等嵌入式设备而设计。

—— OpenGL ES - Wikipedia

简单总结一下:目前使用广泛的三维图形渲染技术有 Direct3D、Metal、OpenGL、Vulkan 等。Direct3D 仅限微软生态内的 Windows 和 XBOX 等平台,Metal 仅限苹果生态内的 MacOS/iOS 等平台, Vulkan 和 OpenGL 一样跨平台,作为新一代图形接口被寄予厚望。OpenGL 由于其先发优势、开放和免费的特性,在 Mac、Windows、Linux、手机、平板、游戏机等多种平台中都有广泛的应用。OpenGL 提供了一些列标准和接口让我们绘制复杂的二维和三维图形。OpenGL ES 是 OpenGL 的子集,精简了很多无用的旧特性,使它在保持轻量级的同时,仍然能够渲染出精美的三维图形。


PS:自 MacOS 10.14 Mojave(2018.6月)开始苹果宣布不再继续维护 OpenGL/CL 接口转而推行自研的 Metal 引擎。但为了向后兼容依旧维持 OpenGL 在苹果系统中的运行。

而在 Safari 15(跟随 iOS、iPadOS 15 以及 macOS 12 系统于 2021.9 月发布)发布之后,WebGL 2.0 出乎意料地被支持以及默认开启了(其底层由苹果主推的 Metal 引擎来实现),这意味着在未来的一段时期内,OpenGL / WebGL 依旧可以在跨平台和易用性上保持绝对优势:


WebGL:英文全称 Web Graphics Library,是一项用来在网页上绘制和渲染复杂三维图形,并允许用户与之交互的技术。它是一种 JavaScript API,用于在不使用插件的情况下在任何兼容的网页浏览器中呈现交互式 2D 和 3D 图形。WebGL 完全集成到浏览器的所有网页标准中,可将影像处理和效果的 GPU 加速使用方式当做网页 Canvas 的一部分。

—— WebGL - Wikipedia

WebGL 可以理解为 OpenGL ES 在浏览器中的实现,而且 WebGL 在开发下一代富交互用户界面和生产互联网内容上发挥着重要的作用。由于移动终端性能的逐步提升,同时利用了 WebGL 在浏览器中原生支持(无需任何额外插件)而且跨平台的特性,我们将迎来一个全新的视觉和交互时代。

下面的图表很好的展示了 OpenGL、OpenGL ES、WebGL 的发展历史和继承关系:

注意:OpenGL 2.0 的发布是一个里程碑,它支持了一项非常重要的特性,即「可编程着色器方法」(Programmable Shader Functions),该特性被 OpenGL ES 2.0 继承,并成为了 WebGL 1.0 标准的核心部分。

什么是着色器?

为了更好地了解什么是着色器(Shader),我们不妨尝试理解一下 OpenGL 的工作流。设想一下,现在的你就是 OpenGL 的设计者,你的目标就是在电子设备中展示各类图形和三维场景,你会如何把一些抽象的二维或三维图形(如正方形或立方体)转化为屏幕真实可见的图形?

首先我们要面临一个问题:如何定义图形?一些灵感从脑海中浮现,不就是点线面嘛:

例图来自 图解WebGL&Three.js工作原理,下同

似乎我们已经掌握了定义图形的方法,但是必须知道的是,目前这些图形都是抽象的概念。你并不知道它有多大,它在屏幕中会如何放置,所以我们需要把这些图形映射到屏幕中。先定义个坐标系(从 0~1),这样的好处是不管实际屏幕有多大,都可以通过 0~1 来映射。我们常常把坐标或数值转化为 0~1,这个过程称为「归一化」。

最后一步就是将连续矢量的图形数据通过屏幕的物理像素呈现为离散数据。恭喜你,你已经把整个成像流程想通了,OpenGL 标准只是比你多了一些细节(不用着急理解,后面会有文章详细阐述):

Shader 着色器的作用就在这里:它由 顶点着色器 (Vertex Shader)片元着色器 (Fragment Shader) 共同组成,顶点着色器用于定义我们的顶点属性(位置、大小),片元着色器则是在光栅化后进行逐片元的上色操作。如何理解逐片元操作?就是每个像素点都会执行一遍片元着色器的代码!

Shader 的全称是 OpenGL Shading Language,简称 GLSL。GLSL 着色器本身只是简单的字串集,这些字串集会传送到硬件厂商的驱动程序,并从程序内部的 OpenGL API 进入点编译。着色器可从程序内部或读入纯文字档来即时建立,但必须以字串形式传送到驱动程式。OpenGL 2.0 支持的可编程着色器方法让开发者操作像素成为了可能。

—— GLSL - Wikipedia

不同的图形接口都有对应的 Shader 着色器语言,Shader 可以理解为这些着色器语言的统称:

  • OpenGL 的着色器语言为 GLSL
  • OpenGL ES 的着色器语言为 GLSL ES(也被 WebGL 所使用)
  • Direct3D 的着色器语言为 HLSL(High-Level Shading Language)
  • Metal 的着色器语言为 MSL(Metal Shading Language)
  • Vulkan 的着色器语言无指定,可采用 GLSL 或 HLSL 编写,然后转换为 SPIR-V 的二进制中间层格式

听到「逐片元操作」你也许会陷入思考:一个 1080x1920 的屏幕,共有差不多 200w 个像素点,逐片元操作就意味着如果全屏显示一张图片,那就得执行 200w 次着色器代码?如果是渲染 60fps 的动效,每秒计算量将达到 200w * 60 = 1.2 亿次,这需要什么样的机器才能驾驭得了?!

高性能的秘密

为解答上述问题,大家不妨想想:在 PC 电脑里玩大型主机游戏,为何显卡特别的重要,显卡的优劣直接决定了游戏的体验(流畅度、画质等)?这一切都是因为 GPU:

例图来自 techradar

GPU 是显卡的处理器,称为图形处理器(Graphics Processing Unit,即 GPU),又称显示核心、视觉处理器、显示芯片,是一种专门在个人电脑、工作站、游戏机和一些移动设备(如平板电脑、智能手机等)上图像运算工作的微处理器,它是显卡的「心脏」,与 CPU 类似,只不过 GPU 是专为执行复杂的数学和几何计算而设计的,这些计算是图形渲染所必需的。

—— GPU - Wikipedia

在没有 GPU 之前,基本上所有的任务都是交给 CPU 来做的。有 GPU 之后,二者就进行了分工,CPU 负责逻辑性强的事务处理和串行计算,GPU 则专注于执行高度线程化的并行计算(大规模密集运算)。为什么这么分工?这是由二者的硬件构成决定的。

例图来自 geekforgeeks.org

如你所见,GPU 采用了数量众多的计算单元 ALU,相比起 CPU 精简了复杂的控制逻辑 Control 和缓存 Cache,拥有更高吞吐量。为了更形象的表示 CPU 和 GPU 两种不同的架构在图像渲染方面的差异,NVIDIA 在 Youtube 上有一个视频非常直观的表达了它们在图形渲染上的巨大差异:

视频来自 Mythbusters Demo GPU versus CPU

当书法遇到活字印刷,当一个诸葛亮遇到三个臭皮匠,也许这就是 CPU 和 GPU 的差异吧!而 Shader 就是运行在 GPU 上的指令集,在 GPU 的加持下,计算机图形的性能被提到了一个更高的层次。

思维转变

得益于 GPU 的架构设计,通过高密度的微处理器并行地大批量处理计算,甚至一些复杂的数学操作可以直接被微芯片解决,而无须通过软件。

然而这也给代码编写带来了挑战,而且更多的是思维上的挑战。GPU 为了能使每个计算单元并行运行,每一个线程必须相互独立。这就意味着计算单元之间是没有「沟通」的,不存在数据交互,无法检测彼此状态,无法修改输入数据。如不同流水线上的工人,忙碌的重复着独立的工作,甚至不知道旁边的人在做什么。简单来说,即「无沟通」「无状态」「无记忆」。

再以画图作为例子:以往我们以「流式」或者说「面向过程」的思维去绘制图像,比如在一张纸上画一张笑脸,手起笔落,在空中舞动弧线,就画好了。但假设你是 2008 年北京奥运会开幕式的导演,想要控制成百上千的演员去展示活字印刷艺术时,就会遇到一个问题:我怎么让每个演员知道在任意时刻他们应该做什么动作?

这就是我们在通过 Shader 绘制图像时面临的思维难题,摆在你面前的,虽然不是成千上百的人,但的确是成千上百的像素 (pixel),他们由 RGB 三种颜色组成:

例图来自 pixel-anatomy

你只能且唯一能借助的就是像素点的位置时间变量,通过片元着色器,决定每个像素在特定时间内,在屏幕中显示什么颜色。所以我们整个思考方式应该是「动态」且「多维」的。这个过程不容易,需要反复训练。

小结

本节简单地为大家介绍了 OpenGL 的概念、简易渲染流程以及 Shader 在其中的扮演的角色和作用,只为让大家有一个粗略的认识,若存在困惑请别担心,后面将会针对每个知识点进行更细致的阐述。