在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.
此外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
7void 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 |
|
接口块
为了方便在着色器之间传递数据,可以定义in out块,类似结构体1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
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
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
9layout (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
5unsigned 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块绑定到相同的绑定点,把它们连接到一起。下面的这个图示展示了这个:
你可以看到,我们可以绑定多个Uniform缓冲到不同的绑定点上。因为着色器A和着色器B都有一个链接到绑定点0的Uniform块,它们的Uniform块将会共享相同的uniform数据,uboMatrices,前提条件是两个着色器都定义了相同的Matrices Uniform块。
为了将Uniform块绑定到一个特定的绑定点中,我们需要调用glUniformBlockBinding函数,它的第一个参数是一个程序对象,之后是一个Uniform块索引和链接到的绑定点。Uniform块索引(Uniform Block Index)是着色器中已定义Uniform块的位置值索引。这可以通过调用glGetUniformBlockIndex来获取,它接受一个程序对象和Uniform块的名称1
2unsigned 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
3glBindBufferBase(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
4glBindBuffer(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向适应的向量计算库
- 向量类:
glm::vec2
: 2D 向量。glm::vec3
: 3D 向量。glm::vec4
: 4D 向量(通常用于颜色或齐次坐标)。
- 矩阵类:
glm::mat2
: 2x2 矩阵。glm::mat3
: 3x3 矩阵。glm::mat4
: 4x4 矩阵(常用于变换矩阵)。
- 四元数类:
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
3glm::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);
投影矩阵 (
perspective
和orthographic
):1
2
3glm::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
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.
mFlags
: 这个标志表示场景的一些特性,例如是否是不完整的 (AI_SCENE_FLAGS_INCOMPLETE
) 或者是否有无效的数据 (AI_SCENE_FLAGS_INVALID_DATA
)。mRootNode
: 指向场景根节点的指针。每个场景都有一个根节点,所有其他节点都是它的子节点。通过遍历这个节点树,你可以获取场景的所有几何信息。mNumMeshes
和mMeshes
: 分别表示场景中网格的数量和指向网格数组的指针。网格包含了顶点、面和其他几何数据。mNumMaterials
和mMaterials
: 分别表示场景中材质的数量和指向材质数组的指针。材质定义了网格的外观属性。mNumTextures
和mTextures
: 分别表示场景中文本贴图的数量和指向文本贴图数组的指针。请注意,不是所有的模型格式都支持直接导出纹理,所以这个成员可能为空。mNumCameras
和mCameras
: 分别表示场景中相机的数量和指向相机数组的指针。并不是所有模型文件都会包含相机信息。mNumLights
和mLights
: 分别表示场景中光源的数量和指向光源数组的指针。同样地,并非所有模型文件都包含光源信息。mMetaData
: 包含有关场景的元数据。这可以包括版本号、作者等信息
1 | // 假设我们已经有一个 aiScene* scene |
Nodes
节点是场景中名字不大的实体,相对于它们的父节点有一个位置和方向。从场景的根节点开始,所有节点可以有0到x个子节点,从而形成一个层次结构。
mTransformation
:- 类型:
aiMatrix4x4
- 描述: 表示节点的本地变换矩阵,它定义了该节点相对于其父节点的位置、旋转和缩放。
- 类型:
mNumMeshes
和mMeshes
:- 类型:
unsigned int
和unsigned int*
- 描述:
mNumMeshes
表示此节点直接关联的网格数量;mMeshes
是一个索引数组,指向aiScene
的mMeshes
数组中的相应网格。如果mNumMeshes
为 0,则该节点没有直接关联的网格。
- 类型:
mParent
:- 类型:
aiNode*
- 描述: 指向该节点的父节点的指针。根节点的
mParent
为nullptr
。
- 类型:
mNumChildren
和mChildren
:- 类型:
unsigned int
和aiNode**
- 描述:
mNumChildren
表示该节点的子节点数量;mChildren
是一个指针数组,指向该节点的所有子节点。
- 类型:
mName
:- 类型:
aiString
- 描述: 节点的名字。在某些情况下,这个名字可能被用来标识特定的节点或作为动画等的参考
- 类型:
1 | // 假设我们有一个 aiNode* node |
Mesh
mPrimitiveTypes
:- 类型:
unsigned int
- 描述: 表示该网格中包含的图元类型,例如点 (
aiPrimitiveType_POINT
)、线 (aiPrimitiveType_LINE
) 或三角形 (aiPrimitiveType_TRIANGLE
)。
- 类型:
mNumVertices
和mVertices
:- 类型:
unsigned int
和aiVector3D*
- 描述:
mNumVertices
表示顶点的数量;mVertices
是指向aiVector3D
数组的指针,每个元素代表一个顶点的位置。
- 类型:
mNormals
:- 类型:
aiVector3D*
- 描述: 指向法线数组的指针。如果网格包含法线数据,则每个顶点都有一个对应的法线向量。
- 类型:
mTextureCoords
:- 类型:
aiVector3D**
- 描述: 指向纹理坐标数组的二维数组指针。第一维是纹理坐标集的数量(最多8个),第二维是实际的纹理坐标。如果某个顶点有纹理坐标,则可以通过这个成员访问。
- 类型:
mColors
:- 类型:
aiColor4D**
- 描述: 指向颜色数组的二维数组指针。第一维是颜色集的数量(最多8个),第二维是实际的颜色值。如果顶点有色值,则可以在这里找到。
- 类型:
mNumFaces
和mFaces
:- 类型:
unsigned int
和aiFace*
- 描述:
mNumFaces
表示面的数量;mFaces
是指向aiFace
数组的指针,每个aiFace
定义了一个由若干顶点组成的多边形(通常是三角形)。
- 类型:
mMaterialIndex
:- 类型:
unsigned int
- 描述: 网格使用的材质在场景的
mMaterials
数组中的索引。通过这个索引,你可以获取与该网格关联的aiMaterial
对象。
- 类型:
mNumBones
和mBones
:- 类型:
unsigned int
和aiBone**
- 描述: 如果网格支持骨骼动画,则
mBones
包含了指向aiBone
数组的指针,每个aiBone
定义了一个影响顶点位置的骨骼。mNumBones
是骨骼的数量。
- 类型:
mName
:- 类型:
aiString
- 描述: 网格的名字。某些情况下,这个名字可能是有意义的,比如用于标识特定的网格或作为其他资源的引用。
- 类型:
1 | // 假设我们有一个 aiMesh* mesh 和 aiScene* scene |
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
提供了一系列的 Get
和 Set
函数。最常用的 Get
函数包括:
Get(AI_MATKEY key, aiColor4D& out)
:- 获取指定键的颜色值。
Get(AI_MATKEY key, float& out)
:- 获取指定键的浮点数值。
Get(AI_MATKEY key, bool& out)
:- 获取指定键的布尔值。
GetTexture(aiTextureType type, unsigned int index, aiString\* path)
:- 获取指定类型的纹理路径。
index
参数允许你访问同一类型下的多个纹理(例如,多层扩散纹理)。
- 获取指定类型的纹理路径。
HasProperty(const char\* key)
:- 检查材质是否具有给定键的属性。
Get(AI_MATKEY key, unsigned int& out)
:- 获取指定键的无符号整数值(例如,用于获取纹理的数量)
1 | // 假设我们有一个 aiMaterial* material |
Texture
通常,资源使用的纹理存储在单独的文件中,但是也有文件格式将纹理直接嵌入到模型文件中。这样的纹理被加载到aittexture结构中
mWidth
和mHeight
:- 类型:
unsigned int
- 描述: 分别表示纹理图像的宽度和高度(以像素为单位)。对于非图像格式(例如程序生成的纹理),这些值可能为0。
- 类型:
mData
:- 类型:
unsigned char*
- 描述: 指向包含纹理数据的缓冲区。注意,并不是所有情况下都会提供实际的纹理数据;某些导入器可能会直接返回文件路径而不是加载图像数据到内存中。
- 类型:
mHeight
:- 类型:
unsigned int
- 描述: 纹理的高度(以像素为单位)。
- 类型:
achFormatHint
:- 类型:
char[AI_TEXTURE_FORMAT_MAX]
- 描述: 提供关于纹理格式的提示字符串,例如
"jpg"
或"png"
。这可以帮助你确定如何正确地解码纹理数据。
- 类型:
mFilename
:- 类型:
aiString
- 描述: 包含纹理文件的相对或绝对路径名。这是最常用的方式来获取纹理资源的位置。
- 类型:
1 | // 假设我们有一个 aiTexture* texture |
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
46GLuint 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;
}