opengl es初学习


声明:

 opengl是一个跨平台的图形,广泛采用的3D api;opengl es 是一个为手持和嵌入设备为目标,对opengl相应标准进行精简而来的;实现了具有可编程着色功能的图形管线,所以它包括es 和es着色器语言(我个人对着色器语言的初步理解);管线流程,这个确实有必要,但是,一个入门应该是先有个直接的印象,所以,我先上个程序来看下

学习文档:

参考书籍:opengl es编程指南3.0

示例

对于android来说,使用可以是ndk,也可以是java层

java api提供了GLSurfaceView,它的绘制主要在Render回调中

1、检测opengl es版本

应用市场声

明:低于3.0版本的手机不可安装

<uses-feature android:glEsVersion="0x00030000" android:required="true"/>

opengl es 3.0  对应android 4.3

opengle es 2.0 对应android 2.2  (有人说使用2.0,因为可以兼容3.0, 如果需要使用3.0的新特性,可以判断使用)

opengl es 1.0 已经退出历史舞台了。。。

检查支持版本

如果不是使用真机测试,支持情况,请左转

0x2000是2.0版本

2、设置布局和声明界面刷新回调

设置版本:setEGLContextClientVersion

设置刷新回调:setRenderer

布局回调设置

3、回调

onSurfaceCreated:

 每次创建都会回调一次,也就是创建后只有一次,但不一定只有一次创建

里面主要的操作是,生成包括vetex shader和fragment shader的程序

shader: 创建---- 添加shader程序-----编译

program:创建---- 添加shader ---- 链接

onSurfaceCreated

onSurfaceChanged:

横竖屏变化时,调用;设置展示区域

onSurfaceChanged

onDrawFrame:绘制每一帧内容,可以跟随系统,也可以自己控制

绘制图形

onDrawFrame

这是一个绘制矩形的示例,代码github

是不是感觉都什么方法啊,我都不会啊,不急,下面慢慢来,上面只是让看官感受下

opengl es内容

opengl es的api对于所有的平台都应该是相同的方法,具体平台可能参数,返回值略有不同;而由于android开发,ndk和java-api,所以,我们讲常用api方法

图片来源其它博客,不想自己绘制了;这就是api绘制的总过程;

图形管线图

方法:以gl前缀,还有可能采用参数数量,数据类型

数据类型: GL开头

常量:GL_开头

方法执行成功与否均需要自己判断

android平台 java层,都封装到GLES30(3.0版本)中

特别注意:介绍的api全部是c语言的形式,其它平台方法名一致,功能一致,参数并不一定一致,参数类型也有区别,意义还是说了的

指针传入,可以获取结果;c中一般都这样搞,java语言引用对象作参数也可以,但很少这样搞

c中方法返回值,表示成功与否,所以才这样搞;返回false表示失败

有几个特别的函数

glGetError() :获取当前错误代码,并复位

glEnable:启用某个功能

glDisenable:禁用某个功能

glIsEnabled:查询某个功能

a、EGL

opengl es需要渲染上下文和绘制表面,这就是EGL干的事,它是opengl ES到具体系统的中间件,具体实现和硬件有关;渲染上下文存储相关状态,绘制表面是用于绘制图元的表面,需要指定缓存区类型、缓存区位深度等;总体来说主要干三件事

查询并初始化可用显示器

创建渲染表面:屏幕内表面,连接到系统窗口;屏幕外表面,是可以用作渲染的像素缓存取

创建渲染上下文

EGL命名规则和GL类似,以egl, EGL, EGL_开头

eglGetDisplay(displayId):打开EGL显示服务器的链接,参数使用EGL_DEFAULT_DISPLAY

eglInitalize(diaplayId, *majorVersion, *minorVersio):初始化,后两个参数是返回的版本号结果,可能为null

eglGetConfigs(displayId,*configs, max, *numConfigs):获取可用表面配置;1、configs传入null,这是numConfigs返回可用数目;2、configs未初始化数组,max传入数组大小,numConfigs传回实际可用个数(为了使用更少的内存,可以先使用1获取可用个数,2再获取具体可用值);EGL包括颜色,缓存区,表面类型等特性信息

eglGetConfigAttrib(displayId, config, attrib, *value):获取某个特性信息的值

eglChooseConfig(diaplayId,*attriList, *config, max, *num):让EGL自己根据attrlist属性列表选择匹配的config信息,max最多返回个数,num实际返回个数

eglCreateWindowSurface(displayId, config, window, *attribList):创建屏幕内渲染区域;需要原生窗口系统先创建一个窗口window,attribList为单一属性,即指出渲染表面是前台缓存区还是后台缓存区 eg:attribList[] = {EGL_RENDER_BUFFR,  EGL_BACK_BUFFER, EGL_NONE}

EGL_NONE:结束,其它都是值对的形式

eglCreatePbufferSurface(displayId, config, *attribList):创建屏幕外渲染区域,也叫pbuffer(像素缓存区),attribList和上个方法书写类似,只不过属性不同

eglCreateContext(displayId, config, contexFlag, *attrbList):创建上下文,attrbList只有一个属性,即版本号,需要和使用openGL es的版本好一致

eglMakeCurrent(displayId, drawSurface, readSurface, context):指定context为当前上下文

同步渲染(不会。。。)

eglWaitClient()和eglWaitNative()

b、着色器和程序

OpenGL ES中每个程序必须有一个vetex shader和fragment shader

glCreateShader(type):创建shader,type有两种:GL_VETEX_SHADER, GL_FRAGMENT_SHADER

glDeleteShader(shader):删除

glShaderSource(shader, count, *str, *length):提供着色器代码,count着色器源字符串数量,为1,str字符串,length字符串长度

glCompileShader(shader):编译

对于shader编译成功与否需要校验:

glGetShaderiv(shader, name, *params):name查询着色器相关状态:一般我们用编译状态:GL_COMPILE_STATUS

glGetShaderInfoLog(shader,maxL, *len, *info):log缓存区大小maxL,写入len长度,写入信息info

glReleaseShaderCompiler:释放着色器编译使用的资源


glCreateProgram():创建程序

glDeleteProgram(program):删除程序

glAttachShader(program,shader):添加shader到程序program

glDetachShader(program, shader):断开

glLinkProgram(program):链接,生成最终可执行程序

对于程序链接成功需要校验:

glGetProgramiv(program, name, *param):相似不? name一般取GL_LINK_STATUS

glGetProgramInfoLog(program,maxL, *len, *info):相似不?

链接成功,并不代表有效,所有还要进行最后一步:

glValidateProgram(program)

保存到系统中多次使用:

glGetProgramBinary   glProgramBinary

校验在开始联系阶段,debug版本都是要做的,为了更好的验证shader编写

glUseProgram(program):设置为活动程序进行渲染

现在对于shader还没有完。。。如果我们不对shader交互,直接后台处理上面这些就ok了啊,我们只需要设置shader程序不是很简单

与着色器交互

统一变量(uniform):存储应用程序通过gl es-api传递给着色器的只读常数值变量

多个统一变量可以组合为统一变量块,类似c中struct(关键字变换而已。。。)

特别注意:如果在vetex和fragment着色器中均有声明,必须完全一致

被使用的统一变量块,是活动的,不使用会被优化掉。。。

统一变量

glGetUniformLocation(program, *name):查找name的统一变量位置(位置可以直接用layout(location = 数值)来指定)

设置uniform值

glUniform+数字+类型首字母+v(向量,可以省略,这时,需要传入数字个相应类型的值)

glUniformMatrix+数字m+x+数字n+类型+v:m×n矩阵

参数:位置,加载数组元素个数,行优先(GL_TRUE)或者列优先,统一变量值,元素数组

如果shader是你写的,上面几个就够用了,但是如果相互独立开发,那么你需要取定类型

glGetActiveUniform(program, index, bufSize, *len,*size, *type, *name):index索引,名称数组的字符数,名字数组写入字符数(输出),统一变量大小,统一变量类型(输出),统一变量名字(输出);用于统一变量

glGetActiveUniformsiv(program, count, *indices, name, *params):啥东西,不太懂。。。


检索统一变量块

统一变量缓存区:存储统一变量块的,单独统一变量没有这个东东。。没用过,不太懂,大致写下,以后便于研读

glGetUniformBlockIndex(program, *name)

glGetActiveUniformBlockName(program, index, bufSize, *len, *name)

glGetActiveUniformBlockiv(program, index, name, *param)

glUniformBlockBinding(program, index, binding):将索引和程序中统一变量块绑定点关联

绑定到目标:GL_UNIFORM_BUFFER

glBindBufferRange(target, index, buffer, offset, size)

glBindBufferBase(target, index, buffer)

buffer:缓存区句柄,offset:字节计算,对象起始偏移量,size能够读取和写入数据量大小

还有些方法:glBufferData, glBufferSubData, glMapBufferRange, glUnmapBuffer

b、顶点属性、数组和缓存区对象

vetex着色器中以in修饰符作为vetex属性,只读,不能修改

glGetActiveAttrib

glVertexAttrib+数字+类型+v:设置常量定点属性

glVertexAttribPointer(position, size, type, normalized, stride, *ptr):size每个定点的大小维度(1,2,3,4),normalized表示非浮点转换为浮点是否规范化,stride:顶点数据间隔步长,ptr数据

glEnableVertexAttribArray:启动顶点属性数组

glDisableVertexAttribArray:禁用

glGetAttribLocation(program,*name):获取位置

glBindAttribLocation(program, index, *name)绑定到位置

glDrawArrays 或glDrawElements 调用时,顶点数据从客户内存复制到图形内存

顶点缓存区对象

顶点数据缓存-GL_ARRAY_BUFFER 和图元数据缓存-GL_ELEMENTS_ARRAY_BUFFER

glGenBuffers(n, *buffers):生成n个条目的数组指针buffers

glBindBuffer(target, buffer):target类型缓存区使用buffer缓存

glBufferData(target, size, *data, usage):创建和初始化;data客户区数据指针??

glBufferSubData(target, offset,size, *data)创建和初始化;data客户区数据指针

glDeleteBuffers(n, *buffers)


顶点缓存区优于顶点数组

顶点数组对象(VAO)

glGenVetexArrays(n, *arrays)

glBindVetexArray(array)

glDeleteVetexArrays(n, *arrays)

映射缓存区对象 由于普通缓存区

*glMapBufferRange(target, offset, len, access):返回缓存区存储范围指针,access访问标志位域组合 GL_MAP_READ_BIT,GL_MAP_WRITE_BIT等,只不过这两个至少包括一个,其它标志可选

glUnmapBuffer(target) :取消映射

*glFlushMappedBufferRange(target, offset, length)

glCopyBufferSubData(readTarget,writeTarget,readOffset, writeOffset, size)

c、图元和光栅化

图元就是使用绘制方法绘制的几何形状对象,由一组表示顶点位置的顶点描述,每个顶点可以关联颜色,纹理坐标、几何法线

opengl es可渲染三种图元:点,线,三角形, 颜色对于图元来说是填充

三角形

GL_TRIANGLES(3个点一组,组成各自的三角形)

GL_TRIANGLE_STRIP(按照顺序相邻的三个点组成三角形)

GL_TRIANGLE_FAN(第一个点参与每个三角形的连接, 其它两个点为相邻的两个点)

直线图元:

GL_LINES(两个点一条线,一系列不相连的线)、GL_LINE_STRIP(相邻点均以直线连接)、GL_LINE_LOOP(相邻两点相连,头尾也连)

glLineWidth(width):线宽,像素值

点图元

GL_POINTS:一系列点

内建变量: 

gl_PointSize 点半径或者点尺寸;有限制范围,可以查询

glGetFloatv(GL_ALIASED_POINT_SIZE_RANGE, *range):range尺寸为2的一维数组

gl_PointCoord:作用点,在fragment shader中使用,vec2类型,float精度mediump

绘制图元

glDrawArrays(mode, first, count):图元类型mode,顶点数据中起始点first,数量count

glDrawElements(mode, count, type, *indices)

glDrawRangeElements(mode, start, end, count, type, *indices):数据指针indices

type:保存元素索引类型:有效值:GL_UNSIGNED_BYTE、GL_UNSIGNED_SHORT,GL_UNSIGNED_INT

图元重启:使用索引类型的最大值隔开多个不相连的图元

几何形状实例化

glDrawArraysInstanced(mode, ffirst, count, instanceCount):instanceCount绘制图元的实际数量

glDrawElementsInstanced(mode, count, type, *indecs, instanceCount)

glVetexAttribDivisor(index, divisor):index索引,divisor指定index位置属性更新之间传递实例数量

图元装配:

裁剪、透视分割、视口变换

坐标系:规范坐标系(左x正,上y正,左边半轴长度为1),窗口坐标(设备实际坐标,android左上角远点,右下正)

视口变换:视口是一个二维窗口区域

glViewport(x, y, w, h)

glDepthRangef(n, f) 深度范围设置n~f,默认0f~1f

光栅化:

图元裁剪之后,光栅化管道取得单独图元,并为其生成对应片段

剔除

glFrontFace(dir):dir方向,取值GL_CW或者GL_CCW

glCullFace(mode):剔除的面,GL_FRONT,GL_BACK,GL_FRONT_AND_BACK

glEnable/glDisable(cap): cap = GL_CULL_FACE 启用或者禁用剔除

多边形偏移

由于光栅化精度问题,会造成多个多边形重叠,造成伪像;偏移就是解决这个问题的

glPolygonOffset(factor, unit)

禁用启动标志:GL_POLYGON_OFFSET_FILL

遮挡查询

用查询对象来跟踪通过深度测试的任何片段和样本

glBeginQuery(target, id)

glEndQuery(target)

glGenQueries(n, *ids)

glDeleteQueries(n, *ids)

glGetQueryObjetuiv(id, name, *params)

target:有效值:GL_ANY_SAMPLE_PASSED, GL_ANY_SAMPLE_PASSED_CONSERVATIVE, GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN

d、vetex shader

提供顶点操作的通用编程方法

其输入包括:属性、统一变量和统一变量缓存区、采样器、着色器程序

内建变量

gl_VertexID:输入,highp,顶点的整数索引,什么鬼。。。

gl_InstanceId:输入,实例化绘图调用中图元实例编号,默认为0

gl_Position   gl_PointSize

内建常量:。。。

常用技术:矩阵变换、生成纹理坐标、顶点蒙皮

变换反馈:glTransformFeedbackVaryings  glBeginTransformFeedback  glEndTransformFeedback等方法

这一节是对编写shader的一个功底考验,也是opengl es绘制各种图形的关键

e、纹理

表现出从网格几何图形无法得到的附加细节,2D纹理,2D纹理数组,3D纹理,立方图纹理

2D纹理

一个图像数据的二维数组,一个纹理单独的数据元素称为纹素

立方图纹理

由6个2D纹理组成的纹理

3D纹理

看作2D纹理多个切片的一个数组,体纹理

2D纹理数组

素组的每个切片表示纹理动画的一帧

纹理对象和纹理加载

创建纹理对象,是一个容器对象,保存渲染所需的纹理数据,例如图像数据、过滤模式、包装模式,用无符号整数表示,也就是纹理对象句柄

glGenTextures(n, *textures)

glDeteleTextures(n, *texture)

glBindTexture(target, texture): target取值:GL_TEXTURE_2D、GL_TEXTURE_3D、 GL_TEXTURE_2D_ARRAY 、GL_TEXTURE_CUBE_MAP


glTexture2D(target, level, iformat, w, h, border, format, type, *pixs)

ifromat:纹理保存的内部格式,format:输入纹理数据格式,type:输入像素数据类型


glPixelStorei(name, param)

过滤包装 glTexParameter+[i|f][v](target, name, param)

glGenerateMipmap(target):在绑定纹理对象上生成mip贴图

glActiveTexture(texture):设置为当前纹理单元,为后续绑定作准备

着色器内建函数:texture(sampler, coord  [, bias]):sampler采样器, coord从纹理贴图中读取的[2|3]D纹理坐标, bias可选,mip贴图偏置

glTexImage3D(target, level, iformat, w, h [, depth], border, imgeSize, *data)

glCompressedTexImage[2|3]D(target, level, iformat, w, h [, depth], border, imgeSize, *data)

gl[Compressed]TexSubImage2D(target, level ,xoffset, yoffset, w, h, format, type, *pixs)

gl[Compressed]TexSubImage3D(target, level ,xoffset, yoffset,zoffset, w, h,depth,  format, type, *pixs)


glReadBuffer

glCopyTexImage2D

glCopyTexSubImage2D

glCopyTexImage3D

glCopyTexSubImage3D

采样器对象

glGenSamples(n, *samples)

glDeleteSamples(n, *samples)

glBindSampler(unit , sampler): unit 纹理单元

glSamplerParameter[i|f][v](sampler, name ,param)

不可变纹理:

glTextStorage2D(target, levels,format, w, h)

glTextStorage3D(target, levels,format, w, h, depth)

f、fragment shader

内建变量:gl_FragCoord、 gl_FrontFacing 、gl_PointCoord 、gl_FragDepth等

必须使用精度限定符

技术:多重纹理,雾化,alpha测试,用户裁剪平面

g、片段操作

清除缓存区

glClear(mask): 清除缓存区,mast GL_[COLOR|DEPTH|STENCIL]_BUFFER_BIT,不同标志位使用不同的清理方式

glClearColor(red, green, blue, alpha):指定颜色清除缓存区

glClearDepthf(depth):指定深度清理

glClearStencil(s):指定模板清理

多个绘图缓存区清理

glClearBuffer[iv|uiv|fv](buffer, drawbuffer, *value):buffer缓存区类型,drawbuffer缓存区名字,一个思维向量(颜色)或者一个值的指针(深度,模板)

glClearBufferfi(buffer, drawbuffer, depth, stencil):buffer必须为GL_DEPTH_STENCIL, drawbuffer必须为0,清除缓存区depth深度值,stencil模板值

使用标志位控制

glColorMask (bred, bgreen, bblue, blpha)

glDepthMask(bdepth)

glStencilMast(bs)

glStencilMaskSeparate(face, mask):face图元的面顶点

片段测试:默认所有片段测试和操作都被禁用,可以使用glEnable开启

裁剪测试:glScissor(x, y, w, h); 启用标志GL_SCISSOR_TEST

模板缓存区测试:glStencilFunc(func,ref, mask)

glStencilFuncSeparate(face, func, ref, mask)

启动标志:GL_STENCIL_TEST

face:面类型,func比较函数,ref比较值

深度缓存区测试:深度缓存区通常用与隐藏表面的消除 glDepthFunc(func)-GL_DEPTH_TEST

混合测试:

glBlendFunc(sfactor, dfactor): sfactor输入系数,dfactor目标像素系数

glBlendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha)

glBlendColor(red, green, blue, apha)

glBlendEquation(mode):mode 混合元算

glBlendEquationSeparate(modeRgb, modeAlpha)

抖动: 模拟最大色深

启动标志:GL_SAMPLE_COVERAGE

glSampleCoverage(value, invert):value 样本掩码[0,1],invert指定掩码中所有位被反转

质心采样: centroid修饰符修饰

读取写入像素: glReadPixels(x, y, w, h, format, type, *pixels)

多重渲染目标:涉及帧缓存区对象

h、帧缓存区对象(FBO)

一组颜色、深度、和模板纹理或者渲染目标;仅支持单缓存区

glGenRenderbuffers(n, *renderbuffer):分配渲染缓存区

glGenFramebuffers(n, *ids)

glBindRenderbuffer(target, buffer):target= GL_RENDERBUFFER

glRenderbufferSorage(target, iformat, w, h)

glRenderbufferStorageMultisample(target, samples, iformat, w, h):samples:样本数

glBindFramebuffer(target, buffer): target取值: GL[_READ|_DRAW]_FRAMEBUFFER


连接渲染区作为帧缓存区的附着点

glFramebufferRenderbuffer(target, attachment, rendertarget, renderbuffer):帧缓存区目标,attachment必须为GL_[COLOE|DEPTH|STENCIL|DEPTH_STENCIL]_ATTACHMENT

glFramebufferTexture2D(target, attachment, textarget, texture, level)

glFramebufferTextureLayer(target, attachment, texture, level ,layer)

检查缓存区完整性:glCheckFramebufferStatus(target)

帧缓存区块传输: glBlitFramebuffer(srcx0, srcy0, srcx1, srcy1, dstx0, sdty0,dstx1, dsty1, mask, filter)

缓存区失效

glInvalidateFramebuffer(target, nums, *attachments)

glInvalidateSubFramebuffer(target, num , *attachments, x y, w, h)

删除帧缓存区

glDeleteRenderBuffers(n, *renderbuffer)

glDeleteFrameBuffers(n, *framebuffers)

i、同步对象和栅栏

与glFinish命令比更高效

glFenceSync(condition, flags):condition = GL_SYNC_GPU_COMMANDS_COMPLETE, 

flags当前必须为0;返回sync句柄

glDeleteSync(sync):删除

glClientWaitSync(sync, flags, timeout):阻塞,timeout单位纳秒;返回结果表示当前状态

glWaitSync(sync, flags, timeout):立即返回结果, flags必须为0


哈哈,终于大致过一遍了,上面的我大部分只有写概念,具体什么鬼,只能问上帝了,写了这些,就一行一行的撸代码了,体会后,再写续集

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

推荐阅读更多精彩内容