什么是着色器?
着色器是功能强大的程序,最初用于为 3D 场景中的对象着色。如今,着色器有多种用途。着色器程序通常在计算机的图形处理单元 (GPU) 上运行,它们可以在其中并行运行。
高级着色语言 (HLSL)和OpenGL着色语言 (GLSL)等着色器语言是用于对 GPU 的渲染管道进行编程的最常用语言。这些语言的语法类似于C 编程语言。
当你玩诸如 Minecraft 之类的游戏时,从 2D 屏幕(即你的计算机显示器或手机屏幕)查看世界时,着色器用于使世界看起来像 3D。着色器还可以通过调整光与对象的交互方式或对象在屏幕上的渲染方式来彻底改变游戏的外观。
你通常会看到着色器有两种形式:顶点着色器和片段着色器。顶点着色器用于创建各种对象的 3D 网格的顶点,例如球体、立方体、3D 游戏的主角等。来自顶点着色器的信息被传递给几何着色器,几何着色器然后可以操作这些顶点或者在片段着色器之前执行额外的操作。通常不需要关注几何着色器。管道的最后一部分是片段着色器。片段着色器计算像素的最终颜色并确定是否应该向用户显示像素。
image.png
在threejs中通常围绕顶点着色器和片段着色器进行材质的高级定制
在Unity或Unreal等游戏引擎中,顶点着色器和片段着色器大量用于 3D 游戏。Unity 在着色器之上提供了一个名为ShaderLab的抽象,这是一种位于 HLSL 之上的语言,可帮助您更轻松地为游戏编写着色器。此外,Unity 提供了一个名为Shader Graph的可视化工具,让您无需编写代码即可构建着色器。如果您在 Google 上搜索“Unity 着色器”,您会发现数百个执行许多不同功能的着色器。您可以创建使对象发光、使角色变得半透明的着色器,甚至创建将着色器应用到游戏的整个视图的“图像效果”。有无数种方法可以使用着色器。
您可能经常听到片段着色器被称为像素着色器。术语“片段着色器”更准确,因为着色器可以防止像素被绘制到屏幕上。在某些应用程序(例如 Shadertoy)中,您必须将每个像素都绘制到屏幕上,因此在这种情况下将它们称为像素着色器更有意义。
着色器还负责渲染游戏中的阴影和光照,但它们的用途不止于此。着色器程序可以在 GPU 上运行,那么为什么不利用它提供的并行化呢?您可以创建一个在 GPU 而不是 CPU 中运行大量计算的计算着色器。事实上,Tensorflow.js利用 GPU 在浏览器中更快地训练机器学习模型(这个我会诶~)。
着色器确实是强大的程序!
什么是Shadertoy?
Shadertoy 是一个帮助用户创建像素着色器并与他人共享的网站,由于它能帮助学习者零配置通过网页运行shader程序,此教程将在此平台上进行。Shadertoy 利用WebGL API在浏览器中使用 GPU 渲染图形。WebGL 允许您在 GLSL 中编写着色器并支持硬件加速。
也就是说,您可以利用 GPU 并行处理屏幕上的像素以加快渲染速度。还记得在使用HTML Canvas APIctx.getContext('2d')
时必须如何使用吗?Shadertoy 使用带有上下文的画布而不是 2d,因此您可以使用 WebGL 以更高的性能将像素绘制到屏幕上。
现代 3D 游戏引擎(如 Unity 和 Unreal Engine)和 3D 建模软件(如Blender)运行速度非常快,因为它们同时使用顶点和片段着色器,并且为您执行了大量优化。在 Shadertoy 中,您无法访问顶点着色器。您必须依靠光线行进和有符号距离场/函数 (SDF) 等算法来渲染 3D 场景,这在计算上可能会很昂贵。
请注意,在 Shadertoy 中编写着色器并不能保证它们可以在 Unity 等其他环境中工作。您可能必须将 GLSL 代码转换为目标环境支持的语法,例如 HLSL。Shadertoy 还提供了其他环境可能不支持的全局变量。不要让这阻止你!完全可以对您的 Shadertoy 代码进行调整并在您的游戏或建模软件中使用它们。它只需要一些额外的工作。事实上,Shadertoy 是在您喜欢的游戏引擎或建模软件中使用着色器之前试验着色器的好方法。
Shadertoy 是练习使用 GLSL 创建着色器的好方法,可帮助您进行更多数学思考。绘制 3D 场景需要大量的矢量运算。这是智力上的刺激,也是向朋友炫耀你的技能的好方法。
如果您浏览Shadertoy,您会看到大量仅用数学和代码绘制的精美作品!一旦你掌握了 Shadertoy 的窍门,你会发现它真的很有趣!
Shadertoy 简介
Shadertoy 负责设置支持 WebGL 的 HTML 画布,因此您只需担心用 GLSL 编程语言编写着色器逻辑。不利的一面是,Shadertoy 不允许您编写顶点着色器,而只允许您编写像素着色器。它本质上为试验着色器的片段端提供了一个环境,因此您可以并行操作画布上的所有像素。
在 Shadertoy 的顶部导航栏上,您可以单击新建以启动新的着色器。
image.png
然后你将看到以下界面:
image.png
让我们分析一下我们在屏幕上看到的一切。显然,我们在右侧看到了一个代码编辑器,用于编写我们的 GLSL 代码,但让我来看看大多数可用的工具,因为它们在上图中已编号。
- 用于显示着色器代码输出的画布。您的着色器将为画布中的每个像素并行运行。
- 左:将时间倒回零。中心:播放/暂停着色器动画。右:页面加载后的时间(以秒为单位)。
- 每秒帧数 (fps) 将让您了解您的计算机处理着色器的能力。通常以 60fps 或更低的速度运行。
- 画布的宽高分辨率。这些值在“iResolution”全局变量中提供给您。
- 左:通过按下、录制并再次按下来录制 html 视频。中:调整着色器中音频播放的音量。右:按下符号将画布展开为全屏模式。
- 单击加号图标以添加其他脚本。可以使用 Shadertoy 提供的“通道”访问缓冲区(A、B、C、D)。使用“Common”在脚本之间共享代码。当您想编写一个生成音频的着色器时使用“声音”。使用“Cubemap”生成立方体贴图。
- 单击小箭头可查看 Shadertoy 提供的全局变量列表。您可以在着色器代码中使用这些变量。
- 单击小箭头编译着色器代码并查看画布中的输出。您可以使用 Alt+Enter 或 Option+Enter 来快速编译您的代码。您可以单击“Compiled in ...”文本以查看已编译的代码。
- Shadertoy 提供了四个通道,可以通过“iChannel0”、“iChannel1”等全局变量在您的代码中访问它们。如果您单击其中一个通道,您可以通过键盘的形式为您的着色器添加纹理或交互性,网络摄像头、音频等。
- Shadertoy 为您提供了在代码窗口中调整文本大小的选项。如果单击问号,您可以看到有关用于运行代码的编译器的信息。您还可以查看 Shadertoy 添加了哪些功能或输入。
Shadertoy 为编写 GLSL 代码提供了一个很好的环境,但请记住,它会注入变量、函数和其他实用程序,这可能使其与您在其他环境中编写的 GLSL 代码略有不同。在您开发着色器时,Shadertoy 为您提供了这些便利。例如,变量“iTime”是一个全局变量,用于访问自页面加载以来经过的时间(以秒为单位)。
了解着色器代码
当你第一次在 Shadertoy 中启动一个新的着色器时,你会发现如下代码:
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { // Normalized pixel coordinates (from 0 to 1) vec2 uv = fragCoord/iResolution.xy; vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4)); // Output to screen fragColor = vec4(col,1.0); } 复制代码
您可以通过上图中提到的小箭头来运行代码,或者通过按Alt+Center
或Option+Enter
作为键盘快捷键来运行代码。
如果您以前从未使用过着色器,那没关系!我将尽力解释您在 Shadertoy 中编写着色器时使用的 GLSL 语法。马上,您会注意到这是一种静态类型语言,如 C、C++、Java 和 C#。GLSL 也使用类型的概念。其中一些类型包括:bool
(boolean)、int
(integer)、float
(decimal) 和vec
(vector)。GLSL 还要求在每行的末尾放置分号。否则,编译器会抛出错误。
在上面的代码片段中,我们定义了一个mainImage
必须存在于我们的 Shadertoy 着色器中的函数。它什么也不返回,所以返回类型是void
. 它接受两个参数:fragColor
和fragCoord
。
你可能对in
and摸不着头脑out
。对于 Shadertoy,您通常只需要担心mainImage
函数内部的这些关键字。还记得我说过着色器允许我们为 GPU 渲染管道编写程序吗?将in
andout
视为输入和输出。Shadertoy 给了我们一个输入,我们正在写一个颜色作为输出。
在继续之前,让我们将代码更改为更简单的代码:
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { // Normalized pixel coordinates (from 0 to 1) vec2 uv = fragCoord/iResolution.xy; vec3 col = vec3(0., 0., 1.); // RGB values // Output to screen fragColor = vec4(col,1.0); } 复制代码
当我们运行着色器程序时,我们应该得到一个完全蓝色的画布。着色器程序以并行方式为画布上的每个像素运行。记住这一点非常重要。你必须考虑如何编写代码来根据像素坐标改变像素的颜色。事实证明,我们可以只用像素坐标创造出令人惊叹的艺术品!
image.png
在着色器中,我们使用介于 0 和 1 之间的范围指定 RGB(红、绿、蓝)值。如果您的颜色值介于 0 和 255 之间,则可以通过除以 255 来标准化它们。
所以我们已经看到了如何改变画布的颜色,但是在我们的着色器程序中发生了什么?mainImage
函数内的第一行声明了一个uv
类型为 的变量vec2
。如果你记得你在学校的向量算术,这意味着我们有一个带有“x”分量和“y”分量的向量。类型为 的变量vec3
将有一个额外的“z”分量。
您可能在学校学习过3D 坐标系。它让我们可以在纸片或其他平面上绘制 3D 坐标。显然,在 2D 表面上可视化 3D 有点困难,所以过去杰出的数学家创建了一个 3D 坐标系来帮助我们可视化 3D 空间中的点。
但是,您应该将着色器代码中的向量视为可以容纳 1 到 4 个值的“数组”。有时,向量可以保存有关 3D 空间中 XYZ 坐标的信息,或者它们可以包含有关 RGB 值的信息。因此,以下在着色器程序中是等价的:
color.r = color.x color.g = color.y color.b = color.z color.a = color.w 复制代码
是的,可以有类型为 的变量vec4
,而字母w
或a
用于表示第四个值。代表“ a
alpha”,因为颜色可以具有 alpha 通道以及正常的 RGB 值。我猜他们之所以选择w
是因为它x
在字母表中,而且他们已经到达了最后一个字母。
该uv
变量并不真正代表任何东西的首字母缩写词。它指的是UV 映射的主题,通常用于将纹理(例如图像)的片段映射到 3D 对象上。与 Shadertoy 不同,UV 映射的概念更适用于允许您访问顶点着色器的环境,但您仍然可以利用 Shadertoy 中的纹理数据。
该fragCoord
变量表示画布的 XY 坐标。左下角从 (0, 0) 开始,右上角是 (iResolution.x, iResolution.y)。通过除以fragCoord
,iResolution.xy
我们能够将像素坐标归一化在零和一之间。
请注意,我们可以很容易地在两个相同类型的变量之间执行算术运算,即使它们是向量。这与对单个组件执行操作相同:
uv = fragCoord/iResolution.xy // The above is the same as: uv.x = fragCoord.x/iResolution.x uv.y = fragCoord.y/iResolution.y 复制代码
当我们说类似iResolution.xy
时,该.xy
部分仅指向量的 XY 分量。这让我们只剥离我们关心的向量的组件,即使iResolution
碰巧是 type vec3
。
根据这篇Stack Overflow 帖子,z 分量表示像素纵横比,通常为1.0
. 值为 1 表示您的显示器具有方形像素。如果有的话,您通常不会看到人们iResolution
经常使用它的 z 组件。
我们还可以在定义向量时执行快捷方式。下面的代码片段会将整个画布的颜色设置为黑色。
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { // Normalized pixel coordinates (from 0 to 1) vec2 uv = fragCoord/iResolution.xy; vec3 col = vec3(0); // Same as vec3(0, 0, 0) // Output to screen fragColor = vec4(col,1.0); } 复制代码
当我们定义一个向量时,如果你只指定一个值,着色器代码足够聪明,可以在向量的所有值上应用相同的值。因此vec3(0)
扩展为vec3(0,0,0)
。
如果您尝试使用小于零的值作为输出片段颜色,它将被限制为零。同样,任何大于 1 的值都将被限制为 1。这仅适用于最终片段颜色中的颜色值。
重要的是要记住,Shadertoy 和大多数着色器环境中的调试通常主要是可视化的。你没有什么喜欢console.log
来拯救你的。您必须使用颜色来帮助您进行调试。
让我们尝试使用以下代码可视化屏幕上的像素坐标:
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { // Normalized pixel coordinates (from 0 to 1) vec2 uv = fragCoord/iResolution.xy; vec3 col = vec3(uv, 0); // This is the same as vec3(uv.x, uv.y, 0) // Output to screen fragColor = vec4(col,1.0); } 复制代码
我们最终应该得到一个混合了黑色、红色、绿色和黄色的画布。
image.png
这看起来很漂亮,但它对我们有什么帮助?该uv
变量表示 x 轴和 y 轴上介于 0 和 1 之间的标准化画布坐标。画布左下角的坐标为 (0, 0)。画布的右上角有坐标 (1, 1)。
在col
变量内部,我们将其设置为等于(uv.x, uv.y, 0)
,这意味着我们不应该期望画布中有任何蓝色。当uv.x
和uv.y
等于 0 时,我们得到黑色。当它们都等于 1 时,我们会得到黄色,因为在计算机图形学中,黄色是红色和绿色值的组合。画布的左上角是 (0, 1),这意味着col
变量将等于 (0, 1, 0),即绿色。右下角的坐标为 (1, 0),表示col
等于 (1, 0, 0),即红色。
让颜色帮助你调试!