iOS图像:OpenGL ES 实现粒子和滤镜效果

原创:知识探索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、粒子效果
    • 1、通过CoreAnimation实现粒子效果
    • 2、下雨的森林
    • 3、QQ点赞时的波纹效果
    • 4、天上落下红包雨
    • 5、通过OpenGL实现粒子系统
  • 二、分屏滤镜
    • 1、准备工作
    • 2、滤镜处理初始化
    • 3、滤镜动画
    • 4、着色器程序
    • 5、原图
    • 6、二分屏
    • 7、三分屏
    • 8、四分屏
    • 9、六分屏
    • 10、九分屏
  • 三、静态滤镜
    • 1、灰度滤镜
    • 2、颠倒滤镜
    • 3、漩涡滤镜
    • 4、方块马赛克滤镜
    • 5、六边形马赛克滤镜
    • 6、三角形马赛克滤镜
  • 四、动态滤镜
    • 1、缩放滤镜
    • 2、灵魂出窍滤镜
    • 3、抖动滤镜
    • 4、撕裂滤镜
    • 5、闪白滤镜
  • 五、GPUImage框架实现照片和视频滤镜
    • 1、GPUImage框架简介
    • 2、GPUImage框架实现照片滤镜
    • 3、GPUImage框架实现视频滤镜
  • Demo
  • 参考文献

一、粒子效果

1、通过CoreAnimation实现粒子效果

a、设置发射器和粒子参数
- (void)setupEmitter
{
}
❶ 创建发射器
@property (nonatomic, strong) CAEmitterLayer * colorBallLayer;

// CAEmitterLayer用来控制粒子效果
CAEmitterLayer * colorBallLayer = [CAEmitterLayer layer];
[self.view.layer addSublayer:colorBallLayer];
self.colorBallLayer = colorBallLayer;
❷ 设置发射器的参数
// 发射源的尺寸大小
colorBallLayer.emitterSize = self.view.frame.size;
// 发射源的形状:点、线、矩形框、立体矩形框、圆形、立体圆形
colorBallLayer.emitterShape = kCAEmitterLayerPoint;
// 发射模式:点、轮廓边缘、表面
colorBallLayer.emitterMode = kCAEmitterLayerPoints;
// 粒子发射形状的中心点
colorBallLayer.emitterPosition = CGPointMake(self.view.layer.bounds.size.width, 0.f);
❸ 创建发射的粒子
CAEmitterCell * colorBallCell = [CAEmitterCell emitterCell];
❹ 设置发射的粒子的参数
// 粒子名称
colorBallCell.name = @"colorBallCell";
// 粒子产生率,默认为0
colorBallCell.birthRate = 20.f;
// 粒子生命周期
colorBallCell.lifetime = 10.f;
// 粒子速度,默认为0
colorBallCell.velocity = 40.f;
// 粒子速度平均量
colorBallCell.velocityRange = 100.f;
// x,y,z方向上的加速度分量,三者默认都是0
colorBallCell.yAcceleration = 15.f;
// 指定纬度,纬度角代表了在x-z轴平面坐标系中与x轴之间的夹角,默认0
colorBallCell.emissionLongitude = M_PI; // 向左
// 发射角度范围,默认0,以锥形分布开的发射角度。角度用弧度制。粒子均匀分布在这个锥形范围内
colorBallCell.emissionRange = M_PI_4; // 围绕X轴向左90度
// 缩放比例, 默认是1
colorBallCell.scale = 0.2;
// 缩放比例范围,默认是0
colorBallCell.scaleRange = 0.1;
// 在生命周期内的缩放速度,默认是0
colorBallCell.scaleSpeed = 0.02;
// 粒子的内容,为CGImageRef的对象
colorBallCell.contents = (id)[[UIImage imageNamed:@"circle_white"] CGImage];
// 颜色
colorBallCell.color = [[UIColor colorWithRed:0.5 green:0.f blue:0.5 alpha:1.f] CGColor];
// 粒子颜色red,green,blue,alpha能改变的范围,默认0
colorBallCell.redRange = 1.f;
colorBallCell.greenRange = 1.f;
colorBallCell.alphaRange = 0.8;
// 粒子颜色red,green,blue,alpha在生命周期内的改变速度,默认都是0
colorBallCell.blueSpeed = 1.f;
colorBallCell.alphaSpeed = -0.1f;
❺ 添加粒子到图层
colorBallLayer.emitterCells = @[colorBallCell];

b、手指控制粒子的发射位置
获取手指所在点的位置
- (CGPoint)locationFromTouchEvent:(UIEvent *)event
{
    UITouch * touch = [[event allTouches] anyObject];
    return [touch locationInView:self.view];
}
手指触摸到屏幕后将发射器移动到触摸位置
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    CGPoint point = [self locationFromTouchEvent:event];
    [self setBallInPsition:point];
}
手指移动过程中发射器位置随之移动
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    CGPoint point = [self locationFromTouchEvent:event];
    [self setBallInPsition:point];
}

c、移动发射源到某个点上
- (void)setBallInPsition:(CGPoint)position
{
}
创建基础动画
CABasicAnimation * anim = [CABasicAnimation animationWithKeyPath:@"emitterCells.colorBallCell.scale"];
// fromValue
anim.fromValue = @0.2f;
// toValue
anim.toValue = @0.5f;
// duration
anim.duration = 1.f;
// 线性动画,使动画在其持续时间内均匀地发生
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
用事务包装隐式动画
[CATransaction begin];
// 设置是否禁止由于该事务组内的属性更改而触发的操作
[CATransaction setDisableActions:YES];
// 为colorBallLayer添加动画
[self.colorBallLayer addAnimation:anim forKey:nil];
// 为colorBallLayer指定位置添加动画效果
[self.colorBallLayer setValue:[NSValue valueWithCGPoint:position] forKeyPath:@"emitterPosition"];
// 提交动画
[CATransaction commit];

2、下雨的森林

a、设置发射器和粒子参数
- (void)setupEmitter
{
}
❶ 创建发射器
CAEmitterLayer * rainLayer = [CAEmitterLayer layer];
// 在背景图上添加粒子图层
[self.imageView.layer addSublayer:rainLayer];
self.rainLayer = rainLayer;
❷ 设置发射器的参数
// 发射形状为线
rainLayer.emitterShape = kCAEmitterLayerLine;
// 发射模式为表面
rainLayer.emitterMode = kCAEmitterLayerSurface;
// 发射源大小为整个屏幕
rainLayer.emitterSize = self.view.frame.size;
// 发射源位置,y最好不要设置为0,而是<0,营造黄河之水天上来的效果
rainLayer.emitterPosition = CGPointMake(self.view.bounds.size.width * 0.5, -10);
❸ 创建雨滴
CAEmitterCell * snowCell = [CAEmitterCell emitterCell];
// 粒子内容为雨滴
snowCell.contents = (id)[[UIImage imageNamed:@"rain_white"] CGImage];
❹ 设置雨滴的参数
// 每秒产生的粒子数量的系数
snowCell.birthRate = 25.f;
// 粒子的生命周期
snowCell.lifetime = 20.f;
// speed粒子速度
snowCell.speed = 10.f;
// 粒子速度系数, 默认1.0
snowCell.velocity = 10.f;
// 每个发射物体的初始平均范围,默认等于0
snowCell.velocityRange = 10.f;
// 粒子在y方向是加速的
snowCell.yAcceleration = 1000.f;
// 粒子缩放比例
snowCell.scale = 0.1;
// 粒子缩放比例范围
snowCell.scaleRange = 0.f;
❺ 将雨滴添加到图层上
rainLayer.emitterCells = @[snowCell];

b、控制天气是否下雨
if (!sender.selected)// 停止下雨
{
    sender.selected = !sender.selected;
    [self.rainLayer setValue:@0.f forKeyPath:@"birthRate"];
    
}
else// 开始下雨
{
    sender.selected = !sender.selected;
    [self.rainLayer setValue:@1.f forKeyPath:@"birthRate"];
}

c、控制天气是否下场暴雨
NSInteger rate = 1;
CGFloat scale = 0.05;

if (sender.tag == 100)
{
    NSLog(@"暴雨⛈️");
    
    if (self.rainLayer.birthRate < 30)
    {
        // birthRate变为2,scale变为1.05
        [self.rainLayer setValue:@(self.rainLayer.birthRate + rate) forKeyPath:@"birthRate"];
        [self.rainLayer setValue:@(self.rainLayer.scale + scale) forKeyPath:@"scale"];
    }
}
else if (sender.tag == 200)
{
    NSLog(@"小雨");
    
    if (self.rainLayer.birthRate > 1)
    {
        // birthRate恢复为1,scale恢复为1
        [self.rainLayer setValue:@(self.rainLayer.birthRate - rate) forKeyPath:@"birthRate"];
        [self.rainLayer setValue:@(self.rainLayer.scale - scale) forKeyPath:@"scale"];
    }
}

3、QQ点赞时的波纹效果

a、设置发射器和粒子参数
- (void)setupExplosion
{
}
设置发射器的参数
// 发射器尺寸大小
self.explosionLayer.emitterSize = CGSizeMake(self.bounds.size.width + 40, self.bounds.size.height + 40);
// 表示粒子从圆形形状发射出来
explosionLayer.emitterShape = kCAEmitterLayerCircle;
// 发射模型为轮廓模式表示从形状的边界上发射粒子
explosionLayer.emitterMode = kCAEmitterLayerOutline;
// 渲染模式
explosionLayer.renderMode = kCAEmitterLayerOldestFirst;
设置雨滴的参数
// 透明值变化速度
explosionCell.alphaSpeed = -1.f;
// 透明值范围
explosionCell.alphaRange = 0.10;
// 生命周期
explosionCell.lifetime = 1;
// 生命周期范围
explosionCell.lifetimeRange = 0.1;
// 粒子速度
explosionCell.velocity = 40.f;
// 粒子速度范围
explosionCell.velocityRange = 10.f;
// 缩放比例
explosionCell.scale = 0.08;
// 缩放比例范围
explosionCell.scaleRange = 0.02;
// 粒子图片
explosionCell.contents = (id)[[UIImage imageNamed:@"spark_red"] CGImage];

b、通过判断选中状态实现缩放和爆炸效果
- (void)setSelected:(BOOL)selected
{
    [super setSelected:selected];
    
    // 通过关键帧动画实现缩放
    CAKeyframeAnimation * animation = [CAKeyframeAnimation animation];
    // 设置动画路径
    animation.keyPath = @"transform.scale";
    ......
}
从没有点击到点击状态会有爆炸的动画效果
if (selected)
{
    animation.values = @[@1.5,@2.0, @0.8, @1.0];
    animation.duration = 0.5;
    // 计算关键帧方式
    animation.calculationMode = kCAAnimationCubic;
    // 为图层添加动画
    [self.layer addAnimation:animation forKey:nil];
    
    // 让放大动画先执行完毕,再执行爆炸动画
    [self performSelector:@selector(startAnimation) withObject:nil afterDelay:0.25];
}
从点击状态变为正常状态无动画效果
else
{
    // 如果点赞之后马上取消,那么也立马停止动画
    [self stopAnimation];
}

c、开始和结束动画
开始动画
- (void)startAnimation
{
    // 用KVC设置颗粒个数为1000
    [self.explosionLayer setValue:@1000 forKeyPath:@"emitterCells.explosionCell.birthRate"];
    
    // 开始动画
    self.explosionLayer.beginTime = CACurrentMediaTime();
    
    // 延迟停止动画
    [self performSelector:@selector(stopAnimation) withObject:nil afterDelay:0.15];
}
结束动画
- (void)stopAnimation
{
    // 用KVC设置颗粒个数为0
    [self.explosionLayer setValue:@0 forKeyPath:@"emitterCells.explosionCell.birthRate"];
    
    // 移除动画
    [self.explosionLayer removeAllAnimations];
}

4、天上落下红包雨

a、设置发射器
CAEmitterLayer * rainLayer = [CAEmitterLayer layer];
[self.view.layer addSublayer:rainLayer];
self.rainLayer = rainLayer;

rainLayer.emitterShape = kCAEmitterLayerLine;
rainLayer.emitterMode = kCAEmitterLayerSurface;
rainLayer.emitterSize = self.view.frame.size;
rainLayer.emitterPosition = CGPointMake(self.view.bounds.size.width * 0.5, -10);
b、设置金币、红包、粽子雨
CAEmitterCell * snowCell = [CAEmitterCell emitterCell];
snowCell.contents = (id)[[UIImage imageNamed:@"jinbi.png"] CGImage];
snowCell.birthRate = 1.0;
snowCell.lifetime = 30;
snowCell.speed = 2;
snowCell.velocity = 10.f;
snowCell.velocityRange = 10.f;
snowCell.yAcceleration = 60;
snowCell.scale = 0.1;
snowCell.scaleRange = 0.f;

// 红包、粽子雨同上
.......
c、将金币、红包、粽子一起添加到图层上
rainLayer.emitterCells = @[snowCell,hongbaoCell,zongziCell];

5、通过OpenGL实现粒子系统

实现起来非常复杂,感兴趣的可以自己研究。


二、分屏滤镜

1、准备工作

a、滤镜工具栏选中切换分屏选项

底部的分屏选项使用CollectionView进行实现,切换时候相当于进行了滚动。

- (void)selectIndex:(NSIndexPath *)indexPath
{
    _currentIndex = indexPath.row;
    [_collectionView reloadData];
    
    [_collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
    
    if (self.delegate && [self.delegate respondsToSelector:@selector(filterBar:didScrollToIndex:)])
    {
        [self.delegate filterBar:self didScrollToIndex:indexPath.row];
    }
}

b、使用到的属性
顶点结构体
typedef struct
{
    GLKVector3 positionCoord; //顶点坐标(X, Y, Z)
    GLKVector2 textureCoord; //纹理坐标(U, V)
} SenceVertex;
私有属性
@interface ViewController ()<FilterBarDelegate>

// 坐标
@property (nonatomic, assign) SenceVertex *vertices;
// 上下文
@property (nonatomic, strong) EAGLContext *context;
// 用于刷新屏幕
@property (nonatomic, strong) CADisplayLink *displayLink;
// 开始的时间戳
@property (nonatomic, assign) NSTimeInterval startTimeInterval;
// 着色器程序
@property (nonatomic, assign) GLuint program;
// 顶点缓存
@property (nonatomic, assign) GLuint vertexBuffer;
// 纹理 ID
@property (nonatomic, assign) GLuint textureID;

@end

c、调用到的生命周期方法
viewDidLoad
- (void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor blackColor];
    
    // 1.创建滤镜工具栏
    [self setupFilterBar];
    
    // 2.滤镜处理初始化
    [self filterInit];
    
    // 3.开始一个滤镜动画
    [self startFilerAnimation];
}
viewWillDisappear
- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    
    // 移除用于刷新屏幕的displayLink
    if (self.displayLink)
    {
        [self.displayLink invalidate];
        self.displayLink = nil;
    }
}
dealloc
- (void)dealloc
{
    // 1.上下文释放
    if ([EAGLContext currentContext] == self.context)
    {
        [EAGLContext setCurrentContext:nil];
    }
    
    // 2.顶点缓存区释放
    if (_vertexBuffer)
    {
        glDeleteBuffers(1, &_vertexBuffer);
        _vertexBuffer = 0;
    }
    
    // 3.顶点数组释放
    if (_vertices)
    {
        free(_vertices);
        _vertices = nil;
    }
}

d、创建滤镜工具栏
- (void)setupFilterBar
{
    CGFloat filterBarWidth = [UIScreen mainScreen].bounds.size.width;
    CGFloat filterBarHeight = 100;
    CGFloat filterBarY = [UIScreen mainScreen].bounds.size.height - filterBarHeight;
    FilterBar *filerBar = [[FilterBar alloc] initWithFrame:CGRectMake(0, filterBarY, filterBarWidth, filterBarHeight)];
    filerBar.delegate = self;
    [self.view addSubview:filerBar];
    
    NSArray *dataSource = @[@"原图"];
    filerBar.itemList = dataSource;
}

2、滤镜处理初始化

a、滤镜处理初始化方法
- (void)filterInit
{
}
❶ 初始化上下文并设置为当前上下文
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
[EAGLContext setCurrentContext:self.context];
❷ 开辟顶点数组内存空间
self.vertices = malloc(sizeof(SenceVertex) * 4);
❸ 初始化(0,1,2,3)4个顶点的顶点坐标以及纹理坐标
self.vertices[0] = (SenceVertex){{-1, 1, 0}, {0, 1}};
self.vertices[1] = (SenceVertex){{-1, -1, 0}, {0, 0}};
self.vertices[2] = (SenceVertex){{1, 1, 0}, {1, 1}};
self.vertices[3] = (SenceVertex){{1, -1, 0}, {1, 0}};
❹ 创建CAEAGLLayer图层
CAEAGLLayer *layer = [[CAEAGLLayer alloc] init];
// 设置图层frame
layer.frame = CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.width);
// 设置图层的scale
layer.contentsScale = [[UIScreen mainScreen] scale];
// 给View添加layer
[self.view.layer addSublayer:layer];
❺ 绑定渲染缓存区
[self bindRenderLayer:layer];
❻ 获取纹理图片
// 获取处理的图片路径
NSString *imagePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"luckcoffee.jpg"];
// 读取图片
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
// 将JPG图片转换成纹理图片
GLuint textureID = [self createTextureWithImage:image];
// 将纹理ID保存,方便后面切换滤镜的时候重用
self.textureID = textureID;
❼ 设置视口
glViewport(0, 0, self.drawableWidth, self.drawableHeight);
❽ 开辟顶点缓存区
GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
GLsizeiptr bufferSizeBytes = sizeof(SenceVertex) * 4;
glBufferData(GL_ARRAY_BUFFER, bufferSizeBytes, self.vertices, GL_STATIC_DRAW);
❾ 设置默认着色器
[self setupNormalShaderProgram];
❿ 将顶点缓存保存,退出时才释放
self.vertexBuffer = vertexBuffer;

b、绑定渲染缓存区和帧缓存区
- (void)bindRenderLayer:(CALayer <EAGLDrawable> *)layer
{
    // 渲染缓存区和帧缓存区对象
    GLuint renderBuffer;
    GLuint frameBuffer;
}

获取帧渲染缓存区名称,绑定渲染缓存区以及将渲染缓存区与layer建立连接

glGenRenderbuffers(1, &renderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];

获取帧缓存区名称,绑定帧缓存区以及将渲染缓存区附着到帧缓存区上

glGenFramebuffers(1, &frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER,
                          GL_COLOR_ATTACHMENT0,
                          GL_RENDERBUFFER,
                          renderBuffer);

c、从图片中加载纹理
- (GLuint)createTextureWithImage:(UIImage *)image
{
    ......
    // 返回纹理ID
    return textureID;
}
❶ 将UIImage转换为CGImageRef
CGImageRef cgImageRef = [image CGImage];
// 判断图片是否获取成功
if (!cgImageRef)
{
    NSLog(@"加载图片失败");
    exit(1);
}
❷ 获取图片的大小
GLuint width = (GLuint)CGImageGetWidth(cgImageRef);
GLuint height = (GLuint)CGImageGetHeight(cgImageRef);
CGRect rect = CGRectMake(0, 0, width, height);
❸ 创建上下文
  • data:指向要渲染的绘制图像的内存地址
  • width:bitmap的宽度,单位为像素
  • height:bitmap的高度,单位为像素
  • bitPerComponent:内存中像素的每个组件的位数,比如32位RGBA,就设置为8
  • bytesPerRow:bitmap的每一行的内存所占的比特数
  • colorSpace:bitmap上使用的颜色空间
// 用来获取图片字节数 宽*高*4(RGBA)
void *imageData = malloc(width * height * 4);

// 用来获取图片的颜色空间
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

CGContextRef context = CGBitmapContextCreate(imageData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
❹ 将图片翻转过来(图片默认是倒置的)
CGContextTranslateCTM(context, 0, height);
CGContextScaleCTM(context, 1.0f, -1.0f);
CGColorSpaceRelease(colorSpace);
CGContextClearRect(context, rect);
❺ 对图片进行重新绘制,得到一张新的解压缩后的位图
CGContextDrawImage(context, rect, cgImageRef);
❻ 设置图片纹理属性
GLuint textureID;
glGenTextures(1, &textureID);// 获取纹理ID
glBindTexture(GL_TEXTURE_2D, textureID);
❼ 载入纹理2D数据
  • 参数1:纹理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
  • 参数2:加载的层次,一般设置为0
  • 参数3:纹理的颜色值GL_RGBA
  • 参数4:
  • 参数5:
  • 参数6:边界宽度
  • 参数7:format
  • 参数8:type
  • 参数9:纹理数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
❽ 设置纹理属性
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
❾ 绑定纹理
  • 参数1:纹理维度
  • 参数2:纹理ID,因为只有一个纹理,给0就可以了
glBindTexture(GL_TEXTURE_2D, 0);
❿ 释放context和imageData
CGContextRelease(context);
free(imageData);

d、获取渲染缓存区的宽和高
// 宽度
- (GLint)drawableWidth
{
    GLint backingWidth;
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth);
    return backingWidth;
}

// 高度同上
.....

3、滤镜动画

a、开始一个滤镜动画
- (void)startFilerAnimation
{
}
❶ 创建之前需要先销毁旧的定时器
if (self.displayLink)
{
    [self.displayLink invalidate];
    self.displayLink = nil;
}
❷ 设置定时器调用的方法
self.startTimeInterval = 0;
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(timeAction)];
❸ 将定时器添加到runloop运行循环
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

b、实现滤镜动画重新绘制图形
- (void)timeAction
{
}
❶ 获取定时器的当前时间戳
if (self.startTimeInterval == 0)
{
    self.startTimeInterval = self.displayLink.timestamp;
}
❷ 使用着色器程序
glUseProgram(self.program);
❸ 绑定顶点缓冲区
glBindBuffer(GL_ARRAY_BUFFER, self.vertexBuffer);
❹ 传入时间
CGFloat currentTime = self.displayLink.timestamp - self.startTimeInterval;
GLuint time = glGetUniformLocation(self.program, "Time");
glUniform1f(time, currentTime);
❺ 清除画布
glClear(GL_COLOR_BUFFER_BIT);
glClearColor(1, 1, 1, 1);
❻ 重绘后渲染到屏幕上
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
[self.context presentRenderbuffer:GL_RENDERBUFFER];

c、不使用滤镜动画直接渲染场景(原图)
- (void)render
{
    // 清除画布
    glClear(GL_COLOR_BUFFER_BIT);
    glClearColor(1, 1, 1, 1);
    
    // 使用program
    glUseProgram(self.program);
    // 绑定buffer
    glBindBuffer(GL_ARRAY_BUFFER, self.vertexBuffer);
    
    // 重绘
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    // 渲染到屏幕上
    [self.context presentRenderbuffer:GL_RENDERBUFFER];
}

4、着色器程序

a、设置着色器程序
顶点着色器
attribute vec4 Position;// 顶点
attribute vec2 TextureCoords;// 纹理
varying vec2 TextureCoordsVarying;// 中间量

void main (void) {
    gl_Position = Position;
    TextureCoordsVarying = TextureCoords;
}
片段着色器
precision highp float;// 高精度
uniform sampler2D Texture;// 传进来的纹理
varying vec2 TextureCoordsVarying;// 中间量

void main (void) {
    vec4 mask = texture2D(Texture, TextureCoordsVarying);
    gl_FragColor = vec4(mask.rgb, 1.0);// 透明度强制为1.0
}
默认着色器程序
- (void)setupNormalShaderProgram
{
    // 设置着色器程序
    [self setupShaderProgramWithName:@"Normal"];
}

b、初始化着色器程序
- (void)setupShaderProgramWithName:(NSString *)name
{
}
❶ 获取编译链接后的着色器程序
// 获取编译链接后的着色器program
GLuint program = [self programWithShaderName:name];
// 使用着色器program
glUseProgram(program);
❷ 从CPU中获取顶点、纹理、纹理坐标的索引位置
GLuint positionSlot = glGetAttribLocation(program, "Position");
GLuint textureSlot = glGetUniformLocation(program, "Texture");
GLuint textureCoordsSlot = glGetAttribLocation(program, "TextureCoords");
❸ 激活纹理,绑定纹理ID
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, self.textureID);
glUniform1i(textureSlot, 0);
❹ 打开positionSlot属性并且传递数据到positionSlot中(顶点坐标)
glEnableVertexAttribArray(positionSlot);
glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, positionCoord));
❺ 打开textureCoordsSlot属性并传递数据到textureCoordsSlot(纹理坐标)
glEnableVertexAttribArray(textureCoordsSlot);
glVertexAttribPointer(textureCoordsSlot, 2, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, textureCoord));
❻ 保存program,界面销毁则释放
self.program = program;

c、链接着色器程序
- (GLuint)programWithShaderName:(NSString *)shaderName
{
    ......
    // 返回编译链接后的program
    return program;
}
❶ 编译顶点着色器/片元着色器
GLuint vertexShader = [self compileShaderWithName:shaderName type:GL_VERTEX_SHADER];
GLuint fragmentShader = [self compileShaderWithName:shaderName type:GL_FRAGMENT_SHADER];
❷ 将顶点/片元附着到program
GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
❸ 链接程序并检查是否link成功
glLinkProgram(program);
GLint linkSuccess;
glGetProgramiv(program, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE)
{
    GLchar messages[256];
    glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]);
    NSString *messageString = [NSString stringWithUTF8String:messages];
    NSAssert(NO, @"program链接失败:%@", messageString);
    exit(1);
}

d、编译着色器
- (GLuint)compileShaderWithName:(NSString *)name type:(GLenum)shaderType
{
    .......
    // 返回编译后的着色器
    return shader;
}
❶ 获取着色器路径并读取
NSString *shaderPath = [[NSBundle mainBundle] pathForResource:name ofType:shaderType == GL_VERTEX_SHADER ? @"vsh" : @"fsh"];
NSError *error;
NSString *shaderString = [NSString stringWithContentsOfFile:shaderPath encoding:NSUTF8StringEncoding error:&error];
if (!shaderString)
{
    NSAssert(NO, @"读取shader失败");
    exit(1);
}
❷ 根据shaderType创建着色器
GLuint shader = glCreateShader(shaderType);
❸ 获取shader source
const char *shaderStringUTF8 = [shaderString UTF8String];
int shaderStringLength = (int)[shaderString length];
glShaderSource(shader, 1, &shaderStringUTF8, &shaderStringLength);
❹ 编译着色器并检查是否编译成功
glCompileShader(shader);
GLint compileSuccess;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileSuccess);
if (compileSuccess == GL_FALSE)
{
    GLchar messages[256];
    glGetShaderInfoLog(shader, sizeof(messages), 0, &messages[0]);
    NSString *messageString = [NSString stringWithUTF8String:messages];
    NSAssert(NO, @"shader编译失败:%@", messageString);
    exit(1);
}

5、原图

实现委托方法
- (void)filterBar:(FilterBar *)filterBar didScrollToIndex:(NSUInteger)index
{
    // 使用默认的着色器程序
    if (index == 0)
    {
        [self setupNormalShaderProgram];
    }
   
    // 重新开始滤镜动画
    // [self startFilerAnimation];
    
    // 按照原图进行渲染
    [self render];
}

6、二分屏

a、片源着色器的映射关系
precision highp float;// 高精度
uniform sampler2D Texture;// 纹理
varying highp vec2 TextureCoordsVarying;// 中间量

void main()
{
    vec2 uv = TextureCoordsVarying.xy;// xy赋值给临时变量uv
    float y;
    // 上下分屏
    if (uv.y >= 0.0 && uv.y <= 0.5)
    {// 将纹理坐标0-0.5区间的坐标映射到0.25-0.75区间
        y = uv.y + 0.25;
    }
    else// 将纹理坐标0.5-1.0区间的坐标映射到0.25-0.75区间
    {
        y = uv.y - 0.25;
    }
    gl_FragColor = texture2D(Texture, vec2(uv.x, y));
}

b、二分屏着色器
NSArray *dataSource = @[@"原图",@"二分屏"];

- (void)setupSplitScreen_2ShaderProgram
{
    [self setupShaderProgramWithName:@"SplitScreen_2"];
}

else if (index == 1)
{
    [self setupSplitScreen_2ShaderProgram];
}

7、三分屏

a、片源着色器的映射关系
precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;

void main() {
    vec2 uv = TextureCoordsVarying.xy;
    if (uv.y < 1.0/3.0)
    {// 将0~1/3区间的图像映射到1/3~2/3
        uv.y = uv.y + 1.0/3.0;
    }
    else if (uv.y > 2.0/3.0)
    {// 将1/3~1区间的图像映射到1/3~2/3
        uv.y = uv.y - 1.0/3.0;
    }
    gl_FragColor = texture2D(Texture, uv);
}

b、三分屏着色器
NSArray *dataSource = @[@"原图",@"二分屏",@"三分屏"];

- (void)setupSplitScreen_3ShaderProgram
{
    [self setupShaderProgramWithName:@"SplitScreen_3"];
}

else if (index == 2)
{
    [self setupSplitScreen_3ShaderProgram];
}

8、四分屏

a、片源着色器的映射关系

四分屏需要将原图先缩小

precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;

void main()
{
    vec2 uv = TextureCoordsVarying.xy;
    if(uv.x <= 0.5)
    {
        uv.x = uv.x * 2.0;
    }
    else
    {
        uv.x = (uv.x - 0.5) * 2.0;
    }
    
    if (uv.y<= 0.5)
    {
        uv.y = uv.y * 2.0;
    }
    else
    {
        uv.y = (uv.y - 0.5) * 2.0;
    }
    
    gl_FragColor = texture2D(Texture, uv);
}

b、四分屏着色器
NSArray *dataSource = @[@"原图",@"二分屏",@"三分屏",@"四分屏"];

- (void)setupSplitScreen_4ShaderProgram
{
    [self setupShaderProgramWithName:@"SplitScreen_4"];
}

else if (index == 3)
{
    [self setupSplitScreen_4ShaderProgram];
}

9、六分屏

a、片源着色器的映射关系
precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;

void main()
{
    vec2 uv = TextureCoordsVarying.xy;
   
    if(uv.x <= 1.0 / 3.0)
    {
        uv.x = uv.x + 1.0/3.0;
    }
    else if(uv.x >= 2.0/3.0)
    {
        uv.x = uv.x - 1.0/3.0;
    }
    
    if(uv.y <= 0.5)
    {
        uv.y = uv.y + 0.25;
    }
    else
    {
        uv.y = uv.y - 0.25;
    }
    
    gl_FragColor = texture2D(Texture, uv);
}

b、六分屏着色器
NSArray *dataSource = @[@"原图",@"二分屏",@"三分屏",@"四分屏",@"六分屏"];

- (void)setupSplitScreen_6ShaderProgram
{
    [self setupShaderProgramWithName:@"SplitScreen_6"];
}

else if (index == 4)
{
    [self setupSplitScreen_6ShaderProgram];
}

10、九分屏

a、片源着色器的映射关系

像4分屏、9分屏这种等分的滤镜,乘以其开平方数2、3。

precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;

void main()
{
    vec2 uv = TextureCoordsVarying.xy;
    if(uv.x <= 1.0/3.0)
    {
        uv.x = uv.x * 3.0;
    }
    else if(uv.x <= 2.0/3.0)
    {
        uv.x = (uv.x - 1.0/3.0) * 3.0;
    }
    else
    {
        uv.x = (uv.x - 2.0/3.0) * 3.0;
    }
    
    if(uv.y <= 1.0/3.0)
    {
        uv.y = uv.y * 3.0;
    }
    else if(uv.y <= 2.0/3.0)
    {
        uv.y = (uv.y - 1.0/3.0) * 3.0;
    }
    else
    {
        uv.y = (uv.y - 2.0/3.0) * 3.0;
    }
    
    gl_FragColor = texture2D(Texture, uv);
}

b、九分屏着色器
NSArray *dataSource = @[@"原图",@"二分屏",@"三分屏",@"四分屏",@"六分屏",@"九分屏"];

- (void)setupSplitScreen_9ShaderProgram
{
    [self setupShaderProgramWithName:@"SplitScreen_9"];
}

else if (index == 5)
{
    [self setupSplitScreen_9ShaderProgram];
}

三、静态滤镜

1、灰度滤镜

a、片源着色器的映射关系
仅取绿色(实现简单)
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;

void main (void)
{    
    vec4 mask = texture2D(Texture, TextureCoordsVarying);
    gl_FragColor = vec4(mask.g, mask.g, mask.g, 1.0);
}
浮点算法(效果逼真)
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
const highp vec3 W = vec3(0.2125, 0.7154, 0.0721);// 权值,加起来为1

void main (void)
{    
    vec4 mask = texture2D(Texture, TextureCoordsVarying);
    float luminance = dot(mask.rgb, W);// 将RGB和权值点乘
    gl_FragColor = vec4(vec3(luminance), 1.0);
}

b、灰度滤镜着色器程序
NSArray *dataSource = @[@"原图",@"灰度滤镜"];

- (void)setupGrayShaderProgram
{
    // 设置着色器程序
    [self setupShaderProgramWithName:@"Gray"];
}

else if (index == 1)
{
    [self setupGrayShaderProgram];
}

2、颠倒滤镜

a、片源着色器的映射关系
precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;

void main (void)
{
    vec4 color = texture2D(Texture, vec2(TextureCoordsVarying.x, 1.0 - TextureCoordsVarying.y));
    gl_FragColor = color;
}

b、颠倒滤镜着色器程序
NSArray *dataSource = @[@"原图",@"灰度滤镜",@"颠倒滤镜"];

- (void)setupReversalShaderProgram
{
    // 设置着色器程序
    [self setupShaderProgramWithName:@"Reversal"];
}

else if (index == 2)
{
    [self setupReversalShaderProgram];
}

3、漩涡滤镜

a、原理

图像漩涡主要是在某个半径范围里,把当前采样点旋转一定角度,旋转以后当前点的颜色就被旋转后的点的颜色代替,因此整个半径范围里会有旋转的效果。如果旋转的时候旋转角度随着当前点离半径的距离递减,整个图像就会出现漩涡效果。这里使用的了抛物线递减因子:(1.0-(r/Radius)* (r/Radius) )


b、片源着色器的映射关系
❶ 创建使用到的变量
precision mediump float;// 中等精度

const float PI = 3.14159265;// 圆的Pi值
uniform sampler2D Texture;// 纹理采样器

const float uD = 80.0;// 旋转角度
const float uR = 0.5;// 漩涡直径的一半,用来获取漩涡半径

varying vec2 TextureCoordsVarying;// 纹理坐标
❷ 获取到对角线r
ivec2 ires = ivec2(512, 512);// 旋转范围的宽和高为[512, 512]
float Res = float(ires.y);// 获取漩涡的直径

vec2 st = TextureCoordsVarying;// 将纹理坐标赋值给临时变量
float Radius = Res * uR;// 漩涡半径 = 漩涡直径 * 0.5

vec2 xy = Res * st;// 找到纹理坐标映射到图片上的真实坐标(512,512之类的数据) = 纹理坐标(1,0之类的数据) * 直径

vec2 dxy = xy - vec2(Res/2., Res/2.);// 图片真实坐标的一半,包括x和y两个向量
float r = length(dxy);// length表示两个向量的模 x和y两个向量构成的三角形的另外一条边长,用来和漩涡半径进行比较
❸ 加剧漩涡衰减角度
// 抛物线递减因子,公式为: (1.0-(r/Radius)*(r/Radius) )
float attenValue = (1.0 -(r/Radius)*(r/Radius));

// tanθ=y/x
// 当前原始角度,倘若不累加角度,则不发生旋转:float beta = atan(dxy.y, dxy.x);
// 加剧漩涡角度指在原始的角度上再累加一个角度,这里是2倍uD160度
// 加剧漩涡角度:float beta = atan(dxy.y, dxy.x)+ radians(uD) * 2.0;
// 下面的是加剧漩涡衰减角度,多乘了一个抛物线递减因子
float beta = atan(dxy.y, dxy.x) + radians(uD) * 2.0 * attenValue;
❹ 将纹理坐标映射回来
// 只影响漩涡半径内的图形,对角线r小于漩涡半径表示图形在漩涡内
// 漩涡半径外的图形(如四周边角)保持原样
if(r <= Radius)
{
    // 漩涡的半径 + 获取映射后的纹理坐标旋转beta度后的坐标值
    xy = Res/2.0 + r * vec2(cos(beta), sin(beta));
}

// 坐标映射回来。原始纹理坐标(1,0之类的数据) = 旋转后的映射纹理坐标(512,512之类的数据)/漩涡的直径
st = xy/Res;
❺ 将计算后的颜色值填充到像素点中
// 将旋转后的纹理坐标替换原始纹理坐标,获取对应像素点的颜⾊值
vec3 irgb = texture2D(Texture, st).rgb;

// 将计算后的颜色值填充到像素点中
gl_FragColor = vec4( irgb, 1.0 );

c、漩涡滤镜着色器程序
NSArray *dataSource = @[@"原图",@"灰度滤镜",@"颠倒滤镜",@"漩涡滤镜"];

- (void)setupCirlceShaderProgram
{
    // 设置着色器程序
    [self setupShaderProgramWithName:@"Cirlce"];
}

else if (index == 3)
{
    [self setupCirlceShaderProgram];
}

4、方块马赛克滤镜

a、原理

马赛克效果就是把图片的一个相当大小的区域用同一个点的颜色来表示,可以认为是大规模的降低图像的分辨率,而让图像的一些细节隐藏起来。


b、片源着色器的映射关系
创建使用到的变量
precision mediump float;// 中等精度

varying vec2 TextureCoordsVarying;// 纹理坐标
uniform sampler2D Texture;// 纹理采样器
const vec2 TexSize = vec2(400.0, 400.0);// 纹理理图⽚大小
const vec2 mosaicSize = vec2(16.0, 16.0);// 马赛克大小
计算实际图像位置
vec2 intXY = vec2(TextureCoordsVarying.x*TexSize.x, TextureCoordsVarying.y*TexSize.y);
计算出⼀个⼩⻢赛克的坐标
// floor (x) 内建函数,返回小于/等于X的最⼤整数值
vec2 XYMosaic = vec2(floor(intXY.x/mosaicSize.x)*mosaicSize.x, floor(intXY.y/mosaicSize.y)*mosaicSize.y);
换算回纹理坐标
vec2 UVMosaic = vec2(XYMosaic.x/TexSize.x, XYMosaic.y/TexSize.y);
获取到⻢赛克后的纹理坐标的颜⾊值
vec4 color = texture2D(Texture, UVMosaic);
将⻢赛克颜色值赋值给gl_FragColor
gl_FragColor = color;

c、马赛克滤镜着色器程序
NSArray *dataSource = @[@"原图",@"灰度滤镜",@"颠倒滤镜",@"漩涡滤镜",@"马赛克滤镜"];

- (void)setupMosaicShaderProgram
{
    [self setupShaderProgramWithName:@"Mosaic"];
}

else if (index == 4)
{
    [self setupMosaicShaderProgram];
}

5、六边形马赛克滤镜

片源着色器的映射关系

比较复杂,源码见工程里HexagonMosaic.fsh文件。

马赛克滤镜着色器程序
NSArray *dataSource = @[@"原图",@"灰度滤镜",@"颠倒滤镜",@"漩涡滤镜",@"马赛克滤镜",@"六边形滤镜"];

- (void)setupHexagonMosaicShaderProgram
{
    [self setupShaderProgramWithName:@"HexagonMosaic"];
}

else if (index == 5)
{
    [self setupHexagonMosaicShaderProgram];
}

6、三角形马赛克滤镜

片源着色器的映射关系

比较复杂,源码见工程里TriangularMosaic.fsh文件。

马赛克滤镜着色器程序
NSArray *dataSource = @[@"原图",@"灰度滤镜",@"颠倒滤镜",@"漩涡滤镜",@"马赛克滤镜",@"六边形滤镜",@"三角形滤镜"];

- (void)setupTriangularMosaicShaderProgram
{
    [self setupShaderProgramWithName:@"TriangularMosaic"];
}

else if (index == 6)
{
    [self setupTriangularMosaicShaderProgram];
}

四、动态滤镜

1、缩放滤镜

a、原理

可以通过修改顶点坐标和纹理坐标的对应关系来实现。

b、顶点着色器的映射关系
使用到的变量
attribute vec4 Position;// 顶点坐标
attribute vec2 TextureCoords;// 纹理坐标
varying vec2 TextureCoordsVarying;// 用来传递的纹理坐标
uniform float Time;// 时间戳(用来及时更新画面)
const float PI = 3.1415926;// Pi值
映射计算
void main (void)
{
    float duration = 0.6;// 一次缩放效果的时长
    float maxAmplitude = 0.3;// 最大缩放幅度
    float time = mod(Time, duration);// 时间周期范围为[0.0~0.6];
    
    // 震动幅度范围为[1.0,1.3]
    float amplitude = 1.0 + maxAmplitude * abs(sin(time * (PI / duration)));
    
    // 顶点坐标x/y分别乘以放大系数[1.0,1.3]
    gl_Position = vec4(Position.x * amplitude, Position.y * amplitude, Position.zw);
   
    // 传递纹理坐标
    TextureCoordsVarying = TextureCoords;
}

c、缩放滤镜着色器程序
NSArray *dataSource = @[@"原图",@"缩放滤镜"];

- (void)setupScaleShaderProgram
{
    [self setupShaderProgramWithName:@"Scale"];
}

else if(index == 1)
{
    [self setupScaleShaderProgram];
}

2、灵魂出窍滤镜

a、原理

灵魂出窍滤镜是两个层的叠加,并且上面的那层随着时间的推移,会逐渐放大且不透明度逐渐降低。这里也用到了放大的效果,我们这次用片段着色器来实现。

b、片源着色器的映射关系
使用到的变量
precision highp float;// 高精度浮点数
uniform sampler2D Texture;// 纹理采样器
varying vec2 TextureCoordsVarying;// 纹理坐标
uniform float Time;// 时间戳
上限值
float duration = 0.7;// 一次灵魂出窍效果的时长
float maxAlpha = 0.4;// 透明度上限
float maxScale = 1.8;// 放大图片上限
获取缩放比例范围
// 当前进度值范围 [0,1] = 当前时间范围 / 总时长
float progress = mod(Time, duration) / duration;
float alpha = maxAlpha * (1.0 - progress);// 透明度范围 [0,0.4]
float scale = 1.0 + (maxScale - 1.0) * progress;// 缩放比例范围 [1.0,1.8]
根据放大比例得到放大纹理坐标
// [0,0],[0,1],[1,1],[1,0]
float weakX = 0.5 + (TextureCoordsVarying.x - 0.5) / scale;
float weakY = 0.5 + (TextureCoordsVarying.y - 0.5) / scale;
vec2 weakTextureCoords = vec2(weakX, weakY);
进行颜色混合
// 获取对应放大纹理坐标下的纹素(颜色值rgba)
vec4 weakMask = texture2D(Texture, weakTextureCoords);

// 获取原始的纹理坐标下的纹素(颜色值rgba)
vec4 mask = texture2D(Texture, TextureCoordsVarying);

// 下面是默认颜色混合方程式
gl_FragColor = mask * (1.0 - alpha) + weakMask * alpha;

c、灵魂出窍滤镜着色器程序
NSArray *dataSource = @[@"原图",@"缩放滤镜",@"灵魂出窍"];

- (void)setupSoulOutShaderProgram
{
    [self setupShaderProgramWithName:@"SoulOut"];  
}

else if(index == 2)
{
    [self setupSoulOutShaderProgram];
}

3、抖动滤镜

a、原理

抖动滤镜 = 颜色偏移 + 微弱的放大效果

b、片源着色器的映射关系
使用到的变量
precision highp float;// 高精度浮点数
uniform sampler2D Texture;// 纹理
varying vec2 TextureCoordsVarying;// 纹理坐标
uniform float Time;// 时间戳
上限值
float duration = 0.7;// 一次抖动滤镜的时长
float maxScale = 1.1;// 放大图片上限
float offset = 0.02;// 颜色偏移步长
缩放范围
// 进度范围为[0,1]
float progress = mod(Time, duration) / duration;
// 颜色偏移值范围为[0,0.02]
vec2 offsetCoords = vec2(offset, offset) * progress;
// 缩放范围为[1.0-1.1];
float scale = 1.0 + (maxScale - 1.0) * progress;
放大纹理坐标
vec2 ScaleTextureCoords = vec2(0.5, 0.5) + (TextureCoordsVarying - vec2(0.5, 0.5)) / scale;
获取3组颜色rgb
// 原始颜色+offsetCoords
vec4 maskR = texture2D(Texture, ScaleTextureCoords + offsetCoords);
// 原始颜色-offsetCoords
vec4 maskB = texture2D(Texture, ScaleTextureCoords - offsetCoords);
// 原始颜色
vec4 mask = texture2D(Texture, ScaleTextureCoords);
传递颜色值
// mask.a 获取原图的透明度
gl_FragColor = vec4(maskR.r, mask.g, maskB.b, mask.a);

c、灵魂出窍滤镜着色器程序
NSArray *dataSource = @[@"原图",@"缩放滤镜",@"灵魂出窍",@"抖动滤镜"];

- (void)setupShakeShaderProgram
{
    [self setupShaderProgramWithName:@"Shake"];
}

else if(index == 3)
{
    [self setupShakeShaderProgram];
}

4、撕裂滤镜

a、原理

我们让每一行像素随机偏移 -1 ~ 1 的距离(这里的 -1 ~ 1 是对于纹理坐标来说的),但是如果整个画面都偏移比较大的值,那我们可能都看不出原来图像的样子。所以我们的逻辑是,设定一个阈值,小于这个阈值才进行偏移,超过这个阈值则乘上一个缩小系数。则最终呈现的效果是:绝大部分的行都会进行微小的偏移,只有少量的行会进行较大偏移。

b、片源着色器的映射关系
使用到的变量
precision highp float;
uniform sampler2D Texture;// 纹理
varying vec2 TextureCoordsVarying;// 纹理坐标
uniform float Time;// 时间戳
const float PI = 3.1415926;// Pi
随机数
float rand(float n)
{
    // fract(x)用来返回x的小数部分数据
    return fract(sin(n) * 43758.5453123);
}
上限值
float maxJitter = 0.06;// 最大抖动阀值
float duration = 0.3;// 一次毛刺滤镜的时长
float colorROffset = 0.01;// 红色颜色偏移量
float colorBOffset = -0.025;// 绿色颜色偏移量
是否要做偏移
// 时间周期为[0.0,0.6]
float time = mod(Time, duration * 2.0);
// 振幅为[0,1]
float amplitude = max(sin(time * (PI / duration)), 0.0);
// 像素随机偏移为[-1,1]
float jitter = rand(TextureCoordsVarying.y) * 2.0 - 1.0;
// 是否要做偏移
bool needOffset = abs(jitter) < maxJitter * amplitude;
获取纹理坐标X值
// 根据needOffset来计算X轴方向上的撕裂
// needOffset = YES 撕裂较大 NO 撕裂较小
float textureX = TextureCoordsVarying.x + (needOffset ? jitter : (jitter * amplitude * 0.006));
撕裂后的纹理坐标x,y
vec2 textureCoords = vec2(textureX, TextureCoordsVarying.y);
对撕裂后的纹理颜色进行偏移
// 红色/蓝色部分发生撕裂
vec4 mask = texture2D(Texture, textureCoords);
// 红色偏移
vec4 maskR = texture2D(Texture, textureCoords + vec2(colorROffset * amplitude, 0.0));
// 蓝色偏移
vec4 maskB = texture2D(Texture, textureCoords + vec2(colorBOffset * amplitude, 0.0));
传递颜色值
gl_FragColor = vec4(maskR.r, mask.g, maskB.b, mask.a);

c、撕裂滤镜着色器程序
NSArray *dataSource = @[@"原图",@"缩放滤镜",@"灵魂出窍",@"抖动滤镜",@"撕裂滤镜"];

- (void)setupGitchShaderProgram
{
    [self setupShaderProgramWithName:@"Glitch"];
}

else if(index == 4)
{
    [self setupGitchShaderProgram];
}

5、闪白滤镜

a、原理

添加白色图层,白色图层的透明度随着时间变化。

b、片源着色器的映射关系
使用到的变量
precision highp float;
uniform sampler2D Texture;// 纹理采样器
varying vec2 TextureCoordsVarying;// 纹理坐标
uniform float Time;// 时间戳
const float PI = 3.1415926;// Pi
纹理计算
void main (void)
{
    float duration = 0.6;// 一次闪白滤镜的时长
    
    // 表示时间周期
    float time = mod(Time, duration);
    
    // 白色颜色遮罩层
    vec4 whiteMask = vec4(1.0, 1.0, 1.0, 1.0);
    
    // 振幅范围 (0.0,1.0)
    float amplitude = abs(sin(time * (PI / duration)));
    
    // 纹理坐标对应的纹素(RGBA)
    vec4 mask = texture2D(Texture, TextureCoordsVarying);
    
    // 利用混合方程式来进行混合:白色图层 + 原始纹理图片颜色 
    gl_FragColor = mask * (1.0 - amplitude) + whiteMask * amplitude;
}

c、闪白滤镜着色器程序
NSArray *dataSource = @[@"原图",@"缩放滤镜",@"灵魂出窍",@"抖动滤镜",@"撕裂滤镜",@"闪白滤镜"];

- (void)setupShineWhiteShaderProgram
{
    [self setupShaderProgramWithName:@"ShineWhite"];
}

else if(index == 5)
{
    [self setupShineWhiteShaderProgram];
}

五、GPUImage框架实现照片和视频滤镜

1、GPUImage框架简介

GPU的工作原理是CPU指定显示器工作,显示控制器根据CPU的控制到指定的地方去取数据和指令,目前的数据般是从显存里取,如果显存里存不下,则从内存里取,内存也放不下,则从硬盘里取。

GPUImage是一套主流的图像处理框架, 很多直播、美图APP都采用此技术,你的项目业务可能需要决定使用GPUImage还是Core Image,它们都是相当成熟的工具。

a、GPUImage框架的功能
  • 丰富的输入组件:摄像头、图片、视频、OpenGL 纹理、二进制数 据、UIElement (UlViewCAL ayer)
  • 大量现成的内置滤镜(4大类)
  • 颜色类(亮度、 色度、饱和度、对比度、曲线、白平衡......)。
  • 图像类(仿射变换、裁剪、高斯模糊、毛玻璃效果......)
  • 颜色混合类(差异混合、alpha混合、 遮罩混合......)
  • 效果类(像素化、素描效果、压花效果、球形玻璃效果......)
  • 丰富的输出组件:UIView 、视频文件、GPU纹理、二进制数据
  • 灵活的滤镜链:滤镜效果之间可以相互串联、并联,调用管理相当灵活。
  • 接口易用。滤镜和OpenGL资源的创建及使用都做了统一的封装,简单易用, 并且内置了一个cache模块实现了framebuffer的复用。
  • 线程管理。OpenGL Context不是多线程安全的,GPUImage 创建了专门的contextQueue ,所有的滤镜都会扔到统一的线程中处理。
  • 轻松实现自定义滤镜效果。继承GPUImageFilter自动获得上面全部特性,无需关注上下文的环境搭建,专注于效果的核心算法实现即可。

b、Core Image 的优势
  • 官方框架,使用放心,维护方便。
  • 支持CPU 渲染,可以在后台继续处理和保存图片。
  • 一些滤镜的性能更强劲。 例如由Metal Performance Shaders 支持的模糊滤镜等。
  • 支持使用 Metal渲染图像,而Metal 在i0S 平台上有更好的表现。 与MetalSpriteKitSceneKitCore Animation 等更完美的配合。
  • 支持图像识别功能。包括人脸识别、条形码识别、文本识别等。
  • 支持自动增强图像效果,会分析图像的直方图,图像属性,脸部区域,然后通过一组滤镜来改善图像效果。
  • 支持对原生 RAW 格式图片的处理。
  • 滤镜链的性能比GPUImage高。
  • 支持对大图进行处理,超过GPU纹理限制(4096 * 4096)的时候,会自动拆分成几个小块处理(Automatic tiling)。GPUlmage 当处理超过纹理限制的图像时候,会先做判断,压缩成最大纹理限制的图像,导致图像质量损失。

c、GPUImage框架的层级结构
  • GLProgram:管理着色器shader
  • GPUImageContext:管理上下文 OpenGL Context
  • GPUImageFrameBuffer:管理缓冲区 buffer
  • Sources:数据源头
  • Pipeline:处理管道
  • Filters:滤镜组合
  • Outputs:输出组件

d、滤镜链起点
  • GPUImagePicture:用来处理静态图片。本质解压图片->纹理->用滤镜来进行处理
  • GPUImageRawDataInput:二进制数据->纹理图片CVPixelFormat
  • GPUImageTextureInput:用纹理数据
  • GPUImageUIElement:UlView/CALayer->图像数据 -> 纹理
  • GPUImageMovie:

e、滤镜终点
  • GPUImageMoviewWriter:AVAssetWriter把每一帧纹理的数据从帧缓存区-> 指定文件
  • GPUImageRawData0utput:处理滤镜帧缓存区的二进制数据
  • GPUlmageTextureOutput:
  • GPUlmageView:

f、滤镜的实现原理

GPUlmageCorelmage已经帮我们实现了将近200个滤镜效果。GPUlmage库中提供的大部分滤镜都是通过片段着色器的一系列操作来实现相应的效果。大部分滤镜都是对图片中像素进行计算产生新的像素颜色处理。滤镜的关键在片元着色器。滤镜处理的原理就是把静态图片或者视频的每一帧进行图形变换后再显示到屏幕上,其本质就是像素点的坐标和颜色的变化。


g、OpenGL ES处理图片的步骤
  • 初始化OpenGL ES环境,编译,链接到顶点着色器和片元着色器
  • 缓存顶点、纹理坐标数据,传送图像数据到GPU
  • 绘制图元到特定的帧缓存区
  • 在帧缓存区绘制图像

h、GPUlmage框架解析

GPUImage框架采用的链式( Chain )结构。主要有一个GPUImageOutput interfaceGPUImageInput protocol串联起来。GPUImage0utput负责输出纹理TextureGPUImageInput负责输入纹理Texture

整个链式图像数据过程,纹理作为核心载体。当然GPUImage不仅仅适用于静态图片,还适用视频、实时拍摄等。这些不同的载体都是继承于GPUImageOutput类。基本上每一个滤镜都是继承自GPUImageFilter,而GPUImageFilter是整套框架的核心。

接收一个GPUImageFrameBuffer输入,调用GLProgram渲染处理之后,输出一个GPUImageFrameBuffer,再把输出的GPUImageFrameBuffer传给通过targets属性关联的下一级滤镜,直到传递给最终的输出组件。


2、GPUImage框架实现照片滤镜

a、导入框架
#import <GPUImage.h>
b、设置滤镜

选择合适的滤镜,这里使用图像的饱和度滤镜

if (_disFilter == nil)
{
    _disFilter = [[GPUImageSaturationFilter alloc] init];
}

设置饱和度值,范围为 0.0 - 2.0,默认为1.0

_disFilter.saturation = 1.0;

设置要渲染的区域,这里设置为图片大小

[_disFilter forceProcessingAtSize:_luckinCoffeeImage.size];

使用单个滤镜

[_disFilter useNextFrameForImageCapture];

调整饱和度,用户通过拖动屏幕上的滑条进行控制

_disFilter.saturation = sender.value;

c、设置静态图片

数据源头是一张静态图片

GPUImagePicture *stillImageSoucer = [[GPUImagePicture alloc] initWithImage:_luckinCoffeeImage];

为图片添加一个滤镜

[stillImageSoucer addTarget:_disFilter];

处理图片

[stillImageSoucer processImage];

d、更新图片

处理完成后从帧缓存区中获取新图片

UIImage *newImage = [_disFilter imageFromCurrentFramebuffer];

及时更新屏幕上的图片

_imageView.image = newImage;

3、GPUImage框架实现视频滤镜

a、ViewController
❶ 导入框架和使用到的属性
#import <AVKit/AVKit.h>

// 文件存储路径
@property (nonatomic, copy) NSString *filePath;
// 视频播放视图
@property (nonatomic,strong) UIView *videoView;
// 播放器
@property (nonatomic,strong) AVPlayerViewController *player;
// 视频录制管理者
@property (nonatomic,strong) VideoManager *manager;
❷ 控制按钮

创建视频录制管理者

- (void)setupVideoMananger
{
    _manager = [[VideoManager alloc] init];
    _manager.delegate = self;
    [_manager showWithFrame:CGRectMake(20, 120, kScreenWidth-40, kScreenHeight/2-1) superView:self.view];
    _manager.maxTime = 30.0;
}

开始录制视频

- (void)startRecord
{
    [_manager startRecording];
}

结束录制视频

- (void)endRecordd
{
    [_manager endRecording];
}

播放录制完成的视频

- (void)playVideo
{
    _player = [[AVPlayerViewController alloc] init];
    _player.player = [[AVPlayer alloc] initWithURL:[NSURL fileURLWithPath:_filePath]];
    _player.videoGravity = AVLayerVideoGravityResizeAspect;
    [self presentViewController:_player animated:NO completion:nil];
}
❸ 视频录制的委托方法

开始录制

- (void)didStartRecordVideo
{
    [self.view addSubview:[NoticeView message:@"开始录制..." delaySecond:2]];
}

视频压缩

- (void)didCompressingVideo
{
    [self.view addSubview:[NoticeView message:@"视频压缩中..." delaySecond:2]];
}

录制完毕

- (void)didEndRecordVideoWithTime:(CGFloat)totalTime outputFile:(NSString *)filePath
{
    _filePath = filePath;
    NSLog(@"文件路径为:%@",filePath);
    [self.view addSubview:[NoticeView message:[NSString stringWithFormat:@"视频录制完毕,时长: %f",totalTime] delaySecond:4]];
}

b、VideoManager 工具类提供的属性和方法
导入框架
#import <GPUImage.h>
协议方法
@protocol VideoManagerProtocol <NSObject>

/** 开始录制 */
- (void)didStartRecordVideo;

/** 视频压缩中 */
- (void)didCompressingVideo;

/** 结束录制 */
- (void)didEndRecordVideoWithTime:(CGFloat)totalTime outputFile:(NSString *)filePath;

@end
提供的属性
typedef NS_ENUM(NSUInteger, VideoManagerCameraType) {
    VideoManagerCameraTypeFront = 0,
    VideoManagerCameraTypeBack,
};

/** 代理 */
@property (nonatomic,weak) id <VideoManagerProtocol> delegate;

/** 录制视频区域 */
@property (nonatomic,assign) CGRect frame;

/** 录制视频最大时长 */
@property (nonatomic,assign) CGFloat maxTime;
提供的方法
/** 录制视频单例,若工程中不止一处用到录视频,尺寸有变,直接实例化即可 忽略此方法 */
+ (instancetype)manager;

/** 加载到显示的视图上 */
- (void)showWithFrame:(CGRect)frame superView:(UIView *)superView;

/** 开始录制 */
- (void)startRecording;

/** 结束录制 */
- (void)endRecording;

/** 暂停录制 */
- (void)pauseRecording;

/** 继续录制 */
- (void)resumeRecording;

/** 切换前后摄像头 */
- (void)changeCameraPosition:(VideoManagerCameraType)type;

/** 打开闪光灯 */
- (void)turnTorchOn:(BOOL)on;

c、VideoManager 工具类使用到的私有属性
压缩文件存储路径
#define COMPRESSEDVIDEOPATH [NSHomeDirectory() stringByAppendingFormat:@"/Documents/CompressionVideoField"]
接受委托
@interface VideoManager ()<GPUImageVideoCameraDelegate,UIImagePickerControllerDelegate,UINavigationControllerDelegate>
@end
使用到的私有属性
@property (nonatomic,strong) GPUImageVideoCamera *videoCamera;// 摄像头
@property (nonatomic,strong) GPUImageSaturationFilter *saturationFilter;// 饱和度滤镜
@property (nonatomic,strong) GPUImageView *displayView;// 视频输出视图
@property (nonatomic,strong) GPUImageMovieWriter *movieWriter;// 视频写入
@property (nonatomic,strong) NSURL *movieURL;// 视频写入的地址URL
@property (nonatomic,copy) NSString *moviePath;// 视频写入路径
@property (nonatomic,copy) NSString *resultPath;// 压缩成功后的视频路径
@property (nonatomic,assign) int seconds;// 视频时长
@property (nonatomic,strong) NSTimer *timer;// 系统计时器
@property (nonatomic,assign) int recordSecond;// 计时器常量
饱和度滤镜的懒加载方法中控制了饱和度
- (GPUImageSaturationFilter *)saturationFilter
{
    if (_saturationFilter == nil)
    {
        _saturationFilter = [[GPUImageSaturationFilter alloc] init];
    }
    _saturationFilter.saturation = 0.1;
    return _saturationFilter;
}

d、VideoManager 工具类开始录制视频的方法
- (void)startRecording
{
}
❶ 获取录制路径
NSString *defultPath = [self getVideoPathCache];
self.moviePath = [defultPath stringByAppendingPathComponent:[self getVideoNameWithType:@"mp4"]];
self.movieURL = [NSURL fileURLWithPath:self.moviePath];
unlink([self.moviePath UTF8String]);// 这步的作用我也挺迷糊的
❷ 视频写入
self.movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:self.movieURL size:CGSizeMake(480.0, 640.0)];
self.movieWriter.encodingLiveVideo = YES;// 要对视频进行编码压缩
self.movieWriter.shouldPassthroughAudio = YES;// 支持录制视频的是你传入音频
self.videoCamera.audioEncodingTarget = self.movieWriter;// 要对音频进行编码压缩
❸ 添加饱和度滤镜
[self.saturationFilter addTarget:self.movieWriter];
❹ 开始录制
[self.movieWriter startRecording];
if (self.delegate && [self.delegate respondsToSelector:@selector(didStartRecordVideo)])
{
    [self.delegate didStartRecordVideo];
}
❺ 开启计时器
[self.timer setFireDate:[NSDate distantPast]];
[self.timer fire];

e、VideoManager 工具类结束录制视频的方法
❶ 获取录制路径
[self.timer invalidate];
self.timer = nil;
❷ 完成录制
[self.movieWriter finishRecording];
❸ 移除饱和度滤镜和音频编码的对象
[self.saturationFilter removeTarget:self.movieWriter];
self.videoCamera.audioEncodingTarget = nil;
❹ 录制时间和录制视频最大时长进行比较
if (self.recordSecond > self.maxTime)
{
    NSLog(@"清除录制的视频");
    
}
else
{
    ....
}
❺ 进行压缩
if ([self.delegate respondsToSelector:@selector(didCompressingVideo)])
{
    [self.delegate didCompressingVideo];
}

[self compressVideoWithUrl:self.movieURL compressionType:AVAssetExportPresetMediumQuality filePath:^(NSString *resultPath, float memorySize, NSString *videoImagePath, int seconds) {
    ....
}];
❻ 压缩完后在回调方法中传入压缩文件的数据和耗时
NSData *data = [NSData dataWithContentsOfFile:resultPath];
CGFloat totalTime = (CGFloat)data.length / 1024 / 1024;

if ([weakSelf.delegate respondsToSelector:@selector(didEndRecordVideoWithTime:outputFile:)])
{
    [weakSelf.delegate didEndRecordVideoWithTime:totalTime outputFile:resultPath];
}
❼ 超过最大录制时长结束录制
- (void)updateWithTime
{
    self.recordSecond++;
    if (self.recordSecond >= self.maxTime)
    {
        [self endRecording];
    }
}

f、暂停和恢复录制视频
暂停录制
- (void)pauseRecording
{
    if ([_videoCamera isRunning])
    {
        [self.timer invalidate];
        self.timer = nil;
        
        [_videoCamera pauseCameraCapture];
    }
}
恢复录制
- (void)resumeRecording
{
    [_videoCamera resumeCameraCapture];
    [self.timer setFireDate:[NSDate distantPast]];
    [self.timer fire];
}

g、压缩视频
- (void)compressVideoWithUrl:(NSURL *)url compressionType:(NSString *)type filePath:(void(^)(NSString *resultPath,float memorySize,NSString * videoImagePath,int seconds))resultBlock
{
}
❶ 获取视频压缩前大小
NSData *data = [NSData dataWithContentsOfURL:url];
CGFloat totalSize = (float)data.length / 1024 / 1024;
NSLog(@"压缩前大小:%.2fM",totalSize);
❷ 获取视频时长
AVURLAsset *avAsset = [AVURLAsset URLAssetWithURL:url options:nil];
CMTime time = [avAsset duration];
int seconds = ceil(time.value / time.timescale);
❸ 输出中等质量压缩视频
NSArray *compatiblePresets = [AVAssetExportSession exportPresetsCompatibleWithAsset:avAsset];
if ([compatiblePresets containsObject:type])
{
    // 1.输出中等质量压缩视频
    AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:avAsset presetName:AVAssetExportPresetMediumQuality];
    ......
}
❹ 用时间给文件命名 防止存储被覆盖
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-MM-dd-HH:mm:ss"];
❺ 若压缩路径不存在重新创建
NSFileManager *manager = [NSFileManager defaultManager];
BOOL isExist = [manager fileExistsAtPath:COMPRESSEDVIDEOPATH];
if (!isExist)
{
    [manager createDirectoryAtPath:COMPRESSEDVIDEOPATH withIntermediateDirectories:YES attributes:nil error:nil];
}
❻ 配置输出文件
resultPath = [COMPRESSEDVIDEOPATH stringByAppendingPathComponent:[NSString stringWithFormat:@"user%outputVideo-%@.mp4",arc4random_uniform(10000),[formatter stringFromDate:[NSDate date]]]];
session.outputURL = [NSURL fileURLWithPath:resultPath];
session.outputFileType = AVFileTypeMPEG4;
session.shouldOptimizeForNetworkUse = YES;
❼ 压缩过程中的操作回调
[session exportAsynchronouslyWithCompletionHandler:^{
    switch (session.status)
    {
        case AVAssetExportSessionStatusUnknown:
            break;
        case AVAssetExportSessionStatusWaiting:
            break;
        case AVAssetExportSessionStatusExporting:
            break;
        case AVAssetExportSessionStatusCancelled:
            break;
        case AVAssetExportSessionStatusFailed:
            break;
        case AVAssetExportSessionStatusCompleted:
        {
            .....
        }
        default:
            break;
    }
}];
❽ 打印压缩后的视频大小
NSData *data = [NSData dataWithContentsOfFile:resultPath];
float compressedSize = (float)data.length / 1024 / 1024;
resultBlock(resultPath,compressedSize,@"",seconds);
NSLog(@"压缩后大小:%.2f",compressedSize);

h、加载到显示的视图上
- (void)showWithFrame:(CGRect)frame superView:(UIView *)superView
{
    _frame = frame;
     
    // 将滤镜添加到展示视图上
    [self.saturationFilter addTarget:self.displayView];
    // 为摄像机添加滤镜
    [self.videoCamera addTarget:self.saturationFilter];
    // 将展示视图添加到屏幕视图上
    [superView addSubview:self.displayView];
    // 摄像机开始捕捉视频
    [self.videoCamera startCameraCapture];
}

I、摄像头
手电筒开关
- (void)turnTorchOn:(BOOL)on
{
    if ([_videoCamera.inputCamera hasTorch] && [_videoCamera.inputCamera hasFlash])
    {
        [_videoCamera.inputCamera lockForConfiguration:nil];
        if (on)
        {
            [_videoCamera.inputCamera setTorchMode:AVCaptureTorchModeOn];
            [_videoCamera.inputCamera setFlashMode:AVCaptureFlashModeOn];
        }
        else
        {
            [_videoCamera.inputCamera setTorchMode:AVCaptureTorchModeOff];
            [_videoCamera.inputCamera setFlashMode:AVCaptureFlashModeOff];
        }
        [_videoCamera.inputCamera unlockForConfiguration];
    }
}
切换前后摄像头
- (void)changeCameraPosition:(VideoManagerCameraType)type
{
    switch (type)
    {
        case VideoManagerCameraTypeFront:
        {
            [_videoCamera rotateCamera];
        }
            break;
        case VideoManagerCameraTypeBack:
        {
            [_videoCamera rotateCamera];
        }
            break;
        default:
            break;
    }
}

Demo

Demo在我的Github上,欢迎下载。
Multi-MediaDemo

参考文献

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

推荐阅读更多精彩内容