Shadertoy 教程 Part 1 – 介绍

Note: This series blog was translated from Nathan Vaughn’s Shaders Language Tutorial and has been authorized by the author. If reprinted or reposted, please be sure to mark the original link and description in the key position of the article after obtaining the author’s consent as well as the translator’s. If the article is helpful to you, click this Donation link to buy the author a cup of coffee.
说明:该系列博文翻译自Nathan Vaughn着色器语言教程。文章已经获得作者翻译授权,如有转载请务必在取得作者译者同意之后在文章的重点位置标明原文链接以及说明。如果你觉得文章对你有帮助,点击此打赏链接请作者喝一杯咖啡。

朋友们,你们好。我最近对着色器着迷了。 今天,我将讨论如何使用 Shadertoy 这件惊人在线工具创建像素着色器,该工具由 Inigo QuilezPol Jeremias 这两位非常有才华的人创建的。

什么是着色器

着色器(Shaders)是一门功能强大的程序语言,最初的作用是为 3D 场景中的物体着色。 如今,着色器的用途非常广泛。 着色器程序通常在计算机的图形处理单元 (GPU) 上运行,它们运行的方式是并行的。

提示:了解着色器(Shaders)在 GPU 上并行运行非常重要。 你的程序将会同时内在Shadertoy中的每个像素上独立地运行。

高级着色语言(HLSL) 和 OpenGL 着色语言 (GLSL) 等着色器语言是用于对 GPU 渲染管道进行编程的最常用语言。 他们有着类似于C编程语言的语法。

当你在玩Minecraft这种游戏的时候,只需要一个2D屏幕(例如:电脑显示器或者手机屏幕),你就能体验着色器会为你构建的3D世界。着色器语言也能够通过动态地改变虚拟世界中的光线与物体的交互以及物体的渲染从而改变虚拟世界的样子。这个油管视频展示了10种着色器程序,它们使得同一个游戏看起来完全不同。该示例向我们展示了着色器程序语言之美。

我们经常见到的着色器有两种:顶点着色器和片元着色器。顶点着色器用来创建3D模型的顶点,例如球体,立方体,大象,或者3D游戏中的主角等。来自顶点着色器的信息被传递给几何着色器,几何着色器会在片元着色器之前对它们进行处理或者做一些额外的操作。你一定不太想知道几何着色器的太多东西。最后一部分是片元着色器。片元着色器会计算出每一个像素最终呈现的颜色或者决定该像素是否需要呈现在屏幕上。

图形渲染管线的各个阶段– Learn OpenGL


我们来举个例子,假设我们有一个顶点着色器,它被用来在屏幕上绘制一个三角形的各个顶点。一旦它们被传递给片元着色器,每个顶点之间的颜色会就会被自动填充。GPU非常擅长处理颜色插值。一旦一种颜色被分配给了顶点,GPU就会在每个顶点之间进行颜色插值处理,填充整个三角形。

Unity或者Unreal这种游戏引擎中,构建3D游戏对顶点着色器和片元着色器的使用是非常依赖的。Unity在着色器之上进行了抽象,创建了一门独特的语言ShaderLab, 它是一种基于HLSL的高级语言,旨在帮助开发者更容易地编写游戏。此外,Unity还提供了一个虚拟工具Shader Graph,让开发者无需编写代码就可以构建自己的着色器。如果你在Google上搜索“Unity shaders”,就能找到上百种各种功能的着色器。你可以使用着色器让物体发光,让人物变得透明,甚至可以制造一些“图片效果”为你的游戏增添色彩,总之探索着色器的道路千千万万条。

你可能经常听到片元着色器也被称为“像素着色器”,实际上使用术语“片元着色器”是更为准确的,因为着色器可以防止像素被绘制在屏幕上。在一些应用上例如Shadertoy,你往往只是在往屏幕上绘制像素,所以在那种情况之下,我们才把片元着色器叫做像素着色器。

着色器同时也为你的游戏提供着色或者光照效果,但它的功能还不仅于此。既然着色器运行在GPU上,为什么不利用它的并行优势呢?你可以在GPU而不是CPU上创建非常复杂的计算着色器。事实上,Tensorflow.js就是在浏览器上利用GPU的优势,让他们更快地训练机器学习模型。

着色器真的是一门非常强大的程序!

什么是Shadertoy

在接下来的博文中,我将向诸位介绍Shadertoy。Shadertoy是一个帮助用户创建并且分享像素着色器的网站。类似于上编写HTML, CSS和JavaScript在线编辑器Codepen

提示:学习此教程的过程中,需要确保你的浏览器像谷歌浏览器一样支持WebGL 2.0。

Shadertoy利用了WebGL API 调用GPU的能力去浏览器中绘制图像。WebGL 让你用GLSL编写着色器并且支持硬件加速。因此,你可以利用GPU的并行计算能力,在同一块屏幕上同时地去操作所有像素。你还记得我们在使用HTML Canvas API时调用的ctx.getContext('2d')函数吗?Shadertoy 使用了webgl上下文,它在屏幕上绘制像素有着更高质量的表现。

告警:虽然Shadertoy用GPU提升了视觉效果,但是当你运行复杂计算的着色器程序时,你的电脑也可能会出现卡顿的现象。请确保你的电脑能够运行这些着色器程序,并且要记住,它们会非常耗电。

现代3D游戏引擎例如Unity和Unreal以及3D建模软件例如Blender运行非常顺畅,因为它们同时使用了顶点着色器和片元着色器,此外它们还做了很多优化的工作。在Shadertoy中,你是无法使用顶点着色器的。你只能依赖一些算法例如:光线步进(Ray Marching)以及等符号距离场函数(Signed Distance Fields Functions)去渲染3D场景,这些计算是非常昂贵的。

在Shadertoy中编写的着色器不一定能够在其他环境(如Unity)中运行,这点需要特别注意。你需要将GLSL代码语法翻译成你的目标环境(例如 Unity)支持的代码(如HLSL)。Shadertoy同时提供了一些全局变量,这些变量在其他环境中也是不支持的。但别就此止步不前!只需要额外的做一些工作,例如对着色器做一些小小的修改,就可以把他们应用到你的游戏或者建模软件中去。在使用自己喜欢的游戏引擎或者建模软件之前,Shadertoy让我们能够预先体验着色器的魅力。

Shadertoy能让用户使用GLSL编写着色器,帮助我们数学地思考问题。绘制3D场景时需要很多矢量计算,这是一场智力的模拟赛,你可以向你的朋友们炫耀你的技巧。如果你去Shadertoy网站上浏览过,你就会发现许许多多美丽的作品,这些作品仅仅是一堆代码和数学组成的。一但你掌握了Shadertoy,你会发现它真的很有意思!

Shadertoy 功能介绍

Shadertoy为你封装好了HTML canvas和webgl,你需要关心的就是用GLSL编程语言编写着色器代码逻辑。但是Shadertoy不允许你编写顶点着色器而只允许你编写像素着色器,它提供的环境只能够让我们体验着色器的“片元”部分,这意味着只能在整块画布上地操作所有像素。

在Shadertoy的顶部导航栏上,你可以点击“新建”来创建一个着色器。

让我们分析屏幕上所有我们能看到的东西吧。我们可以看到右手边是一个用来编写GLSL代码的编辑器,然后我们按照图片上标注的数字顺序,依次介绍它们的作用。

  1. 用来展示代码效果的画布(Canvas)。你的着色器代码会并行地在上面的每个像素上运行。
  2. 左边:将时间重置为0。中间:暂停/播放着色器动画。右边:页面加载后经过的时间。
  3. 每秒钟帧数(FPS)会让你了解你的电脑是如何运行这些着色器的。一般在60FPS左右徘徊。
  4. Canvas的高宽分辨率。这些值被指定在“iResolution”全局变量中去。
  5. 左边:点击录制视屏,再次点击停止即可保存。中间:调整音量键。右边:进入全屏模式。
  6. 点击“+”按钮添加新的脚本。缓冲(A,B,C,D)可以通过Shadertoy提供的Channels互相访问。使用“Common”分享着色器片段。使用“音量”当你需要编写一个可以播放音乐的着色器。使用“穹顶”按钮当你需要一个起起钉。
  7. 点击小箭头查看Shadertoy提供的全局变量列表。
  8. 点击小箭头将你的着色器代码编译,输出到画布上。也可以使用Alt+Enter 或者 Option+Enter 作为快捷键。你可以点击“编译到”文本看看编译后的代码。
  9. Shadertoy 提供了四个通道,他们可以通过全局变量“ iChannel0”,“ iChannel1”等进行互相访问。点击一个通道向其中添加纹理或者为你的着色器添加键盘,音量,摄像头等交互行为。
  10. Shadertoy 提供了修改代码字体大小的选项。如果你点击问号标记,你就能看到关于编译器运行你代码的信息了。此外你还可以看看有什么功能或者输入被加入到了Shadertoy中。

Shadertoy为我们编写GLSL 代码提供了环境,并且为我们预设了默认的变量,函数和一些其他功能,这些功能使得它稍微与其他环境中编写的GLSL代码有所区别。Shadertoy为你开发着色器的过程提供了便利,例如:变量“iTime”,就能让你访问页面加载后过去的时间。

理解着色器代码

在Shaderoy中新建第一个着色器,可以看到下面的一段代码:

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 来运行这段代码。

如果你之前没有写过着色器,也没关系。我会尽量想你解释清楚GLSL的语法,帮助你在Shadertoy中编写自己的着色器。现在。相信你一定注意到了这是一种静态类型的语言。和C, C++ 或者Java和C#一样,GLSL也使用类型。这些的类型包括:bool(布尔),int(整数),float(浮点数),以及vec(矢量)。GLSL强制要求换行前有逗号,否则,编译器会抛出错误。

前面的这小段代码,我们定义了一个mainImage函数,这个函数必须出现在Shadertoy着色器中。这个函数什么都不返回,因此它的返回类型就是void。它接受两个参数:fragColorfragCoord

你或许会对inout两个关键字感到疑惑。对于Shadertoy来说,我们只需要在mainImage函数中的关心这些关键字。还记得我说过GPU是怎么样运行我们用着色器渲染管线的吗?把inout想象成输入和输出: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);
}

运行着色器程序,我们就会看到一个纯蓝色的画布。着色器在画布上同时并行地绘制出每一个像素,你需要特别注意这点,因为它非常重要。现在你需要思考的是如何根据每个像素的坐标改变它们颜色从而能创作出惊人的作品。

在着色器中,我们用0~1之间的任意值来指定RGB(红,绿,蓝)。如果颜色值是0到255,你可以将他们除以255来得到同样的结果。

那么,我们已经知道如何改变画布上的颜色,但实际上在着色器中到底发生了什么呢?第一行的mainImage函数声明了一个变量uv,类型为vec2。如果你还记得学校教你的矢量计算,你就会知道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,我猜可能是因为z已经是最后一个字母,所以取的是x字母之前的字母w🤷。

变量uv并非任何单词的缩写,但通常它用来指代映射纹理到3D物体上的纹理映射(UV Mapping)技术。纹理映射(UV mapping) 更适合应用到能够使用顶点着色器的环境中。但是你仍然可以在Shadertoy中使用纹理数据。

fragCoord变量代表着Canvas的X和Y 坐标。左下脚的坐标位置是起点(0,0),右上角的坐标位置是(iResolution.x, iResolution.y)。iResolution.xy除以fragCoord,我们就能归一化像素的坐标从0到1.

请注意,要在两个相同类型的矢量之间做数学计算并不困难。我们只需要分别将里面的元素各自独立的进行计算就可以了。

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元素。这样我们就可以不依靠整个向量而独立地为每个元素进行计算。这种做法在vec3类型的时候也是一样的。

根据一篇来自Stack Overflow 的文章的说法,z元素表示的是像素的比率,大多数情况下是1.0。尽管这样,你也不是很经常地会使用到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)是相同的效果。

提示:如果你尝试用小于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);
}

运行以上代码我们最终就得到了下面这幅由黑色、红色、绿色以及黄色的画布。

看起来还挺漂亮的,但如何理解它的调试功能呢?uv变量代表画布上每个像素点的坐标,它们的值在0到1之间。我们就以屏幕上的四个(左上,右上,左下,右下)特殊的像素点来说明吧。它们的坐标位置分别是(0, 1), (1, 1), (0, 0), (1, 0); 在col变量中我们保存(uv.x, uv.y, 0), 把四个点的位置代入变量:首先是左上角的值就是(0, 1, 0),其次是右上角值为(1, 1, 0), 再次是左下角(0, 0, 0),最后是右下角(1, 1, 0)。哈哈,发现了吗,这四个值代表的颜色分别就是绿色(左上)、黄色(右上)、黑色(左下)和右下(红色)是不是与我们在屏幕上看到的颜色一样呢。

让颜色指引你去调试整个过程吧!

总结

好了!在这篇文章中我提到了很多关于着色器以及Shadertoy的知识, 真希望你还能够读到这里。我第一次学习着色器时,感觉它就是一片完全陌生的领域,与我之前学过的知识完全不一样,但同时也充满了期待和挑战。在下一篇文章中,我将会讨论如何用着色器语言创建物体形状以及动画!

引用