Android OpenGLES3绘图:LiquidFun流体库
在看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优化
调整粒子的数量和大小能得到更精细的展示效果,上图是8000个粒子,在我的骁龙865手机上运行非常流畅,这得益于LiquidFun的计算优化。
在LiquidFun的源码里,我发现了b2ParticleAssembly.neon.s这个文件,它里面居然是汇编语言!查阅了一下,这正是ARM处理器的Neon优化,它可以让ARM处理器并行处理数据,能大幅提高CPU计算速度。项目编译时可以选择是否开启Neon,我打了一个不开启Neon的so跟正常开启的对比,发现开启Neon优化的计算速度能提升4倍左右!
4 Github地址
完整项目在SurfacePaint项目下的liquidapp
模块里。