OpenGL学习之着色器语言

着色器语言

Open GL ES 着色器语言是一种高级的图形编辑语言,主要特性:

  1. OpenGL ES着色器语言是一种高级的过程语言
  2. 对顶点着色器,片元着色器使用的是同样的语言,不做区分
  3. 基于C/C++的语法及流程控制
  4. 完美支持向量与矩阵的各种操作
  5. 拥有大量的内置函数来提供丰富的功能

数据类型

OpenGL ES虽然是基于C/C++语法的,但是还是有很大的不同。该语言不支持double,byte,short,long,也取消了C中的union,enum,unsigned以及位运算。

基本数据类型
GLSL有三种基本数据类型:float,int,bool,还有由这三种数据类型组成的数组和结构体

bool b;
int a = 15;
int b = 036;//0开头的字面常量为八进制
int c = 0x3D;//0x开头的字面常量为十六进制
float myFloat = 1.0;
float floatArray[4];
float a[4] = float[](1.0, 2.0, 3.0, 4.0);
float b[4] = float[4](1.0. 2.0, 3.0, 4.0);

int的取值范围是16位精度。

向量

GLSL中,向量可以看做是用同样类型的标量组成,其基本类型也分为bool,int和float三种。每个向量可以由2个,3个或者4个相同的标量组成:

向量类型 说明 向量类型 说明
vec2 包含了2个浮点数的向量 ivec4 包含了4个整数的向量
vec3 包含了3个浮点数的向量 bvec2 包含2个布尔数的向量
vec4 包含了4个浮点数的向量 bvec3 包含了3个布尔数的向量
ivec2 包含了2个整数的向量 bvec4 包含了4个布尔数的向量
ivec3 包含了3个整数的向量

向量在着色器代码的开发中十分重要,可以很方便的存储以及操作颜色,位置,纹理坐标等。也可以单独访问向量中的某个分量,基本语法为< 向量名>.<分量名>

  • 将向量看做颜色时,可以使用r,g,b,a 4个分量名,分别代码红,绿,蓝,透明度
aColor.r = 0.6;
aColor.g = 0,8;
  • 将一个向量看做位置时。可以使用x,y,z,w等4个分量名,其分别代表x轴,y轴,z轴,向量的模
aPosition.x = 67.2;
aPosition.z = 34.5;
  • 将一个向量看做纹理坐标时,可以使用s,t,p,q 4个分量名,其分别代表纹理坐标中的1维,2维,3维

矩阵

矩阵类型 说明
mat2 2x2的浮点数矩阵
mat3 3x3的浮点数矩阵
mat4 4x4的浮点数矩阵

OpenGL ES着色器语言中,矩阵是按列顺序组织的,也就是一个矩阵可以看做由几个列向量组成。例如,mat3就可以看做由3个vec3组成。

对于矩阵的访问,可以将矩阵作为列向量的数组来访问。如; matrix 为一个mat4,可以使用matrix[2]取到该矩阵的第三列,其为一个vec4;也可以使用matrix[2][2]取得第三列的向量的第三个分量,其为一个float。

GLSL在类型转换方面有非常严格的规则,变量只能赋值给相同类型的其他变量或者与相同类型的变量进行运算。

float myFloat1 = 1.0;
float myFloat2 = 1;  // error: invalid type conversion

只能使用构造函数来完成类型换行

float f = 1.0;
bool b = bool(f);
float f1 = float(b);

同样,向量也可以使用构造器,向量构造器的参数传递有两种基本的用法

  • 如果只为向量构造器提供一个标量参数,则该值涌来设置向量的所有值
  • 如果提供了多个标量或者向量参数,则向量的值从左到右使用这些参数设置
vec4 myVec4 = vec4(1.0); // {1.0, 1.0, 1.0, .10}
vec3 myVec3 = vec3(1.0, 1.0, 1.0); // {1.0, 1.0, 1.0}
vec3 temp = vec3(myVec3);

vec2 myVec2 = vec2(myVec3); // {myVec3.x, myVec3.y}
myVec4 = vec4(myVec2, myVec3); // {myVec2.x, myVec2.y, myVec3.x, myVec3.y}

float a[4] = float[](1.0, 2.0, 3.0, 4.0);
float b[]4 = float[4](1.0. 2.0, 3.0, 4.0); // 数组构造器中的参数数量必须等于数组的大小
vec2 c[2] = vec2[2](vec2(1.0), vec2(2.0));

常量

GLSL中可以使用常量,常量是着色器语言中不变的值,在程序中不能被修改,声明常量时,需要加入const修饰符,并且常量在声明时必须初始化。

const float zero = 0.0;
const mat4 myMat = mat4(1.0)

对于熟悉变量,一致变量以及一遍变量在声明的时候一定不能进行初始化

attribute floa angleSpan;
uniform int k;
varying vec3 position;

结构体

GLSL除了提供基本类型之外,还可以和C语言一样提供结构体

 struct fragStruct {
      vec4 color;
      float start;
      float end;
  } fragVar;  //定义了一个fragStruct的结构体类型和fragVar的结构体变量

fragVar = fragStruct( vec4(0.0, 1.0, 1.0, 1.0),    // color
                                  0.5,  // start
                                  2.0); // end
vec4 color = fragVar.color;
float start = fragVar.start;
float end = fragVar.end;

混合选择

通过运算符“.”可以进行混合选择操作,在运算符“.” 之后列出一个向量中需要的各个分量的名称,就可以选择并重新排列这些分量

vec4 color = vec4(0.7,0.1,0.5,1.0);
vec3 temp = color.agb;//相当于拿一个向量(1.0,0.1,0。5)赋值给temp
vec4 tempL = color.aabb; // 相当于拿了一个向量(1.0,1.0,0.5,0.5)赋值给tempL
vec3 tempLL;
tempLL.grb = color.aab; // 对向量tempLL的3个分量赋值

一次混合最多只能列出4个分量名称,且一次出现的各部分的分量名称必须是来自同一名称组。

限定符

限定符中大部分用来修饰全局变量。

限定符 说明
attribute 一般用于每个顶点都各不相同的量,如顶点位置,颜色,应用程序传递给顶点着色器,仅能用于顶点着色器
uniform 一般用于对同一组顶点组成的单个3D物体中所有顶点都相同的量,如当前光源位置
varying 用于从顶点着色器传递到片元着色器的量
const 用于声明常量
attribute
attribute限定符为属性限定符,其修饰的变量用来接收渲染管线传递进顶点着色器的当前待处理顶点的各种属性。这些属性值每个顶点各自拥有独立的副本,用于描述顶点的各项特征,如顶点坐标,法向量,颜色,纹理坐标等。

用attribute限定符修饰的变量其是由宿主程序批量传入渲染管线的,管线进行基本处理后再传递给顶点着色器。数据中有多少个顶点,管线就调用多少次顶点着色器,每次将一个顶点的各种属性数据传递给顶点着色器中对应的attribute变量。因此,顶点着色器每次执行将完成对一个顶点各项数据的处理。

所以,attribute限定符只能用于顶点着色器中,且attribute限定符只能用来修饰浮点数标量,浮点数向量以及矩阵变量,不能用来修饰其他类型的变量。

attribute vec3 aPosition; // 顶点位置
attribute vec3 aNormal; // 顶点法向量

uniform

uniform为一致变量限定符,一致变量指的是对于同一组顶点组成的单个3D物体中所有顶点都相同的量。uniform变量可以用在顶点着色器或片元着色器中,其支持用来修饰所有的基本数据类型。与属性变量类似,一致变量的值也是有宿主程序传入。

uniform mat4 uMVPMatrix; // 总变换矩阵
uniform mat4 uMMatrix; // 变换矩阵
uniform vec3 uLightLocation; // 光源位置

varying

要想将顶点着色器中的信息传入到片元着色器中,则必须使用varying限定符。用varying修饰的全局变量又称易变变量,易变变量可以看成是顶点着色器以及片元着色器之间的动态接口,方便顶点着色器与片元着色器之间信息的传递。

工作原理:

从图中可以看出来,首先顶点着色器在每一个顶点都对易变变量vPosition进行赋值,然后在片元着色器中接受易变变量vPosition的值时得到的并不是某个顶点的特定值,而是根据片元所在位置及图元中各个顶点的位置进行插值计算产生的值。

如图中,顶点1,2,3的vPosition的值所示,则插值后片元a的vPosition的值为vec3(1.45,2.06,0)。这个值是根据3个顶点对应的着色器给vPosition的赋值,三个顶点的位置及此片元的位置由管线插值计算所得。

函数
和C语言一样,着色器也可以自定义函数,语法一样

但是在参数列表中,参数除了可以指定类型外,还可以指定用途:

in :其修饰的参数为输入参数,仅供函数接收外界传入的值,函数不能修改。默认为它。按值传递
out:其修饰的参数为输出参数,在函数体中对输出参数赋值可以将值传递到调用其的外界变量中。对于输出参数,要注意的是在调用时不可以使用字面常量。该变量的值不被传入函数,函数返回使被修改。该变量的值不被传入函数
inout:其修饰的参数为输入输出参数,具有输入输出两种参数的功能,如果被修改,原参数会被修改。按引用传递。

vec4 myFunc(inout float myFloat, out vec4 myVec4, mat4 myMat4);

注意:GLSL中的函数不能递归

内建函数
GLSL中最强大的功能之一就是提供的内建函数,比如用dot来计算两个向量的点乘,用pow来计算标量的幂次

精度限定符
精度限定符可以指定着色器变量的精度,变量可以声明为低、中、高精度,这些限定符允许编译器在比较低的范围和精度上进行计算,在较低的精度上,有些OpenGL ES实现在运行着色器时可能会更快,或者电源效率更高,当然这种性能的提升是以降低精度为代价的。

精度限定符的关键字是:lowp、mediump、highp

highp vec4 position;
lowp vec4 color;
mediump float exption;
1
2
3
可以设置变量的默认精度,如果变量声明时没有使用精度限定符,将会拥有该类型的默认精度,默认精度可以在顶点或者片段着色器的开头指定:

precision highp float;
precision mediump int;
1
2
同时,在顶点着色器中,如果没有指定默认精度,int 和 float 值的默认精度都是highp,但是在片段着色器中,float没有默认的精度,每个着色器必须声明一个默认的float精度,或者为float变量手动指定精度。
最后需要注意:指定的精度根据不同的实现有不同的范围和精度,具体的范围可以根据OpenGL ES 的API来获取,例如在PowerVR SGX GPU上,lowp float变量用10位表示,medium float用16位表示,而highp用32位来表示。

内建变量
内建变量一般用来实现渲染管线固定功能部分与自定义顶点或片元着色器之间的信息交互。

内建变量分为两类,输入与输出变量。
输入变量负责架构渲染管线中固定功能部分产生的信息传递进着色器
输出变量负责将着色器产生的信息传递给渲染管线中固定功能部分。

顶点着色器中主要是输出变量,如:

  • gl_Position:顶点着色器从应用程序中获得原始顶点的位置数据,这些原始的顶点数据在顶点着色器中经过平移,选择,缩放等数学变换后,生成新的顶点位置。新的顶点位置通过在顶点着色器中写入 gl_Position传递到渲染管线的后继阶段继续处理。
    gl_Position的类型是vec4,写入的顶点位置数据必须与其类型一致

gl_PointSize:顶点着色器中可以计算一个点的大小,并将其赋值给gl_PointSize(标量 float类型)以传递给渲染管线,默认为1.
片元着色器中的输入变量:

gl_FragCoord:vec4类型,含有当前片元相对于窗口位置的坐标值,x,y,z与1/w。其中,x,y分别为片元相对于窗口的二维坐标,z为该片元的深度。
知道这些信息后,就可以实践实践。我们先用一个特别简单的例子,画三角形:

先看效果:

首先我们需要一个util,来进行Shader的加载:

//使用顶点着色器与片元着色器
struct GLESUtils {
    
    static func loadShaderFile(type:GLenum, fileName:String) -> GLuint {
        guard let path = Bundle.main.path(forResource: fileName, ofType: nil) else {
            print("Error: file does not exist !")
            return 0;
        }
        
        do {
            let shaderString = try String(contentsOfFile: path, encoding: .utf8)
            return GLESUtils.loadShaderString(type: type, shaderString: shaderString)
        } catch {
            print("Error: loading shader file: \(path)")
            return 0;
        }
    }
    
  
    
    
    static func loadShaderString(type:GLenum, shaderString:String) ->GLuint {
        //1 创建着色器对象
        let shaderHandle = glCreateShader(type)
        
        var shaderStringLength: GLint = GLint(Int32(shaderString.count))
        var shaderCString = NSString(string: shaderString).utf8String
        
        /* 2 把着色器源码附加到着色器对象上
         glShaderSource(shader: GLuint, count: GLsizei, String: UnsafePointer<UnsafePointer<GLchar>?>!, length: UnsafePointer<GLint>!)
         shader: 着色器对象
         count:指定要传递的源码字符串数量,这里只有一个
         String:着色器源码
         length:源码长度
         */
        glShaderSource(shaderHandle, GLsizei(1), &shaderCString, &shaderStringLength)
        
        // 3 编译着色器
        glCompileShader(shaderHandle)
        
        // 编译是否成功的状态 GL_FALSE GL_TRUE
        var compileStatus: GLint = 0
        // 获取编译状态
        glGetShaderiv(shaderHandle, GLenum(GL_COMPILE_STATUS), &compileStatus)
        
        if compileStatus == GL_FALSE {
            var infoLength: GLsizei = 0
            let bufferLength: GLsizei = 1024
            glGetShaderiv(shaderHandle, GLenum(GL_INFO_LOG_LENGTH), &infoLength)
            
            let info: [GLchar] = Array(repeating: GLchar(0), count: Int(bufferLength))
            var actualLength: GLsizei = 0
            
            // 获取错误消息
            glGetShaderInfoLog(shaderHandle, bufferLength, &actualLength, UnsafeMutablePointer(mutating: info))
            NSLog(String(validatingUTF8: info)!)
            print("Error: Colourer Compilation Failure: \(String(validatingUTF8: info) ?? "")")
            return 0
        }
        
        return shaderHandle
    }
    
    static func loanProgram(verShaderFileName:String,fragShaderFileName:String) -> GLuint {
        
        let vertexShader = GLESUtils.loadShaderFile(type: GLenum(GL_VERTEX_SHADER), fileName: verShaderFileName)
        
        if vertexShader == 0 {return 0}
        
        let fragmentShader = GLESUtils.loadShaderFile(type: GLenum(GL_FRAGMENT_SHADER), fileName: fragShaderFileName)
        
        if fragmentShader == 0 {
            glDeleteShader(vertexShader)
            return 0
        }
        
        // 创建着色器程序对象
        let programHandel = glCreateProgram()
        
        if programHandel == 0 {return 0}
        
        // 将着色器附加到程序上
        glAttachShader(programHandel, vertexShader)
        glAttachShader(programHandel, fragmentShader)
        
        // 链接着色器程序
        glLinkProgram(programHandel)
        
        // 获取链接状态
        var linkStatus: GLint = 0
        glGetProgramiv(programHandel, GLenum(GL_LINK_STATUS), &linkStatus)
        if linkStatus == GL_FALSE {
            var infoLength: GLsizei = 0
            let bufferLenght: GLsizei = 1024
            glGetProgramiv(programHandel, GLenum(GL_INFO_LOG_LENGTH), &infoLength)
            
            let info: [GLchar] = Array(repeating: GLchar(0), count: Int(bufferLenght))
            var actualLenght: GLsizei = 0
            
            // 获取错误消息
            glGetProgramInfoLog(programHandel, bufferLenght, &actualLenght, UnsafeMutablePointer(mutating: info))
            print("Error: Colorer Link Failed: \(String(validatingUTF8: info) ?? "")")
            return 0
        }
        
        // 4 释放资源
        glDeleteShader(vertexShader)
        glDeleteShader(fragmentShader)
        
        return programHandel
    }
}

然后是我们的重点,我们现在只是画一个三角形,并且让这个三角形绕X轴转动,并且向Z轴平移,于是创建一个Triangle类:

public class Triangle {

public static  float[] mProjectMatirx = new float[16];//4x4投影矩阵
public static  float[] mVMatrix = new float[16]; //摄像机位置朝向的参数矩阵
public static  float[] mMVPMatrix = new float[16];//总变换矩阵

private int mProgram;
private int muMVPMatrixHandle;//总变换矩阵的引用
private int maPositionHandle; //顶点位置的引用
private int maColorHandle; //顶点颜色属性引用

private String mVertexShader = "uniform mat4 uMVPMatrix;" +
        "attribute vec3 aPosition;" +
        "attribute vec4 aColor;" +
        "varying vec4 vColor;" +
        "void main(){" +
        "gl_Position = uMVPMatrix* vec4(aPosition,1);" + //根据总变换矩阵计算此次绘制此顶点的位置
        "vColor = aColor;" +
        "}";
private String mFragmentShader = "precision mediump float;" +
        "varying vec4 vColor;" +
        "void main(){" +
        "gl_FragColor = vColor;" +
        "}";

public static float[] mMMatrix = new float[16];//具体物体的3D变换矩阵

private FloatBuffer mVertexBuffer;//顶点坐标数据缓冲
private FloatBuffer mFragmentBuffer;//顶点着色数据缓冲

private int mvCount = 0;//顶点数量
private float mxAngle = 0; //绕X轴旋转的角度

public Triangle() {
    initVertexData();
    initShader();
}

private void initVertexData(){
    mvCount = 3;
    float vertices[] = new float[]{
      -1,0,0,0,-1,0,1,0,0
    };//顶点位置

    ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4);
    vbb.order(ByteOrder.nativeOrder());//设置字节顺序为本地操作系统顺序
    mVertexBuffer = vbb.asFloatBuffer();
    mVertexBuffer.put(vertices);//在缓冲区内写入数据
    mVertexBuffer.position(0);//设置缓冲区的起始位置

    float[] colors = new float[]{
      1,0,0,1,0,1,0,1,0,0,1,1
    };//顶点颜色数组
    ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length*4);
    cbb.order(ByteOrder.nativeOrder());
    mFragmentBuffer = cbb.asFloatBuffer();
    mFragmentBuffer.put(colors);
    mFragmentBuffer.position(0);
}

private void initShader(){
    mProgram = shaderUtil.createProgram(mVertexShader,mFragmentShader);
    maPositionHandle = GLES30.glGetAttribLocation(mProgram,"aPosition");
    maColorHandle = GLES30.glGetAttribLocation(mProgram,"aColor");
    muMVPMatrixHandle = GLES30.glGetUniformLocation(mProgram,"uMVPMatrix");

}

public void drawSelf(){
    GLES30.glUseProgram(mProgram);
    Matrix.setRotateM(mMMatrix,0,0,0,1,0);//初始化变换矩阵
    Matrix.translateM(mMMatrix,0,0,0,1);//向z轴平移1位
    Matrix.rotateM(mMMatrix,0,mxAngle,1,0,0);//绕X轴旋转
    GLES30.glUniformMatrix4fv(muMVPMatrixHandle,1,
            false,getFinalMatrix(mMMatrix),0);//将最终的变换矩阵传进渲染管线。
    GLES30.glVertexAttribPointer(maPositionHandle,
            3,GLES30.GL_FLOAT,
            false,3*4,mVertexBuffer);//将顶点位置数据传进渲染管线
    GLES30.glVertexAttribPointer(
            maColorHandle,4,GLES30.GL_FLOAT,
            false,4*4,mFragmentBuffer);//将顶点颜色数据传进渲染管线
    GLES30.glEnableVertexAttribArray(maPositionHandle);//启用顶点位置数据
    GLES30.glEnableVertexAttribArray(maColorHandle);//启用顶点颜色数据
    GLES30.glDrawArrays(GLES30.GL_TRIANGLES,0,mvCount);//执行绘制
}

public static float[] getFinalMatrix(float[] spec){
    mMVPMatrix =new float[16];
    Matrix.multiplyMM(mMVPMatrix,0,
            mVMatrix,0,spec,0);//摄像机矩阵乘以变换矩阵

    Matrix.multiplyMM(mMVPMatrix,0,
            mProjectMatirx,0,mMVPMatrix,0);//投影矩阵乘以上一步的结果
    return mMVPMatrix;
}

public void setXAngle(float degree){
    mxAngle += degree;
}

首先我们要初始化三角形的数据,然后编译Shader,连接程序。

我们知道,我需要用Render来渲染它:

private class  TriangleRender implements  Renderer{

   public  Triangle mTriangle;
   private RotateThread mThread;

   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
       GLES30.glClearColor(1,1,1,1.0f);//设置屏幕背景色
       mTriangle = new Triangle();
       GLES30.glEnable(GLES30.GL_DEPTH_TEST);
       mThread = new RotateThread();
       mThread.start();

   }

   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES30.glViewport(0,0,width,height);
        float ration = (float)width/height;
       Matrix.frustumM(Triangle.mProjectMatirx,0,
               -ration,ration,-1,1,1,10);//设置透视投影

       Matrix.setLookAtM(Triangle.mVMatrix,0,
               0,0,3,0,
               0f,0,0f,1.0f,0f);//设置摄像机

   }

       @Override
       public void onDrawFrame(GL10 gl) {
            GLES30.glClear(GLES30.GL_DEPTH_BUFFER_BIT|GLES30.GL_COLOR_BUFFER_BIT);
            mTriangle.drawSelf();
       }
   }

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

推荐阅读更多精彩内容