日期:2021年6月11日标签:ComputerGraphics

【webgl】绘制一个三角形 #

作为webgl的第一篇文章,首先我推荐几个学习webgl(opengl)的资源:

learnopengl中文官网:https://learnopengl-cn.github.io/, 这个教程是学习opengl最好的地方,因为webgl是基于opengl的,所以我认为这也是学习webgl最好的地方,如果你熟悉C++的话,建议根据这个教程学习。

webgl理论基础:https://webglfundamentals.org/webgl/lessons/zh_cn/, 这个教程是讲解webgl的,从原理上讲解webgl,但是我认为这个教程没有learnopengl中文官网的教程详细,很多地方直接帖代码,并没有讲解清楚。但是它的确是我所看到的讲解webgl中不错的教程。

learnwebgl: http://learnwebgl.brown37.net/index.html, 一套完整的webgl教程,但是只有英文版本,我也只是看了其中的几篇文章,内容讲解的很详细。

这篇文章主要讲解webgl的完整的绘制过程。

一. 绘制过程 #

gl的绘制过程可以分为以下四个步骤:

  • 准备工作:这个阶段主要工作是一些开始工作,例如创建WebGLRenderingContext、清屏、设置z缓冲、告诉webgl绘制的窗口尺寸大小等。
  • 创建着色器程序:使用GLSL语言编写着色器代码,编译着色器,链接着色器程序。
  • 传递数据:给之前创建的着色器程序,传递数据(attributes,uniform)。
  • 绘制:调用drawArrays或者drawElements方法绘制。

二. 准备阶段 #

这个阶段首先是创建WebGLRenderingContext,你可以将其看作是一根画笔,我们可以利用它完成三维图形的绘制。

const gl = canvas.getContext("webgl");
if (!gl) {
    alert("你不能使用webgl");
}

告诉webgl绘制的目标窗口大小。

gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

使用指定颜色清空屏幕,防止之前的绘制污染画布。

gl.clearColor(0, 0, 0, 0); // 设定clearColor的颜色缓冲
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 用clearColor的颜色,清屏

三.创建着色器程序 #

着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。着色器入口点是main函数。与Javascript不同的是GLSL是强类型语言,如果你习惯了javascript这种弱类型语言,开始写GLSL可能会有些不习惯,例如将int类型传递给float类型会导致编译错误,不过多写总会适应。

在webGL(openGL)中所有的东西都是3D的,但是显示器的屏幕却是2D的,所以webGL的主要工作就是将3D的点转化成屏幕上的2D的点。这个过程由图形渲染管线(简称管线)管理,管线的主要工作分为两部分:

  1. 将3D的点坐标转化为2D的点坐标
  2. 把2D的坐标转化成有颜色的像素

以下是learnoopengl中文官网给出的着色器的定义。

图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。

管线的主要工作过程可用下图表示:

image-20210611171642853

我们一般只关心顶点着色器和片段着色器,非必要一般不会去写几何着色器。

接下来,通过绘制一个三角形,来讲解。

顶点着色器 #

顶点着色器主要负责,接收用户输入的点坐标数据和每个点的颜色数据。因为我们要绘制一个三角形,所以需要传入三角形的顶点坐标,在webgl中顶点坐标通过attribute传递。下面是顶点着色器的代码。

// 顶点着色器代码
const vertex_source = `
    attribute vec3 aPos;
    
    void main() {
        gl_Position = vec4(aPos, 1);
    }
`;

gl_Position是webgl内置的变量,用于接收要绘制的顶点坐标。

接下来创建并编译顶点着色器,使用getShaderParameter获取编译信息,抛出编译失败错误。

var vertexShader = gl.createShader(gl.VERTEX_SHADER);

gl.shaderSource(vertexShader, vertex_source); // 绑定着色器代码
gl.compileShader(vertexShader); // 编译着色器
var success = gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS);
if (!success) {
    throw "colud not compile vertex shader:" + gl.getShaderInfoLog(vertexShader);
}

片段着色器 #

片段着色器主要负责处理当前顶点的颜色,也是利用webgl内置的变量gl_FragColor接受颜色值,传递给下一个阶段。下面是片段着色器代码。

// 片段着色器代码
const fragment_source = `
    precision mediump float;
    uniform vec3 uColor;

    void main() {
        gl_FragColor = vec4(uColor, 1.0);
    }
`;

这里引入了一个uniform全局变量,与attribute不同的是,uniform在每个顶点处理中都保持一致,所以称为全局变量。

同样,也需要创建和编译片段着色器。

// 创建片段着色器
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragment_source); // 绑定着色器代码
gl.compileShader(fragmentShader); // 编译着色器
success = gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS);
if (!success) {
    throw "colud not compile vertex shader:" + gl.getShaderInfoLog(fragmentShader);
}

链接着色器程序 #

将顶点着色器和片段着色器链接成一个程序。和编译着色器类似,也可以通过getProgramParameter方法获得链接信息,如果失败抛出链接错误。最后链接成功了,删除着色器。最后程序运行必须要调用useProgram方法使用创建的着色器程序,useProgram必须在设置变量数据前调用。

// 链接着色器程序
var program = gl.createProgram();
gl.attachShader(program, vertexShader); // 添加着色器
gl.attachShader(program, fragmentShader); 
gl.linkProgram(program);
success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!success) {
    throw "colud not link shader: " + gl.getProgramInfoLog(program);
}
// 已经链接成程序,可以删除着色器
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);

gl.useProgram(program);

四. 传递数据 #

在glsl中,我们设定了一个attribute变量和一个uniform变量,需要我们手动为这两个变量赋值。

attribute #

attribute接受的是一组数据,通常是顶点坐标值或者顶点的颜色值。过程如下:

  • 获得变量位置
  • 创建缓冲
  • 绑定缓冲到指定类型的缓冲
  • 向缓冲输入数据
  • 指示程序如何解析传递的数据
  • 启用缓冲位置
// 三角形的顶点数据
const vertexs: number[] = [
    0, 0.5, 0,
    -0.5, 0, 0,
    0.5, 0, 0
];

const aPosAttLocation = gl.getAttribLocation(program, "aPos"); // 获得变量位置
const aPosAttBuffer = gl.createBuffer(); // 创建缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, aPosAttBuffer); // 绑定缓冲到指定类型
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexs), gl.STATIC_DRAW); // 传入数据

gl.vertexAttribPointer(aPosAttLocation, 3, gl.FLOAT, false, 0, 0); // 告诉gl如何解析数据
gl.enableVertexAttribArray(aPosAttLocation); // 启用数据

因为,webgl的标准屏幕空间范围为-1~+1,所以这里坐标中的0.5表示在画布的四分之一位置,关于屏幕空间和现代openGl中的坐标系统,我将在下一篇文章中讲解。

uniform #

uniform全局变量,在所有顶点渲染过程中保持一致,传递数据过程相对比较简单。

const uColorLocation = gl.getUniformLocation(program, "uColor");
gl.uniform3f(uColorLocation, 0.36, 0.42, 0.60);

将三角形的颜色设置为rgb(0.36, 0.42, 0.60)。

五.绘制 #

直接调用如下代码绘制:

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

gl.TRIANGLES指定了绘制的模式,表示绘制一个三角形,第二个参数表示需要运行的起始顶点索引,第三个表示着色器运行的总的顶点个数。

按照上面的步骤,打开浏览器,应该能看到下图所示的三角形:

image-20210611174903110

如果你存在疑惑,没有得到想要的结果,可以在这里找到源码:https://github.com/pengfeiw/webgl-demos/blob/master/src/demos/webglStart/index.ts

(完)

目录