Opengl ES 3.0 on iOS--- HelloWord(绘制彩色矩形)

简介

本文记录了我初学Opengl 绘制彩色矩形的过程,可能我对内容的描述不够准确,还请多多指正

实现

配置图层

+(Class)layerClass{
    return [CAEAGLLayer class];
}

将当前View的Layer替换成 CAEAGLLayer类,opengl的绘制内容也是在该View上显示的.

//设置不透明度为YES,因为透明图层性能不好
self.layer.opaque = YES;
self.layer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
                                         [NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];

可以对CAEAGLLayer进行额外属性的配置:

kEAGLDrawablePropertyRetainedBacking 传入布尔值,表示是否保持绘制状态,若设置为NO,则下次将重新绘制.
kEAGLDrawablePropertyColorFormat 设置layer的颜色缓冲区格式,EAGLContext对象 使用此格式来创建渲染缓冲区的存储.
kEAGLColorFormatRGB565 ---> 16bit RGB格式
kEAGLColorFormatRGBA8 ---> 32-bit RGBA格式

配置上下文

        _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
        if (!_context) {
            return;
        }
        // 将当前上下文设置为我们创建的上下文
        if (![EAGLContext setCurrentContext:_context]) {
            return;
        }
    }

设置缓冲区(渲染缓冲和帧缓冲)

    //在缓冲区中返回n个渲染缓冲对象句柄,不保证这些句柄是连续的整数,但是肯定没有被使用.
    GLuint renderbuffer[1];
    glGenRenderbuffers(ARRAY_SIZE(renderbuffer), renderbuffer);

    //将缓冲区对象和句柄 绑定到指定的缓冲区目标.
    glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer[0]);

    //检验是否创建绑定成功
    if (glIsRenderbuffer(renderbuffer[0]) == GL_TRUE) {
        NSLog(@"成功生成渲染缓存");
    }
    //为缓冲区对象分配存储空间.
    [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
    
    //设置帧缓冲区(Frame Buffer),和渲染缓冲区大致相同
    GLuint framebuffer[1];
    glGenFramebuffers(ARRAY_SIZE(framebuffer), framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer[0]);
    if (glIsFramebuffer(framebuffer[0]) == GL_TRUE) {
        NSLog(@"成功绑定帧缓存");
    }
    
    //将相关的buffer依附到 帧缓存上 
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer[0]);
    
        //释放渲染缓存
        //glDeleteRenderbuffers(ARRAY_SIZE(renderbuffer), renderbuffer);
        //释放帧缓存
        //glDeleteFramebuffers(ARRAY_SIZE(framebuffer), framebuffer);

渲染缓存: 是OpenGL ES管理的一块高效内存区域,渲染缓存的数据只有关联一个帧缓存对象才有意义,并且需要保证图像缓存格式 必须与OpenGL ES要求的渲染格式相符.

帧缓存:它是屏幕所显示画面的一个直接映象,又称为位映射图(Bit Map)或光栅。帧缓存的每一存储单元对应屏幕上的一个像素,整个帧缓存对应一帧图像。

函数解释

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer[0]);
参数:

target: 指定的帧缓冲区目标 必须是 GL_DRAW_FRAMEBUFFER, GL_READ_FRAMEBUFFER, 或 GL_FRAMEBUFFER. (GL_FRAMEBUFFER = GL_DRAW_FRAMEBUFFER);
attachment: 帧缓存对象依附的目标 GL_COLOR_ATTACHMENT(0~i) ---> 第i个颜色缓存 0为默认值, GL_DEPTH_ATTACHMENT ---> 深度缓存, GL_STENCIL_ATTACHMENT ---> 模板缓存
renderbuffertarget :必须为 GL_RENDERBUFFER,指定的渲染缓存区目标
renderbuffer: 渲染缓冲区对象句柄.

准备着色器源码

OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线.OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线

着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的某个特定部分而运行。从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。

着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。

顶点着色器

#version 300 es //OpenGL ES 3.0

//接受的输入变量
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;

//输出变量
out vec3 outColor;

//相当于C语言的main函数
void main()
{   
    //绘制图形
    gl_Position = vec4(position[0],position[1],position[2], 1.0);
    
    outColor = color;
}

图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入.一个顶点(Vertex)是一个3D坐标的数据的集合。而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据.

片段着色器

#version 300 es

precision mediump float;    //表示 数据精确度 这里设置的为中级

in vec3 outColor;

out vec4 FragColor; //输出的色彩

void main()
{
    FragColor = vec4(outColor.x,outColor.y,outColor.z, 1.0);;
}

片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色

在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

创建着色器对象

static GLuint createGLShader(const char *shaderText, GLenum shaderType)
{
    //创建着色器,将根据传入的type参数 创建一个新的 顶点或片段着色器,返回值为新的着色器对象句柄
    //GL_VERTEX_SHADER(顶点着色器)     GL_FRAGMENT_SHADER(片段着色器)
    GLuint shader = glCreateShader(shaderType);

    //为着色器对象 提供着色器源代码.
    //参数: shader --> 着色器对象句柄
    //      count --> 着色器源字符串数量
    //      string --> 字符串的数组指针
    //      length ---> 指向保存美工着色器字符串大小且元素数量为count的整数数组指针.如果length为NULL 着色器字符串将被认定为空.
    glShaderSource(shader, 1, &shaderText, NULL);

    //调用该方法,将指定的着色器源代码 进行编译
    //参数shader 为着色器句柄
    glCompileShader(shader);

    //调用该方法获取 着色器源代码编译是否成功,并获取其他相关信息
    //第二个参数 pname 表示要查询什么信息
    /*
     GL_COMPILE_STATUS ---> 是否编译成功 成功返回 GL_TRUE
     GL_INFO_LOG_LENGTH ---> 查询源码编译后长度
     GL_SHADER_SOURCE_LENGTH ---> 查询源码长度
     GL_SHADER_TYPE ---> 查询着色器类型()
     GL_DELETE_STATUS ---> 着色器是否被标记删除
     */
    int compiled = 0;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    if (!compiled) {
        GLint infoLen = 0;
        glGetShaderiv (shader, GL_INFO_LOG_LENGTH, &infoLen);
        if (infoLen > 1) {
            char *infoLog = (char *)malloc(sizeof(char) * infoLen);
            if (infoLog) {


                //检索信息日志
                //参数: shader 着色器对象句柄
                //      maxLength 保存信息日志的缓冲区大小
                //      length 写入信息日志长度 ,不需要知道可传NULL
                //      infoLog 保存日志信息的指针
                glGetShaderInfoLog (shader, infoLen, NULL, infoLog);
                GLlog("Error compiling shader: %s\n", infoLog);
                free(infoLog);
            }
        }
        //删除着色器对象, 参数shader为要删除的着色器对象的句柄
        //若一个着色器链接到一个程序对象,那么该方法不会立刻删除着色器,而是将着色器标记为删除,当着色器不在连接到任何程序对象时,它的内存将被释放.
        glDeleteShader(shader);
        return 0;
    }
    
    return shader;
}

创建程序对象

 // 创建一个程序对象,返回程序对象的句柄
    GLuint program = glCreateProgram();

    // 得到需要的着色器
    GLuint vertShader = createGLShader(vertext, GL_VERTEX_SHADER);  //顶点着色器
    GLuint fragShader = createGLShader(frag, GL_FRAGMENT_SHADER);   //片元着色器
    
    if (vertShader == 0 || fragShader == 0) {
        return 0;
    }

    //将程序对象和 着色器对象链接  //在ES 3.0中,每个程序对象 必须连接一个顶点着色器和片段着色器
    //program程序对象句柄 shader着色器句柄
    glAttachShader(program, vertShader);
    glAttachShader(program, fragShader);


    //链接程序对象 生成可执行程序(在着色器已完成编译 且程序对象连接了着色器)
    //链接程序会检查各种对象的数量,和各种条件.
    //在链接阶段就是生成最终硬件指令的时候(和C语言一样)
    glLinkProgram(program);


    //检查链接是否成功
    GLint success;
    glGetProgramiv(program, GL_LINK_STATUS, &success);
    
    
    if (!success) {
        GLint infoLen;
        //使用 GL_INFO_LOG_LENGTH 表示获取信息日志
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);
        if (infoLen > 1) {
            GLchar *infoText = (GLchar *)malloc(sizeof(GLchar)*infoLen + 1);
            if (infoText) {
                memset(infoText, 0x00, sizeof(GLchar)*infoLen + 1);

                // 从信息日志中获取信息
                glGetProgramInfoLog(program, infoLen, NULL, infoText);
                GLlog("%s", infoText);
                free(infoText);

                //此函数用于校验当前的程序对象,校验结果可通过 glGetProgramiv函数检查,此函数只用于调试,因为他很慢.
                //glValidateProgram(program);
            }
        }
        glDeleteShader(vertShader);
        glDeleteShader(fragShader);

        //删除程序对象
        glDeleteProgram(program);
        return 0;
    }
    
    /*
 * 链接完着色器,生成可执行程序. 将着色器断开删除
 */
    //断开指定程序对象和片段着色器
    glDetachShader(program, vertShader);
    glDetachShader(program, fragShader);

    //将着色器标记为删除
    glDeleteShader(vertShader);
    glDeleteShader(fragShader);

程序对象就是一个容器对象,将着色器与之连接,最后链接生成最终的可执行程序.

输入顶点数据

        //三角形的三点坐标+颜色坐标
        static GLfloat vertices[] = {
            //点坐标                     //颜色
            0.5f,  0.5f, 0.0f,          1.0f, 0.0f, 0.0f,
            0.5f, -0.5f, 0.0f,          0.0f, 1.0f, 0.0f,
            -0.5f, -0.5f, 0.0f,         0.0f, 0.0f, 1.0f,
            -0.5f, 0.5f, 0.0f,          1.0f, 0.0f, 1.0f
        };
        
        static unsigned int indices[] = {
            0,1,3,
            1,2,3
        };
        
        unsigned int VAO,VBO,EBO;
        //创建VAO对象,VBO对象,EBO对象
        glGenVertexArrays(1, &VAO);
        glGenBuffers(1, &VBO);
        glGenBuffers(1, &EBO);
        
        //绑定VAO VBO EBO
        glBindVertexArray(VAO);
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
        
        将顶点数据 和 索引数据 复制到缓冲区中
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

        //设置顶点属性指针 输入数据
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0);
        //激活 0号变量,为了性能,若不激活着色器无法接受数据
        glEnableVertexAttribArray(0);
        
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));
        glEnableVertexAttribArray(1);

VAO VBO EBO

不使用VAO VBO绘制代码:

    static GLfloat vertices[] = {
        0.0f,  0.5f, 0.0f,
        -0.5f, -0.5f, 0.0f,
        0.5f, -0.5f, 0.0f
    };
    GLint posSlot = glGetAttribLocation(_program, "position");
    glVertexAttribPointer(posSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);
    glEnableVertexAttribArray(posSlot);
    
    static GLfloat colors[] = {
        0.0f, 1.0f, 1.0f,
        1.0f, 0.0f, 1.0f,
        1.0f, 1.0f, 0.0f
    };
    GLint colorSlot = glGetAttribLocation(_program, "color");
    glVertexAttribPointer(colorSlot, 3, GL_FLOAT, GL_FALSE, 0, colors);
    glEnableVertexAttribArray(colorSlot);

VBO

如上面的例子所示, 普通的顶点数组的传输,需要在绘制的时候频繁地从CPU到GPU传输顶点数据,这种做法效率低下.
为了加快显示速度,显卡增加了一个扩展 VBO (Vertex Buffer object),即顶点缓存。它直接在 GPU 中开辟一个缓存区域来存储顶点数据,因为它是用来缓存储顶点数据,因此被称之为顶点缓存。使用顶点缓存能够大大较少了CPU到GPU 之间的数据拷贝开销,因此显著地提升了程序运行的效率。

函数

1, 创建顶点缓存对象
void glGenBuffers (GLsizei n, GLuint* buffers);

参数 n : 表示需要创建顶点缓存对象的个数
参数 buffers :用于存储创建好的顶点缓存对象句柄

2, 将顶点缓存对象设置为当前数组缓存对象
void glBindBuffer (GLenum target, GLuint buffer);

target :指定绑定的目标,取值为 GL_ARRAY_BUFFER(用于顶点数据) 或 GL_ELEMENT_ARRAY_BUFFER(用于索引数据)
buffer :顶点缓存对象句柄

3, 为顶点缓存对象分配空间(这里就是将数据一次性 拷贝至显存中)
void glBufferData (GLenum target, GLsizeiptr size, const GLvoid* data, GLenum usage);

target:指定绑定的目标,取值为 GL_ARRAY_BUFFER(用于顶点数据) 或 GL_ELEMENT_ARRAY_BUFFER(用于索引数据).
size :指定顶点缓存区的大小,以字节为单位计数;
data :用于初始化顶点缓存区的数据,可以为 NULL,表示只分配空间,之后再由 glBufferSubData 进行初始化;
usage :表示该缓存区域将会被如何使用,它的主要目的是用于对该缓存区域做何种程度的优化,比如经常修改的数据可能就会放在GPU缓存中达到快速操作的目的.

usage:

GL_STATIC_DRAW  表示该缓存区不会被修改
GL_DYNAMIC_DRAW     表示该缓存区会被周期性更改
GL_STREAM_DRAW  表示该缓存区会被频繁更改

4,更新顶点缓冲区数据
void glBufferSubData (GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid* data);

offset: 表示需要更新的数据的起始偏移量;
size: 表示需要更新的数据的个数,也是以字节为计数单位;
data: 用于更新的数据;

5,释放顶点缓存
void glDeleteBuffers (GLsizei n, const GLuint* buffers);

n : 表示顶点缓存对象的个数
buffers :顶点缓存对象句柄

VAO

VAO的全名是 Vertex Array Object。它不用作存储数据,但它与顶点绘制相关。
它的定位是状态对象,记录存储状态信息。VAO记录的是一次绘制中做需要的信息,这包括数据在哪里、数据格式是什么等信息。VAO其实可以看成一个容器,可以包括多个VBO。 由于它进一步将VBO容于其中,所以绘制效率将在VBO的基础上更进一步。目前OpenGL ES3.0及以上才支持顶点数组对象。

图片.png
函数

1, 创建顶点数组对象
glGenVertexArrays (GLsizei n, GLuint* arrays) ;

n : 表示顶点数组对象的个数
arrays :顶点数组对象句柄

2, 将顶点数组对象设置为当前顶点数组对象
glBindVertexArray (GLuint array) ;

arrays :顶点数组对象句柄

3,释放顶点数组对象
glDeleteVertexArrays (GLsizei n, const GLuint* arrays);

n : 表示顶点数组对象的个数
arrays :顶点数组对象句柄

使用

如代码中所写,在绑定VAO后,后续的VBO操作都会存储到当前绑定的VAO中.这样就将当前绘制状态记录下来了. 当下次还要绘制当前图形时, 只需再次绑定当前VAO, 进行后面的绘制操作即可.对于OpenGL ES2.0 使用VAO 则需要使用另外提供的API来实现.

GLvoid glGenVertexArraysOES(GLsizei n, GLuint *arrays)
GLvoid glBindVertexArrayOES(GLuint array);
GLvoid glDeleteVertexArraysOES(GLsizei n, const GLuint *arrays);; 
GLboolean glIsVertexArrayOES(GLuint array);

EBO

索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO),当绘制现有图形时,存在顶点数据复用时,可以使用EBO.

函数

1,创建EBO(和VBO类似)

unsigned int EBO;
glGenBuffers(1, &EBO);

2,绑定EBO,将索引复制到缓冲里

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

3, 使用glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);替代glDrawArrays

函数补充

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

该函数用于将顶点属性传入顶点着色器
参数:

index: 对应顶你个点着色器中变量的location
size :表示该顶点属性对应的分量数量.也就是接收者为几位向量 如写入3 则表示为vec3 接收者为3维向量. 必须是 1~4.
type :表明每个分量的类型 可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT
normalized: 是否对每个分量进行归一化处理, 也就是若type为float类型.
stride:指定连续顶点属性之间的偏移量,如果设置0,则表示各个分量是紧密排在一起,中间没有其他多余数据.
ptr 顶点数据指针

此函数在有无VBO的情况下,使用有所差异~,在不适用VBO时,ptr确实是顶点数据指针.
当使用VBO时,顶点数据都已经拷贝至显存中,这里的ptr 就表示为缓冲区数据的便宜量了.

无EBO:
glVertexAttribPointer(colorSlot, 3, GL_FLOAT, GL_FALSE, 0, colors);

有EBO:

static GLfloat vertices[] = {
            //点坐标                     //颜色
            0.5f,  0.5f, 0.0f,          1.0f, 0.0f, 0.0f,
            0.5f, -0.5f, 0.0f,          0.0f, 1.0f, 0.0f,
            -0.5f, -0.5f, 0.0f,         0.0f, 0.0f, 1.0f,
            -0.5f, 0.5f, 0.0f,          1.0f, 0.0f, 1.0f
        };
.......(VBO与其他代码).......

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));

在这里0号属性和1号属性紧密相连,且0号和1号的分量数都为3,以0号属性开头.
故: 第一个0号和第二个0号 中间有6个间距 stride = 6*sizeof(float).
1号在0号后面, 0号ptr为 (void*)0 . 1号ptr为 (void* )(3*sizeof(float))

绘制

使用 EBO:

glDrawElements(GL_TRIANGLE_STRIP, 6, GL_UNSIGNED_INT, 0);
参数:
model:指定呈现那种图元(将这些点绘制成怎样的形状). 可选项:

GL_POINTS(点),
GL_LINE_STRIP(多端线),
GL_LINE_LOOP(线圈),
GL_LINES(线段),
GL_TRIANGLE_FAN, (三角形扇)
GL_TRIANGLES, (三角形)
count: 传入顶点数据的数量
type: 索引数组的元素属性 GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, or GL_UNSIGNED_INT.
indices: 指向索引数组的指针, 当使用VBO时,则表示为偏移量,若为紧密相连时则传入0.

不使用EBO

glDrawArrays(GL_TRIANGLES, 0, 3);
参数: model 和上面那个含义一样.
first 表示顶点数据起始索引, 从头开始则为0.
count 表示要传入顶点数据的数量.

最后显示

[_context presentRenderbuffer:GL_RENDERBUFFER];

最终结果.png

学习参考

你好三角形
OpenGL 简书专题

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

推荐阅读更多精彩内容