Skip to content

第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_MAP6 个面,用于环境贴图
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 练习题

基础练习

  1. 加载一张图片作为纹理,贴到矩形上

  2. 尝试不同的环绕模式和过滤模式,观察效果

  3. 在一个物体上使用多个纹理(漫反射 + 法线)

进阶练习

  1. 实现视频纹理(每帧更新)

  2. 创建程序化纹理(棋盘格、噪声等)

挑战练习

  1. 实现天空盒和环境反射效果

下一章预告:在第6章中,我们将学习矩阵变换和相机系统,实现完整的 3D 场景。


文档版本:v1.0
字数统计:约 12,000 字
代码示例:35+ 个

如有转载或 CV 的请标注本站原文地址