动画概览
动画的基本原理是连续改变绘制内容,利用人眼的视觉暂留效应,产生画面在动的感觉。每次展示的绘制内容一般称之为帧(Frame),每秒帧的数目越多,人眼会感觉越流畅。一般而言动画能达到每秒30帧就足够了,为了追求更加流畅细腻的动画效果,有时候会将每秒帧数升到60。越高的帧数意味着留给每帧绘制的时间越短,所以为了让动画能够达到足够高的帧数,就必须优化好每一次绘制所消耗的时间。
WebGL动画
为了让WebGL绘制的内容动起来,可以通过以下两种办法
1. 直接修改顶点数据
例子完整项目代码,在
chapter2
,section1-0
目录下
在前面介绍上传顶点数据到GPU时,使用了如下API
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
我们可以每一帧都向GPU传递不同的顶点数据,不过需要将gl.STATIC_DRAW
改为gl.DYNAMIC_DRAW
,这样可以让系统更好的安排顶点数据所在的内存位置。利用这一点在绘制三角形的例子基础上做一些变动,让三角形绕中心旋转。
核心思路就是每次绘制之前改变顶点数据,然后使用gl.bufferData
更新GPU上的数据,绘制代码目前都在render
方法里,需要增加如下代码
首先计算render
调用时从App启动开始经过的时间elapsedTime
,它的单位是ms
let now = (new Date()).getTime();
let delta = now - lastRenderTime;
lastRenderTime = now;
elapsedTime += delta;
根据经过的时间旋转三角形的点,这里利用了二维点的旋转计算公式
function rotatePoint(x: number, y: number, degree: number) {
let rad = degree / 180 * Math.PI;
let newX = x * Math.cos(rad) - y * Math.sin(rad);
let newY = x * Math.sin(rad) + y * Math.cos(rad);
return {
x: newX,
y: newY
};
}
计算出每一帧对应的三个点坐标,使用bufferData
上传到GPU,然后继续之前的绘制流程
// update vertex data
let vertices = [
-0.5, -0.5, 0, // 左下角
0.5, -0.5, 0, // 右下角
0, 0.5, 0, // 中上
];
let rotateDegree = elapsedTime / 1000 * 30; // 每秒30度
let newPoint1 = rotatePoint(vertices[0], vertices[1], rotateDegree);
vertices[0] = newPoint1.x;
vertices[1] = newPoint1.y;
let newPoint2 = rotatePoint(vertices[3], vertices[4], rotateDegree);
vertices[3] = newPoint2.x;
vertices[4] = newPoint2.y;
let newPoint3 = rotatePoint(vertices[6], vertices[7], rotateDegree);
vertices[6] = newPoint3.x;
vertices[7] = newPoint3.y;
gl.bindBuffer(gl.ARRAY_BUFFER, vertexData);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.DYNAMIC_DRAW);
....
// 之前的绘制流程
整体方案很直接,计算改变顶点数据
-> 上传顶点数据
-> GPU绘制
,但是它有个缺点,每帧都要将顶点数据全量传递给GPU,这会导致CPU到GPU的IO消耗直接和顶点数目成正比,是一种不太优化的做法。
2. 使用Vertex Shader
例子完整项目代码,在
chapter2
,section1-1
目录下
在三角形旋转的例子中,核心操作是根据经过的时间对每个顶点坐标进行重新计算,Vertex Shader
的基本原理就是对每个顶点进行重新计算,所以我们可以通过它来进行动画。在Vertex Shader
中我们已经可以拿到每个顶点的坐标了,现在我们需要拿到程序运行的总时间,这就用到了一个新的概念uniform
。uniform
相当与全局变量,所有的Vertex Shader
程序实例都会共享这个变量值,我们可以设置一个程序运行总时间的uniform
,让所有运行的Vertex Shader
都能够拿到这个值。对之前的Vertex Shader
做一些改动
attribute vec4 position;
uniform float elapsedTime;
void main() {
float rad = radians(elapsedTime / 1000.0 * 30.0);
float x = position.x * cos(rad) - position.y * sin(rad);
float y = position.x * sin(rad) + position.y * cos(rad);
gl_Position = vec4(x, y, position.z, position.w);
}
uniform float elapsedTime;
表示申明一个名称为elapsedTime
的uniform
变量,类型是float
。然后在Vertex Shader
的主程序中使用坐标旋转的算法对传入的位置进行计算
float rad = radians(elapsedTime / 1000.0 * 30.0);
float x = position.x * cos(rad) - position.y * sin(rad);
float y = position.x * sin(rad) + position.y * cos(rad);
最后将新的位置作为输出
gl_Position = vec4(x, y, position.z, position.w);
上面的Shader代码使用了一些数据类型和计算函数,下面是Shader中支持的数据类型
类型 | 说明 |
---|---|
void | 无类型 |
bool | 布尔类型 |
int | 有符号整数 |
float | 浮点数 |
vec2, vec3, vec4 | 包含2,3,4 个浮点数的向量,可以使用{x,y,z,w}或者{r,g,b,a}或者{s,t,p,q}访问数据,也可以使用xy,xyz这种组合来访问数据 |
bvec2, bvec3, bvec4 | 包含2,3,4 个布尔值的向量 |
ivec2, ivec3, ivec4 | 包含2,3,4 个整型值的向量 |
mat2, mat3, mat4 | 2x2, 3x3, 4x4 的矩阵,可以通过下标索引访问 |
sampler2D | 2D纹理类型,可以通过它访问纹理的像素 |
samplerCube | 3D纹理类型,可以通过它访问纹理的像素 |
除了sin
正弦,cos
余弦,radians
角度转弧度,这些内置函数外,还有以下内置函数可以使用
T
表示可以是float
,vec2
,vec3
,vec4
任意一种类型
函数名 | 说明 |
---|---|
T radians(T degrees) | 角度转弧度 |
T degrees(T radians) | 弧度转角度 |
T sin(T angle) | 正弦 |
T cos(T angle) | 余弦 |
T tan(T angle) | 正切 |
T asin(T x) | 反正弦 |
T acos(T x) | 反余弦 |
T atan(T y, T x) | 反正切 |
T atan(T y/x) | 反正切 |
T pow(T x, T y) | x的y次方 |
T exp(T x) | e的x次方 |
T log(T x) | x以10为底的对数 |
T exp2(T x) | 2的x次方 |
T log2(T x) | x以2为底的对数 |
T sqrt(T x) | 平方根 |
T inversesqrt(T x) | 求解1/sqrt(x) |
T abs(T x) | 求绝对值 |
T sign(T x) | 获取数字的符号,返回 -1.0, 0.0, 或者 1.0 |
T floor(T x) | 小于等于x的最大整数 |
T ceil(T x) | 大于等于x的最小整数 |
T fract(T x) | x - floor(x),相当于x的小数部分 |
T mod(T x, T y) | x对y求模 |
T mod(T x, float y) | x对y求模 |
T min(T x, T y) | 最小值 |
T min(T x, float y) | 最小值 |
T max(T x, T y) | 最大值 |
T max(T x, float y) | 最大值 |
T clamp(T x, T minVal, T maxVal) | x如果小于minVal,返回minVal,如果大于maxVal,返回maxVal |
T clamp(T x, float minVal, float maxVal) | x如果小于minVal,返回minVal,如果大于maxVal,返回maxVal |
T mix(T x, T y, T a) | 按照a混合x和y,相当于 a * x + (1 - a) * y |
T mix(T x, T y, float a) | 按照a混合x和y,相当于 a * x + (1 - a) * y |
T step(T edge, T x) | x小于edge,返回0,大于edge,返回1 |
T step(float edge, T x) | x小于edge,返回0,大于edge,返回1 |
T smoothstep(T edge0, T edge1, T x) | 将返回值限定在edge0和edge1之间,并对返回的值进行平滑 |
T smoothstep(float edge0, float edge1, T x) | 将返回值限定在edge0和edge1之间,并对返回的值进行平滑 |
float length(T x) | 向量的长度 |
float distance(T p0, T p1) | 两个点的距离 |
float dot(T x, T y) | 向量的点乘 |
vec3 cross(vec3 x, vec3 y) | 向量的叉乘 |
T normalize(T x) | 向量长度规范化为1 |
T faceforward(T N, T I, T Nref) | 如何 dot(Nref, I) < 0(表示Nref和I夹角大于90度) 返回N, 否则返回 -N |
T reflect(T I, T N) | 计算向量I以N为法线的反射向量,公式是 I - 2 * dot(N,I) * N |
T refract(T I, T N, float eta) | 计算向量I以N为法线的折射向量,eta表示折射系数 |
接着在render
方法中使用WebGL的API对uniform
进行赋值,需要先获取uniform elapsedTime
的位置,然后使用这个位置赋值
let elapsedTimeLoc = gl.getUniformLocation(program, "elapsedTime");
gl.uniform1f(elapsedTimeLoc, elapsedTime);
WebGL给uniform
赋值的方法都是uniform{参数个数}{参数类型}
的形式,参数个数一般从1到4,参数类型可以是f(float)
,i(int)
等等,下面列举了一些uniform方法和Shader类型的对应关系
uniform赋值API | 参数类型 | Shader声明 | 例子 |
---|---|---|---|
uniform1f | 1个float | uniform float val; | gl.uniform1f(uniformLoc, 1.0); |
uniform1fv | float数组 | uniform float val[3]; | gl.uniform1fv(uniformLoc, Float32List_of_data); |
uniform2f | 2个float | uniform vec2 val; | gl.uniform2f(uniformLoc, 1.0, 2.0); |
uniform2fv | vec2数组 | uniform vec2 val[3]; | gl.uniform2f(uniformLoc, Float32List_of_data); //需要提供2x3个Float数据 |
uniform3f | 3个float | uniform vec3 val; | gl.uniform3f(uniformLoc, 1.0, 2.0, 3.0); |
uniform4f | 4个float | uniform vec4 val; | gl.uniform4f(uniformLoc, 1.0, 2.0, 3.0, 4.0); |
uniform1i | 1个int | uniform int val; | gl.uniform1i(uniformLoc, 1); |
uniform2i | 2个int | uniform ivec2 val; | gl.uniform2i(uniformLoc, 1, 2); |
uniform3i | 3个int | uniform ivec3 val; | gl.uniform3i(uniformLoc, 1, 2, 3); |
uniform4i | 4个int | uniform ivec4 val; | gl.uniform4i(uniformLoc, 1, 2, 4, 4); |
uniformMatrix2fv | 2x2个float | uniform mat2 val; | gl.uniformMatrix2fv(uniformLoc, false, Float32List_of_mat2); |
uniformMatrix3fv | 3x3个float | uniform mat3 val; | gl.uniformMatrix3fv(uniformLoc, false, Float32List_of_mat3); |
uniformMatrix2fv | 4x4个float | uniform mat4 val; | gl.uniformMatrix2fv(uniformLoc, false, Float32List_of_mat4); |
uniform{1-4}{f/i}
系列方法都有加v
的变体,可以传递对应类型的数组,表格中就不一一列出了。
细谈顶点数据
例子完整项目代码,在
chapter2
,section1-2
目录下
下面详细的说一下顶点数据的相关知识,顶点数据不管多复杂,在JS代码中始终都是一个Float32List
,之前的例子中只是使用了顶点的位置属性,现在给每个顶点增加一个颜色属性,看看应该如何去做。
假设使用rgb三个数据表示颜色,加上之前的位置数据,现在表示一个顶点需要6个float,一共三个顶点,所以需要 3 x 6 = 18
个float,顶点数据就变成了下面这样
let vertices = [
-0.5, -0.5, 0, 1, 0, 0, // 红色
0.5, -0.5, 0, 0, 1, 0, // 绿色
0, 0.5, 0, 0, 0, 1 // 蓝色
];
上传顶点数据给GPU还是之前的步骤
vertexData = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexData);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
Vertex Shader
需要做一些修改来接受颜色这个顶点数据
attribute vec4 position;
attribute vec3 color;
varying vec3 frag_color;
uniform float elapsedTime;
void main() {
float rad = radians(elapsedTime / 1000.0 * 30.0);
float x = position.x * cos(rad) - position.y * sin(rad);
float y = position.x * sin(rad) + position.y * cos(rad);
frag_color = color;
gl_Position = vec4(x, y, position.z, position.w);
}
主要增加了attribute vec3 color;
,attribute
表示这个属性的值来自顶点数据
为了在Fragment Shader中使用顶点的颜色数据,使用了
varying
属性来传递值,这个在介绍Fragment Shader
时再细说
在render
方法中,使用WebGL的API告知Shader取哪些数据给color
赋值
gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexData);
let positionLoc = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 4 * 6, 0);
let colorLoc = gl.getAttribLocation(program, 'color');
gl.enableVertexAttribArray(colorLoc);
gl.vertexAttribPointer(colorLoc, 3, gl.FLOAT, false, 4 * 6, 4 * 3);
这里再说一下vertexAttribPointer
这个方法,前面三个参数很好理解,第一个表示program上属性的位置,第二第三表示属性包含3个Float类型数据,第四个参数如果是true,并且传递的是整型数据,那么在Shader中使用时,会被规范化成-1到1,或者0到1,这里使用的是float,设置为false即可,第五个参数你可以理解为一个完整的顶点数据占多少字节,一个float占4个字节,所以这里填写的是4 * 6
,第六个参数表示属性的数据从一个完整顶点数据的第几个字节开始,现在一个顶点包含位置和颜色6个float数据,颜色数据从第四个float数据开始,所以要从一个完整顶点数据中取得颜色数据,要跳过3个float,也就是跳过4 * 3
个字节取数据。
总结
这一节主要介绍了Vertex Shader
如何使用uniform
制作动画以及如何传递多个顶点属性,这两种是Vertex Shader
和JS程序通讯的主要方式。想要了解更多的Vertex Shader API,可以访问这个页面WebGL API参考,这里列举了WebGL的所有API和Shader的所有API。
练习
可以通过下面的练习巩固对这一节知识的掌握
- 使用Vertex Shader实现三角形的缩放动画,三角形不停的放大和缩小
- 调整顶点数据结构为{r, g, b, x, y},在
section1-2
的例子基础上修改代码,达到之前一样的显示效果, 调整后顶点数据如下
let vertices = [
1, 0, 0, -0.5, -0.5,// 左下角
0, 1, 0, 0.5, -0.5,// 右下角
0, 0, 1, 0, 0.5,// 中上
];