该文章为翻译而来,原文链接 https://itp-xstory.github.io/p5js-shaders/#/./docs/how-to-write-a-shader
该系列翻译文章将不定期更新,将用于学习记录,若有侵权,请联系我。
1.5 如何编写shader
现在让我们来谈谈要加载的shader文件。要制作一个shader,你必须在你的p5js脚本的文件夹中编写两个代码文件。一个shader.vert文件和一个shader.frag文件。你可以给shader文件起任何你想要的名字。myAwesomeShader.vert和myAwesomeShader.frag是完全有效的名字。只要你有以.vert
和.frag
为后缀的文件,就可以开始了。
我们将从制作单色填充的shader开始,就像p5js中的fill()
一样,如下图所示。我们将两个shader文件分别命名为onecolor.vert和onecolor.frag。
sketch.js
代码如下所示。
// 设置一个shader变量
let theShader;
function preload(){
// 加载shader
theShader = loadShader('onecolor.vert', 'onecolor.frag');
}
function setup() {
// 运行shader需要加入WEBGL参数
createCanvas(windowWidth, windowHeight, WEBGL);
noStroke();
}
function draw() {
// shader() 函数用我们自定义的shader设置活动着色器
shader(theShader);
// rect() 函数在屏幕中创建一个矩形
rect(0,0,width,height);
// 打印帧率
// print(frameRate());
}
function windowResized(){
resizeCanvas(windowWidth, windowHeight);
}
onecolor.frag
代码如下所示。
// Adapted by Louise Lessel - 2019
// from
// Author @patriciogv - 2015
// http://patriciogonzalezvivo.com
// https://thebookofshaders.com/02/
/*
示例:
将整个背景涂成蓝色
*/
// 这些是必要的定义,让你的GPU知道如何渲染shader。
#ifdef GL_ES
precision mediump float;
#endif
void main() {
// 设置一个蓝色变量
// 在shader中,RGB的数据范围是从0-1,而不是0-255。
vec3 color = vec3(0.0, 0.0, 1.0);
// gl_FragColor是一个内置的shader变量,你的.frag文件必须包含它。
// 我们将vec3向量的颜色设置为一个新的vec4颜色向量,透明度为1(即完全不透明)。
gl_FragColor = vec4(color, 1.0);
}
onecolor.vert
代码如下所示。
/*
vert file and comments from adam ferriss
https://github.com/aferriss/p5jsShaderExamples
with additional comments from Louise Lessel
*/
// 这些是必要的定义,让你的GPU知道如何渲染shader。
#ifdef GL_ES
precision mediump float;
#endif
// 这个 "vec3 aPosition "是一个内置的shader功能。你必须保持这个命名不变。
// 它自动获得你画布上每个顶点的位置
attribute vec3 aPosition;
// 我们必须在顶点着色器中做的一件事是:
// 告诉每个像素它在屏幕中的哪个位置
void main() {
// 将位置数据复制到一个vec4中,使用1.0作为w分量。
vec4 positionVec4 = vec4(aPosition, 1.0);
// 确保shader覆盖整个屏幕:
// 将矩形放大2倍,并将其移动到屏幕中央。
// 如果我们不这样做,矩形的左下角就会位于草图的中心。
// 尝试注释下面这行代码看看会发生什么
positionVec4.xy = positionVec4.xy * 2.0 - 1.0;
// 将顶点信息发送给片段着色器
// 这是自动完成的,只要你把它放到内置的着色器函数"gl_Position "中。
gl_Position = positionVec4;
}
值得注意的是,这些shader文件是用GLSL(OpenGL着色语言)编写的,这是一种比Javascript更低级的语言,意味着它能更直接地与计算机对话,特别是与GPU对话。这些代码一开始会显得很陌生和混乱,但在这个介绍之后,你应该能对其有一个更好的理解。
shader的结构剖析
.vert
文件处理所有与顶点有关的运算–也就是你所有的几何图形(形状)和它在画布上的位置。顶点是形状角点的另一个名称,所以如果你代码中有rect()
函数,即绘制了一个矩形,其就有四个顶点。更复杂的形状如3D模型有更多的顶点,当所有这些顶点被连接起来时,这就被称为网格(mesh)。Sphere()
函数是p5js中网格的一个很好例子。
.vert
文件处理每个顶点的操作,所以把与网格上的像素位置有关的代码放在.vert
文件中是很好的习惯。在我们的第一个例子中,我们将使用一个覆盖整个画布的矩形,所以.vert
文件是非常简单的。文件的结尾是将内置变量gl_Position
设置为我们的计算结果,这确保shader可以自动在.frag
文件中使用这些位置。
.frag
文件处理所有与像素的实际着色有关的事情,其需要在最后将内置变量gl_FragColor
设置为一种颜色。.frag
文件处理 **每个片段(每个像素)**的操作。使用.frag
着色器来给像素着色是很好的做法。
shader程序是这样运行的
首先运行.vert
文件,并自动将我们对画布上的几何图形(形状)进行的计算传递给.frag
文件。然后,.frag
文件会根据像素的位置给它们上色。
.vert
文件是针对几何体上的每个顶点运行的,而.frag
文件是针对每个像素运行的。这两个文件的最终结果将同时应用于所有的像素! 正如我们在上一节教程中看到的视频解释的那样。
在本指南中,你不需要过多地考虑.vert
文件,因为我们只涉及.frag
着色器。这只是意味着我们要做的所有计算,将放在.frag
文件中。我们在.vert
文件中要做的唯一一件事,就是把几何体上的每个像素的位置传递给.frag
文件。我们稍后将讨论这个问题。
要解释一下其中的区别:如果我们把计算放在.vert
文件中,那么如何给一个像素上色的最终结果就是在几何体的顶点之间进行插值(代码实际上是问–如果某个像素在网格(mesh)的某两个顶点之间,应该是什么颜色?)其结果并不像在.frag
着色器中进行计算那样精细,在.frag
中它是为每个像素(而不仅仅是每个顶点)进行计算的。在.frag
文件中进行计算可能会在性能方面付出更多的代价,这取决于你的着色器看起来有多酷炫。就像所有其他编程一样,你增加的代码行数越多(或者每个像素的计算越多),程序就会变得越慢。但这正是shader在GPU上运行的原因,所以我们可以同时进行这些海量的计算。
如果你有兴趣深入了解运行这些文件时会发生什么,这个关于OpenGL的wikibook页面是一个很好的资源。
shader.vert文件的内容
首先我们写一些必要的定义,让你的GPU知道如何渲染shader。在刚开始使用着色器时,你不应该太担心这个问题。但知道这一点有好处,所以我们将简单地解释一下。
GL_ES是一个着色器API,如果你在浏览器或移动平台上显示shader,它将自动被你的GPU使用。#ifdef
的意思是如果定义。它只是简单地对我们希望图形渲染的精确程度做了一个全局设置,这取决于我们在哪里查看渲染结果。因此,如果我们在浏览器中,他们将得到我们在这里定义的精度水平。在这种情况下,我们将代码中的所有浮点数字设置为中等精度。这对于制作平滑的颜色渐变非常重要。
浮点类型(float)在着色器中是至关重要的,所以其所对应的精度水平也是极为关键的。**较低的精度意味着更快的渲染,但要以质量为代价。**你可以自己指定每个使用浮点的变量的精度。在第一行(
precision mediump float;
)中,我们将所有浮点设置为中等精度。但我们可以选择将它们设置为低精度(precision lowp float;
)或高精度(precision highp float;
)。 - The Book of Shaders
#ifdef GL_ES
precision mediump float;
#endif
然后我们设置一个叫做属性(attributes)的类型,这些包含的信息由p5js的JavaScript脚本自动发送给shader。关于属性的更多信息见此。
对于p5中的shader,我们必须确保在.vert
文件中完成一件事,即每个像素必须被告知它在画布上的位置! 这个属性被称为vec3 aPosition
。你不能改变它的名字,而且这个属性是只读的,也就是说你不能覆盖它。属性通常是以a作为前缀来命名的。比如aSomething
。
该属性包含位置信息,它是一个vec3(三维向量),意味着它包含x、y和z值。
attribute vec3 aPosition;
所有的着色器文件都必须有一个void main()
函数,这是程序开始的地方。请记住,这里的所有内容都是为画布上的每个像素而服务的!所以你需要调整你的思路,考虑只为一个像素编码。
对于p5js中的shader,有一些奇怪的事情需要在main()
函数中解决。我们需要在属性aPosition
被传递到.frag
文件之前对其进行缩放。这可能是一个bug,在以后的p5js版本中会解决。但现在我们可以通过简单地缩放所有像素并将它们移动到正确的位置来解决这个问题。
想象一下,你有一个画布,你正在把一个图像放在画布内。由于某种原因,画布决定图像的左下角应该放在画布的中心,而且图像覆盖于画布中心到画布的右上角的位置,所以它不会填满整个画布。我们需要用一点数学方法来解决这个问题。
首先,我们把位置数据复制到vec4(四维向量)中,这意味着我们现在将有以下数字在里面(x,y,z,w)。我们不使用z,因为我们只在两个维度上操作:x和y。我们将把1.0作为w参数(当w=1.0
时,矢量被当作位置,当w=0.0
时,矢量被当作方向,这是标准的矢量数学)。这里有一个关于非常基本的矢量数学的伟大指南。
然后,我们将像素的位置放大2倍,使其变成2倍大小–这意味着图像的右上角超过了我们的画布右上角,并通过对所有像素做-1操作,将其向左和向下移动。这些奇怪的小数字(0、1)现在可能没有任何意义,但在你阅读了重要的着色器概念之后,它们会变得更有意义。
positionVec4.xy = positionVec4.xy * 2.0 - 1.0; // .xy意味着我们对x和y位置同时进行数学计算
无论我们是否选择在.frag
文件中使用位置数据,这个位置计算总是需要的。
最后,顶点着色器.vert
需要有一个名为gl_Position
的vec4
输出。这将自动把位置信息发送到片段着色器(.frag)文件中。把它放在你的.vert
文件的最后一行是个好习惯。所以让我们把它设置为我们所做的缩放计算:gl_Position = positionVec4
。
void main() {
vec4 positionVec4 = vec4(aPosition, 1.0); // 将位置数据复制到一个vec4向量中,添加1.0作为w参数。
positionVec4.xy = positionVec4.xy * 2.0 - 1.0; // 缩放以使图像位于画布中心并充满整个画布。
gl_Position = positionVec4;
}
shader.frag文件的内容
在我们讨论如何使用.vert
文件中的位置信息之前,暂时先不考虑它,而是给所有像素涂上一种颜色。
在我们的.frag
文件中,我们有和之前一样的定义(指ifdef
那一段),我们也有一个运行程序的main()
函数。
我们将建立一个名为color
的新vec3
向量,并在其中放入一个颜色。这只是一个变量,所以你可以把它赋值为你想要的颜色。让我们填充一个蓝色的颜色。在shader中,RGB颜色是0-1而不是0-255。所以我们编写如下代码。
vec3 color = vec3(0.0, 0.0, 1.0);
最后,片段着色器要求有一个名为gl_FragColor
的vec4
输出。这一行是告诉GPU如何为像素着色的。这必须是你的.frag
文件的最后一行,任何在这一行之后的代码都不会有任何效果,你可能会得到一个"代码未达到(code not reached) "的错误。
gl_FragColor
期望的格式是vec4(r,g,b,a)
,所以我们输入1.0作为我们的alpha
,意味着没有透明度。
gl_FragColor = vec4(color, 1.0);
整个代码看起来像这样。
#ifdef GL_ES
precision mediump float;
#endif
void main() {
// 填充一个蓝色的颜色。在着色器中,RGB颜色从0 - 1,而不是0 - 255
vec3 color = vec3(0.0, 0.0, 1.0);
gl_FragColor = vec4(color, 1.0);
}
写在最后
我创建了一个公众小号【p5js艺术小站】,里面会更新各种作品、教程等精彩内容,欢迎各位大佬关注指导,共同进步!!!