WebGL实战篇(一)——绘制点、三角形

3,499 阅读7分钟

传送门:

WebGL概述——原理篇

前言

之前我们讲过了WebGL的相关原理,我们了解到WebGL绘图的方式是“连接式”的,WebGL就像是一个巨大的电路图,我们修改电路中的电路的连接方式或者是增加其中的电气元件,当我们按下开关时,这个电路就会自动运行。今天,我们就进入实战阶段,一切从0开始,手把手的带你使用原生的WebGL做一些有趣的事情。现在,我们需要从最基础的部分开始,除了WebGL的相关知识,我们也会穿插一些必要的图形学知识。从原生WebGL和图形学知识学起,就一个字:稳!

实战

WebGL是一种JavaScript API,用于在不使用插件的情况下在任何兼容的网页浏览器中呈现交互式2D和3D图形。WebGL完全集成到浏览器的所有网页标准中,可将影像处理和效果的GPU加速使用方式当做网页Canvas的一部分。WebGL元素可以加入其他HTML元素之中并与网页或网页背景的其他部分混合。WebGL程序由JavaScript编写的句柄和OpenGL Shading Language(GLSL)编写的着色器代码组成,该语言类似于C或C++,并在电脑的图形处理器(GPU)上运行。

WebGL基于HTML5 Canvas,所以我们需要使用Canvas作为载体。在通过getContext方法来获取WebGL上下文。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Drawing Triangle</title>
    </head>
    <body>
        <canvas id="canvas" width="500" height="500"></canvas>
    </body>
    <script src="1.js"></script>
</html>
/**
 * @type {HTMLCanvasElement}
 */
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl");

最简单的WebGL绘制程序

现在让我们开始编写一个最简单的WebGL程序:

/**
 * @type {HTMLCanvasElement}
 */
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl");

// 设置清空颜色缓冲区时的颜色
gl.clearColor(1.0, 1.0, 0.0, 1.0);

// 清空颜色缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);

效果如下,我们得到的就是一张黄色的图。gl.clearColor中接受RGBA四个值,这里需要注意,RGBA的值不是0~255,而是0~1,这一点需要注意下。

以上就是一个最简单的WebGL程序了。

渐入佳境

我们已经完成了一个最简单的WebGL程序,可能你还没有什么感觉,现在我们开始编写稍微复杂一点的程序,我们要开始使用着色器了。

初始化Shader

着色器(shader)分为顶点着色器(vertex shader)和片元着色器(fragment shader),它们总是两两成对出现,顶点着色器和片元着色器组成了一个WebGLProgram,我们使用哪一个shader,就是使用哪一个 WebGLProgram。使用shader的步骤如下:

  1. 创建Shader
    • 创建 WebGLShader 对象
    • WebGLShader对象中传入 shader 源代码
    • 编译 shader
  2. 创建 Program
    • 创建 WebGLProgram对象
    • WebGLProgram对象中传入之前创建的 WebGLShader对象
    • 链接 program

代码如下:

/**
 * 
 * @param {WebGLRenderingContext} gl 
 * @param {string} type 
 * @param {string} source 
 */
function createShader(gl, type, source) {
    // 创建 shader 对象
    let shader = gl.createShader(type);
    // 往 shader 中传入源代码
    gl.shaderSource(shader, source);
    // 编译 shader
    gl.compileShader(shader);
    // 判断 shader 是否编译成功
    let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if (success) {
        return shader;
    }
    // 如果编译失败,则打印错误信息
    console.log(gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
}

/**
 * 
 * @param {WebGLRenderingContext} gl 
 * @param {WebGLShader} vertexShader 
 * @param {WebGLShader} fragmentShader 
 */
function createProgram(gl, vertexShader, fragmentShader) {
    // 创建 program 对象
    let program = gl.createProgram();
    // 往 program 对象中传入 WebGLShader 对象
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    // 链接 program
    gl.linkProgram(program);
    // 判断 program 是否链接成功
    let success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (success) {
        return program;
    }
    // 如果 program 链接失败,则打印错误信息
    console.log(gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
}

/**
 * 
 * @param {WebGLRenderingContext} gl 
 * @param {string} vertexSource 
 * @param {string} fragmentSource 
 */
function initWebGL(gl, vertexSource, fragmentSource) {
    // 根据源代码创建顶点着色器
    let vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
    // 根据源代码创建片元着色器
    let fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
    // 创建 WebGLProgram 程序
    let program = createProgram(gl, vertexShader, fragmentShader);
    return program;
}

以上的三个函数就是我们的工具函数了,我们初始化 shader 都会用到上面的三个函数。

编写 Shader 程序

现在让我们开始编写shader程序吧。我们的目标是要在屏幕上显示一个点。

顶点着色器程序如下:

// 顶点着色器
const vertexShader = `
    attribute vec4 a_position;
    void main () {
        // gl_Position为内置变量,表示当前点的位置
        gl_Position = a_position;
        // gl_Position为内置变量,表示当前点的大小,为浮点类型,如果赋值是整数类型会报错
        gl_PointSize = 10.0;
    }  
`;
// 片元着色器
const fragmentShader = `
    // 设置浮点数精度
    precision mediump float;
    void main () {
        // vec4是表示四维向量,这里用来表示RGBA的值[0~1],均为浮点数,如为整数则会报错
        gl_FragColor = vec4(1.0, 0.5, 1.0, 1.0);
    }
`;

WebGL概述——原理篇中我们也讲到了顶点着色器中的 attribute是一个存储限定符,它表示这个变量是顶点信息,可以通过js传递进入 shader 中。

shader编写完毕,我们接下来要使用这个着色器了。

// 初始化shader程序
const program = initWebGL(gl, vertexShader, fragmentShader);
// 告诉WebGL使用我们刚刚初始化的这个程序
gl.useProgram(program);
// 获取shader中a_position的地址
const a_position = gl.getAttribLocation(program, "a_position");
// 往a_position这个地址中传值
gl.vertexAttrib3f(a_position, 0.0, 0.0, 0.0);

// 开始绘制,绘制类型是gl.POINTS绘制点,0表示第一个点的索引,1表示共绘制几个点
gl.drawArrays(gl.POINTS, 0, 1);

效果如下:

好了,你现在已经学会了如何绘制一个点,你学会了在js和shader之间如何传递数据。我们先通过gl.getAttribLocation获取了attribute类型变量的地址,然后使用vertexAttrib3f往这个地址中填充数据。但是这样的传递数据的方式有缺陷,这只能传递一个点的数据,如果我们有许多的点怎么办呢?现在我们稍微加大一点难度,将绘制一个点变为绘制3个点。

我们先准备3个点的数据:

const pointPos = [
    -0.5, 0.0,
    0.5, 0.0,
    0.0, 0.5
];

我们采用WebGLBuffer对象来往WebGL中传递数据,还记得下面这个图么?gl.ARRAY_BUFFER是中间的桥梁,通过这个属性,我们可以往WebGLBuffer中传入数据。

代码如下:

const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// -----------------------------------------------------
// 注意:这里必须采用类型化数组往WebGL传入attribute类型的数据
// -----------------------------------------------------
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(pointPos), gl.STATIC_DRAW);

接下来,我们需要改造之前的这一部分代码

const a_position = gl.getAttribLocation(program, "a_position");
// 我们不再采用这种方式进行传值
// gl.vertexAttrib3f(a_position, 0.0, 0.0, 0.0);
// 采用vertexAttribPointer进行传值
gl.vertexAttribPointer(
    a_position,
    2,
    gl.FLOAT,
    false,
    Float32Array.BYTES_PER_ELEMENT * 2,
    0
);

这里gl.vertexAttribPointer这个函数是告诉WebGL如何从WebGLBuffer中读取数据。

参数名含义
index指定要修改的顶点属性
size每个顶点属性的组成数量,换言之就是几个数据组成了一个顶点属性
type数据的类型
normalized是否进行归一化处理
stride顶点之间的偏移量
offset顶点属性数组中一部分字节的偏移量

参考资料:WebGLRenderingContext.vertexAttribPointer()——MDN

这里你可能会对 stride 和 offset 这两个参数感到迷惑,别着急,这里我们先不解释这两个参数,后面我们会发现它们的奥秘。

最后,我们还需要调用

gl.enableVertexAttribArray(a_position);

这一句话是告诉WebGL,在shader中的a_position这个变量读取的是当前WebGLBuffer的内容。(因为一段程序中可能不止一个WebGLBuffer)

最后,修改绘制的命令:

// 之前只绘制一个点,现在绘制3个点
gl.drawArrays(gl.POINTS, 0, 3);

效果如下:

如果我们需要绘制三角形,则把上面的绘制命令修改为:

gl.drawArrays(gl.TRIANGLES, 0, 3);

效果如下:

练习

通过鼠标点击屏幕,鼠标每点击一次,在点击的位置绘制一个点。

总结

好了,今天你已经学会了如何在WebGL中绘制点和三角形了。今天讲解了初始化Shader的方法并且编写了我们的工具函数。js往shader中如何传递数据?我们可以通过vertexAttrib3f这种方式进行传递,但是只能传递一个点的数据,所以我们改为了使用WebGLBuffer来传递数据。希望你能回想一下WebGL概述——原理篇中关于WebGLBuffer传递数据的内容。

今天留下了两个问题:

  1. gl.vertexAttribPointer的参数中strideoffset这两个参数到底是干什么的?
  2. 一段程序中有多个WebGLBuffer该如何处理呢?

下一章中我们揭晓这两个问题的答案。敬请期待。

完整代码点此查看