使用 WebGL 创建 2D 内容

一旦创建WEBGL上下文成功,你就可以在这个上下文里渲染画图了。而对我们而言最简单的事,莫过于绘制一个没有纹理的2D图形了。那就让我们从画出一个正方形开始吧。

渲染场景

在开始前,我们首先需要明确最重要的一点,虽然我们的例子只是画一个二维物体,但我们仍然是在把它画在一个三维空间里。所以,我们依然需要创建着色器,通过它来渲染我们的简单场景并画出我们的物体。往下,我们将展示正方形是怎样一步步被画出来的。

 

 

着色器

着色器是使用 OpenGL ES Shading Language(GLSL)编写的程序,它携带着绘制形状的顶点信息以及构造绘制在屏幕上像素的所需数据,换句话说,它负责记录着像素点的位置和颜色。

绘制WebGL时候有两种不同的着色器函数,顶点着色器和片段着色器。你需要通过用GLSL 编写这些着色器,并将代码文本传递给WebGL , 使之在GPU执行时编译。顺便一提,顶点着色器和片段着色器的集合我们通常称之为着色器程序。

下面我们通过在WebGL 环境绘制一个2D图像的例子快速介绍这两种着色器。

 

顶点着色器

每次渲染一个形状时,顶点着色器会在形状中的每个顶点运行。 它的工作是将输入顶点从原始坐标系转换到WebGL使用的缩放空间(clipspace)坐标系,其中每个轴的坐标范围从-1.0到1.0,并且不考虑纵横比,实际尺寸或任何其他因素。

顶点着色器需要对顶点坐标进行必要的转换,在每个顶点基础上进行其他调整或计算,然后通过将其保存在由GLSL提供的特殊变量(我们称为gl_Position)中来返回变换后的顶点

顶点着色器根据需要, 也可以完成其他工作。例如,决定哪个包含 texel面部纹理的坐标,可以应用于顶点;通过法线来确定应用到顶点的光照因子等。然后将这些信息存储在变量(varyings)属性(attributes)属性中,以便与片段着色器共享

以下的顶点着色器接收一个我们定义的属性(aVertexPosition)的顶点位置值。之后这个值与两个4x4的矩阵(uProjectionMatrix和uModelMatrix)相乘; 乘积赋值给gl_Position。有关投影和其他矩阵的更多信息,在这里您可能可以找到有帮助的文章.。

// Vertex shader program

  const vsSource = `
    attribute vec4 aVertexPosition;

    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;

    void main() {
      gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
    }
  `;

这个例子中,我们没有计算任何光照效果,因为我们还没有应用到场景,它将后面的 WebGL光照章节介绍。同样我们也还没应用任何纹理,这将在WebGL添加纹理章节补充。

 

 

片段着色器

片段着色器在顶点着色器处理完图形的顶点后,会被要绘制的每个图形的每个像素点调用一次。它的职责是确定像素的颜色,通过指定应用到像素的纹理元素(也就是图形纹理中的像素),获取纹理元素的颜色,然后将适当的光照应用于颜色。之后颜色存储在特殊变量gl_FragColor中,返回到WebGL层。该颜色将最终绘制到屏幕上图形对应像素的对应位置。

 

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

 

 

初始化着色器

现在我们已经定义了两个着色器,我们需要将它们传递给WebGL,编译并将它们连接在一起。下面的代码通过调用loadShader(),为着色器传递类型和来源,创建了两个着色器。然后创建一个附加着色器的程序,将它们连接在一起。如果编译或链接失败,代码将弹出alert。

 

//
//  初始化着色器程序,让WebGL知道如何绘制我们的数据
function initShaderProgram(gl, vsSource, fsSource) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);

  // 创建着色器程序

  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);

  // 创建失败, alert
  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
    return null;
  }

  return shaderProgram;
}

//
// 创建指定类型的着色器,上传source源码并编译
//
function loadShader(gl, type, source) {
  const shader = gl.createShader(type);

  // Send the source to the shader object

  gl.shaderSource(shader, source);

  // Compile the shader program

  gl.compileShader(shader);

  // See if it compiled successfully

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    return null;
  }

  return shader;
}

 

 

loadShader函数将WebGL上下文,着色器类型和源码作为参数输入,然后按如下步骤创建和编译着色器:

1. 调用gl.createShader().创建一个新的着色器。

2. 调用gl.shaderSource().将源代码发送到着色器。

3. 一旦着色器获取到源代码,就使用gl.compileShader().进行编译。

4. 为了检查是否成功编译了着色器,将检查着色器参数gl.COMPILE_STATUS状态。通过调用gl.getShaderParameter()获得它的值,并指定着色器和我们想要检查的参数的名字(gl.COMPILE_STATUS)。如果返回错误,则着色器无法编译,因此通过gl.getShaderInfoLog()从编译器中获取日志信息并alert,然后删除着色器返回null,表明加载着色器失败。

5. 如果着色器被加载并成功编译,则返回编译的着色器。

我们可以像这样调用这段代码

 

  const shaderProgram = initShaderProgram(gl, vsSource, fsSource);

 

 

在创建着色器程序之后,我们需要查找WebGL返回分配的输入位置。在上述情况下,我们有一个属性和两个uniforms。属性从缓冲区接收值。顶点着色器的每次迭代都从分配给该属性的缓冲区接收下一个值。uniforms类似于JavaScript全局变量。它们在着色器的所有迭代中保持相同的值。由于属性和统一的位置是特定于单个着色器程序的,因此我们将它们存储在一起以使它们易于传递

 

const programInfo = {
    program: shaderProgram,
    attribLocations: {
      vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
    },
    uniformLocations: {
      projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
      modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
    },
  };

 

 

创建对象

在画正方形前,我们需要创建一个缓冲器来存储它的顶点。我们会用到名字为 initBuffers() 的函数。当我们了解到更多WebGL 的高级概念时,这段代码会将有更多参数,变得更加复杂,并且用来创建更多的三维物体。

var horizAspect = 480.0/640.0;

function initBuffers() {
  squareVerticesBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBuffer);
  
  var vertices = [
    1.0,  1.0,  0.0,
    -1.0, 1.0,  0.0,
    1.0,  -1.0, 0.0,
    -1.0, -1.0, 0.0
  ];
  
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
}

这段代码简单给出了绘画场景的本质。首先,它调用 gl 的成员函数 createBuffer() 得到了缓冲对象并存储在顶点缓冲器。然后调用 bindBuffer() 函数绑定上下文。

当上一步完成,我们创建一个Javascript 数组去记录每一个正方体的每一个顶点。然后将其转化为 WebGL 浮点型类型的数组,并将其传到 gl 对象的  bufferData() 方法来建立对象的顶点。

绘制场景

当着色器和物体都创建好后,我们可以开始渲染这个场景了。因为我们这个例子不会产生动画,所以 drawScene() 方法非常简单。它还使用了几个工具函数,稍后我们会介绍。

注意: 你可能会得到这样一段错误报告:“ mat4 is not defined”,意思是说你缺少glmatrix库。该库的js文件gl-matrix.js可以从这里获得.

function drawScene(gl, programInfo, buffers) {
  gl.clearColor(0.0, 0.0, 0.0, 1.0);  // Clear to black, fully opaque
  gl.clearDepth(1.0);                 // Clear everything
  gl.enable(gl.DEPTH_TEST);           // Enable depth testing
  gl.depthFunc(gl.LEQUAL);            // Near things obscure far things

  // Clear the canvas before we start drawing on it.

  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  // Create a perspective matrix, a special matrix that is
  // used to simulate the distortion of perspective in a camera.
  // Our field of view is 45 degrees, with a width/height
  // ratio that matches the display size of the canvas
  // and we only want to see objects between 0.1 units
  // and 100 units away from the camera.

  const fieldOfView = 45 * Math.PI / 180;   // in radians
  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  const zNear = 0.1;
  const zFar = 100.0;
  const projectionMatrix = mat4.create();

  // note: glmatrix.js always has the first argument
  // as the destination to receive the result.
  mat4.perspective(projectionMatrix,
                   fieldOfView,
                   aspect,
                   zNear,
                   zFar);

  // Set the drawing position to the "identity" point, which is
  // the center of the scene.
  const modelViewMatrix = mat4.create();

  // Now move the drawing position a bit to where we want to
  // start drawing the square.

  mat4.translate(modelViewMatrix,     // destination matrix
                 modelViewMatrix,     // matrix to translate
                 [-0.0, 0.0, -6.0]);  // amount to translate

  // Tell WebGL how to pull out the positions from the position
  // buffer into the vertexPosition attribute.
  {
    const numComponents = 2;  // pull out 2 values per iteration
    const type = gl.FLOAT;    // the data in the buffer is 32bit floats
    const normalize = false;  // don't normalize
    const stride = 0;         // how many bytes to get from one set of values to the next
                              // 0 = use type and numComponents above
    const offset = 0;         // how many bytes inside the buffer to start from
    gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
    gl.vertexAttribPointer(
        programInfo.attribLocations.vertexPosition,
        numComponents,
        type,
        normalize,
        stride,
        offset);
    gl.enableVertexAttribArray(
        programInfo.attribLocations.vertexPosition);
  }

  // Tell WebGL to use our program when drawing

  gl.useProgram(programInfo.program);

  // Set the shader uniforms

  gl.uniformMatrix4fv(
      programInfo.uniformLocations.projectionMatrix,
      false,
      projectionMatrix);
  gl.uniformMatrix4fv(
      programInfo.uniformLocations.modelViewMatrix,
      false,
      modelViewMatrix);

  {
    const offset = 0;
    const vertexCount = 4;
    gl.drawArrays(gl.TRIANGLE_STRIP, offset, vertexCount);
  }
}

第一步,用背景色擦除画布,接着建立摄像机透视矩阵。设置45度的视图角度,并且设置一个适合实际图像的宽高比。 指定在摄像机距离0.1到100单位长度的范围内的物体可见。

接着加载特定位置,并把正方形放在距离摄像机6个单位的的位置。然后,我们绑定正方形的顶点缓冲到上下文,并配置好,再通过调用 drawArrays() 方法来画出对象。 

如果你的浏览器支持WebGL的话,可以点击这里看看DEMO。完整的源代码从这里获得

矩阵通用计算

矩阵计算是一个很复杂的运算。 没人会想去自己写完所有代码来处理这些运算。通常人们使用一个矩阵运算库,而不会自己实现矩阵运算。在这个例子中我们使用的是glMatrix library.

相关资料