[OpenGL]VBO,VAO和EBO详解

美女

这篇文章通过几个简单实例,讨论了OpenGL编程中的 VBO,VAO 和 EBO 概念。

1. VBO 和 VAO

1.1 VBO(Vertex Buffer Object) 顶点缓冲对象

VBO 是显存中的一片缓冲区域,存放从内存中提交过来的顶点数据。GPU 绘制时,需要对 VBO 中的数据进行解析,以便将数据正确的提交给着色器中对应的属性。例如,GPU 需要知道 VBO 中哪一块数据是某个顶点的坐标,哪一块数据是某个顶点的顶点颜色等,通过调用 glVertexAttribPointer 方法来设置解析规则,GPU 能够取到正确的数据供着色器使用

1.2 VAO(Vertex Array Object)顶点数组对象

上述 glVertexAttribPoint的调用结果被记录到 VAO 中,最终绘制的时候直接通过 VAO 中存储的指针去缓冲区取数据,而不需要再重复解析 VBO,VAO 和 VBO 的关系大概是这样:

VAO和VBO的关系

  • VBO 是纯数据的缓冲区,示意图中分别用两个VBO来保存三角形的顶点位置和顶点颜色数据
  • VAO 是一个数组,保存每一类顶点属性的解析结果,OpenGL中貌似最多支持 16 种顶点属性,这里的顶点属性就是 glVertexAttribPointer 方法的第一个参数指定的,通常0表示顶点坐标,1表示顶点颜色
  • 使用 VAO 的好处是,你只需要针对 VBO 做一次解析,将结果存储到 VAO 中,每一帧渲染使用 VAO 的指针来访问缓冲区数据,而不需要每一帧都做解析
    [OpenGL]绘制三角形 这篇文章中,我们实现了一个完整的绘制三角形的程序,完整的代码都贴在了该文章的最后一部分,我们这里来看看核心的绘制部分代码如下
// draw_triangle.h
#include "shader_common.h"

const GLchar* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x, position.y, position.z, 1.0);\n"
"}\0";
const GLchar* fragmentShaderSource = "#version 330 core\n"
"out vec4 color;\n"
"void main()\n"
"{\n"
"color = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";

int draw_trangle()
{   
    // 初始化 OpenGL
    init_opengl();
    
    // 编译和链接着色器
    GLuint shaderProgram = compile_shader(vertexShaderSource, fragmentShaderSource);

    // 顶点数据
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  0.5f, 0.0f  // Top   
    };

    // 申请缓冲区
    GLuint VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);  

    // 绑定VAO,表示在这之后针对 VBO 的解析都会记录在 VAO 中
    glBindVertexArray(VAO);

    // 提交数据
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // 使用Buffer中的数据在 VAO 生成 0 号顶点属性的指针
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);

    // 启用 0 号顶点属性
    glEnableVertexAttribArray(0);

    // 解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 解绑VAO
    glBindVertexArray(0);
    
    while (!glfwWindowShouldClose(window))
    {
        // 处理事件
        glfwPollEvents();

        // 清屏
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 指定着色器程序
        glUseProgram(shaderProgram);

        // 绑定VAO
        glBindVertexArray(VAO);

        // 绘制指令
        glDrawArrays(GL_TRIANGLES, 0, 3);

        // 解绑 VAO
        glBindVertexArray(0);

        // 双缓冲交换
        glfwSwapBuffers(window);
    }

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);   
    glfwTerminate();
    return 0;
}

可以看到,我们在初始化时将 0 号属性的解析

 // 使用Buffer中的数据在 VAO 生成 0 号顶点属性的指针
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);

记录在了 VAO 中,在后续的渲染循环中,我们没有再访问 VBO,而是直接通过绑定 VAO 来调用渲染命令:

       // 绑定VAO
        glBindVertexArray(VAO);

        // 绘制指令
        glDrawArrays(GL_TRIANGLES, 0, 3);
1.3 VAO 对应多个 VBO

我们来实现一个程序,将三角形的顶点和颜色数据分别用两个 VBO 提交,然后解析到同一个 VAO 中,再绑定这个 VAO 进行绘制。我们需要在上述的代码中做如下几个更改:

  • 首先,着色器代码需要支持除顶点位置(0号顶点属性)之外的另外一个属性:顶点颜色,也就是 1 号顶点属性,着色器代码修改如下
const GLchar* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"layout (location = 1) in vec4 color;\n"
"out vec4 v_color;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x, position.y, position.z, 1.0);\n"
"v_color = color;\n"
"}\0";
const GLchar* fragmentShaderSource = "#version 330 core\n"
"in vec4 v_color;\n"
"layout(location=0) out vec4 o_fragColor;\n"
"void main()\n"
"{\n"
"o_fragColor = v_color;\n"
"}\n\0";
  • 其次,我们需要在内存中分配两个区域分别存放顶点的位置和颜色信息

    // 顶点数据
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  0.5f, 0.0f  // Top   
    };

    GLfloat colors[] = {
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f
    };
  • 再次,在渲染循环之前分别提交顶点位置和颜色信息,绑定到不同的 VBO,并申请 VAO 记录两个 VBO 的解析结果
// 申请缓冲区
    GLuint VBO, VAO, VBO2;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &VBO2);

    // 绑定VAO
    glBindVertexArray(VAO);

    // 提交数据和解析规则
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, VBO2);
    glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(1);

    // 解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 解绑VAO
    glBindVertexArray(0);
1.4 渲染部分完整代码

这里的初始化方法 init_opengl和着色器编译方法 compile_shader 可以查看 [OpenGL]绘制三角形 文章的代码部分,渲染部分的完整代码如下

int draw_trangle()
{
    // 初始化 OpenGL
    init_opengl();

    // 编译和链接着色器
    GLuint shaderProgram = compile_shader(vertexShaderSource, fragmentShaderSource);

    // 顶点数据
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  0.5f, 0.0f  // Top   
    };

    GLfloat colors[] = {
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f
    };

    // 申请缓冲区
    GLuint VBO, VAO, VBO2;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &VBO2);

    // 绑定VAO
    glBindVertexArray(VAO);

    // 提交数据和解析规则
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, VBO2);
    glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(1);

    // 解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 解绑VAO
    glBindVertexArray(0);

    while (!glfwWindowShouldClose(window))
    {
        // 处理事件
        glfwPollEvents();

        // 清屏
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 指定着色器程序
        glUseProgram(shaderProgram);

        // 绑定VAO
        glBindVertexArray(VAO);

        // 绘制指令
        glDrawArrays(GL_TRIANGLES, 0, 3);

        // 解绑 VAO
        glBindVertexArray(0);

        // 双缓冲交换
        glfwSwapBuffers(window);
    }

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glfwTerminate();
    return 0;
}

渲染的结果:


渲染结果

2. EBO

2.1 绘制两个三角形

我们来考虑绘制两个三角形的情形,假如我们复用上述三角形其中两个顶点,另外增加一个顶点,构成一个新的三角形,首先能想到有两种方法可以绘制两个三角形:

  • 第一种方法,增加顶点位置数组和颜色数组的长度到6,复用刚才的绘制代码
    // 顶点数据
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  0.5f, 0.0f,  // Top 
         -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  -1.0f, 0.0f,  // Down
    };
    // 颜色数据
    GLfloat colors[] = {
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f,
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f
    };

在调用 glDrawArrays方法时要修改传参,渲染6个顶点

glDrawArrays(GL_TRIANGLES, 0, 6);
  • 第二种方法,分别使用两个 VAO 保存两个三角形的解析数据进行绘制,绘制部分的代码如下:
int draw_trangle()
{
    // 初始化 OpenGL
    init_opengl();

    // 编译和链接着色器
    GLuint shaderProgram = compile_shader(vertexShaderSource, fragmentShaderSource);

    // 三角形1顶点数据
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  0.5f, 0.0f,  // Top      
    };

    // 三角形2顶点数据
    GLfloat vertices2[] = {
        -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  -1.0f, 0.0f,  // Down   
    };

    // 颜色数据
    GLfloat colors[] = {
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f,
        0.0f, 0.5f, 0.5f, 1.0f
    };

    // 申请缓冲区
    GLuint VBO1, VBO2, ColorVBO, VAO1, VAO2;
    glGenVertexArrays(1, &VAO1);
    glGenVertexArrays(1, &VAO2);
    glGenBuffers(1, &VBO1);
    glGenBuffers(1, &VBO2);
    glGenBuffers(1, &ColorVBO);

    // 绑定VAO
    glBindVertexArray(VAO1);

    // 提交数据和解析规则
    glBindBuffer(GL_ARRAY_BUFFER, VBO1);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, ColorVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(1);

    // 解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 绑定VAO
    glBindVertexArray(VAO2);

    // 提交数据和解析规则
    glBindBuffer(GL_ARRAY_BUFFER, VBO2);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices2), vertices2, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, ColorVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(1);

    // 解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 解绑VAO
    glBindVertexArray(0);

    while (!glfwWindowShouldClose(window))
    {
        // 处理事件
        glfwPollEvents();

        // 清屏
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 指定着色器程序
        glUseProgram(shaderProgram);

        // 绑定VAO
        glBindVertexArray(VAO1);

        // 绘制指令
        glDrawArrays(GL_TRIANGLES, 0, 3);

        // 绑定VAO
        glBindVertexArray(VAO2);

        // 绘制指令
        glDrawArrays(GL_TRIANGLES, 0, 3);

        // 解绑 VAO
        glBindVertexArray(0);

        // 双缓冲交换
        glfwSwapBuffers(window);
    }

    glDeleteVertexArrays(1, &VAO1);
    glDeleteBuffers(1, &VBO1);
    glfwTerminate();
    return 0;
}

两种方法渲染结果相同,示意图


image.png
2.2 优化

上述种方法,其实都有顶点数据的冗余,包括位置和颜色数据的冗余,6个顶点我们需要使用 6 个 float3 来表示位置和颜色,当需要渲染的顶点数时会造成更大的冗余,我们看看如何针对这个问题来进行优化,事实上我们复用了两个顶点,也就是说其实 4 个顶点数据就够用了,为此我们引入了EBO(Element Buffer Object),索引缓冲对象,来解决数据冗余的问题。
使用 EBO 来解决上面两个三角形数据冗余问题的思路是这样的:只保存4个顶点数据,引入EBO来存储两个三角形对于顶点数据的索引:

    // 四个顶点,有两个复用
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f,
         0.0f,  0.5f, 0.0f,
         0.0f,  -1.0f, 0.0f,
    };

    // 四个颜色,两个复用
    GLfloat colors[] = {
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f
    };

    GLuint indices[] = {
        0, 1, 2,    // 第一个三角形使用的顶点下标
        0, 1, 3     // 第二个三角形使用的顶点下标
    };

简单来说就是,VBO 中存放的是去重后的顶点数据,当顶点复用数目较多时可以节省很多存储空间,另外单独开辟一个EBO缓冲区来存储每个顶点的实际数据在 VBO 中对应的下标值,在提交时将EBO信息也提交,计算结果得到 VAO,绘制时绑定 VAO 来访问顶点数据。我这里使用了相同的下标数组 indices 来使用顶点坐标和顶点颜色数据,你也可以使用另外的数组来指定不同三角形使用的颜色值,如

GLuint colorIndices[] = {
    0, 1, 2,
    1, 2, 3
};

只要在下面为 VAO 绑定属性时传递正确的数据就可以了。使用 EBO 来绘制两个三角形的完整逻辑代码:

int draw_trangle()
{
    init_opengl();

    GLuint shaderProgram = compile_shader(vertexShaderSource, fragmentShaderSource);

    // 四个顶点,有两个复用
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f,
         0.0f,  0.5f, 0.0f,
         0.0f,  -1.0f, 0.0f,
    };

    // 四个颜色,两个复用
    GLfloat colors[] = {
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f
    };

    GLuint indices[] = {
        0, 1, 2,    // 第一个三角形使用的顶点下标
        0, 1, 3     // 第二个三角形使用的顶点下标
    };

    // 缓冲区生成
    GLuint VBO, ColorVBO, VAO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &ColorVBO);
    glGenBuffers(1, &EBO);
    glBindVertexArray(VAO);

    // 解析并提交位置属性
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

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

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    // 解析并提交颜色属性
    glBindBuffer(GL_ARRAY_BUFFER, ColorVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

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

    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(1);

    // 解绑缓冲区
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);   

    while (!glfwWindowShouldClose(window))
    {
        glfwPollEvents();
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 指定着色器
        glUseProgram(shaderProgram);

        // 绑定 VAO
        glBindVertexArray(VAO);
        
        // 根据索引绘制
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

        glBindVertexArray(0);

        glfwSwapBuffers(window);
    }

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);
    glfwTerminate();
    return 0;
}

需要特别注意的几点:

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

推荐阅读更多精彩内容