OpenGL (三) OpenGL基本概念和绘制简单三角形

前言

  • 为啥要学习OpenGL?
  1. OpenGL的用途太多了,而且是跨平台的图形API工具,主要用在3D方面。
  2. 3D图形的常见用途:
    (1)实时3D图形的应用范围包括交互式游戏和模拟以及数据的可视化显示(供科学、医学或商业应用)
    (2)在个人计算机领域,3D图形的应用几乎没有止境。目前最常见的用途,游戏、AR、VR,都是基于OpenGL 的。3D图形在科学是视觉和工程应用中非常流行,物美价廉的3D硬件大量涌现使得这些应用技术空前的流行火爆。
    (3)Mac OS X 以及 iOS 都是使用OpenGL对所有窗口和控件进行渲染,从而创建了功能强大,引人入胜的可视化界面。
    (4)设计时使用的3D max ,还是游戏制作的Unity 3D,Cocos2D 底层都是使用了OpenGL。而我们医学上的影像渲染也都依托于OpenGL。
  • 本篇博客主要是以IOS开发者的角度,从环境搭建,到专业术语讲解,逐步深入,为后续学习打好基础。
  • 这里非常感谢:CC老师_HelloCoder ,让我对OpenGL有了全新的认识。

1. Xcode搭建OpenGL环境

  • 做任何学习我们讲究从易到难,循序渐进,学习openGL也是如此,我们先从最简单的一个demo开始我们学习openGL , OpenGLES Metal的旅程。

1. 1 搭建步骤

  • 创建一个mac app工程:


    创建一个mac app工程
创建工程2
  • 添加依赖库


    添加依赖库
添加依赖库2
  • 注意:
  1. 如果是拖入include在此目录:


    不需要修改search path配置
  2. 如果跟工程文件在同级目录,则需要配置search path


    需要配置search path

    配置search paths
  • 创建main.cpp
    创建一个 c++ 类型的文件,命名为 main,创建的时候去掉勾选同时创建头文件的对勾。
创建main.cpp - 1
创建main.cpp - 2
  • 删除不需要的文件
    删除:AppDelegate.h、AppDelegate.m、main.m 、ViewController.h 和 ViewController.m等文件,如下图:


    删除不需要的文件

1. 2 写一个最简单的openGL测试代码

  • OpenGL里面图像绘制都是基于三角形的,下面通过调研OpenGL原始Api来绘制一个三角形。
//
//  main.cpp
//  kylOpenGLDemo001
//
//  Created by 孔雨露 on 2019/12/7.
//  Copyright © 2019 Apple. All rights reserved.
//

//移入了GLTool 着色器管理器(shader Mananger)类。没有着色器,我们就不能在OpenGL(核心框架)进行着色。着色器管理器不仅允许我们创建并管理着色器,还提供一组“存储着色器”,他们能够进行一些初步䄦基本的渲染操作。


#include "GLShaderManager.h"

// GLTool.h头文件包含了大部分GLTool中类似C语言的独立函数
#include "GLTools.h"

#include <glut/glut.h> //MAC使用的库
//在Windows 和 Linux上,我们使用freeglut的静态库版本并且需要添加一个宏。
//#define FREEGLUT_STATIC
//#include<GL/glut.h>

GLBatch triangleBatch;

GLShaderManager shaderManager;

//窗口大小改变时接受新的宽度和高度,其中0,0代表窗口中视口的左下角坐标,w,h代表像素

void ChangeSize(int w,int h)

{
    
    glViewport(0,0, w, h);
    
}

//为程序作一次性的设置

void SetupRC()

{
    
    //设置背影颜色
    
    glClearColor(0.0f,0.0f,1.0f,1.0f);
    
    //初始化着色管理器
    
    shaderManager.InitializeStockShaders();
    
    //设置三角形,其中数组vVert包含所有3个顶点的x,y,笛卡尔坐标对。
    
    GLfloat vVerts[] = {
        
        -0.5f,0.0f,0.0f,
        
        0.5f,0.0f,0.0f,
        
        0.0f,0.5f,0.0f,
        
    };
    
    //批次处理
    
    triangleBatch.Begin(GL_TRIANGLES,3);
    
    triangleBatch.CopyVertexData3f(vVerts);
    
    triangleBatch.End();
    
}

//开始渲染

void RenderScene(void)

{
    
    //清除一个或一组特定的缓冲区
  //缓冲区是一块存在图像信息的储存空间,红色、绿色、蓝色和alpha分量通常一起分量通常一起作为颜色缓存区或像素缓存区引用。
    //OpenGL 中不止一种缓冲区(颜色缓存区、深度缓存区和模板缓存区)。
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);
    
    //设置一组浮点数来表示红色
    
    GLfloat vRed[] = {1.0f,0.0f,0.0f,1.0f};
    
    //传递到存储着色器,即GLT_SHADER_IDENTITY着色器,这个着色器只是使用指定颜色以默认笛卡尔坐标第在屏幕上渲染几何图形
    //没有着色器,在OpenGL 核心框架中就无法进行任何渲染。在后面的课程中我们讲到不用固定渲染管线,当然在前期会先学习如果使用存储着色器。
    
    shaderManager.UseStockShader(GLT_SHADER_IDENTITY,vRed);
    
    //提交着色器
    
    triangleBatch.Draw();
    
    //将在后台缓冲区进行渲染,然后在结束时交换到前台
    
    glutSwapBuffers();
    
}

int main(int argc,char* argv[])

{
    
    //设置当前工作目录,针对MAC OS X
    /*
        `GLTools`函数`glSetWorkingDrectory`用来设置当前工作目录。实际上在Windows中是不必要的,因为工作目录默认就是与程序可执行执行程序相同的目录。但是在Mac OS X中,这个程序将当前工作文件夹改为应用程序捆绑包中的`/Resource`文件夹。`GLUT`的优先设定自动进行了这个中设置,但是这样中方法更加安全。
        */

    gltSetWorkingDirectory(argv[0]);
    
    //初始化GLUT库
  //负责初始化GLUT库。它会处理向程序输入的命令行参数,并且移除其中与控制GLUT如何操作相关的部分。它必须是应用程序第一个GLUT函数,负责设置其他GLUT例程必需的数据结构。

    glutInit(&argc, argv);
    
    /*初始化双缓冲窗口,其中标志GLUT_DOUBLE、GLUT_RGBA、GLUT_DEPTH、GLUT_STENCIL分别指
     
     双缓冲窗口、RGBA颜色模式、深度测试、模板缓冲区
     
     glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);
    GLUT_DOUBLE:双缓存窗口,是指绘图命令实际上是离屏缓存区执行的,然后迅速转换成窗口视图,这种方式,经常用来生成动画效果;
     GLUT_DEPTH:标志将一个深度缓存区分配为显示的一部分,因此我们能够执行深度测试;
     GLUT_STENCIL:确保我们也会有一个可用的模板缓存区。

     */
    //创建窗口并设置显示模式
    glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);
    
    //GLUT窗口大小,标题窗口
    //glutInitWindowSize设置所需的窗口大小,如果不想在这个设置一个固定值,也可以先查询显示设备的尺寸,然后根据计算机的屏幕动态设置窗口的大小。

    glutInitWindowSize(800,600);
    //glutCreateWindow它的功能和它的名字一样,如果当前的系统环境可以满足glutInitDisplayMode()的显示模式要求,这里就会创建一个窗口(此时会调用计算机窗口系统的接口)。只有GLUT创建了一个窗口之后(其中包含创建创建OpenGL环境的过程),我们才可以使用OpenGL相关的函数

    glutCreateWindow("kongyulu first opengl demo");
    
    //注册回调函数
    
    glutReshapeFunc(ChangeSize);
    
    //它设置了一个显示回调(diplay callback),即GLUT在每次更新窗口内容的时候回自动调用该例程
    glutDisplayFunc(RenderScene);
    
    //驱动程序的初始化中没有出现任何问题。
    //glewInit函数,属于另一个辅助库GLEW(OpenGL Extention Wrangler)。GLEW可以简化获取函数地址的过程,并且包含了可以跨平台使用的其他一些OpenGL编程方法。

    GLenum err = glewInit();
    
    if(GLEW_OK != err) {
        
        fprintf(stderr,"glew error:%s\n",glewGetErrorString(err));
        
        return 1;
        
    }
    
    //调用SetupRC
    
    SetupRC();
    
    //glutMainLoop这是一个无限执行的循环,它会负责一直处理窗口和操作系统的用户输入等操作。(注意:不会执行在glutMainLoop()之后的所有命令。)

    glutMainLoop();
    
    return 0;
    
}


  • 运行效果:
opengl绘制三角形

1. 3 源码下载

git  clone https://github.com/Eleven2012/OpenGL2019/tree/master/kylOpenGLDemo001
  • 从一个简单的OpenGL原汁原味的demo,我们可以感觉到OpenGL在图形处理领域的强大,然而看似简单的几个API的调用,里面确隐藏了很多图形学相关的知识。要深入理解OpenGL的处理方式和原理,就需要理解这些图形学相关的知识。下面我们将从图形API逐步深入。

2. 图形API简介

  • 常用的图形API主要有:OpenGL, OpenGL ES,DirectX, Metal

  • 图形API⽬目的是解决什什么问题

  1. 简单来说就是实现图形的底层渲染:
    A. ⽐如在游戏开发中,对于游戏场景/游戏⼈人物的渲染
    B. ⽐如在⾳音视频开发中,对于视频解码后的数据渲染
    C. ⽐如在地图引擎,对于地图上的数据渲染
    D. ⽐如在动画中,实现动画的绘制
    E. ⽐如在视频处理理中,对于视频加上滤镜效果
  2. OpenGL /OpenGL ES/ Metal 在任何项⽬目中解决问题的本质 就是利利⽤用GPU芯⽚片来⾼高效渲染图形图像.
  3. 图形API 是iOS开发者唯⼀一接近GPU的⽅方式.
  4. OpenGL基本上一种底层渲染API,我们不能告诉它“在什么地方绘制什么”--我们需要自己动手,通过载入三角形,应用必要的变化和正确的纹理、着色器并在必要应用混合模式来组合一个模型。这使得我们能够大量的底层控制,与使用高层工具包,使用OpenGL这样的底层API动人之处在于,我们不能仅仅是重现许多标准3D渲染,我们可以创造自己的算法,甚至可以去发现一些新的捷径、性能技巧和艺术视觉技术。
  5. 在OpenGL 或几乎所有的3D API中创建一个用于绘图窗口时,必须指定希望使用的坐标系统以及指定的坐标如何映射到实际的屏幕像素。
  • 学习OpenGL之前,有必要先了解一下什么是OpenGL?
  1. OpenGL 是一种应用程序编程接口(Application Programming Interface,API),它是一种可以对图形硬件设备特性进行访问的软件库。包含了500个不同的命令,可以用于设置所需的对象、图像和操作,以便开发交互式的3维计算机图形应用程序。
  2. OpenGL 被设计为一个现代化的,硬件无关的接口。因此不需要考虑计算机操作系统和窗口系统的前提下,在多种不同的图形硬件系统上,或者完全通过软件的方式实现OpenGL 接口。
  3. OpenGL 自身是不包含任何执行的窗扣任何或者处理用户输入输出的函数。事实上我们需要通过应用程序所运行的窗口系统提供的接口来执行这类操作。
  4. OpenGL也没有提供任何用户表达3维物体模型,或者读取图像文件的操作。我们需要通过一系列几何图元(包括点、线、三角形以及patch)来创建三维空间的物体。
  • 常用的图形API库:
  1. OpenGL (Open Graphics Library):是⼀一个跨编程语⾔言、跨平台的编程图形程序接⼝,它将计算机的资源抽象称为⼀一个个OpenGL的对象,对这些资源的操作抽象为⼀一个个的OpenGL指令.
  2. OpenGL ES (OpenGL for Embedded Systems): 是 OpenGL 三维图形 API 的⼦子集,针对⼿手机、 PDA和游戏主机等嵌⼊入式设备⽽而设计,去除了了许多不不必要和性能较低的API接⼝口
  3. DirectX: 是由很多API组成的,DirectX并不不是⼀一个单纯的图形API. 最重要的是DirectX是属于 Windows上⼀一个多媒体处理理API.并不不⽀支持Windows以外的平台,所以不不是跨平台框架. 按照性 质分类,可以分为四⼤大部分,显示部分、声⾳音部分、输⼊入部分和⽹网络部分.
  4. Metal : Apple为游戏开发者推出了了新的平台技术 Metal,该技术能够为 3D 图 像提⾼高 10 倍的渲染性能.Metal 是Apple为了了解决3D渲染⽽而推出的框架
  • 苹果在2018年宣布在ios系统底层使用metal替代OpenGLES ,并不是意味着在IOS 上不能使用OpenGLES ,只是IOS系统API底层是基于Metal的。


    苹果在2018年宣布在ios系统底层使用metal替代OpenGLES

3. OpenGL 基本概念

3.1 专业名称解析

3.1.1 渲染,变换和投影

  • 渲染

将数学和图形数据转换成 3D 空间图像的操作叫做渲染(Rendering)。当这个术语作为动词使用时,指的是计算机创建三维图像时所经历的过程。它也作为名词使用,指的仅仅是最终的图像作品。

  • 变换: 通过 变换(Transformation),或者说旋转这些点,并在他们之间绘制线段,我们就能在平面的 2D 屏幕上创造出一个 3D 世界的错觉。

  • 顶点:这些点本身叫做 顶点(Vertices,单数为 Vertex),他们能通过一种称为 变换矩阵(Transformation Matrix)的数学结构进行旋转。
    另外还有一种矩阵叫 投影矩阵(Projection Matrix),用于将 3D 坐标转换成二维屏幕坐标,实际的线条也将在二维屏幕坐标上进行绘制。

  • 投影(Projection):用于创建几何图形的3D坐标将投影到一个2D表面(窗口背景)。

  • 正投影(Orthographic Projection):又称平行投影,物体在屏幕上的大小和实际大小相同,不管是远还是近。

  • 透视投影(Perspective Projection):远处的物体看上去比近处的物体更小一些,在模拟和3D动画中,这种投影能够获得最大程度的逼真感。

  • 变换矩阵(Transformation): 例如图形想发⽣平移,缩放,旋转变换.就需要使用变换矩阵.

  • 投影矩阵(Projection): 用于将3D坐标转换为二位屏幕坐标,实际线条也将在二维坐标下进行绘制。

如下图显示的是用线条绘制的一个放置在一个平面上的立方体:
变换和投影
  • 渲染上屏/交换缓冲区(SwapBuffer)
  1. 渲染缓冲区一般映射的是系统的资源⽐如窗口。如果将图像直接渲染到窗口对应的渲染缓冲区,则可以将图像显示到屏幕上。
  2. 但是,值得注意的是,如果每个窗口只有一个缓冲区,那么在绘制过程中屏幕进⾏了刷新,窗⼝可能显示出不完整的图像。
  3. 为了解决这个问题,常规的OpenGL程序⾄少都会有两个缓冲区。显示在屏幕上的称为屏幕缓冲区,没有显示的称为离屏缓冲区。在⼀个缓冲区渲染完成之后,通过将屏幕缓冲区和离屏缓冲区交换,实现图像在屏幕上的显示。
  4. 由于显示器的刷新一般是逐⾏进⾏的,因此为了防⽌交换缓冲区的时候屏幕上下区域的图像分属于两个不同的帧,因此交换一般会等待显示器刷新完成的信号,在显示器两次刷新的间隔中进⾏交换,这个信号就被称为垂直同步信号,这个技术被称为垂直同步
  5. 使⽤了双缓冲区和垂直同步技术之后,由于总是要等待缓冲区交换之后再进行下一帧的渲染,使得帧率⽆法完全达到硬件允许的最⾼⽔平。为了解决这个问题,引入了三缓冲区技术,在等待垂直同步时,来回交替渲染两个离屏的缓冲区,⽽垂直同步发生时,屏幕缓冲区和最近渲染完成的离屏缓冲区交换,实现充分利用硬件性能的目的。

3.1.2 顶点、着色器,片元、图元和光栅化

  • 顶点数组(VertexArray)顶点缓冲区(VertexBuffer)
  1. 画图⼀般是先画好图像的骨架,然后再往⻣架⾥⾯填充颜⾊,这对于 OpenGL也是⼀样的。顶点数据就是要画的图像的骨架,和现实中不同的 是,OpenGL中的图像都是由图元组成。
  2. 在OpenGLES中,有3种类型的图 元:点、线、三⻆角形。
  3. 那这些顶点数据最终是存储在哪⾥的呢?: 开发者可 以选择设定函数指针,在调⽤用绘制⽅方法的时候,直接由内存传⼊入顶点数 据,
  4. 也就是说这部分数据之前是存储在内存当中的,被称为顶点数组
  5. ⽽性能更高的做法是,提前分配⼀块显存,将顶点数据预先传入到显存当 中。这部分的显存,就被称为顶点缓冲区
  • 顶点: 指的是我们在绘制⼀个图形时,它的顶点位置数据.⽽这个数据可以直接 存储在数组中或者将其缓存到GPU内存中.
  • 管线:在OpenGL 下渲染图形,就会有经历一个一个节点.而这样的操作可以理解管 线.⼤家可以想象成流水线.每个任务类似流水线般执⾏.任务之间有先后顺序. 管线是一个抽象的概念,之所以称之为管线是因为显卡在处理数据的时候是按照 一个固定的顺序来的,⽽且严格按照这个顺序。就像⽔从⼀根管⼦的⼀端流到 另一端,这个顺序是不能打破的.
  • 固定管线/存储着⾊器
  1. 在早期的OpenGL 版本,它封装了很多种着⾊器程序块内置的一段包含了光照、坐标变换、裁剪等等诸多功能的固定shader程序来完成,来帮助开发者来完成图形的渲染. 而开发者只需要传⼊相应的参数,就能快速完成图形的渲染. 类似于iOS开发会封装很多API,而我们只需要调用,就可以实现功能.不需要关注底层实现原理.
  2. 但是由于OpenGL 的使⽤场景⾮常丰富,固定管线或存储着⾊器⽆法完成每一 个业务.这时将相关部分开放成可编程.
  • 着色:沿着顶点之间改变颜⾊色值,能够轻松创建光照照射到一个立方体的效果。着色器(Shader)则是在图形硬件上执行的单独程序,用来处理顶点和执行光栅化任务。

  • 着色器:在实时计算机图形中,最前沿的技术是可编程着色器(Programmable Shading)。图形卡不再是低能的渲染芯片。而是功能强大的高度可编程的渲染计算机。类似CPU的术语GPU应运而生。它代表图形处理单元,特指当今图形卡上的可编程芯片。它们是高度并行,并且具有非常快的速度。同样重要的是,程序员可以进行重新配置图形卡的工作方式,几乎可以实现任何可以想得到的特殊效果。

  • 着⾊器程序Shader:
  1. 就全⾯的将固定渲染管线架构变为了可编程渲染管线。因此,OpenGL在实际调用绘制函数之前,还需要指定一个由shader编译成的着⾊器程序。常⻅的着⾊器主要有顶点着⾊器(VertexShader)⽚段着⾊器 (FragmentShader)/像素着⾊器(PixelShader)⼏何着⾊器 (GeometryShader)曲⾯细分着⾊器(TessellationShader)。⽚段着⾊器和像素着⾊器只是在OpenGL和DX中的不同叫法⽽已。可惜的是,直到 OpenGLES 3.0,依然只⽀持了顶点着⾊色器和片段着⾊器这两个最基础的着⾊器.
  2. OpenGL在处理shader时,和其他编译器一样。通过编译、链接等步骤,⽣成了着⾊器程序(glProgram),着⾊器程序同时包含了顶点着⾊器和⽚段着⾊器的运算逻辑。在OpenGL进⾏绘制的时候,⾸先由顶点着⾊器对传入的顶点数据进⾏运算。再通过图元装配,将顶点转换为图元。然后进⾏光栅化,将图元这种⽮量图形,转换为栅格化数据。最后,将栅格化数据传⼊片段着⾊器中进⾏运算。⽚段着⾊器会对栅格化数据中的每一个像素进⾏运算,并决定像素的颜⾊.
  • 顶点着⾊器(VertexShader):
  1. ⼀般⽤来处理图形每个顶点变换(旋转/平移/投影等)
  2. 顶点着⾊器是OpenGL中⽤于计算顶点属性的程序。顶点着⾊器是逐顶点运算的程序,也就是说每个顶点数据都会执行⼀次顶点着⾊器,当然这是并行的,并且顶点着⾊器运算过程中⽆法访问其他顶点的数据.
  3. ⼀般来说典型的需要计算的顶点属性主要包括顶点坐标变换、逐顶点光照运算等等。顶点坐标由自身坐标系转换到归一化坐标系的运算,就是在这里发⽣的。
  • 细分着色

顶点着色器处理每个顶点的关联数据之后,如果同时激活了细分着色器,那么它将进一步处理这些数据。细分着色器阶段会用到两个着色器来分别管理Patch数据并产生最终的形状。

  • 几何着色:允许在光栅化之前对每一个几何图元做更进一步的处理,例如创建新的图元。这个着色阶段是可选的

  • ⽚元着⾊器程序(FragmentShader):

  1. ⼀般⽤来处理图形中每个像素点颜色计算和填充
  2. ⽚段着⾊器是OpenGL中⽤于计算⽚段(像素)颜⾊的程序。⽚段着⾊器是逐像素运算的程序,也就是说每个像素都会执⾏⼀次⽚段着⾊器,当然也是并⾏的.
  • 顶点着色和片元着色器的之间的区别?

顶点着色器(包括细分和几何着色)决定了一个图元应该位于屏幕的什么位置,而片元着色使用这些信息来决定某一个片元的颜色应该是什么?

  • OpenGL着⾊语言 GLSL(OpenGL Shading Language)

OpenGL着⾊语言(OpenGL Shading Language): 是用来在OpenGL中着色编程的语⾔,也即开发⼈员写的短小的⾃定义程序,他们是在图形卡的GPU (Graphic Processor Unit图形处理单元)上执⾏的,代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程性。⽐如:视图转换投影转换等。GLSL(GL Shading Language)的着⾊器代码分成2个部分: Vertex Shader(顶点着⾊器)Fragment(⽚段着⾊器).

  • 图元装配:图元装配将顶点及相关的集合图元之间组织起来,准备下一步剪切和光栅化操作。
  • 剪切:顶点可能落在视口(viewport)之外,此时与顶点相关的图元会做出改动,以保证相关的像素不会在视口外绘制。剪切(clipping)由OpenGL自动完成。
  • 光栅化Rasterization:
  1. 光栅化是把顶点数据转换为⽚元的过程,具有将图转化为一个个栅格组成的图象的作⽤,特点是每个元素对应帧缓冲区中的一像素。
  2. 光栅化就是把顶点数据转换为⽚元的过程。⽚元中的每一个元素对应于帧缓冲区中的⼀个像素。
  3. 光栅化其实是一种将几何图元变为二维图像的过程。该过程包含了两部分的工作。
    第⼀部分⼯作: 决定窗⼝坐标中的哪些整型栅格区域被基本图元占用;
    第⼆部分⼯作:分配一个颜⾊值和⼀个深度值到各个区域。光栅化过程产生的是片元。
  4. 把物体的数学描述以及与物体相关的颜⾊信息转换为屏幕上⽤于对应位置的像素及⽤于填充像素的颜色,这个过程称为光栅化,这是一个将模拟信号转化为离散信号的过程。
  • 纹理

纹理可以理解为图⽚. ⼤家在渲染图形时需要在其编码填充图片,为了使得场景更加逼真.⽽这里使⽤的图片,就是常说的纹理.但是在OpenGL,我们更加习惯叫纹理,⽽不是图片.

  • 混合(Blending):颜⾊混合效果。在测试阶段之后,如果像素依然没有被剔除,那么像素的颜色将会和帧缓冲区中颜⾊附着上的颜色进行混合,混合的算法可以通过OpenGL的函数进⾏指定。但是OpenGL提供的混合算法是有限的,如果需要更加复杂的混合算法,一般可以通过像素着⾊器进⾏实现,当然性能会⽐原⽣的混合算法差一些.

  • 逐步元的操作:这个阶段里会使用深度测试(depth test )和模板测试(stencil test)的方式来决定一个片元是否可见的。

  • 过程:顶点—> 图元 —> 片元—> 像素

  • 顶点—> 图元:

几何顶点被组合为图元(点,线段或多边形),然后图元被合成片元,最后片元被转换为帧缓存中的象素数据。

  • 图元 —> 片元:

图元被分几步转换为片元:图元被适当的裁剪,颜色和纹理数据也相应作出必要的调整,相关的坐标被转换为窗口坐标。最后,光栅化将裁剪好的图元转换为片元。
1) 裁剪
2) 转换到窗口坐标
3) 光栅化
光栅化是将一个图元转变为一个二维图象(其实只是布满平面,没有真正的替换帧缓存区)的过程。二维图象上每个点都包含了颜色、深度和纹理数据。将该点和相关信息叫做一个 片元(fragment)。(这就是片元和像素之间的关键区别,虽然两者的直观印象都是的像素,但是片元比像素多了许多信息,在光栅化中纹理映射之后图元信息转化为了像素)

  • 片元—> 像素:

1)像素所有权(ownership)检测
2)裁剪检测
3)Alpha检测
4)模版检测
5)深度检测
6)融合
7)抖动
8)逻辑操作

剪裁投影光栅化
  • 纹理贴图(Texture Mapping):将纹理理图⽚片附着到你绘图的图像上。一个纹理就是一副用来贴到三角形或多边形上的图片。

3.1.3 状态机

  • 状态机: 状态机是理论上的一种机器..状态机描述了一个对象在其⽣命周期内所经历的各种状态,状态间的 转变,发⽣转变的动因,条件及转变中所执⾏的活动。或者说,状态机是 ⼀种⾏为,说明对象在其⽣命周期中响应事件所经历的状态序列列以及对那 些状态事件的响应。因此具有以下特点::
  1. 它有记忆的能力,能够记住自己当前的状态。
  2. 它可以接收输入,根据输入的内容和自己的状态,修改自己的状态,并且可以得到输出。
  3. 当它进入某个特殊的状态(停机状态)的时候,它不再接收输入,停止工作。
  • OpenGL 也可以看成这样的一种机器:
  1. OpenGL 可以记录自己的状态(比如:当前所使用的颜色、是否开启了混合功能,等等,这些都是要记录的)
  2. OpenGL 可以接收输入(当我们调用 OpenGL 函数的时候,实际上可以看成 OpenGL 在接收我们的输入),根据输入的内容和自己的状态,修改自己的状态,并且可以得到输出(比如我们调用glColor3f,则 OpenGL 接收到这个输入后会修改自己的“当前颜色”这个状态;我们调用glRectf,则 OpenGL 会输出一个矩形)
  3. OpenGL 可以进入停止状态,不再接收输入。这个可能在我们的程序中表现得不太明显,不过在程序退出前,OpenGL 总会先停止工作的。

3.1.4 坐标系

  • 什么是左手坐标系,什么是右手坐标系?


    左手坐标系和右手坐标系
  • 有人说OpenGL里面的坐标系是属于左手坐标系,也有人说它属于右手坐标系,那OpenGL的坐标系到底是怎样的?
  1. OpenGL中 􏰓􏱉􏱊的物体,世界,照相机坐标系都是属于右手坐标系。
  2. 而规范化设备坐标系使用左手坐标系。。
  • 􏱉􏱊􏱋􏱉􏱊􏱋OpenGL坐标系
  1. OpenGL希望每次定点着色后,我们的可见顶点都为标准化设备坐标(Normalized Device Coordinate,NDC).也就是说每个顶点的x,y,z都应该在-1到1之间,超出这个范围的顶点将是不可见的。
  2. 通常情况下我们会自己设定一个坐标范围,之后再在顶点着色器中将这些坐标变换为表转化设备坐标。然后这些标准化设备坐标传入光栅器(Rasterizer),将它们变换为屏幕上的二维坐标和像素。
  3. 将坐标变换为标准化设备坐标,接着在转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System).将物体的坐标变换到几个过渡坐标(Intermediate Coordinate System)的优点在于: 在这些特定的坐标系中,一些操作或运算更加方便和容易,这一点很快就会变得很明显。对于我们来说比较重要的总共有5个不同的坐标系统。:
    局部空间(Local Space,或者称为物体空间Object Space)
    世界空间(World Space)
    观察空间(View Space,或者称为视觉空间(Eye Space))
    裁剪空间(Clip Space)
    屏幕空间(Screen Space)
  4. 就是一个顶点在最终被转化为片段之前需要经历的所有不同状态。为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是:
    模型(Model)
    观察(View)
    投影(Projection)
    这三个矩阵。
  5. 物体顶点的起始坐标在局部空间(Local Space),这里称它为局部坐标(Local Coordinate),它在之后会变成:
    世界坐标(World Coordinate),
    观测坐标(View Coordinate),
    裁剪坐标(Clip Coordinate),
    并最后以屏幕坐标(Screen Coordinate)的形式结束。
  6. 在3D图形学中常用的坐标系:
    世界坐标系
    物体坐标系
    摄像机坐标系
    惯性坐标系

3.1.4.1 2D 笛卡尔坐标系

  • 2D笛卡尔坐标: 在二维绘图中,最为常用的坐标系统是笛卡尔坐标系统。笛卡尔坐标由一个X坐标和Y坐标构成。x坐标测量水平方向的位置,而y坐标则测量垂直方向的位置。具体如下图:


    笛卡尔坐标系

3.1.4.2 3D 笛卡尔坐标系

  • 在2D基础上,现在,我们把二维坐标系统拓展到三维空间中,并增加深度分量;在2D笛卡尔标上加一个新轴,z轴。
  • z轴同时垂直于x轴&y轴。它代表了一条从屏幕的中心朝向你们的直线(我已经旋转了这个坐标系统的视角,把y轴向左旋转,把x轴向下和后旋转。否则,z轴将直接面向我们,无法看到)。现在我们用3个坐标(x,y,z)来指定三维空间中的一个位置。
    ==深度:通常表示窗口坐标z值;==
3D 笛卡尔坐标系

3.1.4.3 视口

视口
视口2

3.1.4.4 OpenGL投影方式

OpenGL投影方式
  • OpenGL正投影
OpenGL正投影
  • 平行投影
平行投影

3.1.4.5 OpenGL 摄影机坐标系

  • 摄像机(照相机)坐标系:

在坐标系的范畴里,摄像机坐标系和照相机坐标系都是一样的意义。照相机坐标系是和观察者密切相关的坐标系。照相机坐标系和屏幕坐标系相似,差别在于照相机坐标系处于3D空间中,而屏幕坐标系在2D平面里。

OpenGL 摄影机坐标系

3.1.4.6 OpenGL 世界坐标系,惯性坐标系,物体坐标系

  • 世界坐标系:

世界坐标系是系统的绝对坐标系,在没有建立用户坐标系之前画面上的所有的点的坐标都可以在该坐标系的元旦来确定各自的位置。世界坐标系始终是固定不变的。

  • 物体坐标系:􏴧􏴝􏰤􏱥􏱉􏱊􏱋
  1. 每个物体都有他们独立的坐标系。当物理移动或者改变方向是。该物体的关联的坐标系将随之移动或改变方向。
  2. 物体坐标系是以物体本身而言,比如,我先向您发指令,“向前走一步”,是向您的物体坐标体系指令。我并不知道你会往哪个绝对的方向移动。比如说,当你开车时,有人会说向左转,有人说向东,但是,向左转是物体坐标系的概念,而向东则是是世界坐标系中的。
  3. 在某种情况下,我们可以理解物体坐标系为模型坐标系。因为模型顶点的坐标都是在模型坐标系中描述的。
  • 惯性坐标系:

惯性坐标系指的是世界坐标到物体坐标系的“半途”。惯性坐标系的原点和物体坐标原点重合,但是惯性坐标系的轴平行于世界坐标系的轴。

  • 为什么要引入惯性坐标系?

因为物体坐标系转换到惯性坐标系只需要旋转,从惯性坐标系转换到世界坐标系只需要平移。

OpenGL 世界坐标系

3.1.5 坐标转换

3.1.5.1 OpenGL 坐标转换全局图

OpenGL 坐标转换全局图1
OpenGL 坐标转换全局图2

3.1.5.2 OpenGL 坐标转换计算

OpenGL 坐标转换计算

3.1.5.2 OpenGL 视变换

OpenGL 视变换

3.1.6 渲染流程

3.1.6.1 着色器渲染流程

着色器渲染流程

3.1.6.2 图片渲染流程

图片渲染流程

3.1.6.3 OpenGL 渲染图像的OpenGL 程序需要执行的操作

  • 什么是渲染管线?
  1. 它是一系列数据处理过程,并且将应用程序的数据转换到最终的渲染图像
  2. OpenGl 首先接收用户提供的几何数据(顶点和几何图元),并且将它输入到一系列着色器阶段中进行处理,包括:顶点着色、细分着色、以及最后的几何着色,然后它将进入光删化单元。光栅化单元负责对所有剪切区域内的图元生成片元数据,然后对每个生成的片元都执行一个片元着色器。
  3. 事实上,只有顶点着色器 和 片元着色器是必须的。细分和几何着色器是可选的步骤。
  • 渲染图像的OpenGL 程序需要执行的操作:
  1. 从OpenGL的几何图元中设置数据,用于构建形状。
  2. 使用不同的着色器(shader)对输入的图元数据执行计算操作,判断它们的位置、颜色,以及其他渲染属性。
  3. 将输入图元的数学描述转化为与屏幕位置对应的像素片元(fragment)。这一步也称为光栅化(rasterization)。
  4. 最后,针对光栅化过程产生的每个片元,执行片元着色器(fragment shader),从而决定这个片元的最终颜色和位置。
  5. 如果有必要,还需要对每个片元执行一些额外的操作,例如判断片元对应的对象是否可见,或者将片元的颜色与当前屏幕位置的颜色进行融合。


    OpenGL常见渲染流程
  • 基于上面的流程,我们可以修改一下本篇开篇提供的这个绘制三角形的demo,我们现在来绘制一个正方形:
#include <iostream>
#include <GLUT/GLUT.h>

void draw() {
    
    //设置清屏色
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    //设置颜色,红色
    glColor3f(1.0f, 0.0f, 0.0f);
    //设置绘图时的坐标系统
    glOrtho(0.0f, 1.0f, 0.0f, 1.0f, -1.0f, 1.0f);
    //开始渲染
    glBegin(GL_POLYGON);
    //设置多边形的4个顶点
    glVertex3f(0.25f, 0.25f, 0.0f);
    glVertex3f(0.75f, 0.25f, 0.0f);
    glVertex3f(0.75f, 0.75f, 0.0f);
    glVertex3f(0.25f, 0.75f, 0.0f);
    //结束渲染
    glEnd();
    //强制刷新缓冲区,保证绘制命令被执行
    glFlush();
    
}

int main(int argc, const char* argv[]) {
    //初始化GLUT库
    glutInit(&argc, (char**)argv);
    //创建一个窗口并制定窗口名
    glutCreateWindow("HelloWorld");
    //注册一个绘图函数,操作系统在必要时刻就会对窗体进行重新绘制操作
    glutDisplayFunc(draw);
    //进入GLUT事件处理循环,让所有的与“事件”有关的函数调用无限循环(永生循环)
    glutMainLoop();
    return 0;
}

3.1.7 简单源码流程分析

  • 在理解了OpenGL的一些基本概念和基础流程后,我们下面可以总结一下开篇给的那个简单绘制三角形的Demo了,其实你查看这个Demo的源码,里面的注释已经写的非常详细了。

  • OpenGL绘制三角形流程总结

  1. 导入框架

1.' #include<GLTools.h> GLTool.h'头文件包含了大部分GLTool中类似C语言的独立函数

  1. '#include<GLShaderManager.h>' 移入了GLTool 着色器管理器(shader Mananger)类。没有着色器,我们就不能在OpenGL(核心框架)进行着色。着色器管理器不仅允许我们创建并管理着色器,还提供一组“存储着色器”,他们能够进行一些初步䄦基本的渲染操作。
  2. 在Mac 系统下,'#include<glut/glut.h>'
    在Windows 和 Linux上,我们使用freeglut的静态库版本并且需要添加一个宏。
    '#define FREEGLUT_STATIC'
    '#include<GL/glut.h>'
  1. 启动GLUT
  1. 程序的总是“main”函数开始处理
    GLTools函数glSetWorkingDrectory用来设置当前工作目录。实际上在Windows中是不必要的,因为工作目录默认就是与程序可执行执行程序相同的目录。但是在Mac OS X中,这个程序将当前工作文件夹改为应用程序捆绑包中的/Resource文件夹。GLUT的优先设定自动进行了这个中设置,但是这样中方法更加安全。
  2. 创建窗口并设置显示模式
    glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);
    GLUT_DOUBLE:双缓存窗口,是指绘图命令实际上是离屏缓存区执行的,然后迅速转换成窗口视图,这种方式,经常用来生成动画效果;
    GLUT_DEPTH:标志将一个深度缓存区分配为显示的一部分,因此我们能够执行深度测试;
    GLUT_STENCIL:确保我们也会有一个可用的模板缓存区。
    深度、模板测试后面会细致讲到。
  3. 初始化GLEW库
    重新调用GLEW库初始化OpenGL 驱动程序中所有丢失的入口点,以确保OpenGL API对开发者完全可用。
    调用glewInit()函数一次就能完成这一步。在试图做任何渲染之前,还要检查确定驱动程序的初始化过程中没有出现任何问题。
  4. SetupRC()
    实际上这个函数对GLUT 没有什么影响,但是在实际开始渲染之前,我们这里进行任何OpenGL 初始化都非常方便。这里的RC代表渲染环境,这是一个运行中的OpenGL状态机的句柄。在任何OpenGL 函数起作用之前必须创建一个渲染环境。而GLUT在我们第一次创建窗口时就完成了这项工作。
  5. 初始化设置
    void glClearColor(GLclampf red,GLclampf green,GLclampf blue,GLclampf alpha);
    在windows 颜色成分取值范围:0-255之间
    在iOS、OS 颜色成分取值范围:0-1之间浮点值
  1. 将数据输出到OpenGL
  1. 当将缓存数据初始化完毕后,我们可以通过调用OpenGL 的一个绘制命令来请求渲染几何图元。使用的是glDrawArrays()就是一个常用的绘制命令。
  2. OpenGL 的绘制通常都是讲顶点数据传输到OpenGL 服务端。我们可以将一个顶点视为一个需要统一处理的数据包。这个包装的数据可以是我们需要的任何数据(也就课),通常其中几乎始终会包含位置数据。其他数据可能用来觉得一个像素的最终颜色。
  • main函数里的常用函数

glutInit() 负责初始化GLUT库。它会处理向程序输入的命令行参数,并且移除其中与控制GLUT如何操作相关的部分。它必须是应用程序第一个GLUT函数,负责设置其他GLUT例程必需的数据结构。

glutInitDisplayMode() 设置了程序所使用的窗口类型。窗口设置更多的OpenGL 特性,例如RAGA颜色空间,使用深度缓存或动画效果。

glutInitWindowsSize() 设置所需的窗口大小,如果不想在这个设置一个固定值,也可以先查询显示设备的尺寸,然后根据计算机的屏幕动态设置窗口的大小。

glutCreateWindow(),它的功能和它的名字一样,如果当前的系统环境可以满足

glutInitDisplayMode()的显示模式要求,这里就会创建一个窗口(此时会调用计算机窗口系统的接口)。只有GLUT创建了一个窗口之后(其中包含创建创建OpenGL环境的过程),我们才可以使用OpenGL相关的函数

glewInit()函数,属于另一个辅助库GLEW(OpenGL Extention Wrangler)。GLEW可以简化获取函数地址的过程,并且包含了可以跨平台使用的其他一些OpenGL编程方法。

glutDisplayFunc(),它设置了一个显示回调(diplay callback),即GLUT在每次更新窗口内容的时候回自动调用该例程

glutMainLoop(),这是一个无限执行的循环,它会负责一直处理窗口和操作系统的用户输入等操作。(注意:不会执行在glutMainLoop()之后的所有命令。)

  • 初始化顶点数组对象

void glGenVertexArray(GLsizei n ,GLUINT * arrays)
分配顶点Arr对象
返回n个未使用的对象名到数组arrays中,用作顶点数组对象。返回的名字可以用来分配的缓存对象,并且它们已经使用未初始化的顶点数组集合的默认状态进行了数值的初始化。

void glBindVertexArray(GLuint array);
绑定激活对象数组

glBindVertexArray(),如果输入的变量array非0,并且是glGenVertexArrays()所返回的,那么它将创建一个新的顶点数组对象并且与其名称关联起来。如果绑定到的是一个已经创建的顶点数组对象中,那么会激活这个顶点数组对象,并且直接影响对象找那个所保存的顶点数组对象,并且将渲染状态重设为顶多数组的默认状态。
如果array不是glGenVertexArrays(),所返回的数值,或者它已经被glDeleteVertexArray()函数释放,那么这里将产生一个GL_INVALID_OPERATION错误。
在2种情况下,我们需要绑定一个对象:
1.创建一个对象并初始化它所对应的数据时;
2.每次准备使用这个对象,而不是当前绑定的对象时。
当我们完成对顶点数组对象的操作之后,是可以调用glDeleteVertexArrays()将它释放的。

void glDeleteVertexArrays(GLsizei n,GLuint *arrays);
删除n个在arrays中定义的顶点数组对象,这样所有的名称可以再次用作顶点数据。如果绑定顶点数组已经被删除,那么当前绑定的顶点数组对象被重设为0(类似执行了glBindBuffer()函数,并且输入参数为0)。默认的顶点数组会变成当前对象。在arrays当中未使用的名称都会被释放,但是当前顶点数组的状态不会发生任何变化

为了保证程序的完整性,可以调用gllsVertexArray()检查某个名称释放已经被保留为一个顶点数组对象了。GLboolean gllsVertexArray(GLuint array);
如果array是一个已经用GLgenVertexArrays()创建且没有被删除的顶点数组对象的名称,那么返回GL_TRUE,如果array为0或者不是任何顶点数组对象的名称,那么返回GL_FALSE;

  • 分配顶点缓存对象:

顶点数组对象负责保存一系列的数据,这些数据保存到缓冲对象中,并且由当前绑定的顶点数组对象管理。缓存对象就是OpenGL 服务端分配和管理的一个块内存区域,并且几乎所有传入的OpenGL的数据都存储在缓存对象当中。
顶点缓冲对象的初始化过程与顶点数组对象的创建过程类似,不过需要有向缓存添加数据的一个过程。

1.创建顶点缓存对象的名称
void glGenBuffers(GLsizei n,GLuint *buffers);返回n个当前未使用的缓存对象名,并保存到buffers数组中。返回到buffers中的名称不一定是连续的整型数据。
这里返回的名称只用于分配其他缓存对象,它们在绑定之后只会记录一个可用状态。
0是一个保留的缓存对象名称,glGenBuffers()永远都不会返回这个值的缓存对象。

  • 绑定缓存对象

由于OpenGL 中有很多不同类型的缓存对象,因此绑定一个缓存时,需要指定对应的类型。

1.指定当前激活/绑定的对象
void glBindBuffer(GLenum target,GLunit buffer);

  1. 参数target的类型:GL_ARRAY_BUFFER、GL_ELEMENT_ARRAY_BUFFER、GL_PIXEL_PACK_BUFFER、GL_PIXEL_UNPACK_BUFFER、GL_COPY_READ_BUFFER、GL_COPY_WRITE_BUFFER、GL_TRANSFORM_FEEDBACK_BUFFER、GL_UNIFORM_BUFFER。
  2. 顶点数据缓存使用GL_ARRAY_BUFFER
  3. 参数buffer:绑定的缓存对象名称
  4. glBindBuffer完成3项工作:1.如果是第一次绑定buffer,且它是一个非零无符号整型,那么将创建一个与该名称相对应的新缓存对象。2.如果绑定到是一个已经创建的缓存对象,那么它将成为当前被激活的缓存对象。3.如果绑定的buffer值为0,那么OpenGL将不在对当前target应用任何缓存对象。

2.释放缓存
void glDeleteBuffers(GLSizei n,const GLuint *buffers);
删除n个保存在buffer数组中的缓存对象。被释放的缓存对象可以重用。
如果删除的缓存已经绑定,那么该对象所以绑定将会重置为默认缓存对象,即相当于用0作为参数执行glBindBuffer(),如果试图删除不存在的缓存对象,或者缓存对象为0,那么将忽略该操作(不会产生错误)

3.判断一个整数值是否是一个缓存对象的名称。
GLboolean gllsBuffer(GLuint buffer);
如果buffer是一个已经分配并且没有释放的缓存对象名称,则返回GL_TRUE。如果buffer为0或者不是一个缓存对象的名称,则返回GL_FALSE。

  • 将数据载入缓存对象

初始化顶点缓存对象之后,我们需要把顶点数据从对象传输到缓存对象中。这一步步是通过glBufferData()来实现的。它主要有2个任务:分配顶点数据所需的存储空间,然后将数据从应用程序的数组拷贝到OpenGL 服务端的内存中

glBufferData(GLenum target,GLsizeiptr size,const GLVoid *data,Glenum usage);
当OpenGL 服务端内存中分配size个存储单元(通常都是byte),用于存储数据或者索引。如果当前绑定的对象已经存在了关联数据,那么首先会删除这些数据。

  1. 参数target:
    顶点属性数据,GL_ARRAY_BUFFER;
    索引数据,索引数据,GL_ELEMENT_ARRAY_BUFFER;
    从OpenGL 中获取的像素数据,GL_PIXEL_PACK_BUFFER;
    OpenGL 的像素数据GL_PIXEL_UNPACK_BUFFER;
    对于缓存直接的复制数据,GL_COPY_READ_BUFFER、GL_COPY_WRITE_BUFFER;
    对于通过transform feedback 着色器获得结果,GL_TRANSFORM_FEEDBACK_BUFFER、
    一致变量,GL_UNIFORM_BUFFER。
    对于纹理缓存中存储的纹理数据,GL_TEXTURE_BUFFER
  2. 参数size:存储数据的总数量,data存储元素的总数*单位元素存储空间
  3. 参数data: 客户端内存的指针,以便初始化缓存对象,要么是NULL,如果传入的指针合法,那么将会有size个大小的数据从客户端拷贝到服务端。如果传入的是NULL,那么将保留size大小的未初始化的数据,以备后用。
    如果所需的size大小超过了服务端能够分配的额度,那么glBufferData()将产生一个GL_OUT_OF_MEMORY错误。如果usage设置的不是可用的模式值,那么会产生GL_INVALID_VALUE错误。

参考资料:

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