GL01-06:着色器的使用

本文主要还是解决图元绘制中的渲染问题-着色器,着色器的使用包含两个部分:(1)OpenGL的API调用,(2)GLSL语言及其语法;本文主要解释OpenGL的API使用(通过grew扩展使用);GLSL那是另外一个话题。
  1. OpenGL着色器使用过程与例子;
  2. OpenGL着色器相关函数的解释;


主要过程为:
  1.编写Shader:
    1)顶点Shader(vetexShader);
    2)片Shader(ragmentShader);
  2.创建顶点着色器对象
    1)创建vertexShader对象;glCreateShader
    2)加载vertexShader脚本到vertexShader对象;glShaderSource
    3)编译vertexShader脚本;glCompileShader
  3.创建片着色器
    1)创建fragmentShader对象;glCreateShader
    2)加载fragmentShader脚本到fragmentShader对象;glShaderSource
    3)编译fragmentShader脚本;glCompileShader
  4.创建着色器程序
    1)创建程序对象;glCreateProgram
    2)添加vertexShader对象到着色器程序;glAttachShader
    3)添加fragmentShader对象到着色器程序;glAttachShader
    4)链接着色器程序,以备渲染管道调用;glLinkProgram
  5.在开启顶点属性之前需要创建一个顶点数组:(用于顶点属性的处理)


加载OpenGL4.2的版本

  • 提前的说明
    1. 在其他系统中,实际上绘制点、线段、三角形等,都可以输出效果; 在Mac的OpenGL中,如果没有使用着色器,则线段、三角形看不见输出;

    2. 在默认情况下,加载的OpenGL的库版本是2.1,但这里讲解的是OpenGL使用4.1

    3. 本文重点不是GLSL,旦会用到GLSL,可以直接放过。

获取当前系统的OpenGL的版本

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <GL/glew.h>
#include <GLFW/glfw3.h>


int main(int argc, char const *argv[]){
    glfwInit();
    GLFWwindow *window = glfwCreateWindow(800, 600, "顶点:Vertex", NULL, NULL); 
    glfwMakeContextCurrent(window);
    if (glewInit() != GLEW_OK){
        printf("OpenGL初始化失败:glew\n");
        exit(-1);
    }
    // ********************************
    const GLubyte* name = glGetString(GL_VENDOR); //返回负责当前OpenGL实现厂商的名字
    const GLubyte* video = glGetString(GL_RENDERER); //返回一个渲染器标识符,通常是个硬件平台
    const GLubyte* OpenGLVersion = glGetString(GL_VERSION); //返回当前OpenGL实现的版本号
    printf("OpenGL实现厂商的名字:%s\n", name);
    printf("显示器显卡:%s\n", video);
    printf("OpenGL实现的版本号:%s\n", OpenGLVersion);
    // 释放
    glfwTerminate();
    return 0;
}

// 编译命令:g++ -omain  gl00_opengl_version.cpp  -lglfw -lglew -framework opengl

  • 说明:
    • 尽管上面不需要Window,但是需要创建上下文,在glfw模块中创建上下文与创建窗体是一起的。
    • 本文使用的系统的OpenGL环境如下:

    OpenGL实现厂商的名字:Intel Inc.
    显示器显卡:Intel(R) Iris(TM) Graphics 6100
    OpenGL实现的版本号:2.1 INTEL-12.9.22

系统安装的OpenGL版本查看

  • 如果想知道硬件支持与系统安装的OpenGL版本,可以在硬件与系统提供商的官网查看。比如苹果的OpenGL版本可以官网查看:
    • https://support.apple.com/zh-cn/HT202823
    • Mac系统的OpenGL版本信息

设置最高的OpenGL版本

  • 在创建上下文之前设置,通过窗体创建前的提示设置即可glfwWindowHint
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <GL/glew.h>
#include <GLFW/glfw3.h>



int main(int argc, char const *argv[]){
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);   // 主版本
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);   // 副版本(可选)
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);    // 设置苹果系统向后兼容

    GLFWwindow *window = glfwCreateWindow(800, 600, "顶点:Vertex", NULL, NULL); 
    glfwMakeContextCurrent(window);
    if (glewInit() != GLEW_OK){
        printf("OpenGL初始化失败:glew\n");
        exit(-1);
    }
    // ********************************
    const GLubyte* name = glGetString(GL_VENDOR); //返回负责当前OpenGL实现厂商的名字
    const GLubyte* video = glGetString(GL_RENDERER); //返回一个渲染器标识符,通常是个硬件平台
    const GLubyte* OpenGLVersion = glGetString(GL_VERSION); //返回当前OpenGL实现的版本号
    printf("OpenGL实现厂商的名字:%s\n", name);
    printf("显示器显卡:%s\n", video);
    printf("OpenGL实现的版本号:%s\n", OpenGLVersion);
    // 释放
    glfwTerminate();
    return 0;
}

// 编译命令:g++ -omain  gl00_opengl_version.cpp  -lglfw -lglew -framework opengl

  • 设置后输出位设置的版本:
    OpenGL实现厂商的名字:Intel Inc.
    显示器显卡:Intel(R) Iris(TM) Graphics 6100
    OpenGL实现的版本号:4.1 INTEL-12.9.22

绘制基本图元

绘制图元的基本模式

  1. 定义顶点数据:类型float数组,
  2. 创建顶点缓冲区对象:glGenBuffers函数;
  3. 指定顶点缓冲区数据类型:glBindBuffer函数;
  4. 设置顶点缓冲区的数据:glBufferData函数;
  5. 启用指定索引的顶点属性:glEnableVertexAttribArray函数;
  6. 定义通用顶点属性数据数组:glVertexAttribPointer函数;
  7. 绘制图元:glDrawArrays函数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <GL/glew.h>
#include <GLFW/glfw3.h>

int main(int argc, char const *argv[]){
    glfwInit();
    GLFWwindow *window = glfwCreateWindow(800, 600, "三角形", NULL, NULL); 
    glfwMakeContextCurrent(window);
    if (glewInit() != GLEW_OK){
        exit(-1);
    }
   
    // 1. 定义顶点数据;
    float vertices[] = {
        -0.5f, -0.5f, 0.0f,  // 第一个点
         0.5f, -0.5f, 0.0f,  // 第二个点
         0.0f,  0.5f, 0.0f   // 第三个点  
    }; 
    // 2. 生成缓冲区对象名;
    GLuint vertex_buffer_object;    
    glGenBuffers(1, &vertex_buffer_object);     
    
    // 3. 绑定命名缓冲区对象;
    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object);  

    // 4. 创建并初始化缓冲区对象的数据存储;
    glBufferData(
        GL_ARRAY_BUFFER,
        sizeof(vertices),
        vertices,
        GL_STATIC_DRAW);            
    // 5. 开启顶点属性;
    glEnableVertexAttribArray(0); 

    // 6. 定义通用顶点属性数据数组;
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); 
    

    while (! glfwWindowShouldClose(window)){
        glClear(GL_COLOR_BUFFER_BIT);
        // 7. 绘制;    
        glDrawArrays(GL_TRIANGLES, 0, 3); 
 
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
 
    glDeleteBuffers(1, &vertex_buffer_object);
    glfwTerminate();
    return 0;
}

// 编译命令:g++ -omain  gl02_vertexes.cpp  -lglfw -lglew -framework opengl

  • 运行是看不到三角形滴。

使用着色器

  • 着色器使用了GLSL语言,这是一个类似C或者C++的语言,着色器主要用来处理两个事情:
    • 多输入的顶点坐标进行变换处理(因为输入的3D坐标,但实际屏幕是2D,这个需要转换)
    • 把坐标最终转换为像素(RGBA构成的像素)
  • 除了GLSL语言外,剩下的就是套路(俗称编程模式)。

着色器编程模式

  1. 编写Shader:

    • 顶点Shader(vetexShader);
    • 片Shader(ragmentShader);
  2. 创建顶点着色器对象

    • 创建vertexShader对象;glCreateShader
    • 加载vertexShader脚本到vertexShader对象;glShaderSource
    • 编译vertexShader脚本;glCompileShader
  3. 创建片着色器

    • 创建fragmentShader对象;glCreateShader
    • 加载fragmentShader脚本到fragmentShader对象;glShaderSource
    • 编译fragmentShader脚本;glCompileShader
  4. 创建着色器程序

    • 创建程序对象;glCreateProgram
    • 添加vertexShader对象到着色器程序;glAttachShader
    • 添加fragmentShader对象到着色器程序;glAttachShader
    • 链接着色器程序,以备渲染管道调用;glLinkProgram
  5. 在开启顶点属性之前需要创建一个顶点数组:(用于顶点属性的处理)

    • 创建一个顶点数组对象;glGenVertexArrays
    • 绑定顶点数组对象;glBindVertexArray
    • 注意:这个与顶点缓冲区不同,不需要绑定数据,因为这个顶点数组内部存放数据;
  • 注意:
    • 着色器程序的创建与顶点缓冲区的创建没有先后关系,但是使用着色器程序的顶点在渲染过程需要顶点数组。

着色器与OpenGL对应版本关系


    /*
    OpenGL Version  GLSL Version
    2.0             110
    2.1             120
    3.0             130
    3.1             140
    3.2             150
    3.3             330
    4.0             400
    4.1             410
    4.2             420
    4.3             430
     */

着色器使用代码

  1. 代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
/*
OpenGL Version  GLSL Version
2.0             110
2.1             120
3.0             130
3.1             140
3.2             150
3.3             330
4.0             400
4.1             410
4.2             420
4.3             430
 */
// 苹果的OpenGL版本可以官网查看:https://support.apple.com/zh-cn/HT202823
// Intel(R) Iris(TM) Graphics 6100:
// MacBook Pro(视网膜显示屏,13 英寸,2015 年初) Intel Iris Graphics 6100     4.1(OpenGL版本)
// GLSL着色器语言
/*
    #version 410 core
    layout (location = 0) in vec3 aPos;

    void main(){
        gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
    }
 */
const char *vertexShaderSource = ""
    "#version 410 core\n"                          // OpenGL版本,核心模式
    "layout (location = 0) in vec3 aPos;\n"        // 顶点属性(输入)
    "void main(){\n"
    "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\0";   // 空字符

const char *fragmentShaderSource = ""
    "#version 410 core\n"
    "out vec4 FragColor;\n"        // 颜色属性(输出变量)
    "void main(){\n"
    "   FragColor = vec4(1.0f, 0.0f, 0.0f, 1.0f);\n"   // 固定颜色输出
    "}\n\0";

int main(int argc, char const *argv[]){
    glfwInit();
    // 下面代码使用最新版本的OpenGL(一定需要glfw初始化后调用,下面版本不设置,会导致某些功能不支持,会导致段错误)
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);   // 主版本
    // glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);   // 副版本(可选)
    GLFWwindow *window = glfwCreateWindow(
        800, 
        600, 
        "着色器", 
        NULL, //glfwGetPrimaryMonitor(), 
        NULL); 
    glfwMakeContextCurrent(window);
    if (glewInit() != GLEW_OK){
        printf("OpenGL初始化失败:glew\n");
        exit(-1);
    }
    /////////////////////////////


    // ********************************
    // 1. 顶点着色器对象
    unsigned int vertexShader;
    vertexShader = glCreateShader(GL_VERTEX_SHADER);
    // 2. 编译顶点着色器
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);  
    // --------------------------------
    // 1. 片着色器对象
    unsigned int fragmentShader;
    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    // 2. 片着色器
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
    // --------------------------------
    // 1. 着色器程序对象
    unsigned int shaderProgram;
    shaderProgram = glCreateProgram();
    // 2. 链接着色器程序
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    // ---------------------------------
    // 1. 激活着色器程序
    // glUseProgram(shaderProgram);
    // 2. 激活后,释放前面分配的内存
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    // ********************************
    const GLubyte* name = glGetString(GL_VENDOR); //返回负责当前OpenGL实现厂商的名字
    const GLubyte* video = glGetString(GL_RENDERER); //返回一个渲染器标识符,通常是个硬件平台
    const GLubyte* OpenGLVersion = glGetString(GL_VERSION); //返回当前OpenGL实现的版本号
    printf("OpenGL实现厂商的名字:%s\n", name);
    printf("显示器显卡:%s\n", video);
    printf("OpenGL实现的版本号:%s\n", OpenGLVersion);
    ///////////////////////////
    // 1. 定义顶点数据;
    float vertices[] = {
        -0.5f, -0.5f, 0.0f, // left  
         0.5f, -0.5f, 0.0f, // right 
         0.0f,  0.5f, 0.0f  // top   
    }; 

    // 2. 生成缓冲区对象名;
    GLuint vertex_buffer_object;    // 定义存储缓冲区对象名的变量
    glGenBuffers(
        1,                          //  缓冲区个数
        &vertex_buffer_object);     // 返回的缓冲区对象名
    
    // 3. 绑定命名缓冲区对象;
    glBindBuffer(
        GL_ARRAY_BUFFER,            // 缓冲区类型是:顶点属性(Vertex attributes)
        vertex_buffer_object);      // 缓冲区对象名

    // 4. 创建并初始化缓冲区对象的数据存储;
    glBufferData(
        GL_ARRAY_BUFFER,            // 指定缓冲区对象绑定的目标
        sizeof(vertices),           // 指定缓冲区对象的新数据存储区的大小
        vertices,                   // 定指向将复制到数据存储中进行初始化的数据的指针,如果不复制任何数据,则指定为空。
        GL_STATIC_DRAW);            // 数据存储内容将重复修改并多次使用,并用作GL绘图和图像操作的源。
            /////////////////////////////////////////////
    // ------------------------------------
    unsigned int VAO;       
    glGenVertexArrays(1, &VAO);    
    glBindVertexArray(VAO);
    // ------------------------------------
    // 1. 开启顶点属性;
    glEnableVertexAttribArray(
        0);                     // 启用的顶点属性的索引。;
    // 2. 定义通用顶点属性数据数组;
    glVertexAttribPointer(
        0,                      // 指定通用顶点属性的索引; 
        3,                      // 指定每个顶点属性的维数;
        GL_FLOAT,               // 指定数组中每个组件的数据类型;
        GL_FALSE,               // 指定访问固定点数据值时,是否规范化;
        3 * sizeof(float),      // 指定顶点属性之间的步长;
        0);                     // 指定当前绑定到GL_ARRAY_BUFFER目标的存储区地址的偏移地址;
    

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // glBindVertexArray(0);   // 解除顶点数组对象的绑定,便于后面绑定使用;
    ///////////////////////////////////////////
    while (! glfwWindowShouldClose(window)){
        // glClearColor(1.0f, 1.0f, 1.0f, 1.0f);  // 可以设置清屏颜色;
        glClear(GL_COLOR_BUFFER_BIT);
        glUseProgram(shaderProgram);
        // 3. 绘制;    
        glDrawArrays(
            GL_TRIANGLES,            // 渲染的基元类型(点);
            0,                       // 指定已启用数组中的起始索引;
            3);                      // 渲染的点的个数;
        ///////////////////////////
        glfwSwapBuffers(window);
        glFlush();
        // glfwPollEvents();
        glfwWaitEvents();
    }
    // 释放
    // 4. 绘制完成,禁用顶点缓冲,这是为了性能;
    glDisableVertexAttribArray(
            0                        // 指定禁用的顶点属性的索引;
    ); 
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &vertex_buffer_object);
    glfwTerminate();
    return 0;
}

// 编译命令:g++ -omain  gl03_shader_step.cpp  -lglfw -lglew -framework opengl


  1. 运行结果
    • 使用着色器渲染图元

着色器编程使用的函数说明

glCreateShader函数

  1. 函数定义
    • glCreateShader创建一个空的shader对象,并返回一个可以引用它的非零值。
    • Shader对象用于维护定义Shader的源代码字符串。
    • shaderType指示要创建的Shader的类型。
    • OpenGL支持五种类型的Shader。
    GLuint glCreateShader(  GLenum shaderType);
  1. 函数参数:
    • GLenum shaderType:指定Shader的类型,GL可以申明的类型包含:
      • GL_COMPUTE_SHADER
      • GL_VERTEX_SHADER
      • GL_TESS_CONTROL_SHADER 与 GL_TESS_EVALUATION_SHADER
      • GL_GEOMETRY_SHADER
      • GL_FRAGMENT_SHADER.
    • 注意:
      • 这里创建两种最基本的Shader。

glShaderSource函数

  1. 函数定义
void glShaderSource(    
    GLuint shader,
    GLsizei count,  
    const GLchar **string,
    const GLint *length);

  1. 函数参数

    • GLuint shader:Shader对象;
    • GLsizei count:字符串个数;
    • const GLchar **string:字符串数组(不是数组字符串:双指针);
    • const GLint *length:字符串长度的数组;如果是空,则表示字符串使用空表示结束;
  2. 说明:

    • 当调用glShaderSource时,OpenGL复制Shader源代码字符串,因此应用程序可以在函数返回后立即释放其源代码字符串的副本。

glCompileShader函数

  1. 函数定义
    void glCompileShader(   GLuint shader);

  1. 函数参数

    • GLuint shader:已经创建,并copy了Shader源代码的Shader对象。
  2. 说明:

    • glCompileShader编译存储在Shader对象中的源代码字符串。

    • 编译状态将作为着色器对象状态的一部分存储。如果在没有错误的情况下编译了明暗器并准备好使用,则此值将设置为GL_TRUE,否则设置为GL_FALSE。

    • 编译状态可以通过调用带有shade参数r和GL_COMPILE_STATUS参数的glGetShader函数进行查询。

    • 由于OpenGL着色语言规范指定的许多原因,着色程序的编译可能会失败。无论编译是否成功,都可以通过调用glGetShaderInfo从着色器对象的信息日志中获取有关编译的信息(包含编译错误的位置)。

glCreateProgram函数

  1. 函数定义
    GLuint glCreateProgram( void);
  1. 函数返回值

    • GLuint:返回创建的程序对象。
  2. 说明:

    • 创建的程序是空的,可以指定Shader,并链接成程序。

glAttachShader函数

  1. 函数定义
void glAttachShader(    
    GLuint program,
    GLuint shader);

  1. 函数参数

    • GLuint program:创建好的程序对象;
    • GLuint shader:需要添加到程序的Shader
  2. 说明

    • 删除Shader使用glDetachShader函数。

glLinkProgram函数

  1. 函数定义

void glLinkProgram( GLuint program);

  1. 函数参数
    • GLuint program:创建好,并添加Shader的程序对象。

glGenVertexArrays函数

  1. 函数定义
void glGenVertexArrays( 
    GLsizei n,           
    GLuint *arrays);
 
  1. 函数参数
    • GLsizei n:创建的顶点数组个数;
    • GLuint *arrays:返回创建的顶点数组对象数组;

glBindVertexArray函数

  1. 函数定义
void glBindVertexArray( GLuint array);
  1. 函数参数

    • GLuint array:需要绑定的顶点数组对象;
  2. 说明:

    • glBindVertexArray使用名称数组绑定顶点数组对象。参数array是先前从调用glGenVertexArrays返回的顶点数组对象的名称

    • 或者为零以中断现有的顶点数组对象绑定。

    • 如果不存在名为array的顶点数组对象,则在数组第一次绑定时创建一个顶点数组对象。

    • 如果绑定成功,则不会更改顶点数组对象的状态,并且任何先前的顶点数组对象绑定都将断开。


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

推荐阅读更多精彩内容