OpenGL 入门到放弃3-- 用openGL展示相机预览 项目优化

上一章我们初步实现了 用GLSurfaceView展示相机预览功能,为了方便大家理解,主要代码都写在了一个类里,这一章我们来优化一下代码和项目结构,为以后的拓展实现各种效果打下基础。

Render 和 View类基本不需要改变,主要来提取一下Filter类中的方法。
我们先来回忆一下 ScreenFilter:

import android.content.Context;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;

import com.xopengl.xopenglcamera.R;
import com.xopengl.xopenglcamera.camera.util.OpenGLUtils;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

/**
 * @author Lixingxing
 */
public class ScreenFilter {
    protected int mProgram;
    protected final int vPosition,vCoord,vMatrix,vTexture;
    protected int mWidth, mHeight;
    protected FloatBuffer vPostionBuffer;
    protected float[] POSITION = new float[]{-1f,-1f,
            1f,-1f,
            -1f,1f,
            1f,1f};

    protected FloatBuffer vCoordBuffer;
    protected float[] TEXTURE = new float[]{1f,0f,
            1f,1f,
            0f,0f,
            0f,1f};

    public ScreenFilter(Context context){
        //1.生成顶点着色器并编译顶点着色器代码
        String vertexSource = OpenGLUtils.readRawFileContent(context, R.raw.screen_vert);
        // 1.1 生成顶点着色器id
        int vShaderVextId = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
        // 1.2 绑定代码到着色器中
        GLES20.glShaderSource(vShaderVextId, vertexSource);
        // 1.3 编译着色器代码
        GLES20.glCompileShader(vShaderVextId);
        // 1.4 主动获取成功 失败状态
        int[] status = new int[1];
        GLES20.glGetShaderiv(vShaderVextId, GLES20.GL_COMPILE_STATUS, status, 0);
        if (status[0] != GLES20.GL_TRUE) {
            // 如果没有成功,抛出异常 如果不做处理,log会输出一条GLERROR的日志
            throw new IllegalStateException(" 顶点着色器配置失败");
        }

        // 2.创建片元着色器并编译顶点着色器代码
        // 2.0 获取片元着色器代码
        String fragSource = OpenGLUtils.readRawFileContent(context, R.raw.screen_frag);
        // 2.1 生成片元着色器id
        int vShaderFragId = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
        // 2.2 绑定代码到着色器中
        GLES20.glShaderSource(vShaderFragId, fragSource);
        // 2.3 编译着色器代码
        GLES20.glCompileShader(vShaderFragId);
        // 2.4 主动获取成功 失败状态
        GLES20.glGetShaderiv(vShaderFragId, GLES20.GL_COMPILE_STATUS, status, 0);
        if (status[0] != GLES20.GL_TRUE) {
            // 如果没有成功,抛出异常 如果不做处理,log会输出一条GLERROR的日志
            throw new IllegalStateException(" 片元着色器配置失败");
        }

        // 3.创建着色器程序并链接着色器
        mProgram = GLES20.glCreateProgram();
        // 把着色器塞入 程序当中
        GLES20.glAttachShader(mProgram, vShaderVextId);
        GLES20.glAttachShader(mProgram, vShaderFragId);
        // 链接着色器
        GLES20.glLinkProgram(mProgram);
        // 主动获取成功 失败状态
        GLES20.glGetProgramiv(mProgram, GLES20.GL_LINK_STATUS, status, 0);
        if (status[0] != GLES20.GL_TRUE) {
            // 如果没有成功,抛出异常 如果不做处理,log会输出一条GLERROR的日志
            throw new IllegalStateException(" 创建着色器程序失败");
        }

        // 4. 释放资源
        GLES20.glDeleteShader(vShaderVextId);
        GLES20.glDeleteShader(vShaderFragId);

        //5. 获得着色器中的参数变量的索引,后面通过这个索引给这个变量赋值,索引都是int类型的
        vPosition = GLES20.glGetAttribLocation(mProgram,"vPosition");
        vCoord = GLES20.glGetAttribLocation(mProgram,"vCoord");
        vMatrix = GLES20.glGetUniformLocation(mProgram,"vMatrix");
        vTexture = GLES20.glGetUniformLocation(mProgram,"vTexture");

        //6. 创建顶点坐标和纹理坐标
        // 顶点坐标
        vPostionBuffer = ByteBuffer.allocateDirect(4*2*4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        vPostionBuffer.clear();
        vPostionBuffer.put(POSITION);
        // 纹理坐标
        vCoordBuffer = ByteBuffer.allocateDirect(4*2*4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        vCoordBuffer.clear();
        vCoordBuffer.put(TEXTURE);
    }

    public void onReady(int width,int height){
        this.mWidth = width;
        this.mHeight = height;
    }

    public void onDrawFrame(int mTexture, float[] mtx) {
        // 1.设置窗口大小
        GLES20.glViewport(0,0, mWidth,mHeight);
        // 2.使用着色器程序
        GLES20.glUseProgram(mProgram);
        // 3.给着色器程序中传值
        // 3.1 给顶点坐标数据传值
        vPostionBuffer.position(0);
        GLES20.glVertexAttribPointer(vPosition,2,GLES20.GL_FLOAT,false,0,vPostionBuffer);
        // 激活
        GLES20.glEnableVertexAttribArray(vPosition);
        // 3.2 给纹理坐标数据传值
        vCoordBuffer.position(0);
        GLES20.glVertexAttribPointer(vCoord,2,GLES20.GL_FLOAT,false,0,vCoordBuffer);
        GLES20.glEnableVertexAttribArray(vCoord);

        // 3.3 变化矩阵传值
        GLES20.glUniformMatrix4fv(vMatrix,1,false,mtx,0);

        // 3.4 给片元着色器中的 采样器绑定
        // 激活图层
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        // 图像数据
        GLES20.glBindTexture(GLES11Ext.GL_SAMPLER_EXTERNAL_OES,mTexture);
        // 传递参数
        GLES20.glUniform1i(vTexture,0);

        //参数传递完毕,通知 opengl开始画画
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);

        // 解绑
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);

    }
}

1. 新建一个抽象类, BaseFilter

import android.content.Context;

/**
 * @author Lixingxing
 */
public abstract class BaseFilter {
    protected float[] mtx = new float[16];
    // 初始化
    public BaseFilter(Context context,int vertRawId,int fragRawId){
        
    }
    
    // 设置窗口大小
    public void onReady(int width,int height){
        
    }
    
    public void setMatrix(float[] mtx){
        this.mtx = mtx;
    }
    // 绘制方法(绘制方法做了一下改变,不再直接传入矩阵数据,而是先设置矩阵数据,绘制的时候直接使用设置的矩阵数据。并且会返回纹理的id,为什么这样做我们接下来的内容里会说到)
    public  int onDrawFrame(int textureId){

   }
}

2. 将 ScreenFilter中的参数都提取到 BaseFilter中,设置成 protected,这样每个子类都可以使用。

protected int mProgram;
protected int vPosition,vCoord,vMatrix,vTexture;
protected int mWidth, mHeight;
protected FloatBuffer vPostionBuffer;
protected float[] POSITION = new float[]{-1f,-1f,
        1f,-1f,
        -1f,1f,
        1f,1f};

protected FloatBuffer vCoordBuffer;
protected float[] TEXTURE = new float[]{1f,0f,
        1f,1f,
        0f,0f,
        0f,1f};

3.将创建着色器程序的代码 提取到 OpenGLUtils中,并在BaseFilter初始化方法中调用

将顶点着色器代码文件id 和 片元着色器代码文件id 动态传入,这样我们就可以调用这个方法生成不同的着色器程序。

public static int createProgram(Context context, int vertRawId,int vertFragId){
        //1.生成顶点着色器并编译顶点着色器代码
        String vertexSource = OpenGLUtils.readRawFileContent(context,vertRawId);
        // 1.1 生成顶点着色器id
        int vShaderVextId = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
        // 1.2 绑定代码到着色器中
        GLES20.glShaderSource(vShaderVextId, vertexSource);
        // 1.3 编译着色器代码
        GLES20.glCompileShader(vShaderVextId);
        // 1.4 主动获取成功 失败状态
        int[] status = new int[1];
        GLES20.glGetShaderiv(vShaderVextId, GLES20.GL_COMPILE_STATUS, status, 0);
        if (status[0] != GLES20.GL_TRUE) {
            // 如果没有成功,抛出异常 如果不做处理,log会输出一条GLERROR的日志
            throw new IllegalStateException(" 顶点着色器配置失败");
        }

        // 2.创建片元着色器并编译顶点着色器代码
        // 2.0 获取片元着色器代码
        String fragSource = OpenGLUtils.readRawFileContent(context, vertFragId);
        // 2.1 生成片元着色器id
        int vShaderFragId = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
        // 2.2 绑定代码到着色器中
        GLES20.glShaderSource(vShaderFragId, fragSource);
        // 2.3 编译着色器代码
        GLES20.glCompileShader(vShaderFragId);
        // 2.4 主动获取成功 失败状态
        GLES20.glGetShaderiv(vShaderFragId, GLES20.GL_COMPILE_STATUS, status, 0);
        if (status[0] != GLES20.GL_TRUE) {
            // 如果没有成功,抛出异常 如果不做处理,log会输出一条GLERROR的日志
            throw new IllegalStateException(" 片元着色器配置失败");
        }

        // 3.创建着色器程序并链接着色器
        int mProgram = GLES20.glCreateProgram();
        // 把着色器塞入 程序当中
        GLES20.glAttachShader(mProgram, vShaderVextId);
        GLES20.glAttachShader(mProgram, vShaderFragId);
        // 链接着色器
        GLES20.glLinkProgram(mProgram);
        // 主动获取成功 失败状态
        GLES20.glGetProgramiv(mProgram, GLES20.GL_LINK_STATUS, status, 0);
        if (status[0] != GLES20.GL_TRUE) {
            // 如果没有成功,抛出异常 如果不做处理,log会输出一条GLERROR的日志
            throw new IllegalStateException(" 创建着色器程序失败");
        }

        // 4. 释放资源
        GLES20.glDeleteShader(vShaderVextId);
        GLES20.glDeleteShader(vShaderFragId);

        return mProgram;
    }

4. 将 BaseFilter的初始化方法设置成需要传入 上下文+顶点着色器文件id+片元着色器id,并分割成两个方法,每个方法里给出默认的实现,子类可以根据自己的需求再进行重写。

在BaseFilter中创建着色器程序,着色器代码文件id由具体实现的子类传入
    protected int mProgram;
    protected int vPosition,vCoord,vMatrix,vTexture;
    protected int mWidth, mHeight;
    protected FloatBuffer vPostionBuffer;
    protected float[] POSITION = new float[]{-1f,-1f,
            1f,-1f,
            -1f,1f,
            1f,1f};

    protected FloatBuffer vCoordBuffer;
   // 已经转过了坐标,所以这里不需要再变了。
    protected float[] TEXTURE = new float[]{1f,0f,
            1f,1f,
            0f,0f,
            0f,1f};
    ...
    public BaseFilter(Context context,int vertRawId,int fragRawId){
         initilize(context,vertRawId,fragRawId);
         initCoord();
    }
    protected void initilize(Context context, int mVershaderId, int mFragShaderId){
        mProgram = OpenGLUtils.createProgram(context,mVershaderId,mFragShaderId);
        // 获得着色器中的变量的索引,通过这个索引给这个变量赋值
        vPosition = GLES20.glGetAttribLocation(mProgram,"vPosition");
        vCoord = GLES20.glGetAttribLocation(mProgram,"vCoord");
        vMatrix = GLES20.glGetUniformLocation(mProgram,"vMatrix");
        vTexture = GLES20.glGetUniformLocation(mProgram,"vTexture");
    }

    protected void initCoord(){
        // 顶点坐标
        vPostionBuffer = ByteBuffer.allocateDirect(4*2*4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        vPostionBuffer.clear();
        vPostionBuffer.put(POSITION);
        // 纹理坐标
        vCoordBuffer = ByteBuffer.allocateDirect(4*2*4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        vCoordBuffer.clear();
        vCoordBuffer.put(TEXTURE);
    }
   ...

5. 将绘制方法提取到 BaseFilter中

 // 绘制方法
    public int onDrawFrame(int textureId){
        // 1.设置窗口大小
        GLES20.glViewport(0,0, mWidth,mHeight);
        // 2.使用着色器程序
        GLES20.glUseProgram(mProgram);
        // 3.给着色器程序中传值
        // 3.1 给顶点坐标数据传值
        vPostionBuffer.position(0);
        GLES20.glVertexAttribPointer(vPosition,2,GLES20.GL_FLOAT,false,0,vPostionBuffer);
        // 激活
        GLES20.glEnableVertexAttribArray(vPosition);
        // 3.2 给纹理坐标数据传值
        vCoordBuffer.position(0);
        GLES20.glVertexAttribPointer(vCoord,2,GLES20.GL_FLOAT,false,0,vCoordBuffer);
        GLES20.glEnableVertexAttribArray(vCoord);

        // 3.3 变化矩阵传值
        GLES20.glUniformMatrix4fv(vMatrix,1,false,mtx,0);

        // 3.4 给片元着色器中的 采样器绑定
        // 激活图层
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        // 图像数据
        GLES20.glBindTexture(GLES11Ext.GL_SAMPLER_EXTERNAL_OES,textureId);
        // 传递参数
        GLES20.glUniform1i(vTexture,0);

        //参数传递完毕,通知 opengl开始画画
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);

        // 解绑
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);

        return textureId;
    }

自此,整个BaseFilter就已经优化完成,贴上全篇代码:

import android.content.Context;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;

import com.xopengl.xopenglcamera.camera.util.OpenGLUtils;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

/**
 * @author Lixingxing
 */
public abstract class BaseFilter {
    protected int mProgram;
    protected int vPosition,vCoord,vMatrix,vTexture;
    protected int mWidth, mHeight;
    protected FloatBuffer vPostionBuffer;
    protected float[] POSITION = new float[]{-1f,-1f,
            1f,-1f,
            -1f,1f,
            1f,1f};

    protected FloatBuffer vCoordBuffer;
    protected float[] TEXTURE = new float[]{1f,0f,
            1f,1f,
            0f,0f,
            0f,1f};
    protected float[] mtx = new float[16];

    // 初始化
    public BaseFilter(Context context,int vertRawId,int fragRawId){
        initilize(context,vertRawId,fragRawId);
        initCoord();
    }

    protected void initilize(Context context, int mVershaderId, int mFragShaderId){
        mProgram = OpenGLUtils.createProgram(context,mVershaderId,mFragShaderId);
        // 获得着色器中的变量的索引,通过这个索引给这个变量赋值
        vPosition = GLES20.glGetAttribLocation(mProgram,"vPosition");
        vCoord = GLES20.glGetAttribLocation(mProgram,"vCoord");
        vMatrix = GLES20.glGetUniformLocation(mProgram,"vMatrix");
        vTexture = GLES20.glGetUniformLocation(mProgram,"vTexture");
    }

    protected void initCoord(){
        // 顶点坐标
        vPostionBuffer = ByteBuffer.allocateDirect(4*2*4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        vPostionBuffer.clear();
        vPostionBuffer.put(POSITION);
        // 纹理坐标
        vCoordBuffer = ByteBuffer.allocateDirect(4*2*4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        vCoordBuffer.clear();
        vCoordBuffer.put(TEXTURE);
    }

    // 设置窗口大小
    public void onReady(int width,int height){
        this.mWidth = width;
        this.mHeight = height;
    }

    public void setMatrix(float[] mtx){
        this.mtx = mtx;
    }

    // 绘制方法
    public int onDrawFrame(int textureId){
        // 1.设置窗口大小
        GLES20.glViewport(0,0, mWidth,mHeight);
        // 2.使用着色器程序
        GLES20.glUseProgram(mProgram);
        // 3.给着色器程序中传值
        // 3.1 给顶点坐标数据传值
        vPostionBuffer.position(0);
        GLES20.glVertexAttribPointer(vPosition,2,GLES20.GL_FLOAT,false,0,vPostionBuffer);
        // 激活
        GLES20.glEnableVertexAttribArray(vPosition);
        // 3.2 给纹理坐标数据传值
        vCoordBuffer.position(0);
        GLES20.glVertexAttribPointer(vCoord,2,GLES20.GL_FLOAT,false,0,vCoordBuffer);
        GLES20.glEnableVertexAttribArray(vCoord);

        // 3.3 变化矩阵传值
        GLES20.glUniformMatrix4fv(vMatrix,1,false,mtx,0);

        // 3.4 给片元着色器中的 采样器绑定
        // 激活图层
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        // 图像数据
        GLES20.glBindTexture(GLES11Ext.GL_SAMPLER_EXTERNAL_OES,textureId);
        // 传递参数
        GLES20.glUniform1i(vTexture,0);

        //参数传递完毕,通知 opengl开始画画
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);

        // 解绑
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);

        return textureId;
    }
}

6. 修改一下之前的 ScreenFilter 和 Render中的调用

import android.content.Context;
import com.xopengl.xopenglcamera.R;

/**
 * @author Lixingxing
 */
public class ScreenFilter extends BaseFilter{
    public ScreenFilter(Context context) {
        super(context, R.raw.screen_vert,R.raw.screen_frag);
    }
}

是不是很简洁。

再修改一下render中的调用方式:

import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;

import com.xopengl.xopenglcamera.camera.filter.BaseFilter;
import com.xopengl.xopenglcamera.camera.filter.ScreenFilter;
import com.xopengl.xopenglcamera.camera.util.CameraHelper;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

/**
 * @author Lixingxing
 */
public class MyCameraRenderer implements GLSurfaceView.Renderer {
    private GLSurfaceView glSurfaceView;

    private CameraHelper cameraHelper;

    private SurfaceTexture surfaceTexture;
    private int[] mTextures;
    // 这里用 BaseFilter
    private BaseFilter baseFilter;
    private float[] mtx = new float[16];

    public MyCameraRenderer(GLSurfaceView glSurfaceView){
        this.glSurfaceView = glSurfaceView;
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        // 打开前置摄像头
        cameraHelper = new CameraHelper(Camera.CameraInfo.CAMERA_FACING_FRONT);
        mTextures = new int[1];
        GLES20.glGenTextures(mTextures.length,mTextures,0);
        surfaceTexture = new SurfaceTexture(mTextures[0]);
        surfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
            @Override
            public void onFrameAvailable(SurfaceTexture surfaceTexture) {
                glSurfaceView.requestRender();
            }
        });
        // 生成相机数据处理类
        baseFilter = new ScreenFilter(glSurfaceView.getContext());
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        // 开启预览
        cameraHelper.WIDTH = width;
        cameraHelper.HEIGHT = height;
        cameraHelper.startPreview(surfaceTexture);

        baseFilter.onReady(width,height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        // 1. 清理屏幕 设置屏幕颜色为 glClearColor中设置的颜色
        GLES20.glClearColor(0,0,0,0);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

        // 2. 把摄像头数据从 SurfaceTexture 中取出来
        //   2.1 更新纹理,然后才能使用openGl从SurfaceTexture中获取数据
        surfaceTexture.updateTexImage();
        //   2.2 取得变换矩阵
        //     SurfaceTexture 在opengl中使用的是特殊的采样器“samplerExternalOES”,必须要通过变换矩阵才能获得 Simple2D的采样器
        //     mtx 代表一个4*4的矩阵数据,所以要用  float[] mtx = new float[16]来声明
        surfaceTexture.getTransformMatrix(mtx);

        // 3.把数据绘制到屏幕上显示
        baseFilter.setMatrix(mtx);
        baseFilter.onDrawFrame(mTextures[0]);
    }

    public void surfaceDestroyed(){
        cameraHelper.stopPreview();
    }
}

这样优化了以后,如果我们需要用不同的filter实现不同的效果(比如镜像翻转等),在BaseFilter具体的子类中做一些坐标或者绘制工作的改动,,然后在这里 new 不同的filter出来就行了。

比如 实现 画面翻转的效果:
只需要实现一个 FlipFilter。

import com.xopengl.xopenglcamera.R;

/**
 * 翻转
 * @author Lixingxing
 */
public class FlipFilter extends BaseFilter{
    public FlipFilter(Context context) {
        super(context, R.raw.screen_vert, R.raw.screen_frag);
    }

    @Override
    protected void initCoord() {
        TEXTURE = new float[]{ 0f,0f,
                0f,1f,
                1f,0f,
                1f,1f,
               };
        super.initCoord();
    }
}

然后在 MyCameraRenderer中把之前的 ScreenFilter 替换成 FlipFilter就行

        // 生成相机数据处理类
//        baseFilter = new ScreenFilter(glSurfaceView.getContext());
        baseFilter = new FlipFilter(glSurfaceView.getContext());

联想到美颜相机里实现的各种效果,是不是突然就有了思路?

之后我们还会在优化后的项目基础上继续进行拓展。
github demo地址

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

推荐阅读更多精彩内容