语言设定

着色器入门

着色器是在图形处理单元(GPU)上运行的特殊程序,可以完成一些令人难以置信的任务。 它们利用 GPU 一次处理多个像素,使其快速且特别适用于某些任务,例如生成噪声、应用模糊滤镜或对多边形进行着色。 一开始学习着色器编程可能会让人望而却步,需要与 p5.js 的二维绘图不同的思路。本文档将概述着色器编程的基础知识,并指向其他资源。

设置

p5.js 是处理着色器的好工具,因为它处理了大部分 WebGL 的设置,使您可以专注于着色器代码本身。在开始使用着色器之前,我们必须设置画布,使其使用 p5.js WebGL 模型。


      ...
      function setup() {
        createCanvas(windowWidth, windowHeight, WEBGL);
      }
      ..
      

着色器程序由两部分组成,一个顶点着色器和一个片元着色器。 顶点着色器影响3D几何图形在屏幕上的绘制位置,片元着色器则负责影响颜色输出。这两个部分分别存储在不同的文件中,并通过 loadShader()加载到p5.js中。一旦着色器被加载,它就可以在 draw()中使用。下面的示例将展示如何在p5.js中设置基本着色器:


let myShader;

function preload() {
  // load each shader file (don't worry, we will come back to these!)
  myShader = loadShader('shader.vert', 'shader.frag');
}

function setup() {
  // the canvas has to be created with WEBGL mode
  createCanvas(windowWidth, windowHeight, WEBGL);
  describe('a simple shader example that outputs the color red')
}

function draw() {
  // shader() sets the active shader, which will be applied to what is drawn next
  shader(myShader);
  // apply the shader to a rectangle taking up the full canvas
  rect(0,0,width,height);
}
      

着色语言 (GLSL)

现在你可能想知道我们实际上在这些着色器文件中写了什么!着色器文件是用图形库着色语言 (GLSL)编写的,它具有非常不同的语法和结构,与我们熟悉的语法和结构有很大不同。GLSL具有类似C的语法,这意味着它具有一些JavaScript中不存在的概念。

首先,着色语言在类型方面要严格得多。每个你创建的变量都必须用存储的数据类型进行标记。以下是一些常见类型的列表:


vec2(x,y) // 由两个浮点数组成的向量
vec3(r,g,b) // 由三个浮点数组成的向量
vec4(r,g,b,a) // 由四个浮点数组成的向量
float // 带小数点的数字
int // 不带小数点的整数
sampler2D // 对纹理的引用

      

通常情况下,着色语言比 JavaScript 更加严格。例如,缺少分号将导致错误信息。 不能在不同类型的数字之间进行交换,如浮点数或整数。

首先,让我们看一个基本的顶点着色器:


attribute vec3 aPosition;

void main() {
  vec4 positionVec4 = vec4(aPosition, 1.0);
  positionVec4.xy = positionVec4.xy * 2.0 - 1.0;
  gl_Position = positionVec4;
}

这个顶点着色器以一个 attribute 开始,p5.js 使用它来与着色器共享顶点位置信息。这个 attribute 是一个 vec3,意味着它包含一个 x、y 和 z 值。 Attributes 是特殊的变量类型,只在顶点着色器中使用,并且通常由 p5.js 提供。

所有顶点着色器都需要一个函数 main(),在其中我们定位我们的顶点。 在这个例子中,顶点着色器重新定位我们的顶点,使着色器输出占据整个画布。在 main() 的末尾,我们必须给 gl_Position 赋值。

如果这还不是很清楚,也不要担心。顶点着色器起着重要的作用, 但通常只负责确保我们在片段着色器中创建的内容在几何体上正确地显示。 在许多项目中,您可能会发现自己反复使用相同的顶点着色器。 另一方面,片段着色器负责着色器的颜色输出,是我们进行大量着色器编程的地方。 以下是一个非常简单的片段着色器,它只会显示红色:


precision mediump float;

void main() {
  vec4 myColor = vec4(1.0, 0.0, 0.0, 1.0);
  gl_FragColor = myColor;
}

片段着色器以指定 float 'precision' 开始。你可以选择低精度 lowp、中精度 mediump 或高精度 highp, 不过在某些情况下,你可能会选择使用中精度或高精度。

precision mediump float;

和顶点着色器类似,片段着色器也需要一个 main() 函数, 但我们不会设置 gl_Position, 而是将颜色分配给 gl_FragColor


...
vec4 myColor = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor = myColor;
...

变量 myColor 被定义为一个 vec4, 这意味着它可以存储 4 个值。由于我们处理的是颜色,因此这四个值分别是红、绿、蓝和 alpha。 着色器不使用像我们的草图一样的 0-255 颜色,而是使用介于 0.0 和 1.0 之间的值。

现在我们已经有了一个顶点着色器和一个片段着色器, 可以将它们保存到单独的文件中(shader.vert 和 shader.frag), 并使用 loadShader() 加载到我们的草图中。

Uniforms:从草图传递数据到着色器

单独使用这样一个简单的着色器是有用的,但有时需要从 p5.js 草图向着色器传递变量。 这就是 uniforms 的作用。Uniforms 是特殊的变量,可以从草图发送到着色器。 这使得能够更好地控制着色器。例如,您可以使用 p5.js 方法 millis() 向我们的草图传递一个“时间” uniform,以引入运动。在着色器中,uniforms 在文件顶部定义, 在 main() 之外。在下面的片段着色器中, 我们创建了一个颜色 uniform myColor,这将允许我们从草图中更改颜色。


precision mediump float;

uniform vec3 myColor;

void main() {
  // the color we have passed in as a uniform is assigned to the pixel
  gl_FragColor = vec4(myColor, 1.0);
}
      

回到我们的 p5.js 草图,现在可以使用 setUniform() 发送这个颜色:


...
function draw() {
  shader(myShader);
  // setUniform can then be used to pass data to our shader variable, myColor
  myShader.setUniform('myColor', [1.0,0.0,0.0]); // send red as a uniform
  // apply the shader to a rectangle taking up the full canvas
  rect(0,0,width,height);
}
...
      

还有 attributes,通常用于在草图和顶点着色器之间共享关于几何体的某些数据,和 varying 变量,它们在顶点着色器和片段着色器之间共享数据。这使得可以在我们的片段着色器中使用位置或其他几何数据。


// (thank you to Adam Ferriss for the foundation of these example shaders)
// position information that is used with gl_Position
attribute vec3 aPosition;

// texture coordinates 
attribute vec2 aTexCoord;

// the varying variable will pass the texture coordinate to our fragment shader
varying vec2 vTexCoord;

void main() {
  // assign attribute to varying, so it can be used in the fragment
  vTexCoord = aTexCoord;

  vec4 positionVec4 = vec4(aPosition, 1.0);
  positionVec4.xy = positionVec4.xy * 2.0 - 1.0;
  gl_Position = positionVec4;
}

现在,将纹理坐标属性分配给变量之后,我们可以在片段着色器中使用纹理坐标。下面的示例结果是蓝色和紫红色的纹理坐标可视化。


precision mediump float;

varying vec2 vTexCoord;

void main() {
  // now because of the varying vTexCoord, we can access the current texture coordinate
  vec2 uv = vTexCoord;
  
  // and now these coordinates are assigned to the color output of the shader
  gl_FragColor = vec4(uv,1.0,1.0);
}
an illustration showing UV coordinates, the x axis in red, and the y axis in blue.
p5.js 为我们处理了许多属性和 uniforms,您可以查看 p5.js 发送到着色器的所有属性列表

结论

通过这些技能,您将能够创建一些基本的着色器,但着色器编程可以深入到无限深处,有许多着色器主题超出了本教程。在 p5.js 中,着色器可以成为创建可映射到 3D 几何体的视觉、效果甚至纹理的强大工具。

想继续了解更多有关着色器的知识吗?请查看以下网站!

  • The Book of Shaders,由 Patricio Gonzalez Vivo 和 Jen Lowe 撰写的着色器指南。
  • P5.js着色器,由Casey Conchinha和Louise Lessél编写的着色器指南。
  • Shadertoy,一个庞大的在线着色器集合,这些着色器都是在浏览器编辑器中编写的。
  • p5jsShaderExamples,由Adam Ferriss编写的资源集。

其他教程

这个教程是关于使用 p5.js 中 WebGL 基础的系列之一。以下是其他教程:

词汇表

着色器 Shader

可以有效地产生许多视觉效果和滤镜的特殊图形卡程序。

GLSL

图形库着色器语言(GLSL)是一种用于编写着色器的编程语言。

Uniform

从你的草图传递到着色器的变量。

矢量 Vector

一种数据类型,它存储一组数字,通常是两个、三个或四个数字,表示颜色、位置等。

浮点数 Float

一种存储浮点数的数据类型,可以有小数点。

整数 Int

一种存储整数的数据类型,没有小数。

取样器 Sampler

表示传递到着色器中的纹理的数据类型。

属性 Attribute

在 p5.js 草图中生成并在顶点着色器中提供的 GLSL 变量。对于大多数情况,这些由 p5.js 提供。

纹理 Texture

传递到着色器程序中的图像。

类型 Type

描述数据类型的特征的标签,例如 int、float、vector 等。

顶点着色器 Vertex Shader

该着色器程序的一部分负责将几何体定位在 3D 空间中。

片段着色器 Fragment Shader

该着色器程序的一部分负责每个像素输出的颜色和外观。