移動端濾鏡開發(fā)(二)初識OpenGl

前世小書童IP屬地: 加州
0.269字?jǐn)?shù) 2,452

寫在前面的話

<p>
上一篇文章對Android端的ColorMatrix進(jìn)行了講解,雖然說可以滿足我們做濾鏡的簡單需求,但是對于視頻和相機(jī)這些更多方面的濾鏡需求就不能夠滿足需求了,所以為了濾鏡可以在更多場景使用,選擇用OpenGL來實現(xiàn)濾鏡效果,由于OpenGL是通過GPU來計算的,所以處理起來速度會更快,也會避免由于圖像計算較復(fù)雜帶來的OOM問題。

但是對于廣大的開發(fā)者來說,OpenGl是一個相對來說比較陌生的東西,可能大家都聽但是不是特別了解,所以這篇文章就對OpenGL進(jìn)行簡單的介紹,并實現(xiàn)圖片用OpenGL顯示出來。

一.OpenGl基礎(chǔ)知識

<p>

  • OpenGL 繪制的都是圖形,包括形狀和填充,基本形狀是三角形。
  • 每個形狀都有頂點,Vertix,頂點的序列就是一個圖形。
  • Shader,著色器,用來描述如何繪制(渲染),GLSL 是 OpenGL 的編程語言,全稱就叫 OpenGL Shader Language。OpenGL 渲染需要兩種 shader,vertex 和 fragment。
  • Vertex shader(頂點著色器),控制頂點的繪制,指定坐標(biāo)、變換等。
  • Fragment shader(片段著色器),控制形狀內(nèi)區(qū)域渲染,紋理填充內(nèi)容。

OpenGl的坐標(biāo)系與Android的坐標(biāo)系不同,是三維坐標(biāo)系,原點在中間,x 軸向右,y 軸向上,z 軸朝向我們,x y z 取值范圍都是 [-1, 1]

如圖所示

圖1 OpenGl坐標(biāo)系

OpenGL 紋理坐標(biāo)系為二維坐標(biāo)系,原點在左下角,s(x)軸向右,t(y)軸向上,x y 取值范圍都是 [0, 1]:

如圖所示

圖2 紋理坐標(biāo)系

其實通過介紹這些基礎(chǔ)知識,大家應(yīng)該可以對OpenGl基本的實現(xiàn)有點思路了。

注意:OpenGL ES 2.0需要Android2.2 (API Level 8) 及以上版本,所以請確保你的Android項目的運行目標(biāo)的API等級不低于8或更高。

二.OpenGl簡單構(gòu)造

<p>

與我們創(chuàng)建界面時候添加的View一樣,實現(xiàn)OpenGl則需要GLSurfaceView,這是讓我們渲染的"畫布"。

1.構(gòu)造一個GLSurfaceView對象

事實上GLSurfaceView并沒有提供很多功能,實際上繪制對象的任務(wù)都在GLSurfaceView.Renderer中進(jìn)行。所以GLSurfaceView中代碼也非常少,甚至可以直接使用GLSurfaceView。但最好別這樣做,因為你需要擴(kuò)展這個類來響應(yīng)觸摸事件。

public class MyGLSurfaceView extends GLSurfaceView{

    public MyGLSurfaceView(Context context) {
        this(context,null);
    }

    public MyGLSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

2.構(gòu)造一個Renderer類

Renderer類負(fù)責(zé)控制在GLSurfaceView中繪制任務(wù),并提供三個回調(diào)方法供Android系統(tǒng)調(diào)用,用來計算在GLSurfaceView中繪制什么以及如何繪制。

  • onSurfaceCreated():僅調(diào)用一次,用于設(shè)置view的OpenGL ES環(huán)境。
  • onDrawFrame():每次重繪view時調(diào)用。
  • onSurfaceChanged():當(dāng)view的幾何形狀發(fā)生變化時調(diào)用,比如設(shè)備從豎屏變?yōu)闄M屏。
public class MyRender implements GLSurfaceView.Renderer{

   @Override
   public void onSurfaceCreated(GL10 gl, EGLConfig config) {
      // 設(shè)置背景色
      GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
   }

   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {

      GLES20.glViewport(0, 0, width, height);
   }

   @Override
   public void onDrawFrame(GL10 gl) {
      // 重繪背景色
      GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
   }
}

這里只是創(chuàng)建了一個帶顏色的顯示,并沒有做任何事情

3.GLSurfaceView與Renderer關(guān)聯(lián)

設(shè)置渲染對象,用于控制在GLSurfaceView中的繪制工作

setRenderer(new MyRenderer());

指定使用的OpenGl版本

setEGLContextClientVersion(2);  

指定刷新方式(刷新方式有兩種, RENDERMODE_WHEN_DIRTY 和 RENDERMODE_CONTINUOUSLY ,前者是懶惰渲染,需要手動調(diào)用 glSurfaceView.requestRender() 才會進(jìn)行更新,而后者則是不停渲染。)

setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); 

4.Activity中顯示

public class OpenGLActivity extends Activity {

    private GLSurfaceView mGLSurfaceView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mGLSurfaceView = new MyGLSurfaceView(this);
        setContentView(mGLSurfaceView);
    }
}

這里就是創(chuàng)建一個GLSurfaceView對象,并將其設(shè)置為當(dāng)前Activity的ContentView

運行如下

圖3 僅帶顏色的GLSurfaceView

三.OpenGl實現(xiàn)Hello World

<p>

OpenGl的Hello World當(dāng)然不是我們認(rèn)知的Hello World,而是實現(xiàn)一個最基礎(chǔ)的圖形顯示

通過上面的簡單構(gòu)造我們可以知道,渲染這部分其實是在Renderer里面去實現(xiàn)的,所以其實我們需要改動的地方就是Renderer這部分

接下來我們一步一步來實現(xiàn)顯示一個三角形圖像

1.變量

<p>

前面最開始提到的兩個著色器并沒有在上個部分有體現(xiàn),而當(dāng)我們實現(xiàn)真正的圖像和圖形,時候則需要用頂點著色器對圖像定位,片段著色器對圖像進(jìn)行渲染

所以首先我們聲明兩個著色器程序

著色器程序則是用著色器語言來寫的,至于著色器語言的知識,這里我就不介紹了,大家可以自行搜索,簡單的了解下就可以繼續(xù)后面的流程了

頂點著色器程序

 private static final String VERTEX_SHADER = "attribute vec4 vPosition;\n"
                + "void main() {\n"
                + "  gl_Position = vPosition;\n"
                + "}";

這里做的事情是將傳過來的坐標(biāo)作為頂點的位置

片段著色器程序

 private static final String FRAGMENT_SHADER = "precision mediump float;\n"
                + "void main() {\n"
                + "  gl_FragColor = vec4(0,0,1,1);\n"
                + "}";

這里做的事情是將紋理的顏色設(shè)置為藍(lán)色

頂點坐標(biāo)數(shù)組

private static final float[] VERTEX = {
        0.0f, 1.0f, 0.0f, // top
        -1.0f, -1.0f, 0.0f, // bottom left
        1.0ff, -1.0f, 0.0f,  // bottom right
};

FloatBuffer存儲頂點坐標(biāo)數(shù)組/片段坐標(biāo)數(shù)組

private final FloatBuffer mVertexBuffer;

這里我們只需要把頂點坐標(biāo)傳給著色器,所以只聲明了頂點所用的FloatBuffer,由于不能直接把上面的頂點坐標(biāo)數(shù)組直接傳給著色器,所以需要FloatBuffer來進(jìn)行傳遞

接下來初始化FloatBuffer,將數(shù)組傳遞給FloatBuffer

mVertexBuffer = ByteBuffer.allocateDirect(VERTEX.length * 4)
                    .order(ByteOrder.nativeOrder())
                    .asFloatBuffer()
                    .put(VERTEX);
mVertexBuffer.position(0);

定義需要傳遞的變量

前面的頂點著色器需要將外部的頂點坐標(biāo)傳入,所以我們要先定義一個變量,并將這個變量與著色器語言中需要傳入的變量關(guān)聯(lián)起來

private int mPositionHandle;

定義GLSL程序

private int mProgram;
2.構(gòu)建GLSL程序

<p>

構(gòu)建步驟如下

  • 創(chuàng)建 GLSL 程序:
  • 加載 shader 代碼
  • attatch shader 代碼
  • 鏈接 shader 代碼
  • 獲取 shader 代碼中的變量索引

和普通的 view 利用 canvas 來繪制不一樣,OpenGL 需要加載 GLSL 程序,讓 GPU 進(jìn)行繪制。所以我們需要定義 shader 代碼,并在 onSurfaceChanged回調(diào)中加載:

@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
    mProgram = GLES20.glCreateProgram();
    int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
    int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
    GLES20.glAttachShader(mProgram, vertexShader);
    GLES20.glAttachShader(mProgram, fragmentShader);
    GLES20.glLinkProgram(mProgram);
    
    mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
}

//加載Shader代碼
static int loadShader(int type, String shaderCode) {
    int shader = GLES20.glCreateShader(type);
    GLES20.glShaderSource(shader, shaderCode);
    GLES20.glCompileShader(shader);
    return shader;
}

就不對代碼進(jìn)行分析了,按照上面的步驟基本是對應(yīng)的

3.繪制

<p>

繪制則是在onDrawFrame回調(diào)中加載

@Override
public void onDrawFrame(GL10 unused) {

//清除指定的buffer到預(yù)設(shè)值。可清除以下四類buffer:
//1)GL_COLOR_BUFFER_BIT
//2)GL_DEPTH_BUFFER_BIT
//3)GL_ACCUM_BUFFER_BIT
//4)GL_STENCIL_BUFFER_BIT
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

//安裝一個program object,并把它作為當(dāng)前rendering state的一部分。
GLES20.glUseProgram(mProgram);

//Enable由索引index指定的通用頂點屬性數(shù)組。
GLES20.glEnableVertexAttribArray(mPositionHandle);

//定義一個通用頂點屬性數(shù)組。當(dāng)渲染時,它指定了通用頂點屬性數(shù)組從索引index處開始的位置和數(shù)據(jù)格式
GLES20.glVertexAttribPointer(mPositionHandle, 3, GLES20.GL_FLOAT, false,
        12, mVertexBuffer);

//三個成員變量mode,first,count
//1) mode:指明render原語,如:GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_QUAD_STRIP, GL_QUADS, 和 GL_POLYGON。
//2) first: 指明Enable數(shù)組中起始索引。
//3) count: 指明被render的原語個數(shù)。
//可以預(yù)先使用單獨的數(shù)據(jù)定義vertex、normal和color,然后通過一個簡單的glDrawArrays構(gòu)造一系列原語。當(dāng)調(diào)用 glDrawArrays時,它使用每個enable的數(shù)組中的count個連續(xù)的元素,來構(gòu)造一系列幾何原語,從第first個元素開始
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);

//Disable由索引index指定的通用頂點屬性數(shù)組。
GLES20.glDisableVertexAttribArray(mPositionHandle);
}

注釋已經(jīng)寫的很詳細(xì)了,就不詳細(xì)分析了

接下來運行如圖

圖4 藍(lán)色的三角形

四.OpenGl實現(xiàn)圖像顯示

<p>

經(jīng)過上面的講解大家應(yīng)該對OpenGl簡單使用有一定的了解了吧,OK,接下來回到我們這個系統(tǒng)最初的目的,首先我們來實現(xiàn)用OpenGl展示圖片

其實將上面的代碼進(jìn)行簡單的修改就可以實現(xiàn)需要的效果

接下來一步一步實現(xiàn)

1.繪制矩形

<p>
這一步其實很簡單,前面的三角形是繪制了三個點,那么這里繪制矩形的話,我們繪制兩個三角形,六個點,自然就實現(xiàn)了

代碼如下

private static final float[] VERTEX = {
        -1.0f, -1.0f, 0.0f, // bottom left
        1.0f, -1.0f, 0.0f,  // bottom right
        1.0f, 1.0f, 0.0f, // top right
        -1.0f, -1.0f, 0.0f,
        1.0f, 1.0f, 0.0f,
        -1f, 1.0f, 0.0f,
};

GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);

當(dāng)然除了這樣以為,我們注意一下glDrawArrays這個第一個參數(shù),我們是使用的GLES20.GL_TRIANGLES,當(dāng)然除了這個參數(shù),還有另外兩個與TRIANGLES的參數(shù),我們來看下區(qū)分

  • GL_TRIANGLES:每三個頂之間繪制三角形,之間不連接
  • GL_TRIANGLE_FAN:以V0V1V2,V0V2V3,V0V3V4,……的形式繪制三角形
  • GL_TRIANGLE_STRIP:順序在每三個頂點之間均繪制三角形。這個方法可以保證從相同的方向上所有三角形均被繪制。以V0V1V2,V1V2V3,V2V3V4……的形式繪制三角形

所以其實我們并不需要6個點那么多,四個點就足夠了

如下

private static final float[] VERTEX = {
                    -1.0f, -1.0f, 0.0f,
                    1.0f, -1.0f, 0.0f,
                    -1.0f, 1.0f, 0.0f,
                     1.0f, 1.0f, 0.0f,
};

GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

運行如下

圖5 矩形
2.著色器程序修改

<p>

因為我們要顯示圖片,自然之前的著色器程序是不能滿足需求的,那么我們簡單分析一下,怎么才能把圖片渲染上去呢?首先圖片紋理要傳進(jìn)去,其次還得知道什么地方繪制

所以對上面的著色器程序進(jìn)行修改

如下:

private static final String VERTEX_SHADER = "attribute vec4 vPosition;" +
        "attribute vec2 a_texCoord;" +
        "varying vec2 v_texCoord;" +
        "void main() {" +
        "  gl_Position = vPosition;" +
        "  v_texCoord = a_texCoord;" +
        "}";
private static final String FRAGMENT_SHADER = "precision mediump float;" +
        "varying vec2 v_texCoord;" +
        "uniform sampler2D s_texture;" +
        "void main() {" +
        "  gl_FragColor = texture2D( s_texture, v_texCoord );" +
        "}";

我們可以看到增加的東西是varying vec2 v_texCoord與uniform sampler2D s_texture,第一是代表紋理坐標(biāo)點,第二個代表圖片紋理,通過GLSL的內(nèi)建函數(shù)texture2D來獲取對應(yīng)位置紋理的顏色RGBA值

這里出現(xiàn)了更多的關(guān)鍵字, uniform , attribute , varying ,包括上面的內(nèi)建函數(shù),這里并不做講解,有興趣的同學(xué)去搜索一下就可以了解了解

3.繪制圖片紋理

<p>

我們已經(jīng)對著色器程序修改,接下來自然就是去將需要的坐標(biāo)和紋理傳給著色器程序

紋理坐標(biāo)系文章最開始有提到,所以我們先創(chuàng)建紋理坐標(biāo)數(shù)組,并初始化

(1)建紋理坐標(biāo)數(shù)組,并初始化

private static final float[] UV_TEX_VERTEX = {   // in clockwise order:
        0.0f, 1.0f,
        1.0f, 1.0f,
        0.0f, 0.0f,
        1.0f, 0.0f,
};
FloatBuffer mUvTexVertexBuffer;

mUvTexVertexBuffer = ByteBuffer.allocateDirect(UV_TEX_VERTEX.length * 4)
            .order(ByteOrder.nativeOrder())
            .asFloatBuffer()
            .put(UV_TEX_VERTEX);
mUvTexVertexBuffer.position(0);

接下來生成圖片紋理

(2)生成圖片紋理

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    ...
    int[] mTexNames = new int[1];
    GLES20.glGenTextures(1, mTexNames, 0);

    Bitmap bitmap = BitmapFactory.decodeResource(mResources, R.drawable.p_300px);
    //選擇當(dāng)前活躍的紋理源
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
    //將生成的紋理的名稱綁定到指定的紋理上
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexNames[0]);
    //設(shè)置紋理被縮小(距離視點很遠(yuǎn)時被縮小)時候的濾波方式
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
        GLES20.GL_LINEAR);
    //設(shè)置紋理被放大(距離視點很近時被方法)時候的濾波方式
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
        GLES20.GL_LINEAR);
    //設(shè)置在橫向、縱向上都是位于紋理邊緣或者靠近紋理邊緣的紋理單元將用于紋理計算,但不使用紋理邊框上的紋理單元。
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
        GLES20.GL_REPEAT);
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
        GLES20.GL_REPEAT);
    // 加載位圖生成紋理
    GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
    bitmap.recycle();
    ...
}

大家要注意 GLES20.glActiveTexture(GLES20.GL_TEXTURE0)這個方法,這是生成紋理,在OpenGl里面有很多個這種GL_TEXTURE表示不同紋理

(3)獲取 shader 代碼中的新的變量索引

private int mTexCoordHandle;
private int mTexSamplerHandle;

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    ...
    mTexCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_texCoord");
    mTexSamplerHandle = GLES20.glGetUniformLocation(mProgram, "s_texture");
    ...
}

(4)繪制

@Override
public void onDrawFrame(GL10 gl) {
    ...
    
    GLES20.glEnableVertexAttribArray(mTexCoordHandle);
    GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0,
            mUvTexVertexBuffer);

    GLES20.glUniform1i(mTexSamplerHandle, 0);
    GLES20.glDisableVertexAttribArray(mTexCoordHandle);
    ...
}

注意這里的glUniform1i方法,這里的第二個參數(shù)和前面提到的glActiveTexture方法的后綴要一致

到這里基本就結(jié)束了,運行如下

圖6 圖像顯示

寫在后面的話

<p>

通過這么一大串的內(nèi)容,我相信大家應(yīng)該都對OpenGl在安卓上面的實現(xiàn)有一定了解了吧,關(guān)于GLSL語言呢,大家可以自行去了解一下,這會對后面的文章有一定幫助,下一篇則是介紹如何用OpenGl實現(xiàn)攝像機(jī)預(yù)覽與音頻播放,peace~~

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

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