一、渲染管线
WebGL 是什么
WebGL (Web图形库)是一个JavaScript API,可在任何兼容的Web浏览器中渲染高性能的交互式3D和2D图形,而无需使用插件。WebGL通过引入一个与OpenGL ES 2.0非常一致的API来做到这一点,该API可以在HTML5 元素中使用。这种一致性使API可以利用用户设备提供的硬件图形加速。
WebGL 发展史
WebGL的发展最早要追溯到2006年, WebGL起源于Mozilla员工弗拉基米尔-弗基西维奇的一项Canvas 3D实验项目,并于2006年首次展示了Canvas 3D的原型,这一技术在2007年底在FireFox和Opera浏览器中实现。 2009年初KhronosGroup联盟创建了WebGL的工作组最初的工作成员包括Apple, Google, Mozilla, Opera等。2011年3月WebGL 1.0规范发布, WebGL 2 规范的发展始于 2013年,并于 2017 年 1月最终完成,WebGL 2 的规范,首度在 Firefox 51、Chrome 56 和 Opera 43 中被支持。
渲染管线
Webgl的渲染依赖底层GPU的渲染能力。所以WEBGL渲染流程和GPU内部的渲染管线是相符的。渲染管线的作用是将3D模型转换为2维图像。
在早期,渲染管线是不可编程的,叫做固定渲染管线,工作的细节流程已经固定,修改的话需要调整一些参数。现代的GPU所包含的渲染管线为可编程渲染管线,可以通过编程GLSL着色器语言来控制一些渲染阶段的细节简单来说: 就是使用shader,我们可以对画布中每个像素点做处理,然后就可以生成各种酷炫的效果了。
二、细讲渲染流程
顶点着色器
WebGL就是和GPU打交道,在GPU上运行的代码是一对着色器,一个是顶点着色器,另一个是片元着色器。每次调用着色程序都会先执行顶点着色器,再执行片元着色器。
一个顶点着色器的工作是生成裁剪空间坐标值,通常是以下的形式:
const vertexshaderSource = `
attribute vec3 position;
void main() {
gl_Position = vec4(position,1);
}
`
每个顶点调用一次(顶点)着色器,每次调用都需要设置一个特殊的全局变量 gl_Position,该变量的值就是裁减空间坐标值。 这里有同学就问了,什么是裁剪空间的坐标值???
其实我之前有讲过,我在讲一遍。
何为裁剪空间坐标?就是无论你的画布有多大,裁剪坐标的坐标范围永远是 -1 到 1。看下面这张图:
如果运行一次顶点着色器,那么gl_Position就是 (-0.5, -0.5, 0, 1)记住他永远是个Vec4,简单理解就是对应x、 y、
z、w。即使你没用其他的,也要设置默认值, 这就是所谓的 3维模型转换到我们屏幕中。
顶点着色器需要的数据,可以通过以下四种方式获得。
- attributes 属性(从缓冲读取数据)
- uniforms 全局变量(一般用来对物体做整体变化、 旋转、缩放)
- textures纹理(从像素或者纹理获得数据)
- varyings 变量(将顶点着色器的变量 传给 片元着色器)
图元装配和光栅化
什么是图元?
描述各种图形元素的函数叫做图元,描述几何元素的称为几何图元(点,线段或多边形)。点和线是最简单的几何图元 经过顶点着色器计算之后的坐标会被组装成组合图元。
通俗解释:图元就是一个点、一条线段、或者是一个多边形。
什么是图元装配呢?
简单理解就是说将我们设置的顶点、颜色、纹理等内容组装称为一个可渲染的多边形的过程。
组装的类型取决于:你最后绘制选择的图形类型
gl.drawArrays(gl.TRIANGLES, 0, 3)
如果是三角形的话,顶点着色器就执行三次
光栅化
什么是光栅化:
通过图元装配生成的多边形,计算像素并填充,剔除不可见的部分,剪裁掉不在可视范围内的部分。最终生成可见的带有颜色数据的图形并绘制。
光栅化流程图解:
剔除和剪裁
剔除:在日常生活中,对于不透明物体,背面对于观察者来说是不可见的。同样,在webgl中,我们也可以设定物体的背面不可见,那么在渲染过程中,就会将不可见的部分剔除,不参与绘制。节省渲染开销。
剪裁:日常生活中不论是在看电视还是观察物体,都会有一个可视范围,在可视范围之外的事物我们是看不到的。类似的,图形生成后,有的部分可能位于可视范围之外,这一部分会被剪裁掉,不参与绘制。以此来提高性能。这个就是视椎体,在■范围内能看到的东西,才进行绘制。
片元着色器
光珊化后,每一个像素点都包含了 颜色、深度、纹理数据,这个我们叫做片元
小tips: 每个像素的颜色由片元着色器的gl_FragColor提供
接收光栅化阶段生成的片元,在光栅化阶段中,已经计算出每个片元的颜色信息,这一阶段会将片元做逐片元挑选的操作,处理过的片元会继续向后面的阶段传递。片元着色器运行的次数由图形有多少个片元决定的。
逐片元挑选
通过模板测试和深度测试来确定片元是否要显示,测试过程中会丢弃掉部分无用的片元内容,然后生成可绘制的二维图像绘制并显示。
- 深度测试: 就是对z轴的值做测试,值比较小的片元内容会覆盖值比较大的。(类似于近处的物体会遮挡远处物体) 。
- 模板测试:模拟观察者的观察行为,可以接为镜像观察。标记所有镜像中出现的片元,最后只绘制有标记的内容。
三、WEBGL绘制三角形
初始化CANVAS
新建一个webgl画布
<canvas id="webgl" width="500" height="500"></canvas>
创建webgl 上下文:
const gl = document.getElementById('webgl').getContext('webgl')
创建着色器程序
着色器的程序这些代码,其实是重复的,我们还是先看下图,看下我们到底需要哪些步骤:
那我们就跟着这个流程图: 一步一步来好吧。
创建着色器
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
gl.VERTEX_SHADER 和 gl.FRAGMENT_SHADER 这两个是全局变量 分别表示顶点着色器 和片元着色器
绑定数据源
顾名思义: 数据源,也就是我们的着色器 代码。
编写着色器代码有很多种方式:
- 用 script 标签 type notjs 这样去写
- 模板字符串 (比较喜欢推荐这种)
我们先写顶点着色器:
const vertexShaderSource = `
attribute vec4 a_position;
void main() {
gl_Position = a_position;
}
`
顶点着色器 必须要有 main 函数 ,他是强类型语言, 记得加分号哇 不是js 兄弟们。 我这段着色器代码非常简单 定义一个vec4 的顶点位置, 然后传给 gl_Position
这里有小伙伴会问 ? 这里a_position一定要这么搞??
这里其实是这样的哇, 就是我们一般进行变量命名的时候 都会的前缀 用来区分 他是属性 还是 全局变量 还是纹理 比如这样:
uniform mat4 u_mat;
表示个矩阵,如果不这样也可以哈。 但是要专业呗,防止bug 影响。
我们接着写片元着色器:
const fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`
这个其实理解起来非常简单哈, 每个像素点的颜色 是红色 , gl_FragColor 其实对应的是 rgba 也就是颜色的表示。
有了数据源之后开始绑定:
// 创建着色器
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
//绑定数据源
gl.shaderSource(vertexShader, vertexShaderSource)
gl.shaderSource(fragmentShader, fragmentShaderSource)
是不是很简答哈哈哈哈,我觉得你应该会了。
后面着色器的一些操作
其实后面编译着色器、绑定着色器、连接着色器程序、使用着色器程序 都是一个api 搞定的事不多说了 直接看代码:
// 编译着色器
gl.compileShader(vertexShader)
gl.compileShader(fragmentShader)
// 创建着色器程序
const program = gl.createProgram()
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
// 链接 并使用着色器
gl.linkProgram(program)
gl.useProgram(program)
这样我们就创建好了一个着色器程序了。
这里又有人问,我怎么知道我创建的着色器是对的还是错的呢? 我就是很粗心的人呢??? 好的他来了 如何调试:
const success = gl.getProgramParameter(program, gl.LINK_STATUS)
if (success) {
gl.useProgram(program)
return program
}
console.error(gl.getProgramInfoLog(program), 'test---')
gl.deleteProgram(program)
getProgramParameter 这个方法用来判断 我们着色器 glsl 语言写的是不是对的, 然后你可以通过 getProgramInfoLog这个方法 类似于打 日志 去发现❌了。
数据存入缓冲区
有了着色器,现在我们差的就是数据了对吧。
上文在写顶点着色器的时候用到了Attributes属性,说明是这个变量要从缓冲中读取数据,下面我们就来把数据存入缓冲中。
首先创建一个顶点缓冲区对象(Vertex Buffer Object, VBO)
const buffer = gl.createBuffer()
gl.createBuffer()函数创建缓冲区并返回一个标识符,接下来需要为WebGL绑定这个buffer
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bindBuffer()函数把标识符buffer设置为当前缓冲区,后面的所有的数据都会都会被放入当前缓冲区,直到bindBuffer绑定另一个当前缓冲区。
我们新建一个数组 然后并把数据存入到缓冲区中。
const data = new Float32Array([0.0, 0.0, -0.3, -0.3, 0.3, -0.3])
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
因为JavaScript与WebGL通信必须是二进制的,不能是传统的文本格式,所以这里使用了ArrayBuffer对象将数据转化为二进制,因为顶点数据是浮点数,精度不需要太高,所以使用Float32Array就可以了,这是JavaScript与GPU之间大量实时交换数据的有效方法。
gl.STATIC_DRAW 指定数据存储区的使用方法: 缓存区的内容可能会经常使用,但是不会更改
gl.DYNAMIC_DRAW 表示 缓存区的内容经常使用,也会经常更改。
gl.STREAM_DRAW 表示缓冲区的内容可能不会经常使用
从缓冲中读取数据
GLSL着色程序的唯一输入是一个属性值a_position。 我们要做的第一件事就是从刚才创建的GLSL着色程序中找到这个属性值所在的位置。
const aposlocation = gl.getAttribLocation(program, 'a_position')
接下来我们需要告诉WebGL怎么从我们之前准备的缓冲中获取数据给着色器中的属性。 首先我们需要启用对应属性
gl.enableVertexAttribArray(aposlocation)
最后是从缓冲中读取数据绑定给被激活的aposlocation的位置
gl.vertexAttribPointer(aposlocation, 2, gl.FLOAT, false, 0, 0)
gl.vertexAttribPointer()函数有六个参数:
- 读取的数据要绑定到哪
- 表示每次从缓存取几个数据,也可以表示每个顶点有几个单位的数据,取值范围是1-4。这里每次取2个数据,之前vertices声明的6个数据,正好是3个顶点的二维坐标。
- 表示数据类型,可选参数有gl.BYTE有符号的8位整数,gl.SHORT有符号的16位整数,gl.UNSIGNED_BYTE无符号的8位整数,gl.UNSIGNED_SHORT无符号的16位整数,gl.FLOAT32位IEEE标准的浮点数。
- 表示是否应该将整数数值归一化到特定的范围,对于类型gl.FLOAT此参数无效。
- 表示每次取数据与上次隔了多少位,0表示每次取数据连续紧挨上次数据的位置,WebGL会自己计算之间的间隔。
- 表示首次取数据时的偏移量,必须是字节大小的倍数。0表示从头开始取。
渲染
现在着色器程序 和数据都已经ready 了, 现在就差渲染了。 渲染之前和2d canvas 一样做一个清除画布的动作:
// 清除canvas
gl.clearColor(0, 0, 0, 0)
gl.clear(gl.COLOR_BUFFER_BIT)
我们用0、0、0、0清空画布,分别对应 r, g, b, alpha (红,绿,蓝,阿尔法)值, 所以在这个例子中我们让画布变透明了。
开启绘制三角形:
gl.drawArrays(gl.TRIANGLES, 0, 3)
- 第一个参数表示绘制的类型
- 第二个参数表示从第几个顶点开始绘制
- 第三个参数表示绘制多少个点,缓冲中一共6个数据,每次取2个,共3个点
绘制类型共有下列几种 看图:
这里我们看下画面是不是一个红色的三角形 :
全部代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
}
</style>
</head>
<body>
<canvas></canvas>
<script>
// 获取canvas元素
let canvas = document.querySelector("canvas");
// 设置canvas的宽高
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// 获取webgl上下文
let gl = canvas.getContext("webgl");
// 创建顶点着色器
const vShader = gl.createShader(gl.VERTEX_SHADER);
// 顶点着色器源码
gl.shaderSource(
vShader,
`
attribute vec4 v_position;
void main(){
gl_Position = v_position;
}
`
);
// 编译顶点着色器
gl.compileShader(vShader);
// 创建片元着色器
const fShader = gl.createShader(gl.FRAGMENT_SHADER);
// 片元着色器源码
gl.shaderSource(
fShader, // vec4-> 参数rgba
`
void main(){
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`
);
// 编译片元着色器
gl.compileShader(fShader);
// 创建着色器程序链接顶点着色器和片元着色器
const program = gl.createProgram();
// 添加顶点着色器
gl.attachShader(program, vShader);
// 添加片元着色器
gl.attachShader(program, fShader);
// 链接着色器程序
gl.linkProgram(program);
// 使用着色器程序
gl.useProgram(program);
// 创建顶点数据
const position = gl.getAttribLocation(program, "v_position");
// 创建缓冲区
const pBuffer = gl.createBuffer();
// 绑定缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, pBuffer);
// 设置顶点数据
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5]),
gl.STATIC_DRAW // 静态绘制
);
// 将顶点数据提供给到atttribute变量
gl.vertexAttribPointer(
//告诉attribute变量从哪里获取数据
position,
2, // 每次迭代提供2个单位的数据
gl.FLOAT, // 每个单位的数据类型是32位浮点型
false, // 不需要归一化数据
0, // 0 步长
0 // 从缓冲区的哪个位置开始读取数据
);
// 开启attribute变量
gl.enableVertexAttribArray(position);
// 绘制
gl.drawArrays(gl.TRIANGLES, 0, 3);
</script>
</body>
</html>
我们创建的数据是这样的:
画布的宽度是 500 * 500 转换出来的实际数据其实是这样的
0,0 ====> 0,0
-0.3, -0.3 ====> 175, 325
0.3, -0.3 ====> 325, 325
四、缩放矩阵与uniform变量和varying变量
执行动画放大缩小
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvasEl = document.querySelector("#canvas");
canvasEl.width = document.body.clientWidth; // 设置 canvas 画布的宽度
canvasEl.height = document.body.clientHeight; // 设置 canvas 画布的高度
const gl = canvasEl.getContext("webgl"); // 获取 WebGL 上下文
gl.viewport(0,0,canvasEl.width, canvasEl.height )
// 创建顶点着色器 语法 gl.createShader(type) 此处 type 为枚举型值为 gl.VERTEX_SHADER 或 gl.FRAGMENT_SHADER 两者中的一个
const vShader = gl.createShader(gl.VERTEX_SHADER);
// 编写顶点着色器的 GLSL 代码 语法 gl.shaderSource(shader, source); shader - 用于设置程序代码的 webglShader(着色器对象) source - 包含 GLSL 程序代码的字符串
gl.shaderSource(
vShader,
`
attribute vec4 a_position;
uniform mat4 u_Mat;
varying vec4 v_Color;
void main() {
gl_Position = u_Mat * a_Position; // 设置顶点位置
v_Color = gl_Position;
}
`
);
gl.compileShader(vShader); // 编译着色器代码
const fShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(
fShader,
`
precision mediump float;
varying vec4 v_Color
void main() {
gl_FragColor = v_Color; // 设置片元颜色
}
`
); // 编写片元着色器代码
gl.compileShader(fShader); // 编译着色器代码
// 创建一个程序用于连接顶点着色器和片元着色器
const program = gl.createProgram();
gl.attachShader(program, vShader); // 添加顶点着色器
gl.attachShader(program, fShader); // 添加片元着色器
gl.linkProgram(program); // 连接 program 中的着色器
gl.useProgram(program); // 告诉 WebGL 用这个 program 进行渲染
// // 用于指定uniform变量在 GPU 内存中的位置
// const color = gl.getUniformLocation(program, "v_Color");
// // 获取 f_color 变量位置
// gl.uniform4f(color, 0.93, 0, 0.56, 1); // 设置它的值
// 获取 v_position 位置
const pBuffer = gl.createBuffer();
// 创建一个顶点缓冲对象,返回其 id,用来放三角形顶点数据,
gl.bindBuffer(gl.ARRAY_BUFFER, pBuffer);
// 将这个顶点缓冲对象绑定到 gl.ARRAY_BUFFER
// 后续对 gl.ARRAY_BUFFER 的操作都会映射到这个缓存
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([0, 0.5, 0.5, 0, -0.5, -0.5]), // 三角形的三个顶点
// 因为会将数据发送到 GPU,为了省去数据解析,这里使用 Float32Array 直接传送数据
gl.STATIC_DRAW // 表示缓冲区的内容不会经常更改
);
// 将顶点数据加入的刚刚创建的缓存对象
const a_Position = gl.getAttribLocation(program, "a_Position");
gl.vertexAttribPointer(
// 告诉 OpenGL 如何从 Buffer 中获取数据
a_Position, // 顶点属性的索引
2, // 组成数量,必须是 1,2,3 或 4。我们只提供了 x 和 y
gl.FLOAT, // 每个元素的数据类型
false, // 是否归一化到特定的范围,对 FLOAT 类型数据设置无效
0, // stride 步长 数组中一行长度,0 表示数据是紧密的没有空隙,让 OpenGL 决定具体步长
0 // offset 字节偏移量,必须是类型的字节长度的倍数。
);
gl.enableVertexAttribArray(a_Position);
// 开启 attribute 变量额,使顶点着色器能够访问缓冲区数据
const scale = {
x: 0.5,
y: 0.5,
z: 0.5
};
// const mat = new Float32Array([
// scale.x, 0.0, 0.0, 0.0,
// 0.0, scale.x, 0.0, 0.0,
// 0.0, 0.0, scale.x, 0.0,
// 0.0, 0.0, 0.0, 1.0,
// ])
// const u_Mat = gl.getUniformLocation(program, 'u_Mat');
// gl.uniformMatrix4fv(u_Mat, false, mat)
// gl.clearColor(0.0, 0.0, 0.0, 0.0); // 设置清空颜色缓冲时的颜色值
// gl.clear(gl.COLOR_BUFFER_BIT); // 清空颜色缓冲区,也就是清空画布
// // 语法 gl.drawArrays(mode, first, count); mode - 指定绘制图元的方式 first - 指定从哪个点开始绘制 count - 指定绘制需要使用到多少个点
// gl.drawArrays(gl.TRIANGLES, 0, 3);
function animate() {
scale.x -= 0.01;
const mat = new Float32Array([
scale.x, 0.0, 0.0, 0.0,
0.0, scale.x, 0.0, 0.0,
0.0, 0.0, scale.x, 0.0,
0.0, 0.0, 0.0, 1.0,
])
const u_Mat = gl.getUniformLocation(program, 'u_Mat');
gl.uniformMatrix4fv(u_Mat, false, mat);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(animate)
}
animate()
</script>
</body>
</html>
五、着色器glsl基本规范
什么是片段着色器?
我们将着色器描述为图形的古腾堡印刷机。为什么?更重要的是:什么是着色器?
如果你已经有使用电脑绘图的经验,你就会知道在这个过程中你会画一个圆,然后是一个矩形、一条线、一些三角形,直到你组成你想要的图像。这个过程与手写一封信或一本书非常相似——它是一组执行一项又一项任务的指令。
着色器也是一组指令,但指令是针对屏幕上的每个像素一次性执行的。这意味着您编写的代码必须根据屏幕上像素的位置表现出不同的行为。就像打字机一样,您的程序将作为一个接收位置并返回颜色的函数工作,并且当它被编译时,它会运行得非常快。
为什么着色器很快?
为了回答这个问题,我将介绍并行处理的奇迹。
将计算机的 CPU 想象成一条大工业管道,而每项任务都通过它——就像一条工厂生产线。有些任务比其他任务大,这意味着它们需要更多的时间和精力来处理。我们说它们需要更多的处理能力。由于计算机的体系结构,作业被迫以一系列方式运行;每项工作必须一次完成一项。现代计算机通常具有四个处理器组,它们像这些管道一样工作,一个接一个地完成任务以保持事情顺利运行。每根管子也称为螺纹。
视频游戏和其他图形应用程序需要比其他程序更多的处理能力。由于它们的图形内容,它们必须进行大量的逐像素操作。屏幕上的每个像素都需要计算,在 3D 游戏中也需要计算几何图形和视角。
让我们回到管道和任务的隐喻。屏幕上的每个像素都代表一个简单的小任务。单独的每个像素任务对 CPU 来说不是问题,但是(这就是问题所在)必须对屏幕上的每个像素完成微小的任务!这意味着在旧的 800x600 屏幕中,每帧必须处理 480,000 个像素,这意味着每秒要进行 14,400,000 次计算!是的!这是一个足以使微处理器超载的问题。在以每秒 60 帧的速度运行的现代 2880x1800 视网膜显示器中,每秒计算总计 311,040,000 次。图形工程师如何解决这个问题?
这是并行处理成为一个很好的解决方案的时候。与其拥有几个大而强大的微处理器或管道,不如让许多微型微处理器同时并行运行更聪明。这就是图形处理器单元 (GPU)。
将微型微处理器想象成一张管道表,将每个像素的数据想象成一个乒乓球。每秒 14,400,000 个乒乓球几乎可以阻塞任何管道。但是一张 800x600 的微型管道表可以顺利处理每秒接收 30 个 480,000 像素的波。这在更高的分辨率下也是一样的——你拥有的并行硬件越多,它可以管理的流就越大。
GPU 的另一个“超能力”是通过硬件加速的特殊数学函数,因此复杂的数学运算直接由微芯片解决,而不是由软件解决。这意味着超快的三角和矩阵运算 - 与电力一样快。
什么是 GLSL?
GLSL 代表 openGL Shading Language,它是着色器程序的特定标准,您将在接下来的章节中看到。根据硬件和操作系统,还有其他类型的着色器。在这里,我们将使用由Khronos Group监管的 openGL 规范。了解 OpenGL 的历史有助于理解其大部分奇怪的约定,为此我建议您查看:openglbook.com/chapter-0-preface-what-is-opengl.html
为什么着色器以痛苦着称?
正如本大叔所说的“能力越大责任越大”,并行计算遵循这个规则;GPU 强大的架构设计有其自身的约束和限制。
为了并行运行,每个管道或线程必须独立于每个其他线程。我们说线程对其余线程的工作视而不见。此限制意味着所有数据必须沿同一方向流动。所以不可能检查另一个线程的结果,修改输入数据,或者将一个线程的结果传递给另一个线程。允许线程到线程通信会使数据的完整性面临风险。
GPU 也使并行微处理器(管道)不断忙碌;一旦他们获得自由,他们就会收到要处理的新信息。线程不可能知道它在前一刻在做什么。它可能是从操作系统的 UI 中绘制一个按钮,然后在游戏中渲染天空的一部分,然后显示电子邮件的文本。每个线程不仅是盲目的,而且是无记忆的。除了编码一个根据位置逐像素改变结果的通用函数所需的抽象之外,盲目和无记忆的约束使得着色器在初级程序员中不太受欢迎。
“Hello world!”通常都是学习一个新语言的第一个例子。这是一个非常简单,只有一行的程序。它既是一个热情的欢迎,也传达了编程所能带来的可能性。
然而在 GPU 的世界里,第一步就渲染一行文字太难了,所以我们改为选择一个鲜艳的欢迎色,来吧躁起来!
#ifdef GL_ES
precision mediump float;
#endif
uniform float u_time;
void main() {
gl_FragColor = vec4(1.0,0.0,1.0,1.0);
}
如果你是在线阅读这本书的话,上面的代码都是可以交互的。你可以点击或者改动代码中任何一部分,尽情探索。多亏 GPU 的架构,shader 会飞速地编译和更新,这使得你的改动都会立刻出现在你眼前。试试改动第 8 行的值,看会发生什么。
尽管这几行简单的代码看起来不像有很多内容,我们还是可以据此推测出一些知识点:
- shader 语言 有一个 main 函数,会在最后返回颜色值。这点和 C 语言很像。
- 最终的像素颜色取决于预设的全局变量 gl_FragColor。
- 这个类 C 语言有内建的变量(像gl_FragColor),函数和数据类型。在本例中我们刚刚介绍了vec4(四分量浮点向量)。之后我们会见到更多的类型,像 vec3 (三分量浮点向量)和 vec2 (二分量浮点向量),还有非常著名的:float(单精度浮点型), int(整型) 和 bool(布尔型)。
- 如果我们仔细观察 vec4 类型,可以推测这四个变元分别响应红,绿,蓝和透明度通道。同时我们也可以看到这些变量是规范化的,意思是它们的值是从0到1的。之后我们会学习如何规范化变量,使得在变量间map(映射)数值更加容易。
- 另一个可以从本例看出来的很重要的类 C 语言特征是,预处理程序的宏指令。宏指令是预编译的一部分。有了宏才可以 #define (定义)全局变量和进行一些基础的条件运算(通过使用 #ifdef 和 #endif)。所有的宏都以 # 开头。预编译会在编译前一刻发生,把所有的命令复制到 #defines 里,检查#ifdef 条件句是否已被定义, #ifndef 条件句是否没有被定义。在我们刚刚的“hello world!”的例子中,我们在第2行检查了 GL_ES 是否被定义,这个通常用在移动端或浏览器的编译中。
- float类型在 shaders 中非常重要,所以精度非常重要。更低的精度会有更快的渲染速度,但是会以质量为代价。你可以选择每一个浮点值的精度。在第一行(precision mediump float;)我们就是设定了所有的浮点值都是中等精度。但我们也可以选择把这个值设为“低”(precision lowp float;)或者“高”(precision highp float;)。
- 最后可能也是最重要的细节是,GLSL 语言规范并不保证变量会被自动转换类别。这句话是什么意思呢?显卡的硬件制造商各有不同的显卡加速方式,但是却被要求有最精简的语言规范。因而,自动强制类型转换并没有包括在其中。在我们的“hello world!”例子中,vec4 精确到单精度浮点,所以应被赋予 float 格式。但是如果你想要代码前后一致,不要之后花费大量时间 debug 的话,最好养成在 float 型数值里加一个 . 的好习惯。如下这种代码就可能不能正常运行:
void main() {
gl_FragColor = vec4(1,0,0,1); // 出错
}
有很多种构造 vec4 类型的方式,试试看其他方式。下面就是其中一种方式:
vec4 color = vec4(vec3(1.0,0.0,1.0),1.0);
尽管这个例子看起来不那么刺激,它却是最最基础的 —— 我们把画布上的每一个像素都改成了一个确切的颜色。在接下来的章节中我们将会看到如何用两种输入源来改变像素的颜色:空间(依据像素在屏幕上的位置)和时间(依据页面加载了多少秒)。