OpenGL 入门 + 红宝书教程讲解

目录:

# OpenGL Introduction
## Laid down 3D world
# OpenGL Programming Guide 红宝书教程讲解
## Example 04 Color
## Example 05 Lighting
## Chapter 6 Blending, Antialiasing, Fog, Polygon Offset

Github 代码仓库

Torus Torus Torus
Torus
Torus
Torus
Teapot Teapot Teapot
-------- -------- --------
Teapot
Teapot
Teapot
开发环境:Sublime + CMake + MinGW

OpenGL Introduction

OpenGL - Open Graphics Library 开放图形库是一套用于渲染 2D、3D 矢量图形的跨语言、跨平台的 API - Application Programming Interface 应用程序编程接口规范,即 API Specification,提供近 350 个 API 函数,用来绘制从简单的图形比特到复杂的三维景象。和 Microsoft Windows 专用的 Direct3D API 接口一样,它们都是用来简化组图设备的软件开发的,在没有 OpenGL 这类图形接口前,不同设备上的绘图软件需要进行各种硬件上的适配,有了统一的接口后就不存在这个问题了,绘图图件只需要按 OpenGL 提供的接口来,硬件供应方负责驱动层的开发,与应用软件分离。OpenGL 这套 API 常用于 CAD、VR 虚拟现实、科学可视化程序和电子游戏开发。

OpenGL 本身并不是一个 API,目前它仅仅是一个由 Khronos 组织制定并维护的规范 Specification。

OpenGL 开发涉及到多个模块或概念:

  • GL - OpenGL 库是核心库,包含了最基本的 3D 函数。
  • GLES - OpenGL for Embedded Systems 用于嵌入式系统的精简版 OpenGL ES。
  • GLU - OpenGL Utility 实用库。
  • GLUT - OpenGL Utility Toolkit 最初是为红宝书开发的一个 OpenGL 窗口管理工具库,已不再维护,最后一版是 3.7.6 (Nov 8, 2001)。
  • FreeGLUT - 跨平台窗口和键盘、鼠标处理,API 是 GLUT API 的超集,同时也比 GLUT 更新、更稳定。
  • GLFW - 这是比 GLUT 更先进的库,管理窗口,读取输入,处理事件等。FW 可以理解为 Framework。
  • GLEW - Extension Wrangler 加载扩展是个体力活,交给这些库处理,GLAD 可以看作是升级版。
  • GLEE - OpenGL Easy Extension 扩展封闭库,提供 OpenGL 3.0 近 400 个扩展的便利加载。
  • GL3W - OpenGL 核心模式配置加载库,简化核心模式开发。
  • GLAD - 是继 GL3W,GLEW 之后,当前最新的用来访问 OpenGL 规范接口的第三方库。
  • GLM - OpenGL Mathematics 一个线性代数库,它经常和 OpenGL 一块使用。
  • GLFX - OpenGL Effect 效果库,依赖 GLEW。

OpenGL 程序需要运行在各种平台上,其上下文 Context 的创建过程相当复杂,在不同的操作系统上也需要不同的做法。因此很多游戏开发和用户界面库都提供了自动创建 OpenGL 上下文的功能,其中包括 SDL、Allegro、SFML、FLTK、Qt 等。也有一些库专门用来创建 OpenGL 窗口,其中最早的便是 GLUT,后被 FreeGLUT 取代,比较新的也有 GLFW 可以使用。

支持创建 OpenGL 窗口的还有一些多媒体库,它们集成了 OpenGL API 同时支持输入、声音等游戏程序所需要的基本功能:

  • Mesa3D - Brian Paul 开发的开源 OpenGL 规范实现,主要应用于 Linux。
  • Allegro 5 跨平台多媒体库,提供针对游戏开发的 C API。
  • SDL - Simple DirectMedia Layer 跨平台多媒体库,提供 C API,支持多种语言开发。
  • SFML - Simple and Fast Multimedia 跨平台多媒体库,提供 C++ API,支持多语言开发。
  • FLTK - 小型的跨平台 C++ 窗口组件库
  • Qt - 跨平台 C++ 窗口组件库,提供了许多 OpenGL 辅助对象。
  • wxWidgets - 跨平台 C++ 窗口组件库。

GLX - OpenGL Extension to the X Window System 扩展,是 X 协议和 X server 的一部分,已经包含在 X server 的代码中了。 GLX 提供了 x window system 使用的 OpenGL 接口,允许通过x调用OpenGL库。OpenGL 在使用时,需要与一个实际的窗口系统关联起来,在不同平台上有不同的机制以关联窗口系统,在 Windows 上是 WGL,在 Linux 上是 GLX,在 Apple OS 上是 AGL 等。 在 OpenGL ES 嵌入式平台上 EGL 就是 WGL、GLX、AGL 的等价物。EGL 假设 OS 会提供窗口系统,但 EGL 与平台无关,并不局限于任何特定的窗口系统。

既然,OpenGL 又快又跨平台,为何没有不在 PC 游戏中占领主导市场?

首先 API 设计反人类,全局状态机不符合人类直觉容易出错,如果你还需要用上多线程那就更酸爽了。其实,不同厂家的 GPU 驱动对 GL 的支持也不尽相同,即便声称支持。举个例子,在一台机子上的 GL 程序可以运行好好的,但一换显卡换可能就 Shader 报错了。

OpenGL 在 PC 上毛病多,PSO 创建和切换慢,还会破坏 GPU 并行,资源状态管理一点都不现代等等,相比之下 DX 能无缝多线程提交,精确控制资源状态和渲染队列,Device 操作还没有明显卡顿。至于 OpenGL 跨平台更是个笑话,但凡做过游戏的就知道手游已经和端游引擎开发根本就是两个次元甚至两个行业,想靠 GFX 实现跨平台是不是有点贪心。

当然 OpenGL 这些缺点并不能阻碍大神们做出工业级的代码和 3A 游戏,当然,想要实现图形编程,掌握一些数学知识,线性代数几何三角学是必需的。

Khronos 手上除了 OpenGL 外,在 2015 年还发布了 Vulkan,次世代 OpenGL,next generation OpenGL initiative,在 Vulkan 的 API 设计上很明显有以下几个特点:

  • 更依赖于程序自身的认知,让程序有更多的权限和责任自主的处理调度和优化,而不依赖于驱动尝试在后台的优化。程序开发者应该程序的最优化行为最为了解,传统图形 API 则靠驱动分析程序中调用 API 模式来揣测并且推断所有操作的优化方法。

  • 多线程友好。让程序尽可能的利用所有 CPU 计算资源从而提高性能。Vulkan 中不再需要依赖于绑定在某个线程上的 Context,而是用全新的基于 Queue 的方式向GPU递交任务,并且提供多种 Synchronization 的组件让多线程编程更加亲民。

  • 强调复用,从而减少开销。大多数 Vulkan API 的组件都可以高效的被复用。

Vulkan 才刚起步,其最大任务不是竞争 DirectX,而是取代 OpenGL。

最初的 GLUT - OpenGL Utility Toolkit library 是 Mark Kilgard 为 OpenGL RedBook 红宝书例程编写的一个工具模块,是一个与窗口系统无关的工具包。它为窗口的管理、事件处理、IO 控制和一些其他的设备管理提供了一个简单的 API。。然而,当时 GLUT 太好用了,大简化了 OpenGL 程序的开发,以至于成为了 OpenGL 标准模块一样,而 FreeGLUT 是其中一个较常用的版本,支持 MSVC 和 MinGW,现在开发中的版本是 FreeGLUT 3.0 可以直接下载编译好的文件。

GLUT 库包含以下功能:

  • 多 OpenGL 窗口渲染。
  • 回调驱动,事件处理。
  • 一个 idle 空闲例程和定时器。
  • 工具例程,产生实体或线框模型。
  • 支持位图和笔触字体。
  • 多功能窗口管理函数。

早期的 OpenGL 使用立即渲染模式 Immediate mode,也就是固定渲染管线,这个模式下绘制图形很方便。OpenGL 的大多数功能都被库隐藏起来,开发者很少能控制 OpenGL 计算的方式,而开发者又迫切希望能有更多的灵活性。

随着时间推移,规范越来越灵活,开发者对绘图细节有了更多的掌控。立即渲染模式确实容易使用和理解,但是效率太低。因此从 OpenGL 3.2 开始,规范文档开始废弃立即渲染模式,并鼓励开发者在 OpenGL 核心模式 Core-profile 下进行开发,这个分支的规范完全移除了旧的特性。

当使用 OpenGL 的核心模式时,就禁止使用已废弃的函数,OpenGL 会为此抛出一个错误并终止绘图。现代函数的优势是更高的灵活性和效率,然而也更难于学习,要求使用者理解 OpenGL 图形编程,它有一些难度。但是,为了获取更多的灵活性,更高的效率,更重要的是可以更深入的理解图形编程,这是值得的。

现今,更高版本的 OpenGL 4.5,但是,核心编程模式还在发挥作用。

OpenGL 的一大特性是扩展 Extension,当一个显卡公司提出一个新特性或者渲染上的大优化,通常会以扩展的方式在驱动中实现。
如果一个程序在支持这个扩展的显卡上运行,开发者可以使用这个扩展提供的一些更先进更有效的图形功能。通过这种方式,开发者不必等待一个新的 OpenGL 规范面世,就可以使用这些新的渲染特性,只需要简单地检查一下显卡是否支持此扩展:

if(GL_ARB_extension_name)
{
    // new feature
}
else
{
    // fallback
}

OpenGL 自身是一个巨大的状态机 State Machine,由一系列的变量描述 OpenGL 此刻应当如何运行。OpenGL 的状态通常被称为 OpenGL 上下文 Context。更改 OpenGL 状态一般操作:设置选项,操作缓冲,最后,使用当前 OpenGL 上下文来渲染。

假设,告诉 OpenGL 画线段而不是三角形,我们通过改变一些上下文变量来改变 OpenGL 状态,从而告诉 OpenGL 如何去绘图。一旦我们改变了 OpenGL 的状态为绘制线段,下一个绘制命令就会画出线段而不是三角形。

OpenGL 库是用 C 语言写的,由于 C 语言一些结构不易被翻译到其它的高级语言,因此 OpenGL 开发的时候引入了一些抽象层,对象 Object 就是其中一个。

在 OpenGL 中一个对象是指一些选项的集合,它代表 OpenGL 状态的一个子集。比如,我们可以用一个对象来代表绘图窗口的设置,之后我们就可以设置它的大小、支持的颜色位数等等。可以把对象看做一个 C 风格的结构体 Struct。

Laid down 3D world

总体来说,OpenGL 编程中,涉及三大部分的坐标关系处理,参考红宝书 OpenGL Programming Guide - 03 Viewing:

  • 模型坐标 Model Coordinates 涉及 Model Matrix 变换;
  • 世界坐标 World Coordinates 涉及 View Matrix 变换;
  • 相机坐标 Camera Coordinates 涉及 Projection Matrix 变换;

在 OpenGL 中使用 gluLookAt 改变的是相机坐标,而设置投影方式的 API 改变的是世界坐标系,即视口的变换,至于模型的坐标,顶点通过变换矩阵的转换后就改变了。

正交投影在 OpenGL 中是比较基础的投影变换,软件在算法上使用的 3D 世界,在成像的过程中,需要将立体的世界投射为设备显示的 2D 画面,这就需要一个投射方法。

常见且较容易实现的有透视法正交投影 Perspective & Orthographic Projection。透视法是西方艺术中基本的构图法,有固定的消逝点 Vanishing point or line,是一种模拟人类真视野中的观测物体,总结起来就是近大远小。明显的例子,就是平行铁轨,远处两条铁轨会相交于一点。而中国的山水法则不然,没有规则的消逝点或线,也不像正交投影,是一种在计算机上难以实现的构图方法。

正交投影通常用在工程制图中,需要比较精确的显示,而不是追求视觉的真实感。形象点说,一个物体特有点向投影平面作垂线,垂线与平面的交点的集合就是需要的投影。与透视法相比,正交投影法一般用于物体不会因为离屏幕的远近而产生大小的变换的情况。

这里的投影是向量的投影,几何的投影,算法实现上需要有一定的线性代数基础、几何学等。

OpenGL API 中的 glOrtho() 就是用来创建一个正交平行的视景体,它代表了一个变换矩阵,与 OpenGL 程序的当前矩阵相乘。而 glFrustum() 则对应一个透视投影的变换矩阵,gluPerspective() 函数封装了 glFrustum()

glOrtho 函数参数表示视景体六面的坐标约束,依次是 left、right、bottom、top,zNear 和
zFar 分别代表 Z 轴上的前后两面约束位置。这个函数简单理解起来,就是创建一个盒子摆在那里。

其中近裁剪平面是一个矩形,靠前方的近端面,矩形左下角三维空间坐标点是 (left,bottom,-near),
右上角坐标点是 (right,top,-near);远裁剪平面也是一个矩形,左下角点空间坐标是 (left,bottom,-far),右上角点是(right,top,-far)。

void glOrtho (GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar);

void glFrustum (GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar);

void gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear, GLdouble zFar);

假设有一个半径为 1 的球体,圆心坐标在 (0, 0, 0),那么,以下两个宽高都是 3 的正交投影盒子,分别会将球体:

glOrtho(-1.5, 1.5, -1.5, 1.5, -1.5, 1.5);
glOrtho( 0.0, 1.5, -1.5, 1.5, -1.5, 1.5);

当 left = right,或者 top = bottom,又或者 near = far,那么这个这个视景体至少有一个维度压缩为 0,这样无法显示任何图形。

( 2 right-left 0 0 tx 0 2 top-bottom 0 ty 0 0 -2 far-near tz 0 0 0 1 )
where

tx = - [right+left, right-left]
ty = - [top+bottom, top-bottom]
tz = - [far+near, far-near]

针对不同的变换,OpenGL 系统中有多个变换矩阵对应,即矩阵堆栈 Matrix Statck 保存的矩阵数据。所以,在执行矩阵变换前,需要通过 glMatrixMode 指定对什么矩阵进行操作:

  • GL_MODELVIEW 开始对模型视图矩阵堆栈操作,进入此模式后可以输出自己的物体模型。
  • GL_PROJECTION 开始对投影矩阵堆栈操作,进入此模式后可以为场景增加透视矩阵变换。
  • GL_TEXTURE 开始对纹理矩阵堆栈操作,进入此模式后可以为模型增加纹理贴图。
  • GL_COLOR 开始对色彩矩阵堆栈操作,可以变换色彩。

每个矩阵模式下都有一个矩阵堆栈,在 GL_MODELVIEW 模式中,堆栈深度至少为 32,在 GL_PROJECTIONGL_TEXTURE 模式中,堆栈深度至少为 2,无论在任何模式下,当前矩阵 Current Matrix 总是该模式下矩阵堆栈中的最顶层矩阵。

OpenGL 整个系统可以理解为一个有限状态机 State Machine,glMatrixMode() 所指定的模式就是在改变状态。一般而言,在需要绘制出对象或要对所绘制对象进行几何变换时,需要将变换矩阵设置成模型视图模式;而当需要对绘制的对象设置某种投影方式时,则需要将变换矩阵设置成投影模式;只有在进行纹理映射时,才需要将变换矩阵设置成纹理模式

操作矩阵时,常常需要 glLoadIdentity() 重置当前矩阵为单位矩阵,4 * 4 的单位矩阵来替换当前矩阵。也就是说,无论以前进行了多少次矩阵变换,在该命令执行后,当前矩阵均恢复成一个单位矩阵,即相当于没有进行任何矩阵变换状态。之后对矩阵的变换都相当于是针对模型是位于世界坐标原点位置处进行的。

无论模型如果变换,最终还是要在设备上显示,glViewport() 设置视口,中心点坐标 (x, y),宽度和高度 width、height,深度另外 API 设置。视口设置,即指定 OpenGL 3D 空间中,哪一部分输出到设备上显示出来。默认视口 在 Z 轴向其负轴方向看。对于显示器坐标系统 Screen
Coordinate 来说,左上角为原点,而且 Y 轴上下颠倒。那么,对于 OpenGL 空间位置坐标为 (-1, 1) 的一个物体,假定视口长宽为 2,它会显示在屏幕的左上角,也就显示器的 (0,0) 的坐标,但是在软件中编码,不去考虑屏幕的坐标,因为视口背后的逻辑已经进行了一个矩阵变换操作,将屏幕的坐标系统映射到了视口中。

void glViewport(GLint x, GLint y, GLsizei width, GLsizei height);
void glDepthRange(GLdouble nearVal​, GLdouble farVal​);
void glDepthRangef(GLfloat nearVal​, GLfloat farVal​);

而通过 glViewport 指定的视口区域代表的是一个仿射变换,affine transformation,只是将设备的坐标映射到了软件中 3D 世界的一个剖面上,函数参数 (x, y) 就在指定平移变换,width、height 就是指定大小缩放变换。

OpenGL 中存在多个视口,保存在 Viewport 数组中,视口数量为 [0, GL_MAX_VIEWPORTS),并且给特定图元使用的视口还可以通过 GS - Geometry Shader 指定。如果 GS 没有指定视口,那么数组中开头那个就是默认的。

其它和视口相关的 API 如下:

void glViewportIndexedf(GLuint index​, GLfloat x​, GLfloat y​, GLfloat w​, GLfloat h​)
void glViewportIndexedfv(GLuint index​, const GLfloat *v​)
void glDepthRangeIndexed(GLuint index​, GLdouble nearVal​, GLdouble farVal​)

void glViewportArrayv(GLuint first​, GLsizei count​, const GLfloat *v​)
void glDepthRangeArrayv(GLuint first​, GLsizei count​, const GLdouble *v​)

在 FreeGLUT 的 CallbackMaker 示例中,展示了调用 glutBitmapString 在窗体上绘制文字,需要结合 API 指定光栅化位置:

glRasterPos2i ( 10, glutGet ( GLUT_WINDOW_HEIGHT ) - 20 );  /* 10pt margin above 10pt letters */
glutBitmapString(void *font, const unsigned char *string); 

光栅坐标设置 API 有 2i2f2d2dv 等后缀形式,i - integer、f - float、d-double,带 v 的表示用一个值表示和个坐标分量,这是二维坐标,还有 3 维坐标,4 维坐标的对应 API。这些 API 设置的是 OpenGL 世界坐标,会受到投影变换和视景体裁剪的影响,也就是说,光栅位置设置的世界坐标,是在经过 ModelView,Projection,和 ViewPort 等变换的基础上设置的。如果坐标没有在视景体之内,你可以理解为 camera 看不见,那么这个坐标是无效的,底层的 glDrawPixel 也不会进行绘制。glWindowPos() 也可以指定光栅位置,它使用窗口坐标,不进行矩阵变换、裁剪、或纹理坐标生成。

你可以通过 glGet 来获取当前光栅坐标是否有效:

glGetBooleanv(GL_CURRENT_RASTER_POSITION_VALID, &if_valid);

OpenGL 世界空间的图像投影到屏幕后,就有一个转换关系,可以利用这个关系来反向求解屏幕上的点对应的 3D 空间坐标,矩阵和模型视图矩阵变换非常复杂,可以利用以下 API:

  • glWindowPos 函数允许你通过设置屏幕坐标来改变光栅坐标,但需 GLEW 库。
  • glUnProject 计算出屏幕坐标对应的世界坐标。

在 OpenGL 对视图变换并不会改变模型坐标,而是移动或缩放摄像机的镜头,从不同的方位观察模型,常用glLookAt 函数设置观察者视角的变换矩阵。

void gluLookAt( GLdouble eyeX,
                GLdouble eyeY,
                GLdouble eyeZ,
                GLdouble centerX,
                GLdouble centerY,
                GLdouble centerZ,
                GLdouble upX,
                GLdouble upY,
                GLdouble upZ);

glLookAt 会定义一个视图矩阵,成像点为 (eyeX, eyeY, eyeZ),目标物体对应 center 坐标方向,镜像上位方向对应 up 坐标,这三点的坐标确定了的的摄像机的姿态。视图变换矩阵与当前矩阵相乘,获得视图矩阵,再该模型视图矩阵左乘模型,就会获得在观察位置上模型的状态,就是我们在屏幕上最终看到的模型的状态。

gluLookAt 用来定义观察者(相机)的状态,包括观察者在世界坐标系中所处的位置、看向世界坐标系中的方向,可以理解为眼睛所看向的方向、观察者头部的朝向(可以在一个平面上360°旋转)。

如果没有调用 glLookAt 设置视图矩阵,默认情况下,相机会被设置为位置在世界坐标系原点,指向 Z 轴负方向,朝上向量为(0,1,0)。

在 2D 和 3D 系统中,有三个基本坐标系统,2D 的笛卡尔坐标系就是一个十字坐标系,而 3D 空间中,任意给定 X 和 Y 轴,对于不同朝向的 Z 轴分为左手系 left-handed 和右手系 right-handed。

始终需要明确的一点是 OpenGL 世界坐标系是右手坐标系 right-hand system ,在二维屏幕上,屏幕水平方向是 X 轴方向,向右为正,屏幕竖起方向是 Y 轴方向,向上为正,垂直于屏幕的方向是 Z 轴方向,从屏幕里往外为正。即右手中指向自己表示 Z 轴、食指竖起向上表示 Y 轴、母指向右表示 X 轴。即使手腕怎么转动,右手系统这种轴向关系是主要的参考。

  • OpenGL 使用右手系,默认窗体中心为原点,左下角为负,右上角为正;
  • 屏幕鼠标的 2D 坐标左上角为原点,右正角为正;

所以,在 OpenGL 空间的坐标处理第一个问题就是屏幕 2D 坐标到 OpenGL 的坐标变换,也就是 Window 到 ViewPort 的坐标统一。从上面的条件可知,屏幕的坐标其实就是视口坐标按 X 轴反转再平移到右上角。将窗口坐标还原操作就是,scale(1, -1, 1),translate(-1, -1, 0)。

对于一个给定的点,可以通过一个矩阵变换将其旋转和平移。OpenGL 已经提供 API 做这些简单的变换,在 API 内部设置好基本的矩阵,只需要通过 API 输入主要参数即可以,例如,给 glRotatef 指定旋转角度和旋转轴,注意后缀 f 表示 float,当然还在 d 表示 double:

  • glRotatef (GLfloat angle, GLfloat x, GLfloat y, GLfloat z);
  • glScalef (GLfloat x, GLfloat y, GLfloat z);
  • glTranslatef (GLfloat x, GLfloat y, GLfloat z);

旋转 API 中的 x, y, z 为对应轴的布尔值,表示按此轴旋转。想象一下,从坐标(0,0,0)即原点,引出一条线到(1,0,0),这条线就是旋转轴,用右手握住这条线,右手大拇指指向和旋转轴同方向,四个手指的弯曲指向即是物体旋转方向。

glTranslatef(-1, -1, 0) 为例,它表示 OpenGL 将坐标从原点,即窗口中心位置向左下移动 1 个单位。那么新的图形绘制就按新的坐标原点为参考进行绘图,即以左下角 1 个单位的位置为原点。但是,只要视口没有进行变换,那么程序在窗口渲染的图像还是按坐标变换前的位置显示。

OpenGL 渲染管线中涉及变换的对象有很多,模型变换 ModelView Transformation 和投影变换 Projection Transformation 是比较常用到的,前面已经讲到通过 glMatrixMode 可以切换变模式。而模型,即 GL_MODELVIEW 模式,就是默认的变换操作对象,后面还要进行 GL_PROJECTION 对视口进行变换。搭配使用的还有 glPushMatrix()glPopMatrix() 这两个函数,前者将当前的矩阵压栈,相当做了一个备份,后者将备份的矩阵还原回来。注意了,无论是什么模式下的矩阵变换,当前矩阵总是当前模式的栈顶那一个矩阵。

三维坐标系两种习俗,左手坐标系和右手坐标系,它们的重点不是在于 z 轴标注的是哪根,而是三个方向的组合。

左手系统 left-hand system 中指向右表示 X 轴,食指向前表示 Z 轴,拇指竖起向上表示 Y 轴。同样,手腕的转动并不影响三轴的组合关系,和右手系统是两个不同的组合。因为,无论如何转动手腕,两系统的三轴方向不可能保持三轴一致,最多只能两轴一致,余下一轴相反方向。

右手系在向量叉乘 Cross product 中还有个妙用,设拇指和食指是两个垂直的向量 ab,那么 a x b 结果就在中指的方向上并且三指垂直。换句话说,将两向量起点放在一起,那么一向量和它左侧的向量做叉乘,结果就和两个向量构成的平面垂直且指向观察者。

长度 Length 就是距离,对于一个空间点到原点的距离就是 sqrt( x² + y² + z² )。

向量点积 Dot product 也叫内积 inner product,将 b 归一化为长度为 1 的单位向量,那么 a · b = length(A)cos(θ),即 ab 上的投影长度,计算时,通常是将各轴分开相乘再相加,A.xB.x +A.yB.y +A.zB.z。 当夹角为 90° 投影为 0,当夹角为 0° 即等于 a 的模。

点积还具有对称性 symmetry,在线性代数的本质 Essence of linear algebra 系列视频中,用动画很好地解析了这一点。

假设 u · v,对称性即无论是 vu 上作投影再乘,还是 uv 上作投影再乘,结果都是一样的。
这一点不太好理解,但又至关重要。

向量相加 Addition a + b 几何意义就是沿着一个向量方向移动其长度,再继续沿着其它向量做相同的移动,最后,原点指向最终移动到的位置就是结果的向量。计算按各轴分别相加,(sum(x),sum(y),sum(z))

向量相减 Substraction a - b 的几何意义就是由 b 点指向 a 点的向量。换个说法,向量 a 减去在左侧的向量 b,结果就会出现在 a 的右侧,因为负号对于向量就是反转方向再相加。

向量加减问题和平行四边形关联的,两个向量作为平行四边形的边,加减的结果就是对角线,相加取原点出发的对角线。

3D 中用 4x4 的矩阵表示一个变换,这个矩阵有能力表征所有需要的变化,这样为了向量乘矩阵在数学上的正确性,向量必须是 4 维向量。一个三维向量坐标 [x y z] 表示,而一个齐次坐标 Homogeneous Coordinate 的 3D 向量用一个四维坐标 [x y z w]表示时,这就称为齐次坐标表示方法。在齐次坐标中,最后一维坐标 w 称为比例因子。

在 OpenGL 二维坐标点当作三维坐标点,所有点都用齐次坐标来描述,统一作为三维齐次点来处理。齐次点具有下列几个性质:

  • w=0 是一个方向,齐次点 (x, y, z, 0) 表示此点位于某方向的无穷远处。
  • w=1 是一个位置,三维空间点 (x, y, z) 和二维平面点 (x,y) 的齐次坐标分别为(x, y, z, 1)、(x, y, 0, 1)。
  • 其它值,可能正确,最好知道是什么作用。齐次点坐标 (x, y, z, w) 即三维空间点坐标 (x/w, y/w, z/w)。

在线性几何中,连续相乘的变换矩阵可以理解为图形变换的叠加。假设当前矩阵为单位矩阵,然后先乘以一个表示旋转的矩阵 R,再乘以一个表示移动的矩阵 T,最后得到的矩阵再乘上每一个顶点的坐标矩阵 v。经过变换得到的顶点坐标就是 (RT)v。按矩阵乘法的结合率,(RT)v = R(Tv),换句话说,实际上是先进行移动,然后进行旋转。即:实际变换的顺序与代码中写的顺序是相反的。由于先移动后旋转和先旋转后移动得到的结果很可能不同,初学的时候需要特别注意这一点。OpenGL 之所以这样设计,是为了得到更高的效率。

为了直观感受这些基础内容,需要祭出 OpenGL 中经典的茶壶神仙:

  • glutSolidTeapot() 实体 3D 茶壶模型。
  • glutWireTeapot() 线框 3D 茶壶模型。

GLUT API 中,提供演示用的模型除了茶壶,共有 9 种常见的几何体,如 Cube、Cone 等,它们在各种 3D 建模软件中都会出现。

  • Cone 圆锥体
  • Tetrahedron 四面体
  • Cube 正方体
  • Dodecahedron 正十二面体
  • Icosahedron 正二十面体
  • Octahedron 正八面体
  • Sphere 球体
  • Torus 圆环体
  • Teapot 茶壶

代码修改自红宝书示例,完整代码见代码仓库的 Demos:

// OpenGL Programming Guide Example 3-1 : Transformed Cube: cube.c

#include <iostream>
#include <cstdarg>
#include <freeglut.h>

using namespace std;

/*
 Key set
 1 - 9 models 
     1 Cone
     2 Cube
     3 Dodecahedron
     4 Icosahedron
     5 Octahedron
     6 Sphere
     7 Teapot
     8 Tetrahedron
     9 Torus
 f/g solid/wired
 a - left
 s - down
 d - right
 w - up
 j - forward, mouse wheel
 k - backward, mouse wheel
 l - left view
 r - right view
 t - top view
 b - back view
*/

OpenGL Programming Guide 红宝书教程讲解

红宝书 8、9 版本代码心 MSVC 编译,如果使用 CMake + MinGW-x64 GCC 8.1.0 编译会有兼容性问题。

  • ✗ 问题一

代码中使用了 GetTickCount64

#if (_WIN32_WINNT >= 0x0600) 
//...
WINBASEAPI ULONGLONG WINAPI GetTickCount64(void);

#endif 

所以,遇到符号未定义请设置 CMake 编译器参数,::GetTickCount64' has not been declared

  • ✗ 问题二

代码 vdds.cpp 使用了 goto 导致 GCC 兼容性错误,error: jump to label 'xxx' [-fpermissive]

void vglLoadDDS(const char* filename, vglImageData* image)
{
    ...
    if (file_header.magic != DDS_MAGIC)
    {
        goto done_close_file;
    }
    size_t current_pos = ftell(f);
    ...

done_close_file:
    fclose(f);
}

可以直接使用宽容模式 -fpermissive 编译选项忽略这些问题,又或者将 goto 后面的变量初始化移到 goto 前面。

  • ✗ 问题三

在 12-particlesimulator.cpp 中使用了一个 STRINGIZE 宏来定义着色器程序代码,导致错误的语法 #version 430,。

#define STRINGIZE(a) #a

此宏定义 #a 原意是给 a 加双引号变成字符串,解决办法是给着色器程序部分加双引号,并将换行符号转义:

static const char compute_shader_source[] = STRINGIZE("\
    #version 430 core\n\
    \n\
    layout (std140, binding = 0) uniform attractor_block\n\
    ..."
    );
  • ✓ 解决办法

给 CMakeLists.txt 脚本设置编译器条件,同时示例依赖的 GLFW 也需要编译,生成的 libglfw3.alibvermilion.a 根据 CMake 编译目录而定,一并设置到链接目录中:

LINK_DIRECTORIES( ${CMAKE_SOURCE_DIR}/build/lib )

message(STATUS "Platform is ----------------> ${CMAKE_SYSTEM_NAME}")
IF (${CMAKE_SYSTEM_NAME} MATCHES "Windows")
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++0x -D_WIN32_WINNT=0x0600 -fpermissive")
ENDIF (${CMAKE_SYSTEM_NAME} MATCHES "Windows")

Example 04 Color

Example 4-1 : Drawing a Smooth-Shaded Triangle: smooth.c

OpenGL 程序初始化时设置一个颜色模式和着色模式:

glutInitDisplayMode (GLUT_SINGLE | GLUT_INDEX);

glShadeModel (mode? GL_FLAT:GL_SMOOTH);

#define  GLUT_RGB                           0x0000
#define  GLUT_RGBA                          0x0000
#define  GLUT_INDEX                         0x0001

彩色可以是 RGB 或 RGBA 模式,也可以是索引颜色。当图形中使用的颜色是有限的数量时,索引颜色能节省内存,原先需要 32bit 的 RBGA 可以用一个索引号码和一个色盘定义就可以替代。

在索引色模式下,OpenGL 使用 glIndexi() 这类方法进行色彩映射或称为 lookup table 来确定颜色,这非常像使用色盘。索引色的使用可以参考 GLUT 中提供的示例 olympic.c 奥林匹克五环,scube.c 旋转盒子。

根据不同数据类型,色彩指定 API 使用相应后缀:

void glColor3{b s i f d ub us ui} (TYPEr, TYPEg, TYPEb);
void glColor4{b s i f d ub us ui} (TYPEr, TYPEg, TYPEb, TYPEa);
void glColor3{b s i f d ub us ui}v (const TYPE*v);
void glColor4{b s i f d ub us ui}v (const TYPE*v);

void glIndex{sifd ub}(TYPE c);
void glIndex{sifd ub}v(const TYPE *c);

索引色管理 API,如下,获取索引色分量或设置索引色,以及获取当前系统索引色数量:

GLfloat glutGetColor(int cell, int component);
void glutSetColor(int cell, GLfloat red, GLfloat green, GLfloat blue);
glutGet(GLUT_WINDOW_COLORMAP_SIZE);
void glutCopyColormap(int win);

参数:

  • cell 表示索引色单元位置,[0 - GLUT_WINDOW_COLORMAP_SIZE)。
  • component 指定分量 GLUT_RED, GLUT_GREEN, or GLUT_BLUE。
  • win 指定要从哪个窗体拷贝色彩映射 colormap。

如果获取索引色大小返回以下信息,表明 GLUT 的实现没有包含此功能,测试当前 Windows 10 运行的程序有 19 种索引色:

glutGet(): missing enum handle 119

设置颜色后,可以通过 glGetIntegerv 获取和分量值 GL_RED_BITS, GL_GREEN_BITS, GL_BLUE_BITS, GL_ALPHA_BITS, GL_INDEX_BITS:

glColor3f (1.0, 0.0, 0.0);  /* the current RGB color is red: */
                            /* full red, no green, no blue. */
glBegin (GL_POINTS);
    glVertex3fv (point_array);
glEnd ();

glGetIntegerv(GL_INDEX_BITS);

通过激活抖动,可以用黑色散点模拟灰度,黑点越密集,抖动形成的灰度色越深:

glEnable(GL_DITHER);
....
glDisable(GL_DITHER);

Table 4.1 颜色值转换浮点表示的对应关系:

Suffix Data Type Minimum Value Min Value Maps to Maximum Value Max Value Maps to
b 1-byte integer -128 -1.0 127 1.0
s 2-byte integer -32,768 -1.0 32,767 1.0
i 4-byte integer -2,147,483,648 -1.0 2,147,483,647 1.0
ub unsigned 1-byte integer 0 0.0 255 1.0
us unsigned 2-byte integer 0 0.0 65,535 1.0
ui unsigned 4-byte integer 0 0.0 4,294,967,295 1.0

在 GL_SMOOTH 着色模式,每个顶点对应颜色会平滑地应用到片元上,在顶点构成的片元空间以渐变色填充;而使用 GL_FLAT 平铺模式,假设几何图形由 n 个三角形构成,则片元空间只会使用顶点颜色数组中最后一个颜色进行着色。

Table 4-2 在各种 Flat-Shaded 多边形中 OpenGL 选择颜色的方式:

多边形类型 用于选择颜色的顶点
single polygon 1
triangle strip i+2
triangle fan i+2
independent triangle 3i
quad strip 2i+2
independent quad 4i

Example 05 Lighting

本示例展示:

  • OpenGL 如果模拟真实世界的光照。
  • 定义光源,照亮模型。
  • 定义材质属性以反映光照。
  • 操纵矩阵堆栈控制光源。

前面讲解模型的彩色使用,但是眼睛看到的彩色并非在完全来自模型,而是光线与模型的材质相互作用下产生的视觉感官效应。光线在物理上可以看作电磁波,不同的波长具有不同的颜色效应,面物体的材质会影响电磁波的形成最终的视觉效果。比如,同样的绿色植物,在白天看起来是绿色的,晚上用其它彩色灯光照射,却会显示不同的颜色。而同样的绿色玻璃,在同样的光线条件下,也与绿色植物看起来不一样,这是材质决定的。

材质影响视觉效果的属性有很多,其中材质颜色属性 Material Colors 是基本的一个,材质的透射率、折射率都会影响视觉效果。透射 Transmission 相关属性对金属无效,对于不透明的材料 Transmission = 0,那么折射率 IOR - Index of Refraction 就是无效属性。物体的粗糙度对光反射有很大的影响,但是 Fresnel 效应一直存在,它会在即使是没有镜面反射的木球也会表现出周边更亮。

而光线与模型接触点是很关键的,按照光的直线传播原理,接触面的法线方向 Normal Vectors 决定的光的反射方向,折射率和入射角共同决定折射方向。

真实世界中,不存在唯一光源,白天除了太阳光,还大量的散射光,透射光。环境光 Ambient, 散射光 Diffuse, 反射光 Specular Light 是 OpenGL 三类主要的模拟光源属性,这决定了模型的材质是否产生效果。

可以参考 PBR - Physically Based Rendering 中对物理学上的光线处理模型。

在 OpenGL 中,基本每次重绘都需要清理画板中旧的图形,不可能看到无限远的物体,只能通过清除方法将这些物休的颜色信息清除:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

OpenGL 抽象的相机在成像过程中,使用到 3D 空间模型的深度信息 Z,它是模型到镜头的距离,如果要理解为镜头坐标的 Z 轴也可以。在决定是否绘制一个物体的表面时,首先将表面对应像素的深度值与当前深度缓冲区中的值进行比较,如果大于等于深度缓冲区中值,则丢弃这部分。否则利用这个像素对应的深度值和颜色值,分别更新深度缓冲区和颜色缓冲区。这一过程称之为深度测试 Depth Testing。

尝试测试的方式可以通过以下 API 设置:

void glDepthFunc(GLenum func);

glEnable (GL_DEPTH_TEST);
glDisable (GL_DEPTH_TEST);

指定 func 为以下测试比较方式:

测试比较方式 说明
GL_NEVER 总是不通过,总是不绘制。
GL_LESS 测试深度小于保存的深度值时通过。
GL_EQUAL 测试深度等于保存的深度值时通过。
GL_LEQUAL 测试深度小于或等于保存的深度值时通过。
GL_GREATER 测试深度大于保存的深度值时通过。
GL_NOTEQUAL 测试深度不等于保存的深度值时通过。
GL_GEQUAL 测试深度大于或等于保存的深度值时通过。
GL_ALWAYS 总是通过测试,问题绘制。

在绘制 3D 场景的模型还需要决定哪些部分是对观察者可见的,或者哪些部分是对观察者不可见的。对于不可见的部分,对于一个不透明的盒子,同时最多只能看到三个面,其它面被遮挡就不应该渲染,这种情况叫做隐藏面消除 Hidden surface elimination。

glEnalbe(GL_CULL_FACE);
glDisable(GL_CULL_FACE);

void glCullFace(GLenum mode);
void glFrontFace(int mode );

剔除模式 mode:

剔除方式 说明
GL_BACK 只剔除背向面,默认
GL_FRONT 只剔除正向面
GL_FRONT_AND_BACK 剔除正向面和背向面

除了需要剔除的面之外,glFrontFace 方法告诉 OpenGL 如何判断正向面:

判断模式 说明
GL_CCW 代表逆时针方向为正向面,默认
GL_CW 代表顺时针方向为正向面

默认设置,假定一个三角形,握住右手四指逆时针方向放在三个顶点对应的绘画顺序上,那么拇指指向的面为正向面。

在 OpenGL 内部定义了 8 个光源属性组 GL_LIGHT0, GL_LIGHT1 ... GL_LIGHT7

glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);

创建光源时要在 light 参数指定光源属于那一组,还有灯光其它属性:

void glLight{if}(GLenum light, GLenum pname, TYPEparam);
void glLight{if}v(GLenum light, GLenum pname, TYPE *param);

Table 5-1 pname 参数指定的光源默认值

Parameter Name Default Value Meaning
GL_AMBIENT (0.0, 0.0, 0.0, 1.0) ambient RGBA intensity of light
GL_DIFFUSE (1.0, 1.0, 1.0, 1.0) diffuse RGBA intensity of light
GL_SPECULAR (1.0, 1.0, 1.0, 1.0) specular RGBA intensity of light
GL_POSITION (0.0, 0.0, 1.0, 0.0) (x, y, z, w) position of light
GL_SPOT_DIRECTION (0.0, 0.0, -1.0) (x, y, z) direction of spotlight
GL_SPOT_EXPONENT 0.0 spotlight exponent
GL_SPOT_CUTOFF 180.0 spotlight cutoff angle
GL_CONSTANT_ATTENUATION 1.0 constant attenuation factor
GL_LINEAR_ATTENUATION 0.0 linear attenuation factor
GL_QUADRATIC_ATTENUATION 0.0 quadratic attenuation factor

其中 GL_DIFFUSE、GL_SPECULAR 指定的默认值只应用于 GL_LIGHT0,其它光源属性 GL_DIFFUSE、GL_SPECULAR 默认值是 (0.0, 0.0, 0.0, 1.0)。

可以在初始化阶段指定光源,也可以在重绘事件中指定:

static void initLights()
{
    glClearColor(0.0f, 0.0f, 0.7f, 1.0f);

    GLfloat ambientLight[]  = {0.2f,  0.2f,  0.2f,  1.0f};//环境光源
    GLfloat diffuseLight[]  = {0.9f,  0.9f,  0.9f,  1.0f};//漫反射光源
    GLfloat specularLight[] = {1.0f,  1.0f,  1.0f,  1.0f};//镜面光源
    GLfloat lightPos[]      = {50.0f, 80.0f, 60.0f, 1.0f};//光源位置
 
    glEnable(GL_LIGHTING);                                //启用光照
    glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);       //设置环境光源
    glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);       //设置漫反射光源
    glLightfv(GL_LIGHT0, GL_SPECULAR, specularLight);     //设置镜面光源
    glLightfv(GL_LIGHT0, GL_POSITION, lightPos);          //设置灯光位置
    glEnable(GL_LIGHT0);                                  //打开第一个灯光
 
    glEnable(GL_COLOR_MATERIAL);                          //启用材质的颜色跟踪
    glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);    //指定材料着色的面
    glMaterialfv(GL_FRONT, GL_SPECULAR, specularLight);   //指定材料对镜面光的反应
    glMateriali(GL_FRONT, GL_SHININESS, 100);             //指定反射系数
}

有了光源后,就可以为模型指定材质属性:

void glMaterialf(   GLenum face, GLenum pname, GLfloat param);
void glMateriali(   GLenum face, GLenum pname, GLint param);
void glMaterialfv(  GLenum face, GLenum pname, const GLfloat * params);
void glMaterialiv(  GLenum face, GLenum pname, const GLint * params);

face 可以设置 GL_FRONT, GL_BACK, GL_FRONT_AND_BACK。

pname 参数设置参考,整形或者浮点值都可以,整形会被映射到 [-1, 1]:

pname 值 对应 params 值
GL_AMBIENT 设置材质对环境光的作用,默认值 (0.2, 0.2, 0.2, 1.0)
GL_DIFFUSE 材质对散射光的作用,默认值 (0.8, 0.8, 0.8, 1.0)
GL_SPECULAR 设置一个高光值,默认值为 (0, 0, 0, 1)
GL_EMISSION 设置一个自发光值,默认初始值为 (0, 0, 0, 1)
GL_SHININESS 设置一个亮度,默认前后面的值为 0。
GL_AMBIENT_AND_DIFFUSE 相当调用两次 API
GL_COLOR_INDEXES 索引色默认值 (0,1,1),对应 ambient, diffuse, specular 索引值

材质索引色示例:

GLfloat mat_colormap[] = { 16.0, 47.0, 79.0 };
glMaterialfv(GL_FRONT, GL_COLOR_INDEXES, mat_colormap);

还可以设置灯光模型:

void glLightModelf( GLenum pname, GLfloat param);
void glLightModeli( GLenum pname, GLint param);

void glLightModelfv( GLenum pname, const GLfloat * params);
void glLightModeliv( GLenum pname, const GLint * params);

pname 和 params 参数参考:

pname 对应 params
GL_LIGHT_MODEL_AMBIENT 默认值 (0.2, 0.2, 0.2, 1.0)
GL_LIGHT_MODEL_COLOR_CONTROL GL_SEPARATE_SPECULAR_COLOR 或 GL_SINGLE_COLOR
GL_LIGHT_MODEL_LOCAL_VIEWER 默认值 0 表示平行 X 轴的光
GL_LIGHT_MODEL_TWO_SIDE 默认值 0 表示前单独光照,否则双面光照

索引色光照的数学 Color-Index Mode Lighting

As you might expect, since the allowable parameters are different for color-index mode than for RGBA mode, the calculations are different as well. Since there's no material emission and no ambient light, the only terms of interest from the RGBA equations are the diffuse and specular contributions from the light sources and the shininess. Even these need to be modified, however, as explained next.

Begin with the diffuse and specular terms from the RGBA equations. In the diffuse term, instead of diffuselight * diffusematerial, substitute dci as defined in the previous section for color-index mode. Similarly, in the specular term, instead of specularlight * specularmaterial, use sci as defined in the previous section. (Calculate the attenuation, spotlight effect, and all other components of these terms as before.) Call these modified diffuse and specular terms d and s, respectively. Now let s' = min{ s, 1 }, and then compute

c = am + d(1-s')(dm-am) + s'(sm-am)

where am, dm, and sm are the ambient, diffuse, and specular material indexes specified using GL_COLOR_INDEXES. The final color index is

c' = min { c, sm }

After lighting calculations are performed, the color-index values are converted to fixed-point (with an unspecified number of bits to the right of the binary point). Then the integer portion is masked (bitwise ANDed) with 2n-1, where n is the number of bits in a color in the color-index buffer.

Chapter 6 Blending, Antialiasing, Fog, Polygon Offset

章节学习目标:

  • 使用色彩混合 Blend 实现半透明效果;
  • 使用抗锯齿平滑边线和多边形;
  • 使用雾化实现大气模糊效果;
  • 在指定深度绘制避免几何体交叠产生不真实感,unaesthetic artifacts;

混合涉及来源和目标两个数据,还有对应的系数,(S_r, S_g, S_b, S_a)(D_r, D_g, D_b, D_a),每个分量值范围 [0,1],混合结果可以这样表示:

(R_s S_r+R_d D_r, G_s S_g+G_d D_g, B_s S_b+B_d D_b, A_s S_a+A_d D_a)

激活混合模式以及设置混合系数:

glEnable(GL_BLEND); 
glDisable(GL_BLEND);

void glBlendFunc(GLenum sfactor, GLenum dfactor);

禁止混合和设置 glBlendFunc(GL_ONE, GL_ZERO); 是等效的,这也是默认的设置。

Table 6-1 Source & Destination 混合因数的计算,加减号表示对应分量相加减:

Constant Relevant Factor Computed Blend Factor
GL_ZERO source or destination (0, 0, 0, 0)
GL_ONE source or destination (1, 1, 1, 1)
GL_DST_COLOR source (Rd, Gd, Bd, Ad)
GL_SRC_COLOR destination (Rs, Gs, Bs, As)
GL_ONE_MINUS_DST_COLOR source (1, 1, 1, 1)-(Rd, Gd, Bd, Ad)
GL_ONE_MINUS_SRC_COLOR destination (1, 1, 1, 1)-(Rs, Gs, Bs, As)
GL_SRC_ALPHA source or destination (As, As, As, As)
GL_ONE_MINUS_SRC_ALPHA source or destination (1, 1, 1, 1)-(As, As, As, As)
GL_DST_ALPHA source or destination (Ad, Ad, Ad, Ad)
GL_ONE_MINUS_DST_ALPHA source or destination (1, 1, 1, 1)-(Ad, Ad, Ad, Ad)
GL_SRC_ALPHA_SATURATE source (f, f, f, 1); f=min(As, 1-Ad)

举例说明:

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

源因子 GL_SRC_ALPHA 表示用来源数据的 Alpha 与来源像素相乘,目标因子 GL_ONE_MINUS_SRC_ALPHA 表示对来源 Alpha 取反再和目标像素相乘。结果就是,来源相素 Alpha 越高即透明度越低,混合后的两个图案中,来源图案更清晰。

另一个例子:

glBlendFunc(GL_ONE, GL_ZERO);

这就使用使用来源的像素覆盖目标像素,因为来源因子全是 1 表示完全保留,目标因子全是 0 表示被覆盖。

示例 Example 6-1 : Blending Example: alpha.c 绘制两个三角形演示了混合因子对混合结果的影响。

示例 Example 6-2 : Three-Dimensional Blending: alpha3D.c 展示 3D 混合,其中使用了显示列表 Display List。

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