iOS图像:OpenGL(上)

原创:知识探索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、OpenGL的知识点
    • 1、OpenGL 专业名词
    • 2、OpenGL 坐标系
    • 3、着⾊器和图片的渲染流程
    • 4、搭建Open GL项目的运行环境
  • 二、使用OpenGL绘制图形的流程
    • 1、绘制三角形
    • 2、绘制正方形
  • 三、OpenGL的图元类型
    • 1、各种需要的类
    • 2、点 GL_POINTS
    • 3、条带线 GL_LINE_STRIP
    • 4、循环线 GL_LINE_LOOP
    • 5、独立三角形 GL_TRIANGLES
  • 四、使用存储着色器
    • 1、着色器的使用
    • 2、着色器种类
  • 五、OpenGL 渲染技巧
    • 1、绘制甜甜圈
    • 2、正面&背面剔除
    • 3、深度测试
    • 4、混合
  • Demo
  • 参考文献

一、OpenGL的知识点

1、OpenGL 专业名词

图形API种类
  • OpenGL (Open Graphics Library):是一个跨编程语言、跨平台的编程图形程序接口
  • OpenGL ES (OpenGL for Embedded Systems):是OpenGL三维图形API的子集,针对手机、Pad和游戏主机等嵌入式设备而设计,去除了许多不必要和性能较低的APl接口
  • Metal:Apple为游戏开发者推出了新的平台技术Metal,该技术能够为3D图像提高10倍的渲染性能。Metal是Apple为了解决3D渲染而推出的框架
  • DirectX:属于Windows上一个多媒体处理APl,并不支持Windows以外的平台
图形API的用途
  • 简单来说就是实现图形的底层渲染,利用GPU芯⽚片来高效渲染图形图像,图形API 是iOS开发者唯⼀接近GPU的⽅式
  • 在游戏开发中,对于游戏场景/游戏人物的渲染
  • 在音视频开发中,对于视频解码后的数据渲染
  • 在地图引擎,对于地图上的数据渲染
  • 在动画中实现动画的绘制
  • 在视频处理中,对于视频加上滤镜效果
OpenGL 上下文(context)

在应用程序调⽤任何OpenGL的指令之前,需要安排⾸先创建⼀一个OpenGL的上下文。这个上下文是一个⾮常庞⼤大的状态机,保存了了OpenGL中的各种状态,这也是OpenGL指令执行的基础。OpenGL的函数不管在哪个语⾔中,都是类似C语言⼀样的面向过程的函数。

OpenGL 状态机
  • OpenGL可以记录自己的状态(如当前所使用的颜色、是否开启了颜色混合功能等)
  • OpenGL可以接收输入(当调⽤用OpenGL函数的时候,实际上可以看成 OpenGL在接收我们的输入),如我们调⽤glColor3f,则OpenGL接收到这个输入后会修改⾃己的“当前颜色”这个状态
  • OpenGL可以进入停止状态,不再接收输入。在程序退出前,OpenGL总会先停⽌工作的
渲染(Rendering)

将图形/图像数据转换成3D空间图像操作叫做渲染。

顶点数组(VertexArray)和顶点缓冲区(VertexBuffer)

画图⼀般是先画好图像的骨架,然后再往⻣架⾥面填充颜色,这对于 OpenGL也是⼀样的。顶点数据就是要画的图像的骨架,和现实中不同的是,OpenGL中的图像都是由图元组成。在OpenGLES中,有3种类型的图元:点、线、三⻆形。

那这些顶点数据最终是存储在哪里的呢?开发者可以选择设定函数指针,在调⽤绘制⽅法的时候,直接由内存传入顶点数据,也就是说这部分数据之前是存储在内存当中的,被称为顶点数组。性能更高的做法是,提前分配⼀块显存,将顶点数据预先传⼊到显存当中。这部分的显存,就被称为顶点缓冲区。

管线

之所以称之为管线是因为显卡在处理数据的时候是按照固定的顺序来的。就像⽔从⼀根管⼦的⼀端流到另一端,这个顺序是不能打破的。

固定管线(存储着色器)

在早期的OpenGL版本,它封装了很多种着色器程序块,内置的一段包含了光照、坐标变换、裁剪等等诸多功能的固定shader程序帮助开发者完成图形的渲染,开发者只需要传入相应的参数,就能快速完成图形的渲染。类似于iOS开发会封装很多APL而我们只需要调用,就可以实现功能不需要关注底层实现原理。但是由于OpenGL的使用场景非常丰富,固定管线或存储着色器无法完成每一个业务,这时将需要部分开放成可编程。

着色器程序shader

将固定渲染管线架构变为了可编程渲染管线。因此,OpenGL在实际调用绘制函数之前,还需要指定一个由shader编译成的着色器程序。常 见的着色器主要有顶点着色器(VertexShader) ,片段着色器(FragmentShader) 。

OpenGL在处理shader时, 和其他编译器一样。通过编译、 链接等步骤,生成了着色器程序(glProgram) ,着色器程序同时包含了顶点着色器和片段着色器的运算逻辑。在OpenGL进行绘制的时候,首先由顶点着色器对传入的顶点数据进行运算。再通过图元装配,将顶点转换为图元。然后进行光栅化,将图元这种矢量图形,转换为栅格化数据。最后,将栅格化数据传入片段着色器中进行运算。片段着色器会对栅格化数据中的每一个像素进行运算,并决定像素的颜色

顶点着色器(VertexShader)

一般用来处理图形每个顶点变换[旋转/平移/投影等]。顶点着色器是OpenGL中用于计算顶点属性的程序。顶点着色器是逐顶点运算的程序,也就是说每个顶点数据都会执行一次顶点着色器,当然这是并行的,并且顶点着色器运算过程中无法访问其他顶点的数据。

片元着色器程序(FragmentShader)

一般用来处理图形中每个像素点颜色计算和填充。片段着色器是OpenGL中用于计算片段(像素)颜色的程序。片段着色器是逐像素运算的程序,也就是说每个像素都会执行一次片段着色器,当然也是并行的。

OpenGL着色语言(OpenGL Shading Language)

用来在OpenGL中着色编程的语言,也即开发人员写的短小的自定义程序,他们是在图形卡的GPU (Graphic Processor Unit图形处理单元)上执行的, 代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程性。比如:视图转换、投影转换等。

光栅化Rasterization

光栅化就是把顶点数据转换为片元的过程,将图转化为一个个栅格组成的图象。片元中的每个元素对应于帧缓冲区中的一个像素。光栅化其实是一种将几何图元变为二维图像的过程。第一部分工作:决定窗口坐标中的哪些整型栅格区域被基本图元占用。第二部分工作:分配一个颜色值和一个深度值到各个区域。

纹理

物理世界中,视域内的颜色会发生快速的变化。你可以看到很多物体表面都会呈现出丰富的颜色,并且在狭小的面积上产生多彩的变化。要捕捉细节如此丰富的色彩变化是非常辛苦和缜密的工作(你需要有效地辨别每个线性色彩变化区域中的每个三角形)。如果能找到一张图片,然后把它“粘”到物体表面上,就像贴墙纸一样,那就简单多了。这就是 纹理映射(texture mapping)。在渲染图形时为了使得场景更加逼真需要填充图片,这里使用的图片就是常说的纹理。

混合(Blending)

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

变换矩阵Transformation

图形想发生平移、缩放、旋转变换就需要使用变换矩阵。

投影矩阵Projection

用于将3D坐标转换为二维屏幕坐标。

渲染上屏/交换缓冲区SwapBuffer

渲染缓冲区一般映射的是系统的窗口。如果将图像直接渲染到窗口对应的渲染缓冲区,则可以将图像显示到屏幕上。但是如果每个窗口只有一个缓冲区,那么在绘制过程中屏幕进行了刷新,窗口可能显示出不完整的图像。为了解决这个问题,常规的OpenGL程序至少都会有两个缓冲区。显示在屏幕上的称为屏幕缓冲区,没有显示的称为离屏缓冲区。在一个缓冲区渲染完成之后,通过将屏幕缓冲区和离屏缓冲区交换,实现图像在屏幕上的显示。

由于显示器的刷新一般是逐行进行的, 因此为了防止交换缓冲区的时候屏幕上下区域的图像分属于两个不同的帧,因此交换一般会等待显示器刷新完成的信号,在显示器两次刷新的间隔中进行交换,这个信号就被称为垂直同步信号,这个技术被称为垂直同步。

使用了双缓冲区和垂直同步技术之后,由于总是要等待缓冲区交换之后再进行下一帧的渲染,使得帧率无法完全达到硬件允许的最高水平。为了解决这个问题,引入了三缓冲区技术,在等待垂直同步时,来回交替渲染两个离屏的缓冲区,而垂直同步发生时,屏幕缓冲区和最近渲染完成的离屏缓冲区交换,实现充分利用硬件性能的目的。


2、OpenGL 坐标系

a、坐标系转化
  • 物体空间(Object Space) / 局部空间(Local Space)
  • 世界空间(World Space)
  • 观察空间(View Space)
  • 裁剪空间(Clip Space)
  • 屏幕空间(Screen Space)

OpenGL希望每次顶点着色后,我们的可见顶点都为标准化设备坐标,也就是说每个顶点的x,y,z都应该在-1到1之间,超出这个范围的顶点将是不可见的。通常情况下我们会自己设定一个坐标范围,之后再在顶点着色器中将这些坐标变换为设备坐标,然后这些标化设备坐标传入光栅器(Rasterizer) 将它们变换为屏幕上的二维坐标和像素。将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。

在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System)。将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易,这一点很快就会变得很明显。

一个顶点在最终被转化为片段之前需要经历不同状态。为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型(Model)、观察(View)、 投影(Projection)三个矩阵 。物体顶点的起始坐标在局部空间(Local Space) ,称它为局部坐标(Local Coordinate),它在之后会变成世界坐标(world Coordinate) 、观测坐标(View Coordinate) 、裁剪坐标(Clip Coordinate) 、并最后以屏幕坐标(Screen Corrdinate)的形式结束。


b、在3D图形学中常用的坐标系
世界坐标系

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

物体坐标系

每个物体都有他们独立的坐标系,当物理移动或者改变方向时,该物体相关联的坐标系将随之移动或改变方向。物体坐标系是以物体本身而言,比如,我先向你发指令,“向前走一步”,但我并不知道你会往哪个绝对的方向移动。比如说,当你开车时,有人会说向左转,有人说向东。但是,向左转是物体坐标系的概念,而向东则是世界坐标系中的概念。

摄像机(照相机)坐标系

照相机坐标系是和观察者密切相关的坐标系。照相机坐标系和屏幕坐标系相似,差别在于照相机坐标系处于3D空间中,而屏幕坐标系在2D平面里。

惯性坐标系

指的是世界坐标系到物体坐标系的"中途"。惯性坐标系的原点和物体坐标原点重合,但惯性坐标系的轴平行于世界坐标系的轴。为什么要引入惯性坐标系?因为物体坐标系转换到惯性坐标系只需要旋转,从惯性坐标系转换到世界坐标系只需要平移。


c、坐标变换的全局图

OpenGL最终的渲染设备是2D的,我们需要将3D表示的场景转换为最终的2D形式,前面使用模型变换和视变换将物体坐标转换到照相机坐标系后,需要进行投影变换,将坐标从相机——》裁剪坐标系,经过透视除法后,变换到规范化设备坐标系(NDC),最后进行视口变换后,3D坐标才变换到屏幕上的2D坐标。

OpenGL只定义 了裁剪坐标系、规范化设备坐标系和屏幕坐标系,而局部坐标系(模型坐标系)、世界坐标系和照相机坐标系都是为了方便用户设计而自定义的坐标系。

图中左边的过程包括模型变换、视变换,投影变换,这些变换可以由用户根据需要自行指定,这些内容在顶点着色器中完成。图中右边的两个步骤,包括透视除法、视口变换,这两个步骤是OpenGL自动执行的,在顶点着色器处理后的阶段完成。

OpenGL会对裁剪坐标执行透视除法从而将它们变换到标准化设备坐标。OpenGL使用glViewPort 内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点,这个过程称为视口变换。


d、模型变换与视变换

局部坐标系(模型坐标系)是为了方便构造模型而设立的坐标系,建立模型时我们无需关心最终对象显示在屏幕哪个位置。模型变换的主要目的是通过变换使得用顶点属性定义或者3d建模软件构造的模型,能够按照需要,通过缩小、平移等操作放置到场景中合适的位置。通过模型变换后,物体放置在一个全局的世界坐标系中,世界坐标系是所有物体交互的一个公共坐标系。

视变换是为了方便观察场景中物体而设立的坐标系,在这个坐标系中相机是个假想的概念,是为了便于计算而引入的。相机坐标系中的坐标,就是从相机的角度来解释世界坐标系中位置。OpenGL中相机始终位于原点,指向-Z轴,而以相反的方式来调整场景中物体,从而达到相同的观察效果。例如要观察-Z轴方向的一个立方体的右侧面,既可以让立方体绕着+Y轴旋转-90度,也可以让立方体保持不动,相机绕着+Y轴旋转+90度。


e、投影
正投影
透视投影

3、着⾊器和图片的渲染流程

a、图片显示到屏幕上是CPU与GPU的协作完成
  • CPU:计算视图frame,图片解码,将需要绘制的纹理图片通过数据总线交给GPU
  • GPU:纹理混合,顶点变换与计算,像素点的填充计算,渲染到帧缓冲区
  • 时钟信号:垂直同步信号V-Sync / 水平同步信号H-Sync
  • iOS设备双缓冲机制:显示系统通常会引入两个帧缓冲区,双缓冲机制

b、CPU中对图片进行解压缩操作

假设我们使用 +imageWithContentsOfFile:方法从磁盘中加载一张图片,这个时候的图片并没有解压缩。然后将生成的 UIImage 赋值给 UIImageView。接着一个隐式的CATransaction捕获到了UIImageView图层树的变化。在主线程的下一个 runloop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,这个 copy 操作可能会涉及以下部分或全部步骤:

  • 分配内存缓冲区用于管理文件 IO 和解压缩操作
  • 将文件数据从磁盘读到内存中
  • 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作
  • 最后Core AnimationCALayer使用未压缩的位图数据渲染 UIImageView 的图层
  • CPU计算好图片的Frame,对图片解压之后就会交给GPU来做图片渲染

我们提到了图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。

既然图片的解压缩需要消耗大量的 CPU 时间,那么我们为什么还要对图片进行解压缩呢?是否可以不经过解压缩,而直接将图片显示到屏幕上呢?答案是否定的。要想弄明白这个问题,我们首先需要知道什么是位图。其实,位图就是一个像素数组,数组中的每个像素就代表着图片中的一个点。我们在应用中经常用到的 JPEG 和 PNG 图片就是位图。不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比。因此,在将磁盘中的图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作,这就是为什么需要对图片解压缩的原因。

既然图片的解压缩不可避免,而我们也不想让它在主线程执行,影响我们应用的响应性,那么是否有比较好的解决方案呢?我们前面已经提到了,当未解压缩的图片将要渲染到屏幕时,系统会在主线程对图片进行解压缩,而如果图片已经解压缩了,系统就不会再对图片进行解压缩。因此,也就有了业内的解决方案,在子线程提前对图片进行强制解压缩。而强制解压缩的原理就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate

YYImage\SDWebImage开源框架实现解压缩的方法接受一个原始的位图参数 imageRef,最终返回一个新的解压缩后的位图 newImage ,中间主要经过了以下三个步骤:

  • 使用 CGBitmapContextCreate 函数创建一个位图上下文
  • 使用 CGContextDrawImage 函数将原始位图绘制到上下文中
  • 使用 CGBitmapContextCreateImage 函数创建一张新的解压缩后的位图

c、图片渲染到屏幕的过程
  • GPU获取获取图片的坐标
  • 将坐标交给顶点着色器(顶点计算)
  • 将图片光栅化(获取图片对应屏幕上的像素点)
  • 片元着色器计算(计算每个像素点的最终显示的颜色值)
  • 从帧缓存区中渲染到屏幕上

d、总的流程

读取文件->计算Frame->图片解码->解码后纹理图片位图数据通过数据总线交给GPU->GPU获取图片Frame->顶点变换计算->光栅化->根据纹理坐标获取每个像素点的颜色值(如果出现透明值需要将每个像素点的颜色*透明度值)->渲染到帧缓存区->渲染到屏幕

OpenGL渲染管线简化流程图
客户端-服务器

管线上半部分是客户端,下半部分是服务器。就 OpenGL 而言,客户端是存储在 CPU 存储器中的,驱动程序将渲染命令与数据组合起来发给服务器执行。服务器和客户端在功能上是异步的。客户端不断的将数据和命令组合在一起送入缓冲区,缓冲区再发送到服务器执行。

着色器

上图中最大的框代表的是顶点着色器 和片元着色器。着色器是使用GLSL编写的程序。顶点着色器处理从客户端输入的数据,用数学运算来计算光照效果、位移、颜色值等。有几个顶点,顶点着色器就要执行几次。

上图中的图元组合(Primitive Assembly)框图意在说明3个顶点已经组合在了一起。

片元着色器来计算片元的最终颜色和它的深度值。在这里我们会使用纹理映射的方式,对顶点处理阶段所计算的颜色值进行补充。

顶点的着色器和片元着色器之间的区别在于顶点着色器决定了一个图元应该位于屏幕的什么位置,而片元着色器使用这些信息来决定某个片元的颜色应该是什么。

Uniform值

通过设置 Uniform 变量就紧接着发送一个图元批次处理命令,设置一个应用于整个表⾯的单个颜色值。


4、搭建Open GL项目的运行环境

❶ Xcode -> macOS -> APP
❷ 添加OpenGl.framework 和 GLUT.framework 两个系统库
❸ 在Bulid Settings的Header Search path中添加 CLTool.h 和 glew.h 的生成路径
❹ 将libGLTools.a 直接拖到工程的Frameworks 里面,再删除掉 AppDelegate文件 、main.m文件、ViewController文件,创建main.cpp文件
❺ 修改main.cpp文件如下所示

代码见于绘制三角形的Demo

❻ 编译报错后,将系统<>引入修改为普通引入“”
❼ Demo的运行效果

二、使用OpenGL绘制图形的流程

1、绘制三角形

a、导入依赖库和创建成员变量

引入了GLTool着色器管理器类(shader Mananger)。没有着色器,我们就不能在OpenGL(核心框架)进行着色。

#include "GLShaderManager.h"

GLTool.h头文件包含了大部分GLTool中类似C语言的独立函数。

#include "GLTools.h"
#include <GLUT/GLUT.h>

定义一个着色管理器

GLShaderManager shaderManager;

简单的批次容器,是GLTools的一个简单的容器类

GLBatch triangleBatch;

b、程序入口函数

传入命令参数初始化GLUT

int main(int argc,char* argv[])
{
    // 设置当前工作目录,针对MAC OS X
    gltSetWorkingDirectory(argv[0]);
    
    // 传入命令参数初始化GLUT库
    glutInit(&argc, argv);
    ......
    // 类似于iOS runloop 运⾏循环
    glutMainLoop();
    
    return 0;
}

初始化双缓冲窗口,其中标志GLUT_DOUBLEGLUT_RGBAGLUT_DEPTHGLUT_STENCIL分别指双缓冲窗口、RGBA颜色模式、深度测试、模板缓冲区。双缓存窗口 GLUT_DOUBLE 指绘图命令实际上是离屏缓存区执行的,然后迅速转换成窗口视图,这种方式经常用来生成动画效果。

glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);

设置GLUT窗口大小

glutInitWindowSize(800,600);

设置GLUT窗口标题

glutCreateWindow("Triangle");

GLUT 内部运行一个本地消息循环,拦截适当的消息,然后调用我们注册的回调函数

// 注册窗口改变大小的重塑函数
glutReshapeFunc(ChangeSize);
// 注册显示函数
glutDisplayFunc(RenderScene);

初始化一个GLEW库,确保OpenGL API对程序完全可用

GLenum err = glewInit();

在试图做任何渲染之前,要检查驱动程序的初始化中没有出现任何问题

if(GLEW_OK != err)
{
    fprintf(stderr,"glew error:%s\n",glewGetErrorString(err));
    return 1;
}

设置用于渲染的数据

SetupRC();

c、设置用于渲染的数据

设置清屏颜色(背景颜色)

void SetupRC()
{
    // 设置清屏颜色(背景颜色)
    glClearColor(0.0f,0.0f,1.0f,1.0f);
    ......
}

没有着色器,在OpenGL 核心框架中是无法进行任何渲染的,所以需要初始化一个渲染管理器。这里采用固管线渲染,后面会学着用OpenGL着色语言来写着色器。

shaderManager.InitializeStockShaders();

设置三角形,其中数组vVert包含3个顶点的x,y,z

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();

d、开始渲染
清除一个或一组特定的缓冲区
  • GL_COLOR_BUFFER_BIT:指示当前激活的用来进行颜色写入缓冲区
  • GL_DEPTH_BUFFER_BIT:指示深度缓存区
  • GL_STENCIL_BUFFER_BIT:指示模板缓冲区
void RenderScene(void)
{
    // 清除一个或一组特定的缓冲区
    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代表着单位(Identity)色器,这个着色器只是使用指定颜色以默认笛卡尔坐标系(坐标范围-1.0~1.0)在屏幕上渲染几何图形

shaderManager.UseStockShader(GLT_SHADER_IDENTITY,vRed);

提交着色器

triangleBatch.Draw();

在开始的设置openGL 窗口的时候,我们指定要一个双缓冲区的渲染环境。这就意味着将在后台缓冲区进行渲染,渲染结束后交换给前台,这种方式可以防止观察者看到可能伴随着动画帧与动画帧之间的闪烁的渲染过程。以下代码表示将在后台缓冲区进行渲染,然后在结束时交换到前台。

glutSwapBuffers();
e、窗口改变大小

新建窗口或者窗口大小改变时触发,用来设置新的宽度和高度。其中0,0代表窗口中视口的左下角坐标。

void ChangeSize(int w,int h)
{
    glViewport(0,0, w, h);
}

2、绘制正方形

a、添加的属性

blockSize表示边长的一半(正方形顶点到Y的距离)

GLfloat blockSize = 0.1f;

正方形的4个点坐标

GLfloat vVerts[] =
{
    -blockSize,-blockSize,0.0f,
    blockSize,-blockSize,0.0f,
    blockSize,blockSize,0.0f,
    -blockSize,blockSize,0.0f
};
b、修改用于渲染的数据
void SetupRC()
{
    glClearColor(0.0f,0.0f,1.0f,1.0f);

    shaderManager.InitializeStockShaders();

    // 修改为GL_TRIANGLE_FAN ,4个顶点
    triangleBatch.Begin(GL_TRIANGLE_FAN,4);
    triangleBatch.CopyVertexData3f(vVerts);
    triangleBatch.End();
}
c、修改程序入口函数
int main(int argc,char* argv[])
{
    gltSetWorkingDirectory(argv[0]);
    
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);
    glutInitWindowSize(800,600);
    glutCreateWindow("Square");
    
    glutReshapeFunc(ChangeSize);
    glutDisplayFunc(RenderScene);

    // 注册特殊键位响应函数
    glutSpecialFunc(SpecialKeys);

    GLenum err = glewInit();
    if(GLEW_OK != err)
    {
        fprintf(stderr,"glew error:%s\n",glewGetErrorString(err));
        return 1;
    }

    SetupRC();

    glutMainLoop();
    
    return 0;
}
d、实现特殊键位响应函数

设置移动步⻓和相对移动顶点D

void SpecialKeys(int key, int x, int y)
{
    // 移动步⻓
    GLfloat stepSize = 0.025f;
    // 相对移动顶点D
    GLfloat blockX = vVerts[0];
    GLfloat blockY = vVerts[10];
    
    printf("v[0] = %f\n",blockX);
    printf("v[10] = %f\n",blockY);
    ......
}

根据键盘标记判断此时正方形移动⽅向(上下左右),然后根据方向调整相对移动坐标(blockX/blockY)值

if (key == GLUT_KEY_UP)// 上
{
    blockY += stepSize;
}

if (key == GLUT_KEY_DOWN)// 下
{
    blockY -= stepSize;
}

if (key == GLUT_KEY_LEFT)// 左
{
    blockX -= stepSize;
}

if (key == GLUT_KEY_RIGHT)// 右
{
    blockX += stepSize;
}

触碰到边界(4个边界)的处理

if (blockX < -1.0f)
{
    blockX = -1.0f;
}

// 当正方形移动到最右边时
// 1.0 - blockSize * 2 = 总边长 - 正方形的边长 = 最左边点的位置
if (blockX > (1.0 - blockSize * 2))
{
    blockX = 1.0f - blockSize * 2;
}

// 当正方形移动到最下面时
// -1.0 - blockSize * 2 = Y(负轴边界) - 正方形边长 = 最下面点的位置
if (blockY < -1.0f + blockSize * 2 )
{
    blockY = -1.0f + blockSize * 2;
}

// 当正方形移动到最上面时
if (blockY > 1.0f)
{
    blockY = 1.0f;
}

printf("blockX = %f\n",blockX);
printf("blockY = %f\n",blockY);

根据相对顶点D计算出ABCD每个顶点坐标值(只需要更新X、Y坐标,不需要更新Z坐标)

vVerts[0] = blockX;
vVerts[1] = blockY - blockSize*2;
printf("(%f,%f)\n",vVerts[0],vVerts[1]);

vVerts[3] = blockX + blockSize*2;
vVerts[4] = blockY - blockSize*2;
printf("(%f,%f)\n",vVerts[3],vVerts[4]);

vVerts[6] = blockX + blockSize*2;
vVerts[7] = blockY;
printf("(%f,%f)\n",vVerts[6],vVerts[7]);

vVerts[9] = blockX;
vVerts[10] = blockY;
printf("(%f,%f)\n",vVerts[9],vVerts[10]);

更新顶点坐标数组

triangleBatch.CopyVertexData3f(vVerts);

手动触发重新渲染

glutPostRedisplay();

三、OpenGL的图元类型

1、各种需要的类

变化管线使用矩阵堆栈

GLShaderManager        shaderManager;// 存储着色器管理工具类
GLMatrixStack        modelViewMatrix;// 模型视图矩阵
GLMatrixStack        projectionMatrix;// 投影矩阵
GLFrame                cameraFrame;// 设置观察者视图坐标
GLFrame             objectFrame;// 设置物体视图坐标

投影矩阵

// 设置图元绘制时候的投影方式
GLFrustum            viewFrustum;

容器类(7种不同的图元对应7种容器对象)

GLBatch             pointBatch;// 点
GLBatch             lineBatch;// 线段
GLBatch             lineStripBatch;// 线带
GLBatch             lineLoopBatch;// 线环
GLBatch             triangleBatch;// 三角形
GLBatch             triangleStripBatch;// 六边形
GLBatch             triangleFanBatch;// 柱体

几何变换的管道

GLGeometryTransform transformPipeline;// 变换管道。存储模型视图/投影矩阵

GLfloat vGreen[] = { 0.0f, 1.0f, 0.0f, 1.0f };// 绿色
GLfloat vBlack[] = { 0.0f, 0.0f, 0.0f, 1.0f };// 黑色

跟踪效果步骤

int nStep = 0;

2、点 GL_POINTS

a、简介

每个特定的顶点在屏幕上都仅仅是一个单独的点。我们可通过调用glPointSize改变默认点的大小。

void glPointSize(GLfloat size);
b、使用窗口维度设置视口和投影矩阵
void ChangeSize(int w, int h)
{
    .......
}

设置视口

glViewport(0, 0, w, h);

创建投影矩阵,并将它载入投影矩阵堆栈中

// viewFrustum是投影矩阵,SetPerspective表示立体图形使用透视投影
viewFrustum.SetPerspective(35.0f, float(w) / float(h), 1.0f, 500.0f);
// 获得投影矩阵加载到投影矩阵堆栈中
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());

调用顶部载入单元矩阵

// 模型视图矩阵加载单元矩阵
modelViewMatrix.LoadIdentity();
c、设置顶点数据
void SetupRC()
{
    ......
}

灰色的背景

glClearColor(0.7f, 0.7f, 0.7f, 1.0f );
shaderManager.InitializeStockShaders();
glEnable(GL_DEPTH_TEST);

设置变换管线以使用两个矩阵堆栈

transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
cameraFrame.MoveForward(-15.0f);

定义一些点,三角形形状

GLfloat vCoast[9] =
{
    3,3,0,
    0,3,0,
    3,0,0
};

用点的形式

pointBatch.Begin(GL_POINTS, 3);
pointBatch.CopyVertexData3f(vCoast);
pointBatch.End();
d、渲染场景
void RenderScene()
{
    .......
}

压栈

modelViewMatrix.PushMatrix();
M3DMatrix44f mCamera;
cameraFrame.GetCameraMatrix(mCamera);

矩阵乘以矩阵堆栈的顶部矩阵,相乘的结果随后简存储在堆栈的顶部

modelViewMatrix.MultMatrix(mCamera);

只要使用 GetMatrix 函数就可以获取矩阵堆栈顶部的值

M3DMatrix44f mObjectFrame;
objectFrame.GetMatrix(mObjectFrame);

矩阵乘以矩阵堆栈的顶部矩阵,相乘的结果随后简存储在堆栈的顶部

modelViewMatrix.MultMatrix(mObjectFrame);
  • 参数1:平面着色器
  • 参数2:运行为几何图形变换指定一个 4 * 4 变换矩阵
    transformPipeline.GetModelViewProjectionMatrix() 获取的
    GetMatrix函数就可以获得矩阵堆栈顶部的值
  • 参数3:颜色值(黑色)
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlack);

设置点的大小

switch(nStep)
{
    case 0:
        // 设置点的大小
        glPointSize(4.0f);
        // 进行绘图
        pointBatch.Draw();
        // 恢复点的大小
        glPointSize(1.0f);
        break;
}

还原到以前的模型视图矩阵(单位矩阵)

modelViewMatrix.PopMatrix();

进行缓冲区交换

glutSwapBuffers();
e、围绕一个指定的X,Y,Z轴旋转

m3dDegToRad表示角度转化为弧度。上下围绕X轴旋转,左右围绕Y轴旋转

void SpecialKeys(int key, int x, int y)
{
    if(key == GLUT_KEY_UP) objectFrame.RotateWorld(m3dDegToRad(-5.0f), 1.0f, 0.0f, 0.0f);
    if(key == GLUT_KEY_DOWN) objectFrame.RotateWorld(m3dDegToRad(5.0f), 1.0f, 0.0f, 0.0f);
    if(key == GLUT_KEY_LEFT) objectFrame.RotateWorld(m3dDegToRad(-5.0f), 0.0f, 1.0f, 0.0f);
    if(key == GLUT_KEY_RIGHT) objectFrame.RotateWorld(m3dDegToRad(5.0f), 0.0f, 1.0f, 0.0f);
    // 重新渲染
    glutPostRedisplay();
}
f、根据空格次数,切换不同的“窗口名称”
void KeyPressFunc(unsigned char key, int x, int y)
{
    if (key == 32)
    {
        nStep++;
        if(nStep > 6) nStep = 0;
    }
    
    switch(nStep)
    {
        case 0:
            glutSetWindowTitle("GL_POINTS");
            break;
        case 1:
            glutSetWindowTitle("GL_LINES");
            break;
        case 2:
            glutSetWindowTitle("GL_LINE_STRIP");
            break;
        case 3:
            glutSetWindowTitle("GL_LINE_LOOP");
            break;
        case 4:
            glutSetWindowTitle("GL_TRIANGLES");
            break;
        case 5:
            glutSetWindowTitle("GL_TRIANGLE_STRIP");
            break;
        case 6:
            glutSetWindowTitle("GL_TRIANGLE_FAN");
            break;
    }
    
    glutPostRedisplay();
}

2、线 GL_LINES

a、简介

线段就是2个顶点之间绘制的。

// 设置独立线段宽度为1.5f
glLineWidth(1.5f);
b、修改设置数据源
// 通过线段的形式
lineBatch.Begin(GL_LINES, 3);
lineBatch.CopyVertexData3f(vCoast);
lineBatch.End();
c、修改渲染场景
case 1:
    // 设置线的宽度
    glLineWidth(2.0f);
    // 进行绘图
    lineBatch.Draw();
    // 恢复线的宽度
    glLineWidth(1.0f);
    break;

3、条带线 GL_LINE_STRIP

为了把图形连接起来,每个连接的顶点会被选定2次。一次作为线段的终点、一次作为下一条线段的起点。

a、修改设置数据源
lineStripBatch.Begin(GL_LINE_STRIP, 3);
lineStripBatch.CopyVertexData3f(vCoast);
lineStripBatch.End();
b、修改渲染场景
case 2:
    glLineWidth(2.0f);
    lineStripBatch.Draw();
    glLineWidth(1.0f);
    break;

4、循环线 GL_LINE_LOOP

在线带的基础上额外增加了一条连接着一批次中最后一个点和第一个点的线段。


a、修改设置数据源
lineLoopBatch.Begin(GL_LINE_LOOP, 3);
lineLoopBatch.CopyVertexData3f(vCoast);
lineLoopBatch.End();
b、修改渲染场景
case 3:
    glLineWidth(2.0f);
    lineLoopBatch.Draw();
    glLineWidth(1.0f);
    break;

5、独立三角形 GL_TRIANGLES

a、修改设置数据源

每3个顶点定义一个新的三角形

GLfloat vPyramid[12][3] =
{
    -2.0f, 0.0f, -2.0f,
    2.0f, 0.0f, -2.0f,
    0.0f, 4.0f, 0.0f,
    
    2.0f, 0.0f, -2.0f,
    2.0f, 0.0f, 2.0f,
    0.0f, 4.0f, 0.0f,
    
    2.0f, 0.0f, 2.0f,
    -2.0f, 0.0f, 2.0f,
    0.0f, 4.0f, 0.0f,
    
    -2.0f, 0.0f, 2.0f,
    -2.0f, 0.0f, -2.0f,
    0.0f, 4.0f, 0.0f
};

GL_TRIANGLES 通过三角形创建金字塔

triangleBatch.Begin(GL_TRIANGLES, 12);
triangleBatch.CopyVertexData3f(vPyramid);
triangleBatch.End();
b、修改渲染场景
case 4:
    DrawWireFramedBatch(&triangleBatch);
    break;

画绿色部分

void DrawWireFramedBatch(GLBatch* pBatch)
{
    // 修改颜色为绿色
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vGreen);
    pBatch->Draw();
    
    .......  
 }
c、画黑色边框

偏移深度。在同一位置要绘制填充和边线,会产生z冲突,所以要偏移

glPolygonOffset(-1.0f, -1.0f);
用于启用各种功能
  • GL_POLYGON_OFFSET_LINE:根据函数glPolygonOffset的设置,启用线的深度偏移
  • GL_LINE_SMOOTH:执行后,过滤线点的锯齿
  • GL_BLEND:启用颜色混合,例如实现半透明效果
  • GL_DEPTH_TEST:启用深度测试,根据坐标的远近自动隐藏被遮住的图形
  • 注意:glEnable() 不能写在glBegin()glEnd()中间
glEnable(GL_POLYGON_OFFSET_LINE);

画反锯齿,让黑边好看些

glEnable(GL_LINE_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

通过调用glPolygonMode将多边形正面或者背面设为线框模式,实现线框渲染

glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

设置线条宽度

glLineWidth(2.5f);

着色器

shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlack);
    pBatch->Draw();

复原原本的设置。通过调用glPolygonMode将多边形正面或者背面设为全部填充模式

glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glDisable(GL_POLYGON_OFFSET_LINE);
glLineWidth(1.0f);
glDisable(GL_BLEND);
glDisable(GL_LINE_SMOOTH);

6、三角形条带 GL_TRIANGLE_STRIP

对于很多表面和形状来说,我们可能需要绘制几个相连的三角形。我们可以使用GL_TRIANGLE_STRIP图元绘制一串相连的三角形。

a、修改设置数据源

设置顶点个数、半径、原点

GLfloat vPoints[100][3];
// 顶点个数
int nVerts = 0;

// 半径
GLfloat r = 3.0f;

// 原点的(x,y,z) = (0,0,0);
vPoints[nVerts][0] = 0.0f;
vPoints[nVerts][1] = 0.0f;
vPoints[nVerts][2] = 0.0f;

M3D_2PI就是2Pi的意思,绘制成的圆形等分成6份。数组下标自增(每自增1次就表示一个顶点)

    for(GLfloat angle = 0; angle < M3D_2PI; angle += M3D_2PI / 6.0f)
    {
        // 数组下标自增(每自增1次就表示一个顶点)
        nVerts++;
        
        // x点坐标 = cos(angle) * 半径
        vPoints[nVerts][0] = float(cos(angle)) * r;
        // y点坐标 = sin(angle) * 半径
        vPoints[nVerts][1] = float(sin(angle)) * r;
        // z点的坐标
        vPoints[nVerts][2] = -0.5f;
    }
b、修改渲染场景
case 5:
    DrawWireFramedBatch(&triangleFanBatch);
    break;

7、三角形扇面 GL_TRIANGLE_FAN

使用GL_TRIANGLE_FAN创建一组围绕一个中心点的相连三角形。

a、修改设置数据源

顶点下标和半径

// 顶点下标
int iCounter = 0;
// 半径
GLfloat radius = 3.0f;

从0度~360度,以0.3弧度为步长

for(GLfloat angle = 0.0f; angle <= (2.0f*M3D_PI); angle += 0.3f)
{
    // 圆形的顶点的X,Y
    GLfloat x = radius * sin(angle);
    GLfloat y = radius * cos(angle);
    ......
}

绘制2个三角形(他们的x,y顶点一样,只是z点不一样)

vPoints[iCounter][0] = x;
vPoints[iCounter][1] = y;
vPoints[iCounter][2] = -0.5;
iCounter++;

vPoints[iCounter][0] = x;
vPoints[iCounter][1] = y;
vPoints[iCounter][2] = 0.5;
iCounter++;

结束循环,在循环位置生成2个三角形

vPoints[iCounter][0] = vPoints[0][0];
vPoints[iCounter][1] = vPoints[0][1];
vPoints[iCounter][2] = -0.5;
iCounter++;

vPoints[iCounter][0] = vPoints[1][0];
vPoints[iCounter][1] = vPoints[1][1];
vPoints[iCounter][2] = 0.5;
iCounter++;

GL_TRIANGLE_STRIP 共用一个条带(strip)上的顶点的一组三角形

triangleStripBatch.Begin(GL_TRIANGLE_STRIP, iCounter);
triangleStripBatch.CopyVertexData3f(vPoints);
triangleStripBatch.End();
b、修改渲染场景
case 6:
    DrawWireFramedBatch(&triangleStripBatch);
    break;

8、其他

a、GLBatch 批次容器

GLTools 库中包含一个简单的容器类,叫做 GLBatch。这个类可以作为7种图元的简单批次容器使用。而且它知道在使用GL_ShaderManager支持的任意存储着色器时如何对图元进行渲染。使用 GLBatch 类非常简单。首先对批次进行初始化,告诉这个类它代表哪种图元。

参数1:图元
参数2:顶点数
参数3:一组或者两组纹理坐标(可选)
void GLBatch::Begain(GLeunm primitive,GLuint nVerts,GLuint nTexttureUnints = 0);

然后,至少要复制一个由3分量(x, y, z)顶点组成的数组。

void GLBatch::CopyVertexData3f(GLfloat *vVerts);

还可以选择复制表面法线、颜色和纹理坐标。

//复制表面法线
void GLBatch::CopyNormalDataf(GLfloat *vNorms);
//复制颜色
void GLBatch::CopyColorData4f(GLfloat *vColors);
//复制纹理坐标
void GLBatch::CopyTexCoordData2f(GLFloat *vTextCoords,GLuint uiTextureLayer);

结束绘制

void GLBatch::End(void);
b、矩阵堆栈GLMatrixStack

变化管线使用矩阵堆栈GLMatrixStack。它的构造函数允许指定堆栈的最大深度,默认的堆栈深度为64。

GLMatrixStack(int iStackDepth = 64);

这个矩阵堆在初始化时已经在堆栈中包含了单位矩阵。通过调用顶部载入单位矩阵。

void GLMatrixStack::LoadIndentiy(void);

在堆栈顶部载入任何矩阵。

void GLMatrixStack::LoadMatrix(const M3DMatrix44f m);

四、使用存储着色器

在OpenGL 核心框架中,并没提供任何内建渲染管线,在提交一个几何图形进行渲染之前,必须实现一个着色器。着色器由GLTools的 C++ 类GLShaderManager管理。他们能够满足进行基本渲染的基本要求,要求不高的程序员,这些存储着色器已经足以满足他们的需求。但随着时间和经验的提升,大部分开发者可能不满足于此,会着手去写着色器。

1、着色器的使用

a、GLShaderManager 的初始化
GLShaderManager     shaderManager;
shaderManager.InitializeStockShaders();
b、GLShaderManager 属性

存储着色器为每个变量都使用一致的内部变量命名规则和相同的属性槽(Attribute Slot)。

标识符 描述
GLT_ATTRIBUTE_VERTEX 3分量(x, y, z)顶点位置
GLT_ATTRIBUTE_COLOR 4分量(r, g, b, a)颜色值
GLT_ATTRIBUTE_NORMAL 3分量(x, y, z)表面法线
GLT_ATTRIBUTE_TEXTURE0 第一对 2 分量(s ,t)纹理坐标
GLT_ATTRIBUTE_TEXTURE1 第二对 2 分量(s ,t)纹理坐标
c、GLShanderManager 的 uniform 值

要对几何图形进行渲染,我们需要为读写递交属性矩阵,首先要绑定到我们想要使用的着色程序上,并提供程序的 Uniform 值。GLShanderManager 类可以为我们完成这项工作。useStockShader函数会选择一个存储着色器并提供这个着色器的 Uniform 值,这些工作通过一次函数调用就能完成:

GLShaderManager::UseStockShader(GLenum shader, ... ...);

在 C 语言(或 C++ 语言)中,......表示函数接受一个可变的参数数量。就这个函数本身而言,它根据我们选择的着色器,从堆栈中提取正确的参数,这些参数就是特定着色器要求的 Uniform 值。


2、着色器种类

单位(Identity)着色器 GLT_SHADER_IDENTITY

单位(Identity)着色器只是简单地使用默认的笛卡尔坐标系(坐标范围-1.0~1.0)。所有片段都应用同一种颜色,结合图形为实心和未渲染的。这种着色器只使用一个属性 GLT_ATTRIBUTE_VERTEXvColor参数包含了要求的颜色。

参数1:单位着色器
参数2:颜色值
GLShaderManager::UseStockShader(GLT_SHADER_IDENTITY, GLfoat vColor[4]);
平面(Flat)着色器 GLT_SHADER_FLAT

平面(Flat)着色器将单位着色器进行了扩展,允许为集合图形变换指定一个 4 x 4 的变换矩阵。经常被称作“模型师徒投影矩阵”。这种着色器只使用一个属性 GLT_ATTRIBUTE_VERTEX

参数1:平面着色器
参数2:允许变化的4*4矩阵
参数3:颜色值
GLShaderManager::UseStockShader(GLT_SHADER_FLAT, GLfoat mvp[16], GLfloat vColor[4]);
上色(Shaded)着色器 GLT_SHADER_SHADED

上色(Shaded)着色器:唯一的 Uniform 值就是在几何图形中应用的变换矩阵。GLT_ATTRIBUTE_VERTEXGLT_ATTRIBUTE_COLOR 在这种着色器中都会使用。颜色值将被平滑地插入顶点之间(称为平滑着色)。

GLShaderManager::UseStockShader(GLT_SHADER_SHADED, GLfoat mvp[16]);
默认光源着色器 GLT_SHADER_DEFAULT_LIGHT

默认光源着色器这种着色器使对象产生阴影和光照的效果。需要模型视图矩阵、投影矩阵和作为基本色的颜色值等 Uniform 值。所需的属性有 GLT_ATTRIBUTE_VERTEX(顶点分量)和 GLT_ATTRIBUTE_NORMAL(表面法线)。

点光源着色器 GLT_SHADER_POINT_LIGHT_DIFF

点光源着色器和默认光源着色器很相似,但光源位置可能是待定的。接受 4 个 Uniform 值,即模型视图矩阵、投影矩阵、视点坐标系中的光源位置和对象的基本漫反射颜色。同样所需的属性有 GLT_ATTRIBUTE_VERTEX(顶点分量)和 GLT_ATTRIBUTE_NORMAL(表面法线)。

参数1:点光源着色器
参数2:模型视图矩阵
参数3:投影矩阵
参数4:视点坐标系中的光源位置
参数5:颜色值
GLShaderManager::UseStockShader(GLT_SHADER_POINT_LIGHT_DIFF, GLfoat mvMatrix[16], GLfloat pMatrix[16], GLfloat vLightPos[3], GLfloat vColor[4]);
纹理替换矩阵 GLT_SHADER_TEXTURE_REPLACE

纹理替换矩阵着色器通过给定的模型视图投影矩阵,使用绑定到 nTextureUnit(纹理单元) 指定的纹理单元的纹理对几何图形进行变换。片段颜色是从纹理样本中直接获取的。所需的属性有 GLT_ATTRIBUTE_VERTEX(顶点分量)和 GLT_ATTRIBUTE_NORMAL(表面法线)。

GLShaderManager::UseStockShader(GLT_SHADER_TEXTURE_MODULATE, GLfoat mvMatrix[16], GLfloat vColor, GLint nTextureUnit);
纹理调整着色器 GLT_SHADER_TEXTURE_MODULATE

纹理调整着色器这种着色器将一个基本色乘以一个取自纹理单元 nTextureUnit 的纹理。所需的属性有 GLT_ATTRIBUTE_VERTEX(顶点分量)和 GLT_ATTRIBUTE_TEXTURE0(纹理坐标)。

GLShaderManager::UseStockShader(GLT_SHADER_TEXTURE_MODULATE, GLfoat mvMatrix[16], GLfloat vColor, GLint nTextureUnit);
纹理光源着色器 GLT_SHADER_TEXTURE_POINT_LIGHT_DIFF

纹理光源着色器着色器将一个纹理通过漫反射照明计算进行调整(相乘),光线在视觉空间中的位置是给定的。这种着色器接受 5 个 Uniform 值,即模型视图矩阵、投影矩阵、视觉空间中的光源位置、几何图形的基本色和将要使用的纹理单元。所需的属性有 GLT_ATTRIBUTE_VERTEX(顶点分量)、GLT_ATTRIBUTE_NORMAL(表面法线)和 GLT_ATTRIBUTE_TEXTURE0(纹理坐标)。

参数1:纹理光源着色器
参数2:模型视图矩阵
参数3:投影矩阵
参数4:视点坐标系中的光源位置
参数5:几何图形的基本色
参数6:将要使用的纹理单元
GLShaderManager::UseStockShader(GLT_SHADER_TEXTURE_POINT_LIGHT_DIFF, GLfloat mvMatrix, GLfoat mvMatrix[16], GLfloat vLightPos[3], GLfloat vBaseColor[4], GLint nTextureUnit);

五、OpenGL 渲染技巧

用到的属性

设置角色帧,作为相机

GLFrame             viewFrame;

标记:背面剔除、深度测试

int iCull = 0;
int iDepth = 0;

1、绘制甜甜圈

a、改变尺寸
void ChangeSize(int w, int h)
{
}

防止h变为0

if(h == 0) h = 1;

设置视口窗口尺寸

glViewport(0, 0, w, h);

setPerspective函数的参数是一个从顶点方向看去的视场角度(用角度值表示)。设置透视模式,初始化其透视矩阵。

viewFrustum.SetPerspective(35.0f, float(w)/float(h), 1.0f, 100.0f);

把透视矩阵加载到透视矩阵对阵中

projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());

初始化渲染管线

transformPipeline.SetMatrixStacks(modelViewMatix, projectionMatrix);

b、设置数据源
void SetupRC()
{
}

设置背景颜色

glClearColor(0.3f, 0.3f, 0.3f, 1.0f );

初始化着色器管理器

shaderManager.InitializeStockShaders();

将相机向后移动7个单元:肉眼到物体之间的距离

viewFrame.MoveForward(7.0);
创建一个甜甜圈
  • 参数1:GLTriangleBatch容器帮助类
  • 参数2:外边缘半径
  • 参数3:内边缘半径
  • 参数4、5:主半径和从半径的细分单元数量
gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26);

点的大小(方便点填充时,肉眼观察)

glPointSize(4.0f);

c、渲染场景
void RenderScene()
{
}

清除窗口和深度缓冲区

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

把摄像机矩阵压入模型矩阵中

modelViewMatix.PushMatrix(viewFrame);

设置绘图颜色

GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
使用默认光源着色器,通过光源、阴影效果体现立体效果
  • 参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器
  • 参数2:模型视图矩阵
  • 参数3:投影矩阵
  • 参数4:基本颜色值
shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);

绘制

torusBatch.Draw();

出栈。绘制完成恢复

modelViewMatix.PopMatrix();

交换缓存区

glutSwapBuffers();

d、键位设置

键位设置,通过不同的键位对其进行设置。控制Camera的移动,从而改变视口。

void SpecialKeys(int key, int x, int y)
{
    ......
}

根据方向调整观察者位置

if(key == GLUT_KEY_UP) viewFrame.RotateWorld(m3dDegToRad(-5.0), 1.0f, 0.0f, 0.0f);
if(key == GLUT_KEY_DOWN) viewFrame.RotateWorld(m3dDegToRad(5.0), 1.0f, 0.0f, 0.0f);
if(key == GLUT_KEY_LEFT) viewFrame.RotateWorld(m3dDegToRad(-5.0), 0.0f, 1.0f, 0.0f);
if(key == GLUT_KEY_RIGHT) viewFrame.RotateWorld(m3dDegToRad(5.0), 0.0f, 1.0f, 0.0f);

重新刷新

glutPostRedisplay();

2、正面&背面剔除

a、存在的问题

如果我们绘制一个由很多个三角形组成的实体对象,那么第一个绘制的三角形可能会被后面绘制的三角形覆盖。如下图这个像游泳圈似的模型,其中一些三角形在游泳圈的背面,另一些在正面,正常我们应该是看不到背面的(不考虑透明几何体的特殊情况)。这样的话,三角形绘制的顺序可能会一团糟,就变成了下图的样子。

对正面和背面三角形进行区分的原因之一就是为了进行剔除。背面剔除能极大提高性能,避免上图出现的问题。它很高效,在渲染的图元装配阶段就整体抛弃了一些三角形。

开启背面剔除

glEnable(GL_CULL_FACE);

关闭背面剔除

glDisable(GL_CULL_FACE);

我们并没有指明剔除的是正面还是背面,这是由另外一个函数 glCullFace 控制的

void glCullFace(GLenum mode);

mode 参数的可用值为 GL_FRONTGL_BACKGL_FRONT_AND_BACK。这样要消除不透明物体的内部几何图形就需要两行代码:

void glCullFace(GL_BACK);
glEnable(GL_CULL_FACE);

在某些情况下,剔除实体几何体的正面也很有必要,比如要显示图形内部渲染的时候。在渲染透明对象时,我们经常会对一个对象进行两次渲染,第一次会开启透明并剔除正面,第二次则消除背面。这样就在渲染正面之前渲染了背面,这也是渲染透明物体的需要。

但是在开启背面剔除后,会发现上面的游泳圈模型还是显示的有问题,如图:

这是因为没有开启深度测试。


b、在入口函数中添加右击菜单栏
glutCreateMenu(ProcessMenu);
glutAddMenuEntry("深度测试",1);
glutAddMenuEntry("正背面剔除",2);
glutAddMenuEntry("颜色填充", 3);
glutAddMenuEntry("线段填充", 4);
glutAddMenuEntry("点填充", 5);
glutAttachMenu(GLUT_RIGHT_BUTTON);

菜单栏函数的实现方式如下

void ProcessMenu(int value)
{
    switch(value)
    {
        case 1:
            iDepth = !iDepth;
            break;
        case 2:
            iCull = !iCull;
            break;
        case 3:
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
            break;
        case 4:
            glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
            break;
        case 5:
            glPolygonMode(GL_FRONT_AND_BACK, GL_POINT);
            break;
    }
    // 重新渲染
    glutPostRedisplay();
}

c、在渲染场景中开启/关闭正背面剔除功能
if (iCull)
{
    glEnable(GL_CULL_FACE);
    // glFrontFace(GL_CCW);
    glCullFace(GL_BACK);
}
else
{
    glDisable(GL_CULL_FACE);
}

3、深度测试

a、介绍深度测试

深度就是在openGL坐标系中,像素点的 Z 坐标距离观察者的距离。观察者可能放在坐标系的任何位置,那么,就不能简单的说 Z 数值越大或越小,就是越靠近观察者。如果观察者在Z轴的正方向,Z 值大的靠近观察者,如果是在Z轴的反方向,则 Z 值小的更靠近观察者。

深度缓冲区原理就是把一个距离观察平面(近裁剪面)的深度值(或距离)与窗口中的每个像素相关联。首先,使用glClear(GL_DEPTH_BUFFER_BIT)把所有像素的深度值设置为最大值。如果启用了深度缓冲区,在绘制每个像素之前,OpenGL会把它的深度值和已经存储在这个像素的深度值进行比较。如果,新像素深度值 < 原先像素深度值,则新像素值会取代原先的;反之,新像素值被遮挡,它的颜色值和深度将被丢弃。这个比较、丢弃的过程就叫做 深度测试,深度测试是另一种高效消除隐藏面的技术。如果没有深度缓冲区,那么启动深度测试的命令将被忽略。

申请一个颜色缓冲区和一个深度缓冲区:

glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);

要启用深度测试,只需调用:

glEnable(GL_DEPTH_TEST);

关闭深度测试:

glDisable(GL_DEPTH_TEST);

在绘制场景前,清除颜色缓冲区和深度缓冲区。清除深度缓冲区的默认值是1.0,表示最大的深度值,深度值的范围在[0,1]之间。

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

开启了深度测试后,我们终于得到了一个我们想要的游泳圈模型。



b、使用深度测试

根据设置iDepth标记来判断是否开启深度测试

if(iDepth)
{
    glEnable(GL_DEPTH_TEST);
}
else
{
    glDisable(GL_DEPTH_TEST);
}

4、混合

a、介绍混合

通常 OpenGL 渲染时会把颜色值放在颜色缓冲区,每个像素的深度值放在深度缓冲区。当深度测试被关闭,新的颜色值会简单地覆盖颜色缓冲区中已经存在的其他值。当深度测试被打开,则会保留深度值Z更小的。但如果打开了 混合功能,那么下层的颜色值就不会被清除了。在打开混合功能的情况下,新的颜色会与已经存在的颜色值在颜色缓冲区中进行组合。

glEnable(GL_BLEND);// 开启混合功能
  • 目标颜色:已经存储在颜色缓冲区中的颜色值。
  • 源颜色:作为当前渲染命令的结果进入颜色缓冲区的颜色值,它可能与目标颜色进行交互,也可能不交互。

目标颜色和源颜色都包含了单独的红、绿、蓝成分和一个可选的 alpha值。如果我们忽略 alpha 值,OpenGL 会将它设为1.0。当混合功能被启用时,源颜色和目标颜色的组合方式是由混合方程式控制的。默认情况下方程式如下:

// Cf:最终计算产生的颜色
// Cs:源颜色
// Cd:是目标颜色
// S:源混合因子
// D:目标混合因子
Cf = (CS * S) + (Cd * D)

这些混合因子是用glBlendFunc函数进行设置的。

// S和D都是枚举值,不是可直接指定的实际值
glBlendFunc(GLenum S, GLenum D);

// 常见的混合函数组合
glBlendFun(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

例如,如果颜色缓冲区已经有一种蓝色(0.0f, 0.0f, 1.0f, 0.0f),这是目标颜色 (Cd)。如果在这上面用一种 alpha 值为 0.6 的 红色(1.0f, 0.0f, 0.0f, 0.6f)画了一些什么东西,就可以像下面这样计算最终颜色。

Cd = 目标颜色 = (0.0f, 0.0f, 1.0f, 0.0f)
Cs = 源颜色 = (1.0f, 0.0f, 0.0f, 0.6f)
S = 源 alpha 值 = 0.6
D = 1 减去 alpha 值 = 1.0 - 0.6 = 0.4

Cf = (Red * S)+ (Blue * 0.4)

最终的颜色是原先的蓝色(目标颜色)与后来的红色(源颜色)进行缩放后的组合。源颜色的 alpha 值越高,添加的源颜色成分就越多,目标颜色所保留的成分就越少。

未开启混合时候的样子

b、使用到的属性
GLBatch squareBatch;// 正方形
GLBatch greenBatch;// 绿
GLBatch redBatch;// 红
GLBatch blueBatch;// 蓝
GLBatch blackBatch;// 黑

GLShaderManager    shaderManager;

// 四个图形的大小
GLfloat blockSize = 0.2f;
// 四个图形的顶点
GLfloat vVerts[] = { -blockSize, -blockSize, 0.0f,
    blockSize, -blockSize, 0.0f,
    blockSize,  blockSize, 0.0f,
    -blockSize,  blockSize, 0.0f};

c、设置数据源
void SetupRC()
{
}

绘制1个移动矩形

squareBatch.Begin(GL_TRIANGLE_FAN, 4);
squareBatch.CopyVertexData3f(vVerts);
squareBatch.End();

绘制4个固定矩形

GLfloat vBlock[] = { 0.25f, 0.25f, 0.0f,
    0.75f, 0.25f, 0.0f,
    0.75f, 0.75f, 0.0f,
    0.25f, 0.75f, 0.0f};

greenBatch.Begin(GL_TRIANGLE_FAN, 4);
greenBatch.CopyVertexData3f(vBlock);
greenBatch.End();

......

d、绘制场景
void RenderScene(void)
{
}

定义4种颜色

GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 0.5f };
GLfloat vGreen[] = { 0.0f, 1.0f, 0.0f, 1.0f };
GLfloat vBlue[] = { 0.0f, 0.0f, 1.0f, 1.0f };
GLfloat vBlack[] = { 0.0f, 0.0f, 0.0f, 1.0f };

绘制场景的时候,使用单位着色器将4个固定矩形绘制好。

shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vGreen);
greenBatch.Draw();

shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vRed);
redBatch.Draw();

shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vBlue);
blueBatch.Draw();

shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vBlack);
blackBatch.Draw();

开启混合

glEnable(GL_BLEND);

开启组合函数,计算混合颜色因子

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

使用着色器管理器

shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vRed);

容器类开始绘制

squareBatch.Draw();

关闭混合功能

glDisable(GL_BLEND);

续文见下篇:iOS多媒体:OpenGL(下)


Demo

Demo在我的Github上,欢迎下载。
Multi-MediaDemo

参考文献

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

推荐阅读更多精彩内容