OpenGL中不可忽视的部分:glsl、glm、assimp以及更多

在OpenGL中着色器的编程语言叫做GLSL,类似C语言但是内置了许多有用的函数. 这里简单学习其基础语法和包含特殊函数.

GLSL

着色器编程语言,在GPU下编程

开发环境搭建

这里使用vscode进行开发,由于着色器语言不像C++,Java这种通用语言,它的语法检查和Lint没有这么丰富. 在vscode主要下载两个插件,一个是语法高亮Shader languages support for VS Code - Visual Studio Marketplace,另一个是Linthsimpson/vscode-glsllint: VSCode extension to lint GLSL shading language files.

image-20241226175503864

此外Lint的插件本身不提供Linter,需要自己下载KhronosGroup/glslang: Khronos-reference front end for GLSL/ESSL, partial front end for HLSL, and a SPIR-V generator.

语法提示规则与文件名后缀相关

The applied stage-specific rules are based on the file extension:

  • .vert for a vertex shader
  • .tesc for a tessellation control shader
  • .tese for a tessellation evaluation shader
  • .geom for a geometry shader
  • .frag for a fragment shader
  • .comp for a compute shader

For ray tracing pipeline shaders:

  • .rgen for a ray generation shader
  • .rint for a ray intersection shader
  • .rahit for a ray any-hit shader
  • .rchit for a ray closest-hit shader
  • .rmiss for a ray miss shader
  • .rcall for a callable shader

此外还可以配置代码片段GLSL snippets for visual studio code/kode studio,输入缩写即可提示.

数据类型

float,int,bool,vec,mat, struct,[]

重要辅助函数

向量和矩阵运算

dot,cross,normalize,transpose,inverse

纹理采样

texture,mix

光照计算

clamp,mix,reflect

高级GLSL

glsl定义了几个以gl_为前缀的变量,它们能提供给我们更多的方式来读取/写入数据。比如顶点着色器的输出向量gl_Position和片段着色器的gl_FragCoord。

顶点着色器

gl_PointSize

通过gl_PointSize设置GL_POINT图元的大小,在顶点着色器中修改点大小的功能默认是禁用的,如果你需要启用它的话,需要启用OpenGL的GL_PROGRAM_POINT_SIZE:

1
glEnable(GL_PROGRAM_POINT_SIZE);

gl_VertexID

gl_Position和gl_PointSize都是输出变量,因为它们的值是作为顶点着色器的输出被读取的。可以对它们进行写入,来改变结果。

顶点着色器还提供了一个有趣的输入变量,只能对它进行读取,它叫做gl_VertexID。

整型变量gl_VertexID储存了正在绘制顶点的当前ID。当使用glDrawElements进行索引渲染的时候,这个变量会存储正在绘制顶点的当前索引。当使用glDrawArrays不使用索引进行绘制的时候,这个变量会储存从渲染调用开始的已处理顶点数量。

片段着色器

GLSL提供给两个输入变量:gl_FragCoord和gl_FrontFacing。

gl_FragCoord

gl_FragCoord的x和y分量是片段的窗口空间(Window-space)坐标,其原点为窗口的左下角。 可以利用它的z值(也就是深度)和xy空间坐标(屏幕空间,归一到[0,1])

1
2
3
4
5
6
7
void main()
{
if(gl_FragCoord.x < 400)
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
else
FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}

gl_FrontFacing

提到OpenGL根据顶点的环绕顺序来决定一个面是正向还是背向面。如果我们不(启用GL_FACE_CULL来)使用面剔除,那么gl_FrontFacing将会告诉我们当前片段是属于正向面的一部分还是背向面的一部分

gl_FrontFacing变量是一个bool,如果当前片段是正向面的一部分那么就是true,否则就是false

如果开启了面剔除,就看不到箱子内部的面了,再使用gl_FrontFacing就没有意义了。

gl_FragDepth

输入变量gl_FragCoord的深度值是一个只读(Read-only)变量,不能修改片段的窗口空间坐标,但实际上修改片段的深度值还是可能的GLSL提供一个叫做gl_FragDepth的输出变量,可以使用它来在着色器内设置片段的深度值。

要想设置深度值,直接写入一个0.0到1.0之间的float值到输出变量就可以了:

1
gl_FragDepth = 0.0; // 这个片段现在的深度值为 0.0

如果着色器没有写入值到gl_FragDepth,它会自动取用gl_FragCoord.z的值。但是只要我们在片段着色器中对gl_FragDepth进行写入,OpenGL就会禁用所有的提前深度测试(Early Depth Testing)。它被禁用的原因是,OpenGL无法在片段着色器运行之前得知片段将拥有的深度值,因为片段着色器可能会完全修改这个深度值。

从4.2起,在片段着色器的顶部使用深度条件(Depth Condition)重新声明gl_FragDepth变量:

1
layout (depth_<condition>) out float gl_FragDepth;

condition可以为下面的值:

条件描述
any默认值。提前深度测试是禁用的,你会损失很多性能
greater你只能让深度值比gl_FragCoord.z更大
less你只能让深度值比gl_FragCoord.z更小
unchanged如果你要写入gl_FragDepth,你将只能写入gl_FragCoord.z的值
1
2
3
4
5
#version 420 core
layout(depth_less) out float gl_FragDepth;
voif main{
gl_FragDepth = gl_FragCoord.z+1;
}

接口块

为了方便在着色器之间传递数据,可以定义in out块,类似结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out VS_OUT
{
vec2 TexCoords;
} vs_out;

void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
vs_out.TexCoords = aTexCoords;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#version 330 core
out vec4 FragColor;

in VS_OUT
{
vec2 TexCoords;
} fs_in;

uniform sampler2D texture;

void main()
{
FragColor = texture(texture, fs_in.TexCoords);
}

只要两个接口块的名字一样,它们对应的输入和输出将会匹配起来.它在几何着色器这样穿插特定着色器阶段的场景下会很有用。

Uniform缓冲对象

假设多个着色器都包含一个uniform变量,它们的值相同,为了不重复地设置,可以使用Uniform缓冲对象.允许定义一系列在多个着色器程序中相同的全局Uniform变量。当使用Uniform缓冲对象的时候,只需要设置相关的uniform一次。当然,我们仍需要手动设置每个着色器中不同的uniform。并且创建和配置Uniform缓冲对象会有一点繁琐。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#version 330 core
layout (location = 0) in vec3 aPos;

layout (std140) uniform Matrices
// 定义的Uniform块对它的内容使用一个特定的内存布局。这个语句设置了Uniform块布局(Uniform Block Layout)。
{
mat4 projection;
mat4 view;
};

uniform mat4 model;

void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
}

这个Uniform块储存了两个4x4矩阵。Uniform块中的变量可以直接访问,不需要加块名作为前缀。 每个声明了这个Uniform块的着色器都能够访问这些矩阵。

Uniform块布局

Uniform块的内容是储存在一个缓冲对象中的,它实际上只是一块预留内存。因为这块内存并不会保存它具体保存的是什么类型的数据,我们还需要告诉OpenGL内存的哪一部分对应着着色器中的哪一个uniform变量。

假设着色器中有以下的这个Uniform块:

1
2
3
4
5
6
7
8
9
layout (std140) uniform ExampleBlock
{
float value;
vec3 vector;
mat4 matrix;
float values[3];
bool boolean;
int integer;
};

我们需要知道的是每个变量的大小(字节)和(从块起始位置的)偏移量,来让我们能够按顺序将它们放进缓冲中。每个元素的大小都是在OpenGL中有清楚地声明的,而且直接对应C++数据类型,其中向量和矩阵都是大的float数组。OpenGL没有声明的是这些变量间的间距(Spacing)。这允许硬件能够在它认为合适的位置放置变量。比如说,一些硬件可能会将一个vec3放置在float边上。不是所有的硬件都能这样处理,可能会在附加这个float之前,先将vec3填充(Pad)为一个4个float的数组。这个特性本身很棒,但是会造成麻烦。

默认情况下,GLSL会使用一个叫做共享(Shared)布局的Uniform内存布局,共享是因为一旦硬件定义了偏移量,它们在多个程序中是共享并一致的。使用共享布局时,GLSL是可以为了优化而对uniform变量的位置进行变动的,只要变量的顺序保持不变因为无法知道每个uniform变量的偏移量,我们也就不知道如何准确地填充Uniform缓冲了。我们能够使用像是glGetUniformIndices这样的函数来查询这个信息

虽然共享布局给了我们很多节省空间的优化,但是我们需要查询每个uniform变量的偏移量,这会产生非常多的工作量。通常的做法是,不使用共享布局,而是使用std140布局。std140布局声明了每个变量的偏移量都是由一系列规则所决定的,这显式地声明了每个变量类型的内存布局。由于这是显式提及的,我们可以手动计算出每个变量的偏移量。

每个变量都有一个基准对齐量(Base Alignment),它等于一个变量在Uniform块中所占据的空间(包括填充量(Padding)),这个基准对齐量是使用std140布局的规则计算出来的。接下来,对每个变量,我们再计算它的对齐偏移量(Aligned Offset),它是一个变量从块起始位置的字节偏移量。一个变量的对齐字节偏移量必须等于基准对齐量的倍数。

GLSL中的每个变量,比如说int、float和bool,都被定义为4字节量。每4个字节将会用一个N来表示。

类型布局规则
标量,比如int和bool每个标量的基准对齐量为N。
向量2N或者4N。这意味着vec3的基准对齐量为4N。
标量或向量的数组每个元素的基准对齐量与vec4的相同。
矩阵储存为列向量的数组,每个向量的基准对齐量与vec4的相同。
结构体等于所有元素根据规则计算后的大小,但会填充到vec4大小的倍数。

使用计算后的偏移量值,根据std140布局的规则,我们就能使用像是glBufferSubData的函数将变量数据按照偏移量填充进缓冲中了。虽然std140布局不是最高效的布局,但它保证了内存布局在每个声明了这个Uniform块的程序中是一致的。

通过在Uniform块定义之前添加layout (std140)语句,告诉OpenGL这个Uniform块使用的是std140布局。除此之外还可以选择两个布局,但它们都需要我们在填充缓冲之前先查询每个偏移量

​ 我们已经见过shared布局了,剩下的一个布局是packed当使用紧凑(Packed)布局时,是不能保证这个布局在每个程序中保持不变的(即非共享),因为它允许编译器去将uniform变量从Uniform块中优化掉,这在每个着色器中都可能是不同的。

使用Uniform缓冲

我们已经讨论了如何在着色器中定义Uniform块,并设定它们的内存布局了,但我们还没有讨论该如何使用它们。

首先,我们需要调用glGenBuffers,创建一个Uniform缓冲对象。一旦我们有了一个缓冲对象,我们需要将它绑定到GL_UNIFORM_BUFFER目标,并调用glBufferData,分配足够的内存。

1
2
3
4
5
unsigned int uboExampleBlock;
glGenBuffers(1, &uboExampleBlock); //创建uniform buffer对象
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_STATIC_DRAW); // 分配152字节的内存
glBindBuffer(GL_UNIFORM_BUFFER, 0);

每当我们需要对缓冲更新或者插入数据,我们都会绑定到uboExampleBlock,并使用glBufferSubData来更新它的内存。我们只需要更新这个Uniform缓冲一次,所有使用这个缓冲的着色器就都使用的是更新后的数据了

在OpenGL上下文中,定义了一些绑定点(Binding Point),我们可以将一个Uniform缓冲链接至它。在创建Uniform缓冲之后,我们将它绑定到其中一个绑定点上,并将着色器中的Uniform块绑定到相同的绑定点,把它们连接到一起。下面的这个图示展示了这个:

img

你可以看到,我们可以绑定多个Uniform缓冲到不同的绑定点上。因为着色器A和着色器B都有一个链接到绑定点0的Uniform块,它们的Uniform块将会共享相同的uniform数据,uboMatrices,前提条件是两个着色器都定义了相同的Matrices Uniform块。

为了将Uniform块绑定到一个特定的绑定点中,我们需要调用glUniformBlockBinding函数,它的第一个参数是一个程序对象,之后是一个Uniform块索引和链接到的绑定点。Uniform块索引(Uniform Block Index)是着色器中已定义Uniform块的位置值索引。这可以通过调用glGetUniformBlockIndex来获取,它接受一个程序对象和Uniform块的名称

1
2
unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");   
glUniformBlockBinding(shaderA.ID, lights_index, 2);

从OpenGL 4.2版本起,也可以添加一个布局标识符,显式地将Uniform块的绑定点储存在着色器中,这样就不用再调用glGetUniformBlockIndex和glUniformBlockBinding了。下面的代码显式地设置了Lights Uniform块的绑定点。

1
layout(std140, binding = 2) uniform Lights { ... };

接下来,我们还需要绑定Uniform缓冲对象到相同的绑定点上,这可以使用glBindBufferBase或glBindBufferRange来完成。

1
2
3
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock); 
// 或
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);

glBindbufferBase需要一个目标,一个绑定点索引和一个Uniform缓冲对象作为它的参数。这个函数将uboExampleBlock链接到绑定点2上,自此,绑定点的两端都链接上了。你也可以使用glBindBufferRange函数,它需要一个附加的偏移量和大小参数,这样子你可以绑定Uniform缓冲的特定一部分到绑定点中。通过使用glBindBufferRange函数,你可以让多个不同的Uniform块绑定到同一个Uniform缓冲对象上。

现在,所有的东西都配置完毕了,我们可以开始向Uniform缓冲中添加数据了。只要我们需要,就可以使用glBufferSubData函数,用一个字节数组添加所有的数据,或者更新缓冲的一部分。要想更新uniform变量boolean,我们可以用以下方式更新Uniform缓冲对象:

1
2
3
4
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
int b = true; // GLSL中的bool是4字节的,所以我们将它存为一个integer
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b);
glBindBuffer(GL_UNIFORM_BUFFER, 0);

同样的步骤也能应用到Uniform块中其它的uniform变量上,但需要使用不同的范围参数。

Uniform缓冲对象比起独立的uniform有很多好处。第一,一次设置很多uniform会比一个一个设置多个uniform要快很多。第二,比起在多个着色器中修改同样的uniform,在Uniform缓冲中修改一次会更容易一些。最后一个好处可能不会立即显现,如果使用Uniform缓冲对象的话,你可以在着色器中使用更多的uniform。OpenGL限制了它能够处理的uniform数量,这可以通过GL_MAX_VERTEX_UNIFORM_COMPONENTS来查询。当使用Uniform缓冲对象时,最大的数量会更高。所以,当你达到了uniform的最大数量时(比如再做骨骼动画(Skeletal Animation)的时候),总是可以选择使用Uniform缓冲对象。

GLM

与opengl向适应的向量计算库

  1. 向量类:
    • glm::vec2: 2D 向量。
    • glm::vec3: 3D 向量。
    • glm::vec4: 4D 向量(通常用于颜色或齐次坐标)。
  2. 矩阵类:
    • glm::mat2: 2x2 矩阵。
    • glm::mat3: 3x3 矩阵。
    • glm::mat4: 4x4 矩阵(常用于变换矩阵)。
  3. 四元数类:
    • glm::quat: 四元数,用于表示旋转。

常用方法

向量操作

  • 创建向量:

    1
    glm::vec3 v(1.0f, 2.0f, 3.0f);
  • 向量加法:

    1
    glm::vec3 sum = v1 + v2;
  • 向量减法:

    1
    glm::vec3 difference = v1 - v2;
  • 标量乘法:

    1
    glm::vec3 scaled = v * scalar;
  • 点积 (dot product):

    1
    float dotProduct = glm::dot(v1, v2);
  • 叉积 (cross product) - 仅适用于3D向量:

    1
    glm::vec3 crossProduct = glm::cross(v1, v2);
  • 长度/模 (length/magnitude):

    1
    float length = glm::length(v);
  • 标准化 (normalize):

    1
    glm::vec3 normalizedV = glm::normalize(v);

矩阵操作

  • 创建单位矩阵:

    1
    glm::mat4 identity = glm::mat4(1.0f);
  • 平移矩阵:

    1
    glm::mat4 translateMatrix = glm::translate(glm::mat4(1.0f), glm::vec3(x, y, z));
  • 缩放矩阵:

    1
    glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0f), glm::vec3(sx, sy, sz));
  • 旋转矩阵:

    1
    glm::mat4 rotateMatrix = glm::rotate(glm::mat4(1.0f), angle, glm::vec3(axisX, axisY, axisZ));
  • 组合变换:

    1
    2
    3
    glm::mat4 model = glm::translate(glm::mat4(1.0f), translation) *
    glm::rotate(glm::mat4(1.0f), rotationAngle, rotationAxis) *
    glm::scale(glm::mat4(1.0f), scale);
  • 视图矩阵 (lookAt):

    1
    glm::mat4 view = glm::lookAt(cameraPosition, cameraTarget, upVector);
  • 投影矩阵 (perspectiveorthographic):

    1
    2
    3
    glm::mat4 projection = glm::perspective(glm::radians(fov), aspectRatio, nearPlane, farPlane);
    // 或者正交投影
    glm::mat4 orthoProjection = glm::ortho(left, right, bottom, top, nearPlane, farPlane);

四元数操作

  • 创建四元数:

    1
    glm::quat q = glm::quat(angle, glm::vec3(axisX, axisY, axisZ));
  • 从旋转矩阵转换到四元数:

    1
    glm::quat qFromMat = glm::quat_cast(rotationMatrix);
  • 四元数插值 (slerp):

    1
    glm::quat interpolatedQ = glm::slerp(q1, q2, t);
  • 四元数转欧拉角:

    1
    glm::vec3 eulerAngles = glm::eulerAngles(q);
  • 四元数转旋转矩阵:

    1
    glm::mat4 matFromQuat = glm::mat4_cast(q);

Assimp

模型加载库

读取文件

1
Assimp::Importer::ReadFile()

​ 该类将读取文件并处理其数据,将导入的数据作为指向一个对象的指针返回。现在可以从文件中提取所需的数据。

​ 导入器为自己管理所有资源。如果导入器被销毁,那么由它创建/读取的所有数据也将被销毁。因此,使用Importer最简单的方法是在本地创建一个实例,使用它的结果,然

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <assimp/Importer.hpp>      // C++ importer interface
#include <assimp/scene.h> // Output data structure
#include <assimp/postprocess.h> // Post processing flags

bool DoTheImportThing( const std::string& pFile) {
// Create an instance of the Importer class
Assimp::Importer importer;

// And have it read the given file with some example postprocessing
// Usually - if speed is not the most important aspect for you - you'll
// probably to request more postprocessing than we do in this example.
const aiScene* scene = importer.ReadFile( pFile,
aiProcess_CalcTangentSpace |
aiProcess_Triangulate |
aiProcess_JoinIdenticalVertices |
aiProcess_SortByPType);

// If the import failed, report it
if (nullptr == scene) {
DoTheErrorLogging( importer.GetErrorString());
return false;
}

// Now we can access the file's contents.
DoTheSceneProcessing( scene);

// We're done. Everything will be cleaned up by the importer destructor
return true;
}

后简单地让它离开作用域。

数据结构

以结构集合的形式返回导入的数据。aiScene形成数据的根,从这里可以访问从导入文件中读取的所有节点,网格,材质,动画或纹理

默认情况下,所有3D数据都以右手坐标系提供,例如OpenGL使用的坐标系。在这个坐标系中,+X指向右侧,+Y指向上方,+Z指向屏幕外的观察者

输出面顺序为逆时针方向

Scene

场景包含一个rootNode用于遍历以及mesh和material,通过node中的mesh索引获得具体mesh,通过mesh中的material索引获得具体material,mesh还包括faces,一个face就是一个primitive.

  1. mFlags: 这个标志表示场景的一些特性,例如是否是不完整的 (AI_SCENE_FLAGS_INCOMPLETE) 或者是否有无效的数据 (AI_SCENE_FLAGS_INVALID_DATA)。
  2. mRootNode: 指向场景根节点的指针。每个场景都有一个根节点,所有其他节点都是它的子节点。通过遍历这个节点树,你可以获取场景的所有几何信息。
  3. mNumMeshesmMeshes: 分别表示场景中网格的数量和指向网格数组的指针。网格包含了顶点、面和其他几何数据。
  4. mNumMaterialsmMaterials: 分别表示场景中材质的数量和指向材质数组的指针。材质定义了网格的外观属性。
  5. mNumTexturesmTextures: 分别表示场景中文本贴图的数量和指向文本贴图数组的指针。请注意,不是所有的模型格式都支持直接导出纹理,所以这个成员可能为空。
  6. mNumCamerasmCameras: 分别表示场景中相机的数量和指向相机数组的指针。并不是所有模型文件都会包含相机信息。
  7. mNumLightsmLights: 分别表示场景中光源的数量和指向光源数组的指针。同样地,并非所有模型文件都包含光源信息。
  8. mMetaData: 包含有关场景的元数据。这可以包括版本号、作者等信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 假设我们已经有一个 aiScene* scene

// 访问根节点
aiNode* rootNode = scene->mRootNode;

// 遍历所有网格
for (unsigned int i = 0; i < scene->mNumMeshes; ++i) {
aiMesh* mesh = scene->mMeshes[i];
// 处理网格...
}

// 遍历所有材质
for (unsigned int i = 0; i < scene->mNumMaterials; ++i) {
aiMaterial* material = scene->mMaterials[i];
// 处理材质...
}

// 如果有纹理,遍历它们
if (scene->mNumTextures > 0) {
for (unsigned int i = 0; i < scene->mNumTextures; ++i) {
aiTexture* texture = scene->mTextures[i];
// 处理纹理...
}
}

// 如果有相机,遍历它们
if (scene->mNumCameras > 0) {
for (unsigned int i = 0; i < scene->mNumCameras; ++i) {
aiCamera* camera = scene->mCameras[i];
// 处理相机...
}
}

// 如果有光源,遍历它们
if (scene->mNumLights > 0) {
for (unsigned int i = 0; i < scene->mNumLights; ++i) {
aiLight* light = scene->mLights[i];
// 处理光源...
}
}
Nodes

节点是场景中名字不大的实体,相对于它们的父节点有一个位置和方向。从场景的根节点开始,所有节点可以有0到x个子节点,从而形成一个层次结构。

  1. mTransformation:
    • 类型: aiMatrix4x4
    • 描述: 表示节点的本地变换矩阵,它定义了该节点相对于其父节点的位置、旋转和缩放。
  2. mNumMeshesmMeshes:
    • 类型: unsigned intunsigned int*
    • 描述: mNumMeshes 表示此节点直接关联的网格数量;mMeshes 是一个索引数组,指向 aiScenemMeshes 数组中的相应网格。如果 mNumMeshes 为 0,则该节点没有直接关联的网格。
  3. mParent:
    • 类型: aiNode*
    • 描述: 指向该节点的父节点的指针。根节点的 mParentnullptr
  4. mNumChildrenmChildren:
    • 类型: unsigned intaiNode**
    • 描述: mNumChildren 表示该节点的子节点数量;mChildren 是一个指针数组,指向该节点的所有子节点。
  5. mName:
    • 类型: aiString
    • 描述: 节点的名字。在某些情况下,这个名字可能被用来标识特定的节点或作为动画等的参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 假设我们有一个 aiNode* node

// 获取节点的变换矩阵
aiMatrix4x4 transformation = node->mTransformation;

// 遍历所有与该节点关联的网格
for (unsigned int i = 0; i < node->mNumMeshes; ++i) {
unsigned int meshIndex = node->mMeshes[i];
aiMesh* mesh = scene->mMeshes[meshIndex];
// 处理网格...
}

// 获取父节点
if (node->mParent != nullptr) {
aiNode* parentNode = node->mParent;
// 处理父节点...
}

// 遍历所有子节点
for (unsigned int i = 0; i < node->mNumChildren; ++i) {
aiNode* childNode = node->mChildren[i];
// 处理子节点...
}

// 获取节点名称
std::string nodeName(node->mName.C_Str());
Mesh
  1. mPrimitiveTypes:
    • 类型: unsigned int
    • 描述: 表示该网格中包含的图元类型,例如点 (aiPrimitiveType_POINT)、线 (aiPrimitiveType_LINE) 或三角形 (aiPrimitiveType_TRIANGLE)。
  2. mNumVerticesmVertices:
    • 类型: unsigned intaiVector3D*
    • 描述: mNumVertices 表示顶点的数量;mVertices 是指向 aiVector3D 数组的指针,每个元素代表一个顶点的位置。
  3. mNormals:
    • 类型: aiVector3D*
    • 描述: 指向法线数组的指针。如果网格包含法线数据,则每个顶点都有一个对应的法线向量。
  4. mTextureCoords:
    • 类型: aiVector3D**
    • 描述: 指向纹理坐标数组的二维数组指针。第一维是纹理坐标集的数量(最多8个),第二维是实际的纹理坐标。如果某个顶点有纹理坐标,则可以通过这个成员访问。
  5. mColors:
    • 类型: aiColor4D**
    • 描述: 指向颜色数组的二维数组指针。第一维是颜色集的数量(最多8个),第二维是实际的颜色值。如果顶点有色值,则可以在这里找到。
  6. mNumFacesmFaces:
    • 类型: unsigned intaiFace*
    • 描述: mNumFaces 表示面的数量;mFaces 是指向 aiFace 数组的指针,每个 aiFace 定义了一个由若干顶点组成的多边形(通常是三角形)。
  7. mMaterialIndex:
    • 类型: unsigned int
    • 描述: 网格使用的材质在场景的 mMaterials 数组中的索引。通过这个索引,你可以获取与该网格关联的 aiMaterial 对象。
  8. mNumBonesmBones:
    • 类型: unsigned intaiBone**
    • 描述: 如果网格支持骨骼动画,则 mBones 包含了指向 aiBone 数组的指针,每个 aiBone 定义了一个影响顶点位置的骨骼。mNumBones 是骨骼的数量。
  9. mName:
    • 类型: aiString
    • 描述: 网格的名字。某些情况下,这个名字可能是有意义的,比如用于标识特定的网格或作为其他资源的引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 假设我们有一个 aiMesh* mesh 和 aiScene* scene

// 获取网格的材质
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];

// 遍历所有顶点
for (unsigned int i = 0; i < mesh->mNumVertices; ++i) {
aiVector3D position = mesh->mVertices[i];
// 处理顶点位置...

if (mesh->HasNormals()) {
aiVector3D normal = mesh->mNormals[i];
// 处理法线...
}

if (mesh->HasTextureCoords(0)) { // 假设使用第0个纹理坐标集
aiVector3D texCoord = mesh->mTextureCoords[0][i];
// 处理纹理坐标...
}
}

// 遍历所有面
for (unsigned int i = 0; i < mesh->mNumFaces; ++i) {
aiFace face = mesh->mFaces[i];
// 处理面...
for (unsigned int j = 0; j < face.mNumIndices; ++j) {
unsigned int vertexIndex = face.mIndices[j];
// 使用顶点索引...
}
}

// 如果有骨骼信息,遍历它们
if (mesh->HasBones()) {
for (unsigned int i = 0; i < mesh->mNumBones; ++i) {
aiBone* bone = mesh->mBones[i];
// 处理骨骼...
}
}

aiMesh 还提供了一些辅助函数来检查网格是否包含特定的数据类型:

  • HasPositions(): 返回网格是否有顶点位置。
  • HasNormals(): 返回网格是否有法线数据。
  • HasTangentsAndBitangents(): 返回网格是否有切线和副切线数据。
  • HasTextureCoords(unsigned int): 接受一个参数指定纹理坐标集的索引,返回网格是否有对应的纹理坐标。
  • HasVertexColors(unsigned int): 接受一个参数指定颜色集的索引,返回网格是否有对应的顶点颜色。
  • HasFaces(): 返回网格是否有面数据。
  • HasBones(): 返回网格是否有骨骼信息。
Material
颜色属性
  • AI_MATKEY_COLOR_DIFFUSE: 扩散(漫反射)颜色。
  • AI_MATKEY_COLOR_AMBIENT: 环境光颜色。
  • AI_MATKEY_COLOR_SPECULAR: 高光(镜面反射)颜色。
  • AI_MATKEY_COLOR_EMISSIVE: 发射光颜色。
  • AI_MATKEY_COLOR_TRANSPARENT: 透明颜色。
  • AI_MATKEY_COLOR_REFLECTIVE: 反射颜色。
浮点数属性
  • AI_MATKEY_SHININESS: 高光强度。
  • AI_MATKEY_SHININESS_STRENGTH: 高光强度因子。
  • AI_MATKEY_REFRACTI: 折射率。
布尔属性
  • AI_MATKEY_ENABLE_WIREFRAME: 是否启用线框模式。
纹理属性
  • AI_MATKEY_TEXTURE_BASE: 基础纹理。
  • AI_MATKEY_TEXTURE_DIFFUSE: 扩散(漫反射)纹理。
  • AI_MATKEY_TEXTURE_SPECULAR: 高光(镜面反射)纹理。
  • AI_MATKEY_TEXTURE_AMBIENT: 环境光纹理。
  • AI_MATKEY_TEXTURE_EMISSIVE: 发射光纹理。
  • AI_MATKEY_TEXTURE_HEIGHT: 高度图。
  • AI_MATKEY_TEXTURE_NORMALS: 法线贴图。
  • AI_MATKEY_TEXTURE_SHININESS: 高光贴图。
  • AI_MATKEY_TEXTURE_OPACITY: 不透明度贴图。
  • AI_MATKEY_TEXTURE_DISPLACEMENT: 位移贴图。
  • AI_MATKEY_TEXTURE_LIGHTMAP: 光照贴图。
  • AI_MATKEY_TEXTURE_REFLECTION: 反射贴图。
方法

为了访问上述属性,aiMaterial 提供了一系列的 GetSet 函数。最常用的 Get 函数包括:

  1. Get(AI_MATKEY key, aiColor4D& out):
    • 获取指定键的颜色值。
  2. Get(AI_MATKEY key, float& out):
    • 获取指定键的浮点数值。
  3. Get(AI_MATKEY key, bool& out):
    • 获取指定键的布尔值。
  4. GetTexture(aiTextureType type, unsigned int index, aiString\* path):
    • 获取指定类型的纹理路径。index 参数允许你访问同一类型下的多个纹理(例如,多层扩散纹理)。
  5. HasProperty(const char\* key):
    • 检查材质是否具有给定键的属性。
  6. Get(AI_MATKEY key, unsigned int& out):
    • 获取指定键的无符号整数值(例如,用于获取纹理的数量)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 假设我们有一个 aiMaterial* material

// 获取扩散颜色
aiColor4D diffuseColor;
if (AI_SUCCESS == material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuseColor)) {
// 使用 diffuseColor...
}

// 获取高光强度
float shininess;
if (AI_SUCCESS == material->Get(AI_MATKEY_SHININESS, shininess)) {
// 使用 shininess...
}

// 获取第一个扩散纹理路径
aiString texturePath;
if (AI_SUCCESS == material->GetTexture(aiTextureType_DIFFUSE, 0, &texturePath)) {
// 使用 texturePath.C_Str()...
}

// 检查是否有环境光颜色
if (material->HasProperty(AI_MATKEY_COLOR_AMBIENT)) {
aiColor4D ambientColor;
if (AI_SUCCESS == material->Get(AI_MATKEY_COLOR_AMBIENT, ambientColor)) {
// 使用 ambientColor...
}
}
Texture

通常,资源使用的纹理存储在单独的文件中,但是也有文件格式将纹理直接嵌入到模型文件中。这样的纹理被加载到aittexture结构中

  1. mWidthmHeight:
    • 类型: unsigned int
    • 描述: 分别表示纹理图像的宽度和高度(以像素为单位)。对于非图像格式(例如程序生成的纹理),这些值可能为0。
  2. mData:
    • 类型: unsigned char*
    • 描述: 指向包含纹理数据的缓冲区。注意,并不是所有情况下都会提供实际的纹理数据;某些导入器可能会直接返回文件路径而不是加载图像数据到内存中。
  3. mHeight:
    • 类型: unsigned int
    • 描述: 纹理的高度(以像素为单位)。
  4. achFormatHint:
    • 类型: char[AI_TEXTURE_FORMAT_MAX]
    • 描述: 提供关于纹理格式的提示字符串,例如 "jpg""png"。这可以帮助你确定如何正确地解码纹理数据。
  5. mFilename:
    • 类型: aiString
    • 描述: 包含纹理文件的相对或绝对路径名。这是最常用的方式来获取纹理资源的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 假设我们有一个 aiTexture* texture

// 获取纹理文件名
std::string filename(texture->mFilename.C_Str());
std::cout << "Texture filename: " << filename << std::endl;

// 如果有纹理数据,可以尝试读取其大小
if (texture->mWidth > 0 && texture->mHeight > 0) {
std::cout << "Texture dimensions: " << texture->mWidth << "x" << texture->mHeight << std::endl;
}

// 根据格式提示来决定如何处理纹理数据
std::string formatHint(texture->achFormatHint);
if (!formatHint.empty()) {
std::cout << "Texture format hint: " << formatHint << std::endl;
}

// 如果有纹理数据,可以直接使用指针访问
if (texture->mData != nullptr) {
// 注意:这里只是示例,通常你需要根据具体的格式解码这些数据
unsigned char* data = texture->mData;
// 使用纹理数据...
}

stb_image

加载图像库,通过stbi_load读取图像的宽高和通道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
GLuint LoadTexture(const char *path, bool clip) {
// 生成纹理
GLuint texture;
glGenTextures(1, &texture);
stbi_set_flip_vertically_on_load(true);
// 加载图像
int img_width, img_height, nrChannels;
unsigned char *data =
stbi_load(path, &img_width, &img_height, &nrChannels, 0);
if (data) {
GLenum format;
if (nrChannels == 1) {
format = GL_RED;
} else if (nrChannels == 3) {
format = GL_RGB;
} else if (nrChannels == 4) {
format = GL_RGBA;
} else {
throw std::runtime_error("No available format.");
}

glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, format, img_width, img_height, 0, format,
GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
if (clip) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
} else {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

} else {
std::cerr << "Failed to load texture" << std::endl;
std::cout << "Error: Failed to load the image because "
<< stbi_failure_reason();
return -1;
}

stbi_image_free(data);
return texture;
}
-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道