《WebGL编程指南》笔记(一)
前言
还是忍不住好奇心,先立flag再学习,这不是我的风格(本来是Q4的flag,想Q3偷跑的)。
主要是男神离职了,加速了这个进程;
不过说都说了,立又立了,反正也就一个早晚问题,就多多少少学点防身吧;
选定的书是《WebGL编程指南》,2014年的书,就是说下面的内容是2014年的时候就有的了;
现在也没有第二本说webgl的书了,看来webgl貌似不行了?
隔壁《openGL编程指南》都第9版了~
anyway,开始吧~
PS:
1. 书本的代码极少部分有bug;
2. 下面的代码都是跑过没问题的 ;
一. WebGL概述
1. WebGL的优势
- 用文本编辑器即可开发(。。。这怎么听起来像个缺点。。。IDE不香吗?)
- 轻松发布三维图形程序(应该说调试吧,毕竟不用编译;发布的话感觉都差不多~)
- 充分利用浏览器功能(这个也是,有html基础的话对比U3D,起码少学2d的一部分内容了)
- 学习和使用WebGL很简单(很简单。。简单。。。单。。。)
2. WebGL起源
三维渲染技术一般是微软的Direct3D和OpenGL。Direct3D只能用于windows;而OpenGL是免费又跨平台的。
现在(2022-7-7)已经有WebGL2.0了:https://blog.csdn.net/weixin_37683659/article/details/80160425
WebGL因为是渲染在Canvas上面的,所以做多线程会比较鸡肋(Canvas依附主线程),但是 --
现在已经有webGPU和webWorker的配合使用了:https://zhuanlan.zhihu.com/p/457600943
未来可期~
- OpenGL ES:主要是嵌入式设备的,手机啥的;
- OpenGL:主要是PC主机上的;
- WebGL:基于OpenGL ES 2.0外围包的一层供js调用的接口;
- GLSL:OpenGL着色器语言;
- GLSL ES:OpenGL ES着色器语言;
3. WebGL程序结构
- GLSL ES也是写在js文件里面,所以感觉上是没啥区别的。
二. WebGL入门
1. Canvas是什么?
- 步骤:1. 获取canvas元素;2. 使用该元素获取上下文(context);3. 在context上进行2d或者3d操作;
- canvas可以同时支持2d图形或3d图形;
- canvas的原点在左上角,向左是正X轴,向下是正Y轴;
- ctx.fillRect(矩形左上顶点的X轴坐标, 矩形左上顶点的Y轴坐标, 宽, 高)
- 下面是以绘制一个蓝色矩形为例:
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>画一个蓝色矩形(canvas版)</title> </head> <body onload="main()"> <canvas id="example" width="400" height="400"> 请使用支持canvas的浏览器 </canvas> </body> <script src="DrawRectangle.js"></script> </html>
DrawRectangle.js
function main(){ const canvas = document.getElementById("example"); if(!canvas){ console.error("获取不到canvas"); return; } // 获取二维图形的绘图上下文 const ctx = canvas.getContext("2d"); // 绘制蓝色矩形 ctx.fillStyle = 'blue'; // 设置填充颜色 ctx.fillRect(120, 10, 150, 150); // 使用填充颜色填充矩形 }
2. 最短的WebGL程序:清空绘图区
- WebGL的context跟canvas的context是不一样的;
- 引入的东西在:http://rodger.global-linguist.com/webgl/examples.zip 这里可以下载;
- gl.clearColor(red, green, blue, alpha),指定了清空画布所需要的颜色;
- gl.clear(buffer),把指定的缓冲区设置为预设值;
- gl.clear(buffer),buffer可以用位操作符"|"间开指定多个值;
- gl.clear(gl.COLOR_BUFFER_BIT),使用 gl.clearColor(red, green, blue, alpha) 指定的颜色填充,默认值:(0.0, 0.0, 0.0, 0.0);
- gl.clear(gl.DEPTH_BUFFER_BIT),使用 gl.clearDepth(depth) 指定的深度缓冲区,默认值:(1.0);
- gl.clear(gl.STENCIL_BUFFER_BIT),使用 gl.clearStencil(s) 指定的模板缓冲区,默认值:(0);
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>使用指定颜色清空画布</title> </head> <body onload="main()"> <canvas id="example" width="400" height="400"> 请使用支持canvas的浏览器 </canvas> </body> <script src="../lib/webgl-utils.js"></script> <script src="../lib/webgl-debug.js"></script> <script src="../lib/cuon-utils.js"></script> <script src="clearCanvas.js"></script> </html>
clearCanvas.js
function main(){ const canvas = document.getElementById("example"); // 获取二维图形的绘图上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失败"); return; } // 指定清空画布所用的颜色 gl.clearColor(0, 0, 1, 1); // 使用上面指定的颜色清空画布 gl.clear(gl.COLOR_BUFFER_BIT); }
3. 绘制一个点(版本1)
- webGL的绘制依赖着色器(shader)的绘图机制;
- 着色器(shader)提供了二维和三维的绘图方法;
- 着色器程序是以字符串的形式嵌入到js文件中;
- 顶点着色器(Vertex shader):用来描述顶点特性(如位置、颜色等);
- 顶点(Vertex):指二维或三维空间中的一个点;
- 片元着色器(Fragment shader):进行逐片元处理过程,比如光照;
- 片元(Fragment):一个webGL术语,可以理解为像素;
- 浏览器渲染过程如下图:
- 这些以字符串形式出现的就是我们的“OpenGL ES着色器语言(GLSL ES)”;
- vec4():表示由4个浮点数组成的矢量(又叫向量);
- gl_Position:中vec4分别表示(x, y, z, w),其中w表示齐次坐标,取值范围在(0, 1];
- gl_Position:齐次坐标(x, y, z, w)等价于三维坐标(x/w, y/w, z/w);
- gl_Position:如果w趋近于0,表示的点将趋近无穷远;
- gl_PointSize:表示点尺寸(像素),默认为1;
- gl_FragColor:指定片元颜色,vec4分别表示(r, g, b, a)值;
- gl.drawArrays(mode, first, count):执行顶点着色器,按照mode指定的方式绘制;
- mode - gl.POINTS,gl.LINES,gl.LINE_STRIP,gl.LINE_LOOP,gl.TRIANGLES,gl.TRIANGLE_STRIP,gl.TRIANGLE_FAN;
- first - 从那个顶点开始绘制;
- count - 需要绘制到第几个顶点;
- webGL坐标系统 - 采用笛卡尔坐标系;
- webGL坐标系统 - 面向屏幕:X轴正向-水平向右;Y轴正向-垂直向上;Z轴正向-垂直屏幕向外;
- webGL坐标系统 - 原点在画布中心;
cuon-utils.js中关于创建program对象的部分
// 初始化着色器 function initShaders(gl, vshader, fshader) { var program = createProgram(gl, vshader, fshader); if (!program) { console.log('Failed to create program'); return false; } // 使用这个program对象 gl.useProgram(program); // initShaders的时候顺手把program注入到gl里面 gl.program = program; return true; } // 新建并返回一个program对象 function createProgram(gl, vshader, fshader) { // 新建着色器对象 var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader); var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader); if (!vertexShader || !fragmentShader) { return null; } // 新建program对象 var program = gl.createProgram(); if (!program) { return null; } // 绑定(Attach)着色器对象到program gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); // 链接program对象 gl.linkProgram(program); // 检查链接状态 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; }
draw_point_1.js
function main(){ const canvas = document.getElementById("example"); // 获取二维图形的绘图上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失败"); return; } // 顶点着色程序 const VSHADER_SOURCE = "void main(){" + " gl_Position = vec4(0.0, 0.0, 0.0, 1.0);" + // 设置坐标,必须有值 " gl_PointSize = 10.0;" + // 设置尺寸,默认为1 "}"; // 片源着色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 设置颜色 "}"; // 初始化着色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化着色器失败'); return; } // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制一个点 gl.drawArrays(gl.POINTS, 0, 1); }
3. 绘制一个点(版本2)
- JavaScript程序可以通过attribute变量和uniform变量传值给顶点着色器;
- 传值流程图如下:
- uniform变量:用于传输那些对所有顶点都相同或与顶点无关的数据;
- attribute变量:用于从外部向顶点着色器内传数据,只有顶点着色器可以使用它;
- attribute变量:声明 - attribute [类型] [变量名];
- attribute变量:每一个变量都会有一个存储地址,需要通过getAttribLocation()来向webGL获取该地址;
- attribute变量:外部js通过 - gl.getAttribLocation([webGL着色器程序], [变量名]) 来获取变量地址;
- attribute变量:获取变量地址后通过 - gl.vertexAttrib3f([变量名], [浮点数1], [浮点数2], [浮点数3]) 来修改;
- attribute变量:gl.vertexAttrib3f的同族函数:vertexAttrib[n]f[v]() - n为1~4数字,表示多少个浮点数,v为这个方法的向量(vector)版,用法如下:
var position = new Float32Array([1.0, 2.0, 3.0, 1.0]); gl.vertexAttrib4fv(a_Position, positon);
- attribute变量:同族里面还有一个int型的 - vertexAttribI4[u]i[v](),但是在macbook上firefox和chrome都用不了;
draw_point_2.js
function main(){ const canvas = document.getElementById("example"); // 获取二维图形的绘图上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失败"); return; } // 顶点着色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "attribute float a_PointSize;" + "void main(){" + " gl_Position = a_Position;" + // 设置坐标 " gl_PointSize = a_PointSize;" + // 设置尺寸 "}"; // 片源着色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 设置颜色 "}"; // 初始化着色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化着色器失败'); return; } // 在着色器程序(program)中获取指定名称的顶点着色器变量地址,估计是数组地址 let a_Position = gl.getAttribLocation(gl.program, 'a_Position'); // a_Position的值为0 let a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize'); // a_Position的值为1 if(a_Position < 0 || a_PointSize < 0){ console.log("获取失败"); return; } // 把顶点位置传给attribute变量。1f表示1个浮点数,如此类推 gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0); gl.vertexAttrib1f(a_PointSize, 20.0); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制一个点 gl.drawArrays(gl.POINTS, 0, 1); }
4. 通过鼠标点击绘点
- canvas坐标系和webGL坐标系如下图:
- webGL的坐标是分量值,可以理解为按照百分比来算的;
- 换个说法:上图canvas如果是长方形,那么webGL坐标x或y轴方向取值范围也会是-1 ~ 1;
- webgl_x = (canvas_x - width / 2) / (width / 2);
- webgl_y = -(canvas_y - height / 2) / (height / 2);
- 另外下面demo,当点击渲染正方形后,会发现鼠标在正方形的中心点;
draw_point_mouse.js
function main(){ const canvas = document.getElementById("example"); // 获取二维图形的绘图上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失败"); return; } // 顶点着色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "attribute float a_PointSize;" + "void main(){" + " gl_Position = a_Position;" + // 设置坐标 " gl_PointSize = a_PointSize;" + // 设置尺寸 "}"; // 片源着色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 设置颜色 "}"; // 初始化着色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化着色器失败'); return; } // 在着色器程序(program)中获取指定名称的顶点着色器变量地址,估计是数组地址 let a_Position = gl.getAttribLocation(gl.program, 'a_Position'); // a_Position的值为0 let a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize'); // a_Position的值为1 if(a_Position < 0 || a_PointSize < 0){ console.log("获取失败"); return; } // 指定半径 gl.vertexAttrib1f(a_PointSize, 30); let g_points = []; // 存储点击的点坐标 // 注册鼠标响应事件 canvas.onmousedown = function(e){ let x = e.clientX; // 鼠标相对于屏幕的水平坐标 let y = e.clientY; // 鼠标相对于屏幕的垂直坐标 let rect = e.target.getBoundingClientRect(); // canvas对象的位置信息 // (x - rect.left):鼠标在x轴方向上的偏移,相当于canvas的x轴坐标 x = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2); // (y - rect.top):鼠标在y轴方向上的偏移,相当于canvas的y轴坐标 y = -((y - rect.top) - canvas.height / 2) / (canvas.height / 2); g_points.push({x, y}); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制点 for(let i = 0; i < g_points.length; i++){ gl.vertexAttrib3f(a_Position, g_points[i].x, g_points[i].y, 0.0); gl.drawArrays(gl.POINTS, 0, 1); } }; }
5. 改变点的颜色
- 使用uniform变量给片元着色器传值,从而让每个点击的点上色;
- webgl的颜色是分量值,与rgb的转换关系:webgl_color = rgb_color / 255;
- precision为精度限定词,总的来说精度越高,执行效率越低;
- uniform变量:通过 gl.getUniformLocation([webGL着色器程序], [变量名]) 来获取变量地址;
- uniform变量:变量不存在返回null;
- uniform变量:通过 gl.uniform4f([变量名], [值1], [值2], [值3], [值4]) 设置值;
- uniform变量:同族函数 - uniform[1234][fi][v]() 其中:f - 浮点;i - 整型;v:向量
draw_point_mouse_color.js
function main(){ const canvas = document.getElementById("example"); // 获取二维图形的绘图上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失败"); return; } // 顶点着色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "attribute float a_PointSize;" + "void main(){" + " gl_Position = a_Position;" + // 设置坐标 " gl_PointSize = a_PointSize;" + // 设置尺寸 "}"; // 片源着色器程序 const FSHADER_SOURCE = "precision mediump float;" + // 定义使用中等精度的浮点数 "uniform vec4 u_FragColor;" + "void main(){" + " gl_FragColor = u_FragColor;" + // 设置颜色 "}"; // 初始化着色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化着色器失败'); return; } // 在着色器程序(program)中获取指定名称的顶点着色器变量地址,估计是数组地址 let a_Position = gl.getAttribLocation(gl.program, 'a_Position'); // a_Position的值为0 let a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize'); // a_Position的值为1 let u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor'); if(a_Position < 0 || a_PointSize < 0 || !u_FragColor){ console.log("获取失败"); return; } // 指定半径 gl.vertexAttrib1f(a_PointSize, 5); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); let g_points = []; // 存储点击的点坐标 let COLORS_CARD = [ [243, 155, 58], [249, 206, 82], [101, 148, 68], [132, 185, 182], [113, 100, 144] ]; // 色卡 // 注册鼠标响应事件 canvas.onmousedown = function(e){ let x = e.clientX; // 鼠标相对于屏幕的水平坐标 let y = e.clientY; // 鼠标相对于屏幕的垂直坐标 let rect = e.target.getBoundingClientRect(); // canvas对象的位置信息 // (x - rect.left):鼠标在x轴方向上的偏移,相当于canvas的x轴坐标 x = ((x - rect.left) - canvas.width / 2) / (canvas.width / 2); // (y - rect.top):鼠标在y轴方向上的偏移,相当于canvas的y轴坐标 y = -((y - rect.top) - canvas.height / 2) / (canvas.height / 2); g_points.push({ x, y, // 轮取色卡值,因为webgl的颜色是分量值,webgl_color = rgb_color / 255 color: COLORS_CARD[g_points.length % COLORS_CARD.length].map(item => item / 255) }); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制点 for(let i = 0; i < g_points.length; i++){ gl.vertexAttrib3f(a_Position, g_points[i].x, g_points[i].y, 0.0); gl.uniform4f(u_FragColor, ...g_points[i].color, 1); gl.drawArrays(gl.POINTS, 0, 1); } }; }
三. 绘制和变换三角形
1. 绘制多个点
- 构成三维模型的基本单位是三角形;
- 缓冲区对象:可以一次性地向着色器传入多个顶点数据;
- 写入缓冲区对象的五个步骤:
- 创建缓冲区对象(gl.createBuffer())
- 绑定缓冲区对象(gl.bindBuffer())
- 把数据写入缓冲区对象(gl.bufferData())
- 把缓冲区对象分配给一个attribute变量(gl.vertexAttribPointer())
- 开启attribute变量(gl.enableVertexAttribArray())
- createBuffer:返回一个WebGLBuffer的实例;
- createBuffer:可以通过 gl.deleteBuffer(buffer) 删除指定的缓冲区对象;
- bindBuffer:允许使用buffer的缓冲区并绑到target指定的目标上;
- bindBuffer:gl.bindBuffer([target], [buffer]);
- bindBuffer:target -
- gl.ARRAY_BUFFER:表示缓冲区对象中包含了顶点的数据;
- gl.ELEMENT_ARRAY_BUFFER:表示缓冲区中包含了顶点的索引值;
- bindBuffer:buffer - 通过createBuffer返回的缓冲区对象;
- bufferData:向绑定再target上的缓冲区对象中写入数据data
- bufferData:gl.bufferData([target], [data], [usage]);
- bufferData:target同上为 - gl.ARRAY_BUFFER / gl.ELEMENT_ARRAY_BUFFER;
- bufferData:data - 写入的数据,类型化数组(关于类型化数组的描述可以参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Typed_arrays);
- bufferData:usage - 表示程序将如何使用缓冲区对象中的数据,可以帮助webGL优化操作、增加效率,取值范围如下 :
- gl.STATIC_DRAW:只向缓冲区对象写入一次数据,但要绘制很多次;
- gl.STREAM_DRAW:只向缓冲区对象写入一次数据,然后绘制若干次;
- gl.DYNAMIC_DRAW:会向缓冲区对象写入多次数据,并且绘制很多次;
- 类型化数组的常用操作如下图,感觉就跟node的buffer操作差不多:
- vertexAttribPointer:将绑定到gl.ARRAY_BUFFER的缓冲区对象分配给由location指定的attribute变量;
- vertexAttribPointer:gl.ertexAttribPointer(location, size, type, normalized, stride, offset);
- vertexAttribPointer:location - getAttribLocation的返回值,绑定了attribute变量;
- vertexAttribPointer:size - 指定缓冲区中每个顶点的分量个数,取值[1, 4],依次表示 x、y、z、w,不足按xyz默认为0,w默认为1补齐;
- vertexAttribPointer:type - 指定数据格式,包括:
- gl.UNSIGNED_BYTE:相当于类型化数组的 - Uint8Array;
- gl.SHORT:相当于类型化数组的 - Int16Array;
- gl.UNSIGNED_SHORT:UInt16Array;
- gl.INT:Int32Array;
- gl.UNSIGNED_INT:Uint32Array;
- gl.FLOAT:Float32Array;
- vertexAttribPointer:normalized - 是否将非浮点类型的数字归一化到 [0, 1] 或 [-1, 1] 区间;
- vertexAttribPointer:stride - 指定相邻两个顶点的字节数,默认为0(0表示按照type的位数直接平分offset之后剩下的buffer);
- vertexAttribPointer:offset - 以字节为单位,指定缓冲区对象中的偏移量(即attribute变量从缓冲区中的何处开始存储);
- vertexAttribPointer:在第二部分第五章有补充用法的demo;
- enableVertexAttribArray:开启location指定的attribute变量;
- enableVertexAttribArray:enableVertexAttribArray(location);
- enableVertexAttribArray:可以使用disableVertexAttribArray(location)来关闭;
- drawArrays不能超过缓冲区的点数量;
draw_multi_points.js
function main(){ const canvas = document.getElementById("example"); // 获取二维图形的绘图上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失败"); return; } // 顶点着色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "void main(){" + " gl_Position = a_Position;" + // 设置坐标 " gl_PointSize = 10.0;" + // 设置尺寸 "}"; // 片源着色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 设置颜色 "}"; // 初始化着色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化着色器失败'); return; } // 两个一组表示三角形三个顶点的x,y坐标 const vertices = new Float32Array([ 0, 0.5, -0.5, -0.5, 0.5, -0.5 ]); const n = 3; // 顶点个数 // (1)创建缓冲区对象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("创建缓冲区对象失败"); return; } // (2)把缓冲区对象绑定到目标 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向缓冲区对象写入数据 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); // (4)把缓冲区对象分配给a_Position变量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); // (5)链接a_Position变量与分配给他的缓冲区对象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制三个点 gl.drawArrays(gl.POINTS, 0, 3); }
2. 绘制三角形、正方形
- gl.drawArrays:有7种不同的绘制模式:
- gl.POINTS:一系列点,绘制在 v0, v1, v2... ;
- gl.LINES:一系列线段,绘制在 (v0, v1), (v2, v3), (v4, v5)... ;
- gl.LINE_STRIP:一系列连接的线段,绘制在 (v0, v1), (v1, v2), (v2, v3)... ;
- gl.LINE_LOOP:一系列连接且闭合的线段,绘制在 (v0, v1), (v1, v2), (v2, v3), ..., (vn, v0);
- gl.TRIANGLES:一系列单独的三角形,绘制在 (v0, v1, v2), (v3, v4, v5)... ;
- gl.TRIANGLE_STRIP:一系列条带状的三角形,绘制在 (v0, v1, v2), (v2, v1, v3), (v2, v3, v4)... ;
- gl.TRIANGLE_STRIP:当前点为奇数点,点序:(n, n + 1, n + 2);当前点为偶数,点序:(n + 1, n, n + 2);
- gl.TRIANGLE_STRIP - 确保每个三角形都是逆时针方向连接 => 统一法向量方向 => 光反射方向;
- gl.TRIANGLE_FAN:一系列三角形组成类似扇形的图形,绘制在 (v0, v1, v2), (v0, v2, v3), (v0, v3, v4)... ;
draw_function_demo.js
function main(){ const canvas = document.getElementById("example"); // 获取二维图形的绘图上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失败"); return; } // 顶点着色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "void main(){" + " gl_Position = a_Position;" + // 设置坐标 " gl_PointSize = 10.0;" + // 设置尺寸 "}"; // 片源着色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 设置颜色 "}"; // 初始化着色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化着色器失败'); return; } // 两个一组表示三角形三个顶点的x,y坐标 const vertices = new Float32Array([ 0, 0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5 ]); const n = 3; // 顶点个数 // (1)创建缓冲区对象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("创建缓冲区对象失败"); return; } // (2)把缓冲区对象绑定到目标 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向缓冲区对象写入数据 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); // (4)把缓冲区对象分配给a_Position变量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); // (5)链接a_Position变量与分配给他的缓冲区对象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制三个点,不能超过缓冲区的点数 gl.drawArrays(gl.POINTS, 1, 4); // 绘制三角形 gl.drawArrays(gl.TRIANGLES, 0, 3); // 绘制线段 gl.drawArrays(gl.LINES, 0, 3); // 绘制线段 gl.drawArrays(gl.LINE_STRIP, 0, 3); // 绘制线段 gl.drawArrays(gl.LINE_LOOP, 0, 3); // 绘制四方形,相当于两个直角三角形相连 gl.drawArrays(gl.TRIANGLE_STRIP, 1, 4); // 绘制四方形,4分3个正方形 gl.drawArrays(gl.TRIANGLE_FAN, 1, 4); }
3. 移动
- 移动前坐标:(x, y, z, w),移动后坐标:(x_after, y_after, z_after, w_after),位移向量:(x_delta, y_delta, z_delta, w_delta),关系如下:
- x + x_delta = x_after;y + y_delta = y_after;z + z_delta = z_after;w + w_delta = 1;
- 其中变化后的必为1;
- GLSL中两个vec4变量相加表示为:两个变量各自对应的位置相加,如下图:
move.js
function main(){ const canvas = document.getElementById("example"); // 获取二维图形的绘图上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失败"); return; } // 顶点着色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "uniform vec4 u_Translation;" + "void main(){" + " gl_Position = a_Position + u_Translation;" + // 设置坐标 " gl_PointSize = 10.0;" + // 设置尺寸 "}"; // 片源着色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 设置颜色 "}"; // 初始化着色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化着色器失败'); return; } // 两个一组表示三角形三个顶点的x,y坐标 const vertices = new Float32Array([ -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5 ]); // (1)创建缓冲区对象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("创建缓冲区对象失败"); return; } // (2)把缓冲区对象绑定到目标 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向缓冲区对象写入数据 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); const u_Translation = gl.getUniformLocation(gl.program, "u_Translation"); // (4)把缓冲区对象分配给a_Position变量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); gl.uniform4f(u_Translation, 0.25, 0.25, 0.0, 0.0); // 按照向量 (0.25, 0.25) 移动 // (5)链接a_Position变量与分配给他的缓冲区对象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制三个点,不能超过缓冲区的点数 gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); }
4. 旋转
- 通过 旋转轴(向量值)、旋转方向(顺逆时针)、旋转角度来描述一个旋转;
- 正旋转 -> 角度为正值 -> 沿旋转轴方向的逆时针旋转;
- 设旋转前向量为(x, y),旋转后向量为(x_after, y_after),旋转角度为p,2D旋转公式为:
- x_after = x * cos(p) - y * sin(p);
- y_after = x * sin(p) + y * cos(p);
- z_after = z;
- 推理用到的公式:sin(A +/- B) = sinA * cosB +/- cosA * sinB;cos(A +/- B) = cosA * cosB -/+ sinA * sinB;
- 弧度(rad):弧长等于半径的弧,其所对的圆心角为1弧度。
- 角度弧度转换:180º = π * rad;
rotate.js
function main(){ const canvas = document.getElementById("example"); // 获取二维图形的绘图上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失败"); return; } // 顶点着色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "uniform float u_CosA, u_SinA;" + "void main(){" + // 分别设置旋转后坐标,x、y、z、w一个不能少 " gl_Position.x = a_Position.x * u_CosA - a_Position.y * u_SinA;" + " gl_Position.y = a_Position.x * u_SinA + a_Position.y * u_CosA;" + " gl_Position.z = a_Position.z;" + " gl_Position.w = 1.0;" + " gl_PointSize = 10.0;" + // 设置尺寸 "}"; // 片源着色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 设置颜色 "}"; // 初始化着色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化着色器失败'); return; } // 两个一组表示三角形三个顶点的x,y坐标 const vertices = new Float32Array([ -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5 ]); const ANGLE = 90; // 旋转90度 const radian = Math.PI * ANGLE / 180.0; // 转为弧度 const cosA = Math.cos(radian); const sinA = Math.sin(radian); // (1)创建缓冲区对象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("创建缓冲区对象失败"); return; } // (2)把缓冲区对象绑定到目标 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向缓冲区对象写入数据 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); const u_SinA = gl.getUniformLocation(gl.program, "u_SinA"); const u_CosA = gl.getUniformLocation(gl.program, "u_CosA"); // (4)把缓冲区对象分配给a_Position变量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); gl.uniform1f(u_SinA, sinA); gl.uniform1f(u_CosA, cosA); // (5)链接a_Position变量与分配给他的缓冲区对象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制三个点,不能超过缓冲区的点数 gl.drawArrays(gl.TRIANGLES, 0, 3); }
5. 矩阵转换
- 对于平移、缩放、旋转这些点位置的操作可以通过矩阵来进行统一的描述;
- 设原来的点p(x, y, z, 1), 操作后的点p1(x1, y1, z1, 1),则有如下矩阵:
- 平移矩阵,Tx、Ty、Tz分别为在x、y、z方向上的偏移量:
- 2D旋转矩阵,β为转角:
- 缩放矩阵,Sx、Sy、Sz分别为x、y、z方向上的缩放比:
6. 使用矩阵转换
- webGL中的矩阵是按列主序的,就是 [a, e, i, m, b, f, j, n, c, g, k, o, d, h, l, p];
- gl.uniformMatrix4fv(location, transpose, array)
- location:uniform变量位置
- transpose:是否置换矩阵,webgl不支持所以必为true;
- array:4x4矩阵数据;
- 置换矩阵:置换操作将交换矩阵的行和列;
matrix.js
function main(){ const canvas = document.getElementById("example"); // 获取二维图形的绘图上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失败"); return; } // 顶点着色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "uniform mat4 u_xformMatrix;" + "void main(){" + " gl_Position = u_xformMatrix * a_Position;" + " gl_PointSize = 10.0;" + // 设置尺寸 "}"; // 片源着色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 设置颜色 "}"; // 初始化着色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化着色器失败'); return; } // 两个一组表示三角形三个顶点的x,y坐标 const vertices = new Float32Array([ -0.5, -0.5, 0.5, -0.5, -0.5, 0.5 ]); // 旋转 const ANGLE = 45; // 旋转90度 const radian = Math.PI * ANGLE / 180.0; // 转为弧度 const cosA = Math.cos(radian); const sinA = Math.sin(radian); // 角度矩阵 const xformMatrix_rotate = new Float32Array([ cosA, sinA, 0.0, 0.0, -sinA, cosA, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ]); // 平移 const Tx = 0.1, Ty = 0.2, Tz = 0.0; const xformMatrix_move = new Float32Array([ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, Tx, Ty, Tz, 1.0 ]); // 缩放 const Sx = 1, Sy = 2, Sz = 1; const xformMatrix = new Float32Array([ Sx, 0.0, 0.0, 0.0, 0.0, Sy, 0.0, 0.0, 0.0, 0.0, Sz, 0.0, 0.0, 0.0, 0.0, 1.0 ]); // (1)创建缓冲区对象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("创建缓冲区对象失败"); return; } // (2)把缓冲区对象绑定到目标 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向缓冲区对象写入数据 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix"); // (4)把缓冲区对象分配给a_Position变量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix); // (5)链接a_Position变量与分配给他的缓冲区对象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制三个点,不能超过缓冲区的点数 gl.drawArrays(gl.TRIANGLES, 0, 3); }
四. 高级变换与动画基础
1. 基于cuon-matrix.js进行矩阵转换
- 这里介绍了这本书专用的一个库“cuon-matrix.js”,下面的代码都是基于这个库的了;
- 这个库主要用来做矩阵的转换,其他框架其实也有自己的矩阵转换库;
- 使用这些矩阵转换库时一定要搞清楚:这些库输出的矩阵是按行主序还是按列主序的;
- cuon-matrix.js会把一系列转换后的结果输出到elements里面;
matrix_base_cuon.js
function main(){ const canvas = document.getElementById("example"); // 获取二维图形的绘图上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失败"); return; } // 顶点着色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "uniform mat4 u_xformMatrix;" + "void main(){" + " gl_Position = u_xformMatrix * a_Position;" + " gl_PointSize = 10.0;" + // 设置尺寸 "}"; // 片源着色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 设置颜色 "}"; // 初始化着色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化着色器失败'); return; } // 两个一组表示三角形三个顶点的x,y坐标 const vertices = new Float32Array([ -0.5, -0.5, 0.5, -0.5, -0.5, 0.5 ]); // 生成一个矩阵 const xformMatrix = new Matrix4(); // 旋转 const ANGLE = 180; // 旋转90度 xformMatrix.setRotate(ANGLE, 0, 0, 1); // 平移 const Tx = 0.1, Ty = 0.2, Tz = 0.0; xformMatrix.setTranslate(Tx, Ty, Tz); // 缩放 const Sx = 1, Sy = 2, Sz = 1; xformMatrix.setScale(Sx, Sy, Sz); // (1)创建缓冲区对象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("创建缓冲区对象失败"); return; } // (2)把缓冲区对象绑定到目标 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向缓冲区对象写入数据 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix"); // (4)把缓冲区对象分配给a_Position变量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix.elements); // (5)链接a_Position变量与分配给他的缓冲区对象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制三个点,不能超过缓冲区的点数 gl.drawArrays(gl.TRIANGLES, 0, 3); }
2. 复合变换
- 3x3矩阵乘法法则:
- 矩阵乘法满足结合律,即:矩阵 A、B、C,( A * B ) * C = A * ( B * C );
- 但是矩阵乘法不满足交换律,即:矩阵 A、B、C,A * B * C ≠ B * A * C;
- 因为矩阵相乘满足结合律,所以矩阵的复合变换可以先把多次转换的矩阵相乘之后再乘以原矩阵;
- 又因为矩阵相乘不满足交换律,所以矩阵的复合变换次序不一样时,输出不一样的模型矩阵;
- 上述多次转换的过程叫:模型变换(model transformation)或称建模变换(modeling transformation);
- 上述多次转换后得到的矩阵叫:模型矩阵(model matrix);
function main(){ const canvas = document.getElementById("example"); // 获取二维图形的绘图上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失败"); return; } // 顶点着色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "uniform mat4 u_xformMatrix;" + "void main(){" + " gl_Position = u_xformMatrix * a_Position;" + " gl_PointSize = 10.0;" + // 设置尺寸 "}"; // 片源着色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 设置颜色 "}"; // 初始化着色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化着色器失败'); return; } // 两个一组表示三角形三个顶点的x,y坐标 const vertices = new Float32Array([ -0.5, -0.5, 0.5, -0.5, -0.5, 0.5 ]); // 生成一个矩阵 const xformMatrix = new Matrix4(); // 旋转 const ANGLE = 180; // 旋转90度 xformMatrix.rotate(ANGLE, 0, 0, 1); // 平移 const Tx = 0.1, Ty = 0.2, Tz = 0.0; xformMatrix.translate(Tx, Ty, Tz); // 缩放 const Sx = 1, Sy = 2, Sz = 1; xformMatrix.scale(Sx, Sy, Sz); // (1)创建缓冲区对象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("创建缓冲区对象失败"); return; } // (2)把缓冲区对象绑定到目标 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向缓冲区对象写入数据 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix"); // (4)把缓冲区对象分配给a_Position变量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix.elements); // (5)链接a_Position变量与分配给他的缓冲区对象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制三个点,不能超过缓冲区的点数 gl.drawArrays(gl.TRIANGLES, 0, 3); }
3. 动画
- requestAnimationFrame:目的是为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画、WebGL动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。
- requestAnimationFrame:类似于settimeout,不过跟的是屏幕刷新率,吃的主线程资源。
- requestAnimationFrame:详情参考:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
- requestAnimationFrame:在不同刷新率的屏幕动画播放的速度会不一样,所以要加入时间戳来保证速率。
- requestAnimationFrame:说是切tab的时候会停止执行。但我试过chrome上切tab,setInterval也会停下来。
- requestAnimationFrame:尝试挂两个回调进去也是莫得问题,应该是跑同一个时钟周期,这样就很漂亮了。
- requestAnimationFrame:返回一个requestId,可以通过cancelAnimationFrame(requestId)中止动画。
animation.js
function main(){ const canvas = document.getElementById("example"); // 获取二维图形的绘图上下文 const gl = getWebGLContext(canvas); if(!gl){ console.error("webgl渲染失败"); return; } // 顶点着色程序 const VSHADER_SOURCE = "attribute vec4 a_Position;" + "uniform mat4 u_xformMatrix;" + "void main(){" + " gl_Position = u_xformMatrix * a_Position;" + " gl_PointSize = 10.0;" + // 设置尺寸 "}"; // 片源着色器程序 const FSHADER_SOURCE = "void main(){" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" + // 设置颜色 "}"; // 初始化着色器 if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){ console.log('初始化着色器失败'); return; } function draw(animationOps){ // 两个一组表示三角形三个顶点的x,y坐标 const vertices = new Float32Array([ -0.5, -0.5, 0.5, -0.5, -0.5, 0.5 ]); // 生成一个矩阵 const xformMatrix = new Matrix4(); // 旋转 const ANGLE = animationOps.rotate || 0; // 旋转90度 xformMatrix.rotate(ANGLE, 0, 0, 1); // 平移 const Tx = animationOps.translate.x || 0, Ty = animationOps.translate.y || 0, Tz = animationOps.translate.z || 0; xformMatrix.translate(Tx, Ty, Tz); // 缩放 const Sx = animationOps.scale.sx || 1, Sy = animationOps.scale.sy || 1, Sz = animationOps.scale.sz || 1; xformMatrix.scale(Sx, Sy, Sz); // (1)创建缓冲区对象 const vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.error("创建缓冲区对象失败"); return; } // (2)把缓冲区对象绑定到目标 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // (3)向缓冲区对象写入数据 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const a_Position = gl.getAttribLocation(gl.program, "a_Position"); const u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix"); // (4)把缓冲区对象分配给a_Position变量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix.elements); // (5)链接a_Position变量与分配给他的缓冲区对象 gl.enableVertexAttribArray(a_Position); // 清空canvas gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); // 绘制三个点,不能超过缓冲区的点数 gl.drawArrays(gl.TRIANGLES, 0, 3); } let last = Date.now(); let current_ang = 0; function tick() { const ANGLE_STEP = 45; // 角速度,一秒45度 const now = Date.now(); const elapsed = now - last; last = now; const new_ang = current_ang + (ANGLE_STEP * elapsed) / 1000.0; current_ang = new_ang; draw({ rotate: new_ang, translate: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } }); requestAnimationFrame(tick); } tick(); }