引入
之前使用webgl实现的波浪效果,只要去除顶点之间连线,渲染顶点圆,其实也可以算是粒子动画。但是那样的方法对于复杂粒子系统动画就有点捉襟见肘了。对于波浪,我们使用不同频率正弦波叠加,传入uniform变量time给gpu。每个顶点都可以简单地计算出每时每刻的位置后,进行顶点变换再渲染。如果系统中粒子要实时响应外部因素变化(如受到速度属性,外部力属性,质量属性),粒子存在相互作用(碰撞,引力)和模型复杂的时候,我们更希望根据上次粒子的位置,速度属性,递推计算粒子位置变换。这时候就可以使用webgl2的transform feedback功能,将前一帧的位置计算结果写入缓冲区供下一帧计算使用。
关于webgl2
webgl2包含了许多webgl没有的新特性,支持glsl 3.0 es。
webgl2的API获取和glsl编写与webgl主要有以下区别:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
//webgl2上下文接口获取
var canvas = document.getElementById("glcanvas");
const gl2 = canvas.getContext("webgl2");
//使用glsl 3.0 es
//in相当于attribute变量
//out相当于varying变量,可以传递值个fragmentShader
const vertexShaderSource = `#version 300 es
in vec4 aPosition;
out vec4 pos;
uniform float deltaTime;
void main()
{
gl_Position = aPosition;
}
`
//不再使用设置gl_FragColor变量作为输出颜色。
//自己设置out变量作为输出。
const fragmentShaderSource = `#version 300 es
out vec4 outColor;
void main()
{
outColor = vec4(1.);
}
`
|
webgl2也可以向下兼容webgl,你依旧可以使用webgl的写法。
webgl2有许多新的特性可以使用,这里主要变换反馈对象transformFeedback。
着色器编写
我们来创建一个简单的粒子动画,收到指向固定点恒力的粒子系统。
我们需要两个gpu程序(即program),一个用于更新粒子的顶点位置,后写入缓冲区,一个用于渲染,读取顶点位置并渲染。
由于更新位置的program不需要渲染出来,我们可以在js中停止片段着色器的调用。链接的时候,片段着色器可以链接渲染程序的片段着色器,满足编译要求即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
//读取old位置,速度属性,计算新的位置和速度,赋值给out变量,之后写入缓冲区。
const updateVertexShaderSource = `#version 300 es
in vec3 oldPosition;
in vec3 oldVelocity;
in float mass;
uniform vec3 gPoint;
uniform float gForce;
uniform float deltaTime;
out vec3 newPosition;
out vec3 newVelocity;
void main()
{
float dt = deltaTime / 1000.;
newPosition = oldPosition + oldVelocity * dt;
vec3 a = normalize(gPoint - newPosition) * gForce / mass;
newVelocity = oldVelocity + a * dt;
if(length(newPosition - gPoint) <= 0.1)
{
newVelocity = oldVelocity - 5. * a * dt;
}
}
`
//渲染点的俩个着色器。
const renderVertexShaderSource = `#version 300 es
in vec4 a_position;
uniform mat4 mvp;
void main()
{
gl_Position = mvp * a_position;
gl_PointSize = 5.;
}
`
const renderFragmentShaderSource = `#version 300 es
precision highp float;
out vec4 outColor;
void main()
{
vec2 coord = gl_PointCoord - vec2(0.5);
float dist = length(coord);
if(dist > 0.43)
{
discard;
}
outColor = vec4(1.0, 0.7, 0.7, 1.0);
outColor.a *= 1.0 - smoothstep(0.4, 0.5, dist);
}
`
|
要设置transformFeedback,需要在使用shaderSource创建好shader,创建program并把shader附加到program后,通过transformFeedbackVaryings函数告诉gpu,哪些out变量要写入缓冲区。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
const updateVertexShader = WebglUtil.createShader(gl2, gl2.VERTEX_SHADER, updateVertexShaderSource);
const renderVertexShader = WebglUtil.createShader(gl2, gl2.VERTEX_SHADER, renderVertexShaderSource);
const renderFragmentShader = WebglUtil.createShader(gl2, gl2.FRAGMENT_SHADER, renderFragmentShaderSource);
const updateProgram = gl2.createProgram();
gl2.attachShader(updateProgram, updateVertexShader);
gl2.attachShader(updateProgram, renderFragmentShader);
gl2.transformFeedbackVaryings(
updateProgram,//指定program
['newPosition', 'newVelocity'],//指定变量
gl2.SEPARATE_ATTRIBS//变量将被写入不同缓冲区
);
gl2.linkProgram(updateProgram);
if(!gl2.getProgramParameter(updateProgram, gl2.LINK_STATUS))
{
throw new Error(gl.getProgramParameter(updateProgram));
}
|
接下来设置要写入的缓冲区。我们使用双缓冲区,以更新位置属性为例,一个存储旧位置用于读取,计算后写入另一个缓冲区。计算好新位置和渲染了旧位置后,交换用于读写缓冲区的指针指向,就可以循环计算每一帧的位置了。可以编写以下函数返回一个匿名类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
function createDoubleBuffer()
{
return {
write : gl2.createBuffer(),
read : gl2.createBuffer(),
swap : function(){
const temp = this.write;
this.write = this.read;
this.read = temp;
}
}
}
var positionDoubleBuffer = createDoubleBuffer();
gl2.bindBuffer(gl2.ARRAY_BUFFER, positionDoubleBuffer.write);
gl2.bufferData(gl2.ARRAY_BUFFER, 120000, gl2.DYNAMIC_DRAW);//设置缓冲区大小
gl2.bindBuffer(gl2.ARRAY_BUFFER, null);//解除引用。
|
创建完双缓冲区后,进行以下操作:
1
2
3
4
|
const tf = gl2.createTransformFeedback();
gl2.bindTransformFeedback(gl2.TRANSFORM_FEEDBACK, tf);
gl2.bindBufferBase(gl2.TRANSFORM_FEEDBACK_BUFFER, 0, positionDoubleBuffer.write);
gl2.bindBufferBase(gl2.TRANSFORM_FEEDBACK_BUFFER, 1, velocityDoubleBuffer.write);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
function draw(currentTime)//渲染循环函数
{
//初始化
gl2.viewport(0, 0, canvas.width, canvas.height);
gl2.clearColor(0., 0., 0., 1.0);
gl2.clearDepth(1.0);
gl2.enable(gl2.DEPTH_TEST);
gl2.clear(gl2.COLOR_BUFFER_BIT | gl2.DEPTH_BUFFER_BIT);
//先绘制旧位置
gl2.useProgram(renderProgram);
//设置相关属性和uniform。
//...
gl2.drawArrays(gl2.POINTS, 0, 10000);
//再更新属性
gl2.useProgram(updateProgram);
gl2.enable(gl2.RASTERIZER_DISCARD);//不调用片段着色器。
//设置相关属性和uniform。
//...
gl2.bindTransformFeedback(gl2.TRANSFORM_FEEDBACK, tf);
gl2.beginTransformFeedback(gl2.POINTS);
gl2.drawArrays(gl2.POINTS, 0, 10000);
gl2.endTransformFeedback();
gl2.bindTransformFeedback(gl2.TRANSFORM_FEEDBACK, null);
gl2.disable(gl2.RASTERIZER_DISCARD);//允许调用片段着色器。
//交换读写缓冲区。
positionDoubleBuffer.swap();
velocityDoubleBuffer.swap();
//重新设置相关属性的缓冲区引用。
//...
}
|
效果(cameraZ为0有bug)