WebGL 在 GPU 上的工作可以总结:
- 第一步是将顶点(或数据流)转换到裁剪空间坐标;
- 第二步是基于第一部分的结果绘制像素点。
- 然后每一帧运行都会重复第1、2步
一、WebGL 工作原理
WebGL 绘图主要是发生在顶点着色器和片元着色器中,每一帧都会把所有的顶点和片元重新计算运行一遍,当许多帧连贯运行起来,就会看到顶点和颜色的动态变化效果。
顶点着色器运行原理
顶点着色器的作用是计算没一个点的位置。当执行gl.drawArrays(gl.TRIANGLES, 0, 9)
,GPU 会先执行顶点着色器代码,依次处理每一个顶点,gl_Position
是顶点着色器内置的一个变量,处理之后的坐标必须要赋值给它。
WebGL 会从 Buffer 中读取,读取之后可以进行一系列加减乘除数学运算,实现平移、旋转、缩放等变换操作,然后赋值给gl_Position
,进而实现顶点位置的定制。
下面这个 gif 动图示意了顶点着色器对每一个顶点的处理结果:每一个顶点的原始坐标乘以一个矩阵后得到新的坐标,赋值给gl_Position
,原顶点就被转换到一个新的坐标位置。
片元着色器运行原理
片元着色器的任务是计算每个像素片元的颜色。在顶点着色器执行完成之后,会进行光栅化操作,然后执行片元着色器代码,片元着色器通过给gl_FragColor
赋值,处理每一个片元。
gl_FragColor
的值可以是顶点着色器使用varying
传过来的,也可以是一个固定值。通过对gl_FragColor
进行一系列加减乘除运算得到不同的颜色值,来绘制不同的片元颜色。
下面的 gif 动图示意了片元着色器运行的时候对每一个片元的处理结果:其中v_color是在顶点着色器里处传进来赋值给gl_FragColor
,在顶点着色器里根据顶点位置进行了加减乘除运算,所以看起来每个位置的颜色都是不一样的。
需要注意的是:
WebGL 里的空间坐标默认是在 -1~1 之间,超出的会被裁剪;
颜色 RGBA 值每一个分量的范围是 0~1 之间;
二、WebGL 应用的整体流程
上面是大致的运行原理,具体到代码上,WebGL 会首先从 HTMLCanvasElement 对象中获取到执行环境(也叫上下文),之后设置上下文的状态和属性,然后创建着色器、绑定数据和渲染,代码结构如下图:
1、创建两个着色器:createShader —> shaderSource —> compileShader
2、创建着色器程序:createProgram —> attachShader —> linkProgram —> useProgram
3、创建 Buffer 数据:createBuffer —> bindBuffer —> bufferData
4、读取数据并渲染:getAttribLocation —> enableVertexAttribArray —> vertexAttribPointer —> drawArrays
看个画三角形的🌰
function render() {
// -------------------------------------------准备环境------------------------------------------------
let canvas = document.getElementById('canvas') as HTMLCanvasElement;
let gl = canvas.getContext('webgl2') as WebGLRenderingContext;
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
// -------------------------------------------准备数据------------------------------------------------
// 三个二维点坐标
let positions = [-0.5, -0.5, 0, 0, 0.5, -0.5];
// 创建一个缓冲区对象
let positionBuffer = gl.createBuffer();
// 把刚才创建的缓冲区对象绑定到gl的 ARRAY_BUFFER,设置为当前默认的缓冲区,后面的所有的数据都会都会被放入当前缓冲区,直到bindBuffer绑定另一个当前缓冲区。
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 将数据存放放到缓冲区,gl.STATIC_DRAW 指定数据存储区的使用方法:缓存区的内容可能会经常使用,但是不会更改。
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// -------------------------------------------绘制准备------------------------------------------------
// 编写 GLSL shaders 代码,是个字符串
let vertexShaderSource = `
attribute vec4 a_position;// 一个属性变量,将会从缓冲中获取数据
// 所有着色器都有一个main方法
void main() {
gl_Position = a_position; // gl_Position 是顶点着色器内置的默认变量,将buffer取到的数据赋值进去
}
`;
let fragmentShaderSource = `
// 片元着色器没有默认精度,所以我们需要设置一个精度
precision mediump float; // mediump 是一个不错的默认值,代表 中等精度
// 所有着色器都有一个main方法
void main() {
gl_FragColor = vec4(1, 0, 0.5, 1); // gl_FragColor 是片元着色器内置的变量,赋值粉色
}
`;
// 创建着色器方法,输入参数:渲染上下文,着色器类型,数据源
let vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource)!;
let fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource)!;
// 创建着色器程序
let program = createProgram(gl, vertexShader, fragmentShader)!;
// 告诉它用我们之前写好的着色程序(一个着色器对)
gl.useProgram(program);
// 获取给定 program 对象中某属性的下标指向位置。
let positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
// 激活每一个属性以便使用
gl.enableVertexAttribArray(positionAttributeLocation);
let size = 2; // 每次迭代运行提取两个单位数据
let type = gl.FLOAT; // 每个单位的数据类型是32位浮点型
let normalize = false; // 不需要归一化数据
let stride = 0; // 0 = 移动单位数量 * 每个单位占用内存(sizeof(type)),每次迭代运行运动多少内存到下一个数据开始点
let offset = 0; // 从缓冲起始位置开始读取
// 告诉显卡从当前绑定的缓冲区(bindBuffer()指定的缓冲区)中读取数据
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);
// -------------------------------------------渲染------------------------------------------------
let primitiveType = gl.TRIANGLES;
let offSet = 0;
let count = 3;
gl.drawArrays(primitiveType, offSet, count);
}
// 创建着色器
function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | undefined {
let shader: WebGLShader = gl.createShader(type)!; // 创建着色器对象
gl.shaderSource(shader, source); // 提供数据源
gl.compileShader(shader); // 编译 -> 生成着色器
let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
console.log(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
// 创建着色器程序
function createProgram(gl: WebGLRenderingContext, vertexShader: WebGLShader, fragmentShader: WebGLShader): WebGLProgram | undefined {
let program: WebGLProgram = gl.createProgram()!;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
let success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
console.log(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}
需要注意的是,寻找属性值位置(和全局属性位置)应该在初始化的时候完成,而不是在渲染循环中。
WebGL会在光栅化的时候把点位从裁剪空间转换到屏幕空间,最终在屏幕空间绘制一个三角形, 如果画布大小是 400×300 我们会得到类似以下的转换。
这是WebGL 系列的入门文章,免费订阅,如有帮助请点赞收藏,纰漏之处欢迎指正!
参考文档:
zhuanlan.zhihu.com/p/409017167
如有帮助请点赞收藏,纰漏之处欢迎指正! 也欢迎关注公众号交流知识哇😄