WebGL编程指南笔记(一)概念及入门
第一章 WebGL概述
介绍
- WebGL被设计出来的目的, 就是在网页上创建三维的应用和用户体验---------从传统意义上来说, 为了显示三维图形, 开发者需要使用C或C++语言, 辅以专门的计算机图形库, 如OpenGL或Direct3D, 来开发一个独立的应用程序。 现在有了WebGL,我们只需要向已经熟悉的(如果你是一名Web前端开发者) HTML和avaScript中添加一些额外的三维图形学的代码, 就可以在网页上显示三维图形了。
- WebGL采用HTML5 中新引入的
<canvas>
标签,它定义了网页上的绘图区域。如果没有 WebGL,JavaScript只能在<canvas
>上绘制二维图形, 有了 WebGL, 就可以在上面绘制三维图形了,开发三维的客户界面、运行三维的网页游戏、对互联网上的海量数据进行三维可视化都成了可能。 - WebGL是内嵌在浏览器中的, 你不必安装插件和库就可以直接使用它。 而且, 因为它是基于浏览器(而不是基于操作系统) 的, 你可以在多种平台上运行WebGL程序,如高配置的个人计算机, 或消费类电子产品(如平板电脑和智能手机)。
技术起源
WebGL的技术规范继承自免费和开源的OpenGL标准。 从2.0版本开始,OpenGL支持了一项非常重要的特性---------可编程着色器方法 。 编写着色器的语言又称为着色器语言(shading language),OpenGL ES 2.0基于OpenGL着色器语言(GLSL), 因此后者又被称为OpenGLES着色器语言(GLSL ES)。 WebGL基于OpenGL ES 2.0, 也使用GLSL ES编写着色器。
程序结构
WebGL程序使用三种语言开发:HTML、JavaScript和GLSL ES。由于着色器代码GLSL ES内嵌在JavaScript中,所以WebGL网页的文件结构和传统网页一样。
第二章 WebGL入门
2.1 熟悉canvas
在HTML5出现之前,如果你想在网页上显示图像,只能使用HTML提供的原生方案<img>
标签。用这个标签显示图像虽然简单,但只能显示静态的图片,不能进行实时绘制和渲染。因此,后来出现了一些第三方解决方案,如FlashPlayer等。 HTML5的出现改变了一切,它引入了<canvas>
标签,允许JavaScript动态地绘制图形。
DrawRectangle.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Draw a blue rectangle (canvas version)</title>
</head>
<!-- 二、为 <body>元素指定 onload 属性
告诉浏览器 <body>元素加载完成后执行 main() 函数,并作为JavaScript程序的入口 -->
<body onload="main()">
<!-- 一、定义canvas标签-->
<canvas id="example" width="400" height="400">
Please use a browser that supports "canvas"
</canvas>
<!-- main函数就在其中 -->
<script src="DrawRectangle.js"></script>
</body>
</html>
DrawRectangle.js
js
function main() {
// 获取 <canvas> 元素
var canvas = document.getElementById('example');
if (!canvas) {
console.log('获取canvas元素失败');
return false;
}
// 获取绘制二维图形的绘图上下文
var ctx = canvas.getContext('2d');
// 设置填充颜色为蓝色
ctx.fillStyle = 'rgba(0, 0, 255, 1.0)';
// 给矩形填充颜色 fillRect(矩形的左上顶点在<canvas>中的x坐标,y坐标,宽度,高度)
ctx.fillRect(120, 10, 150, 150);
}
效果图
2.2 清空绘图区
HelloCanvas.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Clear "canvas"</title>
</head>
<body onload="main()">
<!-- WebGL将使用的<canvas>绘制图形 -->
<canvas id="webgl" width="400" height="400">
Please use a browser that supports "canvas"
</canvas>
<!-- 引入一些转为WebGL准备的、事先定义好的函数库-->
<script src="../lib/webgl-utils.js"></script>
<script src="../lib/webgl-debug.js"></script>
<script src="../lib/cuon-utils.js"></script>
<!-- js文件,在<canvas>中绘制图形-->
<script src="HelloCanvas.js"></script>
</body>
</html>
HelloCanvas.js
js
function main() {
// 荻取<canvas>元素
var canvas = document.getElementById('webgl');
// 获取WebGL绘图上下文
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('获取WebGL绘图上下文失败');
return;
}
// 指定清空<canvas>的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 清空<canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
}
- getWebGLContext(canvas):
- 该辅助函数来自../lib/cuon-utils.js。在获取WebGL绘图上下文时,canvas.getContext()函数接收的参数,在不同浏览器中会不同,所以写了一个函数getWebGLConTex()来隐藏不同浏览器之间的差异。
- gl.clearColor( res, green, blue, alpha):
- 指定背景色,对应着OpenGL ES 2.0或OpenGL中的glClearColor()函数。
- 取值不再是0-255,而是0.0-1.0。
- gl.clear(gl.COLOR_BUFFER_BIT)
- WebGL中的gl.clear()方法实际上继承自OpenGL, 它基于多基本缓冲区模型。 清空绘图区域,实际上是在清空颜色缓冲区(color buffer), 传递参数gl.COLOR_BUFFER_BIT就是在告诉WebGL清空颜色缓冲区。
- 参数可能值:
- gl.COLOR_BUFFER_BIT 指定颜色缓存
- gl.DEPTH_BUFFER_BIT 指定深度缓冲区
- gl.STENCIL BUFFER BIT 指定模板缓冲区
- 清空缓冲区的默认颜色及其相关函数
效果图
2.3绘制一个点
HelloPoint1.html
HTML文件同HelloCanvas.html,此处省略
HelloPoint1.js
js
// Vertex shader program 顶点着色器程序
var VSHADER_SOURCE =
'void main() {\n' +
' gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n' + // 设置坐标
' gl_PointSize = 10.0;\n' + // 设置尺寸
'}\n';
// Fragment shader program 片元着色器程序
var FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + // 设置颜色
'}\n';
function main() {
var canvas = document.getElementById('webgl');
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('获取WebGL绘图上下文失败');
return;
}
// 初始化着色器
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);
}
-
initShaders
- 该辅助函数对字符串形式的着色器进行了初始化。 该函数被定义在../lib/cuon-utils.js中的, 是专为本书编写的。
- 在初始化着色器之前,顶点着色器和片元着色器都是空白的,我们需要将字符串形式的着色器代码从JavaScript传给WebGL系统,并建立着色器,这就是initShaders()所做的事情。
- 顶点着色器先执行,它对gl_Position变量和gl_PointSize变量进行赋值,并将它们传入片元着色器,然后片元着色器再执行。实际上,片元着色器接收到的是经过光栅化处理后的片元值。
- WebGL程序包括【运行在浏览器中的JavaScript】和【运行在WebGL系统的着色器程序】这两个部分。着色器运行在WebGL系统中,而不是JavaScript程序中。
-
顶点着色器程序
和C语言程序一样,必须包含一个main()函数。 main()前面的关键字void表示这个函数不会有返回值。不能为main()指定参数。
vec4 gl_Position
- 表示顶点的位置。
- gl_Position变量必须被赋值, 否则着色器就无法正常工作。
- 内置函数
vec4()
---------帮助你创建vec4 类型的变量,传入4个浮点型分量,创建vec4类型对象- 注意, 我们添加了1.0 作为第4 个分量。 由 4 个分量组成的矢星被称为齐次坐标, 因为它能够提高处理三维数据的效率, 所以在三维图形系统中被大量使用。 虽然齐次坐标是四维的, 但是如果其最后一个 分量是1.0, 那么这个齐次坐标就可以表示 "前三个分量为坐标值" 的那个点。 所以, 当你需要用齐次坐标表示顶点坐标的时候, 只要将最后一个分量赋为1.0 就可以了。
float gl_PointSize
- 表示点的尺寸
- 如果不赋值,着色器就会为其取默认值1.0。
-
片元着色器程序
片元就是显示在屏幕上的一个像素(严格意义上来说, 片元包括这个像素的位置、 颜色和其他信息)。片元着色器的作用是处理片元, 使其显示在屏幕上。
vec4 gl_FragColor
- 指定片元颜色 (RGBA 格式)
- 是片元着色器唯一的内置变量
-
gl.drawArrays(mode,first, count)
- 参数说明
- 因为我们绘制的是单独的点, 所以设置第1个参数为gl. POINTS
- 设置第2个参数为0,表示从第1个顶点(虽然只有1个顶点)开始画起的 ;
- 第3个参数 count为1' 表示在这个简单的程序中仅绘制了1个点。
- 函数规范
- 处理流程
- 当程序调用gl.drawArrays() 时, 顶点着色器将被执行 count次, 每次处理一个顶点
- 在着色器执行的时候, 将调用并逐行执行内部的 main() 函数, 将值(0.0, 0.0, 0.0, 1.0)赋给gl_Position, 将值 10.0赋给gl_PointSize。
- 一旦顶点着色器执行完后,片元着色器就会开始执行,调用 main() 函数,将颜色值赋给gl_FragColor(第 12 行)
- 最后,一个红色的 10 个像素大的点就被绘制在了 (0.0, 0.0, 0.0, 1.0) 处,也就是绘制区域的中心位置
- 参数说明
效果图
WebGL坐标系统
- 可以认为是右手坐标系
- WebGL的坐标系和
<canvas>
绘图区的坐标系不同,需要将前者映射到后者。<canvas>
的中心点(0.0, 0.0, 0.0)<canvas>
的上边缘和下边缘:(-1.0, 0.0, 0.0)和(1.0,0.0, 0.0)<canvas>
的左边缘和右边缘:(0.0, -1.0, 0.0)和(0.0,1.0, 0.0)
着色器
在代码中,着色器程序是以字符串的形式"嵌入" 在JavaScript文件中的。在三维场景中,仅仅用线条和颜色把图形画出来是远远不够的。你必须考虑,比如,光线照上去之后,或者观察者的视角发生变化,对场景会有些什么影响。 着色器可以高度灵活地完成这些工作, 提供各种渲染效果.
- 顶点着色器(Vertex shader): 顶点着色器是用来描述顶点特性(如位置、 颜色等)的程序。 顶点 (vertex) 是指二维或三维空间中的一个点, 比如二维或三维图形的端点或交点。
- 片元着色器(Fragnment shader): 进行逐片元处理过程如光照(见第8章 "光照" ) 的程序。片元(fragment)是一个WebGL术语,你可以将其理解为像素(图像的单元)。
- 从执行JavaScript程序到在浏览器中显示结果的过程
2.4 绘制一个点(版本2)
WebGL程序可以将顶点的位置坐标从JavaScript传到着色器程序中,然后在对应位置上将点绘制出来。
HelloPoint2.html
HTML文件同HelloCanvas.html,此处省略
HelloPoint2.js
js
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' + // attribute 变量
'void main() {\n' +
' gl_Position = a_Position;\n' +
' gl_PointSize = 10.0;\n' +
'}\n';
var FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
'}\n';
function main() {
var canvas = document.getElementById('webgl');
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('获取WebGL绘图上下文失败');
return;
}
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('初始化着色器失败');
return;
}
// 获取attribute变量的存储位置
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if (a_Position < 0) {
console.log('获取attribute变量的存储位置失败');
return;
}
// 将顶点位置传输给attribute变量
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
}
- attribute变量: 传输与顶点相关的数据。attribute变量是一种GLSLES变量, 被用来从外部向顶点着色器内传输数据, 只有顶点着色器能使用它。
- uniform变量: 传输与顶点无关的数据
变量的声明必须按照以下的格式: < 存储限 定符><类型><变量名> 如:attribute vec4 a_Position
(数据类型vec4 表示由四个浮点数组成的矢量)
-
gl.getAttribLocation(program, name)
:获取attribute变量的存储位置 -
gl.vertexAttrib3f(location, v0,v1,v2)
:gl.vertexAttrib3f()
是一系列同族函数中的一个,该系列函数的任务就是从JavaScript向顶点着色器中的attribute变量传值 。gl.vertexAttrib1f()
传输1个单精度值(v0),gl.vertexAttrib2f()
传输2个值(v0和v1), 而gl.vertexAttrib4f()
传输4个值(v0、v1,v2和v3)。
- 如果你省略了第4个参数,这个方法就会默认地将第4个分量设置为了1.0
WebGL相关函数的命名规范------OpenGL中的函数名由三个部分组成:<基础函数名><参数个数><参数类型>,WebGL的函数命名使用同样的结构。<参数类型>中"f"表示浮点数,"i"表示整数。
- 矢量版本:如果函数名后面跟着一个v, 就表示函数也可以接收数组作为参数。 在这种情况下, 函数名中的数字表示数组中的元素个数。
jsvar position= new Float32Array([l.O, 2.0, 3.0, 1.0]); //函数名中4表示数组长度为4 gl.vertexAttrib4fv(a_Position, position);
效果图
同HelloPoint1,此处省略
2.5 通过鼠标点击绘点
ClickedPoints.html
HTML文件同HelloCanvas.html,此处省略
ClickedPoints.js
js
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'void main() {\n' +
' gl_Position = a_Position;\n' +
' gl_PointSize = 10.0;\n' +
'}\n';
var FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
'}\n';
function main() {
var canvas = document.getElementById('webgl');
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('获取WebGL绘图上下文失败');
return;
}
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('初始化着色器失败');
return;
}
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position');
return;
}
// 注册鼠标点击事件响应函数
canvas.onmousedown = function(ev){ click(event, gl, canvas, a_Position); };
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
}
var g_points = []; // //鼠标点击位置数组
function click(event, gl, canvas, a_Position) {
var x = event.clientX; // 鼠标点击处的X坐标
var y = event.clientY; // 鼠标点击处的X坐标
var rect = event.target.getBoundingClientRect() ;
// 获取WebGL坐标系下的x、y轴坐标
x = ((x - rect.left) - canvas.width/2)/(canvas.width/2);
y = (canvas.height/2 - (y - rect.top))/(canvas.height/2);
// 将坐标存储到g_points数组中
g_points.push([x,y])
// 清除 <canvas> 因为在绘制点之后 颜色缓冲区就被WebGL重置为了默认的颜色 (0.0, 0.0, 0.0, 0.0)
gl.clear(gl.COLOR_BUFFER_BIT);
g_points.forEach(item => {
// 将点的位置传递到变量中a_Position
gl.vertexAttrib2f(a_Position, item[0], item[1]);
// 绘制
gl.drawArrays(gl.POINTS, 0, 1);
})
}
canvas.onmoousedown
: 将事件响应函数注册在<canvas>
的onmousedown事件上event.clientX
、event.clientY
: 鼠标点击位置坐标。是在 "浏览器客户区" (client area) 中的坐标,而不是在<canvas>
中的坐标event.target.getBoundingClientRect
: 代码中的rect.left
和rect.top
是<canvas>
的原点在浏览器客户区中的坐标。
计算鼠标点的位置:
-
event.clientX
、event.clientY
获取鼠标点击位置在 "浏览器客户区" 中的坐标。 -
将客户区坐标系下的坐标(x,y)转化为
<canvas>
坐标系下的坐标。 -
将
<canvas>
坐标系下的坐标转化为WebGL坐标系下的坐标 WebGL坐标系统中,中心点的坐标是(canvas.height /2, canvas.width/2)
-
最终结果
x = ((x - rect.left) - canvas.width/2)/(canvas.width/2);
y = (canvas.height/2 - (y - rect.top))/(canvas.height/2);
效果图.gif
2.6 改变点的颜色
ColoredPoint.html
HTML文件同HelloCanvas.html,此处省略
ColoredPoint.js
js
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'void main() {\n' +
' gl_Position = a_Position;\n' +
' gl_PointSize = 10.0;\n' +
'}\n';
var FSHADER_SOURCE =
'precision mediump float;\n' +//使用精度限定词(precision qualifier) 来指定变量的范围(最大值与最小值)和精度, 本例中为中等精度。
'uniform vec4 u_FragColor;\n' + // uniform変数
'void main() {\n' +
' gl_FragColor = u_FragColor;\n' +
'}\n';
function main() {
var canvas = document.getElementById('webgl');
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('获取WebGL绘图上下文失败');
return;
}
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('初始化着色器失败');
return;
}
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if (a_Position < 0) {
console.log('获取attribute变量的存储位置失败');
return;
}
// 获取uniform变量的存储地址
var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
if (!u_FragColor) {
console.log('获取uniform变量的存储位置失败');
return;
}
// 注册鼠标点击事件响应函数
canvas.onmousedown = function(ev){ click(ev, gl, canvas, a_Position, u_FragColor) };
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
}
var g_points = [];
function click(event, gl, canvas, a_Position, u_color) {
var x = event.clientX;
var y = event.clientY;
var rect = event.target.getBoundingClientRect() ;
x = ((x - rect.left) - canvas.width/2)/(canvas.width/2);
y = (canvas.height/2 - (y - rect.top))/(canvas.height/2);
g_points.push([x,y])
gl.clear(gl.COLOR_BUFFER_BIT);
g_points.forEach(item => {
gl.vertexAttrib2f(a_Position, item[0], item[1]);
// 不同象限的点,给uniform变量赋不同的值
if (item[0] > 0){
if (item[1] > 0) {
gl.uniform4f(u_color, 0.0,1.0,0.0, 1.0)
}else {
gl.uniform4f(u_color, 0.0,0.0,1.0, 1.0)
}
}else{
if (item[1] > 0) {
gl.uniform4f(u_color, 1.0,0.0,0.0, 1.0)
}else {
gl.uniform4f(u_color, 1.0,1.0,0.0, 1.0)
}
}
// 绘制
gl.drawArrays(gl.POINTS, 0, 1);
})
}
gl.getUniformLocation(program, name)
:获取uniform变量的存储地址gl.uniform4f(location,v0,v1,v2,v3)
:向uniform变量赋值- gl.uniform1f()函数用来传输1个值(v0)
- gl.uniform2f()传输2个值(v0和v1)
- gl.un]iform3f()传输3个值(v0,v1和v2)