Android OpenGLES3绘图:LiquidFun流体库

kepz0-oim7h.gif

在看Box2D游戏引擎时,发现了一个很有意思的液体引擎 LiquidFun ,它是Box2D的扩展。给Box2D加上了粒子系统,并且粒子能实现液体的特性。

LiquidFun Github是一个Google的开源项目,LiquidFun官网里面有很多有趣的演示和很多资料,包括它的原理PPT讲解:它其实是在Box2D物理引擎的力学计算(弹力、摩擦力)基础上,给Body(液体粒子)添加了跟液体有关的几种力:压力、粘性力、排斥力、表面张力等,能够很好地模拟液体。

由于增加了很多力学计算,并且模拟液体需要大量粒子,这会给CPU带来巨大的压力。对此,LiquidFun 里面使用了一项非常强大的优化技术!

我们来学习和使用一下这个库!

1 编译

LiquidFun项目跟Box2D一样是用C语言写的,然而Box2D有Java版本的JBox2D它没有,如果都自己写JNI接口去调用会很麻烦,好在它提供了Swig,Swig能给C代码自动生成Java接口。咱们可以编译生成一下,也可以使用它生成好的。

至于动态链接库 libliquidfun.so,我用自己编译的遇到了崩溃的问题,不知道是参数还是哪里没配置好。好在Google有一个上架的App:LiquidFun Paint 使用了这个库,我直接从它的apk包提取了 .so 文件使用。LiquidFun Paint 也是开源的,在Github和Google Play商店都能搜到,它是一个功能非常完整的液体绘图app,源码很值得学习。

2 代码实现

2.1 创建工程和导入

新建一个Android工程,添加 libliquidfun.so 和 libliquidfun_jni.so 这两个文件,在 MainActivity中 loadLibrary,并将swig生成的包名为com.google.liquidfun的包和里面的所有Java文件添加到代码中。

    static {
        System.loadLibrary("liquidfun");
        System.loadLibrary("liquidfun_jni");
    }

2.2 添加物理类

由于LiquidFun里面包含了Box2D,就不用再添加后者的库了,直接使用即可。创建一个LiquidManager类管理物理相关的,创建Box2D的World世界边界和ParticleSystem粒子系统。这里主要介绍ParticleSystem,如果不熟悉Box2D可以去搜索相关资料。

下面是创建ParticleSystem的方法,它跟创建Box2D里面的其他物体相似,其中setMaxParticleCount和setRadius这两个方法比较重要,关系到创建粒子的数量和大小。

PolygonShape会在提供的形状区域内创建所有液体粒子,注意如果给它提供的范围小而粒子数很多,会直接崩溃!

ParticleGroupDef的setFlags和setGroupFlags可以改变粒子的行为,比如可以不创建液体,创建一个软体!

创建完成之后通过particleSystem可以获取每个粒子的参数,包括位置、颜色等,当然一个个粒子获取和刷新界面效率太低了,它提供了 updatePosition(ByteBuffer buffer) 方法将所有粒子位置刷新到一个 ByteBuffer中,后面可以直接提供给OpenGL使用。

    private void createLiquid(float w, float h) {
        ParticleSystemDef psd = new ParticleSystemDef();
        psd.setDensity(1.2f);
        psd.setGravityScale(0.4f);
        psd.setRadius(PARTICAL_RADIUS);
        psd.setRepulsiveStrength(0.5f);
        particleSystem = world.createParticleSystem(psd);
        particleSystem.setMaxParticleCount(MAX_COUNT);

        PolygonShape shape = new PolygonShape();
        shape.setAsBox(w / 2, h / 2, 0f, 0f, 0f);

        ParticleGroupDef pd = new ParticleGroupDef();

//        // 软体
//        pd.setFlags(1 << 4);
//        pd.setGroupFlags(1 << 0);
        pd.setFlags(0);
        pd.setGroupFlags(0);
        pd.setLinearVelocity(new Vec2(0,0));
        pd.setShape(shape);

        // signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x25
        // Cause: null pointer dereference
        particleSystem.createParticleGroup(pd);

        psd.delete();
        shape.delete();
        pd.delete();

        Log.d("chao", "create particles " + particleSystem.getParticleCount());
    }

2.3 OpenGL绘制

添加一个LiquidRender类进行OpenGL绘制。液体粒子没有形状的属性,直接使用点方式绘制。创建一个ByteBuffer保存所有点的位置,前面说的particleSystem可以直接将数据刷新到这个mParticlePositionBuffer里面。

    ByteBuffer mParticlePositionBuffer;

//        //分配内存空间,每个浮点型占4字节空间
        mParticlePositionBuffer = ByteBuffer
                .allocateDirect(2 * 4 * LiquidManager.MAX_COUNT)
                .order(ByteOrder.nativeOrder());

        vao = new int[1];
        glGenVertexArrays(1, vao, 0);
        glBindVertexArray(vao[0]);

        vbo = new int[1];
        glGenBuffers(1, vbo, 0);
        glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
//        glBufferData(GL_ARRAY_BUFFER, vertices.length * 4, vertexBuffer, GL_STATIC_DRAW);
        glBufferData(GL_ARRAY_BUFFER, LiquidManager.MAX_COUNT * 4 * 2, mParticlePositionBuffer, GL_STREAM_DRAW);

刷新方法如下:
用 world.step() 更新位置,然后updatePosition,再用glBufferSubData方法刷新vbo数据,注意要重新绑定vbo一下,否则不会生效。最后绘制即可。

    @Override
    public void onDrawFrame(GL10 gl) {
        liquidManager.initWorld(width, height);
        liquidManager.updatePosition(mParticlePositionBuffer);

        // Clear the color buffer
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // 刷新vbo数据
        glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, LiquidManager.MAX_COUNT * 4 * 2,  mParticlePositionBuffer);
        glBindBuffer(GL_ARRAY_BUFFER, 0);


        // Use the program object
        glUseProgram(program);
        glBindVertexArray(vao[0]);

        glDrawArrays(GL_POINTS, 0, LiquidManager.MAX_COUNT);
    }

创建vbo的方法glBufferData(GL_ARRAY_BUFFER, LiquidManager.MAX_COUNT * 4 * 2, mParticlePositionBuffer, GL_STREAM_DRAW)最后一个参数,理论上在每一帧数据都变化时应该使用GL_STREAM_DRAW,也许它能自动刷新GPU里的缓存数据?但我这里设置了并没有生效,跟GL_STATIC_DRAW效果一样。我只能用glBufferSubData去手动刷新。

2.4 Shader 实现

着色器里面根据传入的位置绘制点即可

顶点着色器:shader_base_v.glsl
里面根据实际调整点的大小 gl_PointSize

#version 300 es
layout (location = 0) in vec2 vPosition;

out vec2 vPos;

void main() {
     vPos = vPosition / 10.0f;
     gl_PointSize = 12.0f;
     gl_Position  = vec4(vPos, 0.0f, 1.0f);
}

片段着色器:shader_base_v.glsl
根据位置做了一个简单的颜色渐变

#version 300 es
precision mediump float;

in vec2 vPos;

out vec4 fragColor;

void main() {
     fragColor = vec4((vPos + 1.0f) * 0.5f, 0.5f, 1.0f);
}

如果想要更好的液体展示效果,就不能用简单的点绘制,应该使用Texture纹理贴图和blend混合,这在LiquidFun Paint项目里可以看到。

3 ARM处理器的Neon优化

ub06f-33jm1.gif

调整粒子的数量和大小能得到更精细的展示效果,上图是8000个粒子,在我的骁龙865手机上运行非常流畅,这得益于LiquidFun的计算优化。

在LiquidFun的源码里,我发现了b2ParticleAssembly.neon.s这个文件,它里面居然是汇编语言!查阅了一下,这正是ARM处理器的Neon优化,它可以让ARM处理器并行处理数据,能大幅提高CPU计算速度。项目编译时可以选择是否开启Neon,我打了一个不开启Neon的so跟正常开启的对比,发现开启Neon优化的计算速度能提升4倍左右!

捕获.PNG

4 Github地址

完整项目在SurfacePaint项目下的liquidapp模块里。

posted @ 2022-07-18 18:06  rome753  阅读(309)  评论(0编辑  收藏  举报