第5章:纹理与采样
5.1 章节概述
单纯的颜色和几何形状能表达的内容非常有限。想要让 3D 模型看起来真实——木纹、砖墙、皮肤细节——都需要使用纹理(Texture)。纹理本质上是"贴"在几何表面上的图像。
本章将深入讲解:
- 纹理基础:创建、绑定、上传图像
- 纹理参数:环绑模式、过滤模式
- Mipmap:多级渐远纹理
- 纹理单元:使用多个纹理
- 特殊纹理类型:立方体贴图、浮点纹理、数据纹理
5.2 纹理基础概念
5.2.1 什么是纹理?
纹理(Texture) 是存储在 GPU 显存中的图像数据。在着色器中,我们通过纹理坐标来采样纹理的颜色。
纹理映射过程
3D 模型顶点带有纹理坐标 (UV)
│
▼
┌───────────────────────────┐
│ 3D 三角形 │
│ ╱╲ │
│ ╱ ╲ │
│ ╱ UV ╲ │
│ ╱ ╲ │
│ ────────── │
│ │
└───────────────────────────┘
│
│ 使用 UV 坐标查找纹理
▼
┌───────────────────────────┐
│ 纹理图像 │
│ │
│ (0,1)────────(1,1) │
│ │ 🖼️ │ │
│ │ 图像数据 │ │
│ │ │ │
│ (0,0)────────(1,0) │
│ │
└───────────────────────────┘
│
│ 返回采样的颜色
▼
片段颜色5.2.2 纹理坐标系(UV)
纹理坐标系(也叫 UV 坐标或 ST 坐标)
(0, 1)──────────────(1, 1)
│ │
│ 纹理图像 │
│ V │
│ ↑ │
│ │ │
│ └──→ U │
│ │
(0, 0)──────────────(1, 0)
特点:
- 范围:[0, 1]
- 原点在左下角
- U 向右,V 向上
- 与图像存储格式可能相反(很多图像格式 Y 轴向下)
纹理坐标可以超出 [0, 1]:
- UV < 0 或 UV > 1 时的行为由环绑模式决定5.2.3 纹理类型
| 纹理类型 | WebGL 常量 | 说明 |
|---|---|---|
| 2D 纹理 | TEXTURE_2D | 最常用,普通图像 |
| 立方体纹理 | TEXTURE_CUBE_MAP | 6 个面,用于环境贴图 |
| 3D 纹理 | TEXTURE_3D | 体积纹理(WebGL 2.0) |
| 2D 纹理数组 | TEXTURE_2D_ARRAY | 多层 2D 纹理(WebGL 2.0) |
5.3 创建和使用纹理
5.3.1 纹理创建流程
纹理创建完整流程
1. 创建纹理对象
gl.createTexture()
│
▼
2. 绑定纹理
gl.bindTexture(gl.TEXTURE_2D, texture)
│
▼
3. 设置纹理参数
gl.texParameteri(...)
│
▼
4. 上传图像数据
gl.texImage2D(...)
│
▼
5. 生成 Mipmap(可选)
gl.generateMipmap(...)
│
▼
6. 在渲染时绑定到纹理单元
gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.uniform1i(samplerLocation, 0)5.3.2 加载图像纹理
javascript
/**
* 加载图像并创建纹理
*/
function loadTexture(gl, url) {
// 创建纹理对象
const texture = gl.createTexture();
// 先使用 1x1 像素的占位纹理
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
gl.TEXTURE_2D,
0, // mipmap 级别
gl.RGBA, // 内部格式
1, 1, // 宽高
0, // 边框(必须为 0)
gl.RGBA, // 源数据格式
gl.UNSIGNED_BYTE, // 数据类型
new Uint8Array([255, 0, 255, 255]) // 粉色占位
);
// 异步加载图像
const image = new Image();
image.crossOrigin = 'anonymous'; // 处理跨域
image.onload = () => {
gl.bindTexture(gl.TEXTURE_2D, texture);
// 上传图像数据
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
image
);
// 检查是否是 2 的幂
if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
// 生成 Mipmap
gl.generateMipmap(gl.TEXTURE_2D);
// 设置过滤模式
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
} else {
// 非 2 的幂纹理,WebGL 1.0 有限制
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
}
console.log(`纹理加载完成: ${image.width}x${image.height}`);
};
image.onerror = () => {
console.error('纹理加载失败:', url);
};
image.src = url;
return texture;
}
function isPowerOf2(value) {
return (value & (value - 1)) === 0;
}5.3.3 在着色器中使用纹理
glsl
// 顶点着色器
attribute vec3 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 1.0);
v_texCoord = a_texCoord;
}
// 片段着色器
precision mediump float;
varying vec2 v_texCoord;
uniform sampler2D u_texture;
void main() {
// texture2D 函数采样纹理
vec4 texColor = texture2D(u_texture, v_texCoord);
gl_FragColor = texColor;
}javascript
// JavaScript 端设置纹理
// 激活纹理单元 0
gl.activeTexture(gl.TEXTURE0);
// 绑定纹理
gl.bindTexture(gl.TEXTURE_2D, texture);
// 告诉着色器使用纹理单元 0
const samplerLocation = gl.getUniformLocation(program, 'u_texture');
gl.uniform1i(samplerLocation, 0);5.4 纹理参数详解
5.4.1 环绕模式(Wrap Mode)
环绕模式决定当纹理坐标超出 [0, 1] 范围时的行为:
环绕模式示意
原始纹理(UV 范围 [0,1]):
┌─────────┐
│ A │
│ ↓ │
│ B → ● ← │
│ ↑ │
│ C │
└─────────┘
gl.REPEAT(重复):
UV 超出范围时,纹理会重复
┌─────────┬─────────┬─────────┐
│ A │ A │ A │
│ ↓ │ ↓ │ ↓ │
│ B → ● ← │ B → ● ← │ B → ● ← │
│ ↑ │ ↑ │ ↑ │
│ C │ C │ C │
├─────────┼─────────┼─────────┤
│ A │ A │ A │
│ ... │ │ ... │
└─────────┴─────────┴─────────┘
gl.MIRRORED_REPEAT(镜像重复):
UV 超出范围时,纹理镜像重复
┌─────────┬─────────┬─────────┐
│ C │ A │ C │
│ ↑ │ ↓ │ ↑ │
│ ← ● → B │ B → ● ← │ ← ● → B │
│ ↓ │ ↑ │ ↓ │
│ A │ C │ A │
└─────────┴─────────┴─────────┘
gl.CLAMP_TO_EDGE(边缘钳制):
UV 超出范围时,使用边缘颜色
┌─────────┬─────────┬─────────┐
│ A A A A │ A A A A │ A A A A │
│ B B B B │ B → ● ← │ □ □ □ □ │
│ B B B B │ B → ● ← │ □ □ □ □ │
│ C C C C │ C C C C │ C C C C │
└─────────┴─────────┴─────────┘
左边用 B 填充,右边用 □(右边缘颜色)填充javascript
// 设置环绕模式
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); // S 方向(U)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); // T 方向(V)
// WebGL 2.0 还有 R 方向(用于 3D 纹理)
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.REPEAT);
// 常用组合
// 无缝平铺纹理(地板、墙壁)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
// 边缘不重复(UI 元素)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);5.4.2 过滤模式(Filter Mode)
过滤模式决定纹理在放大/缩小时的采样方式:
纹理过滤示意
放大(TEXTURE_MAG_FILTER):
当一个纹理像素(texel)覆盖多个屏幕像素时
原始纹理(4x4) 放大后(8x8)
┌─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┐
│R│G│R│G│ │?│?│?│?│?│?│?│?│
├─┼─┼─┼─┤ 放大 → ├─┼─┼─┼─┼─┼─┼─┼─┤
│B│Y│B│Y│ │?│?│?│?│?│?│?│?│
├─┼─┼─┼─┤ │ ... │
│R│G│R│G│
├─┼─┼─┼─┤
│B│Y│B│Y│
└─┴─┴─┴─┘
gl.NEAREST(最近邻): gl.LINEAR(线性):
选择最近的texel 混合周围texels
┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┐
│R│R│G│G│R│R│G│G│ │R│~│G│~│~│R│~│G│
├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤
│R│R│G│G│R│R│G│G│ │~│~│~│~│~│~│~│~│
│ ... │ │ ... │
块状、锐利边缘 平滑、模糊
缩小(TEXTURE_MIN_FILTER):
当多个texel覆盖一个屏幕像素时
原始纹理(8x8) 缩小后(2x2)
┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌───┬───┐
│R│G│R│G│B│Y│B│Y│ 缩小→ │ ? │ ? │
├─┼─┼─┼─┼─┼─┼─┼─┤ ├───┼───┤
│B│Y│B│Y│R│G│R│G│ │ ? │ ? │
│ ... │ └───┴───┘
需要决定如何从多个texel中选择颜色javascript
// 放大过滤(通常只有两个选项)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); // 最近邻
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); // 线性
// 缩小过滤(有更多选项,涉及 Mipmap)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
// Mipmap 过滤模式命名规则:
// [texel过滤]_MIPMAP_[mipmap级别过滤]
//
// NEAREST_MIPMAP_NEAREST:
// 在最近的mipmap级别中用最近邻采样
//
// LINEAR_MIPMAP_LINEAR:
// 在两个最近的mipmap级别中分别用线性采样,然后混合
// 这是最高质量的过滤,也叫"三线性过滤"5.5 Mipmap 详解
5.5.1 什么是 Mipmap?
Mipmap 是预先计算好的一系列逐级缩小的纹理版本,用于提高渲染质量和性能。
Mipmap 金字塔
级别 0(原始尺寸):
┌───────────────────────────┐
│ │
│ 256×256 │
│ │
│ │
└───────────────────────────┘
级别 1:
┌─────────────┐
│ 128×128 │
└─────────────┘
级别 2:
┌──────┐
│64×64 │
└──────┘
级别 3:
┌──┐
│32│
└──┘
级别 4-8: 16, 8, 4, 2, 1
总内存 = 256² + 128² + 64² + ... ≈ 原始的 4/3(约增加 33%)5.5.2 为什么需要 Mipmap?
没有 Mipmap 的问题
远处的纹理(纹理像素 >> 屏幕像素):
原始纹理 远处显示 问题
┌───────┐ 闪烁/摩尔纹
│ ░▒▓█▒ │ → ░▒█ 因为采样不足
│ ▓██░▒ │ ▓░ 无法正确表示
│ ░▒▓▒░ │ 细节
└───────┘
使用 Mipmap:
选择合适的mipmap级别 → ▒ 平滑、稳定
预先平均了细节 无闪烁5.5.3 使用 Mipmap
javascript
// 生成 Mipmap
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D); // 自动生成所有级别
// 使用 Mipmap 过滤
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
// 手动上传各级 Mipmap(用于自定义 mipmap)
function uploadCustomMipmaps(gl, images) {
for (let level = 0; level < images.length; level++) {
gl.texImage2D(
gl.TEXTURE_2D,
level, // mipmap 级别
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
images[level]
);
}
}
// WebGL 2.0: 设置 mipmap 级别范围
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_BASE_LEVEL, 0); // 最精细级别
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, 4); // 最粗糙级别5.6 纹理单元
5.6.1 什么是纹理单元?
GPU 有多个纹理单元(Texture Unit),每个单元可以绑定一个纹理。这允许着色器同时访问多个纹理。
纹理单元示意
┌─────────────────────────────────────────────────────┐
│ GPU │
│ │
│ 纹理单元 0 纹理单元 1 纹理单元 2 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 漫反射 │ │ 法线 │ │ 高光 │ │
│ │ 贴图 │ │ 贴图 │ │ 贴图 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ └───────────────┼────────────────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 片段着色器 │ │
│ │ │ │
│ │ sampler2D │ │
│ │ u_diffuse │ → 从单元 0 采样 │
│ │ u_normal │ → 从单元 1 采样 │
│ │ u_specular │ → 从单元 2 采样 │
│ │ │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────┘5.6.2 使用多个纹理
javascript
/**
* 设置多个纹理
*/
function setupMultipleTextures(gl, program, textures) {
// 获取 sampler uniform 位置
const diffuseLoc = gl.getUniformLocation(program, 'u_diffuseMap');
const normalLoc = gl.getUniformLocation(program, 'u_normalMap');
const specularLoc = gl.getUniformLocation(program, 'u_specularMap');
// 绑定纹理到不同的纹理单元
// 纹理单元 0: 漫反射贴图
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textures.diffuse);
gl.uniform1i(diffuseLoc, 0); // 告诉 sampler 使用单元 0
// 纹理单元 1: 法线贴图
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, textures.normal);
gl.uniform1i(normalLoc, 1); // 告诉 sampler 使用单元 1
// 纹理单元 2: 高光贴图
gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, textures.specular);
gl.uniform1i(specularLoc, 2); // 告诉 sampler 使用单元 2
}
// 可用的纹理单元数量
const maxTextureUnits = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
console.log('最大纹理单元数:', maxTextureUnits); // 通常至少 8 个,现代 GPU 32+glsl
// 片段着色器:使用多个纹理
precision mediump float;
varying vec2 v_texCoord;
varying vec3 v_normal;
varying vec3 v_viewDir;
varying vec3 v_lightDir;
uniform sampler2D u_diffuseMap;
uniform sampler2D u_normalMap;
uniform sampler2D u_specularMap;
void main() {
// 采样各个纹理
vec4 diffuseColor = texture2D(u_diffuseMap, v_texCoord);
vec3 normalMap = texture2D(u_normalMap, v_texCoord).rgb * 2.0 - 1.0;
float specularIntensity = texture2D(u_specularMap, v_texCoord).r;
// 使用采样的数据进行光照计算
vec3 N = normalize(v_normal + normalMap);
vec3 L = normalize(v_lightDir);
vec3 V = normalize(v_viewDir);
vec3 R = reflect(-L, N);
float diff = max(dot(N, L), 0.0);
float spec = pow(max(dot(R, V), 0.0), 32.0) * specularIntensity;
vec3 finalColor = diffuseColor.rgb * diff + vec3(spec);
gl_FragColor = vec4(finalColor, diffuseColor.a);
}5.7 纹理格式
5.7.1 常用纹理格式
javascript
// ============ 内部格式(GPU 存储格式)============
// 基本格式
gl.RGBA // 红绿蓝透明,各 8 位
gl.RGB // 红绿蓝,各 8 位
gl.LUMINANCE // 灰度(单通道)
gl.ALPHA // 只有透明通道
// WebGL 2.0 扩展格式
gl.RGBA8 // 8 位 RGBA
gl.RGBA16F // 16 位浮点 RGBA
gl.RGBA32F // 32 位浮点 RGBA
gl.R8 // 8 位单通道
gl.RG8 // 8 位双通道
gl.R16F // 16 位浮点单通道
gl.DEPTH_COMPONENT24 // 24 位深度
// ============ 数据类型 ============
gl.UNSIGNED_BYTE // 0-255
gl.UNSIGNED_SHORT_5_6_5 // 16 位 RGB (5-6-5)
gl.UNSIGNED_SHORT_4_4_4_4 // 16 位 RGBA (4-4-4-4)
gl.UNSIGNED_SHORT_5_5_5_1 // 16 位 RGBA (5-5-5-1)
gl.FLOAT // 32 位浮点(需要扩展或 WebGL 2.0)
gl.HALF_FLOAT // 16 位浮点(需要扩展或 WebGL 2.0)5.7.2 从不同源创建纹理
javascript
// ============ 从 HTML 元素创建 ============
// 从 <img> 元素
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imageElement);
// 从 <canvas> 元素
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvasElement);
// 从 <video> 元素(每帧更新)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, videoElement);
// ============ 从 TypedArray 创建 ============
// 从 Uint8Array(原始像素数据)
const width = 256;
const height = 256;
const pixels = new Uint8Array(width * height * 4); // RGBA
// 填充数据...
for (let i = 0; i < width * height; i++) {
pixels[i * 4 + 0] = (i % 256); // R
pixels[i * 4 + 1] = (i / 256) | 0; // G
pixels[i * 4 + 2] = 128; // B
pixels[i * 4 + 3] = 255; // A
}
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
width, height,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
pixels
);
// ============ 从浮点数据创建(WebGL 2.0)============
const floatData = new Float32Array(width * height * 4);
// 填充浮点数据...
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA32F, // 内部格式
width, height,
0,
gl.RGBA, // 源格式
gl.FLOAT, // 数据类型
floatData
);5.8 立方体贴图(Cubemap)
5.8.1 什么是立方体贴图?
立方体贴图是由 6 个面组成的纹理,用于环境映射、天空盒等:
立方体贴图结构
┌───────┐
│ +Y │ (上)
│ TOP │
┌───────┼───────┼───────┬───────┐
│ -X │ +Z │ +X │ -Z │
│ LEFT │ FRONT │ RIGHT │ BACK │
└───────┼───────┼───────┴───────┘
│ -Y │
│BOTTOM │
└───────┘
6 个面对应的常量:
gl.TEXTURE_CUBE_MAP_POSITIVE_X (+X, 右)
gl.TEXTURE_CUBE_MAP_NEGATIVE_X (-X, 左)
gl.TEXTURE_CUBE_MAP_POSITIVE_Y (+Y, 上)
gl.TEXTURE_CUBE_MAP_NEGATIVE_Y (-Y, 下)
gl.TEXTURE_CUBE_MAP_POSITIVE_Z (+Z, 前)
gl.TEXTURE_CUBE_MAP_NEGATIVE_Z (-Z, 后)5.8.2 创建立方体贴图
javascript
/**
* 创建立方体贴图
*/
function createCubemap(gl, faceImages) {
const cubemap = gl.createTexture();
gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap);
const faces = [
{ target: gl.TEXTURE_CUBE_MAP_POSITIVE_X, image: faceImages.right },
{ target: gl.TEXTURE_CUBE_MAP_NEGATIVE_X, image: faceImages.left },
{ target: gl.TEXTURE_CUBE_MAP_POSITIVE_Y, image: faceImages.top },
{ target: gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, image: faceImages.bottom },
{ target: gl.TEXTURE_CUBE_MAP_POSITIVE_Z, image: faceImages.front },
{ target: gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, image: faceImages.back }
];
faces.forEach(face => {
gl.texImage2D(
face.target,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
face.image
);
});
// 设置参数
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
return cubemap;
}
// 加载立方体贴图
async function loadCubemap(gl, basePath) {
const faceNames = ['right', 'left', 'top', 'bottom', 'front', 'back'];
const images = {};
await Promise.all(faceNames.map(async (name) => {
const image = new Image();
image.crossOrigin = 'anonymous';
await new Promise((resolve, reject) => {
image.onload = resolve;
image.onerror = reject;
image.src = `${basePath}/${name}.jpg`;
});
images[name] = image;
}));
return createCubemap(gl, images);
}5.8.3 使用立方体贴图
glsl
// 天空盒顶点着色器
attribute vec3 a_position;
uniform mat4 u_viewProjectionMatrix;
varying vec3 v_texCoord;
void main() {
v_texCoord = a_position; // 位置就是采样方向
vec4 pos = u_viewProjectionMatrix * vec4(a_position, 1.0);
gl_Position = pos.xyww; // z = w,始终在最远处
}
// 天空盒片段着色器
precision mediump float;
varying vec3 v_texCoord;
uniform samplerCube u_skybox;
void main() {
gl_FragColor = textureCube(u_skybox, v_texCoord);
}
// 环境反射着色器
precision mediump float;
varying vec3 v_worldPos;
varying vec3 v_normal;
uniform vec3 u_cameraPos;
uniform samplerCube u_environment;
void main() {
vec3 I = normalize(v_worldPos - u_cameraPos); // 入射方向
vec3 R = reflect(I, normalize(v_normal)); // 反射方向
vec4 envColor = textureCube(u_environment, R);
gl_FragColor = envColor;
}5.9 纹理工具类封装
javascript
/**
* 纹理管理器
*/
class TextureManager {
constructor(gl) {
this.gl = gl;
this.textures = new Map();
this.defaultTexture = this.createDefaultTexture();
}
createDefaultTexture() {
const gl = this.gl;
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// 粉色棋盘格(易于识别纹理问题)
const pixels = new Uint8Array([
255, 0, 255, 255, 0, 0, 0, 255,
0, 0, 0, 255, 255, 0, 255, 255
]);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
return texture;
}
async load(url, options = {}) {
if (this.textures.has(url)) {
return this.textures.get(url);
}
const gl = this.gl;
const texture = gl.createTexture();
// 先使用默认纹理
this.textures.set(url, this.defaultTexture);
try {
const image = await this.loadImage(url);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
// 设置参数
const isPOT = this.isPowerOf2(image.width) && this.isPowerOf2(image.height);
if (isPOT) {
gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER,
options.minFilter || gl.LINEAR_MIPMAP_LINEAR);
} else {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER,
options.minFilter || gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
}
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER,
options.magFilter || gl.LINEAR);
if (options.wrapS) {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, options.wrapS);
}
if (options.wrapT) {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, options.wrapT);
}
this.textures.set(url, texture);
return texture;
} catch (error) {
console.error(`加载纹理失败: ${url}`, error);
return this.defaultTexture;
}
}
loadImage(url) {
return new Promise((resolve, reject) => {
const image = new Image();
image.crossOrigin = 'anonymous';
image.onload = () => resolve(image);
image.onerror = reject;
image.src = url;
});
}
isPowerOf2(value) {
return (value & (value - 1)) === 0;
}
bind(url, unit = 0) {
const gl = this.gl;
const texture = this.textures.get(url) || this.defaultTexture;
gl.activeTexture(gl.TEXTURE0 + unit);
gl.bindTexture(gl.TEXTURE_2D, texture);
return unit;
}
dispose(url) {
if (this.textures.has(url)) {
this.gl.deleteTexture(this.textures.get(url));
this.textures.delete(url);
}
}
disposeAll() {
for (const texture of this.textures.values()) {
this.gl.deleteTexture(texture);
}
this.textures.clear();
}
}
// 使用
const textureManager = new TextureManager(gl);
// 加载纹理
await textureManager.load('/textures/diffuse.jpg');
await textureManager.load('/textures/normal.jpg');
// 渲染时绑定
const diffuseUnit = textureManager.bind('/textures/diffuse.jpg', 0);
const normalUnit = textureManager.bind('/textures/normal.jpg', 1);
gl.uniform1i(diffuseLocation, diffuseUnit);
gl.uniform1i(normalLocation, normalUnit);5.10 本章小结
核心概念
| 概念 | 说明 |
|---|---|
| 纹理 | GPU 显存中的图像数据 |
| 纹理坐标(UV) | 指定采样位置,范围通常 [0,1] |
| 环绕模式 | UV 超出范围时的行为 |
| 过滤模式 | 纹理放大/缩小时的采样方式 |
| Mipmap | 预计算的多级缩小纹理 |
| 纹理单元 | 允许同时使用多个纹理 |
| 立方体贴图 | 6 面纹理,用于环境映射 |
关键 API
| API | 作用 |
|---|---|
createTexture() | 创建纹理对象 |
bindTexture() | 绑定纹理 |
texImage2D() | 上传图像数据 |
texParameteri() | 设置纹理参数 |
generateMipmap() | 生成 Mipmap |
activeTexture() | 激活纹理单元 |
texture2D() | 着色器中采样纹理 |
5.11 练习题
基础练习
加载一张图片作为纹理,贴到矩形上
尝试不同的环绕模式和过滤模式,观察效果
在一个物体上使用多个纹理(漫反射 + 法线)
进阶练习
实现视频纹理(每帧更新)
创建程序化纹理(棋盘格、噪声等)
挑战练习
- 实现天空盒和环境反射效果
下一章预告:在第6章中,我们将学习矩阵变换和相机系统,实现完整的 3D 场景。
文档版本:v1.0
字数统计:约 12,000 字
代码示例:35+ 个
