Featured image of post webgl实现简单粒子动画

webgl实现简单粒子动画

使用webgl2实现简单粒子动画。

引入

之前使用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

设置transformFeedbackVarying

要设置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));
}

设置transformFeedback buffer

接下来设置要写入的缓冲区。我们使用双缓冲区,以更新位置属性为例,一个存储旧位置用于读取,计算后写入另一个缓冲区。计算好新位置和渲染了旧位置后,交换用于读写缓冲区的指针指向,就可以循环计算每一帧的位置了。可以编写以下函数返回一个匿名类。

 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);

在渲染循环中启用transformFeedback

 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)

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计