本文参考:https://learnopengl.com/。

基础配置

下载 GLFWURL(可以下载源码自己编译,或者直接下载编译后的二进制文件)

Windows 平台 opengl32.lib 包含在 Microsoft SDK 里,在Visual Studio安装的时默认安装了。

OpenGL只是一个标准、规范,具体实现是由驱动开发商针对特定显卡实现的。

OpenGL驱动版本众多,大多数函数的位置都无法在编译时确定下来,需要在运行时查询。开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用。取得地址的方法因平台而异,Windows上是这样:

// 定义函数原型
typedef void (*GL_GENBUFFERS) (GLsizei, GLuint*);
// 找到正确的函数并赋值给函数指针
GL_GENBUFFERS glGenBuffers  = (GL_GENBUFFERS)wglGetProcAddress("glGenBuffers");
// 现在函数可以被正常调用
GLuint buffer;
glGenBuffers(1, &buffer);

需要对每个可能使用的函数都重复这个过程,GLAD可以简化这样的操作。

可以在这里在线加载某个版本所有相关 OpenGL 函数,下面是我的生成设置。

生成后解压放到合适的位置,进行 VS 的相关配置如下:

  • VC ++ 目录
    • 包含目录:加入GLFW 和 GLAD 的 include 文件夹。
    • 库目录:对应版本(x86/x64、lib-vc20xx)的 GLFW 库文件。
  • 链接器
    • 输入:加入 opengl32.lib 和 glfw3.lib。
  • 把生成的 glad.c 文件放到工程目录中。

渲染

概述

OpenGL 大部分工作是把3D坐标转变为适应屏幕的2D像素。

图形渲染管线主要有两部分:第一部分把3D坐标转换为2D坐标,第二部分把2D坐标转变为实际有颜色的像素。

图形渲染管线若干部分,每个部分将会把前一个阶段的输出作为输入,这些部分高度专门化(有一个特定函数),很容易并行执行。当今大多数显卡都有成千上万小处理核心,在GPU上为每一个(渲染管线)部分运行各自的小程序(shader),在图形渲染管线中快速处理数据。

有些部分允许开发者自己写着色器来替换默认的着色器(下图蓝色部分),下面按顺序介绍。

  • 顶点着色器(Vertex Shader):一个单独的顶点作为输入,把一种 3D 坐标转为另一种 3D 坐标,并对顶点属性进行一些基本处理。

  • 图元装配(Primitive Assembly):顶点着色器输出的所有顶点作为输入,把所有的点装配成指定图元的形状。

  • 几何着色器(Geometry Shader):把图元形式的一系列顶点的集合作为输入,可以通过产生新顶点构造出新的图元来生成其他形状。

  • 光栅化(Rasterization):把图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段(fragment)。

    在OpenGL中,一个片段是OpenGL渲染一个像素所需的所有数据。

  • 片段着色器(Fragment Shader):主要是计算一个像素的最终颜色(所有OpenGL高级效果产生的地方)。

    片段着色器含3D场景的数据(如光照、阴影、光的颜色等),这些数据被用来计算最终像素的颜色。

  • Alpha 测试和混合(Blending):检测片段对应深度(和模板(Stencil))值,判断这个像素是在它物体的前面还是后面,决定是否应该丢弃;检查alpha值(透明度),并对物体进行混合。

    片段着色器中计算出来一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

在顶点着色器中处理过的坐标是标准化设备坐标(Normalized Device Coordinates, NDC)

x、y、z 坐标范围在 [-1, 1] 之间,在范围外的坐标都会被丢弃、裁剪,不会显示在屏幕上。

通过 glViewport 函数提供的数据,进行视口变换(Viewport Transform),标准化设备坐标会变换为屏幕空间坐标(Screen-space Coordinates)。

  • 顶点数组对象:Vertex Array Object,VAO
  • 顶点缓冲对象:Vertex Buffer Object,VBO
  • 索引缓冲对象:Element Buffer Object,EBO或Index Buffer Object,IBO

GLSL

GLSL 是C类语言写成的,包含一些针对向量和矩阵操作的特性,整体架构如下:

#version version_number

// layout (location = n) 顶点着色器
in type in_variable_name;

out type out_variable_name;

uniform type uniform_name;

void main()
{
}

顶点着色器的输入变量也叫顶点属性(Vertex Attribute)。

OpenGL 至少有 16 个 4 分量顶点属性,有些硬件允许更多顶点属性,用 GL_MAX_VERTEX_ATTRIBS 获取上限。

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);

基础数据类型intfloatdoubleuintbool

向量:通过 xyzwrgbastpq(纹理)获取各个分量。

类型 含义
vecn 包含nfloat分量
bvecn 包含nbool分量
ivecn 包含nint分量
uvecn 包含nunsigned int分量
dvecn 包含ndouble分量

重组(Swizzling)

vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

在顶点着色器中,使用location指定输入变量,可以在CPU上配置顶点属性。

顶点着色器需要输入提供一个额外的layout标识,才能链接到顶点数据。

忽略layout (location = 0)标识符,在 OpenGL中使用glGetAttribLocation查询属性位置值。

在着色器中设置,会更容易理解而且节省 OpenGL 的工作量。

片段着色器输出一个vec4颜色变量,没有定义输出颜色,OpenGL会把物体渲染为黑色(或白色)。

Uniform:从 CPU 向 GPU 中着色器发送数据的方式,是全局(Global)的。

uniform 变量在每个着色器程序对象中都唯一,可以被任意着色器在任意阶段访问,uniform 会一直数据,直到被重置或更新。

声明了一个 uniform 却在GLSL代码中没使用,编译器会默认移它。

//返回-1表示没有找到位置值
int uniformLocation = glGetUniformLocation(shaderID, "uniformName");

OpenGL 核心是一个C库,不支持类型重载。glUniform 设置 uniform 的值,根据后缀来选用:

  • f 1个float
  • i 1个int
  • ui 1个unsigned int
  • 3f 3个float
  • fv 1个float向量、数组

VBO

通过 VBO 管理储存的顶点数据的 GPU 内存(显存),使用 VBO 可以一次性发送一大批数据到 GPU。

把数据从 CPU 发送到 GPU 相对较慢,要尝试尽量一次性发送尽可能多的数据。

顶点着色器访问显存里面的顶点数据是个非常快的过程。

VBO 有唯一 ID,使用 glGenBuffers函数和一个 ID 生成一个 VBO。

unsigned int VBO;
glGenBuffers(1, &VBO);

VBO 缓冲类型是 GL_ARRAY_BUFFER,使用 glBindBuffer绑定。

glBindBuffer(GL_ARRAY_BUFFER, VBO);

使用任何在 GL_ARRAY_BUFFER 目标上的缓冲调用都会用来配置当前绑定的缓冲(VBO)。

调用 glBufferData 函数会把定义的顶点数据复制到缓冲的显存中。

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
  • param1:target buffer type
  • param2:data size (byte)
  • param3:data
  • param4:data change frequency
    • GL_STATIC_DRAW :never or hardly ever
    • GL_DYNAMIC_DRAW:often
    • GL_STREAM_DRAW :always

链接顶点属性

GLfloat vertices[] = 
{
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

float 和 GLfloat 是同一种数据类型。

typedef float khronos_float_t;
typedef khronos_float_t GLfloat;

glVertexAttribPointer 函数告诉 OpenGL 如何解析顶点数据(应用到逐个顶点属性上)。

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

  • param1:对应顶点着色器中layout(location = 0)
  • param2:一组值的个数(3个)。
  • param3:数据类型。
  • param4:是否映射到 0(signed number 是 -1 ) 到 1。
  • param5:stride,连续顶点属性组间隔(3 * sizeof(float))。
  • param6:void* 类型,表示位置数据在缓冲中起始位置的偏移量(Offset)。

每个顶点属性从一个 VBO 管理的内存中获得数据,从哪个 VBO(程序中可以有多个VBO)获取,是通过在调用glVertexAttribPointer 时绑定到 GL_ARRAY_BUFFER 的 VBO 决定。

使用 glEnableVertexAttribArray,以顶点属性位置值(这里 layout(location = 0))为参数,启用顶点属性。

顶点属性默认是禁用的。

使用一个 VBO 将顶点数据初始化至缓冲中,建立一个顶点着色器和一个片段着色器,并告诉 OpenGL 把顶点数据链接到顶点着色器的顶点属性上。

VAO

VAO 可以被绑定,之后的属性调用都会储存在当前绑定的 VAO 中。

当配置顶点属性指针时,只需要将那些调用执行一次,绘制物体的时候只需要绑定相应的 VAO 就行了。

OpenGL 的 Core 模式要求使用 VAO,它知道该如何处理顶点输入。

绑定 VAO 失败,OpenGL会拒绝绘制任何东西。

VAO 存储的内容:

  • glEnableVertexAttribArrayglDisableVertexAttribArray 的调用。
  • glVertexAttribPointer设置的顶点属性配置和与顶点属性关联的 VBO 。

创建一个 VAO。

unsigned int VAO;
glGenVertexArrays(1, &VAO);

使用 glBindVertexArray 绑定VAO,之后应该绑定和配置对应的 VBO 和属性指针,再解绑 VAO 供之后使用。

绘制一个物体时,只要在绘制物体前地把 VAO 绑定到希望使用的设定上就行。

// 初始化代码只运行一次,除非物体频繁改变
unsigned int VAO;
glGenVertexArrays(1, &VAO);

// 绑定VAO
glBindVertexArray(VAO);

// 顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// ...

// in rendering loop: 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

绘制多个物体时,首先要生成、配置所有的 VAO 和必须的 VBO 及属性指针,然后储存它们供后面使用。

打算绘制物体的时候就拿出相应的 VAO,绑定它,绘制完物体后,再解绑 VAO。

EBO

绘制两个三角形来组成一个矩形。

float vertices[] = 
{
    // 1
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, 0.5f, 0.0f,  // 左上角
    // 2
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};

EBO 是一个缓冲,储存索引,OpenGL 调用顶点的索引来决定该绘制哪个顶点。

定义不重复的顶点,和绘制出矩形所需的索引:

float vertices[] = 
{
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};
unsigned int indices[] =
{ 
    0, 1, 3, // 1
    1, 2, 3  // 2
};

创建一个 EBO。

unsigned int EBO;
glGenBuffers(1, &EBO);

glBufferData 把索引复制到缓冲里,缓冲的类型定义为 GL_ELEMENT_ARRAY_BUFFER

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

glDrawElements 指明索引缓冲渲染,使用当前绑定的 EBO 中的索引进行绘制。

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
  • param1:绘制模式 GL_TRIANGLES
  • param2:绘制顶点个数。
  • param3:索引类型 GL_UNSIGNED_INT
  • param4:EBO 偏移量(不使用 EBO,可以传递一个索引数组)。

glDrawElements函数从当前绑定到 GL_ELEMENT_ARRAY_BUFFER 目标的EBO中获取索引,因此在每次要用索引渲染一个物体时绑定相应的 EBO。VAO 可以保存 EBO 的绑定状态,VAO 绑定时正在绑定的 EBO 会被保存为 VAO 的元素缓冲对象。绑定 VAO 的同时也会自动绑定 EBO。

目标是 GL_ELEMENT_ARRAY_BUFFER ,VAO 会储存 glBindBuffer函数调用,因此会储存解绑调用。

确保没有在解绑 VAO 之前解绑 EBO,否则它就没有这个 EBO 配置了。

// 初始化VAO, VBO, EBO

// 绑定VAO
glBindVertexArray(VAO);

// 把顶点数组复制到VBO中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// 复制索引数组到EBO中,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

//...

// in rendering loop: 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);

通过glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)配置 OpenGL 如何绘制图元。

  • param1:其应用到所有的三角形的正面和背面。
  • param2:用线绘制。

以线框模式绘制三角形,直到用glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)将其设置回默认模式。

多属性填充

unsigned VAO,VBO,EBO;

glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);

glBindVertexArray(VAO); // bind VAO

glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// 位置
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

glBindVertexArray(0);   // bind(0)

顶点着色器

#version 330 core
layout (location = 0) in vec3 aPos;   // 位置变量的属性位置值为 0 
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1

out vec3 ourColor; // 向片段着色器输出一个颜色

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor; 
}

片段着色器

#version 330 core
in vec3 ourColor;
out vec4 FragColor;

void main()
{
    FragColor = vec4(ourColor,1.0f);
} 

out1

线性插值

上面的输出是片段着色器中进行的片段插值(Fragment Interpolation)的结果。当渲染时,光栅化(Rasterization)通常会造成比原指定顶点更多片段。光栅会根据每个片段在三角形形状上的相对位置决定这些片段位置。 基于这些位置,插值(Interpolate)所有片段着色器的输入变量。

纹理

纹理坐标的范围是从 (0, 0) 到 (1, 1) ,坐标范围外的情况由纹理环绕方式决定。

使用 glTexParameter 函数对st(3D纹理有r)轴设置环绕方式。

// 默认行为
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
  • param1:纹理目标,2D 纹理是 GL_TEXTURE_2D

  • param2:纹理轴。

  • param3:环绕(Wrapping)方式,选择 GL_CLAMP_TO_BORDER 要额外指定边缘颜色。

    使用glTexParameter函数的fv后缀形式。

    float borderColor[] = { 1.0f, 1.0f, 1.0f, 1.0f };
    glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

纹理像素(Texture Pixel、Texel) 映射到纹理坐标,当物体很大、纹理分辨率很低时,有对于纹理过滤(Texture Filtering)的选项,主要是 GL_NEARESTGL_LINEAR

GL_NEAREST(邻近过滤,Nearest Neighbor Filtering),OpenGL 默认方式,选择中心点最接近纹理坐标的像素。

GL_LINEAR(线性过滤,(Bi)linear Filtering),基于附近纹理像素,计算出插值,近似出纹理像素之间的颜色。

当进行放大(Magnify)和缩小(Minify)时可以设置纹理过滤的选项。使用glTexParameter函数为指定过滤方式:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

看下面的一个问题:

每个物体上都有纹理,有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。远处的物体可能只产生很少的片段,从高分辨率纹理中为这些片段获取正确的颜色值很困难,需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,并且使用高分辨率纹理浪费内存,影响性能。

OpenGL使用多级渐远纹理(Mipmap),一系列的纹理图像,后一个纹理图像是前一个的二分之一。距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。

使用 glGenerateMipmaps 函数,在创建完纹理后调用它, OpenGL 会完成其余工作。

像普通的纹理过滤一样,可以指定不同多级渐远纹理级别之间的过滤方式。

  • GL_NEAREST_MIPMAP_NEAREST:使用最邻近的多级渐远纹理来匹配像素大小,用邻近插值进行纹理采样。
  • GL_LINEAR_MIPMAP_NEAREST:使用最邻近的多级渐远纹理级别,并使用线性插值进行采样。
  • GL_NEAREST_MIPMAP_LINEAR: 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样。
  • GL_LINEAR_MIPMAP_LINEAR:在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样。

使用 glTexParameteri 设置过滤方式:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

多级渐远纹理主要是使用在纹理被缩小的情况下的,纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理会产生一个 GL_INVALID_ENUM 错误代码。

stb_image.h是单头文件图像加载库,可以从这里下载,并整合到项目中。

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

#define STB_IMAGE_IMPLEMENTATION,预处理器会修改头文件,让其只包含相关的函数定义源码。

下面是加载纹理的示例

unsigned m_load_img2D(const char* path)
{
	unsigned texture;
	glGenTextures(1, &texture);

	stbi_set_flip_vertically_on_load(true);
	int width, height, channels;
	unsigned char* data = stbi_load(path, &width, &height, &channels, 0);
	if (data)
	{
		GLenum format;
		if (channels == 1)
			format = GL_RED;
		else if (channels == 3)
			format = GL_RGB;
		else if (channels == 4)
			format = GL_RGBA;

		glBindTexture(GL_TEXTURE_2D, texture);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
		glGenerateMipmap(GL_TEXTURE_2D);

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, format == GL_RGBA ? GL_CLAMP_TO_EDGE : GL_REPEAT); 
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, format == GL_RGBA ? GL_CLAMP_TO_EDGE : GL_REPEAT); 
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

		stbi_image_free(data);
		return texture;
	}
	else
	{
		stbi_image_free(data);
#ifdef USE_LOG
		m_log("Failed to load texture!");
#endif 
		return 0;
	}
}

OpenGL要求 y 轴 0 坐标在图片底部,但图片 y 轴 0 坐标通常在顶部,因此要翻转 y 轴:

stbi_set_flip_vertically_on_load(true);

glTexImage2D 函数:

  • param1:纹理目标,GL_TEXTURE_2D
  • param2:多级渐远纹理的级别,0 是基本级别。
  • param3:图像只有RGB值,把纹理储存为RGB值,GL_RGB
  • param4:纹理宽度。
  • param5:纹理高度。
  • param6:总是 0,历史遗留的问题。
  • param7:源图格式,GL_RGB
  • param8:数据类型,GL_UNSIGNED_BYTE
  • param9:图像数据。

生成纹理之后调用glGenerateMipmap,为当前绑定的纹理自动生成所有需要的多级渐远纹理。

顶点数据中增加纹理坐标信息。

GLfloat vertices[] = 
{
	//   位置    				颜色			    	纹理坐标
		 0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f,   // 右上
		 0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f,   // 右下
		-0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f,   // 左下
		-0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f    // 左上
};

在顶点着色器中接受纹理坐标,再传送给片段着色器。

unsigned texture = m_load_img2D("asset/rika.jpg");
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 ourColor;
out vec2 TexCoord;

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor;
    TexCoord = aTexCoord;
}

GLSL 有一个供纹理对象使用的内建数据类型采样器(Sampler),以纹理类型作为后缀。

uniform sampler2D把一个纹理添加到片段着色器中。

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;

void main()
{
    FragColor = texture(ourTexture, TexCoord);
    //FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
}

在调用 glDrawElements 之前绑定纹理,会自动把纹理赋值给片段着色器的采样器。

glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

纹理单元

一个纹理的位置值称为一个纹理单元(Texture Unit),默认激活纹理单元是 0。

把纹理单元赋值给采样器,可以一次绑定多个纹理,使用glActiveTexture激活纹理单元,再绑定纹理单元。

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);

GL_TEXTURE0 默认被激活,使用glBindTexture时,无需激活任何纹理单元。

OpenGL 至少保证有 16 个纹理单元,从 GL_TEXTURE0GL_TEXTRUE15,按顺序增加的。

// ...
unsigned texture1 = m_load_img2D("asset/1.png");
unsigned texture2 = m_load_img2D("asset/2.jpg"
                                 
m_shader shader("shader/v.vert", "shader/f.frag");
shader.active();
shader.setInt("texture1", 0);
shader.setInt("texture2", 1);
              
while (!glfwWindowShouldClose(*mw))
{
	// ...
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texture1);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, texture2
                  
    shader.active();
    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0
	// ...
}

setInt 函数是一个简单的封装:

void setInt(const char* name, int val)
{
    glUniform1i(glGetUniformLocation(_ID, name), val);
}
#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D texture1;
uniform sampler2D texture2;

void main()
{
    FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.5);
}

out2

Z-Buffer

OpenGL 存储深度信息在一个Z缓冲(Z-buffer)/深度缓冲(Depth Buffer)中,深度值存储在每个片段里面的 z 值里面。当片段想要输出它的颜色时,OpenGL会将它的深度值和 z 缓冲进行比较,如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖。这个过程称为深度测试(Depth Testing),它是由OpenGL自动完成的。

深度测试默认是关闭的。可以通过 glEnable 函数来开启深度测试。

glEnable(GL_DEPTH_TEST);

当使用深度测试,在每次渲染迭代前清除深度缓冲(否则前一帧深度信息仍保存在缓冲中),glClear函数指定DEPTH_BUFFER_BIT位清除深度缓冲.

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

glEnableglDisable 函数启用/禁用某个OpenGL功能,一直保持直到另一个调用来禁用/启用。

坐标系统

GLM

GLM(OpenGL Mathematics)是一个头文件库,不用链接和编译,从这里下载,然后添加进项目即可。

  • GLM 库从0.9.9版本起,默认会将矩阵类型初始化为一个零矩阵。
  • GLM 角度是弧度制的(Radian),使用glm::radians将角度转化为弧度。

GLM 大多数功能在下面的头文件中。

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

把变换矩阵传发送着色器的方法。

unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
  • param1:uniform 位置值。
  • param2:发送矩阵数量。
  • param3:是否进行矩阵转置,OpenGL 默认列主序,不需要转置。
  • param4:矩阵数据,需要 glm::value_ptr来转变。

坐标变换

  • 局部坐标(Local Coordinate):物体相对于局部原点的坐标,即物体起始坐标。
  • 世界坐标(World Coordinate):物体会和其它物体一起相对于世界的原点进行摆放。
  • 观察坐标(View Coordinate):每个坐标都是从摄像机(观察者)的角度进行观察的坐标。
  • 裁剪坐标(Clip Coordinate):坐标被处理至[-1, 1]的范围内,判断哪些顶点会出现在屏幕上。
  • 屏幕坐标(Screen Coordinate):经过视口变换(Viewport Transform),将[-1, 1]范围的坐标变换到由glViewport函数所定义的坐标范围内。变换的坐标会送到光栅器,转化为片段。

投影

正视投影定义一个裁剪空间,空间之外的顶点都会被裁剪掉,类似下面的平截头体。

用 GLM 的内置函数glm::ortho创建一个正视投影矩阵。

glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
  • param1、2:平截头体的左右坐标。
  • param3、4:平截头体的底部和顶部。
  • param5、6:近平面和远平面的距离。

透视投影将给定的平截头体范围映射到裁剪空间,修改顶点坐标 w 值,使离观察者越远的顶点坐标 w 分量越大。

OpenGL要求所有可见的坐标在[-1, 1] 范围内,作为顶点着色器的输出。坐标在裁剪空间内之后,透视除法就会被应用到裁剪空间坐标上:(距离观察者越远顶点坐标就会越小)。 \[ out = \begin{pmatrix} x /w \\ y / w \\ z / w \end{pmatrix} \] 创建一个定义了可视空间的大平截头体,在这个平截头体外的东西不会出现在裁剪空间内,会受到裁剪。

用 GLM 的内置函数glm::perspective创建一个透视投影矩阵。

glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
  • param1:fov(Field of View)。
  • param2:宽高比,视口宽除以高。
  • param3、4:平截头体的近平面和远平面。

MVP

顶点坐标\((Local:V_L)\)通过左乘:

  • 模型矩阵\((Model:M)\):位移、缩放与旋转操作
  • 观察矩阵\((View:V)\):摄像机观察矩阵
  • 投影矩阵\((Projection:P)\):透视投影矩阵

变换到裁剪坐标\((Clip:V_C)\)\[ V_C = P \cdot V \cdot M \cdot V_L \]

矩阵运算的顺序是从右往左。

顶点着色器输出要求所有顶点都在裁剪空间内,这是变换矩阵所做的。

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

摄像机

定义摄像机的条件:

  • 摄像机在世界空间中的位置:

    glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 2.0f);
  • 观察方向(+z):指摄像机指向的方向,这里让摄像机指向场景原点。

    glm::vec3 cameraDirection = glm::normalize(cameraPos - glm::vec3(0.0f, 0.0f, 0.0f));

    OpenGL 是右手系,+z 轴指向屏幕外,摄像机方向是 -z 轴,摄像机向后移动,要沿着 +z 轴移动。

  • 指向它右方的向量(+x):

    glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); 
    glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
  • 指向它上方的向量(+y = +z × +x):

    glm::vec3 cameraUp = glm::normalize(glm::cross(cameraDirection, cameraRight));

本质上创建了一个三个单位轴相互垂直的、以摄像机的位置为原点的坐标系。

LookAt 矩阵:看着给定目标的观察矩阵(R 是右向量,U 是上向量,D 是方向向量,P 是摄像机位置向量)。 \[ LookAt = \begin{bmatrix} \color{red}{R_x} & \color{red}{R_y} & \color{red}{R_z} & 0 \\ \color{green}{U_x} & \color{green}{U_y} & \color{green}{U_z} & 0 \\ \color{blue}{D_x} & \color{blue}{D_y} & \color{blue}{D_z} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} 1 & 0 & 0 & -\color{purple}{P_x} \\ 0 & 1 & 0 & -\color{purple}{P_y} \\ 0 & 0 & 1 & -\color{purple}{P_z} \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

位置向量是相反的,我们希望把世界平移到与我们自身移动的相反方向。

glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), 	// 摄像机位置
                             glm::vec3(0.0f, 0.0f, 0.0f),  	// 目标位置			
                             glm::vec3(0.0f, 1.0f, 0.0f)); 	// 世界空间中的上方向量

欧拉角

俯仰角(pitch:x)、偏航角(yaw:y)、滚转角(roll:z)。

这里用 pitch 和 yaw 来模拟(一般摄像机不会进行 roll )。

pitch 影响 xyz 分量,yaw 影响 xz 分量。

// direction指摄像机前方
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); 
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));