OpenGL ES 2.0 (iOS)[01]: 一步从一个小三角开始


目录结构:

第一步,明确要干嘛

 a.目标
 b.效果
 c.分析

第二步,怎么去画(纯理论)

 a.OpenGL ES 2 的渲染管线

 b.简述绘制流程的每一个单元【至左向右】
     1) OpenGL ES 2.0 API
     2) Vertex Arrays / Buffer Objects
     3) Vertex Shader
     4) Primitive Assembly
     5) Rasterization 
     6) Texture Memory
     7) Fragment Shader
     8) Per-Fragment Operations
     9) Render Buffer & Frame Buffer
     10) EAGL API

 c. OpenGL ES Shader Language 简述
      *) 简单流程图

第三步,怎么去画(实战)

 a.OpenGL ES 2 的渲染流程 细化
    1) 配置环境
    2) 初始化数据
    3) 配置 OpenGL ES Shader
    4) 渲染绘制

 b.流程代码化

  一、配置渲染环境
    1) 配置渲染窗口 [ 继承自 UIView ]
    2) 配置渲染上下文
    3) 配置帧渲染
    4) 配置渲染缓存
    5) 帧缓存装载渲染缓存的内容
    6) 渲染上下文绑定渲染窗口(图层)

  二、修改背景色

  三、 初始化数据

  四、 配置 OpenGL ES Shader

    1) 编写 Vertex Shader Code 文件
    2) 编写 Fragment Shader Code 文件
    3) 配置 Vertex Shader
    4) 配置 Fragment Shader
    5) 创建 Shader Program
    6) 装载 Vertex Shader 和 Fragment Shader
    7) 链接 Shader Program

  五、渲染绘制

    1) 清空旧渲染缓存
    2) 设置渲染窗口
    3) 使用 Shder Program
    4) **关联数据**
    5) 绘制图形

 c.面向对象的重新设计

第四步,练练手

 a.修改背景色

 b.修改三角形的填充色

 c.修改三角形的三个顶点的颜色(填充色)


第一步,明确要干嘛

1. 目标:

使用 OpenGL ES 2.0 在 iOS 模拟器中绘制一个三角形。

2. 效果:

3. 分析图形:

  1. 背景颜色是蓝色
    --> 修改背景颜色

  2. 直角三角形
    --> 绘制三角形

4.绘制三角形?三角形由什么组成?

--> 三个端点 + 三条线 + 中间的填充色,即三个点连成线形成一个三角面。

1). 三个什么端点(屏幕坐标点)?
要回答这个问题要先了解 OpenGL ES 的坐标系在屏幕上是怎样分布的:


OpenGL ES 的坐标系{x, y, z}

注:图片截自 《Learning OpenGL ES For iOS》一书

a. 通过图片的三维坐标系可以知道:
- 它是一个三维坐标系 {x, y, z}
- 三维坐标中心在正方体的几何中心 {0, 0, 0}
- 整个坐标系是 [0, 1] 的点,也就是说 OpenGL 中只支持 0 ~ 1 的点

注意,这里所讲的 0 和 1 ,最好理解成 0 --> 无限小, 1 --> 无限大 ,它并不是指 0 个单位的长度,或 1 个单位的长度。

b. 再来看看我们绘制的三角形,在 iOS 模拟器 或真机上 的坐标是怎样构成的:


三维坐标+坐标值 演示图

注:图片通过 CINEMA4D (c4d)三维软件绘制

二维就是长这样的了:


二维坐标( z = 0 )
  1. 三条线?

a.连接三个端点形成封闭的三角面,那么 OpenGL ES 能不能直接绘制三角形 ? --> 答案是能。

b.那么 OpenGL 能直接画正方形么?
--> 答案是不能。

c.那OpenGL 能直接绘制什么?
--> 答案是:点精灵、线、三角形,它们统称为 图元(Primitive)。

注:答案来自于《OpenGL ES 2.0 Programming Guide》 7. Primitive Assembly and Rasterization 一章,截图如下:


  1. 线元

Line Strip , 指首尾相接的线段,第一条线和最后一条线没有连接在一起;
Line Loops, 指首尾相接的线段,第一条线和最后一条线连接在一起,即闭合的曲线;


Line
  1. 三角图元

Triangle Strip, 指条带,相互连接的三角形
Triangle Fan, 指扇面,相互连接的三角形


Triangle

扇面
  1. 点精灵 【主要应用在 纹理 方面】

3)填充色?

就是指 RGBA 的颜色值;( _ 感觉好废但还是要说)


第二步,怎么去画(纯理论)

怎么去画,就是通过多少个步骤完成一个完整的绘制渲染流程,当然这里指 OpenGL ES 2 的渲染管线流程)

OpenGL ES 2 的渲染管线

图形管线(Graphics Pipeline)

因为这里是 iOS 端的图,所以重新绘制了一下:


OpenGL ES 2 渲染流程图

注:此图根据 《OpenGL ES 2.0 programming guide》的 Graphics Pipeline 和 Diney Bomfim [All about OpenGL ES 2.x - (part 2/3)] 的管线图进行重新绘制。【绘制的软件为:Visio 2016】

1. 简述绘制流程的每一个单元【至左向右】

OpenGL ES 2.0 API :

iOS 环境下

gltypes.h 是包含了 OpenGL ES 2.0 的基本数据类型的定义;
glext.h 是包含各种宏定义,以及矩阵运算等常用的函数;
gl.h 是 OpenGL ES 2.0 所有的核心函数(命令);

扩展
OpenGL ES 2.0 Reference (函数查询)在线

左边选择要查询的函数即可

离线的函数 Card
红框处单击打开

红箭头处选择保存即可

本人推荐使用离线的卡,不受网络影响,而且一目了然。配合官方的编程指南使用就最佳了。

2. Vertex Arrays / Buffer Objects :

  1. __Vertex Arrays Objects __(简称:VAOs),顶点数组对象,就是一个数组,包含顶点坐标、颜色值、纹理坐标等数据;通过 CPU内存关联到 GPU 的内存区被 GPU 所使用;

【官方解释:Vertex data may be sourced from arrays that are stored in application memory (via a pointer) or faster GPU memory (in a buffer object).(意指:顶点数组保存在程序内存或快速GPU内存中,前者通过数组指针访问数据,后者直接通过 Buffer Objects 访问。【就是指 VAOs 或 VBOs 方式访问】)】

绘制的三角形的数组(三个顶(端)点坐标)如下图:


顶点数组

VFVertex

这是 C 语言的知识,应该不难理解。

  1. __ Vertex Buffer Objects__ , (简称:VBOs [ Vertex Buffer Objects ]),缓存对象,就是持有顶点数组数据或数据下标的对象【并不是指面向对象里面的对象哦,其实一块 GPU 内存块】。

【官方解释:Buffer objects hold vertex array data or indices in high-performance server memory. (意指:VBOs 是持有保存在GPU快速内存区的顶点数据或顶点数据下标的缓存对象。)】

a. 为什么是 server ?
--> 答,OpenGL 是基于 CS 模式的设计而成,客户端操作就相当于我们写的 OpenGL API ( OpenGL commands ) 的各种操作,服务器就是图形处理相关的硬件。( ES 当然也是这意思咯。)

【官方解释:OpenGL is implemented as a client-server system, with the application you write being considered the client, and the OpenGL implementation provided by the manufacturer of your computer graphics hardware being the server.】

注:
**1) **a.b. 里面的【官方解释...】在 OpenGL ES 2.0 Reference Card 可以找到。
**2) **b.1 的【官方解释...】在《OpenGL Programming Guide》第八版 Introduction OpenGL 一章的第一小节 What Is OpenGL 中的解释。

3. __Vertex Shader (顶点着色器) : __

处理顶点相关的数据,包括顶点在屏幕的位置(矩阵变换),顶点处的光照计算,纹理坐标等。

顶点着色器的信号图:

注:图片截自:《OpenGL ES 2.0 Programming Guide》 1. Introduction to OpenGL ES 2.0 -- OpenGL ES 2.0 -- Vertex Shader 一节中

  1. __输入信号:__Attributes、Uniforms、Samplers (optional)

a. Attributes : 属性的意思,指每一个顶点数据;

b. __Uniforms : __

b-1. 统一的意思 , 是一个只读全局常量,存储在程序的常量区;
b-2. 当 Vertex Shader 和 Fragment Shader 定义了同名同类型的 Uniform 常量时,此时的 Uniform 常量就变成了全局常量(指向同一块内存区的常量);

c. __Samplers (可选的) : __
是一个特殊的 Uniforms 保存的是 Texteures(纹理) 数据;

  1. 输出信号: Varying

__Varying : __
a. 它是 Vertex Shader 与 Fragment Shader 的接口,是为了解决功能性问题(两个 Shader 的信息交互);

b. 储存 Vertex Shader 的输出信息;

c. Vertex Shader 与 Fragment Shader 中必须要有必须要同名同类型的Varying 变量,不然会编译错误;(因为它是两个 Shader 的信息接口啊,不一样还接什么口啊。)

  1. 交互信息: Temporary Variables

Temporary Variables :
a. 指临时变量;
b. 储存 Shader 处理过程中的中间值用的;
c. 声明在 Funtions(函数) 或 Variable(变量) 内部;

  1. __输出的内建变量:__gl_Position、gl_FrontFacing、gl_PointSize

a. gl_Position ** ( highp vec4 变量 ) :
就是 Vertex Position,Vertex Shader 的输出值,而且是
必须要赋值的变量;只有在 Vertex Shader 中使用才会有效**;

注:highp vec4, highp ( high precision ) 高精度的意思,是精度限定符;vec4 ( Floating Point Vector ) 浮点向量 , OpenGL ES 的数据类型。

b. __ gl_PointSize ( mediump float 变量 ) :__
告诉 Vertex Shader 栅格化点的尺寸(pixels,像素化),想要改变绘制点的大小就是要用这个变量 只有在 Vertex Shader 中使用才会有效

注:mediump , mediump ( medium precision ) 中等精度的意思,是精度限定符;还有最后一个精度限制符是 lowp ( low precision ),低精度的意思。

c. __ gl_FrontFacing ( bool 变量 ) : __
改变渲染物体的 Front Facing 和 Back Facing , 是用于处理物体光照问题的变量,双面光照(3D 物体里外光照)问题的时候才会使用的变量,只能在 Vertex Shader 中进行设置, Fragment Shader 是只读的

4. Primitive Assembly (图元装配) :

  1. 第一步,把 Vertex Shader 处理后的顶点数据组织成 OpenGL ES 可以直接渲染的基本图元:点、线、三角形;

  2. 第二步,裁剪 ( Clipping ) ,只保留在渲染区域(视锥体,视觉区域)内的图元;

  3. 第二步,剔除 ( Culling ),可通过编程决定剔除前面、后面、还是全部;

注:
视锥体,实际上是一个三维锥体包含的空间区域,由摄影机和物体的捕捉关系形成;

视锥体
图片来源 《透视投影详解》一文

5. Rasterization ( 光栅化 ) :

光栅化的信号图:

作用是,将基本图元(点、线、三角形)转换成二维的片元(Fragment, 包含二维坐标、颜色值、纹理坐标等等属性), 像素化基本图元使其可以在屏幕上进行绘制(显示)。

6. __Texture Memory ( 纹理内存 ) : __

Texture 就是指保存了图片(位图)的所有颜色的缓存;Texture Memory 就是图片的颜色(像素)内存;每一个嵌入式系统对 Texture Memory 的大小都是有限制的;

  1. 完整的 iOS 渲染绘制管线图中,向上指向 Vertex Shader 的虚线,意指 Texture Coordinate (纹理坐标)信息是通过程序提供给它的;

  2. 完整的 iOS 渲染绘制管线图中,指向 Fragment Shader 的实线,因为 Fragment Shader 处理的是光栅化后的数据,即像素数据,而 Texture 本身就是像素数据,所以 Texture Memory 可以直接当成 Fragment Shader 的输入;

7. Fragment Shader ( 片元着色器 ) :

片元着色器信号图:

  1. 输入信号: Varying、Uniforms、Samples
    与 Vertex Shader 的输入是同一个意思,具体请查看 Vertex Shader 处的解释~~~;

  2. 输入的内建变量:gl_FragCoord、gl_FrontFacing、gl_PointCoord

a. __ gl_FragCoord ( mediump vec4 只读变量 ) :__
是保存窗口相对坐标的 { x, y, z, 1/w } 的变量,z 表示深度 (will be used for the fragment's depth), w 表示旋转;

b. __ gl_PointCoord ( mediump int 只读变量 ) : __
是包含了当前片元原始点位置的二维坐标;点的范围是 [ 0, 1 ] ;

c. gl_FrontFacing
请查看 Vertex Shader 处的解释;

  1. 输出信号 (内建变量) : gl_FragColor、gl_FragData (图上没写)

a. **gl_FragColor ( mediump vec4 ) ** :
片元的颜色值;

b. __gl_FragData ( mediump vec4 ) : __
是一个数组,片元颜色集;

注:两个输出信号只能同时存在一个,就是 写了 gl_FragColor 就不要写 gl_FragData , 反之亦然;【If a shader statically assigns a value to gl_FragColor, it may not assign a value to any element of gl_FragData. If a shader statically writes a value to any element of gl_FragData, it may not assign a value to gl_FragColor. That is, a shader may assign values to either gl_FragColor or gl_FragData, but not both.

补充知识 ( For Shader )

8. __Per-Fragment Operations : __

信号图:

  1. Pixel ownership test ( 像素归属测试 ) :
    判断像素在 Framebuffer 中的位置是不是为当前 OpenGL ES Context 所有,即测试某个像素是否属于当前的 Context 或是否被展示(是否被用户可见);

  2. Scissor Test ( 裁剪测试 ) :
    判断像素是否在由 glScissor* 定义的裁剪区域内,不在该剪裁区域内的像素就会被丢弃掉;

  3. Stencil Test ( 模版测试 ):
    将模版缓存中的值与一个参考值进行比较,从而进行相应的处理;

  4. Depth Test ( 深度测试 ) :
    比较下一个片段与帧缓冲区中的片段的深度,从而决定哪一个像素在前面,哪一个像素被遮挡;

  5. Blending ( 混合 ) :
    将片段的颜色和帧缓存中已有的颜色值进行混合,并将混合所得的新值写入帧缓存 (FrameBuffer) ;

  6. Dithering ( 抖动 ) :
    使用有限的色彩让你看到比实际图象更为丰富的色彩显示方式,以缓解表示颜色的值的精度不够大而导致颜色剧变的问题。

9. Render Buffer & Frame Buffer:

关系图:

  1. __Render Buffer ( 渲染缓存 ) : __

a. 简称 RBO , Render Buffer Object;
b. 是由程序(Application)分配的 2D 图片缓存;
c. Render Buffer 可以分配和存储颜色(color)、深度(depth)、模版(stectil)值,也可以把这三种值装载到 Frame Buffer 里面;

  1. __Frame Buffer ( 帧缓存 ) : __

a. 简称 FBO , Frame Buffer Object;
b. 是颜色、深度、模板缓存装载在 FBO 上所有装载点的合集;
c. 描述颜色、深度、模板的大小和类型的属性状态;
d. 描述 Texture 名称的属性状态;
e. 描述装载在 FBO 上的 Render Buffer Objects ( 渲染缓存对象 ) 的属性状态;

扩充知识(FBO):

FBO API 支持的操作如下:

  1. 只能通过 OpenGL ES 命令 ( API ) 创建 FBO 对象;
  2. 使用一个 EGL Context 去创建和使用多个 FBO , 即不要为每一个 FBO 对象创建一个正在渲染的上下文(rendering context);
  3. 创建 off-screen 的颜色、深度、模板渲染缓存和纹理需要装载在 FBO 上;
  4. 通过多个 FBO 来共享颜色、深度、模板缓存;
  5. 正确地装载纹理的颜色或深度到 FBO 中,避免复制操作;

10. EAGL API :

官方的是 EGL API 与平台无关,因为它本身是可以进行平台定制的,所以 iOS 下就被 Apple 定制成了 EAGL API 。

EAGL.h : 里面的核心类是 EAGLContext , 上下文环境;
EAGLDrawable.h : 用于渲染绘制输出的 EAGLContext 分类;

注:除了上面的两个外,还有一个类 CAEAGLLayer ,它就是 iOS 端的渲染窗口寄宿层;

【 看这里:

  1. EGL API 设计出来的目的就是为了在 OpenGL ES 2 能在窗口系统 (屏幕 ,iOS 是 CAEAGLLayer 类为寄宿层的 View)进行渲染绘制;

  2. 可以进行 EGL 渲染的前提是:

a. 可以进行显示的设备( iOS 下当然是手机或模拟器 )
b. 创建渲染面(rendering surface), 设备的屏幕 ( on-screen ) 或 像素缓存 ( pixel Buffer ) ( off-screen )

注: pixel Buffer , 这种 buffer 是不能直接显示的,只能成为渲染面或通过其它 API 分享出去,如: pbuffers 经常被用于 Texture 的 maps , 因为 Texture 本身也是像素嘛;

  1. 创建渲染上下文 ( rendering context ), 即 OpenGL ES 2 Rendering Context ;

注:

__OpenGL ES Context : __ 保存了渲染过程中的所有数据和状态信息;
图示解释:



图片截自, RW. Beginning. OpenGL ES.and.GLKit Tutorials 教程

OpenGL ES Shader Language 简述

流程图中出现的 Vertex Shader 与 Fragment Shader 都是要使用 GLSL ES 语言来进行编程操作的

1. GLSL ES 版本:

OpenGL ES 2.0 对应的 GLSL ES 版本是 1.0,版本编号是 100;

2. iOS Shader 类:

iOS 环境下 GLKit 提供了一个简单的 Shader 类——GLKBaseEffect 类;


GLKit APIs

3. OpenGL 本身是 C Base 的语言,可以适应多个平台,而在 iOS 下的封装就是 GLKit ;

4. GLSL ES (也称 ESSL ) ?

简单流程图:

OpenGL ES Shader 流程图

  1. 编写 Shader 代码:

a. 同时编写 Vertex Code 和 Fragment Code
b. 建议以文件的形式来编写,不建议使用 " ...... " 字符串的形式进行编写,前者会有编译器的提示作为辅助防止一定的输入错误,但后者不会,为了不必要的麻烦,使用前者;
c. 文件的名称使用应该要形如 xxxVertexShader.glsl / xxxFragmentShader.glsl;

注:(其实文件名和后缀都可以随意的,但是你在编程的时候为了可读性,建议这样写,也是为了防止不必要的麻烦);【 Xcode 只会在 glsl 的文件后缀的文件进行提示,当然有时候会抽一风也是正常的 】

d. 要掌握的知识点是 Shader 的 Data Typies(数据类型,如:GLfloat 等)、Build-in Variables(内置变量,如:attribute 等)、流程控制语句(if、while 等);

  1. 除编写 Shader Code 外,其它的流程都由一个对应的 GLSL ES 的 API (函数)进行相应的操作;

注:此处只是做了一个 Program 的图,不是只能有一个 Program,而是可以有多个,需要使用多少个,由具体项目决定。


第三步,怎么去画(实战)

以本文的小三角为例,开始浪吧~~~!

e981fd1c1e0c35f7e91735fb473b2bec.gif

OpenGL ES 2 的渲染流程 实际绘制环境,流程细化

OpenGL ES 2 iOS 渲染逻辑流程图.png

1.配置环境:

  1. 主要工作是,EAGL API 的设置。


    EAGL Class
  2. 核心操作:

a. CAEAGLLayer 替换默认的 CALayer,配置绘制属性;
b. EAGLContext,即 Render Context ,设置成** OpenGL ES 2 API ** 环境,并使其成为当前活跃的上下文环境;
c. Frame Buffers / Render Buffer 的创建和使用,以及内容绑定;
d. **EAGLContext 绑定渲染的窗口 (on-screen),CAEAGLLayer **;

扩展:
** CAEAGLLayer **

  1. 继承链:

    CALayer有的,当然 CAEAGLLayer 也有;
  1. 作用:
    a. The CAEAGLLayer class supports drawing OpenGL content in iPhone applications. If you plan to use OpenGL for your rendering, use this class as the backing layer for your views by returning it from your view’s layerClass class method. The returned CAEAGLLayer object is a wrapper for a Core Animation surface that is fully compatible with OpenGL ES function calls.
    -->大意就是,CAEAGLLayer 是专门用来渲染 OpenGL 、OpenGL ES 内容的图层;如果要使用,则要重写 layerClass 类方法。

b. Prior to designating the layer’s associated view as the render target for a graphics context, you can change the rendering attributes you want using the drawableProperties property.
-->大意就是,在 EAGLContext 绑定 CAEAGLLayer 为渲染窗口之前,可以通过修改 drawableProperties 属性来改变渲染属性。

  1. 使用注意:
    a. 修改 opaque 属性为 YES ( CAEAGLLayer.opaque = YES; );
    b. 不要修改 Transform ;
    c. 当横竖屏切换的时候,不要去修改 CAEAGLLayer 的 Transform 而进行 Rotate, 而是要通过 OpenGL / OpenGL ES 来 Rotate 要渲染的内容。

EAGLContext **
是管理 OpenGL ES 渲染上下文(包含,信息的状态、openGL ES 的命令(API)、OpenGL ES 需要绘制的资源)的对象,要使用 OpenGL ES 的 API (命令)就要使该 Context 成为当前活跃的渲染上下文。
(原文: An EAGLContext object manages an OpenGL ES rendering context—the state information, commands, and resources needed to draw using OpenGL ES. To execute OpenGL ES commands, you need a current rendering context.)

2. 初始化数据

这里主要是考虑是否使用 VBOs ,由于移动端对效率有所要求,所以一般采用 VBOs 快速缓存;

3. 配置 OpenGL ES Shader

  1. 这里的核心工作是 Shader Code ,即学习 GLSL ES 语言;
  2. iOS 端采用 glsl 后缀的文件来编写代码;

4. 渲染绘制

  1. 这里要注意的是 清空旧缓存、设置窗口,虽然只是一句代码的问题,但还是很重要的;
  2. 核心是学习 glDraw* 绘制 API ;

流程代码化

1.配置渲染环境

  1. 配置渲染窗口 [ 继承自 UIView ]

a. 重写 layerClass 类方法

+ (Class)layerClass {
   return [CAEAGLLayer class];
} 

b. 配置 drawableProperties ,就是绘制的属性

- (void)commit {
    
    CAEAGLLayer *glLayer = (CAEAGLLayer *)self.layer;
    
    // Drawable Property Keys
    /*
     // a. kEAGLDrawablePropertyRetainedBacking
     // The key specifying whether the drawable surface retains its contents after displaying them.
     // b. kEAGLDrawablePropertyColorFormat
     // The key specifying the internal color buffer format for the drawable surface.
     */
    
    glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking : @(YES), // retained unchange
                                   kEAGLDrawablePropertyColorFormat     : kEAGLColorFormatRGBA8 // 32-bits Color
                                   };
    
    glLayer.contentsScale = [UIScreen mainScreen].scale;
    glLayer.opaque = YES;
    
}
  1. 配置渲染上下文
// a. 定义 EAGLContext
@interface VFGLTriangleView ()
@property (assign, nonatomic) VertexDataMode vertexMode;
@property (strong, nonatomic) EAGLContext *context;
@end
// b. 使用 OpenGL ES 2 的 API,并使该 Context ,成为当前活跃的 Context
- (void)settingContext {
    
    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:self.context];
    
} 
  1. 配置帧渲染
- (GLuint)createFrameBuffer {
    
    GLuint ID;
    
    glGenFramebuffers(FrameMemoryBlock, &ID);
    glBindFramebuffer(GL_FRAMEBUFFER, ID);
    
    return ID;
    
}
函数 描述
glGenFramebuffers 创建 帧缓存对象
glBindFramebuffer 使用 帧缓存对象
glGenFramebuffers
void glGenFramebuffers (GLsizei n, GLuint * framebuffers)
n 指返回多少个 Frame Buffer 对象
framebuffers 指 Frame Buffer 对象的标识符的内存地址
glBindFramebuffer
void glBindFramebuffer (GLenum target, GLuint framebuffer)
target 只能填 GL_FRAMEBUFFER
framebuffer 指 Frame Buffer 对象的标识符
  1. 配置渲染缓存
- (GLuint)createRenderBuffer {
    
    GLuint ID;
    
    glGenRenderbuffers(RenderMemoryBlock, &ID);
    glBindRenderbuffer(GL_RENDERBUFFER, ID);
    
    return ID;
    
}
函数 描述
glGenRenderbuffers 创建 渲染缓存对象
glBindRenderbuffer 使用 渲染缓存对象
glGenRenderbuffers
void glGenRenderbuffers(GLsizei n, GLuint *renderbuffers)
n 指返回多少个 Render Buffer 对象
renderbuffers 指 Render Buffer 对象的标识符的内存地址
glBindRenderbuffer
void glBindRenderbuffer(GLenum target, GLuint renderbuffer)
target 只能填 GL_RENDERBUFFER
renderbuffers 指 Render Buffer 对象的标识符
  1. 帧缓存装载渲染缓存的内容
- (void)attachRenderBufferToFrameBufferWithRenderID:(GLuint)renderBufferID {
    
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBufferID);
    
}
函数 描述
glFramebufferRenderbuffer 装载 渲染缓存的内容到帧缓存对象中
glFramebufferRenderbuffer
void glFramebufferRenderbuffer (GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer)
target 只能填 GL_FRAMEBUFFER
attachment *只能是三个中的一个:GL_COLOR_ATTACHMENT0 ( 颜色缓存 )、GL_DEPTH_ATTACHMENT ( 深度缓存 )、GL_STENCIL_ATTACHMENT ( 模板缓存 ) *
renderbuffertarget 只能填 GL_RENDERBUFFER
renderbuffer 指 Render Buffer 对象的标识符,而且当前的 Render Buffer 对象一定要是可用的
  1. 渲染上下文绑定渲染窗口(图层)
- (void)bindDrawableObjectToRenderBuffer {
    
    [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];
    
}
函数 描述
renderbufferStorage: fromDrawable: 关联 当前渲染上下文和渲染窗口
renderbufferStorage: fromDrawable:
- (BOOL)renderbufferStorage:(NSUInteger)target fromDrawable:(id<EAGLDrawable>)drawable
target 只能填 GL_RENDERBUFFER
drawable *只能是 CAEAGLLayer 对象 *

函数解释:

  1. 为了使创建的 Render Buffer 的内容可以显示在屏幕上,要使用这个函数绑定 Render Buffer 而且分配共享内存;
  2. 要显示 Render Buffer 的内容, 就要使用 presentRenderbuffer:来显示内容;
  3. 这个函数的功能等同于 OpenGL ES 中的它【内容太多,简书不好排版】


函数 描述
glRenderbufferStorage 保存渲染缓存内容
glRenderbufferStorage
void glRenderbufferStorage(GLenum target, GLenum internalformat, GLsizei width, GLsizei height)
target 只能填 GL_RENDERBUFFER
internalformat *分三种 color render buffer、 depth render buffer、stencil render buffer *
width *像素单位,大小必须 <= GL_MAX_RENDERBUFFER_SIZE *
height *像素单位,大小必须 <= GL_MAX_RENDERBUFFER_SIZE *
internalformat
color render buffer [01] GL_RGB565, GL_RGBA4, GL_RGB5_A1,
color render buffer [02] GL_RGB8_OES, GL_RGBA8_OES
depth render buffer [01] GL_DEPTH_COMPONENT16,
depth render buffer [02] GL_DEPTH_COMPONENT24_OES, GL_DEPTH_COMPONENT32_OE
stencil render buffer GL_STENCIL_INDEX8, GL_STENCIL_INDEX4_OES, GL_STENCIL_INDEX1_OE

2.修改背景色

typedef struct {
    CGFloat red;
    CGFloat green;
    CGFloat blue;
    CGFloat alpha;
} RGBAColor;

static inline RGBAColor RGBAColorMake(CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha) {
    
    RGBAColor color = {
    
        .red = red,
        .green = green,
        .blue = blue,
        .alpha = alpha,
        
    };
    
    return color;
    
}

- (void)setRenderBackgroundColor:(RGBAColor)color {
    
    glClearColor(color.red, color.green, color.blue, color.alpha);
    
}
函数 描述
glClearColor 清空 Render Buffer 的 Color Render Buffer 为 RGBA 颜色
glClearColor
void glClearColor (GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
red 指 [0, 1] 的红色值
green 指 [0, 1] 的绿色值
blue 指 [0, 1] 的蓝色值
alpha 指 [0, 1] 的透明度值

注: 不想定义 RGBAColor 的话,可以直接使用 GLKit 提供的 GLKVector4 ,原型是

#if defined(__STRICT_ANSI__)
struct _GLKVector4
{
    float v[4];
} __attribute__((aligned(16)));
typedef struct _GLKVector4 GLKVector4;  
#else
union _GLKVector4
{
    struct { float x, y, z, w; };
    struct { float r, g, b, a; };  // 在这呢......
    struct { float s, t, p, q; };
    float v[4];
} __attribute__((aligned(16)));
typedef union _GLKVector4 GLKVector4; // 是一个共用体
#endif
GLK_INLINE GLKVector4 GLKVector4Make(float x, float y, float z, float w)
{
    GLKVector4 v = { x, y, z, w };
    return v;
}

3. 初始化数据

如果要使用 VBOs 最好在这里创建 VBOs 对象并绑定顶点数据,当然直接在关联数据一步做也没问题;

#define VertexBufferMemoryBlock    (1)

- (GLuint)createVBO {
    
    GLuint vertexBufferID;
    glGenBuffers(VertexBufferMemoryBlock, &vertexBufferID);
    
    return vertexBufferID;
    
}

#define PositionCoordinateCount      (3)

typedef struct {
    GLfloat position[PositionCoordinateCount];
} VFVertex;

static const VFVertex vertices[] = {
    {{-0.5f, -0.5f, 0.0}}, // lower left corner
    {{ 0.5f, -0.5f, 0.0}}, // lower right corner
    {{-0.5f,  0.5f, 0.0}}, // upper left corner
};

- (void)bindVertexDatasWithVertexBufferID:(GLuint)vertexBufferID {
    
    glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
    
    // 创建 资源 ( context )
    glBufferData(GL_ARRAY_BUFFER,   // 缓存块 类型
                 sizeof(vertices),  // 创建的 缓存块 尺寸
                 vertices,          // 要绑定的顶点数据
                 GL_STATIC_DRAW);   // 缓存块 用途
    
}
函数 描述
glGenBuffers 申请 VBOs 对象内存
glBindBuffer 绑定 VBOs 对象
glBufferData 关联顶点数据,并创建内存
glGenBuffers
void glGenBuffers (GLsizei n, GLuint * buffers)
n *指返回多少个 VBO *
buffers 指 VBO 的标识符内存地址
glBindBuffer
void glBindBuffer (GLenum target, GLuint buffer)
target 可以使用 GL_ARRAY_BUFFER 或 GL_ELEMENT_ARRAY_BUFFER
buffer 指 VBO 的标识符
glBufferData
void glBufferData(GLenum target, GLsizeiptr size, const void *data, GLenum usage)
target 可以使用 GL_ARRAY_BUFFER 或 GL_ELEMENT_ARRAY_BUFFER
size 字节单位,数据在内存中的大小(sizeof(...))
data 顶点数据的内存指针
usage 告诉程序怎么去使用这些顶点数据
usage
GL_STATIC_DRAW 程序只指定一次内存对象的数据(顶点数据),而且数据会被多次(非常频繁地)用于绘制图元。
GL_DYNAMIC_DRAW 程序不断地指定内存对象的数据(顶点数据),而且数据会被多次(非常频繁地)用于绘制图元。
GL_STREAM_DRAW 程序只指定一次内存对象的数据(顶点数据),而且数据会被数次(不确定几次)用于绘制图元。

glGenBuffers 、glBindBuffer、glBufferData 都干了什么?

  1. glGenBuffers 会在 OpenGL ES Context ( GPU )里面,申请一块指定大小的内存区;

  2. glBindBuffer 会把刚才申请的那一块内存声明为 GL_ARRAY_BUFFER ,就是以什么类型的内存来使用;

  3. glBufferData 把存放在程序内存的顶点数据 ( CPU 内存 ) 关联到刚才申请的内存区中;

注: 图片截自, RW. Beginning. OpenGL ES.and.GLKit Tutorials 教程;图片中的 “~~ 3) 拷贝顶点数据~~ ” 更正为 “ 3) 关联顶点数据 ”, 因为从 CPU 拷贝数据到 GPU 是在 OpenGL ES 触发绘制方法(后面会进到)的时候才会进行;

4. 配置 OpenGL ES Shader

  1. 编写 Vertex Shader Code 文件

a. 这是文件形式的,建议使用这种, Xcode 会进行关键字提示

#version 100

attribute vec4 v_Position;

void main(void) {
    gl_Position = v_Position;
}
a 对应的图片

b. 这是直接 GLchar * 字符串形式

+ (GLchar *)vertexShaderCode {
    return  "#version 100 \n"
            "attribute vec4 v_Position; \n"
            "void main(void) { \n"
                "gl_Position = v_Position;\n"
            "}";
}
b 对应的图片

非常明显地看出,a 不管编写和阅读都很轻松,而 b 就是一堆红,不知道是什么鬼,看久了眼睛会很累;

代码解释:
a. #version 100 ,首先 OpenGL ES 2 使用的 GLSL ES 版本是100, 这个没什么好解释的。《OpenGL ES 2 programming Guide》有提及


同时也说明了,我们编写 GLSL Code 的时候,要使用 《OpenGL ES Shading Language》的语言版本;

b. attribute vec4 v_Position;
b-1. attribute 存储类型限定符,表示链接,链接 OpenGL ES 的每一个顶点数据到顶点着色器(一个一个地);

注:

  1. attribute 只能定义 float, vec2, vec3, vec4, mat2, mat3,mat4 这几种类型的变量,不能是结构体或数组;
  2. 只能用在顶点着色器中,不能在片元着色器中使用,不然会编译错误;

补充:其它的存储类型限定符

限定符 描述
none (默认)表示本地的可读写的内存 输入的参数
const 表示编译期固定的内容 只读的函数参数
attribute 表示链接,链接 OpenGL ES 的每一个顶点数据到顶点着色器(一个一个地)
uniform 表示一旦正在被处理的时候就不能改变的变量,链接程序、OpenGL ES 、着色器的变量
varying 表示链接顶点着色器和片元着色器的内部数据

b-2. [ vec4 ],基本的数据类型,直接上图

注: 图片截自,OpenGL ES Shading Language 1.0 Quick Reference Card - Page 3

c. **gl_Position **内建变量
因为顶点数据里面


只是用到了 Position 顶点数据;

  1. 编写 Fragment Shader Code 文件

a. 文件形式

#version 100

void main(void) {
    gl_FragColor = vec4(1, 1, 1, 1); // 填充色,白色
}

b. 字符串形式

+ (GLchar *)fragmentShaderCode {
    return  "#version 100 \n"
            "void main(void) { \n"
                "gl_FragColor = vec4(1, 1, 1, 1); \n"
            "}";
}
  1. 配置 Vertex Shader
- (GLuint)createShaderWithType:(GLenum)type {
    
    GLuint shaderID = glCreateShader(type);
    
    const GLchar * code = (type == GL_VERTEX_SHADER) ? [[self class] vertexShaderCode] : [[self class] fragmentShaderCode];
    glShaderSource(shaderID,
                   ShaderMemoryBlock,
                   &code,
                   NULL);
    
    return shaderID;
}

- (void)compileVertexShaderWithShaderID:(GLuint)shaderID type:(GLenum)type {
    
    glCompileShader(shaderID);
    
    GLint compileStatus;
    glGetShaderiv(shaderID, GL_COMPILE_STATUS, &compileStatus);
    if (compileStatus == GL_FALSE) {
        GLint infoLength;
        glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &infoLength);
        if (infoLength > 0) {
            GLchar *infoLog = malloc(sizeof(GLchar) * infoLength);
            glGetShaderInfoLog(shaderID, infoLength, NULL, infoLog);
            NSLog(@"%s -> %s", (type == GL_VERTEX_SHADER) ? "vertex shader" : "fragment shader", infoLog);
            free(infoLog);
        }
    }
    
}
函数 描述
glCreateShader 创建一个着色器对象
glShaderSource 关联顶点、片元着色器的代码
glCompileShader 编译着色器代码
glGetShaderiv 获取着色器对象的相关信息
glGetShaderInfoLog 获取着色器的打印消息
glCreateShader
GLuint glCreateShader (GLenum type)
type 只能是 GL_VERTEX_SHADER、GL_FRAGMENT_SHADER中的一个
return GLuint 返回着色器的内存标识符
glShaderSource
void glShaderSource (GLuint shader, GLsizei count, const GLchar __ __ const* string, const GLint length)
shader 着色器的内存标识符
count 有多少块着色代码字符串资源
string 着色代码字符串首指针
length 着色代码字符串的长度
glCompileShader
void glCompileShader(GLuint shader)
shader 着色器的内存标识符
glGetShaderiv
void glGetShaderiv(GLuint shader, GLenum pname, GLint *params)
shader 着色器的内存标识符
pname 指定获取信息的类型,有 GL_COMPILE_STATUS、GL_DELETE_STATUS、GL_INFO_LOG_LENGTH、GL_SHADER_SOURCE_LENGTH、GL_SHADER_TYPE 五种
params 用于存储当前获取信息的变量内存地址
glGetShaderInfoLog
void glGetShaderInfoLog(GLuint shader, GLsizei maxLength, GLsei** *length, GLchar *infoLog)
shader 着色器的内存标识符
maxLength 指最大的信息长度
length *获取的信息长度,如果不知道可以是 NULL *
infoLog 存储信息的变量的内存地址
  1. 配置 Fragment Shader
    与 3) 方法一样;

  2. 创建 Shader Program

- (GLuint)createShaderProgram {
    
    return glCreateProgram();
    
}
函数 描述
glCreateProgram 创建 Shader Program 对象
glCreateProgram
GLuint glCreateProgram()
return GLuint 返回着色器程序的标识符
  1. 装载 Vertex Shader 和 Fragment Shader
- (void)attachShaderToProgram:(GLuint)programID vertextShader:(GLuint)vertexShaderID fragmentShader:(GLuint)fragmentShaderID {
    
    glAttachShader(programID, vertexShaderID);
    glAttachShader(programID, fragmentShaderID);
    
}
函数 描述
glAttachShader 装载 Shader 对象
glAttachShader
void glAttachShader(GLuint program, GLuint shader)
program 着色器程序的标识符
shader 要装载的着色器对象标识符
  1. 链接 Shader Program
- (void)linkProgramWithProgramID:(GLuint)programID {
    
    glLinkProgram(programID);
    
    GLint linkStatus;
    glGetProgramiv(programID, GL_LINK_STATUS, &linkStatus);
    if (linkStatus == GL_FALSE) {
        GLint infoLength;
        glGetProgramiv(programID, GL_INFO_LOG_LENGTH, &infoLength);
        if (infoLength > 0) {
            GLchar *infoLog = malloc(sizeof(GLchar) * infoLength);
            glGetProgramInfoLog(programID, infoLength, NULL, infoLog);
            NSLog(@"%s", infoLog);
            free(infoLog);
        }
    }
    
}
函数 描述
glLinkProgram 链接 Shader Program 对象
glGetProgramiv 获取 着色器程序的相关信息
glGetProgramInfoLog 获取 着色器程序的打印信息
glLinkProgram
void glLinkProgram(GLuint program)
program 着色器程序的标识符
glGetProgramiv
void glGetProgramiv(GLuint program, GLenum pname,GLint *params)
program 着色器程序的标识符
pname 可以选择的消息类型有如下几个,GL_ACTIVE_ATTRIBUTES、GL_ACTIVE_ATTRIBUTE_MAX_LENGTH、GL_ACTIVE_UNIFORMS、GL_ACTIVE_UNIFORM_MAX_LENGTH、GL_ATTACHED_SHADERS、GL_DELETE_STATUS、GL_INFO_LOG_LENGTH、GL_LINK_STATUS、GL_VALIDATE_STATUS
params 存储信息的变量的内存地址
glGetProgramInfoLog
void glGetProgramInfoLog(GLuint program,GLsizei maxLength, GLsizei** *length, GLchar *infoLog)
program 着色器程序的标识符
maxLength 指最大的信息长度
length *获取的信息长度,如果不知道可以是 NULL *
infoLog 存储信息的变量的内存地址

5.渲染绘制

  1. 清空旧渲染缓存
- (void)clearRenderBuffer {
    
    glClear(GL_COLOR_BUFFER_BIT);
    
}
函数 描述
glClear 清空 渲染缓存的旧内容
glClear
void glClear (GLbitfield mask)
mask 三者中的一个GL_COLOR_BUFFER_BIT ( 颜色缓存 ),GL_DEPTH_BUFFER_BIT ( 深度缓存 ), GL_STENCIL_BUFFER_BIT ( 模板缓存 )
  1. 设置渲染窗口
- (void)setRenderViewPortWithCGRect:(CGRect)rect {
    
    glViewport(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
    
}
函数 描述
glViewport 设置 渲染视窗的位置和尺寸
glViewport
void glViewport(GLint x, GLint y, GLsizei w, GLsizei h)
x,y 渲染窗口偏移屏幕坐标系左下角的像素个数
w,h 渲染窗口的宽高,其值必须要大于 0
  1. 使用 Shder Program
- (void)userShaderWithProgramID:(GLuint)programID {
    
    glUseProgram(programID);
    
}
函数 描述
glUseProgram 使用 Shader Program
glUseProgram
void glUseProgram(GLuint program)
program 着色器程序的标识符
  1. 关联数据
#define VertexAttributePosition     (0)
#define StrideCloser                (0)

- (void)attachTriangleVertexArrays {
    
    glEnableVertexAttribArray(VertexAttributePosition);
    
    if (self.vertexMode == VertexDataMode_VBO) {
        
        glVertexAttribPointer(VertexAttributePosition,
                              PositionCoordinateCount,
                              GL_FLOAT,
                              GL_FALSE,
                              sizeof(VFVertex),
                              (const GLvoid *) offsetof(VFVertex, position));
        
    } else {
    
        glVertexAttribPointer(VertexAttributePosition,
                              PositionCoordinateCount,
                              GL_FLOAT,
                              GL_FALSE,
                              StrideCloser,
                              vertices);
        
    }
    
}
函数 描述
glEnableVertexAttribArray 使能顶点数组数据
glVertexAttribPointer 关联顶点数据

a. 使能顶点缓存

glEnableVertexAttribArray
void glEnableVertexAttribArray(GLuint index)
index attribute 变量的下标,范围是[ 0, GL_MAX_VERTEX_ATTRIBS - 1]

b. 关联顶点数据

glVertexAttribPointer
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *ptr)
index attribute 变量的下标,范围是[ 0, GL_MAX_VERTEX_ATTRIBS - 1]
size *指顶点数组中,一个 attribute 元素变量的坐标分量是多少(如:position, 程序提供的就是 {x, y ,z} 点就是 3个坐标分量 ),范围是 [1, 4] *
type 数据的类型,只能是 GL_BYTE、GL_UNSIGNED_BYTE、GL_SHORT、GL_UNSIGNED_SHORT、GL_FLOAT、GL_FIXED、GL_HALF_FLOAT_OES *
normalized *指是否进行数据类型转换的意思,GL_TRUE 或 GL_FALSE *
stride *指每一个数据在内存中的偏移量,如果填 0(零) 就是每一个数据紧紧相挨着。 *
ptr 数据的内存首地址

知识扩展:

  1. 获取最大 attribute 下标的方法
GLint maxVertexAttribs; // n will be >= 8
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &maxVertexAttribs);
  1. 关于 size 补充

    注, 图片截自,《OpenGL ES 2 Programming Guide》第6章
  1. 使能顶点数组数据?
    其实顶点着色器中处理的数据有两种输入类型,CVOs ( Constant
    Vertex Objects )、VAOs ( Vertex Array Objects );
    glEnableVertexAttribArrayglDisableVertexAttribArray 函数就是使用 CVOs 还是 VAOs 的一组开关,看图 :

注: 图片截自,《OpenGL ES 2 Programming Guide》第6章

若使用了 CVOs 作为输入数据的,要使用以下处理函数来替代 glVertexAttribPointer 函数:


  1. OpenGL ES 只支持 float-pointer 类型的数据,所以才会有 normalized 参数;
  1. 顶点着色器的数据传递图,

    注: 图片截自,《OpenGL ES 2 Programming Guide》第6章

特别提醒,VBOs 只是一种为了加快数据访问和渲染调度的一种手段,而不是数据输入方式的一种;

强烈建议您去看一下 《OpenGL ES 2 Programming Guide》的 6. Vertex Attributes, Vertex Arrays, and Buffer Objects 这一章;

  1. 绘制图形
#define PositionStartIndex          (0)
#define DrawIndicesCount            (3)

- (void)drawTriangle {
    
    glDrawArrays(GL_TRIANGLES,
                 PositionStartIndex,
                 DrawIndicesCount);
    
}
函数 描述
glDrawArrays 绘制所有图元
glDrawArrays
void glDrawArrays(GLenum mode, GLint first, GLsizei count)
mode 绘制的图元方式,只能是 GL_POINTS、GL_LINES、GL_LINE_STRIP、GL_LINE_LOOP、GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN 的一种
first 从第几个顶点下标开始绘制
count 指有多少个顶点下标需要绘制
  1. 渲染图形
- (void)render {
    
    [self.context presentRenderbuffer:GL_RENDERBUFFER];
    
}
函数 描述
presentRenderbuffer: 把 Renderbuffer 的内容显示到窗口系统 ( CAEAGLLayer ) 中
presentRenderbuffer:
- (BOOL)presentRenderbuffer:(NSUInteger)target
target *只能是 GL_RENDERBUFFER *
return BOOL 返回是否绑定成功

补充:同时,这个函数也说明了kEAGLDrawablePropertyRetainedBacking 为什么要设为 YES 的原因:


如果要保存 Renderbuffer 的内容就要把 CARAGLLayer 的 drawableProperties 属性的 kEAGLDrawablePropertyRetainedBacking 设置为 YES 。

上面所有代码的工程文件, 在Github 上 DrawTriangle_OneStep


面向对象的重新设计:

消息处理的主流程就是上面的信号流程图的步序。
面向对象,就是把所有的消息交给对象来处理咯,关注的就是消息的传递和处理。【可以按照你的喜好来设计,反正可扩展性和可维护性都比较好就行了,当然也不能把消息的传递变得很复杂咯】

OpenGL ES 2 iOS 渲染逻辑流程图_面向对象化

项目文件结构:


完整代码在 Github 上 DrawTriangle_OOP


第四步,练练手

建议按照自己的思路重新写一个项目

1. 修改背景色

提示:glClear 函数

2.修改三角形的填充色:

提示:CVOs,三个顶点是统一的颜色数据

3. 修改三角形的三个顶点的颜色(填充色):

提示:VAOs / VBOs ,在三个顶点的基础上添加新的颜色数据

它们三个主要是为了 [ 学 + 习 ] 如何关联数据,对应的项目是:Github: DrawTriangle_OOP_Challenges_1

如果你发现文章有错误的地方,请在评论区指出,不胜感激!!!

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

推荐阅读更多精彩内容