1. Three.js 光照贴图与烘焙技术

在 Three.js 中,光照贴图(Lightmaps) 和 烘焙(Baking) 是创建高质量静态光照的关键技术。下面我将为你详细介绍这两种技术的概念、实现方法和最佳实践。

1.1. 光照贴图基础

1.1.1. 什么是光照贴图?

光照贴图是预先计算好的光照信息,存储为纹理贴图,用于模拟复杂光照效果(如全局光照、软阴影等),而不需要实时计算。

1.1.2. 主要优点:

1.2. 烘焙流程

1.2.1. 准备工作(3D建模软件中)

javascript

// 示例模型要求:
// 1. 需要第二组UV坐标(用于光照贴图)
// 2. 合理展开UV,避免重叠和拉伸
// 3. 在Blender、Maya等软件中烘焙光照

1.2.2. 在Three.js中使用光照贴图

javascript

import * as THREE from 'three';

// 加载模型和光照贴图
const loader = new THREE.GLTFLoader();
loader.load('model.glb', (gltf) => {
    const model = gltf.scene;

    // 遍历模型,应用光照贴图
    model.traverse((child) => {
        if (child.isMesh) {
            const mesh = child;

            // 确保材质支持光照贴图
            if (mesh.material) {
                // 加载光照贴图
                const lightMapLoader = new THREE.TextureLoader();
                const lightMap = lightMapLoader.load('lightmap.jpg');

                // 配置光照贴图
                lightMap.flipY = false; // 根据导出软件调整

                // 应用到材质
                mesh.material.lightMap = lightMap;
                mesh.material.lightMapIntensity = 1.0;

                // 重要:需要设置第二组UV
                mesh.geometry.attributes.uv2 = mesh.geometry.attributes.uv;

                // 禁用实时阴影(如果完全使用烘焙光照)
                mesh.castShadow = false;
                mesh.receiveShadow = false;

                // 如果需要混合实时光照和烘焙光照
                mesh.material.needsUpdate = true;
            }
        }
    });

    scene.add(model);
});

1.3. 完整的光照烘焙系统

javascript

class LightmapSystem {
    constructor() {
        this.lightmaps = new Map();
        this.lightmapIntensity = 1.0;
    }

    // 加载并应用光照贴图
    async applyLightmap(model, lightmapPath, options = {}) {
        const {
            intensity = 1.0,
            flipY = false,
            blendMode = 'multiply'
        } = options;

        // 加载光照贴图
        const texture = await this.loadTexture(lightmapPath, flipY);

        model.traverse((child) => {
            if (child.isMesh && child.material) {
                this.setupMaterial(child, texture, intensity, blendMode);
            }
        });

        return model;
    }

    // 设置材质
    setupMaterial(mesh, lightmap, intensity, blendMode) {
        const material = mesh.material;

        // 如果材质是数组(多个材质)
        if (Array.isArray(material)) {
            material.forEach(mat => this.configureMaterial(mat, lightmap, intensity));
        } else {
            this.configureMaterial(material, lightmap, intensity);
        }

        // 确保有第二组UV
        if (!mesh.geometry.attributes.uv2 && mesh.geometry.attributes.uv) {
            mesh.geometry.setAttribute('uv2', mesh.geometry.attributes.uv.clone());
        }

        // 根据混合模式调整
        if (blendMode === 'multiply') {
            material.lightMap = lightmap;
        }

        material.needsUpdate = true;
    }

    // 配置单个材质
    configureMaterial(material, lightmap, intensity) {
        material.lightMap = lightmap;
        material.lightMapIntensity = intensity;

        // 调整材质属性以适应光照贴图
        material.emissiveIntensity = 0.1; // 微小的自发光使阴影区域不纯黑

        // 如果是标准或物理材质
        if (material.isMeshStandardMaterial || material.isMeshPhysicalMaterial) {
            material.envMapIntensity = 0.5; // 降低环境贴图强度
        }
    }

    // 异步加载纹理
    loadTexture(path, flipY = false) {
        return new Promise((resolve, reject) => {
            new THREE.TextureLoader().load(
                path,
                (texture) => {
                    texture.flipY = flipY;
                    texture.encoding = THREE.sRGBEncoding;
                    resolve(texture);
                },
                undefined,
                reject
            );
        });
    }

    // 混合实时光照和烘焙光照
    setupHybridLighting(model, options = {}) {
        const {
            ambientIntensity = 0.3,
            directionalIntensity = 0.7,
            shadowEnabled = false
        } = options;

        model.traverse((child) => {
            if (child.isMesh) {
                // 允许接收实时阴影(可选)
                child.receiveShadow = shadowEnabled;

                // 调整材质以更好地混合
                if (child.material) {
                    // 提高环境光贡献
                    child.material.envMapIntensity = ambientIntensity;

                    // 如果是物理材质,调整光泽度
                    if (child.material.isMeshPhysicalMaterial) {
                        child.material.roughness = Math.min(
                            child.material.roughness * 1.2, 1.0
                        );
                    }
                }
            }
        });
    }
}

1.4. 性能优化技巧

javascript

// 1. 光照贴图压缩
function optimizeLightmap(texture, maxSize = 2048) {
    texture.generateMipmaps = true;
    texture.minFilter = THREE.LinearMipmapLinearFilter;
    texture.magFilter = THREE.LinearFilter;

    // 自动调整尺寸
    if (texture.image) {
        const scale = Math.min(1, maxSize / Math.max(
            texture.image.width, 
            texture.image.height
        ));
        texture.image.width *= scale;
        texture.image.height *= scale;
    }
}

// 2. 批处理静态物体
function batchStaticMeshes(scene) {
    const staticMeshes = [];

    scene.traverse((child) => {
        if (child.isMesh && child.material.lightMap) {
            // 标记为静态
            child.frustumCulled = true;
            staticMeshes.push(child);
        }
    });

    // 可以进一步合并几何体(如果材质相同)
    return staticMeshes;
}

// 3. 光照贴图缓存
class LightmapCache {
    constructor() {
        this.cache = new Map();
        this.maxSize = 10; // 最大缓存数量
    }

    get(key) {
        const entry = this.cache.get(key);
        if (entry) {
            // 更新使用时间
            entry.lastUsed = Date.now();
            return entry.texture;
        }
        return null;
    }

    set(key, texture) {
        if (this.cache.size >= this.maxSize) {
            this.removeOldest();
        }
        this.cache.set(key, {
            texture,
            lastUsed: Date.now()
        });
    }
}

1.5. 常见问题解决

1.5.1. 问题1:光照贴图不显示

javascript

// 检查清单:
// 1. 确认第二组UV存在
if (!mesh.geometry.attributes.uv2) {
    console.error('Missing UV2 coordinates');
    mesh.geometry.setAttribute('uv2', mesh.geometry.attributes.uv.clone());
}

// 2. 检查纹理加载
textureLoader.load('lightmap.jpg', (texture) => {
    console.log('Texture loaded:', texture.image.width, 'x', texture.image.height);
}, undefined, (error) => {
    console.error('Failed to load texture:', error);
});

// 3. 检查材质类型
if (!mesh.material.lightMap) {
    console.warn('Material does not support lightmaps:', mesh.material.type);
}

1.5.2. 问题2:光照贴图过亮或过暗

javascript

// 调整光照贴图强度
material.lightMapIntensity = 0.8; // 调整这个值

// 调整材质颜色
material.color.multiplyScalar(1.2); // 提亮基础颜色

// 使用色调映射
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;

1.5.3. 问题3:接缝问题

javascript

// 在建模软件中:
// 1. 确保UV岛之间有足够的填充(padding)
// 2. 避免UV拉伸

// 在Three.js中:
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;

1.6. 高级技巧:动态物体烘焙

javascript

// 对于有动画的物体,可以使用多张光照贴图
class AnimatedLightmap {
    constructor(mesh, lightmapTextures) {
        this.mesh = mesh;
        this.lightmaps = lightmapTextures;
        this.currentFrame = 0;

        // 创建材质副本以支持动画
        this.originalMaterial = mesh.material.clone();
        this.animatedMaterial = mesh.material.clone();
    }

    update(animationProgress) {
        // 根据动画进度选择光照贴图
        const frameIndex = Math.floor(animationProgress * this.lightmaps.length);

        if (frameIndex !== this.currentFrame && this.lightmaps[frameIndex]) {
            this.currentFrame = frameIndex;
            this.animatedMaterial.lightMap = this.lightmaps[frameIndex];
            this.mesh.material = this.animatedMaterial;
        }
    }
}

1.7. 工具推荐

  1. 烘焙软件

  2. Three.js相关工具

1.8. 总结

光照贴图和烘焙技术在Three.js中实现相对直接,关键在于:

对于完全静态的场景,推荐全部使用烘焙光照;对于半动态场景,可以结合使用烘焙和实时阴影;对于完全动态的场景,则需要依赖实时光照系统。