图形学系列文章目录
- 序章 初探图形编程
- 第1章 你的第一个三角形
- 第2章 变换
- 顶点变换
- 视图矩阵 & 帧速率
- 第3章 纹理映射
- 第4章 透明度和深度
- 第5章 裁剪区域和模板缓冲区
- 第6章 场景图
- 第7章 场景管理
- 第8章 索引缓冲区
- 第9章 骨骼动画
- 第10章 后处理
- 第11章 实时光照(一)
- 第12章 实时光照(二)
- 第13章 立方体贴图
- 第14章 阴影贴图
- 第15章 延迟渲染
- 第16章 高级缓冲区
文章目录
前言
在本教程中,你将学习纹理映射,通过在你之前的教程中一直使用的三角形上执行一些纹理映射操作来实现。
纹理映射是将图像应用到构成你的场景的多边形上的过程,以提高视觉真实感。OpenGL 为我们做了很多繁重的工作,但我们仍然必须手动配置我们图形硬件的纹理单元,以便获得我们想要的图形结果。
纹理数据
一般来说,纹理以位图的形式存储——一大块线性内存,包含组成单个纹理的每个纹素的红色、绿色、蓝色以及可选的透明度信息——纹理以纹理元素为单位进行测量。这意味着像 JPEG 或 PNG 这样的压缩文件格式在加载到图形内存之前必须进行解压缩。然而,现代图形硬件通常支持原生纹理压缩——通常是由 S3 Graphics 开发的 DXT 压缩的一种变体。这使得纹理占用的内存更少,只是在压缩和解压缩纹理数据时会有一点速度损失。
曾经,从速度角度来看,要求纹理的尺寸是 2 的幂次方,或者至少这样做是有益的。也就是说,一个 512×256 的纹理没问题,但一个 512×320 的纹理在处理时会慢得多。同样,现代图形硬件通常可以支持任何尺寸的纹理。
纹理坐标
一旦纹理被加载到图形内存中,就可以对其进行采样以从中获取颜色。我们通过纹理坐标来实现这一点。这些纹理坐标就像位置和颜色一样是顶点属性,以向量形式存储,它们决定了从纹理的每个轴上的哪个位置进行采样。向量的大小取决于纹理的维度数量——二维纹理在其纹理采样坐标中使用一个二维向量,依此类推。这些坐标是归一化的——无论我们使用的纹理有多大,其纹理坐标范围都是从 0.0 到 1.0。这意味着 OpenGL 应用程序在纹理加载后不必担心纹理的大小。例如,在归一化坐标中,0.5 始终是纹理轴上的一半位置,而“256”将是一个无意义的值;它可能是一个 512 纹素纹理的一半位置,也可能只是一个 1024 纹素纹理的四分之一位置。像其他顶点属性一样,纹理坐标在顶点之间自动进行插值,在纹理上创建平滑均匀的采样。
纹理环绕(Texture Wrapping)
虽然纹理被定义为具有归一化坐标,但顶点可以使用超出这个范围的坐标。那会发生什么呢?这取决于纹理当前的纹理环绕模式——要么是钳位(clamped)要么是重复(repeating)。如果纹理使用钳位,那么超出归一化范围的纹理采样坐标将被锁定在 0.0 到 1.0 的范围内。然而,如果允许重复,那么采样坐标会在纹理上“循环”,所以坐标为 1.5、2.5 甚至 100.5 将会在那个轴上对纹理的中间部分进行采样。下面的图片会更清楚地展示这一点——左边是一个三角形的图片,它的所有纹理坐标都在 0.0 到 1.0 的范围内,而另外两个三角形的纹理坐标在 0.0 到 4.0 的范围内,并且纹理环绕分别设置为重复和钳位。你应该能够看到环绕是如何让一个纹理在一个多边形上重复多次的——这对于像砖墙这样自然重复的场景很有用!右边三角形的大灰色区域是由钳位设置将两个纹理坐标轴都设置为 1.0 造成的——所以在三角形的其余部分会反复采样右上角最后一个纹素。
OpenGL给我们提供了如下选项:
环绕方式 | 描述 |
---|---|
GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
纹理单元
虽然纹理实际上只是内存中的数据数组,但为了将它们转化为屏幕上经过恰当变换和过滤的像素,图形卡使用特殊的纹理单元——专门用于处理纹理数据的硬件。不同的图形卡具有不同数量的纹理单元,不过在现代硬件上你通常可以假定至少有 16 个。要对纹理进行采样,它必须绑定到一个纹理单元上——这与顶点缓冲区必须绑定才能进行操作非常相似。
多级渐远纹理(Mip-Mapping)
在大型场景中,经常会出现这样的情况:应用了大纹理的几何体在远处——例如一个赛道,在长长的直道尽头有一个大型广告广告牌,上面应用了一个 2048×2048 纹素的纹理。即使它在很远的地方,这个大纹理仍然必须被采样。可能对最终像素有贡献的每个纹素都必须被处理——即使我们的广告牌在屏幕上只占一个像素,2048×2048 纹理的每个纹素仍然必须被采样!这既占用带宽又耗费处理资源,但幸运的是有一个解决方案——多级渐远纹理(mipmaps)。多级渐远纹理是一系列较小的、细节较低的纹理副本——所以我们的 2048×2048 纹理会有一个 1024×1024 的副本、一个 512×512 的副本,以此类推,一直到 1×1。然后,当对一个带有多级渐远纹理的纹理进行采样时,会根据被纹理化的片段的距离选择合适的多级渐远纹理副本。现在,我们假设远处那个理论上只有 1×1 像素的广告牌只需要采样 1×1 的多级渐远纹理副本——这大大节省了纹理采样!不过,这种采样性能是以每个纹理使用更多的纹理内存来存储多级渐远纹理为代价的。如果纹理不是正方形且不是 2 的幂次方,最低级的多级渐远纹理将是每个轴能达到的整个值——所以一个 512×320 的纹理会生成大小为 256×160、128×80、64×40、32×20、16×10 和 8×5 的多级渐远纹理。出于这个原因,通常会限制纹理至少是一个 2 的幂次方维度,最好也是正方形的。
从原始图像到 1×1 的多级渐远纹理。
纹理过滤。
在对 3D 几何体进行纹理处理时,纹理的纹素和屏幕上的像素之间不太可能存在精确的 1:1 比例。那么几何体是如何完全被纹理化的呢?当对纹理进行采样时,可以选择最近的纹素,或者在几个附近的像素之间进行插值——就像在第一个教程中颜色在顶点之间进行插值一样!纹理过滤的最基本形式是双线性插值——在插值最终像素颜色时,会考虑 x 轴和 y 轴上最接近的纹素。下面是我们的砖块纹理在使用最近纹素和双线性插值纹理过滤方法时的例子。注意,最近的方法会导致块状的“像素化”,而双线性插值看起来有点模糊。
双线性插值的缺点在于使用多级渐远纹理(mip-maps)时——附近纹素的过滤不会跨越 mip-map 边界,所以在边界处会有明显的模糊“跳跃”。解决这个问题的方法是计算更复杂的三线性插值,它对多个 mip-map 进行双线性采样,然后在它们之间进行插值,从而产生更平滑的 mip-map 过渡。双线性和三线性在尝试对与相机非常倾斜的多边形进行纹理处理时往往会失败——因为它们在每个轴上以相同的方式进行插值,多边形的倾斜会导致采样更低级别的 mip-map,从而产生模糊的最终图像。为了解决这个问题,可以使用一种计算成本更高的过滤选项——各向异性过滤。这种过滤方法在每个轴上对纹理进行不同程度的采样(各向异性意味着不同方向),通过一个比率来确定每个轴上的过滤量。下一页的图像展示了各向异性过滤有益的一个典型情况——左边的图片显示,由于过滤精度的损失,即使使用三线性过滤,远处的道路标记也会变得模糊,而右边的图片显示了各向异性过滤如何即使在远处也能保持道路标记清晰。
多重纹理。
在对场景中的三角形进行纹理处理时,你并不局限于使用单个纹理。一个片段着色器可以采样尽可能多的纹理,只要有可用的纹理单元。由于片段着色器是完全可编程的,你对从纹理中采样的数据所做的处理完全由你决定——例如,你可以将它们全部混合在一起,或者从多个单独的纹理采样器中获取通道分量。它们也可以各自有单独的纹理坐标——只需将更多独特的顶点属性数据传递给顶点着色器!
纹理矩阵。
有一种情况是,归一化坐标为 0.5 并不一定会在纹理轴上采样到一半位置的纹素。与顶点位置坐标一样,纹理坐标可以通过矩阵进行变换。这些是 4×4 的矩阵,就像你在教程 2 中介绍的那些一样——它们可以包含缩放、平移和旋转分量。纹理矩阵并不经常使用,但知道它们的存在是很有用的,我们稍后编写的代码将展示一个纹理矩阵的例子,它将旋转我们三角形上的纹理贴图。
插值纹理坐标。
在介绍教程中,你了解到顶点值通常根据当前处理的片段的位置从一个顶点插值到另一个顶点。然后,在教程 2 中,你了解了透视矩阵、透视除法以及投影矩阵如何用于将 3D 空间映射到我们的 2D 屏幕上。然而,我们想要的三角形的透视缩短效果对于插值的顶点值也可能有不良的副作用。以这个 3D 空间中的一条线被展平到我们的图像平面上的例子为例:
一旦我们使用矩阵管道将三角形展平,我们会发现当我们看起来像是在看三角形中心(位置 a)时看到的值并不是我们在 3D 空间中实际看到的线的部分(位置 b)——这给了我们略有不同的颜色。如果这些不是颜色,而是插值的纹理坐标,我们将对纹理的错误部分进行采样,并且 3D 透视缩短效果将被破坏!为了计算透视正确的纹理坐标(或着色器中的任何其他变化的值),纹理坐标必须除以我们顶点坐标的齐次 w 值,然后可以如介绍教程中所述进行插值。最后,这些插值的值除以 1/w,将它们变回“世界空间”插值值,只是在正确的位置。几乎所有的着色器语言都会在顶点和片段阶段之间自动执行这种透视校正以及其余的插值操作,所以你真的不需要过多考虑这个问题,只是知道硬件在做什么很有用!
纹理示例程序。
现在来实现一些你刚刚学到的纹理映射理论。我们要制作一个简单的程序,它不是像教程 1 中那样绘制一个具有插值顶点颜色的三角形,而是绘制一个应用了 2D 纹理贴图的三角形!非常简单,但这将让你很好地理解如何在 OpenGL 中使用纹理。对于这个示例程序,你需要在 Tutorial3 Visual Studio 项目中创建 3 个文件 —— 一个包含我们主函数的简短的 cpp 文件、我们的纹理 OpenGL 渲染器类的头文件和类文件,以及在…/Shaders/ 文件夹中分别为新的顶点着色器和片段着色器各创建一个文件。我们还将使用一个纹理文件 “brick.tga”,它在…/Textures/ 文件夹中为你提供。
网格类
为了在我们的三角形上使用纹理,我们需要修改 GenerateTriangle
方法,为顶点着色器提供顶点坐标属性。如果 textureCoords
成员变量已经被实例化,那么网格类将已经上传这些属性,所以对 GenerateTriangle
的更改非常简单:
...
m->textureCoords = new Vector2[m->numVertices];
m->textureCoords[0] = Vector2(0.5f, 0.0f);
m->textureCoords[1] = Vector2(1.0f, 1.0f);
m->textureCoords[2] = Vector2(0.0f, 1.0f);
...
m->BufferData();
return m;
选择的坐标将创建与几何体形状相同的三角形,并且在两个轴上具有均匀的比例。
OGLRenderer 类
在本教程系列中,我们将经常使用重复纹理的功能。为了避免重复编写实现此功能的代码,我们将在所有渲染器都将派生自的基类中添加一个方法。在项目的 OGLRenderer
基类中,我们需要在protected
部分添加一个新的方法声明:
//OGLRenderer.h
protected:
void SetTextureRepeating(GLuint target, bool state );
在类文件中,我们将如下定义这个函数:
//OGLRenderer.cpp
void OGLRenderer::SetTextureRepeating(GLuint target, bool repeating)
{
glBindTexture(GL_TEXTURE_2D, target);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, repeating ? GL_REPEAT : GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, repeating ? GL_REPEAT : GL_CLAMP);
glBindTexture(GL_TEXTURE_2D, 0);
}
OpenGL 函数glTexParameteri
允许我们在当前绑定到 OpenGL 状态机的任何纹理上设置各种属性(在第 4 行完成)。在 OpenGL 术语中,纹理的坐标被定义为 S 和 T,所以告诉状态机是否环绕的关键字是GL_TEXTURE_WRAP_S
和GL_TEXTURE_WRAP_T
。然后,我们使用 OpenGL 函数glTexParameteri
为我们纹理贴图的两个轴设置纹理环绕状态,最后,我们解除纹理的绑定,因为我们已经完成了对它的修改。稍后我们将更多地看到glTexParameteri
函数,但现在我们已经完成了对OGLRenderer
类的处理!
Renderer头文件
这个头文件与上一个教程类似,因为它重写了OGLRenderer基类的RenderScene
方法。本教程中的新内容是在第 18 行有一个GLuint
,它将存储我们想要应用于在第 17 行声明的三角形的纹理。为了控制纹理采样,我们还有两个布尔值——filtering
(过滤)和repeating
(重复)。还有三个新的公共函数——UpdateTextureMatrix
、ToggleRepeating
和ToggleFiltering
。
#pragma once
#include "../OGLRenderer.h"
class Renderer: public OGLRenderer
{
public:
Renderer(Window& parent);
virtual ~Renderer(void);
virtual void RenderScene();
void UpdateTextureMatrix(float rotation);
void ToggleRepeating();
void ToggleFiltering();
protected:
Shader* shader;
Mesh* triangle;
GLuint texture;
bool filtering;
bool repeating;
};
Renderer类文件
与之前的教程一样,我们首先使用Mesh类创建一个三角形。然后,我们使用简单 OpenGL 图像库加载我们的纹理。这处理了生成纹理数据的所有繁重工作,并返回纹理的 OpenGL 名称——就像我们的着色器阶段以及Mesh类中的顶点缓冲区都有 ID 一样,纹理也有。如果我们得到的纹理索引为 0,这意味着 OpenGL 未能生成纹理(可能是文件名错误?),所以我们退出应用程序。
#include "Renderer.h"
Renderer::Renderer(Window& parent) : OGLRenderer(parent) {
triangle = Mesh::GenerateTriangle();
texture = SOIL_load_OGL_texture(TEXTUREDIR"brick.tga", SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, 0);
if (!texture) {
return;
}
在本教程中,我们的着色器是新的,因为我们有一个新的顶点属性要处理,还有一个新的 GLSL 方法来采样纹理。构造函数的最后一个任务是将其他成员变量设置为有用的默认值,并将init
设置为 true。
shader = new Shader("TexturedVertex.glsl", "texturedfragment.glsl");
if (!shader->LoadSuccess()) {
return;
}
filtering = true;
repeating = false;
init = true;
}
我们的渲染器类的析构函数与上一个教程保持一致,因为我们的Mesh类将在它自己的析构函数中删除我们创建的纹理。
Renderer ::~Renderer(void) {
delete triangle;
delete shader;
glDeleteTextures(1, &texture);
}
我们的Renderer类的RenderScene
函数也与上一个教程非常相似。我们绑定着色器,更新矩阵,然后绘制三角形。这次,我们的片段着色器将有一个新的统一变量叫做diffuseTex
。正如我们很快将在片段着色器代码中看到的,这看起来像是一个全新的数据类型,但在 C++方面,我们给它一个简单的整数值——在这种情况下,我们想要将一个纹理绑定到纹理单元 0,所以我们也将我们的新统一变量设置为 0。在 OpenGL 中绑定纹理可以通过一次调用glBindTexture
来完成。这需要一个类型(在这种情况下我们想要绑定一个 2D 纹理),以及我们想要绑定的纹理的 OpenGL 名称(我们的纹理变量)。这次调用glActiveTexture
并不是严格必要的,但它的作用是告诉 OpenGL,任何调用的纹理函数都应该应用于纹理单元 0,所以当glBindTexture
被绑定时,它是在那个特定的单元上进行绑定的。这就是我们需要做的全部,所以我们可以通过使用我们的新着色器绘制三角形来完成这个方法。
void Renderer::RenderScene() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
BindShader(shader);
UpdateShaderMatrices();
glUniform1i(glGetUniformLocation(shader->GetProgram(), "diffuseTex"), 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
triangle->Draw();
}
在示例程序中,当按下左右键时,我们将使三角形的纹理旋转。为了实现这一点,我们将创建UpdateTextureMatrix
函数。旋转矩阵变量可能很容易理解,它就像我们在上一个教程中应用于模型矩阵的旋转矩阵一样,但是push
和pop
矩阵是怎么回事呢?由于我们的旋转矩阵没有平移数据,它将围绕原点
(
0
,
0
,
0
)
(0,0,0)
(0,0,0)转我们的纹理坐标。在纹理坐标中,那是我们三角形的左下角点,所以我们的纹理将围绕这个点旋转。在某些情况下,这可能是我们想要的效果,但我们将让我们的纹理围绕三角形的中心旋转。在纹理坐标空间中,我们三角形的中心是
(
0.5
,
0.5
,
0
)
(0.5,0.5,0)
(0.5,0.5,0),所以我们将旋转矩阵乘以一个平移矩阵,以平移到我们纹理的中心,然后进行旋转。然后我们再平移回原点以完成我们的旋转变换。这种“推入”和“弹出”矩阵的方法对于围绕除原点以外的点进行旋转很有用——所以请记住这一点!
void Renderer::UpdateTextureMatrix(float value) {
Matrix4 push = Matrix4::Translation(Vector3(-0.5f, -0.5f, 0));
Matrix4 pop = Matrix4::Translation(Vector3(0.5f, 0.5f, 0));
Matrix4 rotation = Matrix4::Rotation(value, Vector3(0, 0, 1));
textureMatrix = pop * rotation * push;
}
我们的纹理映射测试应用程序将支持纹理重复的切换——这由ToggleRepeating
函数控制。我们使用NOT
布尔运算符切换我们的repeating
类成员变量。随着布尔值的翻转,我们可以然后使用前面介绍的SetTextureRepeating
方法使纹理要么夹紧要么重复。
void Renderer::ToggleRepeating() {
repeating = !repeating;
SetTextureRepeating(texture, repeating);
}
我们的最后一个Renderer类函数是ToggleFiltering
。与ToggleRepeating
非常相似,它翻转我们的成员变量,并设置一个纹理参数;这次它在我们三角形的砖块纹理上在双线性过滤和最近邻过滤之间切换。在本教程的其余部分中,我们实际上并不需要设置纹理使用最近邻过滤,但如果你想进一步尝试这个,可以像我们对纹理环绕所做的那样,向OGLRenderer基类添加额外的功能。
void Renderer::ToggleFiltering() {
filtering = !filtering;
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filtering ? GL_LINEAR : GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filtering ? GL_LINEAR : GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
}
顶点着色器。
我们的顶点着色器将会非常简单。这次我们有四个统一矩阵,因为除了上一个教程中介绍的模型、视图和投影矩阵之外,我们还使用了一个纹理矩阵。与上一个教程一样,我们使用组合的模型-视图-投影矩阵来变换我们的顶点。就像顶点位置一样,我们也必须将顶点纹理坐标扩展为一个四分量向量,以便与我们的纹理矩阵相乘——但请注意我们如何然后使用.xy
将其转换回一个vec2
。这是向量混合(vector swizzling)的一个例子,稍后将更详细地解释。
# version 330 core
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projMatrix;
uniform mat4 textureMatrix;
in vec3 position;
in vec2 texCoord;
out Vertex {
vec2 texCoord;
} OUT;
void main ( void ) {
mat4 mvp = projMatrix*viewMatrix*modelMatrix;
gl_Position = mvp*vec4(position, 1.0);
OUT.texCoord = (textureMatrix*vec4(texCoord,0.0,1.0)).xy;
}
片段着色器。
另一个简单的片段着色器,与你在之前的教程中使用的类似。在第 2行我们有一个新的统一变量,一个纹理采样器。这是一种特殊类型的 GLSL 变量,对应于你的图形硬件上的一个纹理单元。请注意它明确是一个 2D 采样器——也有 1D 和 3D 采样器类型。我们在第 10 行使用这个纹理采样器,使用纹理 GLSL 函数。这个函数接受一个纹理采样器和一个纹理坐标作为参数,并返回一个包含在这个点上我们纹理的颜色的vec4
——如果设置了双线性过滤,这可能是一个插值后的颜色。
#version 330 core
uniform sampler2D diffuseTex;
in Vertex {
vec2 texCoord;
} IN;
out vec4 fragColour ;
void main ( void ) {
fragColour = texture(diffuseTex, IN.texCoord);
}
向量混合(Vector Swizzling)
如前所述,纹理 GLSL 函数返回一个vec4
——对应于纹理贴图的红色、绿色、蓝色和透明度分量。如果我们想要使用红色通道分量,我们如何访问它呢?嗯,如果需要的话,我们可以使用.x
来访问它,因为红色是vec4
的第一个分量。然而,幸运的是,当处理颜色时,如果我们想要,GLSL 允许我们使用更直观的.r
/.g
/.b
/.a
来访问vec4
。请注意,我们不能在单个操作中混合和匹配“几何”和“颜色”分量标签。然而,我们可以做的是混合这些分量。如果我们想要,我们可以以不同的顺序提取值——我们可以通过使用.grbw
访问颜色来交换红色和绿色的值,甚至可以使用.rrrr
(或.xxxx
)将源vec4
的第一个通道扩展到所有颜色分量。混合操作的执行成本很低,所以在你的代码中任何合适的地方都可以随意使用它们。混合操作不仅限于你用于颜色的vec4
,在着色器程序中用于任何目的的任何向量类型都可以进行混合!
fragColour = texture ( diffuseTex , IN . texCoord ). rgba ; // 允许
fragColour = texture ( diffuseTex , IN . texCoord ). xyzw ; // 允许
fragColour = texture ( diffuseTex , IN . texCoord ). rgzw ; // 不允许!
fragColour = texture ( diffuseTex , IN . texCoord ). bgra ; // Swizzling!
fragColour = texture ( diffuseTex , IN . texCoord ). xxxw ; // Swizzling !
主文件。
本教程的主文件现在看起来应该相当熟悉了!请注意,这次我们有更多的按键检查,以处理我们的纹理旋转、过滤和重复设置。除此之外,都是一样的。
#include "../nclGL/window.h"
#include "Renderer.h"
int main() {
Window w("Texturing!", 800, 600,false); //This is all boring win32 window creation stuff!
if(!w.HasInitialised()) {
return -1;
}
Renderer renderer(w); //This handles all the boring OGL 3.2 initialisation stuff, and sets up our tutorial!
if(!renderer.HasInitialised()) {
return -1;
}
float rotate = 0.0f;
while(w.UpdateWindow() && !Window::GetKeyboard()->KeyDown(KEYBOARD_ESCAPE)){
if(Window::GetKeyboard()->KeyDown(KEYBOARD_LEFT) ) {
--rotate;
renderer.UpdateTextureMatrix(rotate);
}
if(Window::GetKeyboard()->KeyDown(KEYBOARD_RIGHT) ) {
++rotate;
renderer.UpdateTextureMatrix(rotate);
}
if(Window::GetKeyboard()->KeyTriggered(KEYBOARD_1) ) {
renderer.ToggleFiltering();
}
if(Window::GetKeyboard()->KeyTriggered(KEYBOARD_2) ) {
renderer.ToggleRepeating();
}
renderer.RenderScene();
renderer.SwapBuffers();
}
return 0;
}
总结
如果编译成功,当你运行程序时,你应该在屏幕上看到一个带有纹理的三角形。你可以使用箭头键旋转纹理,并使用数字键 1 和 2 切换双线性过滤和纹理重复。恭喜!你现在知道了纹理的基础知识,并且应该为在本教程系列的其余部分中执行的纹理操作做好了充分准备。下一章节我们将进一步探讨纹理映射,通过研究透明度以及深度缓冲区的使用方法。
课后作业
- 尝试调整在渲染器构造函数中定义的三角形的纹理坐标。了解它们是如何受到纹理环绕命令影响的。记住——纹理环绕可以按轴定义!
- 如果我们将三角形的纹理坐标范围从 0.0 - 1.0 增加到 0.0 - 10.0,我们的旋转矩阵会发生什么变化?它仍然会围绕三角形的中心旋转矩阵吗?除了直接通过顶点缓冲区纹理坐标之外,我们还可以通过什么其他方式来缩放纹理在三角形上重复的次数?
- 在第一个教程中,我们使用颜色顶点缓冲区为三角形着色。尝试将教程 1 中的颜色代码添加到本教程的Renderer中,并在片段着色器中将颜色和纹理采样混合在一起。你甚至可以使用从Renderer类发送到片段着色器的统一浮点值来改变纹理采样颜色的混合程度……
- 尝试向你的Renderer添加另一个纹理,并在片段着色器中将两个纹理混合在一起。在你的片段着色器中需要两个
texture2D
采样器…… - NCLGL 使用的简单 OpenGL 图像库可以在加载图像时使用
SOIL_FLAG_MIPMAPS
枚举自动为纹理生成 mipmap。尝试向你的渲染器添加一个相机,并观察你在本教程中学到的纹理过滤设置如何影响从不同角度和距离观看三角形时的外观。三线性过滤可以通过将最小过滤设置为GL_LINEAR_MIPMAP_NEAREST
来启用。为什么它不应该为放大过滤启用呢? - 研究如何确定你的图形硬件可以执行的最大各向异性过滤量,以及如何在每个纹理的基础上启用它。
- 研究顶点输出值的
noperspective
插值限定符。你认为这有什么作用?
附录 A:艰难地加载纹理
在这些教程中,我们使用外部库来加载纹理数据。但是如果你已经在主内存中有纹理数据了呢(例如,如果你使用自己的纹理加载函数,或者通过程序生成它们)?以下代码演示了如何生成一个 2D 纹理,并将你的纹理加载到图形内存中,同时启用 mip-mapping:
GLuint texture ;
glGenTextures(1 , &texture );
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D , texture);
glTexImage2D(GL_TEXTURE_2D , mipmapLevel , internalFormat ,width , height , border , format , datatype , data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D , GL_TEXTURE_MIN_FILTER ,GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D , GL_TEXTURE_MAG_FILTER , GL_NEAREST );
glBindTexture(GL_TEXTURE_2D , 0);
就像顶点数组对象(Vertex Array Objects)和顶点缓冲对象(Vertex Buffer Objects)一样,我们必须为纹理生成一个新的名称(第 2 行),然后将其绑定到一个选定的纹理单元(第 4 和 5 行)。
第 7 行实际上使用 OpenGL 函数glTexImage
上传我们的纹理数据,这个函数在 OpenGL 规范网站上有更详细的文档说明。mipmapLevel
是这个纹理将代表的 mipmap 级别——通常这将是 0。internalFormat
参数告诉 OpenGL 你的纹理将具有什么样的颜色分量类型——通常是GL_RGB
或GL_RGBA
,尽管也存在其他格式。width
和height
是正在加载的纹理的尺寸,而border
是已弃用的功能,所以应该是 0。format
描述实际正在加载的颜色分量——通常这将与internalFormat
匹配,但例如,你可以有一个GL_RGBA
的内部格式,但只加载红色分量,使用GL_RED
格式。datatype
告诉 OpenGL 主内存中每个像素的数据类型是什么——例如GL_UNSIGNED_BYTE
或GL_FLOAT
。最后,data
是指向包含你的纹理数据的内存起始位置的指针——OpenGL 将能够使用width
、height
和datatype
参数计算出你的纹理的字节大小。
一旦我们的纹理数据在图形内存中,我们可以告诉 OpenGL 为纹理生成 mipmap(第 8行),最后,设置最小和最大过滤器(第 10 和 11 行)。最后,我们可以选择清理并从纹理单元解除绑定我们的纹理。