OpenGL之粒子

OpenGL之基础
OpenGL之绘制简单形状
OpenGL之颜色
OpenGL之调整屏幕宽高比
OpenGL之三维
OpenGL之纹理
OpenGL之构建简单物体
OpenGL之触控反馈

粒子着色器

首先,需要某种方式在内存中表示所有的粒子,可以为此使用一个Java对象的数组,但是在运行时创建、删除大量的对象消耗可能太大了,而且没有一种简单的方法把数据传递给OpenGL,因此,可以把所有的粒子数据内嵌到一个固定大小的数组中,要添加一个粒子,我们只需要增加粒子的计数,把数据写进粒子数组,并把变动的内容复制到本地缓冲区

我们也需要一个方法来绘制每个粒子,可以把每个粒子表示为一个顶点,并把这些顶点绘制为一组点,每个点都有一个唯一的位置和颜色

最后,还需要某种更新这些粒子的方法。通过把这些逻辑放进一个着色器程序中可以让GPU做部分的更新工作。我们要为每个粒子存储一个方向向量和一个创建时间,用这个创建时间,我们可以计算出粒子自被创建出来之后运行了多少时间,然后,可以用这个运行时间、方向向量和位置推算出粒子当前的位置

顶点着色器

uniform float u_Time;
uniform mat4 u_Matrix;

attribute vec3 a_Position;
attribute vec3 a_Color;
attribute float a_Start_Time;
attribute vec3 a_Direction;

varying vec3 v_Color;
varying float v_ElapsedTime;

void main(){
    v_Color=a_Color;
    v_ElapsedTime=u_Time-a_Start_Time;
    float gravityFactor=v_ElapsedTime*v_ElapsedTime/8.0;
    vec3 currentPosition=a_Position+(a_Direction*v_ElapsedTime);
    currentPosition.y-=gravityFactor;
    gl_Position=u_Matrix*vec4(currentPosition, 1.0);
    gl_PointSize=15.0;
}

因为在glsl中写注释,会出现各种问题,因此将说明放到了后面

定义 说明
uniform float u_Time; 粒子系统启动的时间
uniform mat4 u_Matrix; 模型视图投影矩阵
attribute vec3 a_Position; 粒子的最初位置
attribute vec3 a_Color; 粒子的最初颜色
attribute float a_Start_Time; 每个粒子开始运行时间
attribute vec3 a_Direction; 粒子运行方向
varying vec3 v_Color; 片段着色器中也需要使用颜色,将粒子的颜色传给片段着色器
varying float v_ElapsedTime; 每个粒子从创建至今的时间,并传给片段着色器
// 将粒子颜色赋值给v_Color,传到片段着色器
v_Color=a_Color;
// 计算粒子从创建到现在过去的时间,并传到片段着色器
v_ElapsedTime=u_Time-a_Start_Time;
// 重力因子,可以是其他的值,只要符合现实逻辑,粒子会下落并越来越快就行
float gravityFactor=v_ElapsedTime*v_ElapsedTime/8.0;
// 根据粒子的方向,计算粒子当前位置
vec3 currentPosition=a_Position+(a_Direction*v_ElapsedTime);
// 将位置信息加上重力导致的向下距离
currentPosition.y-=gravityFactor;
// 将顶点的位置进行模型视图投影矩阵的计算,并赋值给最终位置
gl_Position=u_Matrix*vec4(currentPosition, 1.0);
// 定义粒子的大小
gl_PointSize=15.0;

做数学运算时,要确保不会意外地把w分量弄混乱,这是很重要的。因此,我们将用 vec3 表示位置和方向,只有需要把它与 u_Matrix 相乘时,才把它转换为完全的vec 4,这确保上面的数学运算只影响x、y和z分量

片段着色器

precision mediump float;

varying vec3 v_Color;
varying float v_ElapsedTime;

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

通过把颜色除以运行时间,会使新的粒子明亮,使旧的粒子暗淡。给分母加上一个很小的数是为了防止除以0的情况,乘以texture2D(u_TextureUnit, gl_PointCoord),会使用 gl_PointCoord 作为纹理坐标在每个点上绘制纹理,纹理的颜色会与点的颜色相乘

封装着色器

public class ParticlesProgram extends BaseProgram {
    // uniform的位置
    private final int mUMatrix;
    private final int mUTime;
    private final int mUTextureUnit;

    // attribute的位置
    private final int mAColor;
    private final int mAPosition;
    private final int mAStartTime;
    private final int mADirection;

    public ParticlesProgram(Context context) {
        // 在父类中编译、附加、链接着色器程序
        super(context, R.raw.particles8_vertex, R.raw.particles8_fragment);
        // 获取 uniform 的位置
        mUTime = glGetUniformLocation(mProgram, U_TIME);
        mUMatrix = glGetUniformLocation(mProgram, U_MATRIX);
        mUTextureUnit = glGetUniformLocation(mProgram, U_TEXTURE_UNIT);

        // 获取 attribute 的位置
        mAColor = glGetAttribLocation(mProgram, A_COLOR);
        mAPosition = glGetAttribLocation(mProgram, A_POSITION);
        mAStartTime = glGetAttribLocation(mProgram, A_START_TIME);
        mADirection = glGetAttribLocation(mProgram, A_DIRECTION);
    }

    /**
     * 为各个 uniform 设置值
     *
     * @param matrix
     * @param currentTime
     * @param textureId
     */
    public void setUniforms(float[] matrix, float currentTime, int textureId) {
        glUniformMatrix4fv(mUMatrix, 1, false, matrix, 0);
        glUniform1f(mUTime, currentTime);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, textureId);
        glUniform1i(mUTextureUnit, 0);
    }

    public int getAColor() {
        return mAColor;
    }

    public int getAPosition() {
        return mAPosition;
    }

    public int getAStartTime() {
        return mAStartTime;
    }

    public int getADirection() {
        return mADirection;
    }
}

粒子系统

public class ParticleSystem {
    private static final int POSITION_COMPONENT_COUNT = 3;
    private static final int COLOR_COMPONENT_COUNT = 3;
    private static final int DIRECTION_COMPONENT_COUNT = 3;
    private static final int START_TIME_COMPONENT_COUNT = 1;

    private static final int TOTAL_COMPONENT_COUNT = (POSITION_COMPONENT_COUNT +
            COLOR_COMPONENT_COUNT + DIRECTION_COMPONENT_COUNT + START_TIME_COMPONENT_COUNT);

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

    // 最大粒子数
    private int mMaxParticleCount;
    // 当前粒子数
    private int mCurrentParticleCount;
    // 下一个粒子数据的编号
    private int mNextParticle;
    // 复制到 native 层的顶点数据
    private VertexArray mVertexArray;
    // 存储粒子顶点数据的数组
    private float[] mData;

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

    /**
     * 添加粒子
     *  @param position 粒子位置
     * @param color 粒子颜色
     * @param direction 粒子发射方向
     * @param currentTime
     */
    public void addParticle(Point position, int color, Vector direction, float currentTime) {
        // 在顶点数据数组中的偏移值
        int currentOffset = mNextParticle * TOTAL_COMPONENT_COUNT;
        // 位置
        mData[currentOffset++] = position.getX();
        mData[currentOffset++] = position.getY();
        mData[currentOffset++] = position.getZ();

        // 颜色
        mData[currentOffset++] = Color.red(color) / 255f;
        mData[currentOffset++] = Color.green(color) / 255f;
        mData[currentOffset++] = Color.blue(color) / 255f;

        // 方向
        mData[currentOffset++] = direction.getX();
        mData[currentOffset++] = direction.getY();
        mData[currentOffset++] = direction.getZ();

        mData[currentOffset++] = currentTime;

        // 把这个新的粒子复制到本地缓冲区,以便OpenGL可以存取这些新的数据
        mVertexArray.updateBuffer(mData, mNextParticle * TOTAL_COMPONENT_COUNT, TOTAL_COMPONENT_COUNT);

        // 自增下一个粒子数据的编号
        mNextParticle++;
        // 如果下一个粒子数据的编号到了最大值,将其赋值为0,之后添加粒子的话,就又是从头开始存储了
        if (mNextParticle >= mMaxParticleCount) {
            mNextParticle = 0;
        }
        // 增加当前的粒子数目,不会大于最大值
        if (mCurrentParticleCount < mMaxParticleCount) {
            mCurrentParticleCount++;
        }
    }

    /**
     * 绑定各个着色器的属性数据
     *
     * @param particlesProgram
     */
    public void bindData(ParticlesProgram particlesProgram) {
        //位置
        int dataOffset = 0;
        mVertexArray.setVertexAttribPointer(
                dataOffset,
                particlesProgram.getAPosition(),
                POSITION_COMPONENT_COUNT,
                STRIDE
        );
        //颜色
        dataOffset += POSITION_COMPONENT_COUNT;
        mVertexArray.setVertexAttribPointer(
                dataOffset,
                particlesProgram.getAColor(),
                COLOR_COMPONENT_COUNT,
                STRIDE
        );
        //方向
        dataOffset += COLOR_COMPONENT_COUNT;
        mVertexArray.setVertexAttribPointer(
                dataOffset,
                particlesProgram.getADirection(),
                DIRECTION_COMPONENT_COUNT,
                STRIDE
        );
        //开始时间
        dataOffset += DIRECTION_COMPONENT_COUNT;
        mVertexArray.setVertexAttribPointer(
                dataOffset,
                particlesProgram.getAStartTime(),
                START_TIME_COMPONENT_COUNT,
                STRIDE
        );
    }

    /**
     * 绘制粒子
     */
    public void draw() {
        glDrawArrays(GL_POINTS, 0, mCurrentParticleCount);
    }
}

粒子发射器

public class ParticleShooter {
    private final Point mPosition;
    private final int mColor;
    // 控制粒子的角度
    private final float mAngleVarianceInDegree;
    // 控制粒子的速度
    private final float mSpeedVariance;
    // 随机生成器
    private final Random mRandom = new Random();
    // 旋转矩阵
    private float[] rotationMatrix = new float[16];
    // 方向向量,用于与旋转矩阵相乘,得到旋转过的方向向量
    private float[] directionVector = new float[4];
    // 保存旋转过的方向向量,之后在改变其速度
    private float[] resultVector = new float[4];

    public ParticleShooter(Point position, Vector direction, int color, float angleVarianceInDegree, float speedVariance) {
        this.mPosition = position;
        this.mColor = color;
        this.mAngleVarianceInDegree = angleVarianceInDegree;
        this.mSpeedVariance = speedVariance;
        directionVector[0] = direction.getX();
        directionVector[1] = direction.getY();
        directionVector[2] = direction.getZ();
    }

    /**
     * 给粒子系统添加粒子
     *
     * @param particleSystem
     * @param currentTime
     * @param count
     */
    public void addParticle(ParticleSystem particleSystem, float currentTime, int count) {
        for (int i = 0; i < count; i++) {
            // Matrix.setRotateEulerM创建一个旋转矩阵
            Matrix.setRotateEulerM(rotationMatrix, 0,
                    (mRandom.nextFloat() - 0.5f) * mAngleVarianceInDegree,
                    (mRandom.nextFloat() - 0.5f) * mAngleVarianceInDegree,
                    (mRandom.nextFloat() - 0.5f) * mAngleVarianceInDegree
            );
            // 把旋转矩阵与方向向量相乘得到一个旋转向量
            Matrix.multiplyMV(resultVector, 0, rotationMatrix, 0, directionVector, 0);
            // 方向向量的每一个分量都与相同的mSpeedVariance的随机调整值相乘
            float speedAdjustment = 1f + mRandom.nextFloat() * mSpeedVariance;
            Vector direction = new Vector(
                    resultVector[0] * speedAdjustment,
                    resultVector[1] * speedAdjustment,
                    resultVector[2] * speedAdjustment
            );
            particleSystem.addParticle(mPosition, mColor, direction, currentTime);
        }
    }
}

修改渲染器

在onSurfaceCreated方法中,创建粒子系统相关对象

// 粒子着色器程序
mParticlesProgram = new ParticlesProgram(mContext);
// 粒子系统,maxParticleCount如果太小的话,可能只能看到比较新的粒子
mParticleSystem = new ParticleSystem(10000);
// 粒子方向
Vector particleDirection = new Vector(0f, 0.5f, 0f);
// 粒子发射器
mRedParticleShooter = new ParticleShooter(new Point(-1f, 0f, 0f), particleDirection, Color.RED, mAngleVarianceInDegree, mSpeedVariance);
mGreenParticleShooter = new ParticleShooter(new Point(0f, 0f, 0f), particleDirection, Color.GREEN, mAngleVarianceInDegree, mSpeedVariance);
mBlueParticleShooter = new ParticleShooter(new Point(1f, 0f, 0f), particleDirection, Color.BLUE, mAngleVarianceInDegree, mSpeedVariance);
// 粒子系统启动的时间
mGlobalStartTime = System.nanoTime();
// 使用图片的样式绘制粒子
mTextureId = TextureHelper.loadTexture(mContext, R.drawable.particle_texture);

在onDrawFrame方法中,添加并绘制粒子

// 创建并添加粒子
float currentTime = (System.nanoTime() - mGlobalStartTime) / 1000000000f;
Log.d("TAG", "addParticle: currentTime=" + currentTime);
mRedParticleShooter.addParticle(mParticleSystem, currentTime, 5);
mGreenParticleShooter.addParticle(mParticleSystem, currentTime, 5);
mBlueParticleShooter.addParticle(mParticleSystem, currentTime, 5);

// 累计混合技术,粒子越多,就越亮
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE);
mParticlesProgram.useProgram();
// 设置uniform的值
mParticlesProgram.setUniforms(modelViewProjectionMatrix, currentTime, mTextureId);
// 绑定粒子系统数据
mParticleSystem.bindData(mParticlesProgram);
// 绘制粒子系统的所有粒子
mParticleSystem.draw();
// 一次绘制完成后,关闭累计混合技术
glDisable(GL_BLEND);

效果

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

推荐阅读更多精彩内容