使用 WebGL 创建 2D 内容

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

渲染场景

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

初始化着色器

WebGL着色器使用 OpenGL ES Shading Language. 为了更方便地处理和更新内容,事实上我们可以将着色器代码写在HTML文档上,而不是把所有代码都写在 JavaScript里。下面我们看一下 initShaders() 函数是怎么实现的:

function initShaders() {
  var fragmentShader = getShader(gl, "shader-fs");
  var vertexShader = getShader(gl, "shader-vs");
  
  // 创建着色器
  
  shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  
  // 如果创建着色器失败
  
  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    alert("Unable to initialize the shader program.");
  }
  
  gl.useProgram(shaderProgram);
  
  vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
  gl.enableVertexAttribArray(vertexPositionAttribute);
}

这段程序中加载了两个着色器。首先,片段着色器是从 ID 为 "shader-fs" 的 script 元素中加载的。然后,顶点着色器是从ID为"shader-vs"的 script 元素中加载的。我们将在下一节看到 getShader() 函数的细节,实现将着色器程序从DOM元素加载到着色器。

接着我们调用 WebGL 对象的 createProgram()函数来创建着色器,并将WebGL 对象与两个着色器关联起来,然后链接着色器程序。完成以上步骤后,gl 对象的 LINK_STATUS 参数被检查以确定着色器程序是否成功连接。如果成功的话,我们激活新着色器程序。

从DOM中加载着色器

如下,getShader() 函数从DOM元素里抓取着色器程序,并返回一个编译好的着色器程序,但是,如果它加载失败或编译失败的话,将返回 null 。

function getShader(gl, id) {
  var shaderScript, theSource, currentChild, shader;
  
  shaderScript = document.getElementById(id);
  
  if (!shaderScript) {
    return null;
  }
  
  theSource = "";
  currentChild = shaderScript.firstChild;
  
  while(currentChild) {
    if (currentChild.nodeType == currentChild.TEXT_NODE) {
      theSource += currentChild.textContent;
    }
    
    currentChild = currentChild.nextSibling;
  }

一旦找到指定ID的元素,其文本内容将被读取保存到变量 theSource 。

  if (shaderScript.type == "x-shader/x-fragment") {
    shader = gl.createShader(gl.FRAGMENT_SHADER);
  } else if (shaderScript.type == "x-shader/x-vertex") {
    shader = gl.createShader(gl.VERTEX_SHADER);
  } else {
     // Unknown shader type
     return null;
  }

一旦读取到着色器源码,我们就根据着色器对象的 MIME 属性来判断它是顶点着色器(MIME type "x-shader/x-vertex"),还是片段着色(MIME type "x-shader-x-fragment"),从而创建相应的着色器。

  gl.shaderSource(shader, theSource);
    
  // 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));  
      return null;  
  }
    
  return shader;
}

最后,源码将传到着色器上并编译。如果在编译途中发生错误,我们会显示一个警告并返回 null,否则,就返回一个新的编译好的着色器。

着色器

接着,我们需要将着色器程序代码加入到 HTML 文档。但是,着色器的语法和具体怎样工作已经超出本篇文章讨论的范围。

片段着色器

在 WebGL 多边形中的每一个像素都叫一个片段。这个片段着色器的工作就是建立每个像素的色彩。在这个案例中,我们会简单地给每个像素填上白色。

gl_FragColor 是一个 GL 内置的特殊变量,用于片段的色彩填充。如下所示,设定它的值就是设定每个像素的颜色。

<script id="shader-fs" type="x-shader/x-fragment">
  void main(void) {
    gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
  }
</script>

顶点着色器

顶点着色器定义了每个顶点的位置和形状。

<script id="shader-vs" type="x-shader/x-vertex">
  attribute vec3 aVertexPosition;

  uniform mat4 uMVMatrix;
  uniform mat4 uPMatrix;
  
  void main(void) {
    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
  }
</script>

创建对象

在画正方形前,我们需要创建一个缓冲器来存储它的顶点。我们会用到名字为 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() 方法非常简单。它还使用了几个工具函数,稍后我们会介绍。

function drawScene() {
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  
  perspectiveMatrix = makePerspective(45, 640.0/480.0, 0.1, 100.0);
  
  loadIdentity();
  mvTranslate([-0.0, 0.0, -6.0]);
  
  gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBuffer);
  gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
  setMatrixUniforms();
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}

第一步,用背景色擦除上下文,接着建立摄像机透视矩阵。设置45度的视图角度,并且宽高比设为 640/480(画布尺寸)。 指定在摄像机距离0.1到100单位长度的范围内,物体可见。

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

如果你的浏览器支持WebGL的话,可以点击这里看看DEMO

矩阵通用计算

矩阵计算是一个很复杂的运算。 没人会想去自己写完所有代码来处理这些运算。幸运的是,这里有 Sylvester, 一个使用方便的 JavaScript处理向量和矩阵运算的库。

在这个例子中使用的 glUtils.js 文件同时也被使用到很多的其它WebGL例子中去了。似乎没有人完全清楚它的来源,但是它简化了对Sylvester 的使用,甚至添加了一些生成特殊矩阵和输出显示它们的方法函数。

另外,这个例子给出了几个有用的代码,它结合这些库来解决特定的任务。准确来说它们已经超出了这个例子讨论的范围,但是这里有一些非常好的有关于矩阵运算的在线文档。可以参考看See also 栏目找到相关资料。

function loadIdentity() {
  mvMatrix = Matrix.I(4);
}

function multMatrix(m) {
  mvMatrix = mvMatrix.x(m);
}

function mvTranslate(v) {
  multMatrix(Matrix.Translation($V([v[0], v[1], v[2]])).ensure4x4());
}

function setMatrixUniforms() {
  var pUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
  gl.uniformMatrix4fv(pUniform, false, new Float32Array(perspectiveMatrix.flatten()));

  var mvUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
  gl.uniformMatrix4fv(mvUniform, false, new Float32Array(mvMatrix.flatten()));
}

相关资料

文档标签和贡献者

标签: 
 此页面的贡献者: Sincoyw, fscholz, ziyunfei, yukai, Phaeton, Zhining
 最后编辑者: Sincoyw,