《OpenGL编程指南》学习笔记

OpenGL核心技术概览
本文概述了OpenGL的关键概念和技术,包括渲染管线、缓存管理、着色器编程、纹理处理及高级特性等内容。深入探讨了OpenGL的API使用方法,以及如何通过不同的技术组合实现高效的图形渲染。

文章目录


本文不分章节。

前言

标准

既然OpenGL是一个API标准,那么其标准的文档可以在官网查找: Documentation,或者在Khronos OpenGL® Registry查找。不过官网速度太慢了,最好下载官方的pdf文档

docs.GL是一个可以检索OpenGL API的网站(基于一个开源项目开发的),更可喜的是有的API说明中还会有示例代码。

虽然KhronosGroup也有官方的文档,但就是难看:OpenGL-Refpages

代码练习环境

如果不看本书的话,可以选择的配置是glfw和glew。Windows上是在这两个官网下载,并在VC++环境下配置;Mac上可以使用homebrew来安装。

但是如果要看本书的话,还是最好选择freeglut和glew。Windows可以下载编译好的文件,或者自行编译;Mac上可以使用homebrew安装。红宝书就是这么拽~

红宝书的代码应该不是在Mac上写的,因为自己带的LoadShaders.cpp在Mac上编译不过。包含OpenGL库的时候,使用的是<GL/gl.h>,而这个在Mac上应该是<OpenGL/gl.h>,位于/System/Library/Frameworks/OpenGL.framework/Headers。

该书第九版的代码在GitHub上OpenGL Red Book,第八版可以在Kylewlk/OpenGL-Programming-Guide-8th-Edition-Code找到。

提醒一下:最好不要在Mac上练习该书代码,Windows+freeglut+glew+VS是最佳练习环境。

正文

相关概念

OpenGL需要将所有的数据都保存到缓存对象中,它相当于是OpenGL服务端维护的一块内存区域。

OpenGL渲染管线

在这里插入图片描述

生成 glGen*

负责分配不同类型的OpenGL对象的名称。可认为类似C语言中的指针变量。比如顶点数组对象(glGenVertexArray),顶点缓存对象(glGenBuffer)……

绑定 glBind*

顶点绑定是将一组顶点设置为当前对象,后续操作都是针对这个当前对象的。一般发生在两种情况下:创建对象并初始化,以及要使用的对象并非当前对象。这也是体现了OpenGL的设计上是基于状态机模型的。

释放 glDelete*

不再使用的话,需要释放缓存。

有效性判断 glIs*

判断某个对象是不是创建了还没有释放。

载入数据

glBufferData。额,感叹一下,初看OpenGL的一些函数好多定义的不清晰,功能重复,而且参数不同功能差异就很大,命名还很烂。

着色器

OpenGL 3.1之后,每个程序必须有两个着色器顶点着色器和片元着色器。

禁用和启用

glEnable & glDisable。这么喜欢使用参数控制状态的标准居然不使用参数来控制禁用和启用,而是使用了两个函数,也是随意啊。

状态测试

glIsEnabled 状态测试。

着色器基础知识点

OpenGL从3.1开始从核心中去掉了固定管线。3.0的时候尚可使用兼容模式写固定管线。所以现代OpenGL渲染严重依赖着色器。OpenGL着色语言是在OpenGL 2.0版本左右开始发布的,目前基本会同OpenGL一起发布更新。写法上类似C/C++。

着色器是一个独立的处理单元,每个着色器都有一个叫做main的入口函数。它使用着色器中特殊的全局变量来将外部的数据传递进来,以及传递出去。

注释风格

类似C/C++

基本数据类型

GLSL是一种强类型语言,必须事先声明类型。基本类型有:
float(32bit float number), double(64bit float number), int(32 bit), uint(32 bit), bool.

命名

类似C,但是不能使用连续的下划线。

变量的作用域

类C

类型转换

隐式类型转换比C少,比C/C++更加注重类型安全。语句“int f = false;” 会报错。

支持的隐式转换规则:

  • int --> uint
  • int, uint --> float
  • int, uint, float --> double

可以显示转换

float f = 3.14;
int ten = int(f);

聚合类型

向量:2D, 3D, 4D, 对应地有float, int, uint, double, bool。GLSL中默认的类型是float,这一点可以从vec2d是使用了float类型看出,使用int的写作ivec2,还有uvec2, dvec2, bvec2。初始化:vec2d velcity=vec2(0.5, 1.0);

对应地还有矩阵类型:mat,类型命名规则类似向量。mat2x2, mat2x3……mat4x4

// 向量
vec3 velocity = vec3(1.0, 2.0, 3.0);
ivec3 steps = vec3(velcity); // 类型转换,并赋值

vec4 color;
vec3 RGB = vec3(color); // 只有前三个分量,相当于截断

vec3 white = vec3(1.0, 1.0, 1.0);
vec4 translucent = vec4(white, 0.5);

// 3维矩阵的初始化(效果相同)
mat3 M = mat3(1.0, 2.0, 3.0, 
              4.0, 5.0, 6.0, 
              7.0, 8.0, 9.0);
vec3 col1 = vec3(1.0, 2.0, 3.0);
vec3 col2 = vec3(4.0, 5.0, 6.0);
vec3 col3 = vec3(7.0, 8.0, 9.0);
mat3 M = (col1, col2, col3);

vec2 col1 = vec2(1.0, 2.0);
vec2 col2 = vec2(4.0, 5.0);
vec2 col3 = vec2(7.0, 8.0);
mat3 M = mat3(col1, 3.0
              col2, 6.0
              col3, 9.0);
向量/矩阵中元素的访问

三种类型的分量:

  • 与位置相关的分量:(x, y, z, w);
  • 与颜色相关的分量:(r, g, b, a);
  • 与纹理相关的分量:(s, t, p, q);
// 访问分量
float red = color.r;
// 通过下标访问
float red = color[0];

// 注意,这里相当于3维
vec3 luminance = color.rrr;
// 反转
color = color.abgr;

// 但是三种分量不能混用
vec4 color = otherColor.rgz; // z来自不同访问符集合

mat4 m = mat4(2.0); // 这是一个单位矩阵乘以2. 只有正对角线的元素为2.0,其余皆为0.0.
vec4 zVec = m[2]; // 第二列
float yScale = m[1][1]; // 即:2.0
结构体
struct Particle {
   float life;
   vec3  position;
   vec3  velocity;
};

vec3 pos = vec3(1.0, 1.0, 2.0);
vec3 vel = vec3(0.0, 0.5, 0.8);
Particle p = Particle(10.0, pos, vel);
数组
// 3维数组
float coeff[3] = float[3](1.0, 1.0, 1.0);
float[3] coeff;

// 未定义多少维,可重新定义。
int indicess[]; 

// 向量/矩阵的长度,对于向量来说就是元素维度,对于矩阵来说是列数。
mat3x4;
int c = m.length(); // 3
int r = m[0].length(); // 4

还有多维数组

float coeff[3][5]; // 三个数组,每个元素包含一个5维数组 
coeff[2][1] *= 2.0; 
coeff.length(); // 常量3
coeff[2]; // 一个5维数组
coeff[2].length(); // 常量5
存储限制符
  • const 只读的变量,仅在初始化时指定 const floata PI = 3.14;
  • in 设置变量为着色器的输入变量
  • out 设置变量为着色器的输出变量
  • uniform 这类变量直接从OpenGL程序中接收数据,它不随着顶点/片元的变化而变化。必须是全局变量。在所有的可用的着色阶段都是共享的
  • buffer 设置应用程序共享的一大块可读写的内存
  • shared 本地工作组中共享使用,只能用于计算着色器中

其中uniform类型变量(也可以是数组和结构体)比较特别,由专门的函数用来取出/设置。GLSL编译器会在链接的时候创建一个uniform变量列表。需要通过glGetUniformLocation()获取索引。同时存在于用户应用程序和着色器中,故而需要在修改着色器的内容同时也调用OpenGL函数修改uniform缓存对象。

// 获取uniform变量索引并设置具体的值
GLint timeLoc;
GLfloat timeValue = ...;
timeLoc = glGetUniformLocation(program, "time");
glUniform1l(timeLoc, timeValue); // set value

glUniform*f可以设置布尔数据。再次证明OpenGL中float才是“第一”基本类型。相对而言int是C/C++的“第一”基本类型。

语句
操作符

操作符基本同C。此外GLSL还重载了大部分操作符,用来支持向量和矩阵的运算。比如矩阵和向量的点乘。

控制

if-else

if (真) {
    
} else {
    
}

switch-case

switch (int_value) {
    case n:
    ...
    break;
    case m:
    ...
    break;
    default:
    ...
    break;
}

for-loop, while-loop, do-while-loop

for (int i = 0; i < 10; ++i) {
    ...
}
while (n < 10) {
    ...
}
do {
    ...
} while (n < 10);

以及:break, continue, return [结果],discard(只能用于片元着色器中,表示终止着色器的执行)。

函数

GLSL有一些内置函数,也支持用户自定义函数,遵循先声明后使用的规则。声明方式同C(如下),只是参数要加上访问修饰符,还有函数名不能以gl_开头,不能连续使用下划线:

returntype functionName([accessModifier] type1 variable1,
                        [accessModifier] type2 variable2,
                        ...) 
{
    return returnValue;
}

上面的参数限定符accessModifier有:in(默认),const in,out,inout。见名知义。

GLSL除了(内容十分丰富的)内置函数之外,还有内置常量,内置变量。

计算的不变性

GLSL着色器无法保证在不同的着色器中,两个完全相同的算式得到的结果完全一致。[惊讶]。原因是由不同的优化导致。当在图形设备上完成计算的时候,GLSL有两个方法invariant/precise来确保;但是对于宿主计算机和图形硬件各自计算的,无能为力,如下例,即使使用了invariant/precise也不行:

uniform float ten;     // 假定应用程序设置这个值为10.0
const float f = sin(10.0); // 宿主机的编译器负责计算
float g = sin(ten);  // 图形硬件负责计算

void main() {
    if (f == g) { // 不一定相等
        ...
    }
}

invariant限制符可以设置任何着色器的(包括内置变量)输出变量。为了保证不变性,GLSL编译器的一些优化可能被迫终止。调试的时候,可以设置全部变量为invariant,方法是:

#pragma STDGL invariant(all)

precise可以设置任何计算中的变量或者函数返回值,可用于避免计算几何体的时候出现裂缝。为了保证表达式结果的一致性,应该使用precise。这里,precise的含义其实是计算的可重复性,而不是增加精度。实际上,使用precise是阻止编译器使用不同的指令来做同一类运算,比如普通的乘法和fma(融混乘法)。

预处理器

同C一样,GLSL也有预处理阶段。不过差异之处是没有包含命令(#include)。

编译器选项

须在函数代码块之外写。

#pragma optimize(on)  // 优化开
#pragma optimize(off) // 优化关

#pragma debug(on)   // 调试开
#pragma debug(off)  // 调试关
// 设置某个扩展编译功能
#extension extension_name : <directive>

extensio_name可由glGetString(GL_EXTENSION);得到。

// 设置全部扩展功能
#extension all : <directive>

directive可选的值有:

  • require 不支持就提示错误,不能设置为all
  • enable 不支持就警告,设置为all就提示错误
  • warn 不支持,或者编译中使用了任何扩展,就警告
  • disable 禁用该扩展,如果设置为all,就禁止所以扩展,之后若使用相应扩展,就警告/错误。

数据块接口

着色器和应用程序,或者着色器的各个阶段之间共享的变量可以组织为变量块。uniform变量可使用uniform块,输入和输出变量可使用in和out块。着色器的存储缓存可以使用buffer块。形式类似。

uniform块

访问函数需要使用glMapBuffer()之类的函数。声明uniform块的方法是:

uniform Matrix {
    mat4 modelView;
    mat4 projection;
    mat4 color;
};

// 或者

uniform Matrix {
    mat4 modelView;
    mat4 projection;
    mat4 color;
} name;

uniform块中只能包含透明类型变量,而所谓不透明变量就是采样器,图像和原子计数器。uniform只能在全局作用域内声明。

布局控制
  • shared 默认布局,表示uniform块是多个程序间共享的。注意与存储限定符shared区分。
  • packed 设置uniform块占用最小内存,但是会禁用程序间共享这个块。
  • std140 使用标准布局方式设置uniform块,或者着色器存储的buffer块。
  • std430 使用标准布局方式设置buffer块。
  • row_major 使用行主序的方式存储uniform块中的矩阵
  • column_major 使用列主序的方式存储uniform块中的矩阵(默认顺序)

看样子以后还会出现std***吧???

布局控制符可以在编译和链接的时候控制uniform变量的布局。

// 设置共享,行主序的uniform块
layout (shared, row_major) uniform {...};

// 设置此后所有行都使用同一种布局,除非下面的代码修改了再写
layout (shared, row_major) uniform;
uniform块中访问uniform变量

注意uniform块中的变量并不受块定义名称的限制,所以同名的uniform块中的uniform变量名不可重复,否则会报错。但是访问块中的变量的时候需要加上uniform块名。[诡异的设定~]

应用程序中访问uniform块

uniform变量是应用程序与着色器之间的桥梁。

方法一:

如果uniform变量是在命名uniform块中的,那么访问该变量需要首先找到这个块在着色器中的索引,再取得变量的具体位置。

// 获取块的索引
GLint glGetUniformBlockIndex();

// 获取块中各变量的大小,偏移量……
glGetActiveUniformBlockiv();

// 获取到块的索引之后需要用一个缓存对象与之关联
void glBindBufferRange(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeptr size);
// 如果这个uniform块都是使用缓存来存储的,那么可以使用下面的方法建立关联:
void glBindBufferBase(GLenum target, GLuint index, GLuint buffer);

一旦建立关联就可以使用缓存相关的命令对块内的数据进行初始化和修改。

方法二:

直接建立某个命名uniform块和缓存对象之间的绑定关系。对于多个着色器共享一个uniform块的时候,这种方式最方便。显式地控制个uniform块的绑定方式,可以在调用链接程序函数(glLinkProgram)之前调用glUniformBlockBinding(),将uniformBlockIndex与uniformBlockBinding绑定起来;

GLint glUniformBlockBinding(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);

如果uniform中使用了默认的布局方式,那么需要得到uniform块中每个uniform变量的索引和大小,方法是根据指定的名称(数组)获取:

void glGetUniformIndices(GLuint  program,
 	                     GLsizei uniformCount,
 	                     const GLchar **uniformNames,
 	                     GLuint *uniformIndices);

获取到索引(数组)之后,使用

buffer块

对于应用程序而言,这就是着色器的存储缓存对象(shader storage buffer object)。类似于uniform块,但是还有不同:

  • 着色器可写入buffer块,并使得结果影响别的着色器和应用程序。
  • 可以在渲染的时候决定其大小,而不是编译和链接的时候。
buffer BufferObject {
    int mode;
    vec4 points[]; // 最后一个成员是大小未定义的
};

那么可以在编译、链接之后,渲染之前设置points的大小。着色器内部使用length()获取渲染时数组的大小。着色器可以对buffer块进行读和写,对于其他着色器调用而言都是可见的。

in out块

着色器从一个阶段输出,到下一个阶段输入,这个过程使用块来表示的话就可以用in/out块。

// 一个顶点着色器的输出
out Lighting {
    vec3 normal;
    vec2 bumpcoord;
};

// 片元着色器的输入
in Lighting {
    vec3 normal;
    vec2 bumpcoord;
};

// 上面二者必须对应(直接拷贝一下,再换一下修饰符就可以了)。

着色器的编译

所有着色器程序都要使用下面的方法进行设置:

  1. 创建着色器对象 glCreateShader(GLenum type);
  2. 将着色器源码编译为对象 glShaderSource(), glCompileShader();
  3. 验证编译是否成功 glGetShaderiv(GL_COMPILE_STATUS);

如果要将多个着色器对象链接为一个着色器程序:

  1. 创建一个着色器程序 glCreateProgram(void);
  2. 将着色器对象关联到着色器程序 glAttachShader(GLuint program, GLuint shader);
  3. 链接着色器程序 glLinkProgram(GLuint program);
  4. 判断链接是否成功 glGetProgramiv(GL_LINK_STATUS);
  5. 使用着色器来处理顶点和片元 glUseProgram(GLuint program);

glGetShaderInfoLog()可以获取编译日志。

glDeleteShader() 标记着色器对象为删除。

glDetachShader(GLuint program, GLuint shader); 移除着色器对象与程序之间的关联。

如果先调用glDeleteShader(),然后又调用glDetachShader()。那么这个着色器对象会被立即删除。

还有一些补充函数:glDeleteProgram(); glIsProgram(); glIsShader();

着色器子程序

定义

定义一个子程序需要3步:

// 定义子程序类型
subroutine returnType subroutineType(type param, ...);

// 定义子程序集合
subroutine (subroutineType list) returnType functionName(...);

// 使用uniform变量保存子程序选择信息
subroutine uniform subroutineType variableName;

示例代码如下:

subroutine vec4 LightFunc(vec3); // step 1

subroutine (LightFunc) vec4 ambient(vec3 n) { // step 2
    return Materials.ambient;
}

subroutine (LightFunc) vec4 diffuse(vec3 n) { // step 2
    return Materials.diffuse * max(dot(normalize(n), LightVec.xyz), 0.0);
}

subroutine uniform LightFunc materialShader; // step 3
使用

前面提到使用uniform来记录子程序信息,不过这个uniform变量需要使用特别的函数来获取自身的位置:

GLint glGetSubroutineUniformLocation(GLuint program, GLenum shaderType, const char* name);

获取子程序的索引需要使用

GLuint glGetSubroutineIndex(GLuint program, GLenum shaderType, const char* name);

shaderType代表来不同的着色阶段。

设置uniform变量指定选择哪一个子程序函数,使用:

GLuint glUniformSubroutineuiv(GLenum shaderType, GLsizei count, const GLuint* indices);
代码
GLint materialShaderLoc;
GLuint ambientIndex;
GLuint diffuseIndex;

glUseProgram(program);

materialShaderLoc = glGetSubroutineUniformLocation(program, GL_VERTEX_SHADER, "materialShader");

if (materialShaderLoc < 0) {
    // materialShader不是uniform变量
}

ambientIndex = glGetSubroutineIndex(program, GL_VERTEX_SHADER, "ambient");
diffuseIndex = glGetSubroutineIndex(program, GL_VERTEX_SHADER, "diffuse");

if (ambientIndex == GL_INVALID_IDNEX ||
    diffuseIndex == GL_INVALID_INDEX) {
        // 当前绑定的程序中GL_VERTEX_SHADER阶段没有启用指定的子程序
} else {
    GLsizei n;
    glGetIntegerv(GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS, &n);
    
    GLuint *indices = new GLuint[n];
    indices[materialShaderLoc] = ambientIndex;
    glUniformSubroutineuiv(GL_VERTEX_SHADER, n, indices);
    delete []indices;
}

独立的着色器对象

OpenGL 4.1之前,应用程序中同一时间只能绑定一个着色器,但是碰到那种某一阶段有多个着色器使用上一阶段的数据的时候,只能将上一阶段的数据复制多份,再绑定到不同的该阶段着色器中,这样会造成资源浪费。

OpenGL4.1开始,可以使用独立着色器对象,它可以将不同的着色阶段合并到一个程序管线中。

首先,需要创建(用于着色器管线的)着色器程序

void glProgramParameteri(GLuint program,
 	                     GLenum pname,
 	                     GLint value);

pname设置为GL_PROGRAM_SEPARABLE,然后链接,这样该着色器程序就被标记为再管线中可用。

或者,可以使用新的函数:

GLuint glCreateShaderProgramv(GLenum type,
 	                          GLsizei count,
 	                          const char **strings);

type设置为GL_PROGRAM_SEPARABLE。这个函数从一个字符串中创建一个着色器程序,包含了编译,链接过程。

第二步,创建着色器管线

void glGenProgramPipelines(	GLsizei n,
 	                        GLuint *pipelines);

然后将着色器管线关联到当前上下文。

void glBindProgramPipeline(	GLuint pipeline);

然后就可以对程序进行编辑(添加,替换着色阶段)。删除管线可以使用:

void glDeleteProgramPipelines(	GLsizei n,
 	                            const GLuint *pipelines);

第三步,将独立的着色器程序绑定到着色器管线上:

void glUseProgramStages(GLuint pipeline,
 	                    GLbitfield stages,
 	                    GLuint program);

着色器阶段之间的接口必须注意in/out变量的匹配。非独立着色器对象再链接的时候就会检查这些接口的匹配情况;独立着色器对象在绘制-调用过程中才会检查,接口不正确会导致所有可变变量out变量未定义。

独立着色器对象中允许有各自独立的uniform集合。有两种方法来设置uniform变量:

一是,使用glActiveShaderProgram()选择活动的着色器程序。然后用glUniform*(), glUniformMatrix*()设置着色器程序中的uniform变量。

二是(推荐),glProgramUniform*()和glProgramUniformMatrix*()函数设置某个程序中的uniform值,而且这个不要求对应的程序是活动的。

OpenGL绘制方式

图元

点 线 三角形

void glPointSize(GLfloat size);

void glLineSize(Glfloat size);

线的类型细分有:条带线,循环线(首尾相接)。

三角形的类型有:独立三角形,条带,扇面。

图形学里面所有的多边形,复杂曲面,几何体都是用三角形拼接来的。多边形(三角形)的渲染有方式有:绘制点,(相邻点的)连线,填充三种方式,对应的mode是GL_POINT, GL_LINE, GL_FILL。

void glPolygonMode(GLenum face, GLenum mode);

图形学中的面有两个:正面、反面。OpenGL可以设置哪一个是正面(通过构成面的点的顺/逆时针排序),如果我们一直位于面的某一侧,可以设置只绘制正/反面(需要开启裁减开关GL_CULL_FACE)。方法如下:

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

OpenGL缓存数据

OpenGL的所有操作几乎都会涉及到缓存数据,这又会涉及到类型,创建,管理,销毁,优化方案。缓存对象是使用GLuint来命名的。

// 创建缓存对象的名称
void glGenBuffers(GLsizei n, GLuint* buffers);

// OpenGL使用一种优化的内存管理策略,只有缓存对象绑定到目标类型target,这个时候才会准备分配内存
void glBindBuffer(GLenum target, GLuint buffer);

// 为缓存对象读入数据,这里才是真正分配内存的时候。
// data为NULL就是用来初始化所在内存区域的。
// 如果新的数据大小比缓存对象当前分配的大,那么缓存对象将被重新分配大小;
// 如果相比较小的话,缓存对象将会收缩空间以适应大小。
void glBuferData(GLenum target, GLsizeiptr size, const GLvoid* data, GLenum usage);
// 部分读入数据
void glBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid* data);

使用glBufferData和glBufferSubData就可以对缓存对象进行分配和初始化了。示例代码:

static const GLfloat positions[] = {
    -1.0f, -1.0f, 0.0f, 1.0f,
     1.0f, -1.0f, 0.0f, 1.0f,
    -1.0f,  1.0f, 0.0f, 1.0f,
    -1.0f,  1.0f, 0.0f, 1.0f
};

static const GLfloat colors[] = {
    1.0f, 0.0f, 0.0f,
    0.0f, 1.0f, 0.0f,
    0.0f, 0.0f, 1.0f,
    1.0f, 1.0f, 1.0f
};

GLuint buffer;  // 缓存对象

glGenBuffer(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER,                   // 目标
            sizeof(positions) + sizeof(colors),  // 总计大小
            NULL,                               // 无数据
            GL_STATIC_DRAW);                    // 用途

glBufferSubData(GL_ARRAY_BUFFER,                // 目标
                0,                              // 偏移地址
                sizeof(positions),              // 大小
                positions);                     // 数据

glBufferSubData(GL_ARRAY_BUFFER,                
                sizeof(position),               
                sizeof(colors).
                colors);

如果仅仅是将缓存对象清除为某一个值,那么可以使用:

// 清除所有数据为某一个值。数据首先转为internalformat格式,然后填充缓存数据指定的区域范围。
void glClearBufferData(	GLenum target,
 	                    GLenum internalformat,
 	                    GLenum format,
 	                    GLenum type,
 	                    const void * data);

// 清除部分数据
void glClearBufferSubData(	GLenum target,
                        GLenum internalformat,
 	                    GLintptr offset,
 	                    GLsizeiptr size,
 	                    GLenum format,
 	                    GLenum type,
 	                    const void * data);

应用程序 & OpenGL的 内存数据拷贝

以下几个方法都会发生数据拷贝,但是拷贝的方向不同:

  • glBufferData(), glBufferSubData() 是由“应用程序”拷贝到“OpenGL管理的内存”。
  • glCopyBufferSubData() 只是拷贝源内存到目标内存,方向不定。
  • glGetBufferSubData() 拷贝“OpenGL管理的内存”到“应用程序”的内存。
void glCopyBufferSubData(GLenum readTarget,
 	                     GLenum writeTarget,
 	                     GLintptr readOffset,
 	                     GLintptr writeOffset,
 	                     GLsizeiptr size);

void glGetBufferSubData(GLenum target,
 	                    GLintptr offset,
 	                    GLsizeiptr size,
 	                    GLvoid * data);

想要在应用程序中访问OpenGL的内存还有一个方便的办法:

// 将缓存对象的全部内存映射到客户端(也就是应用程序)地址空间
void *glMapBuffer(GLenum target,   // 就是缓存绑定目标 Buffer Binding Target
 	              GLenum access);  // 可以控制GL_READ_ONLY, GL_WRITE_ONLY, or GL_READ_WRITE.
 	              
// 对应地,解除映射的方法
GLboolean glUnmapBuffer(GLenum target);

这个函数(glMapBuffer())会返回一个OpenGL管理的地址,但是这个地址不一定是图形处理器的内存区域,只是这个缓存对象本身对应的内存,OpenGL会在应用程序操作的内存和对应映射的图形处理器GPU所需的位置之间做数据的移动,以保证中间层的透明性,即让用户感觉就是在操作GPU内存,但是一旦发生了数据移动的情况就比较耗费性能,要慎重。

那么,既然glMapBuffer可以有机会写入缓存对象,那么就可以用来初始化缓存对象,参考例子:

GLuint buffer;
FILE *f;
size_t filesize;

// 打开文件
f = fopen("data.dat", "rb");
fseek(f, 0, SEEK_END);
filesize = ftell(f);
fssek(f, 0, SEEK_SET);

// 生成并绑定缓存对象
glGenBuffer(1, &buffer);
glBindBuffer(GL_COPY_WRITE_BUFFER, buffer);

// 创建缓存对象空间
glBufferData(GL_COPY_WRITE_BUFFER, (GLsizei)filesize, NULL, GL_STATIC_DRAW);
void *data = glMapBuffer(GL_COPY_WRITE_BUFFER, GL_WRITE_ONLY);

// 向缓存对象写入数据
fread(data, 1, filesize, f);

glUnmapBuffer(GL_COPY_WRITE_BUFFER);
fclose(f);

上述代码中的初始化方式,由于没有使用额外的内存空间,直接将数据拷贝进了OpenGL管理的指针指向的空间,并且解除映射之后也是由OpenGL管理的,那么就不与应用程序相互影响,可以说是与应用程序后续的操作同步进行的,就相当于同时使用了CPU和GPU,提高了程序整体的并发性能。

除了上面那个映射全部缓存对象内存空间的方法之外,还有一个可以控制部分区间的方法:

// 控制只映射缓存的部分空间区域
void *glMapBufferRange( GLenum target,
 	                    GLintptr offset,    // 区间的起始地址
 	                    GLsizeiptr length,  // 区间的大小
 	                    GLbitfield access); // 访问控制权限
 	                    
// 通知OpenGL所选区域中数据发生了变化,立即更新到缓存对象内存中去。
// 对应地glMapBufferRange中的控制位access必须包含GL_MAP_FLUSH_EXPLICIT_BIT,表示需要更新数据。
void glFlushMappedBufferRange(	GLenum target,
 	                            GLintptr offset,
 	                            GLsizeiptr length);

在读取大文件进缓存的时候,就需要glFlushMappedBufferRange来提升性能,原理如前所述,同时运行了CPU和GPU的功能。

如果是要直接丢弃缓存对象中的全部/部分数据,可以直接调用下面两个函数:

void glInvalidateBufferData(GLuint buffer);

void glInvalidateBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr length);

顶点

将顶点数组对象(包含数据的位置和布局信息)加载到着色器中可以使用下面的函数。

// 原型, 如果type是整型的话,需要通过这个参数控制来转化为浮点型才能放入浮点数的顶点属性中。
void glVertexAttribPointer(GLuint index,
 	                       GLint size,
 	                       GLenum type,
 	                       GLboolean normalized, 
 	                       GLsizei stride,
 	                       const GLvoid * pointer);
 	                        
// 变种一:这个函数是用来加载整型数据的,type只能使用整型
void glVertexAttribIPointer(GLuint index,
 	                        GLint size,
 	                        GLenum type,
 	                        GLsizei stride,
 	                        const GLvoid * pointer);

// 变种二:这个函数专门用来加载64位双精度浮点型数据,type只能是GL_DOUBLE。
void glVertexAttribLPointer(GLuint index,
 	                        GLint size,
 	                        GLenum type,
 	                        GLsizei stride,
 	                        const GLvoid * pointer);

type取压缩类型GL_INT_2_10_10_10_REV/GL_UNSIGNED_INT_2_10_10_10_REV的时候,可以通过节约内存空间和系统带宽而提升程序性能。

当使用上面的函数加载数据的时候,必须启用顶点属性数组:

void glEnableVertexAttribArray(GLuint index); 
void glDisableVertexAttribArray(GLuint index);

如果没有启用的话,OpenGL会使用静态顶点属性。这样设置是考虑到,如果有一个数据缓存中所有数值都是一样的,而创建并拷贝这样大小的缓存对象是一种浪费,完全可以考虑使用一个值来做初始化。而设置这个初始的值,可以通过下面的函数:

void glVertexAttrib{1234}{fds}(GLuint index, TYPE values);
void glVertexAttrib{1234}{fds}v(GLuint index, const TYPE* values);
void glVertexAttrib4{bsifd us ui}v(GLuint index, const TYPE* values);
void glVertexAttrib4Nub(GLuint index, GLubyte x, GLubyte y, GLubyte z, GLubyte w)
void glVertexAttribN{bsi ub us ui}v(GLuint index, const TYPE* values);
void glVertexAttribI{1234}{i ui}(GLuint index, TYPE values);
***

绘制命令

OpenGL中大部分绘制命令都是以Draw开头的。绘制命令分为:

  • 索引形式: 需要使用绑定GL_ELEMENT_ARRAY_BUFFER的缓存对象存储的索引数组
  • 非索引形式: 不需要绑定,直接按序读取顶点数组即可
// 最基本的索引绘制命令
void glDrawElements(GLenum mode,
 	                GLsizei count,
 	                GLenum type,
 	                const GLvoid * indices);

// 最基本的非索引绘制命令
void glDrawArrays(GLenum mode,
 	              GLint first,
 	              GLsizei count);

上面的mode可以取的值有:
GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_LINE_STRIP_ADJACENCY, GL_LINES_ADJACENCY, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_TRIANGLE_STRIP_ADJACENCY, GL_TRIANGLES_ADJACENCY 和 GL_PATCHES。

作为变种,还有下面的绘制命令

// 这个命令与glDrawElements类似,只不过在读取每个元素的时候,基于basevertex做了偏移。
// 即:indices[i] + basevertex作为每个元素的起始地址。
void glDrawElementsBaseVertex(GLenum mode,
 	                          GLsizei count,
 	                          GLenum type,
 	                          GLvoid *indices,
 	                          GLint basevertex);
 	                          
// glDrawElements的变种,意味着所有绘制元素皆位于start和end之间
void glDrawRangeElements(GLenum mode,
 	                     GLuint start,
 	                     GLuint end,
 	                     GLsizei count,
 	                     GLenum type,
 	                     const GLvoid * indices);
 	                     
// 将上面二者结合一下就是下面这个区域内元素偏移的命令
void glDrawRangeElementsBaseVertex(GLenum mode,
 	                               GLuint start
 	                               GLuint end,
 	                               GLsizei count,
 	                               GLenum type,
 	                               GLvoid *indices,
 	                               GLint basevertex);

间接绘制命令

这一类绘制命令需要先将缓存对象绑定到GL_DRAW_INDIRECT_BUFFER目标上。

非索引数组绘制指令
// glDrawArrays的间接版本
void glDrawArraysIndirect(GLenum mode,
 	                      const void *indirect);

上面这个函数它的绘制参数是通过indirect中获取的。其指向的结构体原型为:

typedef struct {
    uint  count;
    uint  primCount;
    uint  first;
    uint  baseInstance;  // 
} DrawArraysIndirectCommand;

const DrawArraysIndirectCommand *cmd = (const DrawArraysIndirectCommand *)indirect;
glDrawArraysInstancedBaseInstance(mode, cmd->first, cmd->count, cmd->primCount, cmd->baseInstance);
索引数组绘制指令
// 索引数组绘制glDrawElements的间接版本
void glDrawElementsIndirect(GLenum mode,
 	                        GLenum type,
 	                        const void *indirect);

// 上面这个函数它的绘制参数是通过indirect中获取的。其原型是:

typedef struct {
    uint  count;
    uint  primCount;
    uint  firstIndex;
    uint  baseVertex;
    uint  baseInstance;
} DrawElementsIndirectCommand;

// 内部实现等价于:
void glDrawElementsIndirect(GLenum mode, GLenum type, const void * indirect) {
    const DrawElementsIndirectCommand *cmd  = (const DrawElementsIndirectCommand *)indirect;
    glDrawElementsInstancedBaseVertexBaseInstance(mode,
                                                  cmd->count,
                                                  type,
                                                  cmd->firstIndex * size-of-type,
                                                  cmd->primCount,
                                                  cmd->baseVertex,
                                                  cmd->baseInstance);
}
多变量绘制命令(非Draw开头)
// glDrawArrays的multi绘制版本:
void glMultiDrawArrays(	GLenum mode,
 	                    const GLint * first,
 	                    const GLsizei * count,
 	                    GLsizei drawcount);
 	                    
// 这个函数的实现相当于:
void glMultiDrawArrays(	GLenum mode,
 	                    const GLint * first,
 	                    const GLsizei * count,
 	                    GLsizei drawcount) 
{
 	GLsizei i;
 	for (i = 0; i < drawcount; ++i) {
 	    glDrawArray(mode, first[i], count[i]);
 	}
}

// 类似地有glDrawElements的multi绘制版本:
void glMultiDrawElements(GLenum mode,
 	                     const GLsizei * count,
 	                     GLenum type,
 	                     const GLvoid * const * indices,
 	                     GLsizei drawcount);
 	                     
// 非索引数组的多变量间接绘制版本
void glMultiDrawArraysIndirect(	GLenum mode,
 	const void *indirect,
 	GLsizei drawcount,
 	GLsizei stride);

// 索引数组的多变量间接绘制版本
void glMultiDrawElementsIndirect(	GLenum mode,
 	GLenum type,
 	const void *indirect,
 	GLsizei drawcount,
 	GLsizei stride);

// 索引数组的多变量基址偏移版本
void glMultiDrawElementsBaseVertex(	GLenum mode,
 	const GLsizei *count,
 	GLenum type,
 	const GLvoid * const *indices,
 	GLsizei drawcount,
 	const GLint *basevertex);
图元的重启

在处理大量顶点数据集的时候,需要中断一下绘制指令,重新开始,可以用到:

// 设置一个顶点数组的索引值,指示从index + 1的地方重新开始绘制图元;
// 并且index处顶点不会被绘制。
void glPrimitiveRestartIndex(GLuint index);
多实例绘制命令

实例化(instance)或者多实例渲染(instanced rendering)是用来优化连续执行多条相同渲染命令,但是又有些微不同的方法。

// 最简单的多实例渲染命令,glDrawArrays的多实例版本。
void glDrawArraysInstanced(GLenum mode,
 	                       GLint first,
 	                       GLsizei count,
 	                       GLsizei primcount);
// 效果类似于:
if ( mode or count is invalid )
    generate appropriate error
else {
    for (int i = 0; i < primcount ; i++) {
        instanceID = i;
        glDrawArrays(mode, first, count);
    }
    instanceID = 0;
}

// 其他绘制指令也有对应的instanced版本,参考后面的绘制命令汇总
启用多实例顶点属性

在应用程序端来说,多实例顶点属性与一般的顶点着色器的声明和使用一样,步骤都是:

  • 使用glGetAttribLocation()查询位置
  • 使用glVertexAttribPointer()设置
  • 使用glEnableVertexAttribArray()/glDisableVertexAttribArray()来启用/禁用顶点属性

不同的地方是,多实例顶点属性要使用下面的函数启用:

// 每divisor个实例分配一个新的属性值。取0表示禁用。
void glVertexAttrivDivision(GLuint index, GLuint divisior);

在OpenGL服务端,可以写一个多实例的着色器,在应用程序端调用glDrawArraysInstanced()来绘制多实例。

多实例绘制代码

顶点着色器代码

#version 410 core

layout(location = 0) in vec4 position;
layout(location = 1) in vec3 normal;
layout(location = 2) in vec4 color;

// 变换矩阵,会占用3,4,5,6四个索引位
layout(location = 3) in mat4 model_matrix;

uniform mat4 view_matrix;
uniform mat4 projection_matrix;

out VERTEX {
    vec3 normal;
    vec4 color;
} vertex;

void main() {
    // 根据uniform的观察矩阵和逐实例的模型矩阵构建完整的模型视点矩阵
    mat4 model_view_matrix = view_matrix * projection_matrix;
    // 先使用模型视点矩阵变换位置,然后是投影矩阵
    gl_Position = projection_matrix * (model_view_matrix * position);
    
    // 使用模型视点矩阵的左上3x3元素变换法线
    vertex.normal = mat3(model_view_matrix) * normal;
    // 逐值拷贝颜色
    vertex.color = color;
}

应用程序示意代码,假定一些变量已经初始化好了。这个代码是在应用程序内设置使用多实例代码。

// 设置顶点属性代码
int position_loc = glGetAttribLocation(prog, "position");
int nornal_loc   = glGetAttribLocation(prog, "normal");
int color_loc    = glGetAttribLocation(prog, "color");
int matrix_loc   = glGetAttribLocation(prog, "model_matrix");

// 位置信息的一般设置
glBindBuffer(GL_ARRAY_BUFFER, position_buffer);
glVertexAttribPointer(position_loc, 4, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexAttribArray(position_loc);
// 法线信息的一般设置
glBindBuffer(GL_ARRAY_BUFFER, normal_buffer);
glVertexAttribPointer(normal_loc, 3, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexAttribArray(normal_loc);

// 希望每一个几何体都有一个不同的颜色。这里假设color_buffer中已经存了不同的色值。
// 故而要将颜色值放入缓存对象,而后设置一个实例化的顶点属性。
glBindBuffer(GL_ARRAY_BUFFER, color_buffer);
glVertexAttribPointer(color_loc, 4, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexAttribArray(color_loc);
// 设置颜色数组的更新频率为1,这样OpenGL就会给每1个实例绘制一个颜色,而不是每个顶点
glVertexAttribDivision(color_loc, 1);


glBindBuffer(GL_ARRAY_BUFFER, model_matrix_buffer);
for ( int i = 0; i < 4; ++i) {
    glVertexAttribPointer(matrix_loc + i,           // 位置
                          4, GL_FLOAT, GL_FALSE,    // vec4
                          sizeof(mat4),             // 数据步长
                          (void*)(sizeof(vec4) * i);// 起始偏移值

    glEnableVertexAttribArray(matrix_loc + i);
    glVertexAttrivDivision(matrix_loc + i, 1);
}

// 绘制代码
mat4 * matrices = (mat4 *)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);

for (n = 0; n < INSTANCE_COUNT; n++) {
    float a = 50.0f * float(n) / 4.0f;
    float b = 50.0f * float(n) / 5.0f;
    float c = 50.0f * float(n) / 6.0f;

    matrices[n] = rotate(a + t * 360.0f, 1.0f, 0.0f, 0.0f) *
                  rotate(b + t * 360.0f, 0.0f, 1.0f, 0.0f) *
                  rotate(c + t * 360.0f, 0.0f, 0.0f, 1.0f) *
                  translate(10.0f + a, 40.0f + b, 50.0f + c);
}

glUnmapBuffer(GL_ARRAY_BUFFER);
// 使用多实例程序
glUseProgram(render_prog);

mat4 view_matrix(translate(0.0f, 0.0f, -1500.0f) * rotate(t * 360.0f * 2.0f, 0.0f, 1.0f, 0.0f));
mat4 projection_matrix(frustum(-1.0f, 1.0f, -aspect, aspect, 1.0f, 5000.0f));

glUniformMatrix4fv(view_matrix_loc, 1, GL_FALSE, view_matrix);
glUniformMatrix4fv(projection_matrix_loc, 1, GL_FALSE, projection_matrix);

// 渲染多个实例
glDrawArraysInstanced(GL_TRIANGLES, 0, object_size, INSTANCE_COUNT);

多实例顶点属性除了绘制顶点外,还可用于将一系列纹理打包到一个纹理数组。

多实例顶点的计数器

gl_instanceID作为一个整数,它从0开始,每一个实例被渲染之后自增1。

绘制命令汇总

元素绘制的全部命令如下:

  • glDrawArrays
  • glDrawArraysIndirect
  • glDrawArraysInstanced
  • glDrawArraysInstancedBaseInstance
  • glDrawElements
  • glDrawElementsBaseVertex
  • glDrawElementsIndirect
  • glDrawElementsInstanced
  • glDrawElementsInstancedBaseInstance
  • glDrawElementsInstancedBaseVertex
  • glDrawElementsInstancedBaseVertexBaseInstance
  • glDrawRangeElements
  • glDrawRangeElementsBaseVertex
  • glMultiDrawArrays
  • glMultiDrawArraysIndirect
  • glMultiDrawElements
  • glMultiDrawElementsBaseVertex
  • glMultiDrawElementsIndirect

颜色,像素,帧缓存

计算机图形学的目的就是计算一幅图像里面一个像素的颜色值。

缓存

用途

帧缓存,是有矩阵的像素数组组成的。

OpenGL系统通常包含3种类型的缓存:

  • 一个或者多个颜色缓存 color buffer
  • 深度缓存 depth buffer
  • 模板缓存 stencil buffer

所有这些缓存都集成到帧缓存当中。我们可以控制使用哪些缓存。应用程序启动之后使用默认的帧缓存,它与应用程序窗口关联。

颜色缓存

是我们进行绘制的缓存对象。包含RGB/sRGB/alpha值。帧缓存中可能包含多个颜色缓存,但是主颜色缓存是与屏幕上窗口直接关联的,其他的颜色缓存都是离屏缓存。为了支持立体显示(3D 或者 VR吗?),还可能每个颜色缓存分为左颜色缓存和右颜色缓存。

深度缓存

深度缓存为每个像素保存一个深度值,用来做深度测试,最终决定是否显示。也叫Z缓存(Z-buffer)

模板缓存

作用是限制屏幕特定区域绘制内容。一个例子是后视镜内容的绘制。

缓存的清除

绘制完一帧之后,设置为某一个颜色缓存、深度缓存、模板缓存的清除值。设置的命令有:

void glClearColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
void glClearDepth(GLdouble depth);
void glClearDepthf(GLfloat depth);
void glClearStencil(GLint s);

设置好之后,调用glClear()来执行清除命令:

// mask取值:GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT,  GL_STENCIL_BUFFER_BIT
void glClear(GLbitfield mask);

OpenGL在往颜色,深度,模板缓存写入数据之前可以使用掩码执行一次数据操作:

void glColorMask(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);
void glColorMaski(GLuint buf, GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);
void glDepthMask(GLboolean flag);// GL_TRUE可以写入,否则不能写入深度缓存。
void glStencilMask(GLuint mask); // 按位与,结果为1模板值可写入模板缓存,否则不能。
void glStencilMaskSeparate(	GLenum face, GLuint mask); // 可以为多边形设置正/反面掩码。

OpenGL如何使用颜色

片元着色器用于为每个片元着色。方法有:

  • 设置常量颜色
  • 每个顶点自带色值,在片元着色器之前的着色阶段(顶点着色器)简单处理后传入片元着色器。
  • 计算得来
  • 来自纹理贴图,在片元着色器中引用

一般而言,不做特别的设置,OpenGL内部是使用[0, 1.0]范围来表示颜色值的,这个叫归一化数值(normalized value),写入到帧缓存之后,再根据帧缓存支持的数据区间映射为不同的值,比如[0.0, 1.0]映射到[0, 255]。但是应用程序会使用int,short,byte,float,以及对应无符号版本这类C语言数据类型,这时候就需要让OpenGL将这些值做归一化,即使用glVertexAttribPointer()/glVertexAttribN*()。

在顶点数据中加入了颜色信息,那么就需要将颜色信息从顶点着色器阶段传给片元着色器阶段。方法是添加一个颜色的输入和输出值。

顶点着色器代码:

#version 400 core

layout( location = 0 ) in vec4 vPosition;
layout( location = 1 ) in vec4 vColor;

// 添加这个变量为了传递给下一阶段的片元着色器。
out vec4  color;

void main()
{
   
   
    // 颜色的设置方法也很简单,只是简单的赋值。
    color = vColor;
    gl_Position = vPosition;
}

片元着色器代码:

#version 400 core

in  vec4 color;

out vec4 fColor;

void main()
{
   
   
    // 将输入颜色值与输出颜色值关联。
    // 但是,片元着色器的输入值不是来源于顶点着色器,而是光栅化的结果。
    fColor = color;
}

光栅化 在OpenGL管线中,顶点着色阶段(顶点,细分,几何着色)与片元着色阶段之间的过程叫做光栅化。光栅化是片元生命的开始,片元着色器中得到的结果就是片元的最终颜色。光栅化的主要内容是:计算落在屏幕区域中的几何体,结合输入的顶点数据,对片元着色器中的每个变量进行插值,然后将结果输入到片元着色器中。注意,插值运算是依赖具体实现的,不保证不同平台的一致性。

多重采样 multisampling

对几何图元的边缘进行平滑处理,所以也叫反走样(antialiasing)。具体实现很多。多重采样是对图元的每个像素进行多次采样,每个像素点保存多个样本值(颜色值,有时候外加深度和模板值),到最终需要结果的时候,再解析为最终的颜色值。

应用程序中,获取某个像素采样值得具体操作:

  • 调用glGetIntegerV(GL_SAMPLE_BUFFERS)查询机器是否支持多重采样,如果是的话,继续
  • 调用glEnable(GL_MULTISAMPLE)开启多重采样
  • 调用glGetIntegerv(GL_SAMPLES)得到每个像素有多少个样本用于多重采样,记为N
  • 调用glGetMultiSamplefv(GLenum pname, GLuint index, GLfloat* val); pname设置为GL_SAMPLE_POSITION,对index(小于上一步的返回值)位置的像素采样样本进行查找。

片元着色器中,可以使用读取gl_SamplePosition得到多重采样绘制缓存中某个位置的样本集合,gl_SampleID记录了片元着色器正在处理哪个样本值,取值范围是[0, N)。如果使用这两个变量,那么片元着色器会对每个像素进行多次计算,每次计算得到不同的样本位置信息。使用sample限制输入变量的话,也会有一样的效果。

如果不能使用sample修饰片元着色器的输入变量,可以通过glEnable(GL_SAMPLE_SHADING)强制开启多重采样。也可以使用下面的函数设置最少采样着色比率:

// value表示独立着色样本占样本总数的比例
void glMinSampleShading(GLfloat  value);

片元的测试与操作

OpenGL管线中,片元进入到帧缓存之前会依次执行下面的完整测试:

  • 剪切测试
  • 多重采样的片元操作
  • 模板测试
  • 深度测试
  • 融合
  • 抖动
  • 逻辑操作

执行其中任何一个操作中片元被丢弃,那么就没有后续的操作了。

剪切测试 GL_SCISSOR_TEST

片元可见性判断的第一个附加测试,设置程序窗口中的一个区域,所有绘制操作(包括窗口的清除),如果在这个区域内,则测试通过。

void glScissor(GLint x, GLint y, GLsizei width, GLsizei height);
多重采样的片元操作

默认情况下,多重采样在计算片元的覆盖率的时候不会考虑alpha值,但是开启下面的开关的话,就会考虑alpha值。

  • GL_SAMPLE_ALPHA_TO_COVERAGE 使用片元的alpha值来计算最终片元的覆盖率
  • GL_SAMPLE_ALPHA_TO_ONE 将片元的alpha值设为最大,而后使用这个值来计算片元的覆盖率
  • GL_SAMPLE_COVERAGE 与glSampleCoverage()合用
  • GL_SAMPLE_MASK 与函数glSampleMask()合用,设置一个精确的掩码值。
// 当开启GL_SAMPLE_TO_COVERAGE、GL_SAMPLE_COVERAGE的时候,
// 将使用value作为临时的采样覆盖值,invert控制是否要先做按位反转,然后再与片元覆盖率进行与操作。
void glSampleCoverage(GLfloat value, GLboolean invert);

// 设置一个32bit的采样掩码,掩码原本的位置由index确定,设置新的掩码mask。
// 采样结果写入帧缓存中前,进行掩码运算,其余值丢弃。
void glSampleMaski(GLuint index, GLbitfield mask);
模板测试 GL_STENCIL_TEST

当建立窗口的过程中先请求模板缓存,才能使用模板测试。没有模板缓存,模板测试都算是通过。

模板测试过程中,会取模板缓存中的像素值stencil与一个参考值ref进行比较,根据测试结果对模板缓存中的数据进行修改。模板测试默认禁用,默认func为GL_ALWAYS,ref为0,mask为1。

// func可以取的值:GL_NEVER, GL_LESS, GL_LEQUAL, GL_GREATER,
// GL_GEQUAL, GL_EQUAL, GL_NOTEQUAL和GL_ALWAYS
void glStencilFunc(GLenum func, GLint ref, GLuint mask);

// 可设置多边形图元的正反面的不同参数
void glStencilFuncSeperate(GLenum face, GLenum func, GLint ref, GLuint mask);

// 具体计算方法:
// GL_NEVER 总是失败.
// GL_LESS 如果 ( ref & mask ) < ( stencil & mask )通过.
// GL_LEQUAL 如果 ( ref & mask ) <= ( stencil & mask )通过.
// GL_GREATER 如果 ( ref & mask ) > ( stencil & mask )通过.
// GL_GEQUAL 如果 ( ref & mask ) >= ( stencil & mask )通过.
// GL_EQUAL 如果 ( ref & mask ) = ( stencil & mask )通过.
// GL_NOTEQUAL 如果 ( ref & mask ) != ( stencil & mask )通过.
// GL_ALWAYS 总是通过. 默认值。

当模板测试通过或者没有通过的时候,OpenGL还允许设置如何处理模板中的数据。

void glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass);
void glStencilOpSeparate(GLenum face,
                 GLenum sfail, GLenum dpfail, GLenum dppass);

sfail, dpfail, dppass都可以取值:

  • GL_KEEP 保持模板缓存中当前值
  • GL_ZERO 设置模板缓存中的值为0
  • GL_REPLACE 设置为参考值ref(由glStencilFunc*设置)
  • GL_INCR 模板缓存中的值自增,超过最大值就走向另一端
  • GL_INCR_WRAP 模板缓存中的值自增,最大值就是区间端点值
  • GL_DECR 模板缓存中的值自减,最小为0.
  • GL_DECR_WRAP 模板缓存中的值自减,超过最小值就会变成最大的无符号整数值
  • GL_INVERT 模板缓存中的值按位反转

函数的意思是:

  • 如果片元没有通过模板测试,就执行sfail函数
  • 如果片元通过了模板测试,但是没有通过深度测试,就执行dpfail函数
  • 如果通过了模板测试,也通过了深度测试,或者没有开启深度测试,就执行dppass函数

可以使用glIntegerv()查询模板测试中的相关参数:GL_STENCIL_FUNC, GL_STENCIL_REF, GL_STENCIL_FAIL ……

模板测试的一个例子,在屏幕中间一个区域内,阻止任何绘制。思路是:

首先启用模板测试,并设置模板缓存清除值为0。当窗口变换的时候,清除模板缓存,模板区域的绘制总是通过模板测试(glStencilFunc(GL_ALWAYS, 0x1, 0x1)),并设置模板缓存值为1(glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE));然后进行图元绘制,绘制图元的时候,当模板值是1的时候,保持模板值不变;当模板值不是1的时候,绘制图元。

深度测试 GL_DEPTH_TEST

深度测试的主要用途是隐藏表面消除,就是说先比较深度缓存中的像素点和待渲染物体的像素点距离视点的距离,如果发现距离视点较近,那么就显示;否则就隐藏,这个就叫做隐藏表面消除。所以一般情况下都是先设定一个较远的距离作为深度缓存的初始值,这样的话,简单地开启深度测试就可以了GL_DEPTH_TEST,并且每绘制一帧都需要清除深度缓存。

使用实例:多边形偏移。这个是为了实现让多边形的边线变亮的一个需要。先启用GL_FILL的填充方式绘制一遍图元,然后再换个颜色启用GL_LINE的方式绘制一遍。理论上这样可行,但实际上由于两次绘制的位置一样,会出现两种颜色有规律出现的斑驳。解决办法是在绘制第二次的时候,做一点Z方向上的偏移,让第二次绘制的线脱离图元靠近视点。

这样具体偏移多少就有点讲究:

  • 首先用glEnable()开启GL_POLYGON_OFFSET_FILL/GL_POLYGON_OFFSET_LINE/GL_POLYGON_OFFSET_POINT。
  • 然后用glPolygonMode()设置多边形光栅化方式
  • 最后设置多边形偏移值

多边形的深度斜率示意图:

在这里插入图片描述

计算一个多边形最大深度斜率的方法:其实就是最大斜率的边在z方向上的变化Δz,与其在水平于视口截面上的投影长度的比值。

在这里插入图片描述

由于深度值一般被限制在[0, 1],所以上面这个式子的一个近似计算方式是:

在这里插入图片描述

// mode取值:GL_POINT, GL_LINE, GL_FILL
void glPolygonMode(GLenum face,GLenum mode);
// 最终的偏移值的计算方式:offset = factor * m + units * r;
// m是多边形最大深度斜率,r是不同深度值之间最小可识别差值,它是一个常量,具体依赖于不同的平台。
void glPolygonOffset(GLfloat factor, GLfloat units);

具体的代码设置,参考:

// 初始化深度缓存
glClearDepth(1.0f);
glClear(GL_DEPTH_BUFFER_BIT);

// 开启多边形偏移,并设置偏移值
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(2.0f, 4.0f);
// 绘制场景
DrawScene(true);
glDisable(GL_POLYGON_OFFSET_FILL);

所以水平于远近裁切平面的多边形的边,其深度斜率就是0,于是只需要一个很小的偏移值,这时候设置factor为0,units取1.0就可以了;而几乎垂直于这平面的边其深度斜率接近于tan(90°),值就特别大,于是偏移值就需要很大,这时候设置factor为一个较小的值,比如0.75、1.0,units取0即可。

融混 GL_BLEND

如果一个片元通过了所有的测试,那么就可以与颜色缓存当中的值通过某种方式融混,最直接的方式是覆盖。这里谈到融混就需要提到alpha值。

开启融混是要使用glEnable(GL_BLEND);

计算融混结果涉及到两个操作数:源融混参数和目标融混参数。源融混参数是片元着色器输出的颜色,目标融混参数对应的是帧缓存中的颜色。具体融混计算公式是:

在这里插入图片描述

控制源、目标融混参数需要使用命令:

// 设置所有绘制缓存的融混参数
void glBlendFunc(GLenum sfactor, GLenum dfactor);
// 设置某一个绘制缓存的融混参数
void glBlendFunci(GLuint buf, GLenum sfactor, GLenum dfactor);

// 设置所有绘制缓存的融混参数
void glBlendFuncSeparate(GLenum srcRGB,
 	                     GLenum dstRGB,
 	                     GLenum srcAlpha,
 	                     GLenum dstAlpha);
// 设置某一个绘制缓存的融混参数
void glBlendFuncSeparatei(GLuint buf,
 	                      GLenum srcRGB,
 	                      GLenum dstRGB,
 	                      GLenum srcAlpha,
 	                      GLenum dstAlpha);

具体sfactor/dfactor的参数取值说明:

在这里插入图片描述

取值为GL_CONSTANT_COLOR的时候,需要设置一个常量颜色值,使用命令:

void glBlendColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);

此外,设置glDisable(GL_BLEND)禁用掉颜色融混, 效果上跟设置源参数为GL_ONE,同时设置目标参数为GL_ZERO一样。因为这两个分别都是默认值。

除了上面设置源,目标融混参数的命令之外,还有命令可以设置融混方式,比如取较大的值,相加/减:

void glBlendEquation(GLenum mode);
void glBlendEquationi(GLuint buf, GLenum mode);

void glBlendEquationSeparate(GLenum modeRGB, GLenum modeAlpha);
void glBlendEquationSeparatei(GLuint buf, GLenum modeRGB, GLenum modeAlpha);

参数的含义:

在这里插入图片描述

抖动 GL_DITHER

对于颜色位平面数目较少的系统来说,通过抖动可以提升颜色的分辨率,代价这是损失一定的空间分辨率。可以通过glEnable(GL_DITHER)来开启;不过默认就是开启的,抖动操作是硬件相关的。这里有几个概念要弄清楚,首先,什么是颜色位平面,什么是空间分辨率,什么是抖动。详细的抖动概念可以参考维基百科Dither中的。抖动应该是最初源自信号处理,是一种噪声的人为利用。说到颜色的位平面,就需要了解一下图像显示的知识,在图像发展历史上有几种硬件出现,二值图像显示器,黑白灰阶图像显示器,彩色图像显示器,还有高/真彩色图像显示器。其中最后面的这个才是那种可以直接读取RGB连续存储值来显示的硬件,前面的都采用了不同的显示原理,比如用到颜色位面的,就是彩色显示器中用到的一个图像显示原理。参考网上的一个描述:

像素与帧缓存

在组合像素法中,一个图形像素点的全部信息被编码为一个数据字节,按照一定方式存储到帧缓存中,编码字节的长度与点的属性(如颜色、灰度)有关。

在颜色位面法中,帧缓存被分为若干独立的存储区域、每一个区域称为一个位面,每个位面控制一种颜色或者灰度,每一个图形像素点在每个位面中占一位,通过几个位面中的同一位组合成一个像素。

颜色查找表也称调色板 ,是由高速的随机存储器组成,用来储存表达象素色彩的代码。此时帧缓冲存储器中每一象素对应单元的代码不再代表该象素的色彩值 ,而是作为查色表的地址索引 。

由上面的描述大概可以得知,为了让显示能力有限的硬件显示的图像好看一点,需要做一点“处理”,这就是抖动。所以在现在能够显示真彩色的显示器上,这种方法效果甚微。

片元的最后一个操作,是在输入片元数据(源)与当前颜色缓存中的颜色数据(目标)进行逻辑操作:或、异或、取反操作。

图形窗口中将一块区域的数据拷贝至另一个地方的块操作(bit-blt),其实现的原理一般是将源数据和目标数据进行一次逻辑操作,然后直接使用结果覆盖目标区域。由于这一操作的实现对于硬件来说非常低廉,所以很多系统都支持。要设置具体的逻辑操作,需要启用GL_COLOR_LOGIC_OP,并调用命令进行具体的设置:

void glLogicOp(GLenum Opcode);

Opcode	         操作结果解释
GL_CLEAR	        0
GL_SET	            1
GL_COPY	            s
GL_COPY_INVERTED	~s
GL_NOOP	            d
GL_INVERT	        ~d
GL_AND	            s & d
GL_NAND	            ~(s & d)
GL_OR	            s | d
GL_NOR	            ~(s | d)
GL_XOR	            s ^ d
GL_EQUIV	        ~(s ^ d)
GL_AND_REVERSE	    s & ~d
GL_AND_INVERTED	    ~s & d
GL_OR_REVERSE	    s | ~d
GL_OR_INVERTED	    ~s | d

不调用该命令的话,默认是GL_COPY。

遮挡查询

一般情况下,使用深度测试可以确定一个像素是否应该显示,但是,对于一个复杂的几何体,如果能够事先确定是否被遮挡,那么就可以省去OpenGL管线中后续一系列复杂的操作,从而提升性能。这里就出现遮挡查询的想法了,具体操作步骤:

  1. 为待查询的对象生成一个ID: void glGenQueries(GLsizei n, GLuint* ids);
  2. 用void glBeginQuery(GLenum target, GLuint id)查询
  3. 渲染几何体,完成遮挡测试
  4. 用void glEndQuery(GLenum target)完成查询
  5. 获取本次通过深度测试的样本

最后一步查询结果,调用命令:

// id是生成的查询ID,当pname是GL_QUERY_RESULT的时候,
// 通过深度测试的片元或者样本(如果开启了多重采样)的数量会写在params中。
// 如果结果为0,表示全部被遮挡。
void glGetQueryObjectiv(GLuint id,
 	                    GLenum pname,
 	                    GLint * params);
 
void glGetQueryObjectuiv(GLuint id,
 	                     GLenum pname,
 	                     GLuint * params);

在操作开始前应该禁用掉所有渲染模式,操作结束了再恢复。

需要注意,执行最后一步的结果查询可能会有一些延迟,往往不能只调用一次就给出结果。所以需要特殊处理一下:

count = 1000;

GLuint queryID;
glGenQueries(1, &queryID);
glBeginQuery(GL_SAMPLES_PASSED, queryID);
glDrawArrays(...);
glEndQuery(GL_SAMPLES_PASSED);

// 这里需要检查查询结果是否可用了
GLuint queryRead = GL_FALSE;
while (!queryReady && count--) {
   
   
    glGetQueryObjectiv(queryID, GL_QUERY_AVAILABLE, &queryReady);
}

// 删除查询引用
glDeleteQueries(1, &queryID);

if (queryReady) {
   
   
    glGetQueryObjectiv(queryID, GL_QUERY_RESULT, &samples);
} else {
   
   
    // 失败的话,就只做一次渲染
    samples = 1;
}

if (samples > 0) {
   
   
    // 未被完全遮挡
    glDraw***();
}

上面提到的其他命令:

// 创建和删除查询对象ID
void glGenQueries(GLsizei n, GLuint * ids);
void glDeleteQueries(GLsizei n, const GLuint * ids);

// target可取的值:GL_SAMPLES_PASSED, GL_ANY_SAMPLES_PASSED, GL_ANY_SAMPLES_PASSED_CONSERVATIVE, 
// GL_PRIMITIVES_GENERATED, GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, or GL_TIME_ELAPSED.
void glBeginQuery(GLenum target, GLuint id);
void glEndQuery(GLenum target);
条件渲染

前面提到,查询结果的时候会有一个等待,对于现代的硬件来说,等待是会损耗性能的。为此,OpenGL提出一个解决办法就是条件渲染。思路是通过记录一系列OpenGL渲染命令,然后根据遮挡查询ID的结果来决定是否自动丢弃这些渲染命令。相关的命令:

// mode用来控制:
// GL_QUERY_WAIT           GPU是否在取得查询结果之后才开始渲染
// GL_QUERY_NO_WAIT        GPU可以不用等待结果返回,如果未返回的话,渲染一部分
// GL_QUERY_BY_REGION_WAIT GPU判断片元的结果是否对条件渲染有所影响,并等待渲染解释。
//                         也会等待完整的遮挡查询返回
// GL_QUERY_BY_REGION_NO_WAIT GPU丢弃帧缓存中对遮挡查询没有影响的区域,
//                            不等待查询返回,就开始渲染其他区域。
void glBeginConditionalRender(GLuint id, GLenum mode);
void glEndConditionalRender(void);

具体代码在前面的基础上修改:

GLuint queryID;
glGenQueries(1, &queryID);

glBeginQuery(GL_SAMPLES_PASSED, queryID);
glDrawArrays(...);
glEndQuery(GL_SAMPLES_PASSED);

// 不做遮挡查询的结果while循环判断和结果验证
glBeginConditionalRender(queryID, GL_QUERY_WAIT);
glDrawArrays(...);
glEndConditionalRender();

逐图元的反走样

反走样是要计算理论曲线落在对应每个像素位置上的覆盖率。然后OpenGL会将这个覆盖率与片元的alpha值相乘,在颜色融混(片元与帧缓存中已有颜色融混)的时候使用这个alpha值。

覆盖率的计算是依赖于不同的实现的,OpenGL可以给这个计算做一些提示,但是不保证一定起作用:

// 这个函数就是用来设置一些依赖具体实现的功能提示的。
// target取值:GL_LINE_SMOOTH_HINT, GL_POLYGON_SMOOTH_HINT, 
//   GL_TEXTURE_COMPRESSION_HINT, GL_FRAGMENT_SHADER_DERIVATIVE_HINT
// mode取值:GL_FASTEST(最快), GL_NICEST(最高质量), GL_DONT_CARE(没有偏好,看硬件吧实现!)
void glHint(GLenum target, GLenum mode);

对线,多边形的边开启反走样的方法是:glEnable(GL_LINE_SMOOTH_HINT)和glEnable(GL_POLYGON_SMOOTH_HINT)。

glEnable(GL_LINE_SMOOTH);//启用线段平滑
glEnable(GL_BLEND);//启用融混
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);//设置融混参数
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); // 设置线段平滑质量
glDrawArrays(...);

glEnable(GL_POLYGON_SMOOTH);//启用多边形边平滑
glEnable(GL_BLEND);//启用融混
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);//设置融混参数
glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST); //设置图形平滑质量
glDrawArrays(...);

还有,为了防止一些像素在经过深度测试之后被抛弃,导致后面融混的时候,由于丢失了这部分像素信,会造成融混结果不正确,所以有时候需要适当地禁用深度检测。

帧缓存对象

窗口系统的帧缓存是图形服务器的显示系统唯一可以识别的帧缓存。它是在系统创建窗口的时候创建的,我们屏幕上看到的东西都是这里的内容。但是还有一些需要做离屏渲染的东西,这个时候就需要使用(应用程序自己创建和管理的)帧缓存对象了,当然对应的也有这个帧缓存对象关联的帧缓存。

// 帧缓存对象相关的操作命令
void glGenFramebuffers(GLsizei n, GLuint *ids);
void glBindFramebuffer(GLenum target, GLuint framebuffer);
GLboolean glIsFramebuffer(GLuint framebuffer);
void glDeleteFramebuffers(GLsizei n, GLuint *framebuffers);

// 以及,在帧缓存对象还未关联任何帧缓存之前,设置一些参数,比如宽,高,采样层……
// target取值:GL_DRAW_FRAMEBUFFER, GL_READ_FRAMEBUFFER, GL_FRAMEBUFFER
// pname取值:GL_FRAMEBUFFER_DEFAULT_WIDTH, GL_FRAMEBUFFER_DEFAULT_HEIGHT,
//            GL_FRAMEBUFFER_DEFAULT_LAYERS, GL_FRAMEBUFFER_DEFAULT_SAMPLES,
//            GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS
// param就是对应pname的新值
void glFramebufferParameteri(GLenum target, GLenum pname, GLint param);

// 在同一块缓存的不同区域之间拷贝,或者在不同缓存之间拷贝不同区域。
void glBlitFramebuffer(	GLint srcX0,
                     	GLint srcY0,
                     	GLint srcX1,
                     	GLint srcY1,
                     	GLint dstX0,
                     	GLint dstY0,
                     	GLint dstX1,
                     	GLint dstY1,
                     	GLbitfield mask,
                     	GLenum filter);

渲染缓存

渲染缓存(不局限于颜色数据哦)是OpenGL管理的一块高效内存区域,可以存储格式化的图像数据。但是渲染缓存中的数据只有关联了帧缓存对象之后才有实际使用意义。渲染缓存与帧缓存对象有类似的命令:

void glGenRenderbuffers
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值