本事储备指南,编制程序指南

WebGL技术储备指南

2015/12/22 · HTML5 · 1 评论 · WebGL

原文出处: 淘宝前端团队(FED)- 叶斋   

图片 1

WebGL 是 HTML 5 草案的一部分,可以驱动 Canvas 渲染三维场景。WebGL 虽然还未有广泛应用,但极具潜力和想象空间。本文是我学习 WebGL 时梳理知识脉络的产物,花点时间整理出来与大家分享。

WebGL 是 HTML 5 草案的一部分,可以驱动 Canvas 渲染三维场景。WebGL 虽然还未有广泛应用,但极具潜力和想象空间。本文是我学习 WebGL 时梳理知识脉络的产物,花点时间整理出来与大家分享。

WebGL 是 HTML 5 草案的一部分,可以驱动 Canvas 渲染三维场景。WebGL 虽然还未有广泛应用,但极具潜力和想象空间。本文是我学习 WebGL 时梳理知识脉络的产物,花点时间整理出来与大家分享。

  1. 默认情况下,canvas是透明的。

  2. canvas可以灵活地支持二维图形和三维图形,它不直接提供绘图方法,而是提供一种叫上下文(context)的机制来进行绘图。

  3. WebGL 中的颜色取值rgba继承OpenGL, 取值为0.0 — 1.0

示例

WebGL 很酷,有以下 demos 为证:

寻找奥兹国
赛车游戏
划船的男孩(Goo Engine Demo)

示例

WebGL 很酷,有以下 demos 为证:

寻找奥兹国
赛车游戏
划船的男孩(Goo Engine Demo)

示例

图片 2

本文的目标

本文的预期读者是:不熟悉图形学,熟悉前端,希望了解或系统学习 WebGL 的同学。

本文不是 WebGL 的概述性文章,也不是完整详细的 WebGL 教程。本文只希望成为一篇供 WebGL 初学者使用的提纲。

本文的目标

本文的预期读者是:不熟悉图形学,熟悉前端,希望了解或系统学习 WebGL 的同学。

本文不是 WebGL 的概述性文章,也不是完整详细的 WebGL 教程。本文只希望成为一篇供 WebGL 初学者使用的提纲。

WebGL 很酷,有以下 demos 为证:

gl.clearColor()

Canvas

熟悉 Canvas 的同学都知道,Canvas 绘图先要获取绘图上下文:

JavaScript

var context = canvas.getContext('2d');

1
var context = canvas.getContext('2d');

context上调用各种函数绘制图形,比如:

JavaScript

// 绘制左上角为(0,0),右下角为(50, 50)的矩形 context.fillRect(0, 0, 50, 50);

1
2
// 绘制左上角为(0,0),右下角为(50, 50)的矩形
context.fillRect(0, 0, 50, 50);

WebGL 同样需要获取绘图上下文:

JavaScript

var gl = canvas.getContext('webgl'); // 或 experimental-webgl

1
var gl = canvas.getContext('webgl'); // 或 experimental-webgl

但是接下来,如果想画一个矩形的话,就没这么简单了。实际上,Canvas 是浏览器封装好的一个绘图环境,在实际进行绘图操作时,浏览器仍然需要调用 OpenGL API。而 WebGL API 几乎就是 OpenGL API 未经封装,直接套了一层壳。

Canvas 的更多知识,可以参考:

  • JS 权威指南的 21.4 节或 JS 高级程序设计中的 15 章
  • W3CSchool
  • 阮一峰的 Canvas 教程

Canvas

熟悉 Canvas 的同学都知道,Canvas 绘图先要获取绘图上下文:

var context = canvas.getContext('2d');

context上调用各种函数绘制图形,比如:

// 绘制左上角为(0,0),右下角为(50, 50)的矩形
context.fillRect(0, 0, 50, 50);

WebGL 同样需要获取绘图上下文:

var gl = canvas.getContext('webgl'); // 或 experimental-webgl

但是接下来,如果想画一个矩形的话,就没这么简单了。实际上,Canvas 是浏览器封装好的一个绘图环境,在实际进行绘图操作时,浏览器仍然需要调用 OpenGL API。而 WebGL API 几乎就是 OpenGL API 未经封装,直接套了一层壳。

Canvas 的更多知识,可以参考:

  • JS 权威指南的 21.4 节或 JS 高级程序设计中的 15 章
  • W3CSchool
  • 阮一峰的 Canvas 教程

寻找奥兹国

如果没有指定背景色,默认值如下:

矩阵变换

三维模型,从文件中读出来,到绘制在 Canvas 中,经历了多次坐标变换。

假设有一个最简单的模型:三角形,三个顶点分别为(-1,-1,0),(1,-1,0),(0,1,0)。这三个数据是从文件中读出来的,是三角形最初始的坐标(局部坐标)。如下图所示,右手坐标系。

图片 3

模型通常不会位于场景的原点,假设三角形的原点位于(0,0,-1)处,没有旋转或缩放,三个顶点分别为(-1,-1,-1),(1,-1,-1),(0,1,-1),即世界坐标。

图片 4

绘制三维场景必须指定一个观察者,假设观察者位于(0,0,1)处而且看向三角形,那么三个顶点相对于观察者的坐标为(-1,-1,-2),(1,-1,-2),(0,1,-2),即视图坐标。

图片 5

观察者的眼睛是一个点(这是透视投影的前提),水平视角和垂直视角都是90度,视野范围(目力所及)为[0,2]在Z轴上,观察者能够看到的区域是一个四棱台体。

图片 6

将四棱台体映射为标准立方体(CCV,中心为原点,边长为2,边与坐标轴平行)。顶点在 CCV 中的坐标,离它最终在 Canvas 中的坐标已经很接近了,如果把 CCV 的前表面看成 Canvas,那么最终三角形就画在图中橙色三角形的位置。

图片 7

上述变换是用矩阵来进行的。

局部坐标 –(模型变换)-> 世界坐标 –(视图变换)-> 视图坐标 –(投影变换)–> CCV 坐标。

以(0,1,0)为例,它的齐次向量为(0,0,1,1),上述变换的表示过程可以是:

图片 8

上面三个矩阵依次是透视投影矩阵,视图矩阵,模型矩阵。三个矩阵的值分别取决于:观察者的视角和视野距离,观察者在世界中的状态(位置和方向),模型在世界中的状态(位置和方向)。计算的结果是(0,1,1,2),化成齐次坐标是(0,0.5,0.5,1),就是这个点在CCV中的坐标,那么(0,0.5)就是在Canvas中的坐标(认为 Canvas 中心为原点,长宽都为2)。

上面出现的(0,0,1,1)是(0,0,1)的齐次向量。齐次向量(x,y,z,w)可以代表三维向量(x,y,z)参与矩阵运算,通俗地说,w 分量为 1 时表示位置,w 分量为 0 时表示位移。

WebGL 没有提供任何有关上述变换的机制,开发者需要亲自计算顶点的 CCV 坐标。

关于坐标变换的更多内容,可以参考:

  • 计算机图形学中的5-7章
  • 变换矩阵@维基百科
  • 透视投影详解

比较复杂的是模型变换中的绕任意轴旋转(通常用四元数生成矩阵)和投影变换(上面的例子都没收涉及到)。

关于绕任意轴旋转和四元数,可以参考:

  • 四元数@维基百科
  • 一个老外对四元数公式的证明

关于齐次向量的更多内容,可以参考。

  • 计算机图形学的5.2节
  • 齐次坐标@维基百科

矩阵变换

三维模型,从文件中读出来,到绘制在 Canvas 中,经历了多次坐标变换。

假设有一个最简单的模型:三角形,三个顶点分别为(-1,-1,0),(1,-1,0),(0,1,0)。这三个数据是从文件中读出来的,是三角形最初始的坐标(局部坐标)。如下图所示,右手坐标系。

图片 9

模型通常不会位于场景的原点,假设三角形的原点位于(0,0,-1)处,没有旋转或缩放,三个顶点分别为(-1,-1,-1),(1,-1,-1),(0,1,-1),即世界坐标。

图片 10

绘制三维场景必须指定一个观察者,假设观察者位于(0,0,1)处而且看向三角形,那么三个顶点相对于观察者的坐标为(-1,-1,-2),(1,-1,-2),(0,1,-2),即视图坐标。

图片 11

观察者的眼睛是一个点(这是透视投影的前提),水平视角和垂直视角都是90度,视野范围(目力所及)为[0,2]在Z轴上,观察者能够看到的区域是一个四棱台体。

图片 12

将四棱台体映射为标准立方体(CCV,中心为原点,边长为2,边与坐标轴平行)。顶点在 CCV 中的坐标,离它最终在 Canvas 中的坐标已经很接近了,如果把 CCV 的前表面看成 Canvas,那么最终三角形就画在图中橙色三角形的位置。

图片 13

上述变换是用矩阵来进行的。

局部坐标 –(模型变换)-> 世界坐标 –(视图变换)-> 视图坐标 –(投影变换)–> CCV 坐标。

以(0,1,0)为例,它的齐次向量为(0,0,1,1),上述变换的表示过程可以是:

图片 14

上面三个矩阵依次是透视投影矩阵,视图矩阵,模型矩阵。三个矩阵的值分别取决于:观察者的视角和视野距离,观察者在世界中的状态(位置和方向),模型在世界中的状态(位置和方向)。计算的结果是(0,1,1,2),化成齐次坐标是(0,0.5,0.5,1),就是这个点在CCV中的坐标,那么(0,0.5)就是在Canvas中的坐标(认为 Canvas 中心为原点,长宽都为2)。

上面出现的(0,0,1,1)是(0,0,1)的齐次向量。齐次向量(x,y,z,w)可以代表三维向量(x,y,z)参与矩阵运算,通俗地说,w 分量为 1 时表示位置,w 分量为 0 时表示位移。

WebGL 没有提供任何有关上述变换的机制,开发者需要亲自计算顶点的 CCV 坐标。

关于坐标变换的更多内容,可以参考:

  • 计算机图形学中的5-7章
  • 变换矩阵@维基百科
  • 透视投影详解

比较复杂的是模型变换中的绕任意轴旋转(通常用四元数生成矩阵)和投影变换(上面的例子都没收涉及到)。

关于绕任意轴旋转和四元数,可以参考:

  • 四元数@维基百科
  • 一个老外对四元数公式的证明

关于齐次向量的更多内容,可以参考。

  • 计算机图形学的5.2节
  • 齐次坐标@维基百科

赛车游戏

图片 15

着色器和光栅化

在 WebGL 中,开发者是通过着色器来完成上述变换的。着色器是运行在显卡中的程序,以 GLSL 语言编写,开发者需要将着色器的源码以字符串的形式传给 WebGL 上下文的相关函数。

着色器有两种,顶点着色器和片元(像素)着色器,它们成对出现。顶点着色器任务是接收顶点的局部坐标,输出 CCV 坐标。CCV 坐标经过光栅化,转化为逐像素的数据,传给片元着色器。片元着色器的任务是确定每个片元的颜色。

顶点着色器接收的是 attribute 变量,是逐顶点的数据。顶点着色器输出 varying 变量,也是逐顶点的。逐顶点的 varying 变量数据经过光栅化,成为逐片元的 varying 变量数据,输入片元着色器,片元着色器输出的结果就会显示在 Canvas 上。

图片 16

着色器功能很多,上述只是基本功能。大部分炫酷的效果都是依赖着色器的。如果你对着色器完全没有概念,可以试着理解下一节 hello world 程序中的着色器再回顾一下本节。

关于更多着色器的知识,可以参考:

  • GLSL@维基百科
  • WebGL@MSDN

着色器和光栅化

在 WebGL 中,开发者是通过着色器来完成上述变换的。着色器是运行在显卡中的程序,以 GLSL 语言编写,开发者需要将着色器的源码以字符串的形式传给 WebGL 上下文的相关函数。

着色器有两种,顶点着色器和片元(像素)着色器,它们成对出现。顶点着色器任务是接收顶点的局部坐标,输出 CCV 坐标。CCV 坐标经过光栅化,转化为逐像素的数据,传给片元着色器。片元着色器的任务是确定每个片元的颜色。

顶点着色器接收的是 attribute 变量,是逐顶点的数据。顶点着色器输出 varying 变量,也是逐顶点的。逐顶点的 varying 变量数据经过光栅化,成为逐片元的 varying 变量数据,输入片元着色器,片元着色器输出的结果就会显示在 Canvas 上。

图片 17

着色器功能很多,上述只是基本功能。大部分炫酷的效果都是依赖着色器的。如果你对着色器完全没有概念,可以试着理解下一节 hello world 程序中的着色器再回顾一下本节。

关于更多着色器的知识,可以参考:

  • GLSL@维基百科
  • WebGL@MSDN

划船的男孩(Goo EngineDemo)

default-bg-color

程序

这一节解释绘制上述场景(三角形)的 WebGL 程序。点这个链接,查看源代码,试图理解一下。这段代码出自WebGL Programming Guide,我作了一些修改以适应本文内容。如果一切正常,你看到的应该是下面这样:

图片 18

解释几点(如果之前不了解 WebGL ,多半会对下面的代码困惑,无碍):

  1. 字符串 VSHADER_SOURCE 和 FSHADER_SOURCE 是顶点着色器和片元着色器的源码。可以将着色器理解为有固定输入和输出格式的程序。开发者需要事先编写好着色器,再按照一定格式着色器发送绘图命令。
  2. Part2 将着色器源码编译为 program 对象:先分别编译顶点着色器和片元着色器,然后连接两者。如果编译源码错误,不会报 JS 错误,但可以通过其他 API(如gl.getShaderInfo等)获取编译状态信息(成功与否,如果出错的错误信息)。
JavaScript

// 顶点着色器 var vshader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vshader, VSHADER_SOURCE);
gl.compileShader(vshader); // 同样新建 fshader var program =
gl.createProgram(); gl.attachShader(program, vshader);
gl.attachShader(program, fshader); gl.linkProgram(program);

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f14b3a671c960813930-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f14b3a671c960813930-2">
2
</div>
<div class="crayon-num" data-line="crayon-5b8f14b3a671c960813930-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f14b3a671c960813930-4">
4
</div>
<div class="crayon-num" data-line="crayon-5b8f14b3a671c960813930-5">
5
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f14b3a671c960813930-6">
6
</div>
<div class="crayon-num" data-line="crayon-5b8f14b3a671c960813930-7">
7
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f14b3a671c960813930-8">
8
</div>
<div class="crayon-num" data-line="crayon-5b8f14b3a671c960813930-9">
9
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f14b3a671c960813930-1" class="crayon-line">
// 顶点着色器
</div>
<div id="crayon-5b8f14b3a671c960813930-2" class="crayon-line crayon-striped-line">
var vshader = gl.createShader(gl.VERTEX_SHADER);
</div>
<div id="crayon-5b8f14b3a671c960813930-3" class="crayon-line">
gl.shaderSource(vshader, VSHADER_SOURCE);
</div>
<div id="crayon-5b8f14b3a671c960813930-4" class="crayon-line crayon-striped-line">
gl.compileShader(vshader);
</div>
<div id="crayon-5b8f14b3a671c960813930-5" class="crayon-line">
// 同样新建 fshader
</div>
<div id="crayon-5b8f14b3a671c960813930-6" class="crayon-line crayon-striped-line">
var program = gl.createProgram();
</div>
<div id="crayon-5b8f14b3a671c960813930-7" class="crayon-line">
gl.attachShader(program, vshader);
</div>
<div id="crayon-5b8f14b3a671c960813930-8" class="crayon-line crayon-striped-line">
gl.attachShader(program, fshader);
</div>
<div id="crayon-5b8f14b3a671c960813930-9" class="crayon-line">
gl.linkProgram(program);
</div>
</div></td>
</tr>
</tbody>
</table>
  1. program 对象需要指定使用它,才可以向着色器传数据并绘制。复杂的程序通常有多个 program 对 象,(绘制每一帧时)通过切换 program 对象绘制场景中的不同效果。
JavaScript

gl.useProgram(program);

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f14b3a6720232020477-1">
1
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f14b3a6720232020477-1" class="crayon-line">
gl.useProgram(program);
</div>
</div></td>
</tr>
</tbody>
</table>
  1. Part3 向正在使用的着色器传入数据,包括逐顶点的 attribute 变量和全局的 uniform 变量。向着色器传入数据必须使用 ArrayBuffer,而不是常规的 JS 数组。
JavaScript

var varray = new Float32Array([-1, -1, 0, 1, -1, 0, 0, 1, 0])

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f14b3a6723482450329-1">
1
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f14b3a6723482450329-1" class="crayon-line">
var varray = new Float32Array([-1, -1, 0, 1, -1, 0, 0, 1, 0])
</div>
</div></td>
</tr>
</tbody>
</table>
  1. WebGL API 对 ArrayBuffer 的操作(填充缓冲区,传入着色器,绘制等)都是通过 gl.ARRAY_BUFFER 进行的。在 WebGL 系统中又很多类似的情况。
JavaScript

// 只有将 vbuffer 绑定到 gl.ARRAY_BUFFER,才可以填充数据
gl.bindBuffer(gl.ARRAY_BUFFER, vbuffer); // 这里的意思是,向“绑定到
gl.ARRAY_BUFFER”的缓冲区中填充数据 gl.bufferData(gl.ARRAY_BUFFER,
varray, gl.STATIC_DRAW); // 获取 a_Position
变量在着色器程序中的位置,参考顶点着色器源码 var aloc =
gl.getAttribLocation(program, 'a_Position'); // 将 gl.ARRAY_BUFFER
中的数据传入 aloc 表示的变量,即 a_Position
gl.vertexAttribPointer(aloc, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aloc);

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f14b3a6727492492738-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f14b3a6727492492738-2">
2
</div>
<div class="crayon-num" data-line="crayon-5b8f14b3a6727492492738-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f14b3a6727492492738-4">
4
</div>
<div class="crayon-num" data-line="crayon-5b8f14b3a6727492492738-5">
5
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f14b3a6727492492738-6">
6
</div>
<div class="crayon-num" data-line="crayon-5b8f14b3a6727492492738-7">
7
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f14b3a6727492492738-8">
8
</div>
<div class="crayon-num" data-line="crayon-5b8f14b3a6727492492738-9">
9
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f14b3a6727492492738-1" class="crayon-line">
// 只有将 vbuffer 绑定到 gl.ARRAY_BUFFER,才可以填充数据
</div>
<div id="crayon-5b8f14b3a6727492492738-2" class="crayon-line crayon-striped-line">
gl.bindBuffer(gl.ARRAY_BUFFER, vbuffer);
</div>
<div id="crayon-5b8f14b3a6727492492738-3" class="crayon-line">
// 这里的意思是,向“绑定到 gl.ARRAY_BUFFER”的缓冲区中填充数据
</div>
<div id="crayon-5b8f14b3a6727492492738-4" class="crayon-line crayon-striped-line">
gl.bufferData(gl.ARRAY_BUFFER, varray, gl.STATIC_DRAW);
</div>
<div id="crayon-5b8f14b3a6727492492738-5" class="crayon-line">
// 获取 a_Position 变量在着色器程序中的位置,参考顶点着色器源码
</div>
<div id="crayon-5b8f14b3a6727492492738-6" class="crayon-line crayon-striped-line">
var aloc = gl.getAttribLocation(program, 'a_Position');
</div>
<div id="crayon-5b8f14b3a6727492492738-7" class="crayon-line">
// 将 gl.ARRAY_BUFFER 中的数据传入 aloc 表示的变量,即 a_Position
</div>
<div id="crayon-5b8f14b3a6727492492738-8" class="crayon-line crayon-striped-line">
gl.vertexAttribPointer(aloc, 3, gl.FLOAT, false, 0, 0);
</div>
<div id="crayon-5b8f14b3a6727492492738-9" class="crayon-line">
gl.enableVertexAttribArray(aloc);
</div>
</div></td>
</tr>
</tbody>
</table>
  1. 向着色器传入矩阵时,是按列存储的。可以比较一下 mmatrix 和矩阵变换一节中的模型矩阵(第 3 个)。
  2. 顶点着色器计算出的 gl_Position 就是 CCV 中的坐标,比如最上面的顶点(蓝色)的 gl_Position 化成齐次坐标就是(0,0.5,0.5,1)。
  3. 向顶点着色器传入的只是三个顶点的颜色值,而三角形表面的颜色渐变是由这三个颜色值内插出的。光栅化不仅会对 gl_Position 进行,还会对 varying 变量插值。
  4. gl.drawArrays()方法驱动缓冲区进行绘制,gl.TRIANGLES 指定绘制三角形,也可以改变参数绘制点、折线等等。

关于 ArrayBuffer 的详细信息,可以参考:

  • ArrayBuffer@MDN
  • 阮一峰的 ArrayBuffer 介绍
  • 张鑫旭的 ArrayBuffer 介绍

关于 gl.TRIANGLES 等其他绘制方式,可以参考下面这张图或这篇博文。

图片 19

程序

这一节解释绘制上述场景(三角形)的 WebGL 程序。点这个链接,查看源代码,试图理解一下。这段代码出自WebGL Programming Guide,我作了一些修改以适应本文内容。如果一切正常,你看到的应该是下面这样:

图片 20

解释几点(如果之前不了解 WebGL ,多半会对下面的代码困惑,无碍):

  1. 字符串 VSHADER_SOURCE 和 FSHADER_SOURCE 是顶点着色器和片元着色器的源码。可以将着色器理解为有固定输入和输出格式的程序。开发者需要事先编写好着色器,再按照一定格式着色器发送绘图命令。

  2. Part2 将着色器源码编译为 program 对象:先分别编译顶点着色器和片元着色器,然后连接两者。如果编译源码错误,不会报 JS 错误,但可以通过其他 API(如gl.getShaderInfo等)获取编译状态信息(成功与否,如果出错的错误信息)。

    // 顶点着色器
    var vshader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vshader, VSHADER_SOURCE);
    gl.compileShader(vshader);
    // 同样新建 fshader
    var program = gl.createProgram();
    gl.attachShader(program, vshader);
    gl.attachShader(program, fshader);
    gl.linkProgram(program);
    
  3. program 对象需要指定使用它,才可以向着色器传数据并绘制。复杂的程序通常有多个 program 对 象,(绘制每一帧时)通过切换 program 对象绘制场景中的不同效果。

    gl.useProgram(program);
    
  4. Part3 向正在使用的着色器传入数据,包括逐顶点的 attribute 变量和全局的 uniform 变量。向着色器传入数据必须使用 ArrayBuffer,而不是常规的 JS 数组。

    var varray = new Float32Array([-1, -1, 0, 1, -1, 0, 0, 1, 0])
    
  5. WebGL API 对 ArrayBuffer 的操作(填充缓冲区,传入着色器,绘制等)都是通过 gl.ARRAY_BUFFER 进行的。在 WebGL 系统中又很多类似的情况。

    // 只有将 vbuffer 绑定到 gl.ARRAY_BUFFER,才可以填充数据
    gl.bindBuffer(gl.ARRAY_BUFFER, vbuffer);
    // 这里的意思是,向“绑定到 gl.ARRAY_BUFFER”的缓冲区中填充数据
    gl.bufferData(gl.ARRAY_BUFFER, varray, gl.STATIC_DRAW);
    // 获取 a_Position 变量在着色器程序中的位置,参考顶点着色器源码
    var aloc = gl.getAttribLocation(program, 'a_Position');
    // 将 gl.ARRAY_BUFFER 中的数据传入 aloc 表示的变量,即 a_Position
    gl.vertexAttribPointer(aloc, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(aloc);
    
  6. 向着色器传入矩阵时,是按列存储的。可以比较一下 mmatrix 和矩阵变换一节中的模型矩阵(第 3 个)。

  7. 顶点着色器计算出的 gl_Position 就是 CCV 中的坐标,比如最上面的顶点(蓝色)的 gl_Position 化成齐次坐标就是(0,0.5,0.5,1)。

  8. 向顶点着色器传入的只是三个顶点的颜色值,而三角形表面的颜色渐变是由这三个颜色值内插出的。光栅化不仅会对 gl_Position 进行,还会对 varying 变量插值。

  9. gl.drawArrays()方法驱动缓冲区进行绘制,gl.TRIANGLES 指定绘制三角形,也可以改变参数绘制点、折线等等。

关于 ArrayBuffer 的详细信息,可以参考:

  • ArrayBuffer@MDN
  • 阮一峰的 ArrayBuffer 介绍
  • 张鑫旭的 ArrayBuffer 介绍

关于 gl.TRIANGLES 等其他绘制方式,可以参考下面这张图或这篇博文。

图片 21

本文的目标

  1. gl.clear(gl.COLOR_BUFFER_BIT)清空绘图区域,实际上是在清空颜色缓冲区(color buffer)。
    WebGL中除了颜色缓冲区,还有比如深度缓冲区(DEPTH_BUFFER_BIT)、模板缓冲区(STENCIL_BUFFER_BIT)等。

  2. gl.clearColor() 指定了背景色以后,背景色会驻存在WebGL系统中,在下一次调用gl.clearColr()方法前不会改变。即,如果后面你想用同一种颜色再清空一次绘图区,没必要再指定一次背景色。

  3. WebGL依赖一种新的称为着色器(shader)的绘图机制。

  4. WebGL需要两种着色器:

深度检测

当两个表面重叠时,前面的模型会挡住后面的模型。比如这个例子,绘制了两个交叉的三角形( varray 和 carray 的长度变为 18,gl.drawArrays 最后一个参数变为 6)。为了简单,这个例子去掉了矩阵变换过程,直接向着色器传入 CCV 坐标。

图片 22

图片 23

顶点着色器给出了 6 个顶点的 gl_Position ,经过光栅化,片元着色器获得了 2X 个片元(假设 X 为每个三角形的像素个数),每个片元都离散的 x,y 坐标值,还有 z 值。x,y 坐标就是三角形在 Canvas 上的坐标,但如果有两个具有相同 x,y 坐标的片元同时出现,那么 WebGL 就会取 z 坐标值较小的那个片元。

在深度检测之前,必须在绘制前开启一个常量。否则,WebGL 就会按照在 varray 中定义的顺序绘制了,后面的会覆盖前面的。

JavaScript

gl.enable(gl.DEPTH_TEST);

1
gl.enable(gl.DEPTH_TEST);

实际上,WebGL 的逻辑是这样的:依次处理片元,如果渲染缓冲区(这里就是 Canvas 了)的那个与当前片元对应的像素还没有绘制时,就把片元的颜色画到渲染缓冲区对应像素里,同时把片元的 z 值缓存在另一个深度缓冲区的相同位置;如果当前缓冲区的对应像素已经绘制过了,就去查看深度缓冲区中对应位置的 z 值,如果当前片元 z 值小,就重绘,否则就放弃当前片元。

WebGL 的这套逻辑,对理解蒙版(后面会说到)有一些帮助。

深度检测

当两个表面重叠时,前面的模型会挡住后面的模型。比如这个例子,绘制了两个交叉的三角形( varray 和 carray 的长度变为 18,gl.drawArrays 最后一个参数变为 6)。为了简单,这个例子去掉了矩阵变换过程,直接向着色器传入 CCV 坐标。

图片 24

图片 25

顶点着色器给出了 6 个顶点的 gl_Position ,经过光栅化,片元着色器获得了 2X 个片元(假设 X 为每个三角形的像素个数),每个片元都离散的 x,y 坐标值,还有 z 值。x,y 坐标就是三角形在 Canvas 上的坐标,但如果有两个具有相同 x,y 坐标的片元同时出现,那么 WebGL 就会取 z 坐标值较小的那个片元。

在深度检测之前,必须在绘制前开启一个常量。否则,WebGL 就会按照在 varray 中定义的顺序绘制了,后面的会覆盖前面的。

gl.enable(gl.DEPTH_TEST);

实际上,WebGL 的逻辑是这样的:依次处理片元,如果渲染缓冲区(这里就是 Canvas 了)的那个与当前片元对应的像素还没有绘制时,就把片元的颜色画到渲染缓冲区对应像素里,同时把片元的 z 值缓存在另一个深度缓冲区的相同位置;如果当前缓冲区的对应像素已经绘制过了,就去查看深度缓冲区中对应位置的 z 值,如果当前片元 z 值小,就重绘,否则就放弃当前片元。

WebGL 的这套逻辑,对理解蒙版(后面会说到)有一些帮助。

本文的预期读者是:不熟悉图形学,熟悉前端,希望了解或系统学习 WebGL 的同学。

(1)顶点着色器(Vertex shader):用来描述顶点的特性(如位置、颜色等)。
(2)片元着色器(Fragment shader) : 进行逐片处理过程(如光照等),片元也可以理解为像素(图像的单元)。

顶点索引

gl.drawArrays()是按照顶点的顺序绘制的,而 gl.drawElements()可以令着色器以一个索引数组为顺序绘制顶点。比如这个例子。

图片 26

这里画了两个三角形,但只用了 5 个顶点,有一个顶点被两个三角形共用。这时需要建立索引数组,数组的每个元素表示顶点的索引值。将数组填充至gl.ELEMENT_ARRAY,然后调用 gl.drawElements()。

JavaScript

var iarray = new Uint8Array([0,1,2,2,3,4]); var ibuffer = gl.createBuffer(gl.ARRAY_BUFFER, ibuffer); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, iarray, gl.STATIC_DRAW);

1
2
3
4
var iarray = new Uint8Array([0,1,2,2,3,4]);
var ibuffer = gl.createBuffer(gl.ARRAY_BUFFER, ibuffer);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, iarray, gl.STATIC_DRAW);

顶点索引

gl.drawArrays()是按照顶点的顺序绘制的,而 gl.drawElements()可以令着色器以一个索引数组为顺序绘制顶点。比如这个例子。

图片 27

这里画了两个三角形,但只用了 5 个顶点,有一个顶点被两个三角形共用。这时需要建立索引数组,数组的每个元素表示顶点的索引值。将数组填充至gl.ELEMENT_ARRAY,然后调用 gl.drawElements()。

var iarray = new Uint8Array([0,1,2,2,3,4]);
var ibuffer = gl.createBuffer(gl.ARRAY_BUFFER, ibuffer);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, iarray, gl.STATIC_DRAW);

本文不是 WebGL 的概述性文章,也不是完整详细的 WebGL 教程。本文只希望成为一篇供 WebGL 初学者使用的提纲。

  1. WebGL程序执行流程

纹理

attribute 变量不仅可以传递顶点的坐标,还可以传递其他任何逐顶点的数据。比如 HelloTriangle 程序把单个顶点的颜色传入了 a_Color,片元着色器收到 v_Color 后直接赋给 gl_FragmentColor,就决定了颜色。

attribute 变量还可以帮助绘制纹理。绘制纹理的基本原理是,为每个顶点指定一个纹理坐标(在(0,0)与(1,1,)的正方形中),然后传入纹理对象。片元着色器拿到的是对应片元的内插后的纹理坐标,就利用这个纹理坐标去纹理对象上取颜色,再画到片元上。内插后的纹理坐标很可能不恰好对应纹理上的某个像素,而是在几个像素之间(因为通常的图片纹理也是离散),这时可能会通过周围几个像素的加权平均算出该像素的值(具体有若干种不同方法,可以参考)。

比如这个例子。

图片 28

纹理对象和缓冲区对象很类似:使用 gl 的 API 函数创建,需要绑定至常量 gl.ARRAY_BUFFER 和 gl.TEXTURE_2D ,都通过常量对象向其中填入图像和数据。不同的是,纹理对象在绑定时还需要激活一个纹理单元(此处的gl.TEXTURE0),而 WebGL 系统支持的纹理单元个数是很有限的(一般为 8 个)。

JavaScript

var texture = gl.createTexture(); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, textureImage); var sloc = gl.getUniformLocation(program, 'u_Sampler'); gl.uniform1i(sloc, 0);

1
2
3
4
5
6
7
8
var texture = gl.createTexture();
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, textureImage);
var sloc = gl.getUniformLocation(program, 'u_Sampler');
gl.uniform1i(sloc, 0);

片元着色器内声明了 sampler2D 类型的 uniform 变量,通过texture2D函数取样。

JavaScript

precision mediump float; uniform sampler2D u_Sampler; varying vec2 v_TexCoord; void main() { gl_FragColor = texture2D(u_Sampler, v_TexCoord); };

1
2
3
4
5
6
precision mediump float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main() {
  gl_FragColor = texture2D(u_Sampler, v_TexCoord);
};

纹理

attribute 变量不仅可以传递顶点的坐标,还可以传递其他任何逐顶点的数据。比如 HelloTriangle 程序把单个顶点的颜色传入了 a_Color,片元着色器收到 v_Color 后直接赋给 gl_FragmentColor,就决定了颜色。

attribute 变量还可以帮助绘制纹理。绘制纹理的基本原理是,为每个顶点指定一个纹理坐标(在(0,0)与(1,1,)的正方形中),然后传入纹理对象。片元着色器拿到的是对应片元的内插后的纹理坐标,就利用这个纹理坐标去纹理对象上取颜色,再画到片元上。内插后的纹理坐标很可能不恰好对应纹理上的某个像素,而是在几个像素之间(因为通常的图片纹理也是离散),这时可能会通过周围几个像素的加权平均算出该像素的值(具体有若干种不同方法,可以参考)。

比如这个例子。

图片 29

纹理对象和缓冲区对象很类似:使用 gl 的 API 函数创建,需要绑定至常量 gl.ARRAY_BUFFER 和 gl.TEXTURE_2D ,都通过常量对象向其中填入图像和数据。不同的是,纹理对象在绑定时还需要激活一个纹理单元(此处的gl.TEXTURE0),而 WebGL 系统支持的纹理单元个数是很有限的(一般为 8 个)。

var texture = gl.createTexture();
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, textureImage);
var sloc = gl.getUniformLocation(program, 'u_Sampler');
gl.uniform1i(sloc, 0);

片元着色器内声明了 sampler2D 类型的 uniform 变量,通过texture2D函数取样。

precision mediump float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main() {
  gl_FragColor = texture2D(u_Sampler, v_TexCoord);
};

Canvas

图片 30

混合与蒙版

透明效果是用混合机制完成的。混合机制与深度检测类似,也发生在试图向某个已填充的像素填充颜色时。深度检测通过比较z值来确定像素的颜色,而混合机制会将两种颜色混合。比如这个例子。

图片 31

混合的顺序是按照绘制的顺序进行的,如果绘制的顺序有变化,混合的结果通常也不同。如果模型既有非透明表面又有透明表面,绘制透明表面时开启蒙版,其目的是锁定深度缓冲区,因为半透明物体后面的物体还是可以看到的,如果不这样做,半透明物体后面的物体将会被深度检测机制排除。

开启混合的代码如下。gl.blendFunc方法指定了混合的方式,这里的意思是,使用源(待混合)颜色的 α 值乘以源颜色,加上 1-[源颜色的 α]乘以目标颜色。

JavaScript

gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

1
2
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

所谓 α 值,就是颜色的第 4 个分量。

JavaScript

var carray = new Float32Array([ 1,0,0,0.7,1,0,0,0.7,1,0,0,0.7, 0,0,1,0.4,0,0,1,0.4,0,0,1,0.4 ]);

1
2
3
4
var carray = new Float32Array([
  1,0,0,0.7,1,0,0,0.7,1,0,0,0.7,
  0,0,1,0.4,0,0,1,0.4,0,0,1,0.4
  ]);

混合与蒙版

透明效果是用混合机制完成的。混合机制与深度检测类似,也发生在试图向某个已填充的像素填充颜色时。深度检测通过比较z值来确定像素的颜色,而混合机制会将两种颜色混合。比如这个例子。

图片 32

混合的顺序是按照绘制的顺序进行的,如果绘制的顺序有变化,混合的结果通常也不同。如果模型既有非透明表面又有透明表面,绘制透明表面时开启蒙版,其目的是锁定深度缓冲区,因为半透明物体后面的物体还是可以看到的,如果不这样做,半透明物体后面的物体将会被深度检测机制排除。

开启混合的代码如下。gl.blendFunc方法指定了混合的方式,这里的意思是,使用源(待混合)颜色的 α 值乘以源颜色,加上 1-[源颜色的 α]乘以目标颜色。

gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

所谓 α 值,就是颜色的第 4 个分量。

var carray = new Float32Array([
  1,0,0,0.7,1,0,0,0.7,1,0,0,0.7,
  0,0,1,0.4,0,0,1,0.4,0,0,1,0.4
  ]);

熟悉 Canvas 的同学都知道,Canvas 绘图先要获取绘图上下文:

webgl-flow

浏览器的WebGL系统

WebGL 系统各个组成部分在既定规则下互相配合。稍作梳理如下。

图片 33

这张图比较随意,箭头上的文字表示 API,箭头方向大致表现了数据的流动方向,不必深究。

浏览器的WebGL系统

WebGL 系统各个组成部分在既定规则下互相配合。稍作梳理如下。

图片 34

这张图比较随意,箭头上的文字表示 API,箭头方向大致表现了数据的流动方向,不必深究。

var context = canvas.getContext('2d');

最右边的是颜色缓冲区不是浏览器,因为颜色缓冲区的内容会自动显示在浏览器中。

光照

WebGL 没有为光照提供任何内置的方法,需要开发者在着色器中实现光照算法。

光是有颜色的,模型也是有颜色的。在光照下,最终物体呈现的颜色是两者共同作用的结果。

实现光照的方式是:将光照的数据(点光源的位置,平行光的方向,以及光的颜色和强度)作为 uniform 变量传入着色器中,将物体表面每个顶点处的法线作为 attribute 变量传入着色器,遵循光照规则,修订最终片元呈现的颜色。

光照又分为逐顶点的和逐片元的,两者的区别是,将法线光线交角因素放在顶点着色器中考虑还是放在片元着色器中考虑。逐片元光照更加逼真,一个极端的例子是:

图片 35

此时,点光源在距离一个表面较近处,表面中央 A 处较亮,四周较暗。但是在逐顶点光照下,表面的颜色(的影响因子)是由顶点内插出来的,所以表面中央也会比较暗。而逐片元光照直接使用片元的位置和法线计算与点光源的交角,因此表面中央会比较亮。

光照

WebGL 没有为光照提供任何内置的方法,需要开发者在着色器中实现光照算法。

光是有颜色的,模型也是有颜色的。在光照下,最终物体呈现的颜色是两者共同作用的结果。

实现光照的方式是:将光照的数据(点光源的位置,平行光的方向,以及光的颜色和强度)作为 uniform 变量传入着色器中,将物体表面每个顶点处的法线作为 attribute 变量传入着色器,遵循光照规则,修订最终片元呈现的颜色。

光照又分为逐顶点的和逐片元的,两者的区别是,将法线光线交角因素放在顶点着色器中考虑还是放在片元着色器中考虑。逐片元光照更加逼真,一个极端的例子是:

图片 36

此时,点光源在距离一个表面较近处,表面中央 A 处较亮,四周较暗。但是在逐顶点光照下,表面的颜色(的影响因子)是由顶点内插出来的,所以表面中央也会比较暗。而逐片元光照直接使用片元的位置和法线计算与点光源的交角,因此表面中央会比较亮。

在context上调用各种函数绘制图形,比如:

  1. 初始化着色器initShaders()的行为

复杂模型

复杂模型可能有包括子模型,子模型可能与父模型有相对运动。比如开着雨刮器的汽车,雨刮器的世界坐标是受父模型汽车,和自身的状态共同决定的。若要计算雨刮器某顶点的位置,需要用雨刮器相对汽车的模型矩阵乘上汽车的模型矩阵,再乘以顶点的局部坐标。

复杂模型可能有很多表面,可能每个表面使用的着色器就不同。通常将模型拆解为组,使用相同着色器的表面为一组,先绘制同一组中的内容,然后切换着色器。每次切换着色器都要重新将缓冲区中的数据分配给着色器中相应变量。

复杂模型

复杂模型可能有包括子模型,子模型可能与父模型有相对运动。比如开着雨刮器的汽车,雨刮器的世界坐标是受父模型汽车,和自身的状态共同决定的。若要计算雨刮器某顶点的位置,需要用雨刮器相对汽车的模型矩阵乘上汽车的模型矩阵,再乘以顶点的局部坐标。

复杂模型可能有很多表面,可能每个表面使用的着色器就不同。通常将模型拆解为组,使用相同着色器的表面为一组,先绘制同一组中的内容,然后切换着色器。每次切换着色器都要重新将缓冲区中的数据分配给着色器中相应变量。

// 绘制左上角为(0,0),右下角为(50, 50)的矩形

图片 37

动画

动画的原理就是快速地擦除和重绘。常用的方法是大名鼎鼎的 requestAnimationFrame 。不熟悉的同学,可以参考正美的介绍。

动画

动画的原理就是快速地擦除和重绘。常用的方法是大名鼎鼎的 requestAnimationFrame 。不熟悉的同学,可以参考正美的介绍。

context.fillRect(0, 0, 50, 50);

initShades()

WebGL库

目前最流行的 WebGL 库是 ThreeJS,很强大,官网,代码。

WebGL库

目前最流行的 WebGL 库是 ThreeJS,很强大,官网,代码。

WebGL 同样需要获取绘图上下文:

在初始化着色器之前,顶点着色器和片元着色器都是空白的,我们需要将字符串形式的着色器代码从JavaScript传给WebGL系统,并建立着色器,这就是initShaders()所做的事情。

调试工具

比较成熟的 WebGL 调试工具是WebGL Inspector。

调试工具

比较成熟的 WebGL 调试工具是WebGL Inspector。

var gl = canvas.getContext('webgl'); // 或 experimental-webgl

注意:

网络资源和书籍

英文的关于 WebGL 的资源有很多,包括:

  • learning webgl
  • WebGL@MDN
  • WebGL Cheat Sheet

国内最早的 WebGL 教程是由郝稼力翻译的,放在 hiwebgl 上,目前 hiwebgl 已经关闭,但教程还可以在这里找到。郝稼力目前运营着Lao3D。

国内已经出版的 WebGL 书籍有:

  • WebGL入门指南:其实是一本讲 ThreeJS 的书
  • WebGL高级编程:还不错的一本
  • WebGL编程指南:相当靠谱的全面教程

最后再夹杂一点私货吧。读书期间我曾花了小半年时间翻译了一本WebGL的书,也就是上面的第 3 本。这本书确实相当靠谱,网上各种教程里很多没说清楚的东西,这本书说得很清楚,而且还提供了一份很完整的API文档。翻译这本书的过程也使我受益匪浅。如果有同学愿意系统学一下 WebGL 的,建议购买一本(文青建议买英文版)。

1 赞 2 收藏 1 评论

图片 38

网络资源和书籍

英文的关于 WebGL 的资源有很多,包括:

  • learning webgl
  • WebGL@MDN
  • WebGL Cheat Sheet

国内最早的 WebGL 教程是由郝稼力翻译的,放在 hiwebgl 上,目前 hiwebgl 已经关闭,但教程还可以在这里找到。郝稼力目前运营着Lao3D。

国内已经出版的 WebGL 书籍有:

  • WebGL入门指南:其实是一本讲 ThreeJS 的书
  • WebGL高级编程:还不错的一本
  • WebGL编程指南:相当靠谱的全面教程

但是接下来,如果想画一个矩形的话,就没这么简单了。实际上,Canvas 是浏览器封装好的一个绘图环境,在实际进行绘图操作时,浏览器仍然需要调用 OpenGL API。而 WebGL API 几乎就是 OpenGL API 未经封装,直接套了一层壳。

着色器代码运行在WebGL系统中,而不是JavaScript程序中。
顶点着色器先执行,片元着色器后执行。

Canvas 的更多知识,可以参考:

  1. WebGL程序包括运行在浏览器中的JavaScript运行在WebGL 系统中的着色器这两部分。

  2. 顶点着色器

JS 权威指南的 21.4 节或JS 高级程序设计中的 15 章

W3CSchool

var VSHADER_SOURCE = 
 'void main() {n'  
 ' gl_Position = vec4(0.0, 0.0, 0.0, 1.0);n'   // Set the vertex coordinates of the point
 ' gl_PointSize = 10.0;n'   // Set the point size
 '}n';

阮一峰的 Canvas 教程

(1)void → 没有返回值
(2)不能为main函数指定参数
(3)gl_Position 表示位置,是内置变量,必须被赋值,否则着色器无法工作
(4)gl_PointSize 表示尺寸,也是内置变量,默认值为1.0

矩阵变换

图片 39

三维模型,从文件中读出来,到绘制在 Canvas 中,经历了多次坐标变换。

vshader

假设有一个最简单的模型:三角形,三个顶点分别为(-1,-1,0),(1,-1,0),(0,1,0)。这三个数据是从文件中读出来的,是三角形最初始的坐标(局部坐标)。如下图所示,右手坐标系。

图片 40

图片 41

GLSE_data_type

模型通常不会位于场景的原点,假设三角形的原点位于(0,0,-1)处,没有旋转或缩放,三个顶点分别为(-1,-1,-1),(1,-1,-1),(0,1,-1),即世界坐标。

图片 42

图片 43

vec4

绘制三维场景必须指定一个观察者,假设观察者位于(0,0,1)处而且看向三角形,那么三个顶点相对于观察者的坐标为(-1,-1,-2),(1,-1,-2),(0,1,-2),即视图坐标。

gl_Position = vec4(0.0, 0.0, 1.0, 1.0)

图片 44

前三个分量分别为X, Y, Z,最后一个分量为 齐次坐标

观察者的眼睛是一个点(这是透视投影的前提),水平视角和垂直视角都是90度,视野范围(目力所及)为[0,2]在Z轴上,观察者能够看到的区域是一个四棱台体。

图片 45

图片 46

齐次坐标

将四棱台体映射为标准立方体(CCV,中心为原点,边长为2,边与坐标轴平行)。顶点在 CCV 中的坐标,离它最终在 Canvas 中的坐标已经很接近了,如果把 CCV 的前表面看成 Canvas,那么最终三角形就画在图中橙色三角形的位置。

  1. 片元着色器

图片 47

上述变换是用矩阵来进行的。

// Fragment shader program
var FSHADER_SOURCE =
 'void main() {n'  
 ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);n'   // Set the point color
 '}n';

局部坐标 –(模型变换)-> 世界坐标 –(视图变换)-> 视图坐标 –(投影变换)–> CCV 坐标。

图片 48

以(0,1,0)为例,它的齐次向量为(0,0,1,1),上述变换的表示过程可以是:

fshader

图片 49

  1. 绘制操作
    步骤:
    (1)清空绘制区
    (2)gl.drawArrays(mode, first, count)

上面三个矩阵依次是透视投影矩阵,视图矩阵,模型矩阵。三个矩阵的值分别取决于:观察者的视角和视野距离,观察者在世界中的状态(位置和方向),模型在世界中的状态(位置和方向)。计算的结果是(0,1,1,2),化成齐次坐标是(0,0.5,0.5,1),就是这个点在CCV中的坐标,那么(0,0.5)就是在Canvas中的坐标(认为 Canvas 中心为原点,长宽都为2)。

图片 50

上面出现的(0,0,1,1)是(0,0,1)的齐次向量。齐次向量(x,y,z,w)可以代表三维向量(x,y,z)参与矩阵运算,通俗地说,w 分量为 1 时表示位置,w 分量为 0 时表示位移。

gl.drawArrays()

WebGL 没有提供任何有关上述变换的机制,开发者需要亲自计算顶点的 CCV 坐标。

gl.drawArrays(mode, first, count) 将会执行 count

关于坐标变换的更多内容,可以参考:

图片 51

计算机图形学中的5-7章

shader-flow

变换矩阵@维基百科

  1. WebGL 坐标系统

透视投影详解

图片 52

比较复杂的是模型变换中的绕任意轴旋转(通常用四元数生成矩阵)和投影变换(上面的例子都没收涉及到)。

webgl-coord

关于绕任意轴旋转和四元数,可以参考:

  1. attribute 变量 和 uniorm 变量
    attribute 变量 : GLSL ES变量,传输的是那些与顶点相关的数据(外部向顶点着色器传输,只有顶点着色器能使用)。
    uniform 变量: 传输的是那些与所有顶点都相同(或与顶点无关)的数据 (数据传输的目标是片元着色器,而非顶点着色器)。

四元数@维基百科

图片 53

一个老外对四元数公式的证明

data-transfer

关于齐次向量的更多内容,可以参考。

为了使用 attribute 变量:示例程序需要包含以下步骤:

计算机图形学的5.2节

(1)在顶点着色器中,声明 attribute 变量;

齐次坐标@维基百科

attribute vec4 a_Position;

着色器和光栅化

attribute 被称为存储限定符(storage qualifier),它表示接下来的变量,attribute变量必须声明为全局变量

在 WebGL 中,开发者是通过着色器来完成上述变换的。着色器是运行在显卡中的程序,以 GLSL 语言编写,开发者需要将着色器的源码以字符串的形式传给 WebGL 上下文的相关函数。

图片 54

着色器有两种,顶点着色器和片元(像素)着色器,它们成对出现。顶点着色器任务是接收顶点的局部坐标,输出 CCV 坐标。CCV 坐标经过光栅化,转化为逐像素的数据,传给片元着色器。片元着色器的任务是确定每个片元的颜色。

storage-qualifier

顶点着色器接收的是 attribute 变量,是逐顶点的数据。顶点着色器输出 varying 变量,也是逐顶点的。逐顶点的 varying 变量数据经过光栅化,成为逐片元的 varying 变量数据,输入片元着色器,片元着色器输出的结果就会显示在 Canvas 上。

(2)将 attribute 变量赋值给 gl_Position 变量;

图片 55

先获取 attribute变量的存储位置:

着色器功能很多,上述只是基本功能。大部分炫酷的效果都是依赖着色器的。如果你对着色器完全没有概念,可以试着理解下一节 hello world 程序中的着色器再回顾一下本节。

var a_Position = gl.getAttribLocation(gl.program, 'a_Position');

关于更多着色器的知识,可以参考:

gl.program => 程序对象,包含了顶点着色器片元着色器

GLSL@维基百科

图片 56

WebGL@MSDN

gl.getAttribLotation()

程序

(3)向 attribute 变量传输数据;
将顶点位置传输给 attribute 变量:

这一节解释绘制上述场景(三角形)的 WebGL 程序。点这个链接,查看源代码,试图理解一下。这段代码出自WebGL Programming Guide,我作了一些修改以适应本文内容。如果一切正常,你看到的应该是下面这样:

gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);

图片 57

图片 58

解释几点(如果之前不了解 WebGL ,多半会对下面的代码困惑,无碍):

vertexAttrib3f

字符串 VSHADER_SOURCE 和 FSHADER_SOURCE 是顶点着色器和片元着色器的源码。可以将着色器理解为有固定输入和输出格式的程序。开发者需要事先编写好着色器,再按照一定格式着色器发送绘图命令。

gl.vertexAttrib3f()的同族函数:

Part2 将着色器源码编译为 program 对象:先分别编译顶点着色器和片元着色器,然后连接两者。如果编译源码错误,不会报 JS 错误,但可以通过其他 API(如gl.getShaderInfo等)获取编译状态信息(成功与否,如果出错的错误信息)。

图片 59

// 顶点着色器

vertexAttribnf

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

WebGL 相关函数命名规范:

gl.shaderSource(vshader, VSHADER_SOURCE);

图片 60

gl.compileShader(vshader);

webgl-function-standard

// 同样新建 fshader

使用 uniform 变量步骤:
(1)在片元着色器中准备 uniform 变量;

var program = gl.createProgram();

uniform vec4 u_FragColor;

gl.attachShader(program, vshader);

图片 61

gl.attachShader(program, fshader);

uniform-declaration

gl.linkProgram(program);

precision mediump float; 精度限定词(precision qualifier) ,指定变量的范围(最大值与最小值)和精度

program 对象需要指定使用它,才可以向着色器传数据并绘制。复杂的程序通常有多个 program 对 象,(绘制每一帧时)通过切换 program 对象绘制场景中的不同效果。

(2)用这个 uniform 变量向 gl_FragColor 赋值;
获取 uniform 变量的存储地址。

gl.useProgram(program);

图片 62

Part3 向正在使用的着色器传入数据,包括逐顶点的 attribute 变量和全局的 uniform 变量。向着色器传入数据必须使用 ArrayBuffer,而不是常规的 JS 数组。

getUniformLocation.png

var varray = new Float32Array([-1, -1, 0, 1, -1, 0, 0, 1, 0])

(3)将颜色数据从JavaScript传给该 uniform 变量。
uniform 变量赋值 gl.uniform4f(location, v0, v1, v2, v3)

WebGL API 对 ArrayBuffer 的操作(填充缓冲区,传入着色器,绘制等)都是通过 gl.ARRAY_BUFFER 进行的。在 WebGL 系统中又很多类似的情况。

图片 63

// 只有将 vbuffer 绑定到 gl.ARRAY_BUFFER,才可以填充数据

uniform4f

gl.bindBuffer(gl.ARRAY_BUFFER, vbuffer);

gl.uniform4f()的同族函数

// 这里的意思是,向“绑定到 gl.ARRAY_BUFFER”的缓冲区中填充数据

图片 64

gl.bufferData(gl.ARRAY_BUFFER, varray, gl.STATIC_DRAW);

uniformnf

// 获取 a_Position 变量在着色器程序中的位置,参考顶点着色器源码

var aloc = gl.getAttribLocation(program, 'a_Position');

// 将 gl.ARRAY_BUFFER 中的数据传入 aloc 表示的变量,即 a_Position

gl.vertexAttribPointer(aloc, 3, gl.FLOAT, false, 0, 0);

gl.enableVertexAttribArray(aloc);

向着色器传入矩阵时,是按列存储的。可以比较一下 mmatrix 和矩阵变换一节中的模型矩阵(第 3 个)。

顶点着色器计算出的 gl_Position 就是 CCV 中的坐标,比如最上面的顶点(蓝色)的 gl_Position 化成齐次坐标就是(0,0.5,0.5,1)。

向顶点着色器传入的只是三个顶点的颜色值,而三角形表面的颜色渐变是由这三个颜色值内插出的。光栅化不仅会对 gl_Position 进行,还会对 varying 变量插值。

gl.drawArrays()方法驱动缓冲区进行绘制,gl.TRIANGLES 指定绘制三角形,也可以改变参数绘制点、折线等等。

关于 ArrayBuffer 的详细信息,可以参考:

ArrayBuffer@MDN

阮一峰的 ArrayBuffer 介绍

张鑫旭的 ArrayBuffer 介绍

关于 gl.TRIANGLES 等其他绘制方式,可以参考下面这张图或这篇博文。

图片 65

深度检测

当两个表面重叠时,前面的模型会挡住后面的模型。比如这个例子,绘制了两个交叉的三角形( varray 和 carray 的长度变为 18,gl.drawArrays 最后一个参数变为 6)。为了简单,这个例子去掉了矩阵变换过程,直接向着色器传入 CCV 坐标。

图片 66

图片 67

顶点着色器给出了 6 个顶点的 gl_Position ,经过光栅化,片元着色器获得了 2X 个片元(假设 X 为每个三角形的像素个数),每个片元都离散的 x,y 坐标值,还有 z 值。x,y 坐标就是三角形在 Canvas 上的坐标,但如果有两个具有相同 x,y 坐标的片元同时出现,那么 WebGL 就会取 z 坐标值较小的那个片元。

在深度检测之前,必须在绘制前开启一个常量。否则,WebGL 就会按照在 varray 中定义的顺序绘制了,后面的会覆盖前面的。

gl.enable(gl.DEPTH_TEST);

实际上,WebGL 的逻辑是这样的:依次处理片元,如果渲染缓冲区(这里就是 Canvas 了)的那个与当前片元对应的像素还没有绘制时,就把片元的颜色画到渲染缓冲区对应像素里,同时把片元的 z 值缓存在另一个深度缓冲区的相同位置;如果当前缓冲区的对应像素已经绘制过了,就去查看深度缓冲区中对应位置的 z 值,如果当前片元 z 值小,就重绘,否则就放弃当前片元。

WebGL 的这套逻辑,对理解蒙版(后面会说到)有一些帮助。

顶点索引

gl.drawArrays()是按照顶点的顺序绘制的,而 gl.drawElements()可以令着色器以一个索引数组为顺序绘制顶点。比如这个例子。

图片 68

这里画了两个三角形,但只用了 5 个顶点,有一个顶点被两个三角形共用。这时需要建立索引数组,数组的每个元素表示顶点的索引值。将数组填充至gl.ELEMENT_ARRAY,然后调用 gl.drawElements()。

var iarray = new Uint8Array([0,1,2,2,3,4]);

var ibuffer = gl.createBuffer(gl.ARRAY_BUFFER, ibuffer);

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuffer);

gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, iarray, gl.STATIC_DRAW);

纹理

attribute 变量不仅可以传递顶点的坐标,还可以传递其他任何逐顶点的数据。比如 HelloTriangle 程序把单个顶点的颜色传入了 a_Color,片元着色器收到 v_Color 后直接赋给 gl_FragmentColor,就决定了颜色。

attribute 变量还可以帮助绘制纹理。绘制纹理的基本原理是,为每个顶点指定一个纹理坐标(在(0,0)与(1,1,)的正方形中),然后传入纹理对象。片元着色器拿到的是对应片元的内插后的纹理坐标,就利用这个纹理坐标去纹理对象上取颜色,再画到片元上。内插后的纹理坐标很可能不恰好对应纹理上的某个像素,而是在几个像素之间(因为通常的图片纹理也是离散),这时可能会通过周围几个像素的加权平均算出该像素的值(具体有若干种不同方法,可以参考)。

比如这个例子。

图片 69

纹理对象和缓冲区对象很类似:使用 gl 的 API 函数创建,需要绑定至常量 gl.ARRAY_BUFFER 和 gl.TEXTURE_2D ,都通过常量对象向其中填入图像和数据。不同的是,纹理对象在绑定时还需要激活一个纹理单元(此处的gl.TEXTURE0),而 WebGL 系统支持的纹理单元个数是很有限的(一般为 8 个)。

var texture = gl.createTexture();

gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);

gl.activeTexture(gl.TEXTURE0);

gl.bindTexture(gl.TEXTURE_2D, texture);

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, textureImage);

var sloc = gl.getUniformLocation(program, 'u_Sampler');

gl.uniform1i(sloc, 0);

片元着色器内声明了 sampler2D 类型的 uniform 变量,通过texture2D函数取样。

precision mediump float;

uniform sampler2D u_Sampler;

varying vec2 v_TexCoord;

void main() {

  gl_FragColor = texture2D(u_Sampler, v_TexCoord);

};

混合与蒙版

透明效果是用混合机制完成的。混合机制与深度检测类似,也发生在试图向某个已填充的像素填充颜色时。深度检测通过比较z值来确定像素的颜色,而混合机制会将两种颜色混合。比如这个例子。

图片 70

混合的顺序是按照绘制的顺序进行的,如果绘制的顺序有变化,混合的结果通常也不同。如果模型既有非透明表面又有透明表面,绘制透明表面时开启蒙版,其目的是锁定深度缓冲区,因为半透明物体后面的物体还是可以看到的,如果不这样做,半透明物体后面的物体将会被深度检测机制排除。

开启混合的代码如下。gl.blendFunc方法指定了混合的方式,这里的意思是,使用源(待混合)颜色的 α 值乘以源颜色,加上 1-[源颜色的 α]乘以目标颜色。

gl.enable(gl.BLEND);

gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

所谓 α 值,就是颜色的第 4 个分量。

var carray = new Float32Array([

  1,0,0,0.7,1,0,0,0.7,1,0,0,0.7,

  0,0,1,0.4,0,0,1,0.4,0,0,1,0.4

  ]);

浏览器的WebGL系统

WebGL 系统各个组成部分在既定规则下互相配合。稍作梳理如下。

图片 71

这张图比较随意,箭头上的文字表示 API,箭头方向大致表现了数据的流动方向,不必深究。

光照

WebGL 没有为光照提供任何内置的方法,需要开发者在着色器中实现光照算法。

光是有颜色的,模型也是有颜色的。在光照下,最终物体呈现的颜色是两者共同作用的结果。

实现光照的方式是:将光照的数据(点光源的位置,平行光的方向,以及光的颜色和强度)作为 uniform 变量传入着色器中,将物体表面每个顶点处的法线作为 attribute 变量传入着色器,遵循光照规则,修订最终片元呈现的颜色。

光照又分为逐顶点的和逐片元的,两者的区别是,将法线光线交角因素放在顶点着色器中考虑还是放在片元着色器中考虑。逐片元光照更加逼真,一个极端的例子是:

图片 72

此时,点光源在距离一个表面较近处,表面中央 A 处较亮,四周较暗。但是在逐顶点光照下,表面的颜色(的影响因子)是由顶点内插出来的,所以表面中央也会比较暗。而逐片元光照直接使用片元的位置和法线计算与点光源的交角,因此表面中央会比较亮。

复杂模型

复杂模型可能有包括子模型,子模型可能与父模型有相对运动。比如开着雨刮器的汽车,雨刮器的世界坐标是受父模型汽车,和自身的状态共同决定的。若要计算雨刮器某顶点的位置,需要用雨刮器相对汽车的模型矩阵乘上汽车的模型矩阵,再乘以顶点的局部坐标。

复杂模型可能有很多表面,可能每个表面使用的着色器就不同。通常将模型拆解为组,使用相同着色器的表面为一组,先绘制同一组中的内容,然后切换着色器。每次切换着色器都要重新将缓冲区中的数据分配给着色器中相应变量。

动画

动画的原理就是快速地擦除和重绘。常用的方法是大名鼎鼎的 requestAnimationFrame 。不熟悉的同学,可以参考正美的介绍。

WebGL库

目前最流行的 WebGL 库是 ThreeJS,很强大,官网,代码。

调试工具

比较成熟的 WebGL 调试工具是WebGL Inspector。

网络资源和书籍

英文的关于 WebGL 的资源有很多,包括:

learning webgl

WebGL@MDN

WebGL Cheat Sheet

国内最早的 WebGL 教程是由郝稼力翻译的,放在 hiwebgl 上,目前 hiwebgl 已经关闭,但教程还可以在这里找到。郝稼力目前运营着Lao3D。

国内已经出版的 WebGL 书籍有:

WebGL入门指南:其实是一本讲 ThreeJS 的书

WebGL高级编程:还不错的一本

WebGL编程指南:相当靠谱的全面教程

本文由星彩网app下载发布于前端技术,转载请注明出处:本事储备指南,编制程序指南

TAG标签:
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。