课堂目标
- 对webgl 的概念有一个整体认知
- 掌握webgl 基本绘图原理和流程
知识点
- webgl 概述
- webgl 最短教程 - 在画布上刷底色
- webgl 最简单图形 - 画一个点
第一章 webgl概述
1-webgl是什么?
webgl 是在网页上绘制和渲染三维图形的技术,可以让用户与其进行交互。
我们之前学过的div+css、canvas 2d 都是专注于二维图形的,它们虽然也能模拟一部分三维效果,但它们和webgl 比起来,那就是玩具枪和AK47的差别。
2-webgl行业背景
随着 5G 时代的到来,3D可视化需求大量涌现。3D 游戏,酷炫的活动宣传页,三维数字城市,VR全景展示、3D 产品展示等领域中,很多项目都是用 WebGL 实现的,也只能用WebGL来做,也就是说,WebGL 的时代就在眼前了。
通过一些实际案例,我们可以知道WebGL 能做什么:
- 3D数据可视化:cybermap.kaspersky.com/
- 家居卖场:showroom.littleworkshop.fr/
- 天猫宣传页:shrek.imdevsh.com/show/tmall/
- 汽车模型:ezshine.gitee.io/www/showcas…
- 趣空间:www.3dnest.cn/page/case/c…
3-为什么要学习webgl ?
webgl 的行业背景决定了其在市场中具有广大的需求量。
webgl 发展潜力大,不像曾经的flash,学完了,还会面临被淘汰的风险。
webgl 的职场竞争力要比vue、react等主流框架小。
webgl薪资可观,一般只要你理解webgl原理,可以熟练使用three.js,会用react,月薪可达25k+
下面是我在boss 直聘上的截图:
综上所述,对公司而言,webgl 可以解决他们在三维模型的显示和交互上的问题;对开发者而言,webgl 可以让我们是实现更多、更炫酷的效果,让我们即使工作,也可以乐在其中,并且还会有一份不错的薪资。
第二章 webgl 最短教程
接下来咱们说一个在webgl 画布上刷底色的简单栗子。
1-刷底色的基本步骤
1.在html中建立canvas 画布
<canvas id="canvas"></canvas>
2.在js中获取canvas画布
const canvas=document.getElementById('canvas’);
3.使用canvas 获取webgl 绘图上下文
const gl=canvas.getContext('webgl’);
4.指定将要用来清空绘图区的颜色
gl.clearColor(0,0,0,1);
5.使用之前指定的颜色,清空绘图区
gl.clear(gl.COLOR_BUFFER_BIT);
整体代码
<canvas id="canvas"></canvas>
<script>
const canvas=document.getElementById('canvas');
canvas.width=window.innerWidth;
canvas.height=window.innerHeight;
const gl=canvas.getContext('webgl');
gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT);
</script>
clearColor(r,g,b,a) 中的参数是红、绿、蓝、透明度,其定义域是[0,1]
2-灵活操作webgl中的颜色
css 中有一个“rgba(255,255,255,1)” 颜色,其中r、g、b的定义域是[0,255],这里要和webgl里的颜色区分一下。
我们可以简单了解一下将css颜色解析为webgl 颜色的原理:
const rgbaCSS = "rgba(255,0,0,1)";
const reg = RegExp(/\((.*)\)/);
const rgbaStr = reg.exec(rgbaCSS)[1];
const rgb = rgbaStr.split(",").map((ele) => parseInt(ele));
const r = rgb[0] / 255;
const g = rgb[1] / 255;
const b = rgb[2] / 255;
const a = rgb[3];
gl.clearColor(r, g, b, a);
gl.clear(gl.COLOR_BUFFER_BIT);
在three.js 里有一个非常完美的颜色对象-Color,我们通过这个对象可以轻松的控制颜色。
案例-多姿多彩的画布
1.引入Color 对象
import { Color } from "https://unpkg.com/three/build/three.module.js";
我这是通过CDN 引入的,这种方法不适用于nodejs,因为nodejs 无法直接通过网络路径请求资源。
2.实例化Color 对象
const color = new Color(1, 0, 0);
3.建立色相偏移动画
!(function ani() {
color.offsetHSL(0.005, 0, 0);
gl.clearColor(color.r, color.g, color.b, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
requestAnimationFrame(ani);
})();
关于颜色的操作我们就说到这,Color 对象还有很多其它方法,可以在threejs官网查看。
3-webgl 坐标系
webgl画布的建立和获取,和canvas 2d是一样的。
一旦我们使用canvas.getContext()方法获取了webgl 类型的上下文对象,那这张画布就不再是以前的canvas 2d 画布。
当然,它也不会变成三维的,因为我们的电脑屏幕始终是平的。
那这张画布有什么不一样了呢?
它的坐标系变了。
canvas 2d 画布和webgl 画布使用的坐标系都是二维直角坐标系,只不过它们坐标原点、y 轴的坐标方向,坐标基底都不一样了。
3-1-canvas 2d画布的坐标系
canvas 2d 坐标系的原点在左上角。
canvas 2d 坐标系的y 轴方向是朝下的。
canvas 2d 坐标系的坐标基底有两个分量,分别是一个像素的宽和一个像素的高,即1个单位的宽便是1个像素的宽,1个单位的高便是一个像素的高。
如下图,下图两个方块表示两个像素:
3-2-webgl的坐标系
webgl坐标系的坐标原点在画布中心。
webgl坐标系的y 轴方向是朝上的。
webgl坐标基底中的两个分量分别是半个canvas的宽和canvas的高,即1个单位的宽便是半个个canvas的宽,1个单位的高便是半个canvas的高。
第三章 webgl 最简单的图形-画一个点
点是最简单的形状,是几何图形最基本的组成部分。接下来咱们就说一下在webgl 画布上如何画一个点。
首先咱们先说一下绘图的基本步骤。
1-绘图的基本步骤
-
找一张画布。
-
找一支画笔。
-
开始画画。
这三步是从现实生活中抽离出来的普遍规律,这在哪里都是适用的。
canvas 2d 的绘图逻辑就是这样的。首先要有canvas 画布,然后通过canvas 画布的getContext('2d') 方法获取一支二维画笔,然后直接在画布上画画。就像这样:
//canvas画布
const canvas=document.getElementById('canvas');
//二维画笔
const ctx=canvas.getContext('2d');
//设置画笔的颜色
ctx.fillStyle='red';
//用画笔画一个矩形
ctx.fillRect(20,20,300,200);
webgl 的绘图逻辑亦是如此,只不过它更像电脑绘画,其绘画的步骤里还多了一层介质。
这层介质就是手绘板,就像这样:
- 找一台电脑。
- 找一块手绘板。
- 找一支触控笔。
- 开始画画。
接下来,咱们就详细说一下canvas 2d 画图和webgl 画图的差异。
2-canvas 2d和webgl绘图的差异
在webgl里绘图,或许你会觉得也可以像canvas 2d那样,就像下面这样写:
//canvas画布
const canvas=document.getElementById('canvas');
//三维画笔
const ctx=canvas.getContext('webgl');
//设置画笔的颜色
ctx.fillStyle='red';
//用画笔画一个立方体
ctx.fillBox(20,20,300,200);
然而,实际上,webgl 的绘图逻辑和canvas 2d 的绘图逻辑还有一个本质的差别。
大家在学习html 的时候应该知道,浏览器有三大线程: js 引擎线程、GUI 渲染线程、浏览器事件触发线程。
其中GUI 渲染线程就是用于渲图的,在这个渲染线程里,有负责不同渲染工作的工人。比如有负责渲染HTML+css的工人,有负责渲染二维图形的工人,有负责渲染三维图形的工人。
渲染二维图形的工人和渲染三维图形的工人不是一个国家的,他们说的语言不一样。
渲染二维图形的工人说的是js语言。
渲染三维图形的工人说的是GLSL ES 语言。
而我们在做web项目时,业务逻辑、交互操作都是用js 写的。
我们在用js 绘制canvas 2d 图形的时候,渲染二维图形的工人认识js 语言,所以它可以正常渲图。
但我们在用js 绘制webgl图形时,渲染三维图形的工人就不认识这个js 语言了,因为它只认识GLSL ES 语言。
因此,这个时候我们就需要找人翻译翻译。
这个做翻译的人是谁呢,它就是我们之前提到过的手绘板,它在webgl 里叫“程序对象”。
接下来咱们从手绘板的绘图步骤中捋一下webgl 的绘图思路。
3-webgl 的绘图思路
- 找一台电脑 - 浏览器里内置的webgl 渲染引擎,负责渲染webgl 图形,只认GLSL ES语言。
- 找一块手绘板 - 程序对象,承载GLSL ES语言,翻译GLSL ES语言和js语言,使两者可以相互通信。
- 找一支触控笔 - 通过canvas 获取的webgl 类型的上下文对象,可以向手绘板传递绘图命令,并接收手绘板的状态信息。
- 开始画画 - 通过webgl 类型的上下文对象,用js 画画。
在上面的思路中,大家对其中的一些名词可能还没有太深的概念,比如程序对象。接下来咱们就详细说一下webgl 实际的绘图步骤。
4-webgl 的绘图步骤
1.在html中建立canvas 画布
<canvas id="canvas"></canvas>
2.在js中获取canvas画布
const canvas=document.getElementById('canvas');
3.使用canvas 获取webgl 绘图上下文
const gl=canvas.getContext('webgl');
4.在script中建立顶点着色器和片元着色器,glsl es
//顶点着色器
<script id="vertexShader" type="x-shader/x-vertex">
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 100.0;
}
</script>
//片元着色器
<script id="fragmentShader" type="x-shader/x-fragment">
void main() {
gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
</script>
5.在js中获取顶点着色器和片元着色器的文本
const vsSource = document.getElementById('vertexShader').innerText;
const fsSource = document.getElementById('fragmentShader').innerText;
6.初始化着色器
initShaders(gl, vsSource, fsSource);
7.指定将要用来清空绘图区的颜色
gl.clearColor(0,0,0,1);
8.使用之前指定的颜色,清空绘图区
gl.clear(gl.COLOR_BUFFER_BIT);
9.绘制顶点
gl.drawArrays(gl.POINTS, 0, 1);
整体代码
<canvas id="canvas"></canvas>
<!-- 顶点着色器 -->
<script id="vertexShader" type="x-shader/x-vertex">
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 100.0;
}
</script>
<!-- 片元着色器 -->
<script id="fragmentShader" type="x-shader/x-fragment">
void main() {
gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
</script>
<script>
// canvas 画布
const canvas = document.getElementById('canvas');
canvas.width=window.innerWidth;
canvas.height=window.innerHeight;
// webgl画笔
const gl = canvas.getContext('webgl');
// 顶点着色器
const vsSource = document.getElementById('vertexShader').innerText;
// 片元着色器
const fsSource = document.getElementById('fragmentShader').innerText;
// 初始化着色器
initShaders(gl, vsSource, fsSource);
// 指定将要用来清理绘图区的颜色
gl.clearColor(0., 0.0, 0.0, 1.0);
// 清理绘图区
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制顶点
gl.drawArrays(gl.POINTS, 0, 1);
function initShaders(gl,vsSource,fsSource){
//创建程序对象
const program = gl.createProgram();
//建立着色对象
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
//把顶点着色对象装进程序对象中
gl.attachShader(program, vertexShader);
//把片元着色对象装进程序对象中
gl.attachShader(program, fragmentShader);
//连接webgl上下文对象和程序对象
gl.linkProgram(program);
//启动程序对象
gl.useProgram(program);
//将程序对象挂到上下文对象上
gl.program = program;
return true;
}
function loadShader(gl, type, source) {
//根据着色类型,建立着色器对象
const shader = gl.createShader(type);
//将着色器源文件传入着色器对象中
gl.shaderSource(shader, source);
//编译着色器对象
gl.compileShader(shader);
//返回着色器对象
return shader;
}
</script>
对于上面的步骤1、2、3,大家应该都比较好理解,接下来咱们详细说一下第4 步,在script 里用GLSL ES语言写着色器。
5-着色器
5-1-着色器的概念
webgl 绘图需要两种着色器:
- 顶点着色器(Vertex shader):描述顶点的特征,如位置、颜色等。
- 片元着色器(Fragment shader):进行逐片元处理,如光照。
看了这两个名词的解释,我想很多初学者会是懵的。
我给大家翻译翻译:
补间动画大家知道不?顶点着色器里的顶点就是补间动画里的关键帧,片元着色器里的片元就是关键帧之间以某种算法算出的插值。当然,咱们webgl里的片元是像素的意思。
再给大家举一个更简单、更贴切的例子:
两点决定一条直线大家知道不?顶点着色器里的顶点就是决定这一条直线的两个点,片元着色器里的片元就是把直线画到画布上后,这两个点之间构成直线的每个像素。
关于概念咱们就说到这,接下来咱们说着色器语言。
5-2-着色器语言
webgl 的着色器语言是GLSL ES语言
- 顶点着色程序,要写在type=“x-shader/x-vertex” 的script中。
<script id="vertexShader" type="x-shader/x-vertex">
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 100.0;
}
</script>
- 片元着色程序,要写在type=“x-shader/x-fragment” 的script中。
<script id="fragmentShader" type="x-shader/x-fragment">
void main() {
gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
</script>
void main() {…… } 是主体函数。
在顶点着色器中,gl_Position 是顶点的位置,gl_PointSize 是顶点的尺寸,这种名称都是固定的,不能写成别的。
在片元着色器中,gl_FragColor 是片元的颜色。
vec4() 是一个4维矢量对象。
将vec4() 赋值给顶点点位gl_Position 的时候,其中的前三个参数是x、y、z,第4个参数默认1.0,其含义我们后面会详解;
将vec4() 赋值给片元颜色gl_FragColor 的时候,其中的参数是r,g,b,a。
至于GLSL ES语言的其它知识,咱们会在后面另开一篇详解,这里先以入门为主。
在第6步中,我们使用了一个自定义的方法initShaders() ,这是用于初始化着色器的,接下来咱们详细说一下。
6-着色器初始化
初始化着色器的步骤:
-
建立程序对象,目前这只是一个手绘板的外壳。
const shaderProgram = gl.createProgram();
-
建立顶点着色器对象和片元着色器对象,这是手绘板里用于接收触控笔信号的零部件,二者可以分工合作,把触控笔的压感(js信号)解析为计算机语言(GLSL ES),然后让计算机(浏览器的webgl 渲染引擎)识别显示。
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource); const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
-
将顶点着色器对象和片元着色器对象装进程序对象中,这就完成的手绘板的拼装。
gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader);
-
连接webgl 上下文对象和程序对象,就像连接触控笔和手绘板一样(触控笔里有传感器,可以向手绘板发送信号)。
gl.linkProgram(shaderProgram);
-
启动程序对象,就像按下了手绘板的启动按钮,使其开始工作。
gl.useProgram(program);
上面第二步中的建立着色对象方法loadShader(),是一个自定义的方法,其参数是(webgl上下文对象,着色器类型,着色器源文件),gl.VERTEX_SHADER 是顶点着色器类型,gl.FRAGMENT_SHADER是片元着色器类型。
function loadShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
-
gl.createShader(type) :根据着色器类型建立着色器对象的方法。
-
gl.shaderSource(shader, source):将着色器源文件传入着色器对象中,这里的着色器源文件就是我们之前在script 里用GLSL ES写的着色程序。
-
gl.compileShader(shader):编译着色器对象。
在以后的学习里,initShaders 会经常用到,所以我们可以将其模块化。
function initShaders(gl,vsSource,fsSource){
//创建程序对象
const program = gl.createProgram();
//建立着色对象
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
//把顶点着色对象装进程序对象中
gl.attachShader(program, vertexShader);
//把片元着色对象装进程序对象中
gl.attachShader(program, fragmentShader);
//连接webgl上下文对象和程序对象
gl.linkProgram(program);
//启动程序对象
gl.useProgram(program);
//将程序对象挂到上下文对象上
gl.program = program;
return true;
}
function loadShader(gl, type, source) {
//根据着色类型,建立着色器对象
const shader = gl.createShader(type);
//将着色器源文件传入着色器对象中
gl.shaderSource(shader, source);
//编译着色器对象
gl.compileShader(shader);
//返回着色器对象
return shader;
}
export {initShaders}
后面在需要的时候,import 引入即可。
import {initShaders} from '../jsm/Utils.js';
总结
综上所述,webgl 绘图好麻烦啊!
麻烦不是不学的理由,因为后面还有three.js 为你排忧解难。
那我们为啥不直接学习three.js 呢?
那是因为three.js 若是只想画个旋转的立方体还好,若是要深入学习,实现复杂的模型交互逻辑,就必须要有webgl 基础了。
到这里让大家对webgl 的绘图原理有了一个基本认知。
使用js向着色器传递数据
获取鼠标在canvas 中的webgl 坐标系位置
知识点
- attribute 变量
- gl.vertextAttribute3f() 的同族函数
- 鼠标在canvas 中的css 位置转webgl 坐标位
- uniform 变量
- gl.uniform4f() 的同族函数
第一章 用js控制一个点的位置
1-attribute 变量的概念。
回顾一下我们上一篇中点的定位:
gl_Position = vec4(0,0,0,1);
这是一种将数据写死了的硬编码,缺乏可扩展性。
我们要让这个点位可以动态改变,那就得把它变成attribute变量。
attribute 变量是只有顶点着色器才能使用它的。
js 可以通过attribute 变量向顶点着色器传递与顶点相关的数据。
2-js向attribute 变量传参的步骤
- 在顶点着色器中声明attribute 变量。
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
gl_PointSize = 50.0;
}
</script>
- 在js中获取attribute 变量
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
- 修改attribute 变量
gl.vertexAttrib3f(a_Position,0.0,0.5,0.0);
整体代码
<canvas id="canvas"></canvas>
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
gl_PointSize = 50.0;
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
void main() {
gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
</script>
<script type="module">
import {initShaders} from '../jsm/Utils.js';
const canvas = document.getElementById('canvas');
canvas.width=window.innerWidth;
canvas.height=window.innerHeight;
const gl = canvas.getContext('webgl');
const vsSource = document.getElementById('vertexShader').innerText;
const fsSource = document.getElementById('fragmentShader').innerText;
initShaders(gl, vsSource, fsSource);
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
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);
</script>
接下来详细解释一下。
3-js向attribute 变量传参的原理
3-1-着色器中的attribute 变量
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
gl_PointSize = 50.0;
}
- attribute 是存储限定符,是专门用于向外部导出与点位相关的对象的,这类似于es6模板语法中export 。
- vec4 是变量类型,vec4是4维矢量对象。
- a_Position 是变量名,之后在js中会根据这个变量名导入变量。这个变量名是一个指针,指向实际数据的存储位置。也是说,我们如果在着色器外部改变了a_Position所指向的实际数据,那么在着色器中a_Position 所对应的数据也会修改。
接下来,咱们说一下在js 里如何获取attribute 变量。
3-2-在js中获取attribute 变量
我们在js 里不能直接写a_Position 来获取着色器中的变量。
因为着色器和js 是两个不同的语种,着色器无法通过window.a_Position 原理向全局暴露变量。
那我们要在js 里获取着色器暴露的变量,就需要找人来翻译,这个人就是程序对象。
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
- gl 是webgl 的上下文对象。
- gl.getAttribLocation() 是获取着色器中attribute 变量的方法。
- getAttribLocation() 方法的参数中:
- gl.program 是初始化着色器时,在上下文对象上挂载的程序对象。
- 'a_Position' 是着色器暴露出的变量名。
这个过程翻译过来就是:gl 上下文对象对program 程序对象说,你去顶点着色器里找一个名叫'a_Position' 的attribute变量。
现在a_Position变量有了,接下来就可以对它赋值了。
3-3-在js中修改attribute 变量
attribute 变量即使在js中获取了,他也是一个只会说GLSL ES语言的人,他不认识js 语言,所以我们不能用js 的语法来修改attribute 变量的值:
a_Position.a=1.0
我们得用特定的方法改变a_Position的值:
gl.vertexAttrib3f(a_Position,0.0,0.5,0.0);
-
gl.vertexAttrib3f() 是改变变量值的方法。
-
gl.vertexAttrib3f() 方法的参数中:
-
a_Position 就是咱们之前获取的着色器变量。
-
后面的3个参数是顶点的x、y、z位置
-
a_Position被修改后,我们就可以使用上下文对象绘制最新的点位了。
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
4-扩展
4-1-vertexAttrib3f()的同族函数
gl.vertexAttrib3f(location,v0,v1,v2) 方法是一系列修改着色器中的attribute 变量的方法之一,它还有许多同族方法,如:
gl.vertexAttrib1f(location,v0)
gl.vertexAttrib2f(location,v0,v1)
gl.vertexAttrib3f(location,v0,v1,v2)
gl.vertexAttrib4f(location,v0,v1,v2,v3)
它们都可以改变attribute 变量的前n 个值。
比如 vertexAttrib1f() 方法自定一个矢量对象的v0值,v1、v2 则默认为0.0,v3默认为1.0,其数值类型为float 浮点型。
4-2-webgl 函数的命名规律
GLSL ES里函数的命名结构是:<基础函数名><参数个数><参数类型>
以vertexAttrib3f(location,v0,v1,v2,v3) 为例:
- vertexAttrib:基础函数名
- 3:参数个数,这里的参数个数是要传给变量的参数个数,而不是当前函数的参数个数
- f:参数类型,f 代表float 浮点类型,除此之外还有i 代表整型,v代表数字……
关于用js 控制点位的方法咱们就说到这,接下咱们说一个用鼠标控制点位的例子。
第二章 用鼠标控制点位
我们要用鼠标控制一个点的位置,首先要知道鼠标点在webgl 坐标系中的位置,这样才能让一个点出现在我们鼠标点击的位置。
接下来咱们就说一下如何获取鼠标点在webgl 坐标系中的位置。
1-获取鼠标点在webgl 坐标系中的位置
对于鼠标点在webgl 坐标系中的位置,我们是无法直接获取的。所以我们得先获取鼠标在canvas 这个DOM元素中的位置。
1-1-获取鼠标在canvas 画布中的css 位置
canvas.addEventListener('click',function(event){
const {clientX,clientY}=event;
const {left,top}=canvas.getBoundingClientRect();
const [cssX,cssY]=[
clientX-left,
clientY-top
];
})
对于cssX,cssY 的获取,大家应该都不陌生,这在canvas 2d 也会用到。
我们可以用向量减法来求解。
已知:向量a(clientX,clientY),向量c(left,top)
求:向量c
解:
由向量的减法得:向量a减向量c,等于以向量c 的终点为起点,以向量a的终点为终点的向量c
所以:向量c=a-c=(clientX-left,clientY-top)
将向量c 视之为坐标点c,那点c 就是鼠标在canvas 画布中的css 位。
因为html 坐标系中的坐标原点和轴向与canvas 2d是一致的,所以在我们没有用css 改变画布大小,也没有对其坐标系做变换的情况下,鼠标点在canvas 画布中的css 位就是鼠标点在canvas 2d坐标系中的位置。
2-2-canvas 坐标系转webgl 坐标系
咱们这里的变换思路就是解决差异,接着上面的代码来写。
1.解决坐标原点位置的差异。
const [halfWidth,halfHeight]=[width/2,height/2];
const [xBaseCenter,yBaseCenter]=[cssX-halfWidth,cssY-halfHeight];
上面的[halfWidth,halfHeight]是canvas 画布中心的位置。
[xBaseCenter,yBaseCenter] 是用鼠标位减去canvas 画布的中心位,得到的就是鼠标基于画布中心的位置。
2.解决y 方向的差异。
const yBaseCenterTop=-yBaseCenter;
因为webgl 里的y 轴和canvas 2d 里的y轴相反,所以咱们对yBaseCenter 值取一下反即可。
3.解决坐标基底的差异。
const [x,y]=[xBaseCenter/halfWidth,yBaseCenterTop/halfHeight]
由于canvas 2d 的坐标基底中的两个分量分别是一个像素的宽高,而webgl的坐标基底的两个分量是画布的宽高,所以咱们得求个比值。
整体代码:
canvas.addEventListener('click',function(event){
const {clientX,clientY}=event;
const {left,top,width,height}=canvas.getBoundingClientRect();
const [cssX,cssY]=[
clientX-left,
clientY-top
];
const [halfWidth,halfHeight]=[width/2,height/2];
const [xBaseCenter,yBaseCenter]=[cssX-halfWidth,cssY-halfHeight];
const yBaseCenterTop=-yBaseCenter;
const [x,y]=[xBaseCenter/halfWidth,yBaseCenterTop/halfHeight];
})
关于获取鼠标点在webgl 坐标系中的位置的方法,我们就说到这,接下来咱们基于这个位置,修改着色器暴露出来的位置变量即可。
2-修改attribute 变量
这个步骤和第一章的内容是差不多的:
- 获取attribute 变量
- 在获取鼠标在webgl 画布中的位置的时候,修改attribute 变量
- 清理画布
- 绘图
import {initShaders} from '../jsm/Utils.js';
const canvas = document.getElementById('canvas');
canvas.width=window.innerWidth;
canvas.height=window.innerHeight;
const gl = canvas.getContext('webgl');
const vsSource = document.getElementById('vertexShader').innerText;
const fsSource = document.getElementById('fragmentShader').innerText;
initShaders(gl, vsSource, fsSource);
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
canvas.addEventListener('click',function(event){
const {clientX,clientY}=event;
const {left,top,width,height}=canvas.getBoundingClientRect();
const [cssX,cssY]=[
clientX-left,
clientY-top
];
const [halfWidth,halfHeight]=[width/2,height/2];
const [xBaseCenter,yBaseCenter]=[cssX-halfWidth,cssY-halfHeight];
const yBaseCenterTop=-yBaseCenter;
const [x,y]=[xBaseCenter/halfWidth,yBaseCenterTop/halfHeight];
gl.vertexAttrib2f(a_Position,x,y);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
})
在上面的例子中,大家每点击一次canvas 画布,都会画出一个点,而上一次画的点就会消失,我们无法连续画出多个点。
为何会如此呢?我们分析一下。
3-webgl 的同步绘图原理
具备canvas 2d可能会认为无法画出多点是gl.clear(gl.COLOR_BUFFER_BIT) 清理画布导致,因为我们在用canvas 2d 做动画时,其中就有一个ctx.clearRect() 清理画布的方法。
那咱们将gl.clear() 方法注释掉试试。
3-1-用实践得真知
gl.vertexAttrib2f(a_Position,x,y);
//gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
当我们鼠标点击画布时,画布中原本的黑色已经没有了,而且我们每次也只能画一个点。
我们分析一下。
gl.drawArrays(gl.POINTS, 0, 1) 方法和canvas 2d 里的ctx.draw() 方法是不一样的,ctx.draw() 真的像画画一样,一层一层的覆盖图像。
gl.drawArrays() 方法只会同步绘图,走完了js 主线程后,再次绘图时,就会从头再来。也就说,异步执行的drawArrays() 方法会把画布上的图像都刷掉。
举个栗子:
1.我先画两个点
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.vertexAttrib2f(a_Position,0.1,0);
gl.drawArrays(gl.POINTS, 0, 1);
gl.vertexAttrib2f(a_Position,-0.1,0);
gl.drawArrays(gl.POINTS, 0, 1);
好的,没问题。
2.我想一秒后,再画一个点。
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.vertexAttrib2f(a_Position,0.1,0);
gl.drawArrays(gl.POINTS, 0, 1);
gl.vertexAttrib2f(a_Position,-0.1,0);
gl.drawArrays(gl.POINTS, 0, 1);
setTimeout(()=>{
gl.vertexAttrib2f(a_Position,0,0);
gl.drawArrays(gl.POINTS, 0, 1);
},1000)
以前画好的两个点没了,黑色背景也没了。这就是咱们之前说过的webgl 同步绘图原理。
那这个问题如何解决呢?这就是一个简单的逻辑问题了。
3.我们可以用数组把一开始的那两个顶点存起来,在异步绘制第3个顶点的时候,把那两个顶点也一起画上。
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
const g_points=[
{x:0.1,y:0},
{x:-0.1,y:0},
];
render();
setTimeout(()=>{
g_points.push({x:0,y:0});
render();
},1000)
function render(){
gl.clear(gl.COLOR_BUFFER_BIT);
g_points.forEach(({x,y})=>{
gl.vertexAttrib2f(a_Position,x,y);
gl.drawArrays(gl.POINTS, 0, 1);
})
}
这样就可以以叠加覆盖的方式画出第三个点了。
4.理解上面的原理后,那我们接下来就可以用鼠标绘制多个点了。
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
const g_points=[];
canvas.addEventListener('click',function(event){
const {clientX,clientY}=event;
const {left,top,width,height}=canvas.getBoundingClientRect();
const [cssX,cssY]=[
clientX-left,
clientY-top
];
const [halfWidth,halfHeight]=[width/2,height/2];
const [xBaseCenter,yBaseCenter]=[cssX-halfWidth,cssY-halfHeight];
const yBaseCenterTop=-yBaseCenter;
const [x,y]=[xBaseCenter/halfWidth,yBaseCenterTop/halfHeight];
g_points.push({x,y});
gl.clear(gl.COLOR_BUFFER_BIT);
g_points.forEach(({x,y})=>{
gl.vertexAttrib2f(a_Position,x,y);
gl.drawArrays(gl.POINTS, 0, 1);
})
})
关于用鼠标控制点位,并绘制多点的方法我们就说到这。咱们最后简单总结一下这个原理。
3-2-webgl 同步绘图原理总结
webgl 的同步绘图的现象,其实是由webgl 底层内置的颜色缓冲区导致的。
“胸有成竹”大家知道吧?这个颜色缓冲区就是“胸有成竹”的胸,它在电脑里会占用一块内存。在我们使用webgl 绘图的时候,是先在颜色缓冲区中画出来,这样的图像还在胸中,所以外人看不见,只有webgl系统自己知道。
在我们想要将图像显示出来的时候,那就照着颜色缓冲区中的图像去画,这个步骤是webgl 内部自动完成的,我们只要执行绘图命令即可。
颜色缓冲区中存储的图像,只在当前线程有效。比如我们先在js 主线程中绘图,主线程结束后,会再去执行信息队列里的异步线程。在执行异步线程时,颜色缓冲区就会被webgl 系统重置,我们曾经在主线程里的“胸有成竹”也就没了,既然没了,也就画不出那时的图像了。
webgl 绘图原理我就说到这。接下来咱们用js控制顶点尺寸。
4-用js控制顶点尺寸
用js 控制顶点尺寸的方法和控制顶点位置的方法是一样的,所以咱们这里就不再另起一章了。
1.首先咱们还是要在着色器里暴露出一个可以控制顶点尺寸的attribute 变量。
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
attribute float a_PointSize;
void main(){
gl_Position = a_Position;
gl_PointSize = a_PointSize;
}
</script>
上面的a_PointSize 是一个浮点类型的变量。
2.在js 里获取attribute 变量
const a_PointSize=gl.getAttribLocation(gl.program,'a_PointSize');
3.修改attribute 变量
gl.vertexAttrib1f(a_PointSize,100.0);
整体代码:
<canvas id="canvas"></canvas>
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
attribute float a_PointSize;
void main(){
gl_Position = a_Position;
gl_PointSize = a_PointSize;
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
void main() {
gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
</script>
<script type="module">
import {initShaders} from '../jsm/Utils.js';
const canvas = document.getElementById('canvas');
canvas.width=window.innerWidth;
canvas.height=window.innerHeight;
const gl = canvas.getContext('webgl');
const vsSource = document.getElementById('vertexShader').innerText;
const fsSource = document.getElementById('fragmentShader').innerText;
initShaders(gl, vsSource, fsSource);
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
const a_PointSize=gl.getAttribLocation(gl.program,'a_PointSize');
gl.vertexAttrib3f(a_Position,0.0,0.0,0.0);
gl.vertexAttrib1f(a_PointSize,100.0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
</script>
后面我们也可以用鼠标随机改变顶点大小:
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
const a_PointSize=gl.getAttribLocation(gl.program,'a_PointSize');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
const g_points=[];
canvas.addEventListener('click',function(event){
const {clientX,clientY}=event;
const {left,top,width,height}=canvas.getBoundingClientRect();
const [cssX,cssY]=[
clientX-left,
clientY-top
];
const [halfWidth,halfHeight]=[width/2,height/2];
const [xBaseCenter,yBaseCenter]=[cssX-halfWidth,cssY-halfHeight];
const yBaseCenterTop=-yBaseCenter;
const [x,y]=[xBaseCenter/halfWidth,yBaseCenterTop/halfHeight];
g_points.push({x,y,z:Math.random()*50});
gl.clear(gl.COLOR_BUFFER_BIT);
g_points.forEach(({x,y,z})=>{
gl.vertexAttrib2f(a_Position,x,y);
gl.vertexAttrib1f(a_PointSize,z);
gl.drawArrays(gl.POINTS, 0, 1);
})
})
在我们上面的案例中,无论是控制点位的尺寸,还是控制点位的位置,实际上都是对attribute 变量的操控。
那我们如果想要再改变顶点的颜色呢?那就不能再用attribute 限定符了,因为attribute 限定符限定的就是顶点相关的数据。
接下来咱们就说一下如何用js 控制顶点的颜色。
第三章 用js 控制顶点的颜色
首先我们要知道,限定颜色变量的限定符叫uniform。
uniform 翻译过来是一致、统一的意思。
接下来咱们说一下用js 控制顶点颜色的步骤。
1-用js 控制顶点颜色的步骤
1.在片元着色器里把控制顶点颜色的变量暴露出来。
<script id="fragmentShader" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_FragColor;
void main() {
gl_FragColor = u_FragColor;
}
</script>
上面的uniform 就是咱们刚才说过的限定符,vec4 是4维的变量类型,u_FragColor 就是变量名。
这里还要注意一下,第一行的precision mediump float 是对浮点数精度的定义,mediump 是中等精度的意思,这个必须要有,不然画不出东西来。
2.在js 中获取片元着色器暴露出的uniform 变量
const u_FragColor=gl.getUniformLocation(gl.program,'u_FragColor');
上面的getUniformLocation() 方法就是用于获取片元着色器暴露出的uniform 变量的,其第一个参数是程序对象,第二个参数是变量名。这里的参数结构和获取attribute 变量的getAttributeLocation() 方法是一样的。
3.修改uniform 变量
gl.uniform4f(u_FragColor,1.0,1.0,0.0,1.0);
用js 控制顶点的颜色的基本步骤就是这样,其整体思路和控制顶点点位是一样的。
下面咱们一起对比着看一下整体代码。
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
attribute float a_PointSize;
void main(){
gl_Position = a_Position;
gl_PointSize = a_PointSize;
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_FragColor;
void main() {
gl_FragColor = u_FragColor;
}
</script>
<script type="module">
import {initShaders} from '../jsm/Utils.js';
const canvas = document.getElementById('canvas');
canvas.width=window.innerWidth;
canvas.height=window.innerHeight;
const gl = canvas.getContext('webgl');
const vsSource = document.getElementById('vertexShader').innerText;
const fsSource = document.getElementById('fragmentShader').innerText;
initShaders(gl, vsSource, fsSource);
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
const a_PointSize=gl.getAttribLocation(gl.program,'a_PointSize');
const u_FragColor=gl.getUniformLocation(gl.program,'u_FragColor');
gl.vertexAttrib3f(a_Position,0.0,0.0,0.0);
gl.vertexAttrib1f(a_PointSize,100.0);
gl.uniform4f(u_FragColor,1.0,1.0,0.0,1.0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
</script>
知道了这个原理后,我们就可以用鼠标随机改变顶点的颜色。
<script type="module">
import {initShaders} from '../jsm/Utils.js';
const canvas = document.getElementById('canvas');
canvas.width=window.innerWidth;
canvas.height=window.innerHeight;
const gl = canvas.getContext('webgl');
const vsSource = document.getElementById('vertexShader').innerText;
const fsSource = document.getElementById('fragmentShader').innerText;
initShaders(gl, vsSource, fsSource);
const a_Position=gl.getAttribLocation(gl.program,'a_Position');
const a_PointSize=gl.getAttribLocation(gl.program,'a_PointSize');
const u_FragColor=gl.getUniformLocation(gl.program,'u_FragColor');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
const arr=[];
canvas.addEventListener('click',function(event){
const {clientX,clientY}=event;
const {left,top,width,height}=canvas.getBoundingClientRect();
const [cssX,cssY]=[
clientX-left,
clientY-top
];
const [halfWidth,halfHeight]=[width/2,height/2];
const [xBaseCenter,yBaseCenter]=[cssX-halfWidth,cssY-halfHeight];
const yBaseCenterTop=-yBaseCenter;
const [x,y]=[xBaseCenter/halfWidth,yBaseCenterTop/halfHeight];
const color=new Float32Array([
Math.random(),
Math.random(),
Math.random(),
1.0
])
arr.push({x,y,z:Math.random()*50,color});
gl.clear(gl.COLOR_BUFFER_BIT);
arr.forEach(({x,y,z,color})=>{
gl.vertexAttrib2f(a_Position,x,y);
gl.vertexAttrib1f(a_PointSize,z);
gl.uniform4fv(u_FragColor,color);
gl.drawArrays(gl.POINTS, 0, 1);
})
})
</script>
在上面的代码中,我们使用uniform4fv() 修改的顶点颜色,类似的代码结构咱们之前提到过,在这里再给大家详细介绍一下。
2-uniform4fv() 方法
我们在改变uniform 变量的时候,既可以用uniform4f() 方法一个个的写参数,也可以用uniform4fv() 方法传递类型数组。
- uniform4f 中,4 是有4个数据,f 是float 浮点类型,在我们上面的例子里就是r、g、b、a 这四个颜色数据。
- uniform4fv 中,4f 的意思和上面一样,v 是vector 矢量的意思,这在数学里就是向量的意思。由之前的4f 可知,这个向量由4个浮点类型的分量构成。
在上面呢的案例中,我们可以知道,在修改uniform变量的时候,这两种写法是一样的:
gl.uniform4f(u_FragColor,1.0,1.0,0.0,1.0);
//等同于
const color=new Float32Array([1.0,1.0,0.0,1.0]);
gl.uniform4fv(u_FragColor,color);
uniform4f() 和uniform4fv() 也有着自己的同族方法,其中的4 可以变成1|2|3。
uniform4fv() 方法的第二个参数必须是Float32Array 数组,不要使用普通的Array 对象。
Float32Array 是一种32 位的浮点型数组,它在浏览器中的运行效率要比普通的Array 高很多。
案例-用鼠标绘制星空
1-用鼠标绘制圆形的顶点
星星的形状是圆形的,所以,我们需要绘制一个圆形的顶点。
<script id="fragmentShader" type="x-shader/x-fragment">
precision mediump float;
uniform vec4 u_FragColor;
void main() {
float dist = distance(gl_PointCoord, vec2(0.5, 0.5));
if(dist < 0.5) {
gl_FragColor = u_FragColor;
} else {
discard;
}
}
</script>
- distance(p1,p2) 计算两个点位的距离
- gl_PointCoord 片元在一个点中的位置,此位置是被归一化的
- discard 丢弃,即不会一个片元进行渲染
着色器语法参考地址:www.khronos.org/registry/Op…
2-绘制随机透明度的星星
首先我们可以先给canvas 一个星空背景
#canvas {
background: url("./images/sky.jpg");
background-size: cover;
background-position: right bottom;
}
刷底色的时候给一个透明的底色,这样才能看见canvas的css背景
gl.clearColor(0, 0, 0, 0);
接下来图形的透明度作为变量:
const arr = new Float32Array([0.87, 0.91, 1, a]);
gl.uniform4fv(u_FragColor, arr);
开启片元的颜色合成功能
gl.enable(gl.BLEND)
设置片元的合成方式
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
3-制作闪烁的繁星
当星星会眨眼睛,会变得灵动而可爱,接下来我要让星星对你眨眼睛。
3-1-建立补间动画的意识
在这里推荐大家玩一下AE,因为它可以让你对动画的运行原理和架构方式有一个具象的认知。
比如,我在AE里画一颗星星,加几个关键帧,让它眨一下眼睛。
在这里会涉及以下概念:
- 合成:多个时间轨的集合
- 时间轨:通过关键帧,对其中目标对象的状态进行插值计算
- 补间动画:通过两个关键帧,对一个对象在这两个关键帧之间的状态进行插值计算,从而实现这个对象在两个关键帧间的平滑过渡
3-2-架构代码
1.建立合成对象
export default class Compose{
constructor(){
this.parent=null
this.children=[]
}
add(obj){
obj.parent=this
this.children.push(obj)
}
update(t){
this.children.forEach(ele=>{
ele.update(t)
})
}
}
属性
- parent 父对象,合成对象可以相互嵌套
- children 子对象集合,其集合元素可以是时间轨,也可以是合成对象
方法:
- add(obj) 添加子对象方法
- update(t) 基于当前时间更新子对象状态的方法
2.建立时间轨
export default class Track{
constructor(target){
this.target=target
this.parent=null
this.start=0
this.timeLen=5
this.loop=false
this.keyMap=new Map()
}
update(t){
const {keyMap,timeLen,target,loop}=this
let time=t-this.start
if(loop){
time=time%timeLen
}
for(const [key,fms] of keyMap.entries()){
const last=fms.length-1
if(time<fms[0][0]){
target[key]=fms[0][1]
}else if(time>fms[last][0]){
target[key]=fms[last][1]
}else{
target[key]=getValBetweenFms(time,fms,last)
}
}
}
}
属性
- target 时间轨上的目标对象
- parent 父对象,只能是合成对象
- start 起始时间,即时间轨的建立时间
- timeLen 时间轨总时长
- loop 是否循环
- keyMap 关键帧集合,结构如下:
[
[
'对象属性1',
[
[时间1,属性值], //关键帧
[时间2,属性值], //关键帧
]
],
[
'对象属性2',
[
[时间1,属性值], //关键帧
[时间2,属性值], //关键帧
]
],
]
方法
-
update(t) 基于当前时间更新目标对象的状态。
先计算本地时间,即世界时间相对于时间轨起始时间的的时间。
若时间轨循环播放,则本地时间基于时间轨长度取余。
遍历关键帧集合:
- 若本地时间小于第一个关键帧的时间,目标对象的状态等于第一个关键帧的状态
- 若本地时间大于最后一个关键帧的时间,目标对象的状态等于最后一个关键帧的状态
- 否则,计算本地时间在左右两个关键帧之间对应的补间状态
3.获取两个关键帧之间补间状态的方法
function getValBetweenFms(time,fms,last){
for(let i=0;i<last;i++){
const fm1=fms[i]
const fm2=fms[i+1]
if(time>=fm1[0]&&time<=fm2[0]){
const delta={
x:fm2[0]-fm1[0],
y:fm2[1]-fm1[1],
}
const k=delta.y/delta.x
const b=fm1[1]-fm1[0]*k
return k*time+b
}
}
}
-
getValBetweenFms(time,fms,last)
- time 本地时间
- fms 某个属性的关键帧集合
- last 最后一个关键帧的索引位置
其实现思路如下:
- 遍历所有关键帧
- 判断当前时间在哪两个关键帧之间
- 基于这两个关键帧的时间和状态,求点斜式
- 基于点斜式求本地时间对应的状态
3-3-使用合成对象和轨道对象制作补间动画
- 建立动画相关的对象
const compose=new Compose()
const stars=[]
canvas.addEventListener('click',function(event){
const {x,y}=getPosByMouse(event,canvas)
const a=1
const s=Math.random()*5+2
const obj={x,y,s,a}
stars.push(obj)
const track=new Track(obj)
track.start=new Date()
track.keyMap=new Map([
['a',[
[500,a],
[1000,0],
[1500,a],
]]
])
track.timeLen=2000
track.loop=true
compose.add(track)
})
- compose 合成对象的实例化
- stars 存储顶店数据的集合
- track 时间轨道对象的实例化
2.用请求动画帧驱动动画,连续更新数据,渲染视图。
!(function ani(){
compose.update(new Date())
render()
requestAnimationFrame(ani)
})()
渲染方法如下:
function render(){
gl.clear(gl.COLOR_BUFFER_BIT);
stars.forEach(({x,y,s,a})=>{
gl.vertexAttrib2f(a_Position,x,y);
gl.vertexAttrib1f(a_PointSize,s);
gl.uniform4fv(u_FragColor,new Float32Array([0.87,0.92,1,a]));
gl.drawArrays(gl.POINTS, 0, 1);
})
}
3.最后我们还可以配点应景的音乐,比如虫儿飞
#audio{
position: absolute;
right: 20px;
bottom: 20px;
opacity: 10%;
transition: opacity 200ms;
z-index: 20;
}
#audio:hover{
opacity: 90%;
}
<audio id="audio" controls loop autoplay>
<source src="./audio/cef.mp3" type="audio/mpeg">
</audio>
总结
这一章我们学习了webgl 基础,原理以及js 与着色器间的数据传输,从而去动态控制顶点的位置、大小和颜色,这是webgl 绘图的基础。在接下来的篇章里咱们会说更复杂的图形绘制。