OpenGL ES渲染管線(渲染流程)

渲染管線

蘋果提供了兩種OpenGL ES的可視化模型,一種是客戶端—服務(wù)端的架構(gòu)模型,另一種就是管線的模型。

OpenGL ES client-server architecture

客戶端—服務(wù)端架構(gòu):應(yīng)用程序狀態(tài)一旦更改,紋理、頂點數(shù)據(jù)以及渲染命令都將傳遞給OpenGL ES客戶端。客戶端將這些數(shù)據(jù)轉(zhuǎn)換成圖形硬件可以理解的格式,發(fā)送給GPU。這個過程增加了app圖形性能的開銷。為了獲得好的性能需要管理好這種開銷。一個好的設(shè)計需要減少OpenGL ES調(diào)用的頻率,使用適合硬件的數(shù)據(jù)格式來減少轉(zhuǎn)換成本,并且管理好自身與OpenGL ES的數(shù)據(jù)流。
管線架構(gòu):另外就是本文將要說的管線架構(gòu),由于管線中每個獨立階段都依賴于上一階段的產(chǎn)出,所以任何階段的工作量太大或者運行太慢,其他階段都會被迫閑置以等待前一階段的完成。好的設(shè)計會根據(jù)硬件功能平衡每個階段的執(zhí)行工作。
OpenGL ES graphics pipeline

在 OpenGL ES 1.0 版本中,支持固定管線(fixed-function pipeline),有一系列固定的函數(shù)用來在屏幕上渲染對象,而不是創(chuàng)建一個單獨的程序來指導(dǎo) GPU 的行為。這樣有很大的局限性,你不能做出任何特殊的效果。如果想知道著色器在工程中可以造成怎樣的不同,看看這篇 Brad Larson 寫的他用著色器替代固定函數(shù)重構(gòu) Molecules 應(yīng)用的博客
而 OpenGL ES 2.0 版本不再支持固定管線,只支持可編程管線。

什么是管線?什么又是固定管線和可編程管線?

管線(pipeline):也稱渲染管線,因為 OpenGL ES在渲染處理過程中會順序執(zhí)行一系列操作,這一系列相關(guān)的處理階段就被稱為OpenGL ES 渲染管線。OpenGL ES 渲染過程就如流水線作業(yè)一樣,這樣的實現(xiàn)極大地提高了渲染的效率。如圖就是 OpenGL ES 的管線圖,學(xué)習OpenGL ES 就是學(xué)習這張圖中的每一個部分。

圖中陰影部分的 Vertex Shader 和 Fragment Shader 是可編程管線。可編程管線就是說這個操作可以動態(tài)編程而不必寫死在代碼中。可動態(tài)編程實現(xiàn)這一功能一般都是腳本提供的,在OpenGL ES 中也一樣,編寫這樣腳本的能力是由著色語言GLSL提供的。那可編程管線有什么好處呢?方便我們動態(tài)修改渲染過程,而無需重寫編譯代碼,當然也和很多腳本語言一樣,調(diào)試起來不太方便。

渲染管線中的各個模塊

Vertex Shader

由圖可見,頂點著色器分輸入輸出兩部分。頂點著色器定義了在 2D 或者 3D 場景中幾何圖形是如何處理的,實現(xiàn)了頂點操作的通用可編程方法。一個頂點指的是 2D 或者 3D 空間中的一個點。在圖像處理中,有 4 個頂點:每一個頂點代表圖像的一個角。頂點著色器設(shè)置頂點的位置,并且把位置和紋理坐標這樣的參數(shù)發(fā)送到片段著色器。
輸入:

  • 著色器程序(Shader Program,圖中沒有畫出):由 main 申明的一段程序源碼或可執(zhí)行文件,描述在頂點上執(zhí)行的操作:如坐標變換、計算光照公式產(chǎn)生每個頂點顏色、計算紋理坐標。
  • 屬性(Attribute):由 vertext array 提供的頂點數(shù)據(jù),如空間位置,法向量,紋理坐標以及頂點顏色,屬性可以理解為針對每一個頂點的輸入數(shù)據(jù)。屬性只在頂點著色器中才有,片元著色器中沒有屬性。OpenGL ES 2.0 規(guī)定了所有實現(xiàn)應(yīng)該支持的最大屬性個數(shù)不能少于 8 個。
  • 常量(Uniforms): Uniforms保存由應(yīng)用程序傳遞給著色器的只讀常量數(shù)據(jù)。在頂點著色器中,這些數(shù)據(jù)通常是變換矩陣,光照參數(shù),顏色等。由 uniform 修飾符修飾的變量屬于全局變量,該全局性對頂點著色器與片元著色器均可見,也就是說,這兩個著色器如果被連接到同一個應(yīng)用程序中,它們共享同一份 uniform 全局變量集。因此如果在這兩個著色器中都聲明了同名的 uniform 變量,要保證這對同名變量完全相同:同名+同類型,因為它們實際是同一個變量。此外,uniform 變量存儲在常量存儲區(qū),因此限制了 uniform 變量的個數(shù),OpenGL ES 2.0 也規(guī)定了所有實現(xiàn)應(yīng)該支持的最大頂點著色器 uniform 變量個數(shù)不能少于 128 個,最大的片元著色器 uniform 變量個數(shù)不能少于 16 個。
  • 采樣器(Samplers): 一種特殊的 uniform,用于呈現(xiàn)紋理。sampler 可用于頂點著色器和片元著色器。
    輸出:
  • 可變變量(Varying):varying 變量用于存儲頂點著色器的輸出數(shù)據(jù),也存儲片元著色器的輸入數(shù)據(jù)。varying 變量會在光柵化處理階段被線性插值。頂點著色器如果聲明了 varying 變量,它必須被傳遞到片元著色器中才能進一步傳遞到下一階段,因此頂點著色器中聲明的 varying 變量都應(yīng)在片元著色器中重新聲明為同名同類型的 varying 變量。OpenGL ES 2.0 也規(guī)定了所有實現(xiàn)應(yīng)該支持的最大 varying 變量個數(shù)不能少于 8 個。
  • 在頂點著色器階段至少應(yīng)輸出位置信息-即內(nèi)建變量:gl_Position,其它兩個可選的變量為:gl_FrontFacing 和 gl_PointSize。

Primitive Assembly圖元裝配(還有啥好的翻譯?)

圖元(Primitive):OpenGL ES 支持三種基本圖元:點,線和三角形,它們是可被 OpenGL ES 渲染的。經(jīng)過著色器處理之后的頂點在這一階段被裝配為基本圖元。對于每個圖元,必須確定圖元是否位于視錐體(屏幕上可見的3D空間區(qū)域)內(nèi),保留完全在視錐體中的圖元,丟棄完全不在視錐體中的圖元,對一半在一半不在的圖元進行裁剪。裁剪之后,頂點位置就被轉(zhuǎn)換成了屏幕坐標。也可以再對在視錐體中的圖元進行剔除(cull):這個過程可編碼來決定是剔除正面,背面還是全部剔除。裁剪和剔除之后,圖元便準備傳遞給管線的下一個階段——光柵化階段。

Rasterization

在光柵化階段,基本圖元被轉(zhuǎn)換為一組二維的片元(fragment),fragment 表示可以被渲染到屏幕上的像素,它包含位置,顏色,紋理坐標等信息,這些值是由圖元的頂點信息進行插值計算得到的。這些片元接著被送到片元著色器中處理。這是從頂點數(shù)據(jù)到可渲染在顯示設(shè)備上的像素的質(zhì)變過程。

Fragment Shader

GPU 使用片元著色器在對象或者圖片的每一個像素上進行計算,處理由光柵化階段生成的每個片元,最終計算出每個像素的最終顏色。圖片,歸根結(jié)底,實際上僅僅是數(shù)據(jù)的集合。圖片的文檔包含每一個像素的各個顏色分量和像素透明度的值。因為對每一個像素,算式是相同的,GPU 可以流水線作業(yè)這個過程,從而更加有效的進行處理。使用正確優(yōu)化過的著色器,在 GPU 上進行處理,將獲得百倍于在 CPU 上用同樣的過程進行圖像處理的效率。

  • 可變變量(Varyings):這個在前面已經(jīng)講過了,頂點著色器階段輸出的 varying 變量在光柵化階段被線性插值計算之后輸出到片元著色器中作為它的輸入,即上圖中的 gl_FragCoord,gl_FrontFacing 和 gl_PointCoord。OpenGL ES 2.0 也規(guī)定了所有實現(xiàn)應(yīng)該支持的最大 varying 變量個數(shù)不能少于 8 個。
  • 常量(Uniforms):前面也已經(jīng)講過,這里是用于片元著色器的常量,如霧化參數(shù),紋理參數(shù)等;OpenGL ES 2.0 也規(guī)定了所有實現(xiàn)應(yīng)該支持的最大的片元著色器 uniform 變量個數(shù)不能少于 16 個。
  • 采樣器(Samples):一種特殊的 uniform,用于呈現(xiàn)紋理。
  • 著色器程序(Shader program):由 main 申明的一段程序源碼,描述在片元上執(zhí)行的操作。
  • 在頂點著色器階段只有唯一的 varying 輸出變量-即內(nèi)建變量:gl_FragColor。

頂點著色器與片元著色器的編程區(qū)別

  • 精度上的差異
    著色語言定了三種級別的精度:lowp, mediump, highp。我們可以在 glsl 腳本文件的開頭定義默認的精度。

    precision highp float;
    
    • 在頂點著色階段,如果沒有自定義默認精度,那么 int 和 float 都默認為 highp 級別;
    • 在片元著色階段,如果沒有自定義默認精度,就真的沒有默認精度了.我們必須在每個變量前放置精度描述符。此外,OpenGL ES 2.0 標準也沒有強制要求所有實現(xiàn)在片元階段都支持 highp 精度。我們可以通過查看是否定義 GL_FRAGMENT_PRECISION_HIGH 來判斷具體實現(xiàn)是否在片元著色器階段支持 highp 精度,從而編寫出可移植的代碼。通常不需要在片元著色器階段使用 highp 級別的精度,推薦先使用 mediump 級別的精度,只有在效果不夠好的情況下再考慮 highp 精度。
  • attribute 修飾符只可用于頂點著色。

使用頂點著色器和片元著色器

可編程管線通過用 GLSL 語言編寫腳本文件實現(xiàn)的,這些腳本文件相當于 C 源碼,有源碼就需要編譯鏈接,因此需要對應(yīng)的編譯器與鏈接器,shader 對象與 program 對象就相當于編譯器與鏈接器。shader 對象載入源碼,然后編譯成 object 形式(就像C源碼編譯成 .obj文件)。經(jīng)過編譯的 shader 就可以裝配到 program 對象中,每個 program對象必須裝配兩個 shader 對象:一個頂點 shader,一個片元 shader,然后 program 對象被連接成“可執(zhí)行文件”,這樣就可以在 render 中運行該“可執(zhí)行文件”了。

  • 創(chuàng)建,裝載和編譯 shader

    • 向工程中添加新的類 GLESUtils,讓它繼承自 NSObject。GLESUtils.h 為
    #import <Foundation/Foundation.h>
    #include <OpenGLES/ES2/gl.h>
    
    @interface GLESUtils : NSObject
    
    // Create a shader object, load the shader source string, and compile the shader.
    //
    +(GLuint)loadShader:(GLenum)type withString:(NSString *)shaderString;
    
    +(GLuint)loadShader:(GLenum)type withFilepath:(NSString *)shaderFilepath;
    
    @end
    
    • GLESUtils.m
    #import "GLESUtils.h"
    
    @implementation GLESUtils
    
    +(GLuint)loadShader:(GLenum)type withFilepath:(NSString *)shaderFilepath {
        NSError* error;
        NSString* shaderString = [NSString stringWithContentsOfFile:shaderFilepath 
                                                       encoding:NSUTF8StringEncoding
                                                          error:&error];
        if (!shaderString) {
            NSLog(@"Error: loading shader file: %@ %@", shaderFilepath, error.localizedDescription);
            return 0;
        }
    
        return [self loadShader:type withString:shaderString];
    }
    
    +(GLuint)loadShader:(GLenum)type withString:(NSString *)shaderString {   
        //創(chuàng)建shader
        GLuint shader = glCreateShader(type);
        if (shader == 0) {
            NSLog(@"Error: failed to create shader.");
            return 0;
        }
    
        //裝載shader
        const char * shaderStringUTF8 = [shaderString UTF8String];
        glShaderSource(shader, 1, &shaderStringUTF8, NULL);
    
        //編譯shader
        glCompileShader(shader);
    
        //查詢變異狀態(tài)
        GLint compiled = 0;
        glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    
        if (!compiled) {
            GLint infoLen = 0;
            glGetShaderiv ( shader, GL_INFO_LOG_LENGTH, &infoLen );
        
            if (infoLen > 1) {
                char * infoLog = malloc(sizeof(char) * infoLen);
                glGetShaderInfoLog (shader, infoLen, NULL, infoLog);
                NSLog(@"Error compiling shader:\n%s\n", infoLog );            
            
                free(infoLog);
            }
        
            glDeleteShader(shader);
            return 0;
        }
    
        return shader;
    }
    
    @end
    

    輔助類 GLESUtils 中有兩個類方法用來跟進 shader 腳本字符串或 shader 腳本文件創(chuàng)建 shader,然后裝載它,編譯它。下面詳細介紹每個步驟。

    • 創(chuàng)建/刪除 shader
      函數(shù) glCreateShader 用來創(chuàng)建 shader,參數(shù) GLenum type 表示我們要處理的 shader 類型,它可以是 GL_VERTEX_SHADER 或 GL_FRAGMENT_SHADER,分別表示頂點 shader 或 片元 shader。它返回一個句柄指向創(chuàng)建好的 shader 對象。
      函數(shù) glDeleteShader 用來銷毀 shader,參數(shù)為 glCreateShader 返回的 shader 對象句柄。

    • 裝載 shader
      函數(shù) glShaderSource 用來給指定 shader 提供 shader 源碼。第一個參數(shù)是 shader 對象的句柄;第二個參數(shù)表示 shader 源碼字符串的個數(shù);第三個參數(shù)是 shader 源碼字符串數(shù)組;第四個參數(shù)一個 int 數(shù)組,表示每個源碼字符串應(yīng)該取用的長度,如果該參數(shù)為 NULL,表示假定源碼字符串是 \0 結(jié)尾的,讀取該字符串的內(nèi)容指定 \0 為止作為源碼,如果該參數(shù)不是 NULL,則讀取每個源碼字符串中前 length(與每個字符串對應(yīng)的 length)長度個字符作為源碼。

    • 編譯 shader
      函數(shù) glCompileShader 用來編譯指定的 shader 對象,這將編譯存儲在 shader 對象中的源碼。

    • 查詢shader對象信息
      函數(shù) glGetShaderiv 來查詢 shader 對象的信息,如本例中查詢編譯情況。此外還可以查詢 GL_DELETE_STATUS,GL_INFO_LOG_STATUS,GL_SHADER_SOURCE_LENGTH 和 GL_SHADER_TYPE。在這里我們查詢編譯情況,如果返回 0,表示編譯出錯了,錯誤信息會寫入 info 日志中,我們可以查詢該 info 日志,從而獲得錯誤信息。

  • 編寫著色腳本
    添加VertexShader.glsl和FragmentShader.glsl文件

    • VertexShader.glsl
    attribute vec4 vPosition; 
    
    void main(void) {
        gl_Position = vPosition;
    }
    

    attribute 屬性 vPosition 表示從應(yīng)用程序輸入的類型為 vec4 的位置信息,輸出內(nèi)建 vary 變量 vPosition。注意:這里使用了默認的精度。

    • FragmentShader.glsl
    precision mediump float;
    
    void main() {
        gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);//RGBA,此處為黃色
    }
    

    片元著色腳本源碼也很簡單,前面說過片元著色要么自己定義默認精度,要么在每個變量前添加精度描述符,在這里自定義 float 的精度為 mediump。然后為內(nèi)建輸出變量 gl_FragColor 指定為黃色。

  • 創(chuàng)建 program,裝配 shader,鏈接 program,使用 program

    • OpenGLView.h中添加兩個成員
    GLuint _programHandle;
    GLuint _positionSlot;
    
    • 在 OpenGLView.m 中的匿名 category 中添加成員方法:
    - (void)setupProgram {
        // Load shaders
        NSString * vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"VertexShader"
                                                                  ofType:@"glsl"];
        NSString * fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"FragmentShader"
                                                                    ofType:@"glsl"];
        GLuint vertexShader = [GLESUtils loadShader:GL_VERTEX_SHADER
                                   withFilepath:vertexShaderPath]; 
        GLuint fragmentShader = [GLESUtils loadShader:GL_FRAGMENT_SHADER
                                     withFilepath:fragmentShaderPath];
    
        // Create program, attach shaders.
        _programHandle = glCreateProgram();
        if (!_programHandle) {
            NSLog(@"Failed to create program.");
           return;
        }
    
        glAttachShader(_programHandle, vertexShader);
        glAttachShader(_programHandle, fragmentShader);
    
       // Link program
    
        glLinkProgram(_programHandle);
    
        // Check the link status
        GLint linked;
        glGetProgramiv(_programHandle, GL_LINK_STATUS, &linked );
        if (!linked) {
            GLint infoLen = 0;
            glGetProgramiv (_programHandle, GL_INFO_LOG_LENGTH, &infoLen );
        
            if (infoLen > 1) {
                char * infoLog = malloc(sizeof(char) * infoLen);
                glGetProgramInfoLog (_programHandle, infoLen, NULL, infoLog );
                NSLog(@"Error linking program:\n%s\n", infoLog );            
            
                free (infoLog );
            }
        
            glDeleteProgram(_programHandle);
            _programHandle = 0;
            return;
        }
    
        glUseProgram(_programHandle);
    
        // Get attribute slot from program
        _positionSlot = glGetAttribLocation(_programHandle, "vPosition");
    }  
    

    首先由 GLESUtils 提供的輔助方法從前面創(chuàng)建的腳本中創(chuàng)建,裝載和編譯頂點 shader 和片元 shader;然后我們創(chuàng)建 program,將頂點 shader 和片元 shader 裝配到 program 對象中,再使用 glLinkProgram 將裝配的 shader 鏈接起來,這樣兩個 shader 就可以合作干活了。注意:鏈接過程會對 shader 進行可鏈接性檢查,也就是前面說到同名變量必須同名同型以及變量個數(shù)不能超出范圍等檢查。我們?nèi)绾螜z查 shader 編譯情況一樣,對 program 的鏈接情況進行檢查。如果一切正確,那我們就可以調(diào)用 glUseProgram 激活 program 對象從而在 render 中使用它。通過調(diào)用 glGetAttribLocation 我們獲取到 shader 中定義的變量 vPosition 在 program 的槽位,通過該槽位我們就可以對 vPosition 進行操作。

  • 使用示例
    在 - (void)layoutSubviews 中調(diào)用 render 方法之前,插入對 setupProgram 的調(diào)用

    [self setupProgram];
    [self render];
    

render方法:

- (void)render {
    glClearColor(0.5, 1.0, 0.5, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);

    // Setup viewport
    glViewport(0, 0, self.frame.size.width, self.frame.size.height);

    GLfloat vertices[] = {
        0.0f,  0.5f, 0.0f, 
        -0.5f, -0.5f, 0.0f,
        0.5f,  -0.5f, 0.0f };

    // Load the vertex data
    glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices );
    glEnableVertexAttribArray(_positionSlot);

    // Draw triangle
    glDrawArrays(GL_TRIANGLES, 0, 3);

    [_context presentRenderbuffer:GL_RENDERBUFFER];
}

在新增的代碼中,第一句 glViewport 表示渲染 surface 將在屏幕上的哪個區(qū)域呈現(xiàn)出來,然后我們創(chuàng)建一個三角形頂點數(shù)組,通過 glVertexAttribPointer 將三角形頂點數(shù)據(jù)裝載到 OpenGL ES 中并與 vPositon 關(guān)聯(lián)起來,最后通過 glDrawArrays 將三角形圖元渲染出來。


繪制三角形

代碼存放在繪制一個三角形中New Group文件夾

OpenGL ES渲染管線與著色器
OpenGL ES 3.0編程指南
Apple文檔——OpenGL ES Design Guidelines
Notes on OpenGL ES Graphics Pipeline
GPU 加速下的圖像處理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內(nèi)容