十一、GLSL & 自定着色器加载纹理

GLSL —— OpenGL Shading Language
OpenGL的着色语言是用来在OpenGL中着色编程的语言,也是开发人员编写的短小自定义程序。它们是在GPU(Graphic Processor Unit图形处理单元)上执行的,代替了固定的渲染管线一部分,使得渲染管线中的不同层次具有可编程性。如:试图转换、投影转换等。GLSL的着色器代码分成2个部分:Vertex Shader(顶点着色器)和Fragment Shader(片元着色器)。

着色器与程序

  • 1.创建并编译着色器
  • 2.创建并链接程序( program )
  • 3.获取并设置统一变量 ( attribute )
  • 4.获取并设置属性 ( uniform )
  • 5.着色器编译器编译二进制程序代码

用着色器渲染时需要创建2个基本对象:着色器对象 + 程序对象
获取链接后着色器对象的过程有一下步骤:
-1).创建一个顶点着色器对象和一个片元着色器对象
-2).将源代码链接到每个着色器对象
-3).编译着色器对象
-4).创建一个程序对象
-5).将编译后的着色器对象链接到程序对象
-6).链接程序对象

  • 创建并编译着色器
// type — 创建着⾊器的类型,GL_VERTEX_SHADER 或者GL_FRAGMENT_SHADER ,返回值 — 是指向新着⾊器对象的句柄.可以调⽤glDeleteShader 删除
GLuint glCreateShader(GLenum type); 

// shader — 要删除的着⾊器对象句柄
void glDeleteShader(GLuint shader); 

/*
shader — 指向着⾊器对象的句柄
count — 着⾊器源字符串的数量,着⾊器可以由多个源字符串组成,但是每个着⾊器只有⼀个main函数
string — 指向保存数量的count 的着⾊器源字符串的数组指针
length — 指向保存每个着⾊器字符串⼤⼩且元素数量为count 的整数数组指针.
*/
void glShaderSource(GLuint shader , GLSizei count ,const GLChar * const *string, const GLint 
*length); 

  • 创建与链接程序
/*
创建⼀个程序对象
返回值: 返回⼀个执⾏新程序对象的句柄
*/
GLUint glCreateProgram( ) 

/* 
program : 指向需要删除的程序对象句柄
*/
void glDeleteProgram( GLuint program )

//着⾊器与程序连接/附着
/*program : 指向程序对象的句柄
shader : 指向程序连接的着⾊器对象的句柄
*/
void glAttachShader( GLuint program , GLuint shader ); 

//断开连接
/*program : 指向程序对象的句柄
shader : 指向程序断开连接的着⾊器对象句柄
*/
void glDetachShader(GLuint program); 

// program: 指向程序对象句柄
void glLinkProgram(GLuint program) 

链接程序之后, 需要检查链接是否成功. 你可以使⽤glGetProgramiv 检查链接状态: 

/*
program: 需要获取信息的程序对象句柄
pname : 获取信息的参数,可以是: 
GL_ACTIVE_ATTRIBUTES 
GL_ACTIVE_ATTRIBUTES_MAX_LENGTH 
GL_ACTIVE_UNIFORM_BLOCK 
GL_ACTIVE_UNIFORM_BLOCK_MAX_LENGTH 
GL_ACTIVE_UNIFROMS 
GL_ACTIVE_UNIFORM_MAX_LENGTH 
GL_ATTACHED_SHADERS 
GL_DELETE_STATUS 
GL_INFO_LOG_LENGTH 
GL_LINK_STATUS 
GL_PROGRAM_BINARY_RETRIEVABLE_HINT 
GL_TRANSFORM_FEEDBACK_BUFFER_MODE 
GL_TRANSFORM_FEEDBACK_VARYINGS 
GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH 
GL_VALIDATE_STATUS 
params : 指向查询结果整数存储位置的指针
*/
void glGetProgramiv (GLuint program,GLenum pname, GLint *params); 


/** 从程序信息⽇志中获取信息
program : 指向需要获取信息的程序对象句柄
maxLength : 存储信息⽇志的缓存区⼤⼩
length : 写⼊的信息⽇志⻓度(减去null 终⽌符),如果不需要知道⻓度,这个参数可以为Null. 
infoLog : 指向存储信息⽇志的字符缓存区的指针
void glUseProgram(GLuint program) 
program: 设置为活动程序的程序对象句柄.
*/
void glGetPorgramInfoLog( GLuint program ,GLSizei maxLength, GLSizei *length , GLChar *infoLog ) 

通过自定义着色器加载纹理图
通过一个实例,利用自定义着色器方式绘制纹理图片;通过一个程序句柄program,将顶点数据传递到顶点着色器程序的position中(新建一个shaderVertex.vsh的Empty文件),然后再用glGetAttributeLocation 来获取 着色器程序中的顶点数据、纹理数据。GLKit 则是苹果帮我们省去了这样一个步骤,方便开发者使用,但是仅限于简单的固定着色器,如果复杂度提高,则可采用自定义着色器的方式来加载纹理图片。
以下是实现步骤

实现步骤

新建TextureView视图,将ViewController中的View继承TextureView。主要工作就在TextureView.m中完成。

//
//  TextureView.m
//  GLSL_DIY_ShaderWithTexture
//
//  Created by TL on 2020/7/31.
//  Copyright © 2020 Brain. All rights reserved.
//

#import "TextureView.h"
#import <OpenGLES/ES2/gl.h>
@interface TextureView()

/**
 CALayer 的图层
 */
@property (nonatomic,strong) CAEAGLLayer * mEaglLayer;
@property (nonatomic,strong) EAGLContext * context;

/**
 渲染缓存区
 */
@property (nonatomic,assign) GLuint mRenderBuffer;
/**
 帧缓存区
 */
@property (nonatomic,assign) GLuint mFrameBuffer;

/**
 创建一个程序对象句柄,用来作为信息链接的载体
 */
@property (nonatomic,assign) GLuint mPrograme;


@end


@implementation TextureView

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    // Drawing code
}
*/

-(void)layoutSubviews
{
// 1.初始化layer图层,以备绘制
    [self initSubLayer];
    // 2.设置上下文
    [self setContext];
    // 3.清空缓存区:framebuffer & render buffer
    [self deleteRenderBufferAndFrameBuffer];
    
    // 4.设置renderBuffer
    [self setRenderBuffer];
    
    // 5.设置frameBuffer
    [self setFrameBuffer];
    
    // 6.渲染显示
    [self renderDisplay];
}

/**
 1.初始化layer图层
 */
- (void)initSubLayer
{
    
    // 1.创建图层
    self.mEaglLayer = (CAEAGLLayer *)self.layer;
    
    // 2.设置scale
    [self setContentScaleFactor:[[UIScreen mainScreen]scale]];
    
    /**
    3.设置描述属性——不维持渲染内容,颜色格式为RGBA8
     kEAGLDrawablePropertyRetainedBacking  表示绘图表面显示后,是否保留其内容。
     kEAGLDrawablePropertyColorFormat
     可绘制表面的内部颜色缓存区格式,这个key对应的值是一个NSString指定特定颜色缓存区对象。默认是kEAGLColorFormatRGBA8;
     
     kEAGLColorFormatRGBA8:32位RGBA的颜色,4*8=32位
     kEAGLColorFormatRGB565:16位RGB的颜色,
     kEAGLColorFormatSRGBA8:sRGB代表了标准的红、绿、蓝,即CRT显示器、LCD显示器、投影机、打印机以及其他设备中色彩再现所使用的三个基本色素。sRGB的色彩空间基于独立的色彩坐标,可以使色彩在不同的设备使用传输中对应于同一个色彩坐标体系,而不受这些设备各自具有的不同色彩坐标的影响。
    */
    self.mEaglLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@NO,
                                           kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8
                                           };
    
}
+(Class)layerClass
{
    return [CAEAGLLayer class];
}

/**
 2.设置上下文
 */
- (void)setContext
{
    // 1.创建上下文
    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    if (!self.context) {
        NSLog(@"init context failed");
        return;
    }
    if (![EAGLContext setCurrentContext:self.context]) {
        NSLog(@"setCurrentContext failed");
        return;
    }
}


/**
 3、先清空一下缓存区
 */
- (void)deleteRenderBufferAndFrameBuffer
{
    /*
     buffer 分为 frame buffer 和 render buffer2个大类。
     其中frame buffer 相当于render buffer的管理者。
     frame buffer object即称FBO,是收集颜色、深度、模板缓存区的附着点对象。
     render buffer则又可分为3类。colorBuffer、depthBuffer、stencilBuffer 模板。
     */
    glDeleteBuffers(1,&_mFrameBuffer);
    self.mFrameBuffer = 0;
    
    glDeleteBuffers(1, &_mRenderBuffer);
    self.mRenderBuffer = 0;
    
    
}

/**
 4.设置渲染缓存区
 */
- (void)setRenderBuffer
{
     GLuint buffer;
    // 1.申请一个缓存区标志
    glGenRenderbuffers(1, &buffer);
    self.mRenderBuffer = buffer;
    // 2.绑定renderBUffer
    glBindRenderbuffer(GL_RENDERBUFFER, self.mRenderBuffer);
    
    // 3.将CAEAGLLayer的对象存储绑定到OpenGL ES的FreameBuffer对象上
    [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.mEaglLayer];
    
}

/**
 5.设置帧缓存区
 */
- (void)setFrameBuffer
{
     GLuint buffer;
    // 1.申请一个缓存区标志
    glGenBuffers(1, &buffer);
    self.mFrameBuffer = buffer;
    // 2.绑定frameBUffer
    glBindFramebuffer(GL_FRAMEBUFFER, self.mFrameBuffer);
    
    /**
     3.将renderBuffer 通过 glFramebufferRenderbuffer绑定到 GL_COLOR_ATTACHMENT0上
     生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,
     调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,后面的绘制才能起作用
     */
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.mRenderBuffer);

}

/**
 6.渲染显示
 */
- (void)renderDisplay
{
    // 1.设置清屏颜色以及清除颜色缓存区
    glClear(GL_COLOR_BUFFER_BIT);
    glClearColor(0.6, 0.7, 0.8, 1);
    
    // 2.设置视口大小
    GLfloat scaleF = [[UIScreen mainScreen] scale];
    CGPoint orign = self.frame.origin;
    CGSize size = self.frame.size;
    glViewport(orign.x * scaleF, orign.y * scaleF, size.width * scaleF, size.height * scaleF);;
    
    // 3.s读取顶点着色程序 + 片元着色程序
    NSString * fragmentFile = [[NSBundle mainBundle] pathForResource:@"shader_fragment" ofType:@"fsh"];
    NSString * vertexFile = [[NSBundle mainBundle] pathForResource:@"shader_vertex" ofType:@"vsh"];
    
    NSLog(@"fsh path: %@ \n vsh path:%@",fragmentFile,vertexFile);
    
    // 4.给程序对象句柄加载两个着色器
    self.mPrograme = [self loadShdersWithFragmentFile:fragmentFile WithVextexFile:vertexFile];
    
    // 5.链接program
    glLinkProgram(self.mPrograme);
    GLint linkStatus;
    // 6.记录链接状态
    glGetProgramiv(self.mPrograme, GL_LINK_STATUS, &linkStatus);
    if (linkStatus == GL_FALSE) {
        GLchar message[512];
        glGetProgramInfoLog(self.mPrograme, sizeof(message), 0, &message[0]);
        NSString * messageInfo = [NSString stringWithUTF8String:message];
        NSLog(@"link error messageInfo : %@",messageInfo);
        return;
    }
    NSLog(@"program link success");
    // 7.使用program
    glUseProgram(self.mPrograme);
    
    // 8.设置顶点坐标(前3)、纹理坐标(后2)
    GLfloat attibuteArr[] = {
        0.5f, -0.5f, -1.0f,     1.0f, 0.0f,
       -0.5f, 0.5f, -1.0f,     0.0f, 1.0f,
       -0.5f, -0.5f, -1.0f,    0.0f, 0.0f,
        
        0.5f, 0.5f, -1.0f,      1.0f, 1.0f,
        -0.5f, 0.5f, -1.0f,     0.0f, 1.0f,
        0.5f, -0.5f, -1.0f,     1.0f, 0.0f,
    };
    
    // 9.处理顶点数据
    // 1).开启顶点缓存区,申请一个缓存区标识符
    GLuint attrBuffer;
    glGenBuffers(1, &attrBuffer);
    // 2).将attributeBuffer 绑定到GL_ARRAY_BUFFER标识符
    glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
    // 3).将顶点数据从内存copy至显存,交由GPU操作
    glBufferData(GL_ARRAY_BUFFER, sizeof(attibuteArr), attibuteArr, GL_DYNAMIC_DRAW);
    
    // 10.将顶点数据通过program传递到顶点着色程序的position
    // 1).调用glGetAttribLocation,获取vertex attribute 中的数据,参数2必须要和 shader_vertex.vsh 中 position相同,否则无法获得
    GLuint position = glGetAttribLocation(self.mPrograme, "position");
    
    // 2).从buffer中读取数据
    glEnableVertexAttribArray(position);
    // 3).设置读取方式
    /**
       para1: index,顶点数据的索引
       para2: size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
       para3: type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
       para4: normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
       para5: stride,连续顶点属性之间的偏移量,默认为0;
       para6: 指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
     */
    glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL + 0);
    
    // 11.处理纹理数据
    // 1).从shader_vertex.fsh 获取textureCoordinat
    GLuint textureCoord = glGetAttribLocation(self.mPrograme, "textCoordinate");
    // 2).打开通道,从buffer中读取数据
    glEnableVertexAttribArray(textureCoord);
    // (float * )NULL,不转化为float *,图片将无法显示
    glVertexAttribPointer(textureCoord, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (float *)NULL + 3);
    
    // 加载图片纹理
    [self loadTexture:@"miao.jpg"];
    
    // 12.设置纹理采样器 sampler2D
    glUniform1i(glGetUniformLocation(self.mPrograme, "colorMap"), 0);
    
    // 13.绘制纹理
    glDrawArrays(GL_TRIANGLES, 0, 6);
    
    // 14.从渲染缓存区显示到屏幕上
    [self.context presentRenderbuffer:GL_RENDERBUFFER];
    
}

/// 加载纹理
/// @param imageName 纹理名称
- (GLuint)loadTexture:(NSString *)imageName
{
    // 1.将UIImage转化为CGImageRef
    CGImageRef spriteImage = [UIImage imageNamed:imageName].CGImage;
    if (!spriteImage) {
        NSLog(@"load image failed");
        return 1;
    }
    
    // 2.获得图片大小,尺寸
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    // 获取图片字节数,其中RGBA占4个八位 byteData = width * height * 4
    GLubyte * spriteData = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
    
    /**
     3.创建上下文
       para1: data,指向要渲染的绘制图像的内存地址
       para2: width,bitmap的宽度,单位为像素
       para3: height,bitmap的高度,单位为像素
       para4: bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
       para5: bytesPerRow,bitmap的没一行的内存所占的比特数
       para6: colorSpace,bitmap上使用的颜色空间  kCGImageAlphaPremultipliedLast:RGBA

     */
    CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width * 4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
    // 4.在CGContextRef 将图片绘制出来
    
    CGRect rect = CGRectMake(0, 0, width, height);
    
    // 5.使用默认方式绘制
    /**
       CGContextDrawImage 使用的是Core Graphics框架,坐标系与UIKit 不一样。UIKit框架的原点在屏幕的左上角,Core Graphics框架的原点在屏幕的左下角。
       CGContextDrawImage
       参数1:绘图上下文
       参数2:rect坐标
       参数3:绘制的图片
    */
    CGContextDrawImage(spriteContext, rect, spriteImage);
    // 6.画图完毕后释放上下文
    CGContextRelease(spriteContext);
    // 7.绑定纹理到默认的纹理ID
    glBindTexture(GL_TEXTURE_2D, 0);
    // 8.设置纹理属性
    /**
     参数1:纹理纬度
     参数2:线性过滤、为s,t坐标设置模式
     参数3:wrapMode,环绕模式
     */
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    
    float fWidht = width,fHeight = height;
    
    // 9.载入纹理
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fWidht, fHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
    
    // 10.释放spriteData
    free(spriteData);
    return 0;
    
    
}


/**
 加载顶点着色程序+片元着色程序,返回一个程序program

 @param fragFile 片元着色器文件路径
 @param vertexFile 顶点着色器文件路径
 @return program
 */
- (GLuint)loadShdersWithFragmentFile:(NSString *)fragFile WithVextexFile:(NSString *) vertexFile
{
    
    // 1.定义两个临时着色器对象
    GLuint verTexShader,fragmentShader;
    // 2.创建program
    GLuint program = glCreateProgram();
    // 3.编译顶点着色器程序 & 片元着色器程序
    [self  complieShader:&verTexShader type:GL_VERTEX_SHADER file:vertexFile];
    [self  complieShader:&fragmentShader type:GL_FRAGMENT_SHADER file:fragFile];
    
    //4. 创建最终程序,
    glAttachShader(program, verTexShader);
    glAttachShader(program, fragmentShader);
    
    // 5.释放不需要的shader
    glDeleteShader(verTexShader);
    glDeleteShader(fragmentShader);
    
    return program;
}
- (void)complieShader:(GLuint *)shader type:(GLenum)type file:(NSString *)fileName
{
    // 1.获取文件路径
    NSString * contentFile = [NSString stringWithContentsOfFile:fileName encoding:NSUTF8StringEncoding error:nil];
    // 2.转换字符串
    const GLchar * source = (GLchar*)[contentFile UTF8String];
    // 3.根据type创建shader
    *shader = glCreateShader(type);
    /**
     4. 将着色器源码附着至着色器对象上
     para1: shader,要编译的着色器对象 *shader
     para2: numOfStrings,传递的源码字符串数量 1个
     para3: strings,着色器程序的源码(真正的着色器程序源码)
     para4: lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
     */
    glShaderSource(*shader, 1, &source, NULL);
    // 5.把着色器源码编译成目标代码
    glCompileShader(*shader);
}


@end

按照步骤走完,图片是能加载了,但会出现问题,图片是倒置的,如下图


倒置的纹理

原因是由于view起始绘制点和纹理起始点不一致导致的,在绘制图片时对纹理进行翻转即可得到正确的效果

    // 添加在 CGContextDrawImage(spriteContext, rect, spriteImage); 之后
    // 平移到x,y
    CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y);
    // 再平移图片高度
    CGContextTranslateCTM(spriteContext, 0, rect.size.height);
    // 沿y轴翻转
    CGContextScaleCTM(spriteContext, 1.0, -1.0);
    // 再平移至原位置
    CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y);
    // 再重新绘制
    CGContextDrawImage(spriteContext, rect, spriteImage);
正常效果

github Demo地址

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