01 WEBGL 工作原理
WebGL在GPU上究竟做了什么。 WebGL在GPU上的工作基本上分为两部分,第一部分是将顶点(或数据流)转换到裁剪空间坐标, 第二部分是基于第一部分的结果绘制像素点。
当你调用
var primitiveType = gl.TRIANGLES;var offset = 0;var count = 9;gl.drawArrays(primitiveType, offset, count);// void gl.drawArrays(mode, first, count);
| 参数 | mode GLenum(en-US) 类型,指定绘制图元的方式,可能值如下。 - gl.POINTS: 绘制一系列点。 - gl.LINE_STRIP: 绘制一个线条。即,绘制一系列线段,上一点连接下一点。 - gl.LINE_LOOP: 绘制一个线圈。即,绘制一系列线段,上一点连接下一点,并且最后一点与第一个点相连。 - gl.LINES: 绘制一系列单独线段。每两个点作为端点,线段之间不连接。 - gl.TRIANGLE_STRIP:绘制一个三角带。 - gl.TRIANGLE_FAN:绘制一个三角扇。 - gl.TRIANGLES: 绘制一系列三角形。每三个点作为顶点。 |
first GLint(en-US) 类型 ,指定从哪个点开始绘制。 new Float32Array([ 0, -100, 150, 125, -175, 100] ) 32位整数的浮点类型; |
count GLsizei(en-US) 类型,指定绘制需要使用到多少个点。 |
|---|---|---|---|
| 返回 | 无 | ||
| 异常 | - 如果 mode 不是一个可接受值,将会抛出 gl.INVALID_ENUM 异常。 - 如果 first 或者 count 是负值,会抛出 gl.INVALID_VALUE 异常。 - 如果 gl.CURRENT_PROGRAM 为 null,会抛出 gl.INVALID_OPERATION 异常。 |
这里的9表示“处理9个顶点”,所以将会有9个顶点被转换。。。。

左侧是你提供的数据。顶点着色器(Vertex Shader)是你写进GLSL 中的一个方法,每个顶点调用一次,在这个方法中做一些数学运算后设置了一个特殊的gl_Position变量, 这个变量就是该顶点转换到裁剪空间中的坐标值,GPU接收该值并将其保存起来。
使用它们可以做出非常有趣的东西,但如你所见,到目前为止的例子中, 处理每个像素时片断着色器可用信息很少,幸运的是我们可以给它传递更多信息。 想要从顶点着色器传值到片断着色器,我们可以定义“可变量(varyings)”。
// 定义一个三角形填充到缓冲里function setGeometry(gl) {gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([0, -100,150, 125,-175, 100]),gl.STATIC_DRAW);}// 绘制场景function drawScene() {...// 绘制几何体var primitiveType = gl.TRIANGLES;var offset = 0;var count = 3;gl.drawArrays(primitiveType, offset, count);}varying vec4 v_color;...void main() {// 将位置和矩阵相乘gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);// 从裁减空间转换到颜色空间// 裁减空间范围 -1.0 到 +1.0// 颜色空间范围 0.0 到 1.0v_color = gl_Position * 0.5 + 0.5;}// 在片断着色器中定义同名varying变量precision mediump float;varying vec4 v_color;void main() {gl_FragColor = v_color;}
02 数据渲染
"use strict";function main() {// Get A WebGL context/** @type {HTMLCanvasElement} */var canvas = document.querySelector("#canvas");var gl = canvas.getContext("webgl");if (!gl) {return;}// setup GLSL programvar program = webglUtils.createProgramFromScripts(gl, ["vertex-shader-2d", "fragment-shader-2d"]);// look up where the vertex data needs to go.var positionAttributeLocation = gl.getAttribLocation(program, "a_position");// lookup uniformsvar matrixLocation = gl.getUniformLocation(program, "u_matrix");// Create a buffer.var positionBuffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);// Set Geometry.setGeometry(gl);var translation = [200, 150];var angleInRadians = 0;var scale = [1, 1];drawScene();// Setup a ui.webglLessonsUI.setupSlider("#x", {value: translation[0], slide: updatePosition(0), max: gl.canvas.width });webglLessonsUI.setupSlider("#y", {value: translation[1], slide: updatePosition(1), max: gl.canvas.height});webglLessonsUI.setupSlider("#angle", {slide: updateAngle, max: 360});webglLessonsUI.setupSlider("#scaleX", {value: scale[0], slide: updateScale(0), min: -5, max: 5, step: 0.01, precision: 2});webglLessonsUI.setupSlider("#scaleY", {value: scale[1], slide: updateScale(1), min: -5, max: 5, step: 0.01, precision: 2});function updatePosition(index) {return function(event, ui) {translation[index] = ui.value;drawScene();};}function updateAngle(event, ui) {var angleInDegrees = 360 - ui.value;angleInRadians = angleInDegrees * Math.PI / 180;drawScene();}function updateScale(index) {return function(event, ui) {scale[index] = ui.value;drawScene();};}// Draw the scene.function drawScene() {webglUtils.resizeCanvasToDisplaySize(gl.canvas);// Tell WebGL how to convert from clip space to pixelsgl.viewport(0, 0, gl.canvas.width, gl.canvas.height);// Clear the canvas.gl.clear(gl.COLOR_BUFFER_BIT);// Tell it to use our program (pair of shaders)gl.useProgram(program);// Turn on the attributegl.enableVertexAttribArray(positionAttributeLocation);// Bind the position buffer.gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)var size = 2; // 2 components per iterationvar type = gl.FLOAT; // the data is 32bit floatsvar normalize = false; // don't normalize the datavar stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next positionvar offset = 0; // start at the beginning of the buffergl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);// Compute the matrixvar matrix = m3.projection(gl.canvas.clientWidth, gl.canvas.clientHeight);matrix = m3.translate(matrix, translation[0], translation[1]);matrix = m3.rotate(matrix, angleInRadians);matrix = m3.scale(matrix, scale[0], scale[1]);// Set the matrix.gl.uniformMatrix3fv(matrixLocation, false, matrix);// Draw the geometry.var primitiveType = gl.TRIANGLES;var offset = 0;var count = 3;gl.drawArrays(primitiveType, offset, count);}}// Fill the buffer with the values that define a triangle.// Note, will put the values in whatever buffer is currently// bound to the ARRAY_BUFFER bind pointfunction setGeometry(gl) {gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([0, -100,150, 125,-175, 100]),gl.STATIC_DRAW);}main();
回想一下,我们只计算了三个顶点,调用了三次顶点着色器,所以也只计算出了三个颜色值, 但是我们的三角形却有很多颜色,这就是称之为可变量的varying的原因

我们的给顶点着色器施加了一个包含平移,旋转和缩放的的矩阵,并将结果转换到裁剪空间。 默认平移,旋转和缩放值为:平移 = 200, 150,旋转 = 0,缩放 = 1,所以这里只进行了平移。 画布大小(背景缓冲)为 400×300,所以三个顶点在裁剪空间中为以下坐标值。
想要给片断着色器传值,我们可以先把值传递给顶点着色器然后再传给片断着色器。 让我们来画一个由两个不同颜色三角形组成的矩形。我们需要给顶点着色器添加一个属性值, 把值通过属性传递给它后它再直接传递给片断着色器。
attribute vec2 a_position;attribute vec4 a_color;...varying vec4 v_color;void main() {...// 直接把属性值中的数据赋给可变量v_color = a_color;}// 寻找顶点着色器中需要的数据var positionLocation = gl.getAttribLocation(program, "a_position");var colorLocation = gl.getAttribLocation(program, "a_color");...// 给颜色数据创建一个缓冲var colorBuffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);// 设置颜色setColors(gl);...// 给矩形的两个三角形// 设置颜色值并发到缓冲function setColors(gl) {// 生成两个随机颜色var r1 = Math.random();var b1 = Math.random();var g1 = Math.random();var r2 = Math.random();var b2 = Math.random();var g2 = Math.random();gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([ r1, b1, g1, 1,r1, b1, g1, 1,r1, b1, g1, 1,r2, b2, g2, 1,r2, b2, g2, 1,r2, b2, g2, 1]),gl.STATIC_DRAW);}gl.enableVertexAttribArray(colorLocation);// 绑定颜色缓冲gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);// 告诉颜色属性怎么从 colorBuffer (ARRAY_BUFFER) 中读取颜色值var size = 4; // 每次迭代使用4个单位的数据var type = gl.FLOAT; // 单位数据类型是32位的浮点型var normalize = false; // 不需要归一化数据var stride = 0; // 0 = 移动距离 * 单位距离长度sizeof(type)// 每次迭代跳多少距离到下一个数据var offset = 0; // 从绑定缓冲的起始处开始gl.vertexAttribPointer(colorLocation, size, type, normalize, stride, offset)// 画几何体var primitiveType = gl.TRIANGLES;var offset = 0;var count = 6;gl.drawArrays(primitiveType, offset, count);
03 vertexAttribPointer 中的 normalizeFlag
标准化标记(normalizeFlag)适用于所有非浮点型数据。如果传递false就解读原数据类型。 BYTE 类型的范围是从 -128 到 127,UNSIGNED_BYTE 类型的范围是从 0 到 255, SHORT 类型的范围是从 -32768 到 32767,等等…
如果标准化标记设为true,BYTE 数据的值(-128 to 127)将会转换到 -1.0 到 +1.0 之间, UNSIGNED_BYTE (0 to 255) 变为 0.0 到 +1.0 之间,SHORT 也是转换到 -1.0 到 +1.0 之间, 但比 BYTE 精确度高。
最常用的是标准化颜色数据。大多数情况颜色值范围为 0.0 到 +1.0。 使用4个浮点型数据存储红,绿,蓝和阿尔法通道数据时,每个顶点的颜色将会占用16字节空间, 如果你有复杂的几何体将会占用很多内存。代替的做法是将颜色数据转换为四个 UNSIGNED_BYTE , 其中 0 表示 0.0,255 表示 1.0。现在每个顶点只需要四个字节存储颜色值,省了 75% 空间。
我们来修改之前代码实现。当我们告诉WebGL如何获取颜色数据时将这样
// 告诉颜色属性如何从colorBuffer中提取数据 (ARRAY_BUFFER)var size = 4; // 每次迭代使用四个单位数据var type = gl.UNSIGNED_BYTE; // 数据类型是8位的 UNSIGNED_BYTE 类型。***************var normalize = true; // 标准化数据var stride = 0; // 0 = 移动距离 * 单位距离长度sizeof(type)// 每次迭代跳多少距离到下一个数据var offset = 0; // 从缓冲的起始处开始gl.vertexAttribPointer(colorLocation, size, type, normalize, stride, offset)// 给矩形的两个三角形// 设置颜色值并发到缓冲function setColors(gl) {// 设置两个随机颜色var r1 = Math.random() * 256; // 0 到 255.99999 之间var b1 = Math.random() * 256; // 这些数据var g1 = Math.random() * 256; // 在存入缓冲时var r2 = Math.random() * 256; // 将被截取成var b2 = Math.random() * 256; // Uint8Array 类型var g2 = Math.random() * 256;gl.bufferData(gl.ARRAY_BUFFER,new Uint8Array( // Uint8Array[ r1, b1, g1, 255,r1, b1, g1, 255,r1, b1, g1, 255,r2, b2, g2, 255,r2, b2, g2, 255,r2, b2, g2, 255]),gl.STATIC_DRAW);}
