OpenGL学习资料和记录

学习资料

OpenGL:
3D图形学:
OpenGL ES:
OpenGL 内存/性能优化:

VAO 和VBO的使用流程区别
OpenGLES顶点缓冲
android平台下OpenGL ES 3.0实例详解顶点缓冲区对象(VBO)和顶点数组对象(VAO)

VAO在OpenGL3.0上才能用。在OpenGL2.0上,使用VBO的步骤大致如下:

1. GenBuffer (called once in the whole rendering lifecycle)
2. glUseProgram(m_ProgId); (called on every draw for this one and the following commands)
3. BindBuffer to type GL_ARRAY_BUFFER 
4. glBufferData or glBufferSubData() (called only when data has changed)
5. EnableVertexAttribArray   
6. glVertexAttribPointer 
6. glBindBuffer to 0
7. glDrawXX
8. glDisableVertexAttribArray // 在iOS低端机上频繁调用会导致绘制无效,可以不调用或者在OpenGL环境销毁时调用
9. glUseProgram(0)
10. glDeleteBuffers  (called when the data is no longer used, ie. quit the Activity)

使用两个或以上的Buffer的时候注意: 调用glBufferData 和 glVertexAttribPointer前都要 BindBuffer 到对应的Buffer上. ( 比如顶点信息和参数信息分别使用 GLuint attribPositionBuffer 和 GLuint attribParamBuffer)

在做AE插件开发的时候,发现必须VBO结合VAO, 才能画粒子,并且要设置glEnable(GL_PROGRAM_POINT_SIZE);

VAO的使用示例:

1. GenBuffer (called once in the whole rendering lifecycle)
2. glUseProgram(m_ProgId); (called on every draw for this one and the following commands)
3. BindBuffer to type GL_ARRAY_BUFFER 
4. glBufferData or glBufferSubData() (called only when data has changed)
5. EnableVertexAttribArray   
6. glVertexAttribPointer 
6. glBindBuffer to 0
7. glDrawXX
8. glDisableVertexAttribArray // 在iOS低端机上频繁调用会导致绘制无效,可以不调用或者在OpenGL环境销毁时调用
9. glUseProgram(0)
10. glDeleteBuffers  (called when the data is no longer used, ie. quit the Activity)
#=========================配置VAO阶段===============================
unsigned int VBO[2], VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(2, VBO);
#=========================绑定VAO===============================
    glBindVertexArray(VAO); 
#=======================绑定第一个VBO============================
    glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
#===============================================================
        glBufferData(GL_ARRAY_BUFFER, sphereVertices.size() * sizeof(float), &sphereVertices[0], GL_STATIC_DRAW); //向第一个VBO中写入数据
#================告知VAO该如何解释第一个VBO的信息=================
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
#=======================解绑第一个VBO===========================
    glBindBuffer(GL_ARRAY_BUFFER, 0);
#===============================================================
#=======================绑定第二个VBO============================
    glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
#===============================================================
    glBufferData(GL_ARRAY_BUFFER, sizeof(texVertrices), texVertrices, GL_STATIC_DRAW);//向第二个VBO中写入数据
#================告知VAO该如何解释第二个VBO的信息=================
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
#=======================解绑第二个VBO===========================
glBindBuffer(GL_ARRAY_BUFFER, 0);
#================解绑VAO=================
glBindVertexArray(0);
..............
#================绘制阶段使用VAO=================
    glBindVertexArray(mVAO);
    glDrawArrays(GL_POINTS, 0, mCount);
    glBindVertexArray(0);
# 如果是一个VAO 对应一个VBO, 但VBO里包含两个属性,则VAO解释VBO的两行改成如下形式:
    // Position attribute
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);
    // Color attribute
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
    glEnableVertexAttribArray(1);

用Direct Textures或PBO提高glReadPixels、glTexImage2D性能
Android 关于美颜/滤镜 利用PBO从OpenGL录制视频
如果想在不同的渲染图层或者渲染步骤复用glReadPixels的结果,可以把对应的计算结果存储到纹理的RGB值上(单独一个图层),此纹理就可以作为资源Map引用共享给同一GLContext的不同对象使用了。比如R和G通道存储Mask图glReadPixels处理后得到的目标方框->变换矩阵对应的每个像素的x/y目标坐标。即坐标纹理。
这里要注意两点,1是如果用Alpha存储数据,要考虑Alpha预乘,2是在后面读取此坐标纹理上的值时,可能有精度损失,
引起锯齿。一个解决方案是把坐标纹理用小尺寸再画一遍(比如16 * 16),然后再使用时,因为纹理过滤模式设置为双线性插值,每个像素原来的采样误差,会被双线性插值平滑,就没有锯齿了,只有很轻微的整体位置偏移

GPUImage(OpenGL ES)的性能优化、爬坑与架构改善(原文已删,我在Github私人仓库有备份)

gldrawelements比gldrawarrays更节省GPU消耗(vertex reuse, flexibility)
着色器里不需要精确的数据降低精度; 不需要3D绘制时vTextureCoord可以定义成二维;计算尽量在顶点着色器而不是片段着色器。
Overcome Incoherent Memory Access in OpenGL ES 3.0

  • glDeleteTextures函数对内存的优化

  • OpenGL快问快答
    问答里有“OpenGL有内存泄漏吗?”
    如果一直不停创建texture,还是会使GPU增长。所以还是及时把不用的纹理删除比较好,或者选择更新纹理而不是创建新的纹理。然后GPU有自己的纹理复用回收机制,所以调用了glDeleteTextures方法,并不会马上释放纹理。

  • 一般不要在Shader里加if else判断,因为会影响性能,即使要加,也有替代方法:
    如何在shader中避免使用if else

如何打造一个高性能的前端智能推理引擎

pag的输入目标渲染纹理需要是没有和任何fbo绑定的,不然pag渲染会闪烁。
本来想通过
glFramebufferTexture2D(GL_FRAMEBUFFER,
                               GL_COLOR_ATTACHMENT0,
                               GL_TEXTURE_2D,
                               0,
                               0);
来给pag跟当前fbo解绑的纹理,这样就不用新建纹理了。但发现此方案的总渲染时间会更长。首先此方案需要每次都给pag构造新的BackendTexture对象,不然还是会闪烁。其次,可能每帧都调用glFramebufferTexture2D,比仅创建一次独立纹理更加占用GPU资源。最后,此方案给pag的是已经渲染过的纹理,pag去clean这个纹理的操作,也比给空白纹理时clean要更耗时
  • Gamma 校正
    对 Gamma 校正的个人理解
    颜色空间——Gamma与线性颜色空间
    基于gamma2.2的颜色空间叫做sRGB颜色。也就是sRGB里的颜色值是真实的线性颜色空间里的值经过如下gamma矫正后的
    image

    线性空间与GAMMA校正
    sRGB的话,保存的色值是偏亮的。
    只有OpenGL ES3.0才开始支持配置sRGB到线性空间的自动转换。不不配置的话,sRGB格式(照片和图片软件导出的图片一般都是sRGB格式)的texture,在texture2D后获取的是保存的原始的值,即是偏亮的RGB值。

在做亮度变换/色调分离等功能时,往往也会模拟Gamma矫正以有更符合预期的人眼感知效果:
posterize.frag(此Shader应该是预设输入是线性颜色空间的Texture)

调试:

Kodelife的实用安装指南

Shader:

ShaderToy绘制系列教程Shadertoy Tutorial Part 1 - Intro
优秀的Shader教程推荐
ShaderToy
边栏的小雨伞
shadertoy网站上的一些效果
KodeLife | Shader 实时编辑预览的强大工具使用实践
the Book of Shaders
Shader大神iq的教学网站

ShaderToy使用自定义视频纹理(安装Chrome插件后重启浏览器,然后将图片拖入编辑器底下的纹理槽即可)

  • 性能相关

    1. 复杂运算,如果能在c++代码里方便计算出来,就移到C++代码里计算,然后通过uniform传给shader
    2. 锐化/双边滤波等效果,可能会用到固定长度的坐标数组,这样的数组可以在顶点着色器里计算,然后通过varying参数传到Fragment着色器。因为一般情况下,顶点着色器执行的次数远远少片段着色器,所以这样做能节省性能
    3. 二维模糊卷积核请用先x后y,二次渲染的方式:Android图像处理 - 高斯模糊的原理及实现
    4. 计算量较多的fragment shader, 在不影响效果的情况下,尽量用mediump精度,以提高性能;顶点着色器一般用highp精度
  • 兼容性相关
    如果顶点着色器和片段着色器都用到了同一个uniform值,请不要同时在顶点着色器和片段着色器同时声明一样的uniform值。这样做可能会有兼容性问题。正确的做法是在顶点着色器再声明一个varying值,通过varying参数把值传递给片段着色器

粒子动画(点精灵):

OpenGL 点精灵效果
OPenGL点精灵
粒子
StarWars.Android 界面粉碎效果中的openGL操作解析
opengles绘制点精灵
smartGL

gl_PointCoord.png

(OpenGL标准纹理坐标的原点在左下角,但Android纹理坐标的原点在左上角,所以gl_fragcoord的原点也在左上角了)

对照:


OpenGL世界坐标系&Android纹理坐标系.png

(OpenGL标准纹理坐标的原点在左下角,但Android纹理坐标的原点在左上角)
update:其实是数据的原因,Android的Bitmap的坐标原点是左上角,Android端的OpenGL ES的glReadPixels等接口也没有对此适配,所以读入的图像数据就是颠倒的了,需要纹理坐标y轴颠倒一下来负负得正。所以理解为“Android纹理坐标的原点在左上角”,其实不准确。只是说上下颠倒坐标系,就刚好把数据又转正了。也就是只需要在bitmap 作为纹理输入的时候,需要手动上下颠倒一下,后面就按正常的OpenGL纹理处理就可以了

OpenGL ES 3.0 新增的实例渲染接口,可以增强粒子动画的表现能力,以及优化传输和绘制的性能:
glVertexAttribDivisor
glDrawArraysInstanced
glBeginTransformFeedback

待学习的粒子效果:
unity-optical-flow
PixelFlow

  • 多线程与资源共享
    存在多个OpenGL上下文时,纹理、shader、Buffer(VBO)等资源(包含数据,存储在可共享访问的内存区域内)是可以设置为OpenGL线程间共享,但Frame Buffer Object(FBO)、Vertex Array Object(VAO)等container objects(container objects 主要是用于存放regular objects,以及用于组织regular objects的额外信息)不可共享
    Understanding OpenGL Objects
    关于OpenGL的绘制上下

  • 兼容性

  1. NPOT纹理与平铺模式

OpenGL规范从2.0开始支持显示边长为非2次幂的Texture,但限制条件是需要环绕模式为CLAMP_TO_EDGE并且过滤模式为NEAREST或者LINEAR。
解除限制的条件是硬件支持OES_texture_npot的扩展。
获取硬件扩展列表的代码如下:

    std::string GetGLString(unsigned int pname) {
        const char* gl_string =
                reinterpret_cast<const char*>(glGetString(pname));
        if (gl_string)
            return std::string(gl_string);
        return "";
    }
...
const char* gl_extensions = GetGLString(GL_EXTENSIONS).c_str();
ULOGE("gl_extensions %s", gl_extensions);
//看打印的字符串里是否包含OES_texture_npot

OpenGL 2.0规范里有这个限制,是因为NPOT纹理设置REPEAT等模式会造成在图像连接处有接缝:
Seamless tilemap rendering (borderless adjacent images)

OpenGL ES 3.0 has full NPOT support in core; ES 2.0 has limited NPOT support (no mipmaps, no Repeat wrap mode) in core; and ES 1.1 has no NPOT support.

OpenGL对NPOT纹理的支持情况:
For ES 1.1 and 2.0, full NPOT support comes with GL_ARB_texture_non_power_of_two orGL_OES_texture_npot extension. In practice, iOS devices don’t support this; and on Android side there’s support on Qualcomm Adreno and ARM Mali. Possibly some others.

For ES 1.1, limited NPOT support comes with GL_APPLE_texture_2D_limited_npot (all iOS devices) or GL_IMG_texture_npot (some ImgTec Android devices I guess).

而谷歌从Android4.3开始强制要求设备支持OpenGL3.0:Android 4.3兼容性定义的“本机API兼容性”一栏

从5.0开始设备必须支持OpenGL3.1

Android 兼容性计划概览

所以可以认为,Android 4.3以上的设备以及iOS 7.0(iPhone系列 5S)以上的设备,启用OpenGL3.0后,支持对NPOT纹理应用GL_REPEAT等功能。而Android的4.3以前的设备,如果硬件支持OES_texture_npot扩展,那么也同样支持。
但这个只是理论上,Android的兼容性问题众所周知。

iOS升级3.0的文档:Adopting OpenGL ES 3.0

参考:
NPOT texture in iOS can't be repeat mode!
PowerVR Supported Extensions OpenGL ES and EGL

update:
实践的结论是,用shader在OpenGL2.0上实现NPOT的GL_REPEAT模式,预览效果也还好。在几台Android和iOS设备上测试,连接处的缝隙看不出来。
所以OpenGL2.0不支持的原因,我猜是在mipmap的场景下可能有问题。


记录

精度修饰符

OpenGL精度修饰符

顶点着色器默认精度为highp.
片元着色器没有默认精度, 所以需要专门指定默认精度或者给每个数值类型变量指定精度.
半精度浮点数Half
由于一个浮点型数在计算机内由符号位/指数位/尾数表示,如
1000.1 =1.0001 * 2的3次
1110110.1 = 1.1101101 * 2的6次
所以数值越大,float的精度误差也随着指数的增加而增加

有效值

glGetXXLocation(比如glGetUniformLocation/glGetAttribLocation) 有效值为>=0,失败会返回-1;
GenXX(比如glGenBuffers) 有效值为>0,失败name会是0;
纹理 Texture ID大于0 等于0表示此纹理不合法;
MEDIUMP_FLT_MAX 65504.0
MEDIUMP_FLT_MIN 0.00006103515625
MEDIUMP小数点最大误差为0.048%,平均误差0.018%

其他语法
  • 函数参数限定符
    GLSL 允许自定义函数,但参数默认是以值形式(in限定符)传入的,也就是说任何变量在传入时都会被拷贝一份,若想以引用方式传参,需要增加函数参数限定符。
in  复制到函数中在函数中可读写
out 返回时从函数中复制出来 (可写不可读)
inout   复制到函数中并在返回时复制出来

其中使用 inout 方式传递的参数便与其他 OOP 语言中的引用传递类似,参数可读写,函数内对参数的修改会影响到传入参数本身

其他记录
  • 内存限制:
    设备能使用的varying vec4数组的个数就是GL_MAX_VARYING_VECTORS值,最小值为8(2.x)或15(3.x);
    对应的有GL_MAX_FRAGMENT_UNIFORM_VECTORS 最小值为16(2.x)或224(3.x);
    GL_MAX_VERTEX_UNIFORM_VECTORS 最小值为128(2.x)或256(3.x);
    这里的个数值是指所有UNIFORM的单个int/单个float/float向量/float向量数组等的个数,比如顶点着色器里只定义了两个uniform vec4数组, 个数限制128(2.x)或256(3.x)对应的是是两个数组的size的和
    But, 在魅族和小米的两台手机上测试发现,虽然GL_MAX_FRAGMENT_UNIFORM_VECTORS的值返回256,但仍可以使用长度为280的vec4数组。。。

但因为四字节对齐的问题,可能这个vec4数组的最大个数也是float数组的最大个数:
GLSL float/vec3/vec4 array max size = GL_MAX_VERTEX_UNIFORM_VECTORS?
因此vec4或者4xn的mat,对内存的利用效率是最高的

OpenGL 3.3 guarantees a minimum uniform size of 1024 bytes 也就是16个uniform mat4

  1. With the texture object, the content drawn into the texture object can be used as a texture image.The renderbuffer object is a more general-purpose drawing area, allowing a variety of data types to be written.
  2. Renderbuffer is simply a data storage object containing a single image of a renderable internal format. It is used to store OpenGL logical buffers that do not have corresponding texture format, such as stencil or depth buffer.
  • 如果遇到program的draw执行了,但没有效果,可以看看draw之前和draw之后的fbo有没有异常,看
    是不是draw到了预期的fbo上,并且也restore到了预期的buffer上。
    查看fbo ID的代码:
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &_nowFboId);
    ULOGD("glGetIntegerv filter before draw: %d", _nowFboId);

MAX_VARYING_VECTORS的解释(WebGL shaders: maximum number of varying variables)

OpenGL 关于深度测试
绘制粒子时往往需要 glDisable(GL_DEPTH_TEST);

  • GLSL里的矩阵乘法
  1. vector在左边,右边乘以Martix是把vector当做行向量,运算结果与vector在右边,左边乘以Martix的转置矩阵相同GLSL里的矩阵乘法

  2. GLSL里的Martix是以列主序存储,所以vector在左边,右边乘以Martix的好处是提高运算效率:矩阵:行主序、列主序、行向量、列向量

  3. GLSL里初始化矩阵时,同样以列优先顺序赋值:

vec2 v = vec2(10., 20.);
mat2 m = mat2(1., 2.,  3., 4.);
vec2 w = v * m; // = vec2(1. * 10. + 2. * 20., 3. * 10. + 4. * 20.)
  1. 举例:
// 第二行是vector在左边,右边乘以Martix,所以要转置旋转矩阵
mat2 rmat = mat2(cos(angle),-sin(angle),sin(angle),cos(angle)); 
vec2 sampleCoord = vec2(u,v) * rmat;
  1. 矩阵顺序与执行顺序的关系:
    当前矩阵为A,变换矩阵为B,A执行B变换后新矩阵为AB,和顶点坐标v相乘,从而构成新的顶点坐标ABv。上述过程说明,程序中绘制顶点前的最后一个变换命令最先作用于顶点之上。这同时也说明,OpenGL编程中,实际的变换顺序与指定的顺序是相反的。Shader里顶点坐标v为列向量,如果和顶点坐标相乘的方式为vAB,此时其实相当于把顶点坐标v当做行向量,计算结果与v在右边,左边乘以Martix的转置矩阵相同

  2. 3D渲染矩阵示例:

https://forum.unity.com/threads/update-built-in-skybox-panoramic-shader-to-rotate-around-x-y-z-axis.539002/

float3 RotateAroundYInDegrees (float3 vertex, float degrees)
        {
            float alpha = degrees * UNITY_PI / 180.0;
            float sina, cosa;
            sincos(alpha, sina, cosa);
            float2x2 m = float2x2(cosa, -sina, sina, cosa);
            return float3(mul(m, vertex.xz), vertex.y).xzy;
        }
https://inspirnathan.com/posts/54-shadertoy-tutorial-part-8/

// Rotation matrix around the Y axis.
mat3 rotateY(float theta) {
    float c = cos(theta);
    float s = sin(theta);
    return mat3(
        vec3(c, 0, s),
        vec3(0, 1, 0),
        vec3(-s, 0, c)
    );
}
  • 渲染SDK架构参考
    2D: GPUImage C++版 GPUImage
    3D:主要是ECS,也有Boost Graph Library结构或者传统按逻辑分: 平台/图形API/核心库
    其他:
    GamePlay
    lottie
    Star游戏引擎开发记录
    鬼火
    orge3d

Filament
Filament解析1·主体逻辑
Filament专题 by Night_Aurora liujing7256
技术文章/渲染引擎-技术文章/filament

数据驱动渲染Data Driven Rendering: Pipelines

  • 抓Shader的经验
    见邮箱

噪声性能比较:


8-Table2-1.png

OpenGL硬件属性的统计资料:
OpenGL capabilities report: GL_MAX_COLOR_ATTACHMENTS_EXT

  • 用常量初始化vec3数组
 vec3 u_matrices[3];
 u_matrices[0] = vec3( 1.0, 0.0, 0.0 );
 u_matrices[1] = vec3( 0.0, 1.0, 0.0 );
 u_matrices[2] = vec3( 0.0, 0.0, 1.0 );
  • shader兼容性问题总结
    1. float必须写小数部分,比如100的float形式为100.0,而不是100. 在有些安卓手机上后者可能无法编译成功
  1. fragment必须声明精度,一般为mediump;
    有些手机不支持fragment高精度,如果需要声明高精度,则需要如下的形式:
#ifdef GL_FRAGMENT_PRECISION_HIGH
    precision highp float;
#else
    precision mediump float;
#endif

有些效果比如OilPainting2,在fragment的精度为medium的情况下会有bug,则如上声明高精度后,指定只在高端机上线

  1. logcat里出现以下三个log
    GL error 0x501 (比如第一版的OilPainting在红米10上会出现这个日志,虽然效果正常)
    GL_INVALID_VALUE

GL error 0x500
GL_INVALID_ENUM

GL error 0x0505 GL_OUT_OF_MEMORY
第三个肯定是GPU内存不足,第一和第二个也可能是内存不足的原因

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