OpenGl-ES2.0 For Android 读书笔记(五)

一、开始

在之前的4篇文章里面,我们完成了一个桌上冰球的游戏,接下来我们要做一个喷射的烟花系统,我们要做出来的效果是这个样子的:


效果图.gif

我们分两部分来完成,首先我们先做出喷射的效果,然后我们再去着重优化喷射的每个粒子的绘制。

二、喷射效果的实现

要完成喷射效果,我们的整体思路是这个样子的:

  1. 先实现不断往上移动的粒子系统
  2. 然后我们让往上的粒子随机的有一些角度
  3. 我们给这些粒子的移动向量加上一个重力加速度的衰减变量,让粒子的速度逐渐减慢最终像反方向移动

那现在就让我们一步一步去实现:

1.先实现不断往上移动的粒子系统

我们可以使用之前代码的一些工具类,所以Copy之前的代码,然后新建一个ParticlesRenderer.java类实现Renderer.java接口,在MainActivity.java中移除监听事件,然后把这个Renderer设置给GLView。
我们思考一下,一个不断往上移动的粒子系统要如何去实现,要知道一个粒子如何移动,我们需要知道他的原始位置,粒子创建的时间,移动的方向向量,这样我们就可以根据当前时间去推算出一个粒子当前应该在的位置,现在我们就用代码去实现,新建一个particle_vertex_shader.glsl文件,实现如下代码:

uniform mat4 u_Matrix;
uniform float u_Time;
attribute vec3 a_Position;
attribute vec3 a_Color;
attribute vec3 a_DirectionVector;
attribute float a_ParticleStartTime;
varying vec3 v_Color;
varying float v_ElapsedTime;
void main() {
    v_Color = a_Color;
    v_ElapsedTime = u_Time - a_ParticleStartTime;
    vec3 currentPosition = a_Position + (v_ElapsedTime * a_DirectionVector);
    gl_Position = u_Matrix * vec4(currentPosition , 1.0);
    gl_PointSize = 10.0;
}

然后我们新建一个particle_fragment_shader.glsl文件实现如下代码:

precision mediump float;

varying vec3 v_Color;
varying float v_ElapsedTime;

void main() {
    gl_FragColor = vec4(v_Color / v_ElapsedTime , 1.0);
}

v_Color / v_ElapsedTime是为了让粒子随着时间的流逝逐渐变的黯淡,现在Shader程序完成了,我们需要在Java里面封装一下,我们先在ShaderProgram.java中添加如下声明:

protected static final String U_TIME = "u_Time";
protected static final String A_DIRECTION_VECTOR = "a_DirectionVector";
protected static final String A_PARTICLE_START_TIME = "a_ParticleStartTime";

然后新建一个ParticleShaderProgram.java类,实现如下代码:

public class ParticleShaderProgram extends ShaderProgram{

    private int mUMatrixLocation;
    private int mUTimeLocation;
    private int mAPositionLocation;
    private int mAColorLocation;
    private int mADirectionVectorLocation;
    private int mAParticleStartTimeLocation;

    public ParticleShaderProgram(Context context) {
        super(context, R.raw.particle_vertex_shader, R.raw.particle_fragment_shader);

        mUMatrixLocation = glGetUniformLocation(mProgram , U_MATRIX);
        mUTimeLocation = glGetUniformLocation(mProgram , U_TIME);

        mAPositionLocation = glGetAttribLocation(mProgram , A_POSITION);
        mAColorLocation = glGetAttribLocation(mProgram , A_COLOR);
        mADirectionVectorLocation = glGetAttribLocation(mProgram , A_DIRECTION_VECTOR);
        mAParticleStartTimeLocation = glGetAttribLocation(mProgram , A_PARTICLE_START_TIME);
    }

    public void setUniforms(float[] matrix , float elapsedTime){
        glUniformMatrix4fv(mUMatrixLocation , 1 , false , matrix , 0);
        glUniform1f(mUTimeLocation , elapsedTime);
    }

    public int getAPositionLocation(){
        return mAPositionLocation;
    }

    public int getAColorLocation(){
        return mAColorLocation;
    }

    public int getADirectionVectorLocation(){
        return mADirectionVectorLocation;
    }

    public int getAParticleStartTimeLocation(){
        return mAParticleStartTimeLocation;
    }
}

然后我们在objects包下新建ParticleSystem.java类,去实现我们的粒子系统:

public class ParticleSystem {

    private static final int POSITION_COMPONENT_COUNT = 3;
    private static final int COLOR_COMPONENT_COUNT = 3;
    private static final int VECTOR_COMPONENT_COUNT = 3;
    private static final int PARTICLE_START_TIME_COMPONENT_COUNT = 1;
    private static final int TOTAL_COMPONENT_COUNT = POSITION_COMPONENT_COUNT
            + COLOR_COMPONENT_COUNT
            + VECTOR_COMPONENT_COUNT
            + PARTICLE_START_TIME_COMPONENT_COUNT;

    private static final int STRIDE = TOTAL_COMPONENT_COUNT * Constants.BYTE_PRE_FLOAT;

    private final float[] mParticles;
    private final VertexArray mVertexArray;
    private final int mMaxParticleCount;

    private int mCurrentParticleCount;
    private int mNextParticle;

    public ParticleSystem(int maxParticleCount){
        mParticles = new float[maxParticleCount * TOTAL_COMPONENT_COUNT];
        mVertexArray = new VertexArray(mParticles);
        mMaxParticleCount = maxParticleCount;
    }

    public void addParticle(Geometry.Point position , int color , Geometry.Vector directionVection ,
                            float startTime){
        final int particleOffset = mNextParticle * TOTAL_COMPONENT_COUNT;

        int currentOffset = particleOffset;
        mNextParticle++;

        if (mCurrentParticleCount < mMaxParticleCount){
            mCurrentParticleCount++;
        }

        if (mNextParticle == mMaxParticleCount){
            mNextParticle = 0;
        }

        mParticles[currentOffset++] = position.x;
        mParticles[currentOffset++] = position.y;
        mParticles[currentOffset++] = position.z;
        mParticles[currentOffset++] = Color.red(color) / 255f;
        mParticles[currentOffset++] = Color.green(color) / 255f;
        mParticles[currentOffset++] = Color.blue(color) / 255f;
        mParticles[currentOffset++] = directionVection.x;
        mParticles[currentOffset++] = directionVection.y;
        mParticles[currentOffset++] = directionVection.z;
        mParticles[currentOffset++] = startTime;

        //更新在native的数据
        mVertexArray.updateBuffer(mParticles , particleOffset , TOTAL_COMPONENT_COUNT);
    }

    public void bindData(ParticleShaderProgram particleShaderProgram){
        mVertexArray.setVertexAttribPointer(0 ,
                particleShaderProgram.getAPositionLocation() , POSITION_COMPONENT_COUNT , STRIDE);
        mVertexArray.setVertexAttribPointer(POSITION_COMPONENT_COUNT ,
                particleShaderProgram.getAColorLocation() , COLOR_COMPONENT_COUNT , STRIDE);
        mVertexArray.setVertexAttribPointer(POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT,
                particleShaderProgram.getADirectionVectorLocation() , VECTOR_COMPONENT_COUNT , STRIDE);
        mVertexArray.setVertexAttribPointer(POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT + VECTOR_COMPONENT_COUNT,
                particleShaderProgram.getAParticleStartTimeLocation() , PARTICLE_START_TIME_COMPONENT_COUNT , STRIDE);
    }

    public void draw(){
        glDrawArrays(GL_POINTS, 0, mCurrentParticleCount);
    }


}

粒子系统完成了,接下来我们还需要一个喷泉去使用我们的粒子系统,在objects包下再新建一个类ParticleShooter.java类,实现如下代码:

public class ParticleShooter {

    private final Geometry.Point mPosition;
    private final Geometry.Vector mDirectionVector;
    private final int mColor;

    public ParticleShooter(Geometry.Point position , Geometry.Vector directionVector , int color){
        mPosition = position;
        mDirectionVector = directionVector;
        mColor = color;
    }

    public void addParticles(ParticleSystem particleSystem , float currentTime , int count){
        for (int i = 0; i < count; i++) {
            particleSystem.addParticle(mPosition , mColor , mDirectionVector , currentTime);
        }
    }

}

最后去修改我们的Renderer就实现了第一步了:

public class ParticlesRenderer implements GLSurfaceView.Renderer{

    private Context mContext;

    private final float[] mProjectionMatrix = new float[16];
    private final float[] mViewMatrix = new float[16];
    private final float[] mViewProjectionMatrix = new float[16];

    private ParticleShaderProgram mParticleShaderProgram;
    private ParticleSystem mParticleSystem;
    private ParticleShooter mRedShooter;
    private ParticleShooter mGreenShooter;
    private ParticleShooter mBlueShooter;
    private float mGlobleStartTime;

    public ParticlesRenderer(Context context){
        mContext = context;
    }

    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
        glClearColor(0f ,0f , 0f, 0f);

        mParticleShaderProgram = new ParticleShaderProgram(mContext);
        mParticleSystem = new ParticleSystem(10000);
        mGlobleStartTime = System.nanoTime();

        final Geometry.Vector particleDirection = new Geometry.Vector(0f, 0.5f, 0f);

        mRedShooter = new ParticleShooter(new Geometry.Point(-1 , 0 , 0) ,
                particleDirection , Color.rgb(255, 50, 5));
        mGreenShooter = new ParticleShooter(new Geometry.Point(0 , 0 , 0) ,
                particleDirection , Color.rgb(25, 255, 25));
        mBlueShooter = new ParticleShooter(new Geometry.Point(1 , 0 , 0) ,
                particleDirection , Color.rgb(5, 50, 255));
    }

    @Override
    public void onSurfaceChanged(GL10 gl10, int width, int height) {
        glViewport(0 , 0 , width , height);

        MatrixHelper.perspectiveM(mProjectionMatrix, 45, (float) width
                / (float) height, 1f, 10f);
        setIdentityM(mViewMatrix, 0);
        translateM(mViewMatrix, 0, 0f, -1.5f, -5f);
        multiplyMM(mViewProjectionMatrix, 0, mProjectionMatrix, 0,
                mViewMatrix, 0);
    }

    @Override
    public void onDrawFrame(GL10 gl10) {
        glClear(GL_COLOR_BUFFER_BIT);

        float currentTime = (System.nanoTime() - mGlobleStartTime) / 1000000000f;

        mRedShooter.addParticles(mParticleSystem , currentTime , 5);
        mGreenShooter.addParticles(mParticleSystem , currentTime , 5);
        mBlueShooter.addParticles(mParticleSystem , currentTime , 5);

        mParticleShaderProgram.useProgram();
        mParticleShaderProgram.setUniforms(mViewProjectionMatrix , currentTime);
        mParticleSystem.bindData(mParticleShaderProgram);
        mParticleSystem.draw();
    }
}

运行一下,你就能看到下面的效果了:

Step1.gif

2.加上随机角度

我们知道粒子的移动是方向向量决定的,所以我们的变动要加到方向向量上,我们也需要给我的随机变动加上一个范围,主要是改动ParticleShooter.java类:

public class ParticleShooter {

    private final Geometry.Point mPosition;
    private final Geometry.Vector mDirectionVector;
    private final int mColor;

    private final float mAngleVariance;
    private final float mSpeedVariance;

    private final Random mRandom = new Random();

    private float[] mRotationMatrix = new float[16];
    private float[] mDirection = new float[4];
    private float[] mResultVector = new float[4];

    public ParticleShooter(Geometry.Point position , Geometry.Vector directionVector , int color ,
                           float angleVariance , float speedVariance){
        mPosition = position;
        mDirectionVector = directionVector;
        mColor = color;
        mAngleVariance = angleVariance;
        mSpeedVariance = speedVariance;

        mDirection[0] = directionVector.x;
        mDirection[1] = directionVector.y;
        mDirection[2] = directionVector.z;
    }

    public void addParticles(ParticleSystem particleSystem , float currentTime , int count){

        for (int i = 0; i < count; i++) {

            setRotateEulerM(mRotationMatrix, 0,
                    (mRandom.nextFloat() - 0.5f) * mAngleVariance,
                    (mRandom.nextFloat() - 0.5f) * mAngleVariance,
                    (mRandom.nextFloat() - 0.5f) * mAngleVariance);

            multiplyMV(
                    mResultVector, 0,
                    mRotationMatrix, 0,
                    mDirection, 0);

            float speedAdjustment = 1f + mRandom.nextFloat() * mSpeedVariance;

            Geometry.Vector thisVector = new Geometry.Vector(
                    mResultVector[0] * speedAdjustment,
                    mResultVector[1] * speedAdjustment,
                    mResultVector[2] * speedAdjustment);

            particleSystem.addParticle(mPosition , mColor , thisVector , currentTime);
        }
    }

}

然后修改Renderer里面的代码:

final float angleVarianceInDegrees = 5f;
final float speedVariance = 1f;

mRedShooter = new ParticleShooter(new Geometry.Point(-1 , 0 , 0) ,
        particleDirection , Color.rgb(255, 50, 5) , angleVarianceInDegrees , speedVariance);
mGreenShooter = new ParticleShooter(new Geometry.Point(0 , 0 , 0) ,
        particleDirection , Color.rgb(25, 255, 25) , angleVarianceInDegrees , speedVariance);
mBlueShooter = new ParticleShooter(new Geometry.Point(1 , 0 , 0) ,
       particleDirection , Color.rgb(5, 50, 255) , angleVarianceInDegrees , speedVariance);

再运行一遍,你就能看到下面的效果了:


Step2.gif

3.加上衰减

我们只需要在shader程序里面算出来一个合适的值,然后一直衰减点的y值就可以了,在particle_vertex_shader.glsl文件中加上如下代码:

float gravityFactor = v_ElapsedTime * v_ElapsedTime / 8.0;
currentPosition.y -= gravityFactor;

然后运行一遍就能看到如下效果了:

Step3.gif

三、优化粒子的显示

不知道大家有没有感觉我们上面的烟花有点怪怪的,就是在烟花多得地方应该是比较亮的,但是我们上面的没有那种效果,那我们要如何去实现那种效果呢?其实很简单,只需要在onSurfaceCreated()方法中调用如下两个方法就可以了:

glEnable(GL_BLEND);
glBlendFunc(GL_ONE , GL_ONE);

第一行代码的意思就是打开Blend效果,下面的方法是一个输出效果的方程,原文是这样解释的:

output = (source factor * source fragment) + (destination factor * destination fragment)

In OpenGL, blending works by blending the result of the fragment shader with the color that’s already there in the frame buffer.

个人理解就是把原来在这里的颜色和将要绘制在这里的颜色按一定的比例混合显示,理解可能有误,大家可以自己查资料理解。
加上这两行代码之后的效果是这样的:

ShapeStep1.gif

接下来我们就要优化下粒子的形状了,现在的正方形是不是感觉不舒服,我们先修改成圆形吧!这个时候我们需要用到一个叫做gl_PointCoord的东西,我们把particle_fragment_shader.glsl修改如下:

precision mediump float;

varying vec3 v_Color;
varying float v_ElapsedTime;

void main() {
    float xDistance = 0.5 - gl_PointCoord.x;
    float yDistance = 0.5 - gl_PointCoord.y;
    float distanceFromCenter =
              sqrt(xDistance * xDistance + yDistance * yDistance);
    if (distanceFromCenter > 0.5) {
        discard;
    } else {
        gl_FragColor = vec4(v_Color / v_ElapsedTime, 1.0);
    }
}

gl_PointCoord的解释原文是这样说的:

For each point, when the fragment shader is run, we’ll get a two-dimensional gl_PointCoord coordinate with each component ranging from 0 to 1 on each axis, depending on which fragment in the point is currently being rendered.

代码里面的意思就是以(0.5,0.5)为圆心,0.5为半径之内的圆的fragment是绘制的,圆之外不绘制,这样就绘制出一个圆点了。运行下看看吧!

ShapeStep2.gif

最后我们用一张图片去绘制我们的点,如果大家忘了如何绘制图片可以看看前面的文章。我们先把我们的particle_fragment_shader.glsl文件修改成下面的样子:

precision mediump float;

varying vec3 v_Color;
varying float v_ElapsedTime;

uniform sampler2D u_TextureUnit;

void main() {
    gl_FragColor = vec4(v_Color / v_ElapsedTime, 1.0)
                 * texture2D(u_TextureUnit, gl_PointCoord);
}

意思就是用原来的颜色去画现在的图。
然后我们需要去修改ParticleShaderProgram.java文件,我们需要先声明我们要用到的位置:

private int mUTextureUnitLocation;

然后再初始化的方法中去赋值:

mUTextureUnitLocation = glGetUniformLocation(mProgram , U_TEXTURE_UNIT);

最后需要修改setUniforms()方法,添加一个参数int textureId然后添加如下代码:

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D , textureId);
glUniform1i(mUTextureUnitLocation , 0);

最后我们在Renderer中加载Texture并给Program设置使用:

int textureId = TextureHelper.loadTexture(mContext , R.drawable.particle_texture);
mParticleShaderProgram.setUniforms(mViewProjectionMatrix , currentTime , textureId);

最后运行看看!是不是好看很多了。


ShapeStep3.gif

这一部分主要是对之前知识的回顾,大家可以好好整理下!

项目代码在这里:https://github.com/KevinKmoo/Particles

能力有限,自己读书的学习所得,有错误请指导,轻虐!
转载请注明出处。----by kmoo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容