Skip to content

第10章:纹理映射

10.1 纹理基础

10.1.1 纹理的概念

纹理映射(Texture Mapping)是将 2D 图像"贴"到 3D 模型表面的技术,用于增加细节而不增加几何复杂度。

纹理映射的基本原理

3D 模型上的每个顶点都有一个纹理坐标 (u, v):

纹理图像(2D)                    3D 模型表面
┌─────────────────┐             ╱╲
│(0,1)       (1,1)│            ╱  ╲
│                 │           ╱ 纹 ╲
│    砖墙纹理     │  ──────► ╱  理  ╲
│                 │          ╲  贴  ╱
│                 │           ╲ 图 ╱
│(0,0)       (1,0)│            ╲  ╱
└─────────────────┘             ╲╱


纹理坐标系统(UV 坐标):

    v


  1 ┼───────────●
    │           │
    │   纹理    │
    │           │
  0 ●───────────┼───► u
    0           1

- u 对应水平方向(0 到 1)
- v 对应垂直方向(0 到 1)
- 可以超出 [0,1] 范围(平铺/钳制)

10.1.2 纹理坐标

顶点与纹理坐标

每个顶点除了位置,还存储纹理坐标:

struct Vertex {
    vec3 position;    // 3D 位置
    vec3 normal;      // 法向量
    vec2 uv;          // 纹理坐标
}


示例:一个正方形的纹理映射

    V3──────────V2
    │(0,1)  (1,1)│
    │            │
    │   纹理     │
    │            │
    │(0,0)  (1,0)│
    V0──────────V1

顶点数据:
V0: position=(-1,-1,0), uv=(0,0)
V1: position=(1,-1,0),  uv=(1,0)
V2: position=(1,1,0),   uv=(1,1)
V3: position=(-1,1,0),  uv=(0,1)

10.1.3 纹理采样

javascript
/**
 * 纹理类
 */
class Texture {
    constructor(image) {
        this.width = image.width;
        this.height = image.height;
        
        // 获取像素数据
        const canvas = document.createElement('canvas');
        canvas.width = image.width;
        canvas.height = image.height;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(image, 0, 0);
        this.data = ctx.getImageData(0, 0, image.width, image.height).data;
        
        // 采样模式
        this.wrapS = 'repeat';   // u 方向
        this.wrapT = 'repeat';   // v 方向
        this.filter = 'linear'; // 'nearest' 或 'linear'
    }
    
    /**
     * 采样纹理
     * @param u 纹理坐标 u
     * @param v 纹理坐标 v
     * @returns {r, g, b, a} 颜色值 [0,1]
     */
    sample(u, v) {
        // 应用寻址模式
        u = this.applyWrap(u, this.wrapS);
        v = this.applyWrap(v, this.wrapT);
        
        if (this.filter === 'nearest') {
            return this.sampleNearest(u, v);
        } else {
            return this.sampleBilinear(u, v);
        }
    }
    
    /**
     * 最近邻采样
     */
    sampleNearest(u, v) {
        const x = Math.floor(u * this.width);
        const y = Math.floor((1 - v) * this.height); // y 翻转
        
        const clampedX = Math.max(0, Math.min(this.width - 1, x));
        const clampedY = Math.max(0, Math.min(this.height - 1, y));
        
        const i = (clampedY * this.width + clampedX) * 4;
        
        return {
            r: this.data[i] / 255,
            g: this.data[i + 1] / 255,
            b: this.data[i + 2] / 255,
            a: this.data[i + 3] / 255
        };
    }
    
    /**
     * 双线性插值采样
     */
    sampleBilinear(u, v) {
        const x = u * this.width - 0.5;
        const y = (1 - v) * this.height - 0.5;
        
        const x0 = Math.floor(x);
        const y0 = Math.floor(y);
        const x1 = x0 + 1;
        const y1 = y0 + 1;
        
        const tx = x - x0;
        const ty = y - y0;
        
        // 四个相邻像素
        const c00 = this.getPixel(x0, y0);
        const c10 = this.getPixel(x1, y0);
        const c01 = this.getPixel(x0, y1);
        const c11 = this.getPixel(x1, y1);
        
        // 双线性插值
        return {
            r: this.lerp2D(c00.r, c10.r, c01.r, c11.r, tx, ty),
            g: this.lerp2D(c00.g, c10.g, c01.g, c11.g, tx, ty),
            b: this.lerp2D(c00.b, c10.b, c01.b, c11.b, tx, ty),
            a: this.lerp2D(c00.a, c10.a, c01.a, c11.a, tx, ty)
        };
    }
    
    getPixel(x, y) {
        x = Math.max(0, Math.min(this.width - 1, x));
        y = Math.max(0, Math.min(this.height - 1, y));
        
        const i = (y * this.width + x) * 4;
        return {
            r: this.data[i] / 255,
            g: this.data[i + 1] / 255,
            b: this.data[i + 2] / 255,
            a: this.data[i + 3] / 255
        };
    }
    
    lerp2D(c00, c10, c01, c11, tx, ty) {
        const c0 = c00 * (1 - tx) + c10 * tx;
        const c1 = c01 * (1 - tx) + c11 * tx;
        return c0 * (1 - ty) + c1 * ty;
    }
    
    applyWrap(coord, mode) {
        switch (mode) {
            case 'repeat':
                return coord - Math.floor(coord);
            case 'clamp':
                return Math.max(0, Math.min(1, coord));
            case 'mirror':
                const t = coord - Math.floor(coord);
                return Math.floor(coord) % 2 === 0 ? t : 1 - t;
            default:
                return coord;
        }
    }
}

10.2 纹理过滤

10.2.1 采样问题

纹理采样的问题

当纹理像素(texel)与屏幕像素(pixel)大小不匹配时:

1. 放大(Magnification)
   一个 texel 覆盖多个 pixel
   
   纹理(小):              屏幕(大):
   ┌───┬───┐               ┌─┬─┬─┬─┬─┬─┐
   │ A │ B │               │A│A│A│B│B│B│
   ├───┼───┤    ──────►    ├─┼─┼─┼─┼─┼─┤
   │ C │ D │               │A│A│A│B│B│B│
   └───┴───┘               ├─┼─┼─┼─┼─┼─┤
                           │C│C│C│D│D│D│
   问题:块状、锯齿         └─┴─┴─┴─┴─┴─┘


2. 缩小(Minification)
   多个 texel 映射到一个 pixel
   
   纹理(大):              屏幕(小):
   ┌─┬─┬─┬─┬─┬─┐           ┌───┬───┐
   │a│b│c│d│e│f│           │ ? │ ? │
   ├─┼─┼─┼─┼─┼─┤           ├───┼───┤
   │g│h│i│j│k│l│  ──────►  │ ? │ ? │
   ├─┼─┼─┼─┼─┼─┤           └───┴───┘
   │m│n│o│p│q│r│
   └─┴─┴─┴─┴─┴─┘   问题:闪烁、摩尔纹

10.2.2 最近邻过滤

最近邻过滤(Nearest Neighbor)

选择最近的一个 texel 的颜色。

    纹理坐标 (0.3, 0.7)

    ┌───┬───┬───┐
    │   │   │   │
    ├───┼───┼───┤
    │   │ ● │ ← │ 选择这个 texel
    ├───┼───┼───┤
    │   │   │   │
    └───┴───┴───┘


优点:
- 计算简单
- 保持锐利边缘

缺点:
- 放大时有块状感
- 缩小时有闪烁

WebGL 设置:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);

10.2.3 双线性过滤

双线性过滤(Bilinear Filtering)

对四个相邻 texel 进行插值。

    采样点 P

    ┌───┬───┐
    │c00│c10│
    ├───┼─●─┤  P 在四个 texel 之间
    │c01│c11│
    └───┴───┘

计算:
tx = P.x 的小数部分
ty = P.y 的小数部分

c_top = lerp(c00, c10, tx)
c_bottom = lerp(c01, c11, tx)
result = lerp(c_top, c_bottom, ty)


优点:
- 平滑过渡
- 减少块状感

缺点:
- 缩小时仍有问题
- 轻微模糊

WebGL 设置:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

10.3 Mipmap

10.3.1 Mipmap 原理

Mipmap(多级纹理)

预先生成不同分辨率的纹理,根据距离选择合适的级别。

Level 0 (原始):     Level 1:      Level 2:    Level 3:
┌───────────────┐   ┌───────┐     ┌───┐       ┌─┐
│               │   │       │     │   │       │ │
│  1024×1024    │   │512×512│     │256│       │1│
│               │   │       │     │   │       └─┘
│               │   └───────┘     └───┘
│               │
└───────────────┘


选择级别的依据:
- 屏幕像素对应的纹理区域大小
- 如果一个像素覆盖 4×4 texels,选择 level 2

                    远处(使用小纹理)
          ╱─────────────────────╲
         ╱                       ╲
        ╱                         ╲
       ╱                           ╲
      ╱─────────────────────────────╲
     ╱                               ╲
    ╱                                 ╲
   相机                               近处(使用大纹理)


内存占用:
Mipmap 增加约 1/3 的内存
总内存 = 1 + 1/4 + 1/16 + 1/64 + ... ≈ 4/3

10.3.2 Mipmap 生成

javascript
/**
 * 生成 Mipmap
 */
class MipmapGenerator {
    /**
     * 生成完整的 mipmap 链
     */
    static generate(imageData) {
        const levels = [imageData];
        let width = imageData.width;
        let height = imageData.height;
        let data = imageData.data;
        
        while (width > 1 || height > 1) {
            const newWidth = Math.max(1, Math.floor(width / 2));
            const newHeight = Math.max(1, Math.floor(height / 2));
            const newData = new Uint8ClampedArray(newWidth * newHeight * 4);
            
            // 下采样:2×2 像素取平均
            for (let y = 0; y < newHeight; y++) {
                for (let x = 0; x < newWidth; x++) {
                    const srcX = x * 2;
                    const srcY = y * 2;
                    
                    // 收集 2×2 像素
                    const samples = [];
                    for (let dy = 0; dy < 2 && srcY + dy < height; dy++) {
                        for (let dx = 0; dx < 2 && srcX + dx < width; dx++) {
                            const i = ((srcY + dy) * width + (srcX + dx)) * 4;
                            samples.push({
                                r: data[i],
                                g: data[i + 1],
                                b: data[i + 2],
                                a: data[i + 3]
                            });
                        }
                    }
                    
                    // 计算平均值
                    const avg = {
                        r: 0, g: 0, b: 0, a: 0
                    };
                    for (const s of samples) {
                        avg.r += s.r;
                        avg.g += s.g;
                        avg.b += s.b;
                        avg.a += s.a;
                    }
                    const count = samples.length;
                    
                    const dstI = (y * newWidth + x) * 4;
                    newData[dstI] = avg.r / count;
                    newData[dstI + 1] = avg.g / count;
                    newData[dstI + 2] = avg.b / count;
                    newData[dstI + 3] = avg.a / count;
                }
            }
            
            levels.push({
                width: newWidth,
                height: newHeight,
                data: newData
            });
            
            width = newWidth;
            height = newHeight;
            data = newData;
        }
        
        return levels;
    }
    
    /**
     * 计算应该使用的 mipmap 级别
     * @param dUdx, dUdy, dVdx, dVdy 纹理坐标的屏幕空间导数
     */
    static computeMipLevel(dUdx, dUdy, dVdx, dVdy, texWidth, texHeight) {
        // 计算一个像素在纹理空间中覆盖的面积
        const dudx = dUdx * texWidth;
        const dvdx = dVdx * texHeight;
        const dudy = dUdy * texWidth;
        const dvdy = dVdy * texHeight;
        
        // 取最大伸展
        const maxU = Math.max(Math.abs(dudx), Math.abs(dudy));
        const maxV = Math.max(Math.abs(dvdx), Math.abs(dvdy));
        const rho = Math.max(maxU, maxV);
        
        // 转换为 mipmap 级别
        return Math.log2(rho);
    }
}

// WebGL 中创建 mipmap
// gl.generateMipmap(gl.TEXTURE_2D);

// 设置 mipmap 过滤
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);

10.3.3 三线性过滤

三线性过滤(Trilinear Filtering)

在两个 mipmap 级别之间进行插值。

计算步骤:
1. 计算 mipmap 级别 λ
2. 在 level ⌊λ⌋ 进行双线性采样 → c1
3. 在 level ⌈λ⌉ 进行双线性采样 → c2
4. 在 c1 和 c2 之间插值

       Level n          Level n+1
    ┌───┬───┐          ┌─┬─┐
    │   │   │          │ │ │
    ├───┼─●─┤          ├─●─┤
    │   │   │          └─┴─┘
    └───┴───┘
       c1                c2

result = lerp(c1, c2, frac(λ))


优点:
- 消除 mipmap 级别之间的跳变
- 非常平滑

缺点:
- 8 次纹理采样
- 略有性能开销

10.4 纹理寻址

10.4.1 寻址模式

纹理寻址模式(Wrap Mode)

当纹理坐标超出 [0,1] 范围时的处理方式。

1. Repeat(平铺)
   uv = frac(uv)
   
   ┌───┬───┬───┐
   │ ▲ │ ▲ │ ▲ │
   ├───┼───┼───┤
   │ ▲ │ ▲ │ ▲ │
   └───┴───┴───┘


2. Clamp(钳制)
   uv = clamp(uv, 0, 1)
   
   ┌───┬───────────────┐
   │ ▲ │▲  ▲  ▲  ▲  ▲  │ ← 边缘像素延伸
   ├───┤               │
   │ ▲ │▲  ▲  ▲  ▲  ▲  │
   └───┴───────────────┘


3. Mirror(镜像)
   交替翻转
   
   ┌───┬───┬───┐
   │ ▲ │ ▼ │ ▲ │
   ├───┼───┼───┤
   │ ▼ │ ▲ │ ▼ │
   └───┴───┴───┘


4. Border(边框)
   使用固定颜色
   
   ┌───┬───────────────┐
   │ ▲ │■  ■  ■  ■  ■  │ ← 边框颜色
   ├───┤■  ■  ■  ■  ■  │
   │ ▲ │■  ■  ■  ■  ■  │
   └───┴───────────────┘

10.4.2 WebGL 纹理设置

javascript
/**
 * WebGL 纹理配置
 */
class TextureConfig {
    static createTexture(gl, image, options = {}) {
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        
        // 上传图像数据
        gl.texImage2D(
            gl.TEXTURE_2D,
            0,
            gl.RGBA,
            gl.RGBA,
            gl.UNSIGNED_BYTE,
            image
        );
        
        // 设置过滤模式
        const magFilter = options.magFilter || gl.LINEAR;
        const minFilter = options.minFilter || gl.LINEAR_MIPMAP_LINEAR;
        
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter);
        
        // 设置寻址模式
        const wrapS = options.wrapS || gl.REPEAT;
        const wrapT = options.wrapT || gl.REPEAT;
        
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT);
        
        // 生成 mipmap(如果需要)
        if (this.isPowerOfTwo(image.width) && this.isPowerOfTwo(image.height)) {
            gl.generateMipmap(gl.TEXTURE_2D);
        } else {
            // 非 2 的幂次纹理不支持 mipmap
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, 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);
        }
        
        // 各向异性过滤(扩展)
        const ext = gl.getExtension('EXT_texture_filter_anisotropic');
        if (ext && options.anisotropy) {
            const maxAnisotropy = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
            gl.texParameterf(
                gl.TEXTURE_2D,
                ext.TEXTURE_MAX_ANISOTROPY_EXT,
                Math.min(options.anisotropy, maxAnisotropy)
            );
        }
        
        return texture;
    }
    
    static isPowerOfTwo(value) {
        return (value & (value - 1)) === 0;
    }
}

10.5 凹凸贴图

10.5.1 法线贴图

法线贴图(Normal Mapping)

用纹理存储表面法向量,模拟细节几何。

法线贴图中的颜色表示法向量:
- R = (Nx + 1) / 2
- G = (Ny + 1) / 2
- B = (Nz + 1) / 2

典型的法线贴图呈蓝紫色(因为大部分法线指向 Z 轴)


对比:

实际几何:                 使用法线贴图:
┌───────────────┐         ┌───────────────┐
│╱╲╱╲╱╲╱╲╱╲╱╲╱╲│         │               │
│╱╲╱╲╱╲╱╲╱╲╱╲╱╲│         │   同样效果    │
│╱╲╱╲╱╲╱╲╱╲╱╲╱╲│         │   更少多边形  │
└───────────────┘         └───────────────┘

100,000 面                 2 面 + 法线贴图

10.5.2 切线空间

切线空间(Tangent Space)

法线贴图中的法向量是在切线空间中定义的。

切线空间坐标系:
- T(Tangent):沿纹理 U 方向
- B(Bitangent):沿纹理 V 方向
- N(Normal):表面法向量

         N


    ─────┼─────► T
        ╱│
       ╱ │

      B


TBN 矩阵:
将切线空间法向量转换到世界空间

worldNormal = TBN × tangentNormal

其中 TBN = [T, B, N]

10.5.3 法线贴图实现

javascript
/**
 * 法线贴图着色
 */

// 顶点着色器
const normalMapVertexShader = `
attribute vec3 aPosition;
attribute vec3 aNormal;
attribute vec2 aTexCoord;
attribute vec3 aTangent;

uniform mat4 uModelMatrix;
uniform mat4 uViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat3 uNormalMatrix;

varying vec2 vTexCoord;
varying vec3 vPosition;
varying mat3 vTBN;

void main() {
    vec4 worldPosition = uModelMatrix * vec4(aPosition, 1.0);
    vPosition = worldPosition.xyz;
    vTexCoord = aTexCoord;
    
    // 计算 TBN 矩阵
    vec3 T = normalize(uNormalMatrix * aTangent);
    vec3 N = normalize(uNormalMatrix * aNormal);
    // 重新正交化
    T = normalize(T - dot(T, N) * N);
    vec3 B = cross(N, T);
    
    vTBN = mat3(T, B, N);
    
    gl_Position = uProjectionMatrix * uViewMatrix * worldPosition;
}
`;

// 片段着色器
const normalMapFragmentShader = `
precision mediump float;

uniform sampler2D uDiffuseMap;
uniform sampler2D uNormalMap;
uniform vec3 uLightPosition;
uniform vec3 uViewPosition;

varying vec2 vTexCoord;
varying vec3 vPosition;
varying mat3 vTBN;

void main() {
    // 从法线贴图获取法向量
    vec3 normalMap = texture2D(uNormalMap, vTexCoord).rgb;
    vec3 tangentNormal = normalMap * 2.0 - 1.0;
    
    // 转换到世界空间
    vec3 normal = normalize(vTBN * tangentNormal);
    
    // 漫反射贴图
    vec3 diffuseColor = texture2D(uDiffuseMap, vTexCoord).rgb;
    
    // Blinn-Phong 光照
    vec3 lightDir = normalize(uLightPosition - vPosition);
    vec3 viewDir = normalize(uViewPosition - vPosition);
    vec3 halfDir = normalize(lightDir + viewDir);
    
    float diff = max(dot(normal, lightDir), 0.0);
    float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
    
    vec3 result = diffuseColor * (0.1 + diff * 0.7) + vec3(1.0) * spec * 0.3;
    
    gl_FragColor = vec4(result, 1.0);
}
`;

/**
 * 计算切线
 */
function computeTangents(positions, normals, uvs, indices) {
    const tangents = new Float32Array(positions.length);
    
    for (let i = 0; i < indices.length; i += 3) {
        const i0 = indices[i];
        const i1 = indices[i + 1];
        const i2 = indices[i + 2];
        
        // 顶点位置
        const v0 = [positions[i0*3], positions[i0*3+1], positions[i0*3+2]];
        const v1 = [positions[i1*3], positions[i1*3+1], positions[i1*3+2]];
        const v2 = [positions[i2*3], positions[i2*3+1], positions[i2*3+2]];
        
        // 纹理坐标
        const uv0 = [uvs[i0*2], uvs[i0*2+1]];
        const uv1 = [uvs[i1*2], uvs[i1*2+1]];
        const uv2 = [uvs[i2*2], uvs[i2*2+1]];
        
        // 边向量
        const e1 = [v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]];
        const e2 = [v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]];
        
        // UV 差
        const duv1 = [uv1[0]-uv0[0], uv1[1]-uv0[1]];
        const duv2 = [uv2[0]-uv0[0], uv2[1]-uv0[1]];
        
        const f = 1.0 / (duv1[0] * duv2[1] - duv2[0] * duv1[1]);
        
        // 切线
        const tangent = [
            f * (duv2[1] * e1[0] - duv1[1] * e2[0]),
            f * (duv2[1] * e1[1] - duv1[1] * e2[1]),
            f * (duv2[1] * e1[2] - duv1[1] * e2[2])
        ];
        
        // 累加到顶点
        for (const idx of [i0, i1, i2]) {
            tangents[idx*3] += tangent[0];
            tangents[idx*3+1] += tangent[1];
            tangents[idx*3+2] += tangent[2];
        }
    }
    
    // 归一化
    for (let i = 0; i < tangents.length; i += 3) {
        const len = Math.sqrt(
            tangents[i]*tangents[i] + 
            tangents[i+1]*tangents[i+1] + 
            tangents[i+2]*tangents[i+2]
        );
        if (len > 0) {
            tangents[i] /= len;
            tangents[i+1] /= len;
            tangents[i+2] /= len;
        }
    }
    
    return tangents;
}

10.6 环境贴图

10.6.1 环境贴图类型

环境贴图(Environment Mapping)

用于模拟反射和环境光照。

1. 立方体贴图(Cube Map)
   6 个面组成一个立方体

       ┌─────┐
       │ +Y  │
   ┌───┼─────┼───┬───┐
   │-X │ +Z  │+X │-Z │
   └───┼─────┼───┴───┘
       │ -Y  │
       └─────┘

   采样:使用 3D 方向向量


2. 球形贴图(Sphere Map)
   整个环境映射到一个球面

   ┌─────────────┐
   │ ╱─────────╲ │
   │╱           ╲│
   │      ●      │
   │╲           ╱│
   │ ╲─────────╱ │
   └─────────────┘


3. 等距柱状投影(Equirectangular)
   全景图格式

   ┌───────────────────────┐
   │                       │
   │   360° × 180° 全景    │
   │                       │
   └───────────────────────┘

10.6.2 立方体贴图实现

javascript
/**
 * 立方体贴图反射
 */

// 片段着色器
const cubeMapFragmentShader = `
precision mediump float;

uniform samplerCube uEnvironmentMap;
uniform vec3 uViewPosition;

varying vec3 vPosition;
varying vec3 vNormal;

void main() {
    vec3 normal = normalize(vNormal);
    vec3 viewDir = normalize(vPosition - uViewPosition);
    
    // 计算反射方向
    vec3 reflectDir = reflect(viewDir, normal);
    
    // 采样立方体贴图
    vec3 envColor = textureCube(uEnvironmentMap, reflectDir).rgb;
    
    gl_FragColor = vec4(envColor, 1.0);
}
`;

/**
 * 创建立方体贴图
 */
function createCubeMap(gl, faces) {
    const texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);
    
    const targets = [
        gl.TEXTURE_CUBE_MAP_POSITIVE_X,
        gl.TEXTURE_CUBE_MAP_NEGATIVE_X,
        gl.TEXTURE_CUBE_MAP_POSITIVE_Y,
        gl.TEXTURE_CUBE_MAP_NEGATIVE_Y,
        gl.TEXTURE_CUBE_MAP_POSITIVE_Z,
        gl.TEXTURE_CUBE_MAP_NEGATIVE_Z
    ];
    
    for (let i = 0; i < 6; i++) {
        gl.texImage2D(
            targets[i],
            0,
            gl.RGBA,
            gl.RGBA,
            gl.UNSIGNED_BYTE,
            faces[i]
        );
    }
    
    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_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 texture;
}

10.7 本章小结

纹理技术对比

技术用途性能影响
基础纹理表面颜色
Mipmap减少闪烁内存 +33%
法线贴图表面细节
环境贴图反射
视差贴图深度效果

过滤模式

模式放大缩小质量
NEAREST块状闪烁
LINEAR模糊闪烁
LINEAR_MIPMAP_NEAREST模糊跳变中高
LINEAR_MIPMAP_LINEAR模糊平滑

关键要点

  1. 纹理坐标 UV 范围通常是 [0,1]
  2. Mipmap 预计算不同分辨率,减少缩小时的锯齿
  3. 三线性过滤在 mipmap 级别间插值
  4. 法线贴图用切线空间存储法向量
  5. 立方体贴图用于环境反射

下一章预告:在第11章中,我们将学习碰撞检测算法,包括包围体、SAT 和 GJK 算法。


文档版本:v1.0
字数统计:约 10,000 字

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