OpenGL关键API

OpenGL 的基本形状是三角形,无论是绘制形状还是填充,都是对于图形进行操作

对于一个平面图形,绘制的结果是有正反面的,

着色器语言(GLSL)主要包括两部分:Vertex shader(定点着色器,负责定点位置与坐标变换,即决定显示哪个部分,以何种位置/姿态显示),Fragment shader(片元着色器,负责纹理的填充与转换,即决定显示成什么样子)

OpenGL ES的屏幕坐标系
OpenGL ES是一个三维的图形库,但是三维的图像要在二维平面显示,就要经过一定的投影变换,将三维的空间以一定的方式显示在二维屏幕上。

image
image

上图显示的是OpenGL ES的屏幕坐标系,无论是X还是Y轴,取值范围都是[-1,1],也就是说,即便你的手机是16:9的屏幕,对于OpenGL ES来说也是一个正方形的绘制范围(是不是很奇怪)

初始化OpenGL ES环境

OpenGL ES的使用,一般包括如下几个步骤:

  1. EGL Context初始化
  2. OpenGL ES初始化
  3. OpenGL ES设置选项与绘制
  4. OpenGL ES资源释放(可选)
  5. EGL资源释放

Android平台提供了一个GLSurfaceView,来帮助使用者完成第一步和第五步,由于释放EGL资源时会自动释放之前申请的OpenGL ES资源,所以需要我们自己做的就只有2和3。

使用GLSurfaceView
我们在主布局中引入一个GLSurfaceView,并让他充满整个布局,并在Activity中获取他的实例

<android.opengl.GLSurfaceView
        android:id="@+id/surface_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
public class MainActivity extends AppCompatActivity {

    private GLSurfaceView glSurfaceView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        glSurfaceView= (GLSurfaceView) findViewById(R.id.surface_view);
    }
}

获取实例以后,我们就可以对于这个GLSurfaceView进行配置:

glSurfaceView.setEGLContextClientVersion(2);//设置EGL上下文的客户端版本,因为我们使用的是OpenGL ES 2.0,所以设置为2

glSurfaceView.setRenderer(new GLRenderer());

glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);//代表渲染模式,选项有两种(RENDERMODE_WHEN_DIRTY,RENDERMODE_CONTINUOUSLY),一个是需要渲染(触控事件,渲染请求)才渲染,一个是不断渲染。
GLSurfaceView.Renderer接口

GLRenderer是本文中的关键类,实现了GLSurfaceView.Renderer这个接口,用来完成绘制操作。现在我们来看看这个类的定义:

public class GLRenderer implements GLSurfaceView.Renderer {
    //这个函数在Surface被创建的时候调用,每次我们将应用切换到其他地方,再切换回来的时候都有可能被调用,在这个函数中,我们需要完成一些OpenGL ES相关变量的初始化
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {

    }
    //每当屏幕尺寸发生变化时,这个函数会被调用(包括刚打开时以及横屏、竖屏切换),width和height就是绘制区域的宽和高(上图黑色区域)
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {

    }
    //每一次绘制时这个函数都会被调用,之前设置了GLSurfaceView.RENDERMODE_CONTINUOUSLY,也就是说按照正常的速度,每秒这个函数会被调用60次,虽然我们还什么都没做
    @Override
    public void onDrawFrame(GL10 gl) {

    }
}
private Context context;

public GLRenderer(Context context) {
    this.context = context;
}

glSurfaceView.setRenderer(new GLRenderer(this));
//用来读取raw中的文本文件,并且以String的形式返回
public static String readRawTextFile(Context context, int resId) {
    InputStream inputStream = context.getResources().openRawResource(resId);
    try {
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line).append("\n");
        }
        reader.close();
        return sb.toString();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

我们在raw文件夹中创建两个文件,fragment_shader.glsl和vertex_shader.glsl,他们分别是片元着色器和顶点着色器的脚本,之前说的可编程管线,就是指OpenGL ES 2.0可以即时编译这些脚本,来实现丰富的功能,两个文件的内容如下:

vertex_shader.glsl
attribute vec4 aPosition;
void main() {
  gl_Position = aPosition;
}
  • vec4是一个包含4个浮点数(float,我们约定,在OpenGL中提到的浮点数都是指float类型)的向量,
  • attribute表示变元,用来在Java程序和OpenGL间传递经常变化的数据,
  • gl_Position 是OpenGL ES的内建变量,表示顶点坐标(xyzw,w是用来进行投影变换的归一化变量),我们会通过aPosition把要绘制的顶点坐标传递给gl_Position
fragment_shader.glsl
precision mediump float;
void main() {
    gl_FragColor = vec4(0,0.5,0.5,1);
}
  • precision mediump float用来指定运算的精度以提高效率(因为运算量还是蛮大的),
  • gl_FragColor 也是一个内建的变量,表示颜色,以rgba的方式排布,范围是[0,1]的浮点数

先完成onSurfaceCreated的代码,使用readRawTextFile把文件读进来,然后创建一个OpenGL ES程序

String vertexShader = ShaderUtils.readRawTextFile(context, R.raw.vertex_shader);
String fragmentShader= ShaderUtils.readRawTextFile(context, R.raw.fragment_shader);
programId=ShaderUtils.createProgram(vertexShader,fragmentShader);

读取文件应该好理解,创建程序就比较复杂了,具体的步骤是这样的,我们先看创建程序之前要做的事情:

  1. 创建一个新的着色器对象
  2. 上传和编译着色器代码,就是我们之前读进来的String
  3. 读取编译状态(可选)
public static int createProgram(String vertexSource, String fragmentSource) {
    //我们先创建顶点着色器和片元着色器
    int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
    if (vertexShader == 0) {
        return 0;
    }
    int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
    if (pixelShader == 0) {
        return 0;
    }
    //然后用GLES20.glCreateProgram()创建程序,如果创建成功,会返回一个非零的值
    int program = GLES20.glCreateProgram();
    if (program != 0) {
        
        GLES20.glAttachShader(program, vertexShader);//把程序和着色器绑定起来
        checkGlError("glAttachShader");
        GLES20.glAttachShader(program, pixelShader);
        checkGlError("glAttachShader");
        //然后用GLES20.glLinkProgram(program)链接程序(编译链接)
        GLES20.glLinkProgram(program);
        int[] linkStatus = new int[1];
        
        GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);//和之前的类似,是用来获取链接状态的
        
        if (linkStatus[0] != GLES20.GL_TRUE) {
            Log.e(TAG, "Could not link program: ");
            Log.e(TAG, GLES20.glGetProgramInfoLog(program));
            GLES20.glDeleteProgram(program);
            program = 0;
        }
    }
    return program;
}
//shaderType用来指定着色器类型,取值有GLES20.GL_VERTEX_SHADER和GLES20.GL_FRAGMENT_SHADER
//source就是刚才读入的代码
public static int loadShader(int shaderType, String source) {

    //如果创建成功,那么shader会是一个非零的值
    int shader = GLES20.glCreateShader(shaderType);
    if (shader != 0) {
        GLES20.glShaderSource(shader, source);
        GLES20.glCompileShader(shader);
        int[] compiled = new int[1];
        //我们用GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0)来获取编译的状态
        GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
        if (compiled[0] == 0) {
            Log.e(TAG, "Could not compile shader " + shaderType + ":");
            Log.e(TAG, GLES20.glGetShaderInfoLog(shader));
            //如果创建失败,就删除这个着色器
            GLES20.glDeleteShader(shader);
            shader = 0;
        }
    }
    return shader;
}

另外还有一个打印错误日志的功能函数:

public static void checkGlError(String label) {
    int error;
    while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
        Log.e(TAG, label + ": glError " + error);
        throw new RuntimeException(label + ": glError " + error);
    }
}

创建好了程序之后,我们获取之前顶点着色器中,aPosition的引用,以便于传送顶点数据

aPositionHandle= GLES20.glGetAttribLocation(programId,"aPosition");

完成向OpenGL的数据传送OpenGL ES工作在native层(C、C++),如果要传送数据,我们需要使用特殊的方法把数据复制过去。
首先定义一个顶点数组,这是我们要绘制的三角形的三个顶点坐标(逆时针),三个浮点数分别代表xyz,因为是在平面上绘制,我们把z设置为0

private final float[] vertexData = {
        0f,0f,0f,
        1f,-1f,0f,
        1f,1f,0f
};

如果程序正常工作,那么我们的三角形应该出现在这个区域(见下图):

image
image

我们使用一个FloatBuffer将数据传递到本地内存
我们在类的构造函数中把顶点数据传递过去:

//ByteBuffer用来在本地内存分配足够的大小
vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)//申请一个内存,大小是data*4个字节,因为data里是float,一个float是4个字节
        //设置存储顺序为nativeOrder(关于存储顺序的更多资料可以在维基百科上找到)
        .order(ByteOrder.nativeOrder())
        .asFloatBuffer()
        .put(vertexData);//把vertexData放进去
vertexBuffer.position(0);//设定索引位

完成onDrawFrame
完成了上述工作以后,我们就可以画个三角形,试试手

@Override
public void onDrawFrame(GL10 gl) {
    //清空颜色缓冲区和深度缓冲区
    GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT |GLES20.GL_COLOR_BUFFER_BIT);
    GLES20.glUseProgram(programId);//指定使用刚才创建的那个程序
    
    //启用顶点数组,aPositionHandle就是我们传送数据的目标位置
    GLES20.glEnableVertexAttribArray(aPositionHandle);
    
    GLES20.glVertexAttribPointer(aPositionHandle, 3, GLES20.GL_FLOAT, false,
            12, vertexBuffer);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
}
  • GLES20.glVertexAttribPointer的原型是这样的:
glVertexAttribPointer(
        int indx,
        int size,
        int type,
        boolean normalized,
        int stride,
        java.nio.Buffer ptr
)

stride表示步长,因为一个顶点三个坐标,一个坐标是float(4字节),所以步长是12字节
(当然,这个只在一个数组中同时包含多个属性时才有作用,例如同时包含纹理坐标和顶点坐标,在只有一种属性时(例如现在),和传递0是相同效果)

  • 最后,我们用GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);把三角形画出来,glDrawArrays的原型如下
public static native void glDrawArrays(
    int mode,
    int first,
    int count
);
image
image

因为OpenGL会把整个屏幕(其实是整个可以绘制的区域,也就是前面黑色的区域)当成输出,所以我们画出来的三角形出现了变形。那么横屏的情况下是什么样的呢? 来看一下:

image
image

解决变形问题

要解决变形,要先掌握Projection Matrix(投影矩阵)的概念,在OpenGL中,投影矩阵用来改变场景在屏幕上的显示方式(近大远小,平行投影等等)。
因为我们绘制的是二维平面,所以问题还是比较好解决的,假如我们的屏幕是16:9的(横屏情况),那么只要让OpenGL的绘制范围也是16:9的就好了,如下图所示:

image
image

可以看到,我们把OpenGL的绘制区域横向拉长了,拉长的比例就是(16/9 约1.777)
更多关于透视和投影变换
可以参考http://blog.csdn.net/popy007/article/details/1797121

那么代码要如何实现呢?答案是矩阵,进行正交投影的操作,我们并不需要去推导正交矩阵如何求出来,Android 提供的Matrix类中包含这个方法。

我们先声明一个长度16的float数组,这是Matrix的标准尺寸

private final float[] projectionMatrix=new float[16];

注意,在OpenGL中,数组是row-major的,如下所示:

/**
 * Matrix math utilities. These methods operate on OpenGL ES format
 * matrices and vectors stored in float arrays.
 * <p>
 * Matrices are 4 x 4 column-vector matrices stored in column-major
 * order:
 * <pre>
 *  m[offset +  0] m[offset +  4] m[offset +  8] m[offset + 12]
 *  m[offset +  1] m[offset +  5] m[offset +  9] m[offset + 13]
 *  m[offset +  2] m[offset +  6] m[offset + 10] m[offset + 14]
 *  m[offset +  3] m[offset +  7] m[offset + 11] m[offset + 15]</pre>
 */

创建了projectionMatrix以后,我们还需要更新glsl中顶点着色器的代码,以便把这个变换用的矩阵传递过去,如下所示:

attribute vec4 aPosition;
uniform mat4 uMatrix;
void main() {
  gl_Position = uMatrix*aPosition;
}

uniform 是GLSL中的常量类型,之前的attribute类型是用来在Java代码和顶点着色器(Vertex Shader)传递变量用的,uniform则是给顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)传递常量用的。

我们把uMatrix和aPosition做矩阵乘法,就得到了一个新的顶点位置。
类似的,我们也需要一个入口,以便给这个矩阵传递数据

uMatrixHandle=GLES20.glGetUniformLocation(programId,"uMatrix");
完成正交投影
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    float ratio=width>height?(float)width/height:(float)height/width;
    if (width>height){
        Matrix.orthoM(projectionMatrix,0,-ratio,ratio,-1f,1f,-1f,1f);
    }else Matrix.orthoM(projectionMatrix,0,-1f,1f,-ratio,ratio,-1f,1f);
}

在onSurfaceChanged中,我们获取了屏幕的宽和高,所以我们在这里计算缩放的比例,需要注意的是横屏和竖屏的时候是刚好相反的处理(一个改变x,一个改变y)

正交投影方法 : Matrix.orthoM() 方法设置正交投影;

public static void orthoM(float[] m, int mOffset,float left, float right, float bottom, float top,float near, float far)

左右(x)下上(y)近远(z)

更新onDrawFrame

@Override
public void onDrawFrame(GL10 gl) {
    GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
    GLES20.glUseProgram(programId);
    GLES20.glUniformMatrix4fv(uMatrixHandle,1,false,projectionMatrix,0);
    GLES20.glEnableVertexAttribArray(aPositionHandle);
    GLES20.glVertexAttribPointer(aPositionHandle, 3, GLES20.GL_FLOAT, false,
            12, vertexBuffer);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
}

看一下效果吧:

image
image

如果是竖屏的情况,那么应该是这样子的:

image
image

下面我们来显示一张图片,图片可以看做一个矩形,所以我们先来画一个矩形
之前提到OpenGL的基本形状是三角形,一个矩形可以看成由4个三角形构成,如果我们一个一个画,那需要12个顶点,36个坐标,效率不高,所以我们采用另外一种方式——顶点索引与glDrawElements配合使用。
什么是顶点索引呢?顶点索引就是给出顶点的下标而不给出具体的顶点坐标,看代码:

private final float[] vertexData = {
        0f,0f,0f,
        1f,1f,0f,
        -1f,1f,0f,
        -1f,-1f,0f,
        1f,-1f,0f
};

private final short[] indexData = {
        0,1,2,
        0,2,3,
        0,3,4,
        0,4,1
};

我们的绘制区域是(-1,-1)到(1,1)的平面区域,vertexData给出了5个顶点,indexData给出了4个三角形的描述:

声明一个ShortBuffer ,用来存放顶点的索引数据

private ShortBuffer indexBuffer;
indexBuffer = ByteBuffer.allocateDirect(indexData.length * 2)
        .order(ByteOrder.nativeOrder())
        .asShortBuffer()
        .put(indexData);
indexBuffer.position(0);

然后,使用GLES20.glDrawElements把三角形画出来,注意如果我们之前的数组类型是byte,那么就应该使用GLES20.GL_UNSIGNED_BYTE,总之类型要对齐

GLES20.glDrawElements(GLES20.GL_TRIANGLES,indexData.length,GLES20.GL_UNSIGNED_SHORT,indexBuffer);

创建一个纹理

纹理的创建比较复杂,我们创建一个新的工具类,并且加入如下代码:

public class TextureHelper {
    private  static  final String TAG="TextureHelper";
    public static int loadTexture(Context context,int resourceId){

        final int[] textureObjectIds=new int[1];
        GLES20.glGenTextures(1,textureObjectIds,0);
        if (textureObjectIds[0]==0){
            Log.d(TAG,"生成纹理对象失败");
            return 0;
        }

        BitmapFactory.Options options=new BitmapFactory.Options();
        options.inScaled=false;

        Bitmap bitmap=BitmapFactory.decodeResource(context.getResources(),resourceId,options);

        if (bitmap==null){
            Log.d(TAG,"加载位图失败");
            GLES20.glDeleteTextures(1,textureObjectIds,0);
            return 0;
        }

        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureObjectIds[0]);

        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_LINEAR_MIPMAP_LINEAR);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);

        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D,0,bitmap,0);

        bitmap.recycle();

        //为与target相关联的纹理图像生成一组完整的mipmap
        GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);

        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);

        return textureObjectIds[0];
    }
}
  • GLES20.glGenTextures(1,textureObjectIds,0);生成一个纹理,放入textureObjectIds中,同样地,如果生成成功,那么就会返回一个非零值

  • 然后我们将bitmap从raw中读进来,这个就不解释了

  • GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureObjectIds[0]);的作用是将我们刚生成的纹理和OpenGL的2D纹理绑定,告诉OpenGL这是一个2D的纹理(贴图)

  • GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_LINEAR_MIPMAP_LINEAR);
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINE AR);
    这两句话用来设置纹理过滤的方式,GL_TEXTURE_MIN_FILTER是指缩小时的过滤方式,GL_TEXTURE_MAG_FILTER则是放大的
    关于放大和缩小时的可用方式,参见下图:

  • GLUtils.texImage2D(GLES20.GL_TEXTURE_2D,0,bitmap,0);
    然后将纹理加载到OpenGL中,并且及时回收bitmap

  • GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);用于解除和纹理的绑定,等使用时再绑定

OpenGL纹理坐标系

又是一个新的坐标系,

image
image

S-T坐标系(橙色)就是OpenGL纹理坐标系,蓝色的是OpenGL屏幕坐标系,对比一下,明显的区别有:

  • 原点的位置不一样
  • 取值的范围不一样
  • Y轴的方向也刚好和T轴相反
  • 如果我们要将一张图贴到整个显示区域,即(-1,-1,0)-(1,1,0),那么对应的关系就如图中所示
vertex_shader.glsl

attribute vec4 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
uniform mat4 uMatrix;
void main() {
    vTexCoord=aTexCoord;
    gl_Position = uMatrix*aPosition;
}
  • 首先,aTexCoord是一个二维向量,表示纹理的坐标,
  • varying这个变量是用来在vertex_shader和fragment_shader之间传递值用的,所以名称要相同,我们把aTexCoord赋值给vTexCoord,

然后来看片元着色器的代码

fragment_shader.glsl

precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D sTexture;
void main() {
    //gl_FragColor = vec4(0,0.5,0.5,1);
    gl_FragColor = texture2D(sTexture,vTexCoord);
}
  • 在片元着色器中,我们声明了一个uniform常量,类型是sampler2D,这个类型是指一个二维的纹理数据数组

  • 使用texture2D来处理被插值的纹理坐标vTexCoord和纹理数据sTexture,得到的颜色值就是要显示的颜色,交给gl_FragColor

更新Renderer类

首先,利用刚才写的类来获得一个纹理ID,这句话放在onSurfaceCreated里面:
textureId=TextureHelper.loadTexture(context,R.raw.demo_pic);
然后我们加入纹理坐标数据(参见上图的对应关系)

private final float[] textureVertexData = {
        0.5f,0.5f,
        1f,0f,
        0f,0f,
        0f,1f,
        1f,1f
};

并把它复制到OpenGL的本地内存中

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

类似的,我们要获得刚才的变量、常量引用(handle):

uTextureSamplerHandle=GLES20.glGetUniformLocation(programId,"sTexture");
aTextureCoordHandle=GLES20.glGetAttribLocation(programId,"aTexCoord");

更新onDrawFrame
首先,把纹理坐标用类似的方法传递过去:

GLES20.glEnableVertexAttribArray(aTextureCoordHandle);
GLES20.glVertexAttribPointer(aTextureCoordHandle,2,GLES20.GL_FLOAT,false,8,textureVertexBuffer);

然后,我们启用一个纹理,并把它和刚才生成的纹理ID绑定,再把纹理数据引用传过去,因为我们启用的是GL_TEXTURE0,所以在glUniform1i中第二个参数是0(大家可以都改成1试一下,在这里应该是一样的效果):

GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureId);

GLES20.glUniform1i(uTextureSamplerHandle,0);

目前onDrawFrame的代码如下:

@Override
public void onDrawFrame(GL10 gl) {
    GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
    GLES20.glUseProgram(programId);
    GLES20.glUniformMatrix4fv(uMatrixHandle,1,false,projectionMatrix,0);
    GLES20.glEnableVertexAttribArray(aPositionHandle);
    GLES20.glVertexAttribPointer(aPositionHandle, 3, GLES20.GL_FLOAT, false,
            12, vertexBuffer);



    GLES20.glEnableVertexAttribArray(aTextureCoordHandle);
    GLES20.glVertexAttribPointer(aTextureCoordHandle,2,GLES20.GL_FLOAT,false,8,textureVertexBuffer);

    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureId);

    GLES20.glUniform1i(uTextureSamplerHandle,0);

    GLES20.glDrawElements(GLES20.GL_TRIANGLES,indexData.length,GLES20.GL_UNSIGNED_SHORT,indexBuffer);
}

运行一下,显示图片

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

推荐阅读更多精彩内容