第6章 纹理映射(Texture Mapping)
一、导读
1.1 本章学习目标
纹理映射是为三维模型添加表面细节的核心技术。没有纹理的三维模型看起来像塑料玩具,而有了高质量的纹理,模型可以呈现出照片级的真实感——木头的纹理、砖墙的粗糙、金属的划痕、布料的编织图案,这些都是纹理映射的功劳。
本章的核心学习目标包括:
- 理解纹理映射的基本原理:掌握将二维图像映射到三维表面的数学方法,理解纹理坐标(UV 坐标)的定义和生成。
- 掌握纹理滤波技术:理解最近邻、双线性、三线和各向异性滤波的原理和效果差异,以及为什么远处物体会出现闪烁和摩尔纹。
- 理解 Mipmap 技术:掌握多级纹理的原理和生成方法,理解它如何解决远处物体的走样问题并提高缓存命中率。
- 掌握高级纹理技术:深入理解凹凸映射、法线贴图、视差映射和视差遮挡映射的原理和实现。
- 了解 PBR 纹理工作流:理解金属度-粗糙度工作流中各纹理贴图的物理含义和制作方法。
- 掌握纹理压缩和格式:了解 BC、ASTC、ETC2 等纹理压缩格式的特点和适用场景。
1.2 为什么本章如此重要
纹理映射是实时渲染中提升视觉质量最有效的手段。一个只有 1000 个三角形的模型,配上高分辨率的纹理贴图,看起来可能比 100000 个三角形但没有纹理的模型更加精细。这就是纹理映射的力量——它用极小的性能代价(主要是显存带宽)换取了巨大的视觉提升。
从技术角度看,纹理映射涉及图形学中多个核心问题:采样理论(如何正确地从连续纹理中采样)、信号处理(如何避免走样)、内存管理(如何高效地存储和访问纹理数据)。理解这些底层原理,你才能正确选择纹理参数、诊断纹理相关的渲染问题、优化纹理的内存和带宽使用。
在现代 PBR 渲染中,纹理不再仅仅是"贴一张图片"。一套完整的 PBR 材质可能包含 5-6 张纹理贴图(基础颜色、金属度、粗糙度、法线、环境光遮蔽、高度),每张都编码了不同的物理属性。理解纹理映射的原理,是掌握 PBR 工作流的前提。
1.3 核心问题引导
在阅读本章时,请带着以下问题思考:
纹理坐标是如何定义的?为什么使用 (u, v) 而不是 (x, y)?UV 展开的本质是什么问题?
当一个纹理像素(texel)对应多个屏幕像素时,或者一个屏幕像素对应多个纹理像素时,分别会出现什么问题?如何解决?
各向异性滤波为什么比三线性滤波效果好?它的性能开销在哪里?
法线贴图为什么用紫色(蓝紫色)存储法线?如何从法线贴图中恢复出实际的法线向量?
视差遮挡映射(POM)与视差映射有什么区别?它如何模拟真正的几何位移效果?
纹理压缩如何在有限的显存中存储高质量的纹理?有损压缩对渲染质量有什么影响?
二、核心概念详解
2.1 纹理映射基础
2.1.1 纹理坐标
纹理坐标(Texture Coordinates),也称为 UV 坐标,定义了三维模型表面上的点与二维纹理图像之间的对应关系。对于每个顶点,除了位置和法线,还需要指定纹理坐标 (u, v),其中 u 和 v 的范围通常是 [0, 1]。
纹理坐标系统:
纹理图像 模型表面
(0,1)───(1,1) ┌─────┐
│ │ │ / │
│ 纹理 │ 映射 │ / │
│ │ ──→ │ / │
(0,0)───(1,0) └─────┘
u 轴对应水平方向,v 轴对应垂直方向
注意:图像坐标的 v 轴通常向下,而 OpenGL 纹理坐标的 v 轴向上// 带纹理坐标的顶点结构
struct Vertex {
glm::vec3 position;
glm::vec3 normal;
glm::vec2 texCoord; // UV 坐标
};
// 立方体的纹理坐标
std::vector<Vertex> cubeVertices = {
// 前面
{{-0.5f, -0.5f, 0.5f}, {0, 0, 1}, {0.0f, 0.0f}},
{{ 0.5f, -0.5f, 0.5f}, {0, 0, 1}, {1.0f, 0.0f}},
{{ 0.5f, 0.5f, 0.5f}, {0, 0, 1}, {1.0f, 1.0f}},
{{-0.5f, 0.5f, 0.5f}, {0, 0, 1}, {0.0f, 1.0f}},
// ... 其他面类似
};2.1.2 纹理加载与绑定
// 使用 stb_image 加载纹理
#include <stb_image.h>
GLuint loadTexture(const char* path) {
GLuint textureID;
glGenTextures(1, &textureID);
int width, height, nrChannels;
stbi_set_flip_vertically_on_load(true); // OpenGL v 轴向上
unsigned char* data = stbi_load(path, &width, &height, &nrChannels, 0);
if (data) {
GLenum format = GL_RGB;
if (nrChannels == 1) format = GL_RED;
else if (nrChannels == 3) format = GL_RGB;
else if (nrChannels == 4) format = GL_RGBA;
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0,
format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
// 设置纹理参数
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: " << path << std::endl;
}
stbi_image_free(data);
return textureID;
}2.1.3 纹理环绕模式
当纹理坐标超出 [0, 1] 范围时,需要定义如何处理:
// GL_REPEAT:重复纹理(默认)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
// 效果:纹理在表面上平铺
// GL_MIRRORED_REPEAT:镜像重复
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
// 效果:每次重复时镜像翻转
// GL_CLAMP_TO_EDGE:拉伸边缘像素
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
// 效果:边缘像素延伸到边界
// GL_CLAMP_TO_BORDER:使用边框颜色
float borderColor[] = {1.0f, 0.0f, 0.0f, 1.0f};
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);2.2 纹理滤波
2.2.1 放大滤波(Magnification)
当一个纹理像素(texel)小于一个屏幕像素时,需要决定用哪个 texel 的颜色。
最近邻滤波(GL_NEAREST):选择距离纹理坐标最近的 texel。速度快但会产生块状效果。
双线性滤波(GL_LINEAR):对最近的 4 个 texel 进行加权平均。效果好但稍慢。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
// 或
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);最近邻 vs 双线性:
纹理坐标落在 4 个 texel 之间:
┌────┬────┐
│ A │ B │
├────✕────┤ ← 纹理坐标位置
│ C │ D │
└────┴────┘
最近邻:选择最近的 texel(如 A)
双线性:先水平插值再垂直插值
top = lerp(A, B, fx)
bot = lerp(C, D, fx)
result = lerp(top, bot, fy)2.2.2 缩小滤波(Minification)
当一个纹理像素大于一个屏幕像素时(物体远离相机),一个屏幕像素覆盖了多个 texel,直接采样会导致走样(闪烁、摩尔纹)。
问题:假设一个 1024×1024 的纹理映射到屏幕上只有 10×10 像素的区域。每个屏幕像素覆盖了约 100 个 texel,但标准采样只取其中 1 个或 4 个,大量纹理信息被丢弃,导致严重的走样。
Mipmap 解决方案:预计算一系列缩小版本的纹理,每级宽高减半。
Mipmap 层级:
Level 0: 1024 × 1024 (原始纹理)
Level 1: 512 × 512
Level 2: 256 × 256
Level 3: 128 × 128
Level 4: 64 × 64
Level 5: 32 × 32
Level 6: 16 × 16
Level 7: 8 × 8
Level 8: 4 × 4
Level 9: 2 × 2
Level 10: 1 × 1
总显存 = 原始纹理 × (1 + 1/4 + 1/16 + ...) ≈ 原始纹理 × 4/3// 生成 Mipmap
glGenerateMipmap(GL_TEXTURE_2D);
// 使用 Mipmap 的滤波模式
// GL_NEAREST_MIPMAP_NEAREST:最近 Mipmap 级 + 最近邻
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_NEAREST_MIPMAP_NEAREST);
// GL_LINEAR_MIPMAP_NEAREST:最近 Mipmap 级 + 双线性
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_NEAREST);
// GL_NEAREST_MIPMAP_LINEAR:相邻两级各最近邻 + 级间线性插值
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_NEAREST_MIPMAP_LINEAR);
// GL_LINEAR_MIPMAP_LINEAR:相邻两级各双线性 + 级间线性插值(最好)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);2.2.3 各向异性滤波
三线性滤波假设屏幕像素在纹理上的投影是正方形,但实际上当表面倾斜时,投影可能是细长的矩形。各向异性滤波(Anisotropic Filtering)考虑了这种非均匀的投影形状。
// 启用各向异性滤波
float maxAniso;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY, &maxAniso);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, maxAniso);
// 通常设为 16,表示沿最长轴最多采样 16 个 texel各向异性滤波示意:
三线性滤波采样区域: 各向异性滤波采样区域:
┌───┐ ┌───────────────┐
│ │ │ │
└───┘ └───────────────┘
正方形采样 沿倾斜方向拉长采样
倾斜表面模糊 倾斜表面清晰2.3 高级纹理技术
2.3.1 凹凸映射(Bump Mapping)
凹凸映射通过扰动表面法线来模拟微观几何细节,而不改变实际的几何形状。原始的凹凸映射使用高度图,通过计算高度图的梯度来扰动法线。
// 简单凹凸映射
float height = texture(heightMap, texCoord).r;
float scale = 0.05;
// 通过有限差分计算高度梯度
float hL = texture(heightMap, texCoord - vec2(texOffset, 0)).r;
float hR = texture(heightMap, texCoord + vec2(texOffset, 0)).r;
float hD = texture(heightMap, texCoord - vec2(0, texOffset)).r;
float hU = texture(heightMap, texCoord + vec2(0, texOffset)).r;
// 梯度 → 法线扰动
vec3 bumpNormal = normalize(vec3(
(hL - hR) * scale,
(hD - hU) * scale,
1.0
));2.3.2 法线贴图(Normal Mapping)
法线贴图是凹凸映射的改进版本。它直接在纹理中存储法线方向,而不是高度值。每个像素的 RGB 值编码了该点的法线方向 (Nx, Ny, Nz),范围从 [0,1] 映射到 [-1,1]。
法线贴图的紫色外观:
默认法线方向 (0, 0, 1) 映射为 RGB (0.5, 0.5, 1.0)
→ 看起来是蓝紫色
平坦区域的法线贴图就是蓝紫色的// 法线贴图采样和变换
vec3 sampleNormalMap(sampler2D normalMap, vec2 texCoord,
vec3 normal, vec3 tangent, vec3 bitangent) {
// 从纹理采样法线([0,1] → [-1,1])
vec3 tangentNormal = texture(normalMap, texCoord).rgb * 2.0 - 1.0;
// 构建 TBN 矩阵(从切线空间到世界空间)
mat3 TBN = mat3(
normalize(tangent),
normalize(bitangent),
normalize(normal)
);
// 将法线从切线空间变换到世界空间
return normalize(TBN * tangentNormal);
}
// 使用偏导数自动计算切线和副切线
vec3 getNormalFromMap(sampler2D normalMap, vec3 worldPos,
vec3 normal, vec2 texCoord) {
vec3 tangentNormal = texture(normalMap, texCoord).xyz * 2.0 - 1.0;
vec3 Q1 = dFdx(worldPos);
vec3 Q2 = dFdy(worldPos);
vec2 st1 = dFdx(texCoord);
vec2 st2 = dFdy(texCoord);
vec3 N = normalize(normal);
vec3 T = normalize(Q1 * st2.t - Q2 * st1.t);
vec3 B = normalize(cross(N, T));
return normalize(T * tangentNormal.x +
B * tangentNormal.y +
N * tangentNormal.z);
}2.3.3 视差映射(Parallax Mapping)
视差映射在法线贴图的基础上,根据观察角度和高度信息偏移纹理坐标,产生真正的视差效果——从不同角度看,表面细节的位置会变化。
// 视差映射
vec2 parallaxMapping(vec2 texCoord, vec3 viewDir) {
float height = texture(heightMap, texCoord).r;
vec2 p = viewDir.xy / viewDir.z * (height * heightScale);
return texCoord - p;
}
// 使用视差偏移后的纹理坐标进行法线贴图和颜色采样
vec2 newTexCoord = parallaxMapping(vTexCoord, viewDir);
vec3 normal = sampleNormalMap(normalMap, newTexCoord);
vec3 color = texture(diffuseMap, newTexCoord).rgb;2.3.4 视差遮挡映射(Parallax Occlusion Mapping)
视差遮挡映射是视差映射的高级版本,通过沿视线方向步进搜索正确的深度交叉点,实现更精确的视差效果,甚至可以模拟遮挡(高处的细节遮挡低处的细节)。
// 视差遮挡映射
vec2 parallaxOcclusionMapping(vec2 texCoord, vec3 viewDir) {
const int minLayers = 10;
const int maxLayers = 50;
float numLayers = mix(maxLayers, minLayers,
abs(dot(vec3(0, 0, 1), viewDir)));
float layerDepth = 1.0 / numLayers;
float currentLayerDepth = 0.0;
vec2 p = viewDir.xy / viewDir.z * heightScale;
vec2 deltaTexCoords = p / numLayers;
vec2 currentTexCoords = texCoord;
float currentDepthMapValue = texture(heightMap, currentTexCoords).r;
while (currentLayerDepth < currentDepthMapValue) {
currentTexCoords -= deltaTexCoords;
currentDepthMapValue = texture(heightMap, currentTexCoords).r;
currentLayerDepth += layerDepth;
}
// 线性插值精确化
vec2 prevTexCoords = currentTexCoords + deltaTexCoords;
float afterDepth = currentDepthMapValue - currentLayerDepth;
float beforeDepth = texture(heightMap, prevTexCoords).r -
currentLayerDepth + layerDepth;
float weight = afterDepth / (afterDepth - beforeDepth);
return mix(currentTexCoords, prevTexCoords, weight);
}2.4 PBR 纹理工作流
2.4.1 纹理贴图类型
现代 PBR 渲染使用多种纹理贴图来描述材质属性:
| 贴图类型 | 通道 | 含义 | 典型值范围 |
|---|---|---|---|
| Albedo(基础颜色) | RGB | 表面颜色 | sRGB 空间 |
| Metallic(金属度) | R | 金属/非金属 | 0 或 1 |
| Roughness(粗糙度) | G | 表面粗糙程度 | 0-1 |
| Normal(法线) | RGB | 表面法线方向 | [-1,1] |
| AO(环境光遮蔽) | R | 自遮蔽因子 | 0-1 |
| Height(高度) | R | 位移量 | 0-1 |
// PBR 材质结构
struct PBRMaterial {
GLuint albedoMap;
GLuint normalMap;
GLuint metallicMap;
GLuint roughnessMap;
GLuint aoMap;
GLuint heightMap;
void bind(GLuint shader) const {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, albedoMap);
glUniform1i(glGetUniformLocation(shader, "albedoMap"), 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, normalMap);
glUniform1i(glGetUniformLocation(shader, "normalMap"), 1);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, metallicMap);
glUniform1i(glGetUniformLocation(shader, "metallicMap"), 2);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, roughnessMap);
glUniform1i(glGetUniformLocation(shader, "roughnessMap"), 3);
glActiveTexture(GL_TEXTURE4);
glBindTexture(GL_TEXTURE_2D, aoMap);
glUniform1i(glGetUniformLocation(shader, "aoMap"), 4);
glActiveTexture(GL_TEXTURE5);
glBindTexture(GL_TEXTURE_2D, heightMap);
glUniform1i(glGetUniformLocation(shader, "heightMap"), 5);
}
};2.4.2 通道打包
为了节省显存和带宽,通常将多个灰度贴图打包到一张纹理的不同通道中:
常见的通道打包方式:
ORM 贴图:
R 通道 → AO(环境光遮蔽)
G 通道 → Roughness(粗糙度)
B 通道 → Metallic(金属度)
好处:3 张贴图合并为 1 张,节省 2/3 的纹理采样和显存// ORM 贴图采样
vec3 orm = texture(ormMap, texCoord).rgb;
float ao = orm.r;
float roughness = orm.g;
float metallic = orm.b;2.5 纹理压缩
2.5.1 为什么需要纹理压缩
纹理占用大量显存。一张 4K(4096×4096)的 RGBA 纹理占用 64MB 显存。一个现代游戏场景可能使用数百张纹理,总显存可达数 GB。纹理压缩可以显著减少显存占用和带宽需求。
2.5.2 常见压缩格式
| 格式 | 平台 | 压缩比 | 特点 |
|---|---|---|---|
| BC1/DXT1 | PC | 6:1 | RGB,无 Alpha |
| BC3/DXT5 | PC | 4:1 | RGBA |
| BC5 | PC | 3:1 | 双通道(法线贴图) |
| BC7 | PC | 3:1 | 高质量 RGB/RGBA |
| ASTC | 移动 | 可变 | 高质量,灵活块大小 |
| ETC2 | 移动 | 可变 | OpenGL ES 3.0 标准 |
// 使用压缩纹理
// BC7 压缩纹理加载
glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_BPTC_UNORM,
width, height, 0, dataSize, compressedData);
// ASTC 压缩纹理
glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_ASTC_8x8,
width, height, 0, dataSize, compressedData);三、实战案例
3.1 完整纹理映射场景
// 纹理化的 3D 场景
class TexturedScene {
public:
void init() {
// 加载 PBR 材质
woodMaterial.albedoMap = loadTexture("textures/wood_albedo.png");
woodMaterial.normalMap = loadTexture("textures/wood_normal.png");
woodMaterial.ormMap = loadTexture("textures/wood_orm.png");
metalMaterial.albedoMap = loadTexture("textures/metal_albedo.png");
metalMaterial.normalMap = loadTexture("textures/metal_normal.png");
metalMaterial.ormMap = loadTexture("textures/metal_orm.png");
// 创建立方体网格
setupCubeMesh();
}
void render(const glm::mat4& view, const glm::mat4& projection) {
glUseProgram(pbrShader);
// 设置光源
glUniform3fv(glGetUniformLocation(pbrShader, "lights[0].position"),
1, glm::value_ptr(glm::vec3(10.0f, 10.0f, 10.0f)));
glUniform3fv(glGetUniformLocation(pbrShader, "lights[0].color"),
1, glm::value_ptr(glm::vec3(300.0f)));
glUniform3fv(glGetUniformLocation(pbrShader, "camPos"),
1, glm::value_ptr(cameraPos));
// 渲染木纹立方体
glm::mat4 model1 = glm::translate(glm::mat4(1.0f),
glm::vec3(-2.0f, 0.0f, 0.0f));
glUniformMatrix4fv(glGetUniformLocation(pbrShader, "model"),
1, GL_FALSE, glm::value_ptr(model1));
woodMaterial.bind(pbrShader);
drawCube();
// 渲染金属立方体
glm::mat4 model2 = glm::translate(glm::mat4(1.0f),
glm::vec3(2.0f, 0.0f, 0.0f));
glUniformMatrix4fv(glGetUniformLocation(pbrShader, "model"),
1, GL_FALSE, glm::value_ptr(model2));
metalMaterial.bind(pbrShader);
drawCube();
}
private:
PBRMaterial woodMaterial;
PBRMaterial metalMaterial;
GLuint cubeVAO;
GLuint pbrShader;
glm::vec3 cameraPos = glm::vec3(0.0f, 2.0f, 6.0f);
};3.2 纹理滤波效果对比
// 纹理滤波对比工具
class TextureFilterComparison {
public:
void render() {
// 左半屏:最近邻滤波
glViewport(0, 0, screenWidth / 2, screenHeight);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
renderTexturedQuad();
// 右半屏:三线性滤波
glViewport(screenWidth / 2, 0, screenWidth / 2, screenHeight);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
renderTexturedQuad();
// 可选:各向异性滤波
// glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, 16.0f);
}
};四、本章小结
本章全面讲解了纹理映射技术从基础到高级的完整知识:
纹理坐标定义了三维表面与二维纹理图像之间的对应关系,UV 展开是将三维表面映射到二维平面的过程。
纹理滤波解决采样问题。双线性滤波处理放大,Mipmap + 三线性滤波处理缩小,各向异性滤波处理倾斜表面。
Mipmap 通过预计算多级缩略纹理,有效解决了远处物体的走样问题,同时提高了缓存命中率。
法线贴图通过在片元级别扰动法线来模拟表面细节,是性价比最高的细节增强技术。
视差映射通过偏移纹理坐标产生真正的视差效果,视差遮挡映射进一步支持了自遮挡效果。
PBR 纹理工作流使用多张贴图描述材质的物理属性,通道打包技术有效节省了显存和带宽。
纹理压缩在保持可接受质量的前提下大幅减少显存占用,是移动平台和大场景渲染的必备技术。
五、思考与练习
思考题
为什么法线贴图中平坦区域的颜色是蓝紫色(约 RGB 128, 128, 255)?如果一个区域的法线完全指向右侧,它的颜色是什么?
Mipmap 的总显存开销是原始纹理的多少倍?请推导这个结果。
各向异性滤波在什么场景下效果最明显?为什么它的性能开销比三线性滤波大?
视差遮挡映射中的"步进数"对效果有什么影响?步进数太少会出现什么问题?如何自适应选择步进数?
为什么金属材质的 albedo 贴图通常是彩色的,而非金属的 albedo 贴图通常是灰度或低饱和度的?
编程练习
纹理滤波器对比:实现一个程序,同时显示最近邻、双线性、三线性各向异性四种滤波模式的效果,支持通过按键切换。
法线贴图:加载一个低多边形模型和配套的法线贴图,实现法线贴图光照。对比有无法线贴图时的视觉效果。
视差映射:实现视差遮挡映射,在一个简单的四边形平面上模拟砖墙效果。调整高度缩放参数观察效果变化。