初识 WebGL

什么是 WebGL

webgl 在支持 canvas 的浏览器中进行 2d 或 3d 渲染。

webgl 程序除了有 Html、javascript,还需要加入着色器语言(GLSL ES)。

WebGL 使得网页在支持 HTML <canvas> 标签的浏览器中,不需要使用任何插件,便可以使用基于 OpenGL ES 2.0 的 API 在 canvas 中进行 3D 渲染 —— MDN WebGL 教程

通过 caniuse 得知 webgl(98.15%) 和 webgl 2.0(94.12%) 的支持情况。请看下图:

Tip:个人计算机上,绘制三维最广泛使用的技术有 Direct3D 和 OpenGL,前者是微软的,后者是开源免费的。OpenGL 有个特殊版本 OpenGL ES 专门用于嵌入式计算机、手机,而 WebGL 就是从 OpenGL ES 派生出来的。下图是 OpenGL、OpenGL ES、WebGL 三者之间的关系。其中 webgl 2.0 基于 OpenGL ES 3.0 未画出来:

canvas

Canvas_API 提供了一个通过JavaScript 和 HTML的 <canvas>元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。

Canvas API 主要聚焦于 2D 图形。而同样使用<canvas>元素的 WebGL API 则用于绘制硬件加速的 2D 和 3D 图形。

示例:

// canvas.html
<body>
<canvas id="canvas" width="300" height="300">
抱歉,您的浏览器不支持 canvas 元素
(这些内容将会在不支持<canvas>元素的浏览器或是禁用了 JavaScript 的浏览器内渲染并展现)
</canvas>
<script>
var canvas = document.getElementById('canvas'); // getContext - 方法返回canvas 的上下文,如果上下文没有定义则返回 null
var ctx = canvas.getContext('2d'); // 设置填充颜色
ctx.fillStyle = 'green';
// 绘制矩形
ctx.fillRect(10, 10, 100, 100);
</script>
</body>

效果如下:

Tip:不管绘制二维还是三维都是这三步:

  1. 获取 canvas
  2. 请求绘图上下文
  3. 调用绘图上下文中的绘图函数

第一个webgl示例

需求:清空绘图区。也就是使用背景色清空 canvas 的绘图区

实现如下:

// webgl01.html
<body>
<canvas id="canvas" width="300" height="300">
抱歉,您的浏览器不支持 canvas 元素
(这些内容将会在不支持<canvas>元素的浏览器或是禁用了 JavaScript 的浏览器内渲染并展现)
</canvas>
<script>
var canvas = document.getElementById('canvas');
const gl = canvas.getContext("webgl");
// 使用完全不透明的蓝色清除所有图像
gl.clearColor(0.0, 0.0, 1.0, 1.0);
// 用上面指定的颜色清除缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);
</script>
</body>

效果如下:

仍旧是3步:

  1. 获取 canvas
  2. 请求绘图上下文
  3. 调用绘图上下文中的绘图函数

在 canvas 绘制矩形之前需要指定颜色(ctx.fillStyle = 'green';),在 webgl 中类似,清空绘图区之前也得指定背景色,一旦指定背景色,背景色就会在 webgl 系统中存留,将来还需要使用同样的颜色清空绘图区,,就不需要再次指定背景色。

clearColor 和 clear语法如下:

// WebGLRenderingContext.clearColor() 方法用于设置清空颜色缓冲时的颜色值。指定调用 clear() 方法时使用的颜色值
void gl.clearColor(red, green, blue, alpha) // WebGLRenderingContext.clear() 方法使用预设值来清空缓冲。
void gl.clear(mask);
mask
gl.COLOR_BUFFER_BIT // 颜色缓冲区
gl.DEPTH_BUFFER_BIT // 深度缓冲区 - 三维世界中使用
gl.STENCIL_BUFFER_BIT // 模板缓冲区 - 很少使用

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

  • 颜色缓冲区 - (0.0, 0.0, 0.0, 0.0)
  • 深度缓冲区 - 1.0

绘制一个点

需求

需求:在 canvas 中心画一个 10px 红色的点。

效果如下:

思路

用 canvas 绘制一个矩形很简单,先指定颜色,在绘制矩形。就像这样:

// canvas绘制矩形
ctx.fillStyle = 'green';
ctx.fillRect(10, 10, 100, 100);

但 webgl 需要使用着色器,着色器提供了灵活且强大的绘制二维或三维的方法,也更加复杂。

我们先看代码,有一个具体的感受后,在分析其中细节。

代码

共3个文件。重点关注 point01.js 即可。

  • 新建入口文件 point01.html:
<!-- point01.html -->
<script src="./cuon-utils.js"></script>
<script src="./point01.js"></script> <body onload="main()">
<canvas id="webgl" width="300" height="300"> 抱歉,您的浏览器不支持 canvas 元素</canvas>
</body>

Tip:以上这段代码在 chrome 中运行通过,浏览器会自动补全格式,例如把 script 标签放入 head 中。

  • 新建 point01.js:
// point01.js
// 顶点着色器
const VSHADER_SOURCE = `
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 10.0;
}
` // 片元着色器
const FSHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
` function main() {
const canvas = document.getElementById('webgl'); const gl = canvas.getContext("webgl"); // 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
} gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.POINTS, 0, 1);
}

文档加载后运行 main() 方法,相对第一个 webgl 示例,这里增加了初始化着色器

Tip:现在只需要把初始化着色器的方法(initShaders() - 请看本篇 cuon-utils.js 章节)作为一个库中的辅助方法看待,后续文章将介绍其中原理。

  • 新建 cuon-utils.js(内容见本篇扩展),主要提供初始化着色器的方法

代码解析

总体流程

文档加载后执行 main() 方法,有如下5个阶段:

  • 获取canvas
  • 取得 webgl 上下文
  • 初始化着色器
  • 清除绘图区
  • 调用 drawArrays 绘图

下面我们主要讲一下第三步和最后一步。

齐次坐标

齐次坐标就是将一个原本是 n 维的向量用一个 n+1 维向量来表示。齐次坐标能提高处理三维数据的有效率,所以在三维系统中大量使用。齐次坐标(x, y, z, w) 等价于三维坐标 (x/w, y/w, z/w)

顶点着色器

顶点着色器(Vertex Shader) - 用来描述顶点特征的程序。例如这里的位置和大小。顶点指二维(x, y)或三维(x, y, z)空间中的一个点,例如端点或交点。

内置变量:

  • gl_Position - 用于描述顶点位置,必传,类型是 vec4(即4个float)
  • gl_PointSize - 用户描述顶点的尺寸(像素),如果不传,默认 1.0,类型是 float

关于位置,我们只有 (x, y, z) 三个变量,但 vec4 是 4 个,所以需要使用内置函数 vec4() 帮忙创建 vec4 类型的变量。

代码中 vec4(0.0, 0.0, 0.0, 1.0),这里第四个分量是 1.0,使用的是齐次坐标。

Tip:先记着 (0.0, 0.0, 0.0) 就是绘图区的中心,本篇 坐标系统 中会详细讲解。

片元着色器

片元着色器(Fragment Shader) - 进行逐片元处理过程如光照的程序。片元是 webgl 的一个术语,暂时可以将其理解成像素

内置变量:

  • gl_FragColor - 指定片元颜色(RGBA格式),类型是 vec4
初始化着色器

webgl 需要两种着色器:顶点着色器(Vertex Shader)、片元着色器(Fragment Shader)。

在三维场景中,仅仅用线条和颜色把图画出来不够,还需要考虑光照上去或者观察者的视角发生变化,对场景有什么影响。着色器可以灵活的完成这些工作。

初始化着色器之前,顶点着色器和片元着色器都是空白,把着色器程序作为字符串形式传给 initShaders(initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE))之后,webgl 系统中的着色器就建立好。

下图是执行 initShaders() 前后的情形:

Tip: 先执行顶点着色器,然后把 gl_Positiongl_PointSize 传给片元着色器。实际上片元着色器接收到的是经过栅格化处理后的片元(栅格化在画三角形时在讲解)。

绘图

建立着色器之后,首先清空绘图区域,然后使用 gl.drawArrays() 进行绘制。

gl.drawArrays(mode, first, count) 执行顶点着色器,按照 mode 指定的参数绘制图形。first 指定从哪个点开始绘制,count 指绘制需要几个点。

Tip:mode 类型有:

  • gl.POINTS: 绘制一系列点。
  • gl.LINE_STRIP: 绘制一个线条。即,绘制一系列线段,上一点连接下一点。
  • gl.LINE_LOOP: 绘制一个线圈。即,绘制一系列线段,上一点连接下一点,并且最后一点与第一个点相连。
  • gl.LINES: 绘制一系列单独线段。每两个点作为端点,线段之间不连接。
  • gl.TRIANGLE_STRIP:绘制一个三角带。
  • gl.TRIANGLE_FAN:绘制一个三角扇。
  • gl.TRIANGLES: 绘制一系列三角形。每三个点作为顶点。

例如我们这里是:gl.drawArrays(gl.POINTS, 0, 1),绘制图形(点),需要一个点,从第一个点开始绘制。后续画多个点时会对 first 和 count 有更清晰的理解。

代码注释
// point01.js
// 顶点着色器
const VSHADER_SOURCE = `
// 和 C 语言一样,必须包含一个 main() 函数,void 表示没有返回值
// 注:不能给 main() 指定参数
void main() {
// 顶点着色器内置变量: gl_Position 顶点位置、gl_PointSize 顶点尺寸
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 10.0;
}
` // 片元着色器
const FSHADER_SOURCE = `
void main() {
// 片元着色器内置变量: gl_FragColor 指定片元颜色
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
` function main() {
const canvas = document.getElementById('webgl'); const gl = canvas.getContext("webgl"); // 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('初始化着色器失败');
return;
} // 清空绘图区
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT); // 绘制图形(点),需要一个点,从第一个点开始绘制
gl.drawArrays(gl.POINTS, 0, 1);
}

扩展

坐标系统

webgl 的坐标系(x, y, z)和 canvas 的坐标系(x, y)不同。

canvas 的原点(0, 0)在左上角。webgl 处理的是三维,所以使用三维坐标系统(笛卡尔坐标系),可用(x, y, z) 表示。也可认为是右手坐标系。请看下图:

webgl 坐标和 canvas 坐标对应关系如下(可对照上面中间那张图):

cuon-utils.js

// cuon-utils.js (c) 2012 kanda and matsuda
/**
* Create a program object and make current
* @param gl GL context
* @param vshader a vertex shader program (string)
* @param fshader a fragment shader program (string)
* @return true, if the program object was created and successfully made current
*/
function initShaders(gl, vshader, fshader) {
var program = createProgram(gl, vshader, fshader);
if (!program) {
console.log('Failed to create program');
return false;
} gl.useProgram(program);
gl.program = program; return true;
} /**
* Create the linked program object
* @param gl GL context
* @param vshader a vertex shader program (string)
* @param fshader a fragment shader program (string)
* @return created program object, or null if the creation has failed
*/
function createProgram(gl, vshader, fshader) {
// Create shader object
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
if (!vertexShader || !fragmentShader) {
return null;
} // Create a program object
var program = gl.createProgram();
if (!program) {
return null;
} // Attach the shader objects
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader); // Link the program object
gl.linkProgram(program); // Check the result of linking
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
var error = gl.getProgramInfoLog(program);
console.log('Failed to link program: ' + error);
gl.deleteProgram(program);
gl.deleteShader(fragmentShader);
gl.deleteShader(vertexShader);
return null;
}
return program;
} /**
* Create a shader object
* @param gl GL context
* @param type the type of the shader object to be created
* @param source shader program (string)
* @return created shader object, or null if the creation has failed.
*/
function loadShader(gl, type, source) {
// Create shader object
var shader = gl.createShader(type);
if (shader == null) {
console.log('unable to create shader');
return null;
} // Set the shader program
gl.shaderSource(shader, source); // Compile the shader
gl.compileShader(shader); // Check the result of compilation
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
var error = gl.getShaderInfoLog(shader);
console.log('Failed to compile shader: ' + error);
gl.deleteShader(shader);
return null;
} return shader;
}

最新文章

  1. querySelector系列方法相比 getElementsBy 系列方法有什么区别?
  2. SPRING 标签库
  3. This Android SDK requires Android Developer Toolkit version 23.0.0 or above
  4. WCF大数据量传输配置
  5. dede列表标签递增数字生成
  6. js中定义对象的几种方式
  7. 容器—stack
  8. Nginx配置选项
  9. Git如何回滚代码?
  10. 【BZOJ1011】【HNOI2008】遥远的行星 误差分析
  11. 常用LaTeX随时更
  12. (转)java 序列化ID的作用
  13. 演示stop暴力停止线程导致数据不一致的问题,但是有些有趣的发现 (2017-07-03 21:25)
  14. [解决]CXF wsdl2java 生成代码存在的一些问题
  15. 【接口时序】5、QSPI Flash的原理与QSPI时序的Verilog实现
  16. 01-初始Java
  17. Ajax中返回数据的格式
  18. 关于ascii码的一些内容
  19. 移动端常见bug汇总001
  20. NetBeans Lookups Explained!

热门文章

  1. 1.5.5 HDFS读写解析-hadoop-最全最完整的保姆级的java大数据学习资料
  2. 【LeetCode】剑指 Offer 30. 包含min函数的栈
  3. Dijkstra 算法说明与实现
  4. USB转TTL串口 (CH340 G)
  5. Kubernetes监控手册-01体系概述
  6. APP兼容测试点与WEB兼容测试点
  7. 蚂蚁感冒【第五届蓝桥杯省赛C++A/B组】
  8. 【MySQL】MySQL8安装
  9. angular---路由传参后点击获取详情点开自动加载详情
  10. vue构建打包兼容操作(vue代码规范建议)-转载Vuejs项目不改动一行代码同时支持用Rollup,vue-cli,parcel构建的一些建议