WebGL之入门

1,563 阅读5分钟

WebGL在GPU中运行,工作基本上分为两部分,第一部分是将顶点(或数据流)转换到裁剪空间坐标(坐标范围为[-1,1]超出部分将被裁掉), 第二部分是基于第一部分的结果绘制像素点。这两部分分别由顶点找色器和片段着色器来完成。所以webgl中程序是由方法对组成的而每个方法对中必须提供一个顶点找色器方法和一个片段着色器方法,编写这些方法的语言叫做GLSL(Graphics Library Shader Language),是一种类似与C/C++的强类型语言.下面通过一个例子来展开原理:

有关webgl的API可以看看一下MDN的官方文档,这里就不展开了。

首先编写一个顶点找色器

<script id="2d-vertex-shader" type="notjs">
 
  // 一个属性变量,将会从缓冲中获取数据
  attribute vec4 a_position;
 
  // 所有着色器都有一个main方法
  void main() {
 
    // gl_Position 是一个顶点着色器主要设置的变量
    gl_Position = a_position;
  }
 
</script>

需要注意的是下面用script标签只是为了获取着色器程序字符串用的,如果你愿意的话完全可以像下面一样用字符串的形式来写着色器程序,至于type='notjs'只是为了让浏览器忽略标签里面的内容,其实只要type不等与javascript或者text/javascript或者不写type类型使用默认值,其他写什么无所谓,浏览器都会忽略。

const vertexShader = `
 attribute vec4 a_position;
  void main() {
    gl_Position = a_position;
  }`;

接着编写一个片段着色器:

<script id="2d-fragment-shader" type="notjs">
 
  // 片断着色器没有默认精度,所以我们需要设置一个精度
  // mediump是一个不错的默认值,代表“medium precision”(中等精度)
  precision mediump float;
 
  void main() {
    // gl_FragColor是一个片断着色器主要设置的变量
    gl_FragColor = vec4(1, 0, 0.5, 1); // 返回“瑞迪施紫色”
  }
 
</script>

OK,到这里我们就算写完了最基本的着色器程序,下面来看一下如何通过JavaScript让这段程序在GPU中运行起来。

首先我们需要一个canvas标签然后通过他来获得webgl上下文。

// html
<canvas id="container"></canvas>

// js
const canvas = document.getElementById("container");
var gl = canvas.getContext("webgl");
if (!gl) {
    // do something     
}

接下来我们来创建两个着色器程序。

// 创建着色器方法,输入参数:渲染上下文,着色器类型,数据源
function createShader(gl, type, source) {
  var shader = gl.createShader(type); // 创建着色器对象
  gl.shaderSource(shader, source); // 提供数据源
  gl.compileShader(shader); // 编译 -> 生成着色器
  var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (success) {
    return shader;
  }
 
  console.log(gl.getShaderInfoLog(shader));
  gl.deleteShader(shader);
}

var vertexShaderSource = document.getElementById("2d-vertex-shader").text;
var fragmentShaderSource = document.getElementById("2d-fragment-shader").text;
 
var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

现在我们已经有两个着色器程序了,接下来要做的就是将这两个着色器程序link在一起,然后创建一个着色程序。

function createProgram(gl, vertexShader, fragmentShader) {
  var program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  var success = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (success) {
    return program;
  }
 
  console.log(gl.getProgramInfoLog(program));
  gl.deleteProgram(program);
}
var program = createProgram(gl, vertexShader, fragmentShader);

好了我们已经创建好了着色程序,但是这个程序还没有数据,我们要做的就是为它提供数据。着色程序的数据都是从缓冲区拿的,所以我们要做的就是将数据放到缓冲区里面。

// 创建一个缓冲区
var positionBuffer = gl.createBuffer();
// 将缓冲区和gl.ARRAY_BUFFER这个全局变量绑定
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 三个二维点坐标
var positions = [
  0, 0,
  0, 0.5,
  0.7, 0,
];
// 将数据和gl.ARRAY_BUFFER这个全局变量绑定
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

webgl需要强类型的数据所以我们创建了32位浮点型数据序列,并将positions中的数据复制到这个序列中,然后将数据复制到之前与gl.ARRAY_BUFFER绑定的缓冲区中。gl.STATIC_DRAW的作用是提示webgl我们不会经常改动这些数据,webgl会根据我们的提示作出一些性能上的优化。

数据和程序都已经创建好了,接下来我们就开始做一些渲染的准备。

//css 设置画布大小
#container {
    width: 100%;
    height: 100%;
}
// js
// 告诉WebGL裁剪空间的 -1 -> +1 分别对应到x轴的 0 -> gl.canvas.width 和y轴的 0 -> gl.canvas.height
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// 清空画布
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 告诉它用我们之前写好的着色程序(一个着色器对)
gl.useProgram(program);

接下来我们要告诉webgl怎么从缓冲区中获取属性给找色器。

// 告诉WebGL我们想从缓冲中提供数据
gl.enableVertexAttribArray(positionAttributeLocation);
// 获取webgl给属性分配的地址。
var positionLocation = gl.getAttribLocation(program, "a_position");
// 告诉属性怎么从positionBuffer中读取数据 (ARRAY_BUFFER)
var size = 2;          // 每次迭代运行提取的单位数据
var type = gl.FLOAT;   // 每个单位的数据类型
var normalize = false; // 是否归一化数据
var stride = 0;        // 从一个数据到下一个数据要跳过多少位
var offset = 0;        // 数据在缓冲的什么位置(距离起点的偏移量)
gl.vertexAttribPointer(
    positionAttributeLocation, size, type, normalize, stride, offset)

需要注意的是GLSL的顶点着色器中a_position属性的数据类型是vec4,可以想象成JavaScript中的a_position = {x: 0, y: 0, z: 0, w: 0},我们x,y,z,w默认值为0, 0, 0, 1我们设置size=2,属性将会从缓冲区获取2个值(x, y)而z,w为默认值(0, 1); 最后终于可以让webgl运行我们的程序了。

// 定义图元类型为三角形
var primitiveType = gl.TRIANGLES;
// 指定从数组的哪个位置开始获取数据
var offset = 0;
// 顶点找色器将运行三次
var count = 3;
gl.drawArrays(primitiveType, offset, count);

现在webgl将在中画出一个三角形,绘制每个像素时WebGL都将调用片断着色器。 我们的片断着色器只是简单设置gl_FragColor为1, 0, 0.5, 1, 由于画布的每个通道宽度为8位,这表示WebGL最终在画布上绘制[255, 0, 127, 255]。

至此我们完成了一个最基本的三角形的绘制,过程相对来说还是比较繁琐的,每到这时候我都会感叹three.js原来为我们做了这么多工作。