二、着色器
1.基本概念
使用 GLSL 的着色器(shader),GLSL 是一门特殊的有着类似于 C 语言的语法,在图形管道 (graphic pipeline) 中直接可执行的 OpenGL 着色语言。着色器有两种类型:
- 顶点着色器(Vertex Shader) :描述顶点的特征,如位置、颜色等。
- 片段着色器(Fragment Shader):进行逐片元处理,如光照。
GLSL 不同于 JavaScript, 它是强类型语言,并且内置很多数学公式用于计算向量和矩阵。快速编写着色器非常复杂,但创建一个简单的着色器并不难。在这篇文章我们将介绍使用着色器的基础知识,并且构建一个使用 Three.js 的例子来加速代码编写。
你可能记得基本原理那篇文章,一个顶点 (vertex) 是在空间中有自己 3D 坐标的点,并且通常包含些被定义的其他信息。空间本身会被坐标系统定义。在那个 3D 空间中一切都是关于形状的呈现。
注意:GLSL语法,每一句代码后都要加上;,不然会报错
1.1 顶点着色器
在script中建立顶点着色器
- 顶点着色程序,要写在
type=“x-shader/x-vertex”的script中。 void main() {…… }是主体函数。gl_Position是顶点的位置,gl_PointSize是顶点的尺寸,这种名称都是固定的,不能写成别的。- vec4() 是一个4维矢量对象。
- 将vec4() 赋值给顶点点位gl_Position 的时候,其中的前三个参数是x、y、z,第4个参数默认1.0,其含义我们后面会详解;
<script id="vertexShader" type="x-shader/x-vertex">
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 100.0;
// ...
}
</script>在js中获取顶点着色器中的文本
const VERTEX_SHADER_SOURCE = document.getElementById('vertexShader').innerText;用字符串的形式也行,不过推荐用script
const VERTEX_SHADER_SOURCE =`
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 100.0;
// ...
}
`1.2 片段着色器
片段 (或者纹理) 着色器 在计算时定义了每像素的 RGBA 颜色 — 每个像素只调用一次片段着色器。这个着色器的作用是设置 gl_FragColor 变量,也是一个 GLSL 内置变量:
在script中建立片元着色器
- 片元着色程序,要写在
type=“x-shader/x-fragment”的script中。 gl_FragColor是片元的颜色。- 将vec4() 赋值给片元颜色gl_FragColor 的时候,其中的参数是r,g,b,a。
<script id="fragmentShader" type="x-shader/x-fragment">
void main() {
gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
</script>在js中获取片元着色器的文本
const FRAGMENT_SHADER_SOURCE = document.getElementById('fragmentShader').innerText;字符串形式
const FRAGMENT_SHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
// ...
}
`1.3 绘制流程

1.4 着色器初始化
1.4.1 创建着色器对象
const shader = gl.createShader(shader);shader:着色器类型gl.VERTEX_SHADER:表示该着色器程序编译后被顶点着色器执行gl.FRAGMENT_SHADER:表示该着色器程序编译后被片元着色器执行
1.4.2 指定着色器的源码
gl.shaderSource(shaderObject, shaderSource);haderObject: 着色器对象shaderSource:着色器源码,字符串格式着色器程序
1.4.3 编译着色器
gl.compileShader(shaderObject);shaderObject:着色器对象
1.4.4 创建程序对象
创建程序对象,程序对象存在的意义是为了实现CPU和GPU的通信,控制GPU着色器的工作状态,切换不同的着色器程序。
const program = gl.createProgram();可用deleteProgram(program)删除程序对象
1.4.5 绑定着色器至程序对象
gl.attachShader(program, shader);program:程序对象shader:着色器对象
1.4.6 连接webgl上下文对象和程序对象
在执行useprogram方法之前,要先连接程序对象program的顶点和片元着色器程序,检查着色程序的错误。 通过连接测试后,才能通过useprogram方法把着色器程序传递给GPU,否则报错。
测试项
检查顶点、片元着色器程序中同名varying变量是否一一对应
检查顶点着色器程序中是否给varying变量赋值顶点数据
硬件资源有限,要检测attribute、uniform、varying变量的数量是否超出限制范围
gl.linkProgram(program);program:程序对象
1.4.7 判断程序是否连接成功
通过该方法可以判断linkProgram()方法是否连接成功,program参数指定要连接的程序对象,参数value定义了该方法执行后,需要反馈的数据,
getProgramParameter(program, value)program:程序对象valuegl.DELETE_STATUS:是否执行deleteProgram删除程序对象program,返回结果true或falsegl.LINK_STATUS:程序对象program是否通过linkProgram()方法连接验证,返回结果true或falsegl.VALIDATE_STATUS:程序对象program是否通过验证,返回结果true或falsegl.ATTACHED_SHADERS:分配给程序的着色器对象数量gl.ACTIVE_ATTRIBUTES:attribute变量的数量gl.ACTIVE_UNIFORMS:uniform变量的数量
1.4.8 启动程序对象
gl.useProgram(program);program:程序对象
1.5 封装初始化函数
以下代码以后是不变的,所以需要封装,在script标签中引入使用
/**
* 初始化着色器
* @param {any} gl webgl绘制对象
* @param {any} VERTEX_SHADER_SOURCE 顶点着色器源码
* @param {any} FRAGMENT_SHADER_SOURCE 片段着色器源码
* @returns {any}
*/
const initShader = (gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE) => {
// 1.创建着色器对象
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
// 2.指定着色器的源码
gl.shaderSource(vertexShader, VERTEX_SHADER_SOURCE);
gl.shaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE);
// 3.编译着色器
gl.compileShader(vertexShader);
gl.compileShader(fragmentShader);
// 4.创建一个程序对象
const program = gl.createProgram();
// 5.把着色器对象装进程序对象中
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
// 6.连接webgl上下文对象和程序对象
gl.linkProgram(program);
// 7.启动程序对象
gl.useProgram(program);
// 8.返回程序对象,方便以后使用
return program;
};2.webgl坐标系
与canvas坐标系不同,webgl坐标系的原点在canvas元素的中间,x轴向右,y轴向上,并且多了一个z轴,方向往我们的视线方向
webgl坐标系中每个正半轴的范围都是0~1,负半轴范围是-1~ 0

2.1 绘制一个点
首先要用script标签引入我们封装的初始化着色器initShader
const canvas = document.querySelector("#canvas");
const gl = canvas.getContext("webgl");
// 顶点着色器
const VERTEX_SHADER_SOURCE = `
void main() {
gl_Position = vec4(0.0,0.0,0.0,1.0); // 点位置在坐标原点
gl_PointSize = 30.0; // 点的大小为30像素
}
`;
const FRAGMENT_SHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0); // 点的颜色为红色,rgba,r为1
}
`;
const program = initShader(
gl,
VERTEX_SHADER_SOURCE,
FRAGMENT_SHADER_SOURCE
);
// 绘制点
gl.drawArrays(gl.POINTS, 0, 1);可以看到绘制的点在坐标原点,也就是canvas元素的中心

3.着色器变量
我们通过上面的方式绘制一个点,只能通过vec4()的方式,写入一个固定的数据
但是我们想通过js的方式,灵活的控制变量,于是我们就可以通过使用变量的方式
3.1 attribute变量
3.1.1 作用
js 可以通过attribute 变量向顶点着色器传递与顶点相关的数据。
- attribute 变量是只有顶点着色器才能使用它的。
- attribute类型只能指定为float、vec2、vec3、vec4、mat2、mat3和mat4。
3.1.2 js控制顶点位置
1.声明attribute变量
- 声明attribute变量要在glsl语言中,main函数外定义
- attribute变量只能在顶点着色器中使用
- 需要将定义的变量赋值给
gl_Position - 注意要加
;
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
gl_PointSize = 50.0;
}
</script>
- attribute 是存储限定符,是专门用于向外部导出与点位相关的对象的,这类似于es6模板语法中export 。
- vec4 是变量类型,vec4是4维矢量对象。
- a_Position 是变量名,之后在js中会根据这个变量名导入变量。这个变量名是一个指针,指向实际数据的存储位置。也是说,我们如果在着色器外部改变了a_Position所指向的实际数据,那么在着色器中a_Position 所对应的数据也会修改。
2.获取attribute变量
- 通过
gl.getAttribLocation() - 获取 attribute 变量需要在 initShader 函数之后,因为会用到 program 这个程序对象。
- 返回变量的存储地址
const location=gl.getAttribLocation(program,'a_Position');参数
- gl 是webgl 的上下文对象。
- getAttribLocation() 方法的参数中:
- program 是初始化着色器时,在上下文对象上挂载的程序对象。
- 'a_Position' 是着色器暴露出的变量名。
3.修改attribute 变量
gl.vertexAttrib1f(location,v0)
gl.vertexAttrib2f(location,v0,v1)
gl.vertexAttrib3f(location,v0,v1,v2)
gl.vertexAttrib4f(location,v0,v1,v2,v3)参数
location:通过gl.getAttribLocation()获取的着色器变量v0、v1、v2:顶点的x、y、z位置,浮点数,范围在0.0-1.0,默认值为0.0v3:默认为1.0,其数值类型为float 浮点型。
4.webgl函数命名规律
GLSL ES里函数的命名结构是:<基础函数名><参数个数><参数类型>
以vertexAttrib3f(location,v0,v1,v2,v3) 为例:
- vertexAttrib:基础函数名
- 3:参数个数,这里的参数个数是要传给变量的参数个数,而不是当前函数的参数个数
- f:参数类型,f 代表float 浮点类型,除此之外还有i 代表整型,v代表数字……
3.1.3 用鼠标绘制点
1.获取鼠标在canvas元素中的位置
canvas.onclick = (e) => {
// 获取鼠标相对文档左上角的位置
const x = e.clientX;
const y = e.clientY;
// 获取canvas元素盒子信息
const rect = e.target.getBoundingClientRect();
// 相减得到鼠标相对canvas左上角的坐标
const domX = x - rect.x;
const domY = y - rect.y;
}2.canvas 坐标系转webgl 坐标系
由于webgl坐标系相对于canvas坐标系是向右下方移动了一半canvas高和一半canvas宽,并且需要将距离转化为0.0-1.0的小数从而修改attribute变量
// 获取canvas元素一半的宽高
const harfWidth = canvas.offsetWidth / 2;
const harfHeight = canvas.offsetHeight / 2;
const changePoint = (x, y) => {
// 左边向右下平移一半宽高,并转化为0.0-1.0的浮点数
x = (x - harfWidth) / harfWidth;
y = (harfHeight - y) / harfHeight;
// 返回最终浮点数
return {
x,
y,
};
};3.最后修改attribute变量
canvas.onclick = (e) => {
const x = e.clientX;
const y = e.clientY;
const rect = e.target.getBoundingClientRect();
const domX = x - rect.x;
const domY = y - rect.y;
const newPoint = changePoint(domX, domY);
// 修改attribute变量
gl.vertexAttrib2f(aPosition, newPoint.x, newPoint.y);
// 绘制
gl.drawArrays(gl.POINTS, 0, 1);
};
4.整体代码
const canvas = document.querySelector("#canvas");
const gl = canvas.getContext("webgl");
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
void main() {
gl_Position = aPosition;
gl_PointSize = 30.0;
}
`;
const FRAGMENT_SHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`;
const program = initShader(
gl,
VERTEX_SHADER_SOURCE,
FRAGMENT_SHADER_SOURCE
);
const aPosition = gl.getAttribLocation(program, "aPosition");
const harfWidth = canvas.offsetWidth / 2;
const harfHeight = canvas.offsetHeight / 2;
const changePoint = (x, y) => {
x = (x - harfWidth) / harfWidth;
y = (harfHeight - y) / harfHeight;
return {
x,
y,
};
};
canvas.onclick = (e) => {
const x = e.clientX;
const y = e.clientY;
const rect = e.target.getBoundingClientRect();
const domX = x - rect.x;
const domY = y - rect.y;
const newPoint = changePoint(domX, domY);
gl.vertexAttrib2f(aPosition, newPoint.x, newPoint.y);
// 绘制点的方法
gl.drawArrays(gl.POINTS, 0, 1);
};5.绘制多个点
const canvas = document.querySelector("#canvas");
const gl = canvas.getContext("webgl");
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute float aPointSize;
void main() {
gl_Position = aPosition;
gl_PointSize = aPointSize;
}
`;
const FRAGMENT_SHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`;
const program = initShader(
gl,
VERTEX_SHADER_SOURCE,
FRAGMENT_SHADER_SOURCE
);
const aPosition = gl.getAttribLocation(program, "aPosition");
const harfWidth = canvas.offsetWidth / 2;
const harfHeight = canvas.offsetHeight / 2;
const changePoint = (x, y) => {
x = (x - harfWidth) / harfWidth;
y = (harfHeight - y) / harfHeight;
return {
x,
y,
};
};
// 创建数组用于存放点
const points = [];
canvas.onclick = (e) => {
const x = e.clientX;
const y = e.clientY;
const rect = e.target.getBoundingClientRect();
const domX = x - rect.x;
const domY = y - rect.y;
const newPoint = changePoint(domX, domY);
// 数组中心增点击的坐标
points.push({
x: newPoint.x,
y: newPoint.y,
});
// 遍历数组绘制每一个点
points.forEach((item) => {
gl.vertexAttrib2f(aPosition, item.x, item.y);
gl.drawArrays(gl.POINTS, 0, 1);
});
};3.1.4 js控制顶点尺寸
1.定义attribute变量
尺寸是浮点类型的,所以用float
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
attribute float a_PointSize;
void main(){
gl_Position = a_Position;
gl_PointSize = a_PointSize;
}
</script>2.获取attribute 变量
const a_PointSize=gl.getAttribLocation(gl.program,'a_PointSize');3.修改attribute 变量
gl.vertexAttrib1f(a_PointSize,100.0);4.点击绘制不同尺寸点
const canvas = document.querySelector("#canvas");
const gl = canvas.getContext("webgl");
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute float aPointSize;
void main() {
gl_Position = aPosition;
gl_PointSize = aPointSize;
}
`;
const FRAGMENT_SHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`;
const program = initShader(
gl,
VERTEX_SHADER_SOURCE,
FRAGMENT_SHADER_SOURCE
);
const aPosition = gl.getAttribLocation(program, "aPosition");
const aPointSize = gl.getAttribLocation(program, "aPointSize"); //获取尺寸attribute变量
const harfWidth = canvas.offsetWidth / 2;
const harfHeight = canvas.offsetHeight / 2;
const changePoint = (x, y) => {
x = (x - harfWidth) / harfWidth;
y = (harfHeight - y) / harfHeight;
return {
x,
y,
};
};
const points = [];
canvas.onclick = (e) => {
const x = e.clientX;
const y = e.clientY;
const rect = e.target.getBoundingClientRect();
const domX = x - rect.x;
const domY = y - rect.y;
const newPoint = changePoint(domX, domY);
points.push({
x: newPoint.x,
y: newPoint.y,
size: Math.random() * 50, // 生成随尺寸
});
points.forEach((item) => {
gl.vertexAttrib2f(aPosition, item.x, item.y);
gl.vertexAttrib1f(aPointSize, item.size); // 传入尺寸信息
gl.drawArrays(gl.POINTS, 0, 1);
});
};3.2 uniform变量
3.2.1 作用
attribute变量只有顶点着色器才能使用,使用片元着色器,你就需要使用uniform变量
uniform变量可以指定为除数组和结构体之外的任意类型。
uniform变量可以在顶点着色器和片元着色器中使用,且必须是全局变量
3.2.2 语法
1.定义uniform变量
在片元着色器里把控制顶点颜色的变量暴露出来。
<script id="fragmentShader" type="x-shader/x-fragment">
uniform vec4 u_FragColor;
void main() {
gl_FragColor = u_FragColor;
}
</script>- uniform:限定符
- vec4:4维的变量类型
- u_FragColor:变量名。
2.设置精度
通过precision <精度> float来设置精度,这个必须要有,不然画不出东西来。
片元着色器中一定要设置精度
- highp:高精度
- mediump:中精度
- lowp:低精度
<script id="fragmentShader" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_FragColor;
void main() {
gl_FragColor = u_FragColor;
}
</script>3.获取uniform变量
const location=gl.getUniformLocation(gl.program,'u_FragColor');4.修改uniform变量
gl.uniform1f(location,v0);
gl.uniform2f(location,v0,v1);
gl.uniform3f(location,v0,v1,v2);
gl.uniform4f(location,v0,v1,v2,v3);参数:
location:指定uniform变量的存储位置v0,v1,v2:颜色的r、g、b色值,浮点数,范围在0.0-1.0,v3:透明度,浮点数,范围在0.0-1.0,默认值1.0
5.点击生成不同颜色点
const canvas = document.querySelector("#canvas");
const gl = canvas.getContext("webgl");
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute float aPointSize;
void main() {
gl_Position = aPosition;
gl_PointSize = aPointSize;
}
`;
const FRAGMENT_SHADER_SOURCE = `
precision mediump float;
uniform vec4 u_FragColor;
void main() {
gl_FragColor = u_FragColor;
}
`;
const program = initShader(
gl,
VERTEX_SHADER_SOURCE,
FRAGMENT_SHADER_SOURCE
);
const aPosition = gl.getAttribLocation(program, "aPosition");
const aPointSize = gl.getAttribLocation(program, "aPointSize");
const aFragColor = gl.getUniformLocation(program, "u_FragColor"); // 获取颜色uniform变量
const harfWidth = canvas.offsetWidth / 2;
const harfHeight = canvas.offsetHeight / 2;
const changePoint = (x, y) => {
x = (x - harfWidth) / harfWidth;
y = (harfHeight - y) / harfHeight;
return {
x,
y,
};
};
const points = [];
canvas.onclick = (e) => {
const x = e.clientX;
const y = e.clientY;
const rect = e.target.getBoundingClientRect();
const domX = x - rect.x;
const domY = y - rect.y;
const newPoint = changePoint(domX, domY);
points.push({
x: newPoint.x,
y: newPoint.y,
size: Math.random() * 50,
color: { // 生成随机颜色
r: Math.random(),
g: Math.random(),
b: Math.random(),
},
});
points.forEach((item) => {
gl.vertexAttrib2f(aPosition, item.x, item.y);
gl.vertexAttrib1f(aPointSize, item.size);
// 传入颜色信息
gl.uniform4f(
aFragColor,
item.color.r,
item.color.g,
item.color.b,
1.0
);
gl.drawArrays(gl.POINTS, 0, 1);
});
};6.uniform4vf()
我们在改变uniform 变量的时候,既可以用uniform4f() 方法一个个的写参数,也可以用uniform4fv() 方法传递类型数组。
- uniform4f 中,4 是有4个数据,f 是float 浮点类型,在我们上面的例子里就是r、g、b、a 这四个颜色数据。
- uniform4fv 中,4f 的意思和上面一样,v 是vector 矢量的意思,这在数学里就是向量的意思。由之前的4f 可知,这个向量由4个浮点类型的分量构成。
在上面呢的案例中,我们可以知道,在修改uniform变量的时候,这两种写法是一样的:
gl.uniform4f(u_FragColor,1.0,1.0,0.0,1.0);
//等同于
const color=new Float32Array([1.0,1.0,0.0,1.0]);
gl.uniform4fv(u_FragColor,color);uniform4f() 和uniform4fv() 也有着自己的同族方法,其中的4 可以变成1|2|3。
uniform4fv() 方法的第二个参数必须是Float32Array 数组,不要使用普通的Array 对象。
Float32Array 是一种32 位的浮点型数组,它在浏览器中的运行效率要比普通的Array 高很多。
3.3 varying变量
3.3.1 作用
用作顶点着色器向片元着色器传递数据
varying变量只能是以下类型:float、vec2、vec3、vec4、mat2、mat3和mat4。
3.3.2 语法
varying变量必须同时在两个着色器中声明
顶点着色器
- 声明后在
main()函数中给变量赋值
varying vec4 vColor;
void main() {
vColor = vec4(1.0,0.0,0.0,1.0);
}片元着色器
precision lowp float;
varying vec4 vColor;
void main() {
gl_FragColor = vColor;
}