年輕人的第一篇OpenGL ES 2.0教程

Before we go

在高性能graphics領(lǐng)域蚜枢,特別是3D graphics領(lǐng)域,OpenGL無疑是目前的最佳選擇针饥,雖然祟偷,現(xiàn)在有很多集成度高的三方的庫或者SDK,但是學(xué)習(xí)一下OpenGL仍然是非常有好處的打厘,你可以了解基本的computer graphics的概念,這會讓你在使用它們的時候更加的從容贺辰。

OpenGL是一個跨平臺的高性能3D渲染API户盯,OpenGL ES是它的嵌入式平臺版本嵌施。

OpenGL ES

我們即將踏上學(xué)習(xí)OpenGL ES 2.0之旅,主要針對于Android平臺莽鸭,會有一系列文章來分享學(xué)習(xí)OpenGL ES的總結(jié)吗伤。

主要編程語言將使用Kotlin,對于Kotlin還不熟悉的同學(xué)可以先看前面的介紹實例來快速的熟悉一下硫眨。

Android上面的OpenGL ES一共有三個版本足淆,1.0,2.0以及現(xiàn)在的3.x(3.1, 3.2)礁阁,其中1.0是舊式的API巧号,與桌面版本的OpenGL非常接近,但是卻不太好用姥闭。從2.0開始丹鸿,API有較大變化,具體的渲染相關(guān)使用專門的著色語言來表達
矩陣的處理放到一個單獨的類Matrix中棚品,這樣解耦后靠欢,學(xué)習(xí)起來和理解起來相對容易,API也不會依賴于具體的對象铜跑,直接使用static式的GLES20或者GLES30就好了门怪。3.0是向后兼容的,它完全兼容2.0锅纺。所以掷空,從2.0開始學(xué)習(xí),是一個
比較好的選擇伞广,而且2.0被Android 2.3以后的SDK支持拣帽,應(yīng)該說目前所有的設(shè)備API上面都是支持OpenGL ES 2.0的(當(dāng)然,具體的支持情況還看硬件GPU)嚼锄。

為了方便减拭,在此系列文章中,OpenGL区丑,或者OpenGL ES或者GL拧粪,都是指OpenGL ES 2.0。

關(guān)于平臺沧侥,雖然我們是基于Android平臺來學(xué)習(xí)可霎,但是OpenGL是跨平臺的,所有平臺的GL的API(OpenGL, ES宴杀,或者WebGL癣朗,或者水果平臺)長的都類似,方法名字旺罢,以及參數(shù)都差不多旷余。雖然不可以直接使用绢记,但是當(dāng)作參考都沒有問題。

開發(fā)環(huán)境搭建

首先是Android app的開發(fā)環(huán)境搭建正卧,這個不多說了蠢熄,大家自行Google。SDK版本最好高一點炉旷,至少要是5.0 (API 20)以上吧签孔。

其次是Kotlin語言的支持,如是是Android Studio 3.0以上的版本窘行,自帶支持饥追,不用折騰。否則可以參考官方網(wǎng)站的指導(dǎo)抽高。

涉及到SDK相關(guān)的東西就是Activity判耕,我們是有頁面顯示的,所以必須要有一個Activity翘骂,這個都懂得壁熄。主要是widget就是android.oepngl.GLSurfaceView
以及android.opengl.GLSurfaceView.Renderer碳竟。GLSurfaceView是Android平臺專門用于OpenGL繪制的組件草丧,我們只需要創(chuàng)建一個
實例,然后做一些基本的配置就好了莹桅,每個例子的配置都是很類似昌执。重點就是要實現(xiàn)一個GLSurfaceView.Renderer,這個是OpenGL開發(fā)的重點诈泼。

Step by step guide

首先懂拾,新建一個Android app項目,注意帶上Kotlin支持铐达,默認是鉤上的岖赋。名字隨意,比如叫EffectiveGL瓮孙。

然后唐断,在項目新建一個空白Activity,不用鉤選backward compat和創(chuàng)建layout杭抠,因為我們只用一個GLSurfaceView脸甘,用不著layout文件,另外偏灿,我們是用Kotlin丹诀,Kotlin是用Anko來用代碼寫布局。

再有,在Activity里面忿墅,創(chuàng)建一個GLSurfaceView對象扁藕,然后當(dāng)作Activity的布局。

最后疚脐,實現(xiàn)一個Renderer接口,塞給GLSurfaceView邢疙,并對其做簡單的配置棍弄。

最終,一個準(zhǔn)備好開發(fā)OpenGL的基本代碼是這樣子的疟游,這些基礎(chǔ)的準(zhǔn)備工作呼畸,后面的示例中會略掉。

class HelloPoints : Activity() {
    private lateinit var glSurfaceView: GLSurfaceView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        title = "Play with Points"

        glSurfaceView = GLSurfaceView(this)
        setContentView(glSurfaceView)

        glSurfaceView.setEGLContextClientVersion(2)
        glSurfaceView.setRenderer(PointsRender)
        glSurfaceView.renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
    }

    override fun onResume() {
        super.onResume()
        glSurfaceView.onResume()
    }

    override fun onPause() {
        super.onPause()
        glSurfaceView.onPause()
    }

    companion object PointsRender : GLSurfaceView.Renderer {
        override fun onDrawFrame(p0: GL10?) {
        }

        override fun onSurfaceChanged(p0: GL10?, p1: Int, p2: Int) {
        }

        override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
        }
    }
}

基礎(chǔ)概念理解

有一些基礎(chǔ)中的基礎(chǔ)的概念需要理解一下,才能開始碼代碼。剛接觸這么多概念首量,可能還沒有理解它們旁振,沒有關(guān)系,先建立一個大概印象刁品,隨著學(xué)習(xí)的深入,就慢慢理解它們了。

GL context

GL API的調(diào)用蹦漠,雖然都是static形式的,沒有限制车海,在哪里都能直接call笛园,但是實際上它是有一個上下文環(huán)境的,叫GL context(目前階段先這么叫著吧侍芝,不是太嚴(yán)謹(jǐn)哈)研铆。這有點聽不懂,用人話說州叠,
就是所有的GL API的調(diào)用都要在GLSurfaceView.Renderer的三個方法里面來call棵红,就是方法的調(diào)用棧必須從這幾個方法開始。在其他地方call是沒有效果的:

onSurfaceCreated

onSurfaceChanged

onDrawFrame

GL的坐標(biāo)系

OpenGL的坐標(biāo)系是所謂的右手坐標(biāo)系留量。

右手坐標(biāo)系


首先它是三維的笛卡爾坐標(biāo)系:原點在屏幕正中窄赋,x軸從屏幕左向右,最左是-1楼熄,最右是1忆绰;y軸從屏幕下向上,最下是-1可岂,最上是1错敢;z軸從屏幕里面向外,最里面是-1,最外面是1稚茅。


右手坐標(biāo)系示意

shader

GL ES 2.0與1.0版本最大的區(qū)別在于纸淮,把渲染相關(guān)的操作用一個專門的叫作著色語言的程序來表達,全名叫作OpenGL ES Shading language亚享,它是一個編程語言咽块,與C語言非常類似,能夠直接操作矩陣和向量欺税,運行在GPU之上
專門用于圖形渲染侈沪。它又分為兩種,一個叫做頂點著色器(vertex shader)晚凿,另一個叫做片元著色器(fragment shader)亭罪。前者用來指定幾何形狀的頂點;后者用于指定每個頂點的著色歼秽。
每個GL程序必須要有一個vertex shader和一個fragment shader应役,且它們是相互對應(yīng)的。(相互對應(yīng)燥筷,意思是vertex shader必須要有一個fragment shader箩祥,反之亦然,但并不一定是一一對應(yīng))荆责。當(dāng)然滥比,也是可以復(fù)用的,
比如同一個vertex shader做院,可能會多個fragment shader來表達不同的著色方案盲泛。

坐標(biāo)值和顏色值

坐標(biāo)正常的取值范圍都是-1到1,且是float類型键耕。
顏色值是0到1寺滚,也是float類型,0是空(無的意思屈雄,比如黑色村视,或者全透明),1是有(全的意思酒奶,比如白色蚁孔,或者不透明),有些API是使用0~255惋嚎,這時就需要轉(zhuǎn)換一下杠氢。
其實呢,寫成超過此范圍的值也是可以的另伍,比如坐標(biāo)傳2鼻百,或者顏色寫成5,OpenGL會處理成為它的合理的取值之內(nèi),用clamp的方式温艇,超過的會被砍掉因悲,如傳5,相當(dāng)于傳1勺爱。

好了晃琳,準(zhǔn)備工作差不多了,我們來擼代碼吧琐鲁。

年輕人的第一個OpenGL程序

我們的目標(biāo)是畫一個紅色的點蝎土,就是這個樣子的:

畫一紅色的點


注意: 鑒于方便理解,我們暫時只做一些2D的渲染绣否,也不調(diào)整view port,因為這會涉及比較復(fù)雜的Model View Projection矩陣的設(shè)置挡毅。

最終的代碼就是這個樣子的蒜撮,重點看一下Renderer的實現(xiàn),后面詳細講解:

const val TAG = "HelloPoints"

class HelloPoints : Activity() {
    private lateinit var glSurfaceView: GLSurfaceView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        title = "Play with Points"

        glSurfaceView = GLSurfaceView(this)
        setContentView(glSurfaceView)

        glSurfaceView.setEGLContextClientVersion(2)
        glSurfaceView.setRenderer(PointsRender)
        glSurfaceView.renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
    }

    override fun onResume() {
        super.onResume()
        glSurfaceView.onResume()
    }

    override fun onPause() {
        super.onPause()
        glSurfaceView.onPause()
    }

    companion object PointsRender : GLSurfaceView.Renderer {
        private const val VERTEX_SHADER =
                "void main() {\n" +
                        "gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n" +
                        "gl_PointSize = 20.0;\n" +
                        "}\n"
        private const val FRAGMENT_SHADER =
                "void main() {\n" +
                        "gl_FragColor = vec4(1., 0., 0.0, 1.0);\n" +
                        "}\n"
        private var mGLProgram: Int = -1

        override fun onDrawFrame(p0: GL10?) {
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
            GLES20.glUseProgram(mGLProgram)

            GLES20.glDrawArrays(GLES20.GL_POINTS, 0, 1)
        }

        override fun onSurfaceChanged(p0: GL10?, p1: Int, p2: Int) {
            GLES20.glViewport(0, 0, p1, p2)
        }

        override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
            GLES20.glClearColor(0f, 0f, 0f, 1f)

            val vsh = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER)
            GLES20.glShaderSource(vsh, VERTEX_SHADER)
            GLES20.glCompileShader(vsh)

            val fsh = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER)
            GLES20.glShaderSource(fsh, FRAGMENT_SHADER)
            GLES20.glCompileShader(fsh)

            mGLProgram = GLES20.glCreateProgram()
            GLES20.glAttachShader(mGLProgram, vsh)
            GLES20.glAttachShader(mGLProgram, fsh)
            GLES20.glLinkProgram(mGLProgram)

            GLES20.glValidateProgram(mGLProgram)

            val status = IntArray(1)
            GLES20.glGetProgramiv(mGLProgram, GLES20.GL_VALIDATE_STATUS, status, 0)
            Log.d(TAG, "validate shader program: " + GLES20.glGetProgramInfoLog(mGLProgram))
        }
    }
}

示例代碼講解

基礎(chǔ)設(shè)施

先來看一下Activity的onCreate/onResume和onPause這三個方法跪呈。先是在onCreate里面創(chuàng)建一個GLSurfaceView實例段磨,設(shè)置為content view,因為我們要使用OpenGL ES 2.0耗绿,所以要setEGLContextClientVersion(2)苹支。然后,再
設(shè)置一個Renderer實例误阻,渲染模式(render mode)分為兩種债蜜,一個是GLSurfaceView主動刷新(continuously),不停的回調(diào)Renderer的onDrawFrame究反,另外一種叫做被動刷新(when dirty)寻定,就是當(dāng)請求刷新時才調(diào)一次onDrawFrame。

這里我們用continuously的方式精耐。


至于onResume/onPause狼速,API要求是要調(diào)用一下GLSurfaceView的onResume和onPause,照做就好卦停,對于我們的示例來說向胡,其實調(diào)與不調(diào)看不出區(qū)別。這只是影響離開Activity頁面時的性能惊完,我們學(xué)習(xí)初期僵芹,可以不予關(guān)注。

Renderer之onSurfaceCreated

這個是最先被回調(diào)到的方法专执,告訴你系統(tǒng)層面淮捆,已經(jīng)ready了,你可以開始做你的事情了。一般我們會在此方法里面做一些初始化工作攀痊,比如編譯鏈接shader程序桐腌,初始化buffer等。我們一行一行的來分析:

GLES20.glClearColor(0f, 0f, 0f, 1f) // 參數(shù)順序 r, g, b, a

這句是告訴OpenGL苟径,給我把背景案站,或者叫作畫布,畫成黑色棘街,不透明蟆盐。比較繞人的說法是用參數(shù)指定的(r, g, b, a)這個顏色來初始化顏色緩沖區(qū)(color buffer)。目前就理解成為畫面背景色就可以了遭殉。

接下來的這一坨是編譯和鏈接shader程序:

val vsh = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER)

創(chuàng)建一個vertex shader程序石挂,返回的是它的句柄,此返回值會用在后續(xù)操作的參數(shù)险污,所以痹愚,要用變量記錄下來。

GLES20.glShaderSource(vsh, VERTEX_SHADER) // 告訴OpenGL蛔糯,這一坨字串里面是vertex shader的源碼拯腮。
GLES20.glCompileShader(vsh) // 編譯vertex shader

接下來的三行,是編譯fragment shader蚁飒,跟vertex shader是一樣的动壤。
然后是創(chuàng)建shader program并把shader鏈到上頭去。同樣的淮逻,先創(chuàng)建一個shader program句柄琼懊,后面要用,所以要記錄一下弦蹂,因為要在此方法外使用program句柄肩碟,所以要用全局變量來記錄。

mGLProgram = GLES20.glCreateProgram() // 創(chuàng)建shader program句柄
GLES20.glAttachShader(mGLProgram, vsh) // 把vertex shader添加到program
GLES20.glAttachShader(mGLProgram, fsh) // 把fragment shader添加到program
GLES20.glLinkProgram(mGLProgram) // 做鏈接凸椿,可以理解為把兩種shader進行融合削祈,做好投入使用的最后準(zhǔn)備工作

到此,其實shader program的準(zhǔn)備工作已經(jīng)做完了脑漫,但是如果shader編譯或者鏈接過程出錯了怎么辦呢髓抑?能不能提早發(fā)現(xiàn)呢?當(dāng)然优幸,有辦法檢查一下吨拍,就是用接下來的這幾句:

GLES20.glValidateProgram(mGLProgram) // 讓OpenGL來驗證一下我們的shader program,并獲取驗證的狀態(tài)
val status = IntArray(1)
GLES20.glGetProgramiv(mGLProgram, GLES20.GL_VALIDATE_STATUS, status, 0) // 獲取驗證的狀態(tài)
Log.d(TAG, "validate shader program: " + GLES20.glGetProgramInfoLog(mGLProgram))

如果有語法錯誤网杆,編譯錯誤羹饰,或者狀態(tài)出錯伊滋,這一步是能夠檢查出來的。如果一切正常队秩,則取出來的status[0]為0笑旺。

Renderer之onSurfaceChanged

此回調(diào),會在surface發(fā)生改變時馍资,通常是size發(fā)生變化筒主。這里我們改變一下視角。

GLES20.glViewport(0, 0, p1, p2) // 參數(shù)是left, top, width, height

就是要指定OpenGL的可視區(qū)域(view port)鸟蟹,(0, 0)是左上角乌妙,然后是width和height。
我們目前只學(xué)習(xí)2D繪制建钥,所以藤韵,先不管三維視角的處理。

Renderer之onDrawFrame

這個是最重要的方法熊经,沒有之一荠察。前面兩個,只會在surface created時調(diào)一次奈搜。而此方法是用來繪制每幀的,所以每次刷新都會被調(diào)一次盯荤,所有的繪制都發(fā)生在這里馋吗。

GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) // 清除顏色緩沖區(qū),因為我們要開始新一幀的繪制了秋秤,所以先清理宏粤,以免有臟數(shù)據(jù)。
GLES20.glUseProgram(mGLProgram) // 告訴OpenGL灼卢,使用我們在onSurfaceCreated里面準(zhǔn)備好了的shader program來渲染
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, 1) // 開始渲染绍哎,發(fā)送渲染點的指令, 第二個參數(shù)是offset鞋真,第三個參數(shù)是點的個數(shù)崇堰。目前只有一個點,所以是1涩咖。

vertex shader

private const val VERTEX_SHADER =
                "void main() {\n" +
                        "gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n" +
                        "gl_PointSize = 20.0;\n" +
                        "}\n"

shader語言跟C語言很像海诲,它有一個主函數(shù),也叫void main(){}檩互。

gl_Position是一個內(nèi)置變量特幔,用于指定頂點,它是一個點闸昨,三維空間的點蚯斯,所以用一個四維向量來賦值薄风。vec4是四維向量的類型,vec4()是它的構(gòu)造方法拍嵌。等等遭赂,三維空間,不是(x, y, z)三個嗎撰茎?咋用vec4呢嵌牺?
四維是叫做齊次坐標(biāo),它的幾何意義仍是三維龄糊,先了解這么多逆粹,記得對于2D的話,第四位永遠傳1.0就可以了炫惩。這里僻弹,是指定原點(0, 0, 0)作為頂點,就是說想在原點位置畫一個點他嚷。gl_PointSize是另外一個內(nèi)置變量蹋绽,用于指定點的大小。

這個shader就是想在原點畫一個尺寸為20的點筋蓖。

fragment shader

private const val FRAGMENT_SHADER =
                "void main() {\n" +
                        "gl_FragColor = vec4(1., 0., 0.0, 1.0);\n" +
                        "}\n"

gl_FragColor是fragment shader的內(nèi)置變量卸耘,用于指定當(dāng)前頂點的顏色,四個分量(r, g, b, a)粘咖。這里是想指定為紅色蚣抗,不透明。

Fun time

更改一些參數(shù)瓮下,看看會發(fā)生什么:

  1. 改變onSurfaceCreated中的glClearColor的顏色值
  2. 改變gl_Position
  3. 改變gl_PointSize
  4. 改變gl_FragColor

One more thing

此系列教程會共存在同一個Android app項目里面翰铡,所以我們會隨著代碼的增加而進行一系列的重構(gòu),但是這與我們的主題OpenGL無關(guān)讽坏,如果是單純學(xué)習(xí)OpenGL锭魔,可以略過此節(jié)。

因為路呜,每個教程會講解不同的點迷捧,對Activity可能有不同的需求,所以胀葱,一個教程對應(yīng)著一個Activity党涕,這樣就需要一個列表來作為路由目錄頁面:

class HomeActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        title = "Learn OpenGL ES Effectively"

        verticalLayout {
            textView("Welcome to the world of OpenGL ES") {
                gravity = Gravity.CENTER
            }.onClick { startActivity<HelloPoints>() }
        }
    }
}

參考資料

  • 《WebGL Programming Guide》

    WebGL跟OpenGL ES 2.0相差無幾,可以直接參考巡社。這本書最大好處是講解比較清晰膛堤,層次遞進,代碼完整晌该,非常適合初學(xué)者上手肥荔。
  • 《OpenGL? ES 2.0 Programming Guide》

    這本書比較啰嗦和枯燥绿渣,它更接近于規(guī)范,非常詳盡嚴(yán)謹(jǐn)?shù)闹v述燕耿,但是講解過少中符,示例也少。所以誉帅,它更適合于有一定基礎(chǔ)淀散,想要更深入的全面的理解某一概念時看,不適合入門蚜锨。

    所以档插,這兩本書加起來看效果最佳,先入門亚再,理解基本概念郭膛,然后再通過后者全面理解,鞏固加強氛悬。

原文鏈接:http://toughcoder.net/blog/2018/07/31/introduction-to-opengl-es-2-dot-0/

微信公眾號:稀有猿訴

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末则剃,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子如捅,更是在濱河造成了極大的恐慌棍现,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件镜遣,死亡現(xiàn)場離奇詭異轴咱,居然都是意外死亡,警方通過查閱死者的電腦和手機烈涮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窖剑,“玉大人坚洽,你說我怎么就攤上這事∥魍粒” “怎么了讶舰?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長需了。 經(jīng)常有香客問我跳昼,道長,這世上最難降的妖魔是什么肋乍? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任鹅颊,我火速辦了婚禮,結(jié)果婚禮上墓造,老公的妹妹穿的比我還像新娘堪伍。我一直安慰自己锚烦,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布帝雇。 她就那樣靜靜地躺著涮俄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪尸闸。 梳的紋絲不亂的頭發(fā)上彻亲,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音吮廉,去河邊找鬼苞尝。 笑死,一個胖子當(dāng)著我的面吹牛茧痕,可吹牛的內(nèi)容都是我干的野来。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼踪旷,長吁一口氣:“原來是場噩夢啊……” “哼曼氛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起令野,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤舀患,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后气破,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體聊浅,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年现使,在試婚紗的時候發(fā)現(xiàn)自己被綠了低匙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡碳锈,死狀恐怖顽冶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情售碳,我是刑警寧澤强重,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站贸人,受9級特大地震影響间景,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜艺智,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一倘要、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧十拣,春花似錦碗誉、人聲如沸召嘶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽弄跌。三九已至,卻和暖如春尝苇,著一層夾襖步出監(jiān)牢的瞬間铛只,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工糠溜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留淳玩,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓非竿,卻偏偏與公主長得像蜕着,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子红柱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

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