实现GPUImage【OpenGL渲染原理】

本篇主要讲解GPUImage底层是如何渲染的,GPUImage底层使用的是OPENGL,操控GPU来实现屏幕展示

一、由于网上OpenGL实战资料特别少,官方文档对一些方法也是解释不清楚,我通过查找一些博客,文档整理相关的知识如下,有些不到位的地方希望大家指教,我虚心学习

二、GPUImageVideoCamera

可以捕获采集的视频数据

关键是捕获到一帧一帧视频数据如何展示?

通过这个方法可以获取采集的视频数据

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection

采集视频注意点:要设置采集竖屏,否则获取的数据是横屏

通过AVCaptureConnection就可以设置

[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];

三、自定义OpenGLView渲染视频  //以下的一些采集原理GPUImage框架GPUImageMovieWriter基本封装的比较好了(直接查看GPUImageMovieWriter方法也行),下面是讲的是采集到视频的帧数据 渲染实现的原理和步骤

暴露一个接口,获取采集到的帧数据,然后把帧数据传递给渲染View,展示出来

- (void)displayFramebuffer:(CMSampleBufferRef)sampleBuffer;

四、利用OpenGL渲染帧数据并显示

导入头文件#import ,GLKit.h底层使用了OpenGLES,导入它,相当于自动导入了OpenGLES

步骤

01-自定义图层类型

02-初始化CAEAGLLayer图层属性(可以参考GPUImageView里面的内容)

03-创建EAGLContext

04-创建渲染缓冲区

05-创建帧缓冲区

06-创建着色器

07-创建着色器程序

08-创建纹理对象

09-YUV转RGB绘制纹理

10-渲染缓冲区到屏幕

11-清理内存

01-自定义图层类型

为什么要自定义图层类型CAEAGLLayer? CAEAGLLayer是OpenGL专门用来渲染的图层,使用OpenGL必须使用这个图层

#pragma mark - 1.自定义图层类型

+ (Class)layerClass

{

return [CAEAGLLayer class];

}

02-初始化CAEAGLLayer图层属性

1.不透明度(opaque)=YES,CALayer默认是透明的,透明性能不好,最好设置为不透明.

2.设置绘图属性

kEAGLDrawablePropertyRetainedBacking :NO (告诉CoreAnimation不要试图保留任何以前绘制的图像留作以后重用)

kEAGLDrawablePropertyColorFormat :kEAGLColorFormatRGBA8 (告诉CoreAnimation用8位来保存RGBA的值)

其实设置不设置都无所谓,默认也是这个值,只不过GPUImage设置了

#pragma mark - 2.初始化图层

- (void)setupLayer

{

CAEAGLLayer *openGLLayer = (CAEAGLLayer *)self.layer;

_openGLLayer = openGLLayer;

// 设置不透明,CALayer 默认是透明的,透明性能不好,最好设置为不透明.

openGLLayer.opaque = YES;

// 设置绘图属性drawableProperties

// kEAGLColorFormatRGBA8 : red、green、blue、alpha共8位

openGLLayer.drawableProperties = @{

kEAGLDrawablePropertyRetainedBacking :[NSNumber numberWithBool:NO],

kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8

};

}

03-创建EAGLContext

需要将它设置为当前context,所有的OpenGL ES渲染默认渲染到当前上下文

EAGLContext管理所有使用OpenGL ES进行描绘的状态,命令以及资源信息,要绘制东西,必须要有上下文,跟图形上下文类似。

当你创建一个EAGLContext,你要声明你要用哪个version的API。这里,我们选择OpenGL ES 2.0

#pragma mark - 3、创建OpenGL上下文,并且设置上下文

- (void)setupContext

{

// 指定OpenGL 渲染 API 的版本,目前都使用 OpenGL ES 2.0

EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;

// 创建EAGLContext上下文

_context = [[EAGLContext alloc] initWithAPI:api];

// 设置为当前上下文,所有的渲染默认渲染到当前上下文

[EAGLContext setCurrentContext:_context];

}

04-创建渲染缓冲区

有了上下文,openGL还需要在一块buffer进行描绘,这块buffer就是RenderBuffer

OpenGLES 总共有三大不同用途的color buffer,depth buffer 和 stencil buffer.

最基本的是color buffer,创建它就好了

函数glGenRenderbuffers

函数 void glGenRenderbuffers (GLsizei n, GLuint* renderbuffers)

它是为renderbuffer(渲染缓存)申请一个id(名字),创建渲染缓存

参数n表示申请生成renderbuffer的个数

参数renderbuffers返回分配给renderbuffer(渲染缓存)的id

。 注意:返回的id不会为0,id 0 是OpenGL ES保留的,我们也不能使用id 为0的renderbuffer(渲染缓存)。

函数glBindRenderbuffer

void glBindRenderbuffer (GLenum target, GLuint renderbuffer)

告诉OpenGL:我在后面引用GL_RENDERBUFFER的地方,其实是引用_colorRenderBuffer

参数target必须为GL_RENDERBUFFER

参数renderbuffer就是使用glGenRenderbuffers生成的id

。 当指定id的renderbuffer第一次被设置为当前renderbuffer时,会初始化该 renderbuffer对象,其初始值为:

width 和 height:像素单位的宽和高,默认值为0;

internal format:内部格式,三大 buffer 格式之一 -- color,depth or stencil;

Color bit-depth:仅当内部格式为 color 时,设置颜色的 bit-depth,默认值为0;

Depth bit-depth:仅当内部格式为 depth时,默认值为0;

Stencil bit-depth: 仅当内部格式为 stencil,默认值为0

函数renderbufferStorage

EAGLContext方法 - (BOOL)renderbufferStorage:(NSUInteger)target fromDrawable:(id)drawable

把渲染缓存(renderbuffer)绑定到渲染图层(CAEAGLLayer)上,并为它分配一个共享内存。

参数target,为哪个renderbuffer分配存储空间

参数drawable,绑定在哪个渲染图层,会根据渲染图层里的绘图属性生成共享内存。

//    底层调用这个分配内存

glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, _openGLLayer.bounds.size.width, _openGLLayer.bounds.size.height);

实战代码

#pragma mark - 4、创建渲染缓存

- (void)setupRenderBuffer

{

glGenRenderbuffers(1, &_colorRenderBuffer);

glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderBuffer);

// 把渲染缓存绑定到渲染图层上CAEAGLLayer,并为它分配一个共享内存。

// 并且会设置渲染缓存的格式,和宽度

[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_openGLLayer];

}

05-创建帧缓冲区

它相当于buffer(color, depth, stencil)的管理者,三大buffer可以附加到一个framebuffer上

本质是把framebuffer内容渲染到屏幕

函数glFramebufferRenderbuffer

void glFramebufferRenderbuffer (GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer)

该函数是将相关buffer()三大buffer之一)attach到framebuffer上,就会自动把渲染缓存的内容填充到帧缓存,在由帧缓存渲染到屏幕

参数target,哪个帧缓存

参数attachment是指定renderbuffer被装配到那个装配点上,其值是GL_COLOR_ATTACHMENT0, GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT中的一个,分别对应 color,depth和 stencil三大buffer。

renderbuffertarget:哪个渲染缓存

renderbuffer渲染缓存id

#pragma mark - 5、创建帧缓冲区

- (void)setupFrameBuffer

{

glGenFramebuffers(1, &_framebuffers);

glBindFramebuffer(GL_FRAMEBUFFER, _framebuffers);

// 把颜色渲染缓存 添加到 帧缓存的GL_COLOR_ATTACHMENT0上,就会自动把渲染缓存的内容填充到帧缓存,在由帧缓存渲染到屏幕

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderBuffer);

}

06-创建着色器

着色器

什么是着色器? 通常用来处理纹理对象,并且把处理好的纹理对象渲染到帧缓存上,从而显示到屏幕上。

提取纹理信息,可以处理顶点坐标空间转换,纹理色彩度调整(滤镜效果)等操作。

着色器分为顶点着色器,片段着色器

顶点着色器用来确定图形形状

片段着色器用来确定图形渲染颜色

步骤: 1.编辑着色器代码 2.创建着色器 3.编译着色器

只要创建一次,可以在一开始的时候创建

着色器代码

// 顶点着色器代码

NSString *const kVertexShaderString = SHADER_STRING

(

attribute vec4 position;

attribute vec2 inputTextureCoordinate;

varying vec2 textureCoordinate;

void main()

{

gl_Position = position;

textureCoordinate = inputTextureCoordinate;

}

);

// 片段着色器代码

NSString *const kYUVFullRangeConversionForLAFragmentShaderString = SHADER_STRING

(

varying highp vec2 textureCoordinate;

precision mediump float;

uniform sampler2D luminanceTexture;

uniform sampler2D chrominanceTexture;

uniform mediump mat3 colorConversionMatrix;

void main()

{

mediump vec3 yuv;

lowp vec3 rgb;

yuv.x = texture2D(luminanceTexture, textureCoordinate).r;

yuv.yz = texture2D(chrominanceTexture, textureCoordinate).ra - vec2(0.5, 0.5);

rgb = colorConversionMatrix * yuv;

gl_FragColor = vec4(rgb, 1);

}

);

实战代码

#pragma mark - 06、创建着色器

- (void)setupShader

{

// 创建顶点着色器

_vertShader = [self loadShader:GL_VERTEX_SHADER withString:kVertexShaderString];

// 创建片段着色器

_fragShader = [self loadShader:GL_FRAGMENT_SHADER withString:kYUVFullRangeConversionForLAFragmentShaderString];

}

// 加载着色器

- (GLuint)loadShader:(GLenum)type withString:(NSString *)shaderString

{

// 创建着色器

GLuint shader = glCreateShader(type);

if (shader == 0) {

NSLog(@"Error: failed to create shader.");

return 0;

}

// 加载着色器源代码

const char * shaderStringUTF8 = [shaderString UTF8String];

glShaderSource(shader, 1, &shaderStringUTF8, NULL);

// 编译着色器

glCompileShader(shader);

// 检查是否完成

GLint compiled = 0;

// 获取完成状态

glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);

if (compiled == 0) {

// 没有完成就直接删除着色器

glDeleteShader(shader);

return 0;

}

return shader;

}

07-创建着色器程序

步骤: 1.创建程序 2.贴上顶点和片段着色器 3.绑定attribute属性 4.连接程序 5.绑定uniform属性 6.运行程序

注意点:第3步和第5步,绑定属性,必须有顺序,否则绑定不成功,造成黑屏

#pragma mark - 7、创建着色器程序

- (void)setupProgram

{

// 创建着色器程序

_program = glCreateProgram();

// 绑定着色器

// 绑定顶点着色器

glAttachShader(_program, _vertShader);

// 绑定片段着色器

glAttachShader(_program, _fragShader);

// 绑定着色器属性,方便以后获取,以后根据角标获取

// 一定要在链接程序之前绑定属性,否则拿不到

glBindAttribLocation(_program, ATTRIB_POSITION, "position");

glBindAttribLocation(_program, ATTRIB_TEXCOORD, "inputTextureCoordinate");

// 链接程序

glLinkProgram(_program);

// 获取全局参数,注意 一定要在连接完成后才行,否则拿不到

_luminanceTextureAtt = glGetUniformLocation(_program, "luminanceTexture");

_chrominanceTextureAtt = glGetUniformLocation(_program, "chrominanceTexture");

_colorConversionMatrixAtt = glGetUniformLocation(_program, "colorConversionMatrix");

// 启动程序

glUseProgram(_program);

}

08-创建纹理对象

纹理

采集的是一张一张的图片,可以把图片转换为OpenGL中的纹理, 然后再把纹理画到OpenGL的上下文中

什么是纹理?一个纹理其实就是一幅图像。

纹理映射,我们可以把这幅图像的整体或部分贴到我们先前用顶点勾画出的物体上去.

比如绘制一面砖墙,就可以用一幅真实的砖墙图像或照片作为纹理贴到一个矩形上,这样,一面逼真的砖墙就画好了。如果不用纹理映射的方法,则墙上的每一块砖都必须作为一个独立的多边形来画。另外,纹理映射能够保证在变换多边形时,多边形上的纹理图案也随之变化。

纹理映射是一个相当复杂的过程,基本步骤如下:

1)激活纹理单元、2)创建纹理 、3)绑定纹理 、4)设置滤波

注意:纹理映射只能在RGBA方式下执行

函数glTexParameter

void glTexParameter{if}[v](GLenum target,GLenum pname,TYPE param);

{if}:表示可能是否i,f

[v]:表示v可有可无

控制滤波,滤波就是去除没用的信息,保留有用的信息

一般来说,纹理图像为正方形或长方形。但当它映射到一个多边形或曲面上并变换到屏幕坐标时,纹理的单个纹素很少对应于屏幕图像上的像素。根据所用变换和所用纹理映射,屏幕上单个象素可以对应于一个纹素的一小部分(即放大)或一大批纹素(即缩小)

固定写法

/* 控制滤波 */

glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);

glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);

函数glPixelStorei

void glPixelStorei(GLenum pname, GLint param);

设置像素存储方式

pname:像素存储方式名

一种是GL_PACK_ALIGNMENT,用于将像素数据打包,一般用于压缩。

另一种是GL_UNPACK_ALIGNMENT,用于将像素数据解包,一般生成纹理对象,就需要用到解包.

param:用于指定存储器中每个像素行有多少个字节对齐。这个数值一般是1、2、4或8,

一般填1,一个像素对应一个字节;

函数CVOpenGLESTextureCacheCreateTextureFromImage

CVOpenGLESTextureCacheCreateTextureFromImage(CFAllocatorRef  _Nullable allocator, CVOpenGLESTextureCacheRef  _Nonnull textureCache, CVImageBufferRef  _Nonnull sourceImage, CFDictionaryRef  _Nullable textureAttributes, GLenum target, GLint internalFormat, GLsizei width, GLsizei height, GLenum format, GLenum type, size_t planeIndex, CVOpenGLESTextureRef  _Nullable * _Nonnull textureOut)

根据图片生成纹理

参数allocator kCFAllocatorDefault,默认分配内存

参数textureCache 纹理缓存

参数sourceImage 图片

参数textureAttributes NULL

参数target , GL_TEXTURE_2D(创建2维纹理对象)

参数internalFormat GL_LUMINANCE,亮度格式

参数width 图片宽

参数height 图片高

参数format GL_LUMINANCE 亮度格式

参数type 图片类型 GL_UNSIGNED_BYTE

参数planeIndex 0,切面角标,表示第0个切面

参数textureOut 输出的纹理对象

fotmat格式                    描述

GL_ALPHA            按照ALPHA值存储纹理单元

GL_LUMINANCE        按照亮度值存储纹理单元

GL_LUMINANCE_ALPHA    按照亮度和alpha值存储纹理单元

GL_RGB                按照RGB成分存储纹理单元

GL_RGBA                按照RGBA成分存储纹理单元

实战代码

#pragma mark - 7、创建纹理对象,渲染采集图片到屏幕

- (void)setupTexture:(CMSampleBufferRef)sampleBuffer

{

// 获取图片信息

CVImageBufferRef imageBufferRef = CMSampleBufferGetImageBuffer(sampleBuffer);

// 获取图片宽度

GLsizei bufferWidth = (GLsizei)CVPixelBufferGetWidth(imageBufferRef);

_bufferWidth = bufferWidth;

GLsizei bufferHeight = (GLsizei)CVPixelBufferGetHeight(imageBufferRef);

_bufferHeight = bufferHeight;

// 创建亮度纹理

// 激活纹理单元0, 不激活,创建纹理会失败

glActiveTexture(GL_TEXTURE0);

// 创建纹理对象

CVReturn err;

err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCacheRef, imageBufferRef, NULL, GL_TEXTURE_2D, GL_LUMINANCE, bufferWidth, bufferHeight, GL_LUMINANCE, GL_UNSIGNED_BYTE, 0, &_luminanceTextureRef);

if (err) {

NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);

}

// 获取纹理对象

_luminanceTexture = CVOpenGLESTextureGetName(_luminanceTextureRef);

// 绑定纹理

glBindTexture(GL_TEXTURE_2D, _luminanceTexture);

// 设置纹理滤波

glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);

glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

// 激活单元1

glActiveTexture(GL_TEXTURE1);

// 创建色度纹理

err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCacheRef, imageBufferRef, NULL, GL_TEXTURE_2D, GL_LUMINANCE_ALPHA, bufferWidth / 2, bufferHeight / 2, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, 1, &_chrominanceTextureRef);

if (err) {

NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);

}

// 获取纹理对象

_chrominanceTexture = CVOpenGLESTextureGetName(_chrominanceTextureRef);

// 绑定纹理

glBindTexture(GL_TEXTURE_2D, _chrominanceTexture);

// 设置纹理滤波

glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);

glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

}

09-YUV转RGB绘制纹理

纹理映射只能在RGBA方式下执行

而采集的是YUV,所以需要把YUV 转换 为 RGBA,

本质其实就是改下矩阵结构

注意点:glDrawArrays如果要绘制着色器上的点和片段,必须和着色器赋值代码放在一个代码块中,否则找不到绘制的信息,就绘制不上去,造成屏幕黑屏

之前是把glDrawArrays和YUV转RGB方法分开,就一直黑屏.

函数glUniform1i

glUniform1i(GLint location, GLint x)

指定着色器中亮度纹理对应哪一层纹理单元

参数location:着色器中纹理坐标

参数x:指定那一层纹理

函数glEnableVertexAttribArray

glEnableVertexAttribArray(GLuint index)

开启顶点属性数组,只有开启顶点属性,才能给顶点属性信息赋值

函数glVertexAttribPointer

glVertexAttribPointer(GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *ptr)

设置顶点着色器属性,描述属性的基本信息

参数indx:属性ID,给哪个属性描述信息

参数size:顶点属性由几个值组成,这个值必须位1,2,3或4;

参数type:表示属性的数据类型

参数normalized:GL_FALSE表示不要将数据类型标准化

参数stride 表示数组中每个元素的长度;

参数ptr 表示数组的首地址

10-渲染缓冲区到屏幕

注意点:必须设置窗口尺寸glViewport

注意点:渲染代码必须调用[EAGLContext setCurrentContext:_context]

原因:因为是多线程,每一个线程都有一个上下文,只要在一个上下文绘制就好,设置线程的上下文为我们自己的上下文,就能绘制在一起了,否则会黑屏.

注意点:每次创建纹理前,先把之前的纹理引用清空[self cleanUpTextures],否则卡顿

函数glViewport

glViewport(GLint x, GLint y, GLsizei width, GLsizei height)

设置OpenGL渲染窗口的尺寸大小,一般跟图层尺寸一样.

注意:在我们绘制之前还有一件重要的事情要做,我们必须告诉OpenGL渲染窗口的尺寸大小

方法presentRenderbuffer

- (BOOL)presentRenderbuffer:(NSUInteger)target

是将指定renderbuffer呈现在屏幕上

11-清理内存

注意:只要有Ref结尾的,都需要自己手动管理,清空

函数glClearColor

glClearColor (GLclampf red, GLclampf green, GLclampf blue, GLclampfalpha)

设置一个RGB颜色和透明度,接下来会用这个颜色涂满全屏.

函数glClear

glClear (GLbitfieldmask)

用来指定要用清屏颜色来清除由mask指定的buffer,mask可以是 GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT的自由组合。

在这里我们只使用到 color buffer,所以清除的就是 clolor buffer。

#pragma mark - 11.清理内存

- (void)dealloc

{

// 清空缓存

[self destoryRenderAndFrameBuffer];

// 清空纹理

[self cleanUpTextures];

}

#pragma mark - 销毁渲染和帧缓存

- (void)destoryRenderAndFrameBuffer

{

glDeleteRenderbuffers(1, &_colorRenderBuffer);

_colorRenderBuffer = 0;

glDeleteBuffers(1, &_framebuffers);

_framebuffers = 0;

}

// 清空纹理

- (void)cleanUpTextures

{

// 清空亮度引用

if (_luminanceTextureRef) {

CFRelease(_luminanceTextureRef);

_luminanceTextureRef = NULL;

}

// 清空色度引用

if (_chrominanceTextureRef) {

CFRelease(_chrominanceTextureRef);

_chrominanceTextureRef = NULL;

}

// 清空纹理缓存

CVOpenGLESTextureCacheFlush(_textureCacheRef, 0);

}

GPUImage工作原理

GPUImage最关键在于GPUImageFramebuffer这个类,这个类会保存当前处理好的图片信息。

GPUImage是通过一个链条处理图片,每个链条通过target连接,每个target处理完图片后,会生成一个GPUImageFramebuffer对象,并且把图片信息保存到GPUImageFramebuffer。

这样比如targetA处理好,要处理targetB,就会先取出targetA的图片,然后targetB在targetA的图片基础上在进行处理.

总结:GPUImage是基于OpenGL ES 处理,我一直在持续学习过程中,分析和总结的东西那些不到位的地方,希望读友们指点,我会虚心学习,我们一起进步。

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

推荐阅读更多精彩内容