OpenGL学习- 14.粒子效果

594 阅读6分钟

14.粒子效果

本次尝试实现一些简单iOS粒子效果的实现。
项目重点

// 可用纹理单元数组
uniform sampler2D u_sampler[10];
// 粒子透明度
varying lowp float v_particleAlpha;
// 选用纹理单元索引
varying lowp float v_textureIndex;

void main()
{
    //gl_PointCoord是片元着色器的内建只读变量,它的值是当前片元所在点图元的二维坐标。点的范围是0.0到1.0
    lowp int indx = int(floor(v_textureIndex));
    if (v_textureIndex - floor(v_textureIndex) > 0.0) {
        indx = int(ceil(v_textureIndex));
    }
    lowp vec4 textureColor = texture2D(u_sampler[indx], gl_PointCoord);
    textureColor.a = textureColor.a * v_particleAlpha;
    gl_FragColor = textureColor;
}

片元着色器中纹理用数组传入,并传入使用的纹理的索引来选择纹理。因为OpenGL ES 2.0版本属性不支持int型,所以传入float类型作索引,但是float类型与int类型转换时有误差,有时会变小、有时会变大,所以为保证同一个粒子每次渲染的时候使用同一个纹理,对indx多进行了一次判断。

// 发射源位置
attribute vec3 a_position;
// 初速度
attribute vec3 a_initialSpeed;
// 加速度
attribute vec3 a_acceleration;
// 纹理索引
attribute lowp float a_textureIndex;
// 发射时间
attribute highp float a_launchTime;
// 持续时间
attribute highp float a_duration;
// 渐消时间
attribute highp float a_disappearDuration;
// 粒子大小
attribute highp float a_size;

// 当前时间
uniform highp float u_runTime;
// 变换矩阵
uniform highp mat4 u_mvpMatrix;

// 粒子透明度
varying lowp float v_particleAlpha;
// 选用纹理单元索引
varying lowp float v_textureIndex;

void main()
{
    //终点: S = V0 * t + (a * t^2)/2.0 + S0
    highp vec3 currentPoint = (u_runTime - a_launchTime) * a_initialSpeed  + pow((u_runTime - a_launchTime), 2.0) * a_acceleration + a_position;
    // 变换矩阵一定要在*前面,因为矩阵乘法不满足交换律的
    gl_Position = u_mvpMatrix * vec4(currentPoint, 1.0);
    gl_PointSize = a_size;
    if (u_runTime < a_launchTime || (a_launchTime + a_duration) < u_runTime) {
        v_particleAlpha = 0.0;
    }else {
        if ((a_launchTime + a_duration - a_disappearDuration) > u_runTime) {
            v_particleAlpha = 1.0;
        }else {
            v_particleAlpha = (a_launchTime + a_duration - u_runTime)/a_disappearDuration;
        }
    }
    v_textureIndex = a_textureIndex;
}

在顶点着色器中,因为是匀变速直线运动,所以根据运动公式来计算当前时间下,粒子运动到的位置。根据当前时间、粒子出现时间、粒子持续时间、粒子渐消时间来计算前时间下粒子的透明度。

/// 预加载纹理
- (void)setTexturesWithPathList:(NSArray <NSString *> *)pathList {
    int index = 0;
    for (int i = 0; i < pathList.count; i ++) {
        NSString *path = pathList[i];
        UIImage *image = [UIImage imageWithContentsOfFile:path];
        CGImageRef imageRef = image.CGImage;
        if (!imageRef) {
            continue;
        }
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        GLbyte *imageData = calloc(width * height * 4, sizeof(GLbyte));
        /// 因为本次的粒子不区分上下,所以Context可以省略上下翻转的仿射变化
        CGContextRef contextRef = CGBitmapContextCreate(imageData, width, height, 8, width * 4, CGImageGetColorSpace(imageRef), kCGImageAlphaPremultipliedLast);
        CGRect rect = CGRectMake(0, 0, width, height);
        CGContextDrawImage(contextRef, rect, imageRef);
        CGContextRelease(contextRef);
        /// 把要配置的纹理设为活跃状态
        glActiveTexture(GL_TEXTURE0 + index);
        glBindTexture(GL_TEXTURE_2D, index);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)width, (GLsizei)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
        /// 纹理数据载入后,可以释放自己开辟的内存了
        free(imageData);
        index ++;
    }
    _textureCount = index;
}

因为本项目会频繁调用这个方法,所以不通过glGenTextures方法来分配纹理ID了,那样会每次调用开辟一份新的内存来存储纹理数据,因为纹理数据的释放时OpenGL自动控制的我们无法手动释放,在多次调用的时候纹理数据会多次存储,内存会累加。如果我们每次给分配固定的ID,那么内存就不会累加,只会覆盖。

typedef NS_ENUM(NSInteger,AttributeKey) {
    positionAttributeKey,///< 发射源位置
    initialSpeedAttributeKey,///< 初速度
    accelerationAttributeKey,///< 加速度
    textureIndexAttributeKey,///< 纹理索引
    launchTimeAttributeKey,///< 发射时间
    durationAttributeKey,///< 持续时间
    disappearDurationAttributeKey,///< 渐消时间
    sizeAttributeKey,///粒子大小
};

glAttachShader(program, vShader);
glAttachShader(program, fShader);
//绑定属性位置 这需要在链接之前完成.
glBindAttribLocation(program, positionAttributeKey, "a_position");
glBindAttribLocation(program, initialSpeedAttributeKey, "a_initialSpeed");
glBindAttribLocation(program, accelerationAttributeKey, "a_acceleration");
glBindAttribLocation(program, textureIndexAttributeKey, "a_textureIndex");
glBindAttribLocation(program, launchTimeAttributeKey, "a_launchTime");
glBindAttribLocation(program, durationAttributeKey, "a_duration");
glBindAttribLocation(program, disappearDurationAttributeKey, "a_disappearDuration");
glBindAttribLocation(program, sizeAttributeKey, "a_size");
glLinkProgram(program);

glEnableVertexAttribArray(positionAttributeKey);
glVertexAttribPointer(positionAttributeKey, size, type, normalized, stride, ptr);

在link Program之前,我们可以绑定属性到本地index,使用的时候根据绑定的本地index来使用,不过这个功能只有属性支持。

typedef NS_ENUM(NSInteger,UniformKey) {
    runTimeUniformKey,///< 运行时间
    mvpMatrixUniformKey,///< 变换矩阵
    samplerUniformKey,///< 纹理数组
};

@interface MCustomParticleManager ()
{
    ///纹理总数目
    int _textureCount;
    ///全部粒子属性数据
    NSMutableData *_particleAttributesData;
    /// 是否需要更新粒子属性数据
    BOOL _needUpdateParticleAttributesData;
    GLuint program;
    GLint uniforms[3];
    GLuint buffer;
}

uniforms[runTimeUniformKey] = glGetUniformLocation(program, "u_runTime");
uniforms[mvpMatrixUniformKey] = glGetUniformLocation(program, "u_mvpMatrix");
uniforms[samplerUniformKey] = glGetUniformLocation(program, "u_sampler");

GLint sampler[_textureCount];
for (int i = 0; i < _textureCount; i ++) {
    sampler[i] = i;
}
glUniform1iv(uniforms[samplerUniformKey], _textureCount, sampler);

在link Program之后我们也可以把Uniform跟本地索引绑定,类似属性的操作,不过没有提供给我们方法,我们需要自己来管理这些映射关系

- (void)draw {
    /// 绘制禁用深度缓冲区写入,可以放置重叠的时候会看到透明的方形的渲染bug
    glDepthMask(GL_FALSE);
    NSUInteger count = _particleAttributesData.length / sizeof(MCustomParticle);
    glDrawArrays(GL_POINTS, 0, (GLsizei)count);
    glDepthMask(GL_TRUE);
}

在开始绘制粒子之前一定要禁用深度缓冲区写入, 否者因为深度比较的原因会看到粒子纹理的透明部分的,如下图 15936691276989.jpg

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    glClearColor(0.5, 0.5, 0.5, 1.0);
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
    glEnable(GL_DEPTH_TEST);
    //开启混合
    glEnable(GL_BLEND);
    //设置混合因子
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    [self.manager start];
//    NSLog(@"glkView drawInRect", self.manager.runTime);
}

带透明部分的粒子纹理来渲染粒子,一定要设置颜色混合和混合因子,不然,透明部分会被填充为黑色,如下图 15936693173863.jpg

- (void)update {
    self.manager.runTime = self.timeSinceFirstResume;
//    NSLog(@"update %lf", self.manager.runTime);
}

计算当前运行时间,可以通过重写update来读取self.timeSinceFirstResume获得,这是自视图控制器第一次恢复发送更新事件以来所经过的时间。

/// 变换矩阵
@property (strong, nonatomic) GLKEffectPropertyTransform *transform;

float aspect = CGRectGetWidth(self.view.bounds) / CGRectGetHeight(self.view.bounds);
self.manager.transform.projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(85.0f), aspect, 0.1f,  20.0f);
self.manager.transform.modelviewMatrix = GLKMatrix4MakeLookAt( 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
/*
 GLKMatrix4 GLKMatrix4MakeLookAt(float eyeX, float eyeY, float eyeZ,
 float centerX, float centerY, float centerZ,
 float upX, float upY, float upZ)
 等价于 OpenGL 中
 void gluLookAt(GLdouble eyex,GLdouble eyey,GLdouble eyez,GLdouble centerx,GLdouble centery,GLdouble centerz,GLdouble upx,GLdouble upy,GLdouble upz);

 目的:根据你的设置返回一个4x4矩阵变换的世界坐标系坐标。
 参数1:眼睛位置的x坐标
 参数2:眼睛位置的y坐标
 参数3:眼睛位置的z坐标
 第一组:就是脑袋的位置

 参数4:正在观察的点的X坐标
 参数5:正在观察的点的Y坐标
 参数6:正在观察的点的Z坐标
 第二组:就是眼睛所看物体的位置

 参数7:摄像机上向量的x坐标
 参数8:摄像机上向量的y坐标
 参数9:摄像机上向量的z坐标
 第三组:就是头顶朝向的方向(因为你可以头歪着的状态看物体)
 */

项目中还有一些不足之处:

  1. 可调节参数的调节区域,本次使用单个浮点型数值来控制,即(-f,+f)区域,如果采用2个值来控制会更灵活一些,即(-a,+b),可以使用2维向量来存储
  2. 本次对于时间、加速度等各种调节参数的配置都是固定的,如果可以更加自由的配置每个粒子或多个粒子支持的效果会更好,这样可能就需要配置计算函数来,具体的函数如何配置还没有思路,但是通过配置函数来控制粒子更加合理,或许可以给manager添加一个block或代理来控制每个粒子的实现,这样就可以使用的时候来计算粒子的实现了,manager也就不需要那么多配置参数了。嗯,可以提供2套方案,一套通过属性配置的简单版本,一个基于block或代理的自定义版本。
  3. 当前的粒子位置计算是根据匀变速运动计算的,如果要实现变加速运动就无法支持了。
  4. 其实我想实现一个烟花绽放的效果的,但是当前的项目并不支持,如果有基于block或代理的自定义版本了,应该就能实现了。

代码示例见:Github