鸿蒙OpenGL入门,绘制三角形

一、简介

  在鸿蒙NDK开发入门中介绍了ArkTS与C/C++相互调用流程,本文承接上文,介绍使用OpenGL绘制一个三角形,通过绘制三角形来熟悉OpenGL的绘制流程。CPU和GPU都能用于图形渲染,部分场景下如果使用CPU渲染,性能就非常差。但GPU可以大大提高渲染速度,OpenGL可以操作GPU,是一个2D/3D图形库,用于视频渲染、视频编辑、视频特效、游戏引擎等。除了OpenGL外,ValKan、Metal、Direct3D、WebGL、WebGPU等都是优秀的2D/3D图形库。目前OpenGL应用最广泛,资料也最丰富,跨平台,学习起来相对容易。

二、OpenGL ES

  OpenGL ES是OpenGL的子集,专门用于手机、平板等小型设备,删除了不必要的方法、减少了体积。所以准确的来说,我们学习的其实是OpenGL ES

三、XComponent

  XComponent组件作为一种渲染组件,通常用于满足较为复杂的自定义渲染需求,例如相机预览流的显示、游戏画面的渲染、视频的渲染。XComponent又拥有单独的NativeWindow,可以在native侧提供native window用来创建EGL/OpenGLES环境,进而使用标准的OpenGL ES开发。

3、1添加EGL/OpenGLES库

# the minimum version of CMake.
cmake_minimum_required(VERSION 3.5.0)
project(egl)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

if(DEFINED PACKAGE_FIND_FILE)
    include(${PACKAGE_FIND_FILE})
endif()

include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include)

# 查找相关库 (包括OpenGL ES相关库和XComponent提供的ndk接口)
find_library( EGL-lib
              EGL )

find_library( GLES-lib
              GLESv3 )

find_library( libace-lib
              ace_ndk.z )

find_library(
    hilog-lib
    hilog_ndk.z)

add_library(egl SHARED
    napi_init.cpp
    manager/EglManager.cpp
    render/EglCore.cpp
    render/EglRender.cpp)
target_link_libraries(egl PUBLIC ${hilog-lib} ${EGL-lib} ${GLES-lib} ${libace-lib} libace_napi.z.so libc++.a)

3、2 ArkTS侧添加XComponent

  XComponent有三个重要的属性:

  • id : 与XComponent组件为一一对应关系,不建议重复。通常开发者可以在native侧通过OH_NativeXComponent_GetXComponentId接口来获取对应的id从而绑定对应的XComponent。如果id重复,在native侧将无法对多个XComponent进行区分。
  • type:指定为surface。
  • libraryname:加载模块的名称,必须与在native侧Napi模块注册时nm_modname的名字一致。
interface XComponentAttrs {
  id: string;
  type: number;
  libraryname: string;
}

@Component
export struct GLComponent {
  @State message: string = 'Hello World';
  xComponentContext: object | undefined = undefined;
  xComponentAttrs: XComponentAttrs = {
    id: 'xcomponentId', // 与XComponent组件为一一对应关系,不建议重复。可以在native侧通过OH_NativeXComponent_GetXComponentId接口来获取对应的id从而绑定对应的XComponent。
    type: XComponentType.SURFACE,
    libraryname: 'egl' // 加载模块的名称,必须与在native侧Napi模块注册时模块名字一致。
  }

  build() {
    Row() {
      Column() {
        XComponent(this.xComponentAttrs)
          .onLoad(() => {
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

3、2 获取OH_NativeXComponent

  在ArkTS侧添加XComponent后,就可以在C++侧获取OH_NativeXComponent。

  • 调用napi_get_named_property函数解析参数。
  • 调用napi_unwrap函数获取OH_NativeXComponent对象。
  • 调用OH_NativeXComponent_GetXComponentId获取id,这个id就是在ArkTS侧给XComponent设置的id。
void EglManager::Export(napi_env env, napi_value exports) {
    napi_value exportInstance = nullptr;
    if (napi_ok != napi_get_named_property(env, exports, OH_NATIVE_XCOMPONENT_OBJ, &exportInstance)) {
        LOGE("解析参数出错");
        return;
    }
    OH_NativeXComponent *nativeXComponent = nullptr;
    if (napi_ok != napi_unwrap(env, exportInstance, reinterpret_cast<void **>(&nativeXComponent))) {
        LOGE("获取OH_NativeXComponent对象出错");
        return;
    }
    // 获取id
    char idStr[OH_XCOMPONENT_ID_LEN_MAX + 1] = {'\0'};
    uint64_t size = OH_XCOMPONENT_ID_LEN_MAX + 1;
    if (napi_ok != OH_NativeXComponent_GetXComponentId(nativeXComponent, idStr, &size)) {
        LOGE("获取XComponentId出错");
        return;
    }
    string id(idStr);
    if (nativeXComponent != nullptr) {
        setNativeXComponent(id, nativeXComponent);
        getRender(id);
        // 注册回调
        OH_NativeXComponent_RegisterCallback(nativeXComponent, &EglRender::callback);
    }
}

3、4注册回调

  OH_NativeXComponent_Callback是个结构体,结构体里面是函数指针。

typedef struct OH_NativeXComponent_Callback {
    /** 当surface创建完成后回调 */
    void (*OnSurfaceCreated)(OH_NativeXComponent* component, void* window);
    /** 当surface发生改变时回调 */
    void (*OnSurfaceChanged)(OH_NativeXComponent* component, void* window);
    /** 当surface被销毁时回调 */
    void (*OnSurfaceDestroyed)(OH_NativeXComponent* component, void* window);
    /** 当触发触摸事件时调用 */
    void (*DispatchTouchEvent)(OH_NativeXComponent* component, void* window);
} OH_NativeXComponent_Callback;

  调用OH_NativeXComponent_RegisterCallback函数注册回调,OH_NativeXComponent_RegisterCallback函数的第一个参数是OH_NativeXComponent对象,第二个参数OH_NativeXComponent_Callback结构体指针。

OH_NativeXComponent_RegisterCallback(nativeXComponent, &EglRender::callback);

  当surface创建完成后回调OnSurfaceCreated函数,我们需要在OnSurfaceCreated函数里面搭建EGL环境,这样才能调用OpenGL ES的函数

四、EGL

  OpenGL是跨平台接口,面对不同平台的差异,需要有一个介于平台设备与OpenGL之间的桥梁,EGL则是其中的桥梁。EGL是OpenGL ES和系统之间的通信接口,OpenGL ES的平台无关性正是借助EGL实现的,EGL屏蔽了不同平台的差异。主要包括以下几个类:

  • EGLDisplay一个抽象的系统显示类,用于操作设备窗口,加载OpenGL库。
  • EGLConfig,EGL配置,如rgba位数。
  • EGLSurface渲染缓存,一块内存空间,所有要渲染到屏幕上的图像数据,都要先缓存在EGLSurface上。
  • EGLContext上下文,用于存储OpenGL的绘制状态信息、数据。

要想调用OpenGL ES的函数,首先就得搭建EGL环境

4、1 搭建EGL环境

  1、获取EGLDisplay对象:调用eglGetDisplay函数得到EGLDisplay,并加载OpenGL ES库。

EGLDisplay eglGetDisplay(EGLNativeDisplayType displayId);

  2、初始化EGL连接:调用eglInitialize函数初始化,获取库的版本号。

GLBoolean eglInitialize(EGLDisplay display,   // EGLDisplay对象
              EGLint *majorVersion, // 主版本号
              EGLint *minorVersion) // 次版本号

  3、确定渲染表面的配置信息:调用eglChooseConfig函数得到EGLConfig。

GLBoolean eglChooseConfig (EGLDisplay dpy, 
const EGLint *attrib_list, 
EGLConfig *configs, 
EGLint config_size, 
EGLint *num_config);

  4、创建渲染表面:通过EGLDisplay和EGLConfig,调用eglCreateWindowSurface函数创建渲染表面,得到EGLSurface。

EGLSurface eglCreateWindowSurface(    
    EGLDisplay display,    //EGLDisplay对象
     EGLConfig config,    //EGLConfig对象
     NativeWindowType native_window,    //原生窗口
     EGLint const * attrib_list);    // attrib_list为Window Surface属性列表,可以为NULL

  5、创建渲染上下文:通过EGLDisplay和EGLConfig,调用eglCreateContext函数创建渲染上下文,得到EGLContext。

EGLContext EGLAPIENTRY eglCreateContext(
        EGLDisplay display, 
        EGLConfig config,
        EGLContext share_context,
        const EGLint *attribList);

  6、绑定上下文:通过eglMakeCurrent函数将EGLSurface、EGLContext、EGLDisplay三者绑定,接下来就可以使用OpenGL进行绘制了。

EGLBoolean EGLAPIENTRY eglMakeCurrent(
        EGLDisplay display,                     
        EGLSurface draw,
        EGLSurface read, EGLContext context);

  7、交换缓冲:当用OpenGL绘制结束后,调用eglSwapBuffers函数交换前后缓冲,将绘制内容显示到屏幕上。

  下面搭建EGL环境的完整代码。EglContextInit函数是在OnSurfaceCreated函数中调用的。

bool EglCore::EglContextInit(void *window, int width, int height) {
    this->width = width;
    this->height = height;
    // 获取EGLDisplay对象:调用eglGetDisplay函数得到EGLDisplay,并加载OpenGL ES库。
    eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (eglDisplay == EGL_NO_DISPLAY) {
        LOGE("eGLDisplay获取失败");
        return false;
    }
    EGLint major;
    EGLint minor;
    // 初始化EGL连接:调用eglInitialize函数初始化,获取库的版本号。
    if (!eglInitialize(eglDisplay, &major, &minor)) {
        LOGE("eGLDisplay初始化失败");
        return false;
    }
    const EGLint maxConfigSize = 1;
    EGLint numConfigs;
    // 确定渲染表面的配置信息:调用eglChooseConfig函数得到EGLConfig。
    if (!eglChooseConfig(eglDisplay, ATTRIB_LIST, &eglConfig, maxConfigSize, &numConfigs)) {
       LOGE("eglConfig初始化失败");
        return false; 
    }
    eglWindow = reinterpret_cast<EGLNativeWindowType>(window);
    // 创建渲染表面:通过EGLDisplay和EGLConfig,调用eglCreateWindowSurface函数创建渲染表面,得到EGLSurface。
    eglSurface = eglCreateWindowSurface(eglDisplay, eglConfig, eglWindow, nullptr);
    if (nullptr == eglSurface) {
        LOGE("创建eGLSurface失败");
        return false;
    }
    // 创建渲染上下文:通过EGLDisplay和EGLConfig,调用eglCreateContext函数创建渲染上下文,得到EGLContext。
    eglContext = eglCreateContext(eglDisplay, eglConfig, EGL_NO_CONTEXT, CONTEXT_ATTRIBS);
    if (nullptr == eglContext) {
        LOGE("创建eglContext失败");
        return false;
    }
    // 绑定上下文:通过eglMakeCurrent函数将EGLSurface、EGLContext、EGLDisplay三者绑定,接下来就可以使用OpenGL进行绘制了。
    if (!eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
        LOGE("eglMakeCurrent失败");
        return false;
    }
    return true;
}

五、OpenGL ES坐标系

  OpenGL ES两个重要坐标系分别是标准坐标系和屏幕坐标系。

5、1标准坐标系

  • 屏幕中心是原点。
  • 横纵坐标的范围在-1到1之间。


    标准坐标系

5、2屏幕坐标系

  • 屏幕左上角是原点。
  • 单位是像素。
  • OpenGL会把标准坐标系转换成屏幕坐标系。


    屏幕坐标系

5、3标准坐标系转换成屏幕坐标系

  • 将标准坐标系的顶点加1,标准坐标系的顶点范围是-1到1之间,加1后,顶点范围就是0到2之间。
  • 每个顶点乘以屏幕的宽高。
  • 上面两步可以通过变换矩阵完成。

六、着色器

  着色器是运行在GPU上的小程序,分为顶点着色器和片元着色器。着色器允许开发者自定义渲染过程,提高了灵活度。可以充分利用GPU的并行计算能力,提高渲染速度。可以通过变成编程,灵活的控制GPU。

6、1顶点着色器

  顶点着色器用于处理几何图形的顶点,对应标准坐标系,标准坐标系的原点是屏幕中心。

6、2片元着色器

  片元着色器用于处理像素颜色和纹理,为每个像素设置不同的颜色,对应屏幕坐标系,屏幕坐标系的左上角为原点。

6、3着色器语言GLSL

  OpenGL 2.0加入了可编程渲染管线,可以更加灵活的控制渲染。但也因此需要学习多一门针对GPU的编程语言,语法与C语言类似,名为GLSL。

6、3、1顶点着色器语言

  下面是顶点着色器的代码,我们可以看到熟悉的主函数,在主函数中,将变量vPosition赋值给gl_Position。其含义是将图形的顶点坐标vPosition赋值给OpenGL的内建变量gl_Position,这样确定了几何图形的顶点坐标。vPosition被attribute修饰,attribute用在顶点着色器中,相当于Java/C的局部变量,一般用来传递的是顶点坐标和纹理坐标。uniform修饰的是统一变量,相当于Java/C的全局变量,传的通常是矩阵。varying是由顶点着色器传递到片元着色器的。

attribute vec4 vPosition;
void main() {
  // 将图形的顶点坐标vPosition赋值给OpenGL的内建变量gl_Position,确定几何图形的顶点坐标。
  gl_Position = vPosition;
} 

  下面的顶点坐标就是下图所对应的坐标,这就是一个三角形的坐标。再次强调下,下面的顶点坐标对应的是标准坐标系,标准坐标系的原点是屏幕中心。这个顶点坐标会传给顶点着色器的vPosition变量,vPosition变量再把顶点坐标传给内建变量gl_Position,这样就确定了几何图形的顶点。

const GLfloat rectangleVertices[] = {
        0.0f, 0.5f, 0.0f,
        -0.5f, -0.5f, 0.0f,
        0.5f, -0.5f, 0.0f
    };
三角形
6、3、2片元着色器语言

  下面是片元着色器的代码,片元装饰器用于设置颜色。第一行代码指定精度,精度分别有lowp低精度,mediump中精度,highp高精度。代码第二行定义vColor变量,它是uniform类型,vec4表示有4个元素的向量,颜色是由ARGB组成,需要4个元素。gl_FragColor是内建变量,gl_FragColor用于确定每个像素的颜色,这里将vColor赋值给gl_FragColor,vColor变量将会通过程序传递过来。

precision mediump float;
uniform vec4 vColor;
void main() {
  // 将vColor赋值给内建变量gl_FragColor,确定几何图形的颜色
  gl_FragColor = vColor;
} 

  下面的代码定义了一个有4个元素的数组,这四个元素分别代表颜色RGBA,也就是红绿蓝和透明度。这个数组就会传递给片元着色器的vColor变量,vColor变量再把颜色值传给内建变量gl_FragColor,这样就确定几何图形的颜色。

// 颜色值#7E8FFB
const GLfloat DRAW_COLOR[] = { 126.0f / 255, 143.0f / 255, 251.0f / 255, 1.0f };

  更多的着色器语言介绍,可以查看文章着色器语言

七、编译和链接着色器

  • 调用glCreateShader创建着色器。
  • 调用glShaderSource绑定源码。
  • 调用glCompileShader编译着色器。
  • 调用glCreateProgram创建程序。
  • 调用glAttachShader绑定着色器。
  • 调用glLinkProgram链接程序。
/**
 * 创建程序
 * 
 * @param vertexShader 顶点着色器源码
 * @param fragmentShader 片元着色器源码
 * @return
 */
GLuint EglCore::createProgram(const char* vertexShader, const char* fragmentShader) {
    GLuint vertex = loadShader(GL_VERTEX_SHADER, vertexShader);
    if (vertex == PROGRAM_ERROR) {
        LOGE("编译顶点着色器失败");
        return PROGRAM_ERROR;
    }
    GLuint fragment = loadShader(GL_FRAGMENT_SHADER, fragmentShader);
    if (fragment == PROGRAM_ERROR) {
        LOGE("编译片元着色器失败");
        return PROGRAM_ERROR;
    }
    GLuint program = glCreateProgram();
    if (program == PROGRAM_ERROR) {
        LOGE("创建程序失败");
        return PROGRAM_ERROR;
    }
    // 绑定着色器
    glAttachShader(program, vertex);
    glAttachShader(program, fragment);
    // 链接程序
    glLinkProgram(program);
    GLint link;
    glGetProgramiv(program, GL_LINK_STATUS, &link);
    if (PROGRAM_ERROR != link) {
        glDeleteShader(vertex);
        glDeleteShader(fragment);
        // 链接程序成功
        return program;
    }
    // 链接程序失败
    glDeleteShader(vertex);
    glDeleteShader(fragment);
    glDeleteProgram(program);
    return PROGRAM_ERROR;
}
    
GLuint EglCore::loadShader(GLenum type, const char* shaderSrc) {
    // 创建着色器
    GLuint shader = glCreateShader(type);
    // 绑定做得起源码
    glShaderSource(shader, 1, &shaderSrc, nullptr);
    // 编译着色器
    glCompileShader(shader);
    GLint compiled;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    if (0 != compiled) {
        return shader;
    }
    glDeleteShader(shader);
    return PROGRAM_ERROR;
}

八、绘制三角形

8、1 清屏,设置屏幕颜色

  • 调用glViewport设置窗口大小
  • 调用glClearColor清屏,将屏幕颜色设置为黑色。
  • 调用glClear清除颜色缓冲
void EglCore::prepareDraw() {
    // 设置窗口大小
    glViewport(DEFAULT_X_POSITION, DEFAULT_X_POSITION, width, height);
    // 清屏,将屏幕颜色设置为黑色
    glClearColor(GL_RED_DEFAULT, GL_GREEN_DEFAULT, GL_BLUE_DEFAULT, GL_ALPHA_DEFAULT);
    // 清除颜色缓冲
    glClear(GL_COLOR_BUFFER_BIT);
}

8、2 绘制

  • 调用glUseProgram函数使用程序。
  • 调用glGetAttribLocation函数获取顶点着色器中定义的属性。
  • 调用glEnableVertexAttribArray函数启用顶点数组。
  • 调用glVertexAttribPointer函数向顶点着色器传递顶点数组。
  • 调用glGetUniformLocation函数获取片元着色器中定义的变量。
  • 调用glUniform4fv函数向片元着色器传递颜色。
  • 调用glDrawArrays函数绘制三角形。
  • 调用glDisableVertexAttribArray函数释放属性变量。
  • 调用eglSwapBuffers函数交换前后缓冲,将绘制内容显示到屏幕上。
void EglCore::draw() {
    prepareDraw();
    // 使用程序
    glUseProgram(program);
    // 获取顶点着色器中定义的属性
    LOGD("绘制");
    GLint positionHandler = glGetAttribLocation(program, POSITION_NAME);
    // 启用顶点数组
    glEnableVertexAttribArray(positionHandler);
    /*
     * 向顶点着色器传递顶点数组
     * 第一个参数是属性变量的下标
     * 第二个参数是顶点坐标的个数,我们在定义顶点坐标的时候,使用了空间坐标系,每个坐标使用x,y,z,所以第二个参数为3
     * 第三个参数是数据的类型
     * 第四个参数是否进行了归一化处理,这里写false
     * 第五个参数是跨度,这里是0,没有跨度
     * 第六个参数是要传递的顶点数据
     */
    glVertexAttribPointer(positionHandler, 3, GL_FLOAT, false, 0, rectangleVertices);
    // 获取片元着色器中定义的变量
    GLint colorHandler = glGetUniformLocation(program, COLOR_NAME);
    /*
     * 向片元着色器传递颜色
     * 第一个参数是变量的下标
     * 第二个参数是数据的数量,由于将所有的像素都设置成一样的颜色,所以第二个参数是1
     * 第三个参数是颜色
     */
    glUniform4fv(colorHandler, 1, DRAW_COLOR);
    // 绘制三角形
    GLsizei count = sizeof(rectangleVertices) / sizeof(rectangleVertices[0]) / 3;
    /*
     * 绘制三角形
     * 第一个参数是绘制的图形
     * 第二个参数是从哪里开始读取,这里从0开始读取
     * 第三个参数是顶点的数量
     */
    glDrawArrays(GL_TRIANGLES, 0, count);
    // 释放属性变量
    glDisableVertexAttribArray(positionHandler);
    finishDraw();
}

bool EglCore::finishDraw() {
    glFlush();
    glFinish();
    // 交换前后缓冲,将绘制内容显示到屏幕上
    return eglSwapBuffers(eglDisplay, eglSurface);
}

  看下绘制出来的结果吧。


绘制的三角形

九、总结

  真是不容易呀,如果大家能坚持看到文章末尾,希望大家能够熟悉OpenGL绘制流程。我们用大量的代码和大量的篇幅来介绍使用OpenGL绘制三角形。可能有人会问,使用Canvas可以轻轻松松的绘制出三角形,为什么要用大量的代码和大量的篇幅来介绍使用OpenGL绘制一个简简单单的三角形?
  Canvas绘制其实上使用的是CPU渲染。大部分情况下,CPU渲染性能不差。但是视频渲染、视频编辑、视频特效、游戏引擎等使用CPU渲染,性能非常差,GPU渲染会大大提高渲染速度,OpenGL就是用来操作GPU的。
  虽然OpenGL代码量大,但是大部分的代码是不变的,只需要写一次。自定义XComponent,注册XComponent回调,搭建EGL环境,编译和链接着色器等等,这些代码都是不变的。最重要的还是顶点着色器和片元着色器,只要OpenGL框架代码搭建起来了,大部分情况下,我们还是在写下面的代码。

attribute vec4 vPosition;
void main() {
  // 将图形的顶点坐标vPosition赋值给OpenGL的内建变量gl_Position,确定几何图形的顶点坐标。
  gl_Position = vPosition;
} 

precision mediump float;
uniform vec4 vColor;
void main() {
  // 将vColor赋值给内建变量gl_FragColor,确定几何图形的颜色
  gl_FragColor = vColor;
} 

  后续我会持续的分享OpenGL。
  源码

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

推荐阅读更多精彩内容