iOS高级进阶|OpenGL ES之GLSL自定义着色器编程实现粒子效果
效果展示
文章图片
实现流程
一、自定义着色器
- 顶点着色器:YDWPointParticleShader.vsh
// 位置
attribute vec3 a_emissionPosition;
// 速度
attribute vec3 a_emissionVelocity;
// 受力
attribute vec3 a_emissionForce;
// 大小和Fade持续时间size = GLKVector2Make(aSize, aDuration);
attribute vec2 a_size;
// 发射时间和消失时间
attribute vec2 a_emissionAndDeathTimes;
// UNIFORMS
uniform highp mat4u_mvpMatrix;
// 变换矩阵
uniform sampler2Du_samplers2D[1];
// 纹理
uniform highp vec3u_gravity;
// 重力
uniform highp floatu_elapsedSeconds;
// 当前时间// Varyings:粒子透明度
varying lowp floatv_particleOpacity;
void main() {// 流逝时间
highp float elapsedTime = u_elapsedSeconds - a_emissionAndDeathTimes.x;
// 质量假设是1.0 加速度 = 力 (a = f/m)
// v = v0 + at : v 是当前速度;
v0 是初速度;
//a 是加速度;
t 是时间
// a_emissionForce 受力,u_gravity 重力// 求速度velocity
highp vec3 velocity = a_emissionVelocity +
((a_emissionForce + u_gravity) * elapsedTime);
// s = s0 + 0.5 * (v0 + v) * t
// s 当前位置
// s0 初始位置
// v0 初始速度
// v 当前速度
// t 是时间// 运算是对向量运算,相当于分别求出x、y、z的位置,再综合
// 求粒子的受力后的位置 = a_emissionPosition(原始位置) + 0.5 * (速度+加速度) * 流逝时间highp vec3 untransformedPosition = a_emissionPosition +
0.5 * (a_emissionVelocity + velocity) * elapsedTime;
//得出点的位置
gl_Position = u_mvpMatrix * vec4(untransformedPosition, 1.0);
gl_PointSize = a_size.x / gl_Position.w;
// 消失时间减去当前时间,得到当前的寿命; 除以Fade持续时间,当剩余时间小于Fade时间后,得到一个从1到0变化的值
// 如果这个值小于0,则取0
float remainTime = a_emissionAndDeathTimes.y - u_elapsedSeconds;
float keepTime = max(a_size.y, 0.00001);
v_particleOpacity = max(0.0, min(1.0,remainTime /keepTime));
}
- 自定义片元着色器
// UNIFORMS
uniform highp mat4u_mvpMatrix;
uniform sampler2Du_samplers2D[1];
uniform highp vec3u_gravity;
uniform highp floatu_elapsedSeconds;
// Varyings
varying lowp floatv_particleOpacity;
void main() {
// 通过texture2D函数我们可以得到一个纹素(texel),这是一个纹理图片中的像素。函数参数分别为simpler2D以及纹理坐标:
// gl_PointCoord是片元着色器的内建只读变量,它的值是当前片元所在点图元的二维坐标。点的范围是0.0到1.0
lowp vec4 textureColor = texture2D(u_samplers2D[0], gl_PointCoord);
// 粒子透明度 与 v_particleOpacity 值相关
textureColor.a = textureColor.a * v_particleOpacity;
// 设置片元颜色值
gl_FragColor = textureColor;
}
二、封装着色器工具 该工具主要是将OpenGL ES 的部分操作封装成了自定义的方法,增加了代码的复用性:
① initWithAttribStride函数 初始化,用于在OpenGL ES 中创建一个VBO(顶点属性数组缓冲区)创建VBO的3个步骤:
- 生成新缓存对象glGenBuffers
- 绑定缓存对象glBindBuffer : 告诉VBO缓存对象时保存顶点数组数据还是索引数组数据 :GL_ARRAY_BUFFER/GL_ELEMENT_ARRAY_BUFFER,任何顶点属性,如顶点坐标、纹理坐标、法线与颜色分量数组都使用GL_ARRAY_BUFFER。用于glDraw[Range]Elements()的索引数据需要使用GL_ELEMENT_ARRAY绑定。
注意,target标志帮助VBO确定缓存对象最有效的位置,如有些系统将索引保存AGP或系统内存中,将顶点保存在显卡内存中。 - 将顶点数据拷贝到缓存对象中glBufferData
// 此方法在当前的OpenGL ES上下文中创建一个顶点属性数组缓冲区
- (id)initWithAttribStride:(GLsizeiptr)aStride
numberOfVertices:(GLsizei)count
bytes:(const GLvoid *)dataPtr
usage:(GLenum)usage {
self = [super init];
if(self != nil) {
_stride = aStride;
_bufferSizeBytes = _stride * count;
// 初始化缓存区
// 创建VBO的3个步骤
// 1.生成新缓存对象glGenBuffers
// 2.绑定缓存对象glBindBuffer
// 3.将顶点数据拷贝到缓存对象中glBufferData// STEP 1 创建缓存对象并返回缓存对象的标识符
glGenBuffers(1,&_name);
// STEP 2 将缓存对象对应到相应的缓存上
/*
glBindBuffer (GLenum target, GLuint buffer);
target:告诉VBO缓存对象时保存顶点数组数据还是索引数组数据 :GL_ARRAY_BUFFER\GL_ELEMENT_ARRAY_BUFFER
任何顶点属性,如顶点坐标、纹理坐标、法线与颜色分量数组都使用GL_ARRAY_BUFFER。用于glDraw[Range]Elements()的索引数据需要使用GL_ELEMENT_ARRAY绑定。注意,target标志帮助VBO确定缓存对象最有效的位置,如有些系统将索引保存AGP或系统内存中,将顶点保存在显卡内存中。
buffer: 缓存区对象
*/glBindBuffer(GL_ARRAY_BUFFER,self.name);
/*
数据拷贝到缓存对象
void glBufferData(GLenum target,GLsizeiptr size, const GLvoid*data, GLenum usage);
target:可以为GL_ARRAY_BUFFER或GL_ELEMENT_ARRAY
size:待传递数据字节数量
data:源数据数组指针
usage:
GL_STATIC_DRAW
GL_STATIC_READ
GL_STATIC_COPY
GL_DYNAMIC_DRAW
GL_DYNAMIC_READ
GL_DYNAMIC_COPY
GL_STREAM_DRAW
GL_STREAM_READ
GL_STREAM_COPY”static“表示VBO中的数据将不会被改动(一次指定多次使用)
”dynamic“表示数据将会被频繁改动(反复指定与使用)
”stream“表示每帧数据都要改变(一次指定一次使用)
”draw“表示数据将被发送到GPU以待绘制(应用程序到GL)
”read“表示数据将被客户端程序读取(GL到应用程序)
*/
// STEP 3 数据拷贝到缓存对象
glBufferData(
GL_ARRAY_BUFFER,// Initialize buffer contents
_bufferSizeBytes,// Number of bytes to copy
dataPtr,// Address of bytes to copy
usage);
// Hint: cache in GPU memory}return self;
}
② reinitWithAttribStride函数: 更新数据,用于在已有VBO的情况下,更新传入着色器中的数据:
- 将缓存对象对应到相应的缓存上
- 数据拷贝到缓存对象
// 此方法加载由接收存储的数据
- (void)reinitWithAttribStride:(GLsizeiptr)aStride
numberOfVertices:(GLsizei)count
bytes:(const GLvoid *)dataPtr {
_stride = aStride;
_bufferSizeBytes = aStride * count;
// STEP 1 将缓存对象对应到相应的缓存上
glBindBuffer(GL_ARRAY_BUFFER, self.name);
// STEP 2 数据拷贝到缓存对象
glBufferData(
GL_ARRAY_BUFFER,
_bufferSizeBytes,
dataPtr,
GL_DYNAMIC_DRAW);
}
③ prepareToDrawWithAttrib函数:
- 准备绘制,用于打开attribute通道 & 设置数据的读取方式;
- 当应用程序希望使用缓冲区呈现任何几何图形时,必须准备一个顶点属性数组缓冲区。当你的应用程序准备一个缓冲区时,一些OpenGL ES状态被改变,允许绑定缓冲区和配置指针。
- 将缓存对象对应到相应的缓存上。
- 出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的,意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU,由glEnableVertexAttribArray启用指定属性,才可在顶点着色器中访问逐顶点的属性数据。
- VBO只是建立CPU和GPU之间的逻辑连接,从而实现了CPU数据上传至GPU。但是,数据在GPU端是否可见,即着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU(服务器端)数据。
- 顶点数据传入GPU之后,还需要通知OpenGL如何解释这些顶点数据,这个工作由函数glVertexAttribPointer完成。
// 当应用程序希望使用缓冲区呈现任何几何图形时,必须准备一个顶点属性数组缓冲区。当你的应用程序准备一个缓冲区时,一些OpenGL ES状态被改变,允许绑定缓冲区和配置指针。
- (void)prepareToDrawWithAttrib:(GLuint)index
numberOfCoordinates:(GLint)count
attribOffset:(GLsizeiptr)offset
shouldEnable:(BOOL)shouldEnable {
if (count < 0 || count > 4) {
NSLog(@"Error:Count Error");
return ;
}if (_stride < offset) {
NSLog(@"Error:_stride < Offset");
return;
}if (_name == 0) {
NSLog(@"Error:name == Null");
}// STEP 1 将缓存对象对应到相应的缓存上
glBindBuffer(GL_ARRAY_BUFFER,self.name);
// 判断是否使用
if(shouldEnable) {
// Step 2
// 出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的,意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU,由glEnableVertexAttribArray启用指定属性,才可在顶点着色器中访问逐顶点的属性数据.
// VBO只是建立CPU和GPU之间的逻辑连接,从而实现了CPU数据上传至GPU。但是,数据在GPU端是否可见,即着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU(服务器端)数据。
// 顶点数据传入GPU之后,还需要通知OpenGL如何解释这些顶点数据,这个工作由函数glVertexAttribPointer完成
glEnableVertexAttribArray(index);
}// Step 3
// 顶点数据传入GPU之后,还需要通知OpenGL如何解释这些顶点数据,这个工作由函数glVertexAttribPointer完成
/*
glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)
indx:参数指定顶点属性位置
size:指定顶点属性大小
type:指定数据类型
normalized:数据被标准化
stride:步长
ptr:偏移量 NULL+offset
*/
glVertexAttribPointer(
index,
count,
GL_FLOAT,
GL_FALSE,
(int)self.stride,
NULL + offset);
}
④ drawPreparedArraysWithMode函数:
- 绘制,调用OpenGL ES 的数组绘制方式进行图形的绘制;
- 将绘图命令模式和instructsopengl ES确定使用缓冲区从顶点索引的第一个数的顶点,顶点索引从0开始;
- 当采用顶点数组方式绘制图形时,使用该函数glDrawArrays (GLenum mode, GLint first, GLsizei count)。该函数根据顶点数组中的坐标数据和指定的模式,进行绘制。
// 将绘图命令模式和instructsopengl ES确定使用缓冲区从顶点索引的第一个数的顶点,顶点索引从0开始
- (void)drawArrayWithMode:(GLenum)mode
startVertexIndex:(GLint)first
numberOfVertices:(GLsizei)count {
if (self.bufferSizeBytes < (first + count) * self.stride) {
NSLog(@"Vertex Error!");
}// 绘制
/*
glDrawArrays (GLenum mode, GLint first, GLsizei count);
提供绘制功能。当采用顶点数组方式绘制图形时,使用该函数。该函数根据顶点数组中的坐标数据和指定的模式,进行绘制。
参数列表:
mode,绘制方式,OpenGL2.0以后提供以下参数:GL_POINTS、GL_LINES、GL_LINE_LOOP、GL_LINE_STRIP、GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN。
first,从数组缓存中的哪一位开始绘制,一般为0。
count,数组中顶点的数量。
*/
glDrawArrays(mode, first, count);
}
三、粒子类:管理并绘制粒子 【iOS高级进阶|OpenGL ES之GLSL自定义着色器编程实现粒子效果】主要是管理并绘制所有的粒子,相当GLKit中的GLKBaseEffect类
① 定义变量
- 定义需要用到的枚举和结构体
// 用于定义粒子属性的类型
typedef struct {
GLKVector3 emissionPosition;
// 发射位置
GLKVector3 emissionVelocity;
// 发射速度
GLKVector3 emissionForce;
// 发射重力
GLKVector2 size;
// 发射大小
GLKVector2 emissionTimeAndLife;
// 发射时间和寿命[出生时间,死亡时间]
} CCParticleAttributes;
// GLSL程序Uniform参数
enum {
CCMVPMatrix,// MVP矩阵
CCSamplers2D,// Samplers2D纹理
CCElapsedSeconds,// 耗时
CCGravity,// 重力
CCNumUniforms// Uniforms个数
};
// 属性标识符
typedef enum {
CCParticleEmissionPosition = 0,// 粒子发射位置
CCParticleEmissionVelocity,// 粒子发射速度
CCParticleEmissionForce,// 粒子发射重力
CCParticleSize,// 粒子发射大小
CCParticleEmissionTimeAndLife, // 粒子发射时间和寿命
} CCParticleAttrib;
- 定义属性
// 重力
@property(nonatomic,assign)GLKVector3 gravity;
// 耗时
@property(nonatomic,assign)GLfloat elapsedSeconds;
// 纹理
@property (strong, nonatomic, readonly)GLKEffectPropertyTexture *texture2d0;
// 变换
@property (strong, nonatomic, readonly) GLKEffectPropertyTransform *transform;
@interface YDWPointParticleEffect() {
GLfloat elapsedSeconds;
// 耗时
GLuint program;
// 程序
GLint uniforms[CCNumUniforms];
// Uniforms数组
}// 顶点属性数组缓冲区
@property (strong, nonatomic, readwrite) YDWVertexAttribArrayBuffer*particleAttributeBuffer;
// 粒子个数
@property (nonatomic, assign, readonly) NSUInteger numberOfParticles;
// 粒子属性数据
@property (nonatomic, strong, readonly) NSMutableData *particleAttributesData;
// 是否更新粒子数据
@property (nonatomic, assign, readwrite) BOOL particleDataWasUpdated;
- 定义方法名
// 加载shaders
- (BOOL)loadShaders;
// 编译shaders
- (BOOL)compileShader:(GLuint *)shader
type:(GLenum)type
file:(NSString *)file;
// 链接Program
- (BOOL)linkProgram:(GLuint)prog;
// 验证Program
- (BOOL)validateProgram:(GLuint)prog;
② addParticleAtPosition函数:用于添加粒子,每次调用仅添加一个粒子,在视图控制器中创建粒子对象,通过粒子对象来调用该方法创建粒子
- 初始化纹理属性
// 初始化
- (id)init {
self = [super init];
if (self != nil) {
// 初始化纹理属性
texture2d0 = [[GLKEffectPropertyTexture alloc] init];
// 是否可用
texture2d0.enabled = YES;
/* 命名纹理对象
* 等价于:void glGenTextures (GLsizei n, GLuint *textures);
* 在数组textures中返回n个当期未使用的值,表示纹理对象的名称
* 零作为一个保留的纹理对象名,它不会被此函数当做纹理对象名称而返回
*/
texture2d0.name = 0;
// 纹理类型 默认值是glktexturetarget2d
texture2d0.target = GLKTextureTarget2D;
/* 纹理用于计算其输出片段颜色的模式。看到GLKTextureEnvMode
* GLKTextureEnvModeReplace,输出颜色设置为从纹理获取的颜色,忽略输入颜色
* GLKTextureEnvModeModulate, 默认!输出颜色是通过将纹理的颜色乘以输入颜色来计算的
* GLKTextureEnvModeDecal,输出颜色是通过使用纹理的alpha组件来混合纹理颜色和输入颜色来计算的
*/
texture2d0.envMode = GLKTextureEnvModeReplace;
// 坐标变换的信息用于GLKit渲染效果。GLKEffectPropertyTransform类定义的属性进行渲染时的效果提供的坐标变换
transform = [[GLKEffectPropertyTransform alloc] init];
// 重力.默认地球重力
gravity = CCDefaultGravity;
// 耗时
elapsedSeconds = 0.0f;
// 粒子属性数据
particleAttributesData = https://www.it610.com/article/[NSMutableData data];
}return self;
}
- 创建粒子和设置粒子属性
// 添加一个粒子
- (void)addParticleAtPosition:(GLKVector3)aPosition
velocity:(GLKVector3)aVelocity
force:(GLKVector3)aForce
size:(float)aSize
lifeSpanSeconds:(NSTimeInterval)aSpan
fadeDurationSeconds:(NSTimeInterval)aDuration {
// 创建新的粒子
CCParticleAttributes newParticle;
// 设置相关参数(位置\速度\抛物线\大小\耗时)
newParticle.emissionPosition = aPosition;
newParticle.emissionVelocity = aVelocity;
newParticle.emissionForce = aForce;
newParticle.size = GLKVector2Make(aSize, aDuration);
// 向量(耗时,发射时长)
newParticle.emissionTimeAndLife = GLKVector2Make(elapsedSeconds, elapsedSeconds + aSpan);
BOOL foundSlot = NO;
// 粒子个数
const long count = self.numberOfParticles;
// 循环设置粒子到数组中
for(int i = 0;
i < count && !foundSlot;
i++) {// 获取当前旧粒子
CCParticleAttributes oldParticle = [self particleAtIndex:i];
// 如果旧的粒子的死亡时间小于当前时间:emissionTimeAndLife.y = elapsedSeconds + aspan
if(oldParticle.emissionTimeAndLife.y < self.elapsedSeconds) {
// 更新粒子的属性
[self setParticle:newParticle atIndex:i];
// 是否替换
foundSlot = YES;
}
}// 如果不替换
if(!foundSlot) {
// 在particleAttributesData 拼接新的数据
[self.particleAttributesData appendBytes:&newParticle
length:sizeof(newParticle)];
// 粒子数据是否更新
self.particleDataWasUpdated = YES;
}
}
// 设置粒子的属性
- (void)setParticle:(CCParticleAttributes)aParticle
atIndex:(NSUInteger)anIndex {
// mutableBytes:指向可变数据对象所包含数据的指针
// 获取粒子属性结构体内容
CCParticleAttributes *particlesPtr = (CCParticleAttributes *)[self.particleAttributesData mutableBytes];
// 将粒子结构体对应的属性修改为新值
particlesPtr[anIndex] = aParticle;
// 更改粒子状态! 是否更新
self.particleDataWasUpdated = YES;
}
- 获取粒子个数
// 获取粒子个数
- (NSUInteger)numberOfParticles {
static long last;
// 总数据/粒子结构体大小
long ret = [self.particleAttributesData length] / sizeof(CCParticleAttributes);
// 如果last != ret 表示粒子个数更新
if (last != ret) {
// 则修改last数量
last = ret;
NSLog(@"count %ld", ret);
}return ret;
}
③ prepareToDraw函数
- 加载 & 使用shader
- (BOOL)loadShaders {
GLuint vertShader, fragShader;
NSString *vertShaderPathname, *fragShaderPathname;
// 创建program
program = glCreateProgram();
// 创建并编译 vertex shader.
vertShaderPathname = [[NSBundle mainBundle] pathForResource:
@"CCPointParticleShader" ofType:@"vsh"];
if (![self compileShader:&vertShader type:GL_VERTEX_SHADER
file:vertShaderPathname]) {
NSLog(@"Failed to compile vertex shader");
return NO;
}// 创建并编译 fragment shader.
fragShaderPathname = [[NSBundle mainBundle] pathForResource:
@"CCPointParticleShader" ofType:@"fsh"];
if (![self compileShader:&fragShader type:GL_FRAGMENT_SHADER
file:fragShaderPathname]) {
NSLog(@"Failed to compile fragment shader");
return NO;
}// 将vertex shader 附加到程序.
glAttachShader(program, vertShader);
// 将fragment shader 附加到程序.
glAttachShader(program, fragShader);
// 绑定属性位置
// 这需要在链接之前完成.
/*
应用程序通过glBindAttribLocation把“顶点属性索引”绑定到“顶点属性名”,glBindAttribLocation在program被link之前执行。
void glBindAttribLocation(GLuint program, GLuint index,const GLchar *name)
program:对应的程序
index:顶点属性索引
name:属性名称
*///位置
glBindAttribLocation(program, CCParticleEmissionPosition,
"a_emissionPosition");
//速度
glBindAttribLocation(program, CCParticleEmissionVelocity,
"a_emissionVelocity");
//重力
glBindAttribLocation(program, CCParticleEmissionForce,
"a_emissionForce");
//大小
glBindAttribLocation(program, CCParticleSize,
"a_size");
//持续时间、渐隐时间
glBindAttribLocation(program, CCParticleEmissionTimeAndLife,
"a_emissionAndDeathTimes");
// Link program 失败
if (![self linkProgram:program]) {
NSLog(@"Failed to link program: %d", program);
//link识别,删除vertex shader\fragment shader\program
if (vertShader) {
glDeleteShader(vertShader);
vertShader = 0;
}
if (fragShader) {
glDeleteShader(fragShader);
fragShader = 0;
}
if (program) {
glDeleteProgram(program);
program = 0;
}return NO;
}// 获取uniform变量的位置.
// MVP变换矩阵
uniforms[CCMVPMatrix] = glGetUniformLocation(program,"u_mvpMatrix");
// 纹理
uniforms[CCSamplers2D] = glGetUniformLocation(program,"u_samplers2D");
// 重力
uniforms[CCGravity] = glGetUniformLocation(program,"u_gravity");
// 持续时间、渐隐时间
uniforms[CCElapsedSeconds] = glGetUniformLocation(program,"u_elapsedSeconds");
// 使用完
// 删除 vertex and fragment shaders.
if (vertShader) {
glDetachShader(program, vertShader);
glDeleteShader(vertShader);
}
if (fragShader) {
glDetachShader(program, fragShader);
glDeleteShader(fragShader);
}
return YES;
}
- 编译shader
// 编译shader
- (BOOL)compileShader:(GLuint *)shader
type:(GLenum)type
file:(NSString *)file {
//状态
//GLint status;
//路径-C语言
const GLchar *source;
//从OC字符串中获取C语言字符串
//获取路径
source = (GLchar *)[[NSString stringWithContentsOfFile:file
encoding:NSUTF8StringEncoding error:nil] UTF8String];
//判断路径
if (!source)
{
NSLog(@"Failed to load vertex shader");
return NO;
}//创建shader-顶点\片元
*shader = glCreateShader(type);
//绑定shader
glShaderSource(*shader, 1, &source, NULL);
//编译Shader
glCompileShader(*shader);
//获取加载Shader的日志信息
//日志信息长度
GLint logLength;
/*
在OpenGL中有方法能够获取到 shader错误
参数1:对象,从哪个Shader
参数2:获取信息类别,
GL_COMPILE_STATUS//编译状态
GL_INFO_LOG_LENGTH//日志长度
GL_SHADER_SOURCE_LENGTH //着色器源文件长度
GL_SHADER_COMPILER//着色器编译器
参数3:获取长度
*/
glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength);
// 判断日志长度 > 0
if (logLength > 0) {
// 创建日志字符串
GLchar *log = (GLchar *)malloc(logLength);
/*
获取日志信息
参数1:着色器
参数2:日志信息长度
参数3:日志信息长度地址
参数4:日志存储的位置
*/
glGetShaderInfoLog(*shader, logLength, &logLength, log);
//打印日志信息
NSLog(@"Shader compile log:\n%s", log);
//释放日志字符串
free(log);
return NO;
}
return YES;
}
- 通过Uniform传递数据
// 使用program
glUseProgram(program);
// 计算MVP矩阵变化
// 投影矩阵 与 模式视图矩阵 相乘结果
GLKMatrix4 modelViewProjectionMatrix = GLKMatrix4Multiply(self.transform.projectionMatrix,self.transform.modelviewMatrix);
/* 将结果矩阵,通过unifrom传递
* glUniformMatrix4fv (GLint location, GLsizei count, GLboolean transpose, const GLfloat* value)
* 参数1:location,要更改的uniforms变量的位置
* 参数2:cout ,更改矩阵的个数
* 参数3:transpose,指是否要转置矩阵,并将它作为uniform变量的值,必须为GL_FALSE
* 参数4:value ,指向count个数的元素指针.用来更新uniform变量的值.
*/
glUniformMatrix4fv(uniforms[CCMVPMatrix], 1, 0,modelViewProjectionMatrix.m);
/* 一个纹理采样均匀变量
* glUniform1f(GLint location,GLfloat v0);
* location:指明要更改的uniform变量的位置
* v0:指明在指定的uniform变量中要使用的新值
*/
glUniform1i(uniforms[CCSamplers2D], 0);
/* 粒子物理值:重力
* void glUniform3fv(GLint location,GLsizei count,const GLfloat *value);
* 参数列表:
* location:指明要更改的uniform变量的位置
* count:指明要更改的向量个数
* value:指明一个指向count个元素的指针,用来更新指定的uniform变量
*/
glUniform3fv(uniforms[CCGravity], 1, self.gravity.v);
// 耗时
glUniform1fv(uniforms[CCElapsedSeconds], 1, &elapsedSeconds);
- 更新粒子数据,需要调用工具类中的初始化/更新数据方法,将数据从CPU拷贝至GPU
// 粒子数据更新
if(self.particleDataWasUpdated) {
// 缓存区为空,且粒子数据大小>0
if(self.particleAttributeBuffer == nil && [self.particleAttributesData length] > 0) {
/* 顶点属性没有送到GPU:初始化缓存区
* 1.数据大小sizeof(CCParticleAttributes)
* 2.数据个数 (int)[self.particleAttributesData length] / sizeof(CCParticleAttributes)
* 3.数据源[self.particleAttributesData bytes]
* 4.用途 GL_DYNAMIC_DRAW
*/// 数据大小
GLsizeiptr size = sizeof(CCParticleAttributes);
// 个数
int count = (int)[self.particleAttributesData length] /
sizeof(CCParticleAttributes);
self.particleAttributeBuffer = [[YDWVertexAttribArrayBuffer alloc] initWithAttribStride:size numberOfVertices:count bytes:[self.particleAttributesData bytes] usage:GL_DYNAMIC_DRAW];
} else {
/* 如果已经开辟空间,则接收新的数据
* 1.数据大小 sizeof(CCParticleAttributes)
* 2.数据个数(int)[self.particleAttributesData length] / sizeof(CCParticleAttributes)
* 3.数据源 [self.particleAttributesData bytes]
*/// 数据大小
GLsizeiptr size = sizeof(CCParticleAttributes);
// 个数
int count = (int)[self.particleAttributesData length] /
sizeof(CCParticleAttributes);
[self.particleAttributeBuffer
reinitWithAttribStride:size
numberOfVertices:count
bytes:[self.particleAttributesData bytes]];
}// 恢复更新状态为NO
self.particleDataWasUpdated = NO;
}
- 准备相关数据,需要调用工具类的封装的prepareToDrawWithAttrib方法,打开attribute通道,并设置数据的读取方式
// 准备顶点数据
[self.particleAttributeBuffer
prepareToDrawWithAttrib:CCParticleEmissionPosition
numberOfCoordinates:3
attribOffset:
offsetof(CCParticleAttributes, emissionPosition)
shouldEnable:YES];
// 准备粒子发射速度数据
[self.particleAttributeBuffer
prepareToDrawWithAttrib:CCParticleEmissionVelocity
numberOfCoordinates:3
attribOffset:
offsetof(CCParticleAttributes, emissionVelocity)
shouldEnable:YES];
// 准备重力数据
[self.particleAttributeBuffer
prepareToDrawWithAttrib:CCParticleEmissionForce
numberOfCoordinates:3
attribOffset:
offsetof(CCParticleAttributes, emissionForce)
shouldEnable:YES];
// 准备粒子size数据
[self.particleAttributeBuffer
prepareToDrawWithAttrib:CCParticleSize
numberOfCoordinates:2
attribOffset:
offsetof(CCParticleAttributes, size)
shouldEnable:YES];
// 准备粒子的持续时间和渐隐时间数据
[self.particleAttributeBuffer
prepareToDrawWithAttrib:CCParticleEmissionTimeAndLife
numberOfCoordinates:2
attribOffset:
offsetof(CCParticleAttributes, emissionTimeAndLife)
shouldEnable:YES];
- 绑定纹理
// 将所有纹理绑定到各自的单位
/*
void glActiveTexture(GLenum texUnit);
该函数选择一个纹理单元,线面的纹理函数将作用于该纹理单元上,参数为符号常量GL_TEXTUREi ,i的取值范围为0~K-1,K是OpenGL实现支持的最大纹理单元数,可以使用GL_MAX_TEXTURE_UNITS来调用函数glGetIntegerv()获取该值
可以这样简单的理解为:显卡中有N个纹理单元(具体数目依赖你的显卡能力),每个纹理单元(GL_TEXTURE0、GL_TEXTURE1等)都有GL_TEXTURE_1D、GL_TEXTURE_2D等
*/
glActiveTexture(GL_TEXTURE0);
//判断纹理标记是否为空,以及纹理是否可用
if(0 != self.texture2d0.name && self.texture2d0.enabled) {
//绑定纹理到纹理标记上
//参数1:纹理类型
//参数2:纹理名称
glBindTexture(GL_TEXTURE_2D, self.texture2d0.name);
} else {
//绑定一个空的
glBindTexture(GL_TEXTURE_2D, 0);
}
④ draw函数:用于绘制粒子,底层需要调用工具类的drawArrayWithMode方法,使用OpenGL ES数组绘制的方式进行绘制
- (void)draw {
// 禁用深度缓冲区写入
glDepthMask(GL_FALSE);
//绘制
/*
1.模式
2.开始的位置
3.粒子个数
*/
[self.particleAttributeBuffer drawArrayWithMode:GL_POINTS startVertexIndex:0 numberOfVertices:(int)self.numberOfParticles];
// 启用深度缓冲区写入
glDepthMask(GL_TRUE);
}
四、视图控制器 ②③④bai⑤⑥⑦⑧⑨du⑩??? 通过粒子类YDWPointParticleEffect创建粒子对象,并实现4种粒子效果,然后系统调用GLKView & GLKViewDelegate代理方法进行绘制和更新
- viewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
// 新建OpenGLES 上下文
self.mContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
GLKView* view = (GLKView *)self.view;
view.context = self.mContext;
view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
[EAGLContext setCurrentContext:self.mContext];
// 纹理路径
NSString *path = [[NSBundle bundleForClass:[self class]]
pathForResource:@"ball" ofType:@"png"];
if (path == nil) {
NSLog(@"ball texture image not found");
return;
}// 加载纹理对象
NSError *error = nil;
self.ballParticleTexture = [GLKTextureLoader textureWithContentsOfFile:path options:nil error:&error];
// 粒子对象
self.particleEffect = [[YDWPointParticleEffect alloc]init];
self.particleEffect.texture2d0.name = self.ballParticleTexture.name;
self.particleEffect.texture2d0.target = self.ballParticleTexture.target;
// 开启深度测试
glEnable(GL_DEPTH_TEST);
// 开启混合
glEnable(GL_BLEND);
// 设置混合因子
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// 执行代码块.4种不同效果
void(^blockA)(void) = ^{
self.autoSpawnDelta = 0.5f;
// 重力
self.particleEffect.gravity = CCDefaultGravity;
// X轴上随机速度
float randomXVelocity = -0.5f + 1.0f * (float)random() / (float)RAND_MAX;
/*
Position:出发位置
velocity:速度
force:抛物线
size:大小
lifeSpanSeconds:耗时
fadeDurationSeconds:渐逝时间
*/
[self.particleEffect
addParticleAtPosition:GLKVector3Make(0.0f, 0.0f, 0.9f)
velocity:GLKVector3Make(randomXVelocity, 1.0f, -1.0f)
force:GLKVector3Make(0.0f, 9.0f, 0.0f)
size:8.0f
lifeSpanSeconds:3.2f
fadeDurationSeconds:0.5f];
};
void(^blockB)(void) = ^{
self.autoSpawnDelta = 0.05f;
// 重力
self.particleEffect.gravity = GLKVector3Make(0.0f,0.5f, 0.0f);
// 一次创建多少个粒子
int n = 50;
for(int i = 0;
i < n;
i++) {
// X轴速度
float randomXVelocity = -0.1f + 0.2f *(float)random() / (float)RAND_MAX;
// Y轴速度
float randomZVelocity = 0.1f + 0.2f * (float)random() / (float)RAND_MAX;
[self.particleEffect
addParticleAtPosition:GLKVector3Make(0.0f, -0.5f, 0.0f)
velocity:GLKVector3Make(
randomXVelocity,
0.0,
randomZVelocity)
force:GLKVector3Make(0.0f, 0.0f, 0.0f)
size:16.0f
lifeSpanSeconds:2.2f
fadeDurationSeconds:3.0f];
}};
void(^blockC)(void) = ^{
self.autoSpawnDelta = 0.5f;
//重力
self.particleEffect.gravity = GLKVector3Make(0.0f, 0.0f, 0.0f);
int n = 100;
for(int i = 0;
i < n;
i++) {
//X,Y,Z速度
float randomXVelocity = -0.5f + 1.0f * (float)random() / (float)RAND_MAX;
float randomYVelocity = -0.5f + 1.0f * (float)random() / (float)RAND_MAX;
float randomZVelocity = -0.5f + 1.0f * (float)random() / (float)RAND_MAX;
//创建粒子
[self.particleEffect
addParticleAtPosition:GLKVector3Make(0.0f, 0.0f, 0.0f)
velocity:GLKVector3Make(
randomXVelocity,
randomYVelocity,
randomZVelocity)
force:GLKVector3Make(0.0f, 0.0f, 0.0f)
size:4.0f
lifeSpanSeconds:3.2f
fadeDurationSeconds:0.5f];
}};
void(^blockD)(void) = ^{
self.autoSpawnDelta = 3.2f;
// 重力
self.particleEffect.gravity = GLKVector3Make(0.0f, 0.0f, 0.0f);
int n = 100;
for(int i = 0;
i < n;
i++) {
// X,Y速度
float randomXVelocity = -0.5f + 1.0f * (float)random() / (float)RAND_MAX;
float randomYVelocity = -0.5f + 1.0f * (float)random() / (float)RAND_MAX;
// GLKVector3Normalize 计算法向量
// 计算速度与方向
GLKVector3 velocity = GLKVector3Normalize( GLKVector3Make(
randomXVelocity,
randomYVelocity,
0.0f));
[self.particleEffect
addParticleAtPosition:GLKVector3Make(0.0f, 0.0f, 0.0f)
velocity:velocity
force:GLKVector3MultiplyScalar(velocity, -1.5f)
size:4.0f
lifeSpanSeconds:3.2f
fadeDurationSeconds:0.1f];
}};
// 将4种不同效果的BLOCK块存储到数组中
self.emitterBlocks = @[[blockA copy],[blockB copy],[blockC copy],[blockD copy]];
// 纵横比
float aspect = CGRectGetWidth(self.view.bounds) / CGRectGetHeight(self.view.bounds);
// 设置投影方式\模型视图变换矩阵
[self preparePointOfViewWithAspectRatio:aspect];
}
- MVP矩阵
// MVP矩阵
- (void)preparePointOfViewWithAspectRatio:(GLfloat)aspectRatio {
// 设置透视投影方式
self.particleEffect.transform.projectionMatrix =
GLKMatrix4MakePerspective(
GLKMathDegreesToRadians(85.0f),
aspectRatio,
0.1f,
20.0f);
// 模型视图变换矩阵
// 获取世界坐标系去模型矩阵中.
/*
LKMatrix4 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坐标
第三组:就是头顶朝向的方向(因为你可以头歪着的状态看物体)
*/self.particleEffect.transform.modelviewMatrix =
GLKMatrix4MakeLookAt(
0.0, 0.0, 1.0,// Eye position
0.0, 0.0, 0.0,// Look-at position
0.0, 1.0, 0.0);
// Up direction
}
- update更新:
// 更新
- (void)update {
// 时间间隔
NSTimeInterval timeElapsed = self.timeSinceFirstResume;
/*
// 上一次更新时间
NSLog(@"timeSinceLastUpdate: %f", self.timeSinceLastUpdate);
// 上一次绘制的时间
NSLog(@"timeSinceLastDraw: %f", self.timeSinceLastDraw);
// 第一次恢复时间
NSLog(@"timeSinceFirstResume: %f", self.timeSinceFirstResume);
// 上一次恢复时间
NSLog(@"timeSinceLastResume: %f", self.timeSinceLastResume);
*/
// 消耗时间
self.particleEffect.elapsedSeconds = timeElapsed;
// 动画时间 < 当前时间与上一次更新时间
if(self.autoSpawnDelta < (timeElapsed - self.lastSpawnTime)) {
// 更新上一次更新时间
self.lastSpawnTime = timeElapsed;
// 获取当前选择的block
void(^emitterBlock)() = [self.emitterBlocks objectAtIndex: self.currentEmitterIndex];
// 执行block
emitterBlock();
}
}
- glkView:(GLKView *)view drawInRect:
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
glClearColor(0.3, 0.3, 0.3, 1);
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
// 准备绘制
[self.particleEffect prepareToDraw];
// 绘制
[self.particleEffect draw];
}
完整示例传送门 OpenGL_ES之GLSL自定义粒子效果
推荐阅读
- 2020-04-07vue中Axios的封装和API接口的管理
- iOS中的Block
- 唐嫣可真会穿,西装搭牛仔裤都能穿出高级感,一双大长腿太抢镜
- 推荐系统论文进阶|CTR预估 论文精读(十一)--Deep Interest Evolution Network(DIEN)
- 记录iOS生成分享图片的一些问题,根据UIView生成固定尺寸的分享图片
- 鹿鸣高级营养老师徐老师分享应该注意的6种食物
- Java基础-高级特性-枚举实现状态机
- 2019-08-29|2019-08-29 iOS13适配那点事
- Hacking|Hacking with iOS: SwiftUI Edition - SnowSeeker 项目(一)
- HTTP高级(Cookie,Session|HTTP高级(Cookie,Session ,LocalStorage )