WebGL 与动画实现 | 青训营笔记

313 阅读8分钟

这是我参与「第四届青训营 」笔记创作活动的的第13天

初识WebGL

Why WebGL / Why GPU?

  • WebGL 是什么?
    • GPU ≠ WebGL ≠ 3D
  • WebGL 为什么不像其他前端技术那么简单?

WebGL(Web 图形库)是一个 JavaScript API,可在任何兼容的 Web 浏览器中渲染高性能的交互式 3D 和 2D 图形,而无需使用插件。WebGL 通过引入一个与 OpenGL ES 2.0 非常一致的 API 来做到这一点,该 API 可以在 HTML5 <canvas>元素中使用。 这种一致性使 API 可以利用用户设备提供的硬件图形加速。

Modern Graphics System(现代图形系统)

image.png

  • 光栅(Raster):几乎所有的现代图形系统都是基于光栅来绘制图形的,光栅就是指构成图像的像素阵列。
  • 像素(Pixel) :一个像素对应图像上的一个点,它通常保存图像上的某个具体位置的颜色等信息。
  • 帧缓存(Frame Buffer):在绘图过程中,像素信息被存放于帧缓存中,帧缓存是一块内存地址。
  • CPU (Central Processing Unit):中央处理单元,负责逻辑计算。
  • GPU (Graphics Processing Unit):图形处理单元,负责图形计算。

image.png

  • 如上图,现代图像的渲染如图过程

    • 轮廓提取/ meshing
    • 光栅化
    • 帧缓存
    • 渲染

The Pipeline

image.png

CPU vs GPU

image.png

上面这张图,是CPU的工作原理,它就像是一个管道,数据(图中的箱子)从左侧输入,在CPU中完成处理,然后从右侧输出。CPU是由多个这样的管道构成的,每个管道我们叫做一个CPU内核,如果你打开你电脑的操作系统,查看本机信息,你可能会看到类似这样的信息:2.6 GHz 六核Intel Core i7,这里的六核,你可以理解成有6个这样的管道,因此可以同时处理6个任务。

CPU的工作能力,与管道本身的处理速度(频率)合管道的数量(内核数)有关系,频率越高,那么运算处理单一任务的速度就越快,内核数越多,那么能同时并行处理的任务数就越多。

虽然现代计算机的CPU运算能力很强,但是它也有局限性,对于某些场景,它并不擅长,比如图形渲染

我们知道,计算机图像是由像素构成,所谓像素,可以简单理解为最终呈现在显示设备上的一个1x1的颜色小方块。

image.png

现在的显示设备非常先进,可以用非常多的像素小方块来精确构图。前端的CSS中的px单位,就是像素单位,一张800px长、600px宽的图片,逻辑上是由600*800,也就是48万个像素点构成的。如果要对这张图片的像素进行计算,用CPU来运算的话,单核CPU需要处理48万个微小任务,就像下面这张图:

image.png

不是说CPU不能完成这样的处理,每一个像素的计算可能是非常简单的(只是处理一下颜色),但是数量太多,对CPU这样的结构也会造成负担。因此,在这个时候,另外一种高并发结构,也就是GPU就登场了。

image.png

GPU

  • GPU由大量的小运算单元构成
  • 每个运算单元只负责处理很简单的计算
  • 每个运算单元彼此独立
  • 因此所有计算可以并行处理

与CPU不同,GPU可以看成是由数量非常多的微小管道构成的结构,每一个管道恰好可以处理“一粒沙子”,这样,如果对于一张600像素x800像素的图片,有48万个管道组成的GPU,就可以同时处理这48万个像素点了!事实上,GPU几乎就是这样做的。

WebGL & OpenGL

OpenGL, OpenGL ES, WebGL, GLSL, GLSL ES APIs Table image.png

WebGL Startup

  • 创建WebGL上下文

  • 创建WebGL Program

  • 将数据存入缓冲区

  • 将缓冲区数据读取到GPU

  • GPU执行WebGL程序,输出结果

image.png

Create WebGL Context

const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');

// 创建上下文, 注意兼容
function create3DContext(canvas, options) {
    const names = ['webgl', 'experimental-webgL','webkit-3d','moz-webgl'];  // 特性判断
    if(options.webgl2) names.unshift(webgl2);
    let context = null;
    for(let ii = 0; ii < names.length; ++ ii) {
        try {
            context = canvas.getContext(names[ii], options);
        } catch(e) {
            // no-empty
        }
        if(context) {
            break;
        }
    }
    return context;
}

The Shaders

  • Vertex Shader(顶点着色器): 通过类型数组position,并行处理每个顶点的位置
attribute vec2 position; // vec2 二维向量
void main() {
    gl_PointSize = 1.0;
    gl_Position = vec4(position, 1.0, 1.0);
}

  • Fragment Shader(片元着色器):为顶点轮廓包围的区域内所有像素进行着色
precision mediump float;
void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);//对应rgba(255,0,0,1.0),红色
}

Create Program

  • 使用 createShader() 创建着色器对象

  • 使用 shaderSource() 设置着色器的程序代码

  • 使用 compileShader()编译一个着色器

  • 使用createProgram()创建 WebGLProgram 对象

  • 使用 attachShader()WebGLProgram添加一个片段或者顶点着色器。

  • 使用 linkProgram() 链接给定的WebGLProgram,从而完成为程序的片元和顶点着色器准备GPU代码的过程。

  • 使用 useProgram() 将定义好的WebGLProgram 对象添加到当前的渲染状态

// 顶点着色器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertex);
gl.compileShader(vertexShader);

// 片元着色器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragment);
gl.compileShader(fragmentShader);

// 创建着色器程序并链接
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);

gl.useProgram(program);

Data to Frame Buffer

坐标轴: WebGL的坐标系统是归一化的,浏览器和canvas2D的坐标系统是以左上角为坐标原点,y轴向下,x轴向右,坐标值相对于原点。而WebGL的坐标系是以绘制画布的中心点为原点,正常的笛卡尔坐标系。

通过一个顶点数组表示其顶点,使用 createBuffer() 创建并初始化一个用于储存顶点数据或着色数据的WebGLBuffer对象并返回bufferId,然后使用 bindBuffer() 将给定的 bufferId 绑定到目标并返回,最后使用bufferData(),将数据绑定至buffer中。

Axes

image.png

// 顶点数据
const points = new Float32Array([
    -1, -1,
    0, 1,
    1, -1,
]);
// 创建缓冲区
const bufferId = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);

Frame Buffer to GPU

getAttribLocation() 返回了给定WebGLProgram对象中某属性的下标指向位置。

vertexAttribPointer() 告诉显卡从当前绑定的缓冲区(bindBuffer()指定的缓冲区)中读取顶点数据。

enableVertexAttribArray() 可以打开属性数组列表中指定索引处的通用顶点属性数组。

const vPosition = gl.getAttribLocation(program, 'position'); // 获取顶点着色器中的position变量的地址
gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0); // 给变量设置长度和类型
gl.enableVertexAttribArray(vPosition); // 激活这个变量

Output

drawArrays() 从向量数组中绘制图元

// output
gl.clear(gl.COLOR_BUFFER_BIT);  //清除缓冲的数据
gl.drawArrays(gl.TRIANGLES, 0, points.length / 2);

WebGL 难在哪儿?

canvas 2D

Canvas 是 HTML5 提供的一个特性,可以把它当做一个载体,简单的说就是一张白纸。而 Canvas 2D 相当于获取了内置的二维图形接口,也就是二维画笔。

canvas2D,绘制同样的三角形:

// canvas 简单粗暴,都封装好了
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(250, 0);
ctx.lineTo(500, 500);
ctx.lineTo(0, 500);
ctx.fillStyle = 'red';
ctx.fill();

Mesh.js

const {Renderer, Figure2D, Mesh2D} = meshjs;
const canvas = document.querySelector ('canvas');
const renderer = new Renderer(canvas);

const figure = new Figure2D();
figurie.beginPath();
figure.moveTo(250, 0);
figure.lineTo(500,500);
figure.lineTo(0, 500);
const mesh = new Mesh2D(figure, canvas);
mesh.setFill({
    color: [1, 0, 0, 1],
});
renderer.drawMeshes([mesh]);

Polygons

Polygons(多边形)

image.png

Triangulations(三角形剖分)

image.png

Draw Polygon with 2D Triangulations(绘制二维三角剖分多边形)

使用Earcut进行三角剖分

image.png

const vertices = [        [-0.7, 0.5],
    [-0.4, 0.3],
    [-0.25, 0.71],
    [-0.1, 0.56],
    [-0.1, 0.13],
    [0.4, 0.21],
    [0, -0.6],
    [-0.3, -0.3],
    [-0.6, -0.3],
    [-0.45, 0.0],
];
const points = vertices.flat();
const triangles = earcut(points)

3D Meshing

SpriteJS

image.png

Tranforms

image.png

image.png

Apply Tranforms

image.png

Matrix

  • 3D标准模型的四个齐次矩阵(mat4)

    • 投影矩阵 Projection Matrix(正交投影和透视投影)

    • 模型矩阵 Model Matrix (对顶点进行变换Transform)

    • 视图矩阵 View Matrix(3D的视角,想象成一个相机,在相机的视口下)

    • 法向量矩阵 Normal Matrix(垂直于物体表面的法向量,通常用于计算物体光照)

拓展阅读

专栏阅读:充分理解WebGL

  • Fragment Shaders(片段着色器)的入门指南: The Book of Shaders

  • 一个为可视化而生的图形系统 (A graphics system born for visualization)

  • 这是 Fragment Shaders 的一个轻量库,可以用来作为 WebGL 学习入门,也可以用在项目中实现高性能的、有趣的图案和图形效果。

  • 这是 Fragment Shaders 的一个轻量库,可以用来作为 WebGL 学习入门,也可以用在项目中实现高性能的、有趣的图案和图形效果。

  • ThreeJS

image.png

image.png

image.png

总结

在前端方向里,WebGL算是典型的小众领域,真正理解的人并不多。

之所以这样,是因为WebGL的技术栈和传统的Web前端技术有极大的差别。相对而言,传统Web前端使用的API比较高级,不存在太多需要理解的底层原理合概念,而WebGL的核心是OpenGL,它是OpenGL在Web上的实现。OpenGL是通过操作GPU来完成图形绘制渲染的,因此它的API相对比较底层,使用起来较为繁琐,这使得一些习惯于前端开发的工程师很难适应,所以就会觉得学习门槛较高。

实际上,要理解和学会WebGL,并没有那么困难,我们只需要理解一下GPU,了解它与CPU的不同点,然后再理解运行GPU代码的语言——glsl,了解着色器的基本概念和用法,就可以轻松理解WebGL的本质原理,然后在花一点时间和耐心,慢慢学习WebGL的API,就可以掌握WebGL这门技术了。

拓展链接:【零基础】充分理解WebGL(一)