Opengl ES之VBO和VAO

前言

本文主要介绍了什么是VBO/VAO,为什么需要使用VBO/VAO以及如何使用VBO和VAO。

VBO

什么是VBO

VBO(vertex Buffer Object):顶点缓冲对象。是在显卡存储空间中开辟的一块区域,在显卡存储空间中开辟一块区域,用于存放顶点的各类属性信息。如顶点坐标、纹理坐标、顶点颜色等数据。
在渲染时直接从显VBO去取数据而不必与CPU进行数据交换。

为什么需要使用VBO

将顶点数据保存在内存中,在调用glDrawArrays或者glDrawElements等绘制方法前需要调用相应的方法将数据送入显存,I/O开销大,性能不够好。
若采用顶点缓冲区对象存放顶点数据,则不需要在每次绘制前都将顶点数据复制进显存,而是在初始化顶点缓冲区对象时一次性将顶点数据送入显存,
每次绘制时直接使用显存中的数据,可以大大提高渲染性能。

如何使用VBO

  • 使用函数glGenBuffers和一个缓冲ID生成一个VBO对象:
unsigned int VBO;
glGenBuffers(1, &VBO);
  • 使用函数glBindBuffer绑定顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER

OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。

glBindBuffer(GL_ARRAY_BUFFER, VBO); 
  • 使用函数glBufferData把定义好的顶点数据复制到缓冲的内存中:
// vertices表示顶点数组
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。它的第一个参数是目标缓冲的类型,其中VBO代表是的GL_ARRAY_BUFFER。第二个参数指定传输数据的大小(以字节为单位),用一个简单的sizeof计算出顶点数据大小就行。
第三个参数是我们希望发送的实际数据。第四个参数指定了我们希望显卡如何管理给定的数据,它有三种形式:

GL_STATIC_DRAW :数据不会或几乎不会改变。
GL_DYNAMIC_DRAW:数据会被改变很多。
GL_STREAM_DRAW :数据每次绘制时都会改变。

一般情况下位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型一般是GL_STATIC_DRAW。如果一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,这样就能确保显卡把数据放在能够高速写入的内存部分。

  • 用完后使用函数glDeleteBuffers删除缓冲区

今天我们以之前绘制四边形的实践为例子,使用VBO的方式来实现四边形的绘制:Opengl ES之四边形绘制

我们的目标是灵活使用Opengl绘制一个蓝色的四边形...

普通常规的绘制这里就不多说了,后续可以看代码,或者回顾之前的四边形绘制的文章,这里主要介绍搭配VBO的两种绘制方式:

首先它们使用的顶点着色器和片段着色器都是一样的,都是:

// 顶点着色器
static const char *ver = "#version 300 es\n"
                         "in vec4 aColor;\n"
                         "in vec4 aPosition;\n"
                         "out vec4 vColor;\n"
                         "void main() {\n"
                         "    vColor = aColor;\n"
                         "    gl_Position = aPosition;\n"
                         "}";

// 片元着色器
static const char *fragment = "#version 300 es\n"
                              "precision mediump float;\n"
                              "in vec4 vColor;\n"
                              "out vec4 fragColor;\n"
                              "void main() {\n"
                              "    fragColor = vColor;\n"
                              "}";
  1. 顶点坐标与颜色值坐标分离的方式(数组结构)

先看顶点数据与颜色数据:


// 使用绘制两个三角形组成一个矩形的形式(三角形带)
// 第一第二第三个点组成一个三角形,第二第三第四个点组成一个三角形
const static GLfloat VERTICES[] = {
        0.5f,-0.5f, // 右下
        0.5f,0.5f, // 右上
        -0.5f,-0.5f, // 左下
        -0.5f,0.5f, // 左上
};

// vbo颜色
const static GLfloat COLOR_ICES[] = {
        0.0f,0.0f,1.0f,1.0f,
        0.0f,0.0f,1.0f,1.0f,
        0.0f,0.0f,1.0f,1.0f,
        0.0f,0.0f,1.0f,1.0f,
};

VBO数据与绑定:

  // vbo
    glGenBuffers(3,vbo);
    glBindBuffer(GL_ARRAY_BUFFER,vbo[0]);
    glBufferData(GL_ARRAY_BUFFER,sizeof(VERTICES),VERTICES,GL_STATIC_DRAW);

    // 颜色
    glBindBuffer(GL_ARRAY_BUFFER,vbo[1]);
    glBufferData(GL_ARRAY_BUFFER,sizeof(COLOR_ICES),COLOR_ICES,GL_STATIC_DRAW);

主要绘制代码:

     // 使用VBO的方式绘制,顶点与颜色分开
//    glBindBuffer(GL_ARRAY_BUFFER,vbo[0]);
//    glVertexAttribPointer(positionHandle,2,GL_FLOAT,GL_FALSE,0,(void *)0);
//    // 启用顶点数据
//    glEnableVertexAttribArray(positionHandle);
//
//    glBindBuffer(GL_ARRAY_BUFFER,vbo[1]);
//    glVertexAttribPointer(colorHandle,4,GL_FLOAT,GL_FALSE,0,(void *)0);
//    // 启用颜色顶点数据
//    glEnableVertexAttribArray(colorHandle);
//    // 解除绑定
//    glBindBuffer(GL_ARRAY_BUFFER,0);
  1. 顶点坐标与颜色值坐标结合的方式(结构数组)

顶点数据与颜色数据混合打乱:

const static GLfloat VERTICES_AND_COLOR[] = {
        0.5f,-0.5f, // 右下
        // 颜色
        0.0f,0.0f,1.0f,1.0f,
        0.5f,0.5f, // 右上
        // 颜色
        0.0f,0.0f,1.0f,1.0f,
        -0.5f,-0.5f, // 左下
        // 颜色
        0.0f,0.0f,1.0f,1.0f,
        -0.5f,0.5f, // 左上
        // 颜色
        0.0f,0.0f,1.0f,1.0f,
};

VBO数据绑定:

    // vbo
    glGenBuffers(3,vbo);
    glBindBuffer(GL_ARRAY_BUFFER,vbo[0]);
    glBufferData(GL_ARRAY_BUFFER,sizeof(VERTICES),VERTICES,GL_STATIC_DRAW);

    // 颜色
    glBindBuffer(GL_ARRAY_BUFFER,vbo[1]);
    glBufferData(GL_ARRAY_BUFFER,sizeof(COLOR_ICES),COLOR_ICES,GL_STATIC_DRAW);

    // 顶点与颜色混合,先顶点坐标,再颜色坐标
    glBindBuffer(GL_ARRAY_BUFFER,vbo[2]);
    glBufferData(GL_ARRAY_BUFFER,sizeof(VERTICES_AND_COLOR),VERTICES_AND_COLOR
                 ,GL_STATIC_DRAW);

主要绘制代码:

    // VBO绘制 顶点坐标与颜色坐标一起
//    glBindBuffer(GL_ARRAY_BUFFER,vbo[2]);
//    // stride 步长 每个顶点坐标之间相隔6个数据点,数据类型是float
//    glVertexAttribPointer(positionHandle,2,GL_FLOAT,GL_FALSE,6 * sizeof(float ),(void *)0);
//    // 启用顶点数据
//    glEnableVertexAttribArray(positionHandle);
//    // stride 步长 每个颜色坐标之间相隔6个数据点,数据类型是float,颜色坐标索引从2开始
//    glVertexAttribPointer(colorHandle,4,GL_FLOAT,GL_FALSE,6 * sizeof(float ) ,(void *)(2 * sizeof(float)) );
//    // 启用颜色顶点数据
//    glEnableVertexAttribArray(colorHandle);
//    // 解除绑定
//    glBindBuffer(GL_ARRAY_BUFFER,0);

它们的运行结果都是成功绘制了一个蓝色的四边形:
[图片上传失败...(image-18c8a8-1663898992041)]

性能

在上面我们使用了多种不同的组合方式进行了四边形的绘制,其中有将顶点坐标和颜色坐标分离写在两个不同的数组的方式,也有将顶点坐标和颜色坐标组合写在同一个数组,然后使用步长(stride)和偏移量(*pointer)参数控制的方式进行绘制,那么这两种方式那种性能更佳呢?

其中将各种顶点坐标分离在不同数组的写法又成为数组结构,而将各种顶点坐标合并成一个数组的写法又称为结构数组,在《OPENGL ES 3.0编程指南》一书中作者指出,结构数组的写法性能更好。

在大部分情况下,答案是结构数组。
原因是,每个顶点的属性数据可以顺序方式读取,这最有可能造成高效的内存访问模式。

VAO

什么是VAO

VAO(vertex Array Object):顶点数组对象。

注意: VAO是OpenGL ES 3.0之后才推出的新特性, 所以在使用VAO前需要确定OpenGL ES的版本是否是3.0之后的版本。

顶点数组对象可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。

一个顶点数组对象会储存以下这些内容:

  • glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
  • 通过glVertexAttribPointer设置的顶点属性配置。
  • 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。

在上面VBO的介绍中我们知道每次在绘制的时候都需要频繁地绑定与解绑VBO,每次绘制还需要取出VBO中的数据进行赋值之后才能进行绘制渲染。当数据量大的时候,重复这些操作就会变得很繁琐。通过VAO就可以简化这个过程,因此VAO可以简单理解成VBO的管理者,避免在帧绘制时再去手动操纵VBO,VAO不能单独使用,
需要搭配VBO使用。

对于GPU来说VBO就是一堆数据,但是这堆数据怎么解析使用,需要glEnableVertexAttribArray等相关函数在每次绘制的时候告诉GPU,那么VAO的作用就是简化这个过程的,只需要在初始化的时候将这些解析逻辑与VAO绑定一次即可,
然后每次在绘制的时候只需绑定对应的VAO,而不必每次再绑定VBO,然后去告诉GPU如何解析相关数据了,可以说是一个性能的优化了。

[图片上传失败...(image-9e2a55-1663898992041)]

如何使用VAO

  • 首先调用函数glGenVertexArrays生成VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
  • 调用函数glBindVertexArray绑定VAO
glBindVertexArray(VAO);
// 管理VBO,让VAO记住VBO的数据如何解析使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);

然后在绘制的时候使用函数glBindVertexArray(VAO)使用即可。

  • 退出时通过函数glDeleteVertexArrays删除VAO
glDeleteVertexArrays

主要绑定代码:

    // VAO
    glGenVertexArrays(1,&vao);
    glBindVertexArray(vao);
    // VAO与VBO关联
    glBindBuffer(GL_ARRAY_BUFFER,vbo[2]);
    // stride 步长 每个顶点坐标之间相隔6个数据点,数据类型是float
    glVertexAttribPointer(positionHandle,2,GL_FLOAT,GL_FALSE,6 * sizeof(float ),(void *)0);
    // 启用顶点数据
    glEnableVertexAttribArray(positionHandle);
    // stride 步长 每个颜色坐标之间相隔6个数据点,数据类型是float,颜色坐标索引从2开始
    glVertexAttribPointer(colorHandle,4,GL_FLOAT,GL_FALSE,6 * sizeof(float ) ,(void *)(2 * sizeof(float)) );
    // 启用颜色顶点数据
    glEnableVertexAttribArray(colorHandle);
    // 解除绑定
    glBindBuffer(GL_ARRAY_BUFFER,0);

    glBindVertexArray(0);

主要使用绘制代码:

   // VBO与VAO配合绘制
    // 使用vao
    glBindVertexArray(vao);
    // 4个顶点绘制两个三角形组成矩形
    glDrawArrays(GL_TRIANGLE_STRIP,0,4);
    glUseProgram(0);
    // vao解除绑定
    glBindVertexArray(vao);

下面是完整代码:

VBOVAOOpengl.h

static const int NUM_VBO = 3;

class VBOVAOOpengl:public BaseOpengl{

public:
    VBOVAOOpengl();
    virtual ~VBOVAOOpengl();
    virtual void onDraw();
private:
    GLint positionHandle{-1};
    GLint colorHandle{-1};
    GLuint vbo[NUM_VBO];
    GLuint vao{0};

};

VBOVAOOpengl.cpp

// 顶点着色器
static const char *ver = "#version 300 es\n"
                         "in vec4 aColor;\n"
                         "in vec4 aPosition;\n"
                         "out vec4 vColor;\n"
                         "void main() {\n"
                         "    vColor = aColor;\n"
                         "    gl_Position = aPosition;\n"
                         "}";

// 片元着色器
static const char *fragment = "#version 300 es\n"
                              "precision mediump float;\n"
                              "in vec4 vColor;\n"
                              "out vec4 fragColor;\n"
                              "void main() {\n"
                              "    fragColor = vColor;\n"
                              "}";

// 使用绘制两个三角形组成一个矩形的形式(三角形带)
// 第一第二第三个点组成一个三角形,第二第三第四个点组成一个三角形
const static GLfloat VERTICES[] = {
        0.5f,-0.5f, // 右下
        0.5f,0.5f, // 右上
        -0.5f,-0.5f, // 左下
        -0.5f,0.5f, // 左上
};

const static GLfloat VERTICES_AND_COLOR[] = {
        0.5f,-0.5f, // 右下
        // 颜色
        0.0f,0.0f,1.0f,1.0f,
        0.5f,0.5f, // 右上
        // 颜色
        0.0f,0.0f,1.0f,1.0f,
        -0.5f,-0.5f, // 左下
        // 颜色
        0.0f,0.0f,1.0f,1.0f,
        -0.5f,0.5f, // 左上
        // 颜色
        0.0f,0.0f,1.0f,1.0f,
};

// rgba
//const static GLfloat COLOR_ICES[] = {
//        0.0f,0.0f,1.0f,1.0f
//};

// vbo颜色
const static GLfloat COLOR_ICES[] = {
        0.0f,0.0f,1.0f,1.0f,
        0.0f,0.0f,1.0f,1.0f,
        0.0f,0.0f,1.0f,1.0f,
        0.0f,0.0f,1.0f,1.0f,
};

VBOVAOOpengl::VBOVAOOpengl() {
    initGlProgram(ver,fragment);
    positionHandle = glGetAttribLocation(program,"aPosition");
    colorHandle = glGetAttribLocation(program,"aColor");
    // vbo
    glGenBuffers(3,vbo);
    glBindBuffer(GL_ARRAY_BUFFER,vbo[0]);
    glBufferData(GL_ARRAY_BUFFER,sizeof(VERTICES),VERTICES,GL_STATIC_DRAW);

    // 颜色
    glBindBuffer(GL_ARRAY_BUFFER,vbo[1]);
    glBufferData(GL_ARRAY_BUFFER,sizeof(COLOR_ICES),COLOR_ICES,GL_STATIC_DRAW);

    // 顶点与颜色混合,先顶点坐标,再颜色坐标
    glBindBuffer(GL_ARRAY_BUFFER,vbo[2]);
    glBufferData(GL_ARRAY_BUFFER,sizeof(VERTICES_AND_COLOR),VERTICES_AND_COLOR
                 ,GL_STATIC_DRAW);

    glBindBuffer(GL_ARRAY_BUFFER,0);

    // VAO
    glGenVertexArrays(1,&vao);
    glBindVertexArray(vao);
    // VAO与VBO关联
    glBindBuffer(GL_ARRAY_BUFFER,vbo[2]);
    // stride 步长 每个顶点坐标之间相隔6个数据点,数据类型是float
    glVertexAttribPointer(positionHandle,2,GL_FLOAT,GL_FALSE,6 * sizeof(float ),(void *)0);
    // 启用顶点数据
    glEnableVertexAttribArray(positionHandle);
    // stride 步长 每个颜色坐标之间相隔6个数据点,数据类型是float,颜色坐标索引从2开始
    glVertexAttribPointer(colorHandle,4,GL_FLOAT,GL_FALSE,6 * sizeof(float ) ,(void *)(2 * sizeof(float)) );
    // 启用颜色顶点数据
    glEnableVertexAttribArray(colorHandle);
    // 解除绑定
    glBindBuffer(GL_ARRAY_BUFFER,0);

    glBindVertexArray(0);

    LOGD("program:%d",program);
    LOGD("positionHandle:%d",positionHandle);
    LOGD("colorHandle:%d",colorHandle);
}

void VBOVAOOpengl::onDraw() {
    // 清屏
    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(program);

    // 普通方式绘制
//    /**
//     * size 几个数字表示一个点,显示是两个数字表示一个点
//     * normalized 是否需要归一化,不用,这里已经归一化了
//     * stride 步长,连续顶点之间的间隔,如果顶点直接是连续的,也可填0
//     */
//    glVertexAttribPointer(positionHandle,2,GL_FLOAT,GL_FALSE,0,VERTICES);
//    // 启用顶点数据
//    glEnableVertexAttribArray(positionHandle);
//
//    // 这个不需要glEnableVertexAttribArray
//    glVertexAttrib4fv(colorHandle, COLOR_ICES);

// ############################################# 分割线 #################################
     // 使用VBO的方式绘制,顶点与颜色分开
    //    /**
//    glBindBuffer(GL_ARRAY_BUFFER,vbo[0]);
//    glVertexAttribPointer(positionHandle,2,GL_FLOAT,GL_FALSE,0,(void *)0);
//    // 启用顶点数据
//    glEnableVertexAttribArray(positionHandle);
//
//    glBindBuffer(GL_ARRAY_BUFFER,vbo[1]);
//    glVertexAttribPointer(colorHandle,4,GL_FLOAT,GL_FALSE,0,(void *)0);
//    // 启用颜色顶点数据
//    glEnableVertexAttribArray(colorHandle);
//    // 解除绑定
//    glBindBuffer(GL_ARRAY_BUFFER,0);

// ############################################# 分割线 #################################
    // VBO绘制 顶点坐标与颜色坐标一起
//    glBindBuffer(GL_ARRAY_BUFFER,vbo[2]);
//    // stride 步长 每个顶点坐标之间相隔6个数据点,数据类型是float
//    glVertexAttribPointer(positionHandle,2,GL_FLOAT,GL_FALSE,6 * sizeof(float ),(void *)0);
//    // 启用顶点数据
//    glEnableVertexAttribArray(positionHandle);
//    // stride 步长 每个颜色坐标之间相隔6个数据点,数据类型是float,颜色坐标索引从2开始
//    glVertexAttribPointer(colorHandle,4,GL_FLOAT,GL_FALSE,6 * sizeof(float ) ,(void *)(2 * sizeof(float)) );
//    // 启用颜色顶点数据
//    glEnableVertexAttribArray(colorHandle);
//    // 解除绑定
//    glBindBuffer(GL_ARRAY_BUFFER,0);

// ############################################# 分割线 #################################

    // VBO与VAO配合绘制
    // 使用vao
    glBindVertexArray(vao);
    // 4个顶点绘制两个三角形组成矩形
    glDrawArrays(GL_TRIANGLE_STRIP,0,4);
    glUseProgram(0);
    // vao解除绑定
    glBindVertexArray(vao);

    // 禁用顶点
    glDisableVertexAttribArray(positionHandle);
    if(nullptr != eglHelper){
        eglHelper->swapBuffers();
    }
}

VBOVAOOpengl::~VBOVAOOpengl() noexcept {
    glDeleteBuffers(NUM_VBO,vbo);
    glDeleteVertexArrays(1,&vao);
}

往期笔记

Opengl ES之EGL环境搭建
Opengl ES之着色器
Opengl ES之三角形绘制
Opengl ES之四边形绘制
Opengl ES之纹理贴图

关注我,一起进步,人生不止coding!!!

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

推荐阅读更多精彩内容