1. GLTF 模型更换贴图

在 Three.js 中更换 GLTF 模型的贴图需要了解模型的结构和材质系统。以下是详细的方法和示例:

1.1. 基础方法

1.1.1. 加载 GLTF 模型

javascript

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const loader = new GLTFLoader();
let model;

loader.load('model.gltf', (gltf) => {
    model = gltf.scene;
    scene.add(model);

    // 加载后更换贴图
    replaceTextures(model);
});

1.2. 更换所有材质贴图

1.2.1. 递归遍历模型

javascript

function replaceAllTextures(model, newTexture) {
    model.traverse((child) => {
        if (child.isMesh) {
            // 检查材质类型
            if (child.material) {
                replaceMeshTextures(child, newTexture);
            }
        }
    });
}

function replaceMeshTextures(mesh, newTexture) {
    const material = mesh.material;

    // 处理 MeshStandardMaterial 或 MeshPhysicalMaterial
    if (material.isMeshStandardMaterial || material.isMeshPhysicalMaterial) {
        // 更换漫反射贴图
        if (material.map) {
            material.map = newTexture;
            material.needsUpdate = true;
        }

        // 或者直接设置新贴图(覆盖原有贴图)
        material.map = newTexture;
        material.needsUpdate = true;
    }
}

1.3. 针对性更换特定贴图

1.3.1. 根据贴图类型更换

javascript

// 加载新贴图
const textureLoader = new THREE.TextureLoader();

const newDiffuseMap = textureLoader.load('new_diffuse.jpg');
const newNormalMap = textureLoader.load('new_normal.jpg');
const newRoughnessMap = textureLoader.load('new_roughness.jpg');

function replaceSpecificTextures(model) {
    model.traverse((child) => {
        if (child.isMesh && child.material) {
            const material = child.material;

            // 更换不同类型贴图
            if (material.map) {
                // 漫反射贴图
                material.map = newDiffuseMap;
            }

            if (material.normalMap) {
                // 法线贴图
                material.normalMap = newNormalMap;
                material.normalScale.set(1, 1); // 设置法线贴图强度
            }

            if (material.roughnessMap) {
                // 粗糙度贴图
                material.roughnessMap = newRoughnessMap;
            }

            if (material.metalnessMap) {
                // 金属度贴图
                material.metalnessMap = textureLoader.load('new_metalness.jpg');
            }

            if (material.aoMap) {
                // 环境光遮蔽贴图
                material.aoMap = textureLoader.load('new_ao.jpg');
            }

            if (material.emissiveMap) {
                // 自发光贴图
                material.emissiveMap = textureLoader.load('new_emissive.jpg');
                material.emissive.set(0xffffff); // 设置自发光颜色
            }

            material.needsUpdate = true;
        }
    });
}

1.4. 根据材质名称更换贴图

1.4.1. 更精确的控制

javascript

function replaceTexturesByName(model) {
    model.traverse((child) => {
        if (child.isMesh) {
            const material = child.material;

            // 检查材质名称
            if (material.name) {
                switch (material.name.toLowerCase()) {
                    case 'body_material':
                        material.map = textureLoader.load('body_diffuse.jpg');
                        material.normalMap = textureLoader.load('body_normal.jpg');
                        break;

                    case 'glass_material':
                        // 更换透明材质
                        material.map = textureLoader.load('glass_texture.png');
                        material.transparent = true;
                        material.opacity = 0.8;
                        break;

                    case 'metal_material':
                        material.map = textureLoader.load('metal_diffuse.jpg');
                        material.metalness = 0.8;
                        material.roughness = 0.2;
                        break;

                    case 'plastic_material':
                        material.map = textureLoader.load('plastic_diffuse.jpg');
                        material.roughness = 0.7;
                        material.metalness = 0.1;
                        break;
                }

                material.needsUpdate = true;
            }
        }
    });
}

1.5. PBR 材质完整更换

1.5.1. 完整的 PBR 材质设置

javascript

function createNewPBRMaterial() {
    const material = new THREE.MeshStandardMaterial({
        color: 0xffffff,
        metalness: 0.5,
        roughness: 0.5,
    });

    // 加载所有贴图
    const maps = {
        map: textureLoader.load('albedo.jpg'),
        normalMap: textureLoader.load('normal.jpg'),
        roughnessMap: textureLoader.load('roughness.jpg'),
        metalnessMap: textureLoader.load('metalness.jpg'),
        aoMap: textureLoader.load('ao.jpg'),
        emissiveMap: textureLoader.load('emissive.jpg'),
        alphaMap: textureLoader.load('alpha.png'), // 透明贴图
        displacementMap: textureLoader.load('height.png'), // 高度贴图
    };

    // 设置贴图参数
    Object.assign(material, maps);

    // 设置贴图缩放和偏移
    material.map.repeat.set(2, 2);
    material.map.wrapS = THREE.RepeatWrapping;
    material.map.wrapT = THREE.RepeatWrapping;

    // 设置法线贴图强度
    material.normalScale.set(1, 1);

    // 设置置换贴图参数
    material.displacementScale = 0.1;
    material.displacementBias = -0.05;

    return material;
}

function replaceAllMaterials(model) {
    model.traverse((child) => {
        if (child.isMesh) {
            // 保存原始材质的某些属性(如果需要)
            const oldMaterial = child.material;
            const newMaterial = createNewPBRMaterial();

            // 如果需要继承某些属性
            newMaterial.side = oldMaterial.side;
            newMaterial.transparent = oldMaterial.transparent;
            newMaterial.opacity = oldMaterial.opacity;

            child.material = newMaterial;
        }
    });
}

1.6. 实时更换贴图(交互功能)

1.6.1. 动态贴图切换

javascript

class TextureManager {
    constructor(model) {
        this.model = model;
        this.originalTextures = new Map();
        this.textureCache = new Map();
        this.currentTextureSet = 'default';

        // 保存原始贴图
        this.saveOriginalTextures();
    }

    saveOriginalTextures() {
        this.model.traverse((child) => {
            if (child.isMesh && child.material) {
                const material = child.material;
                const textures = {
                    map: material.map,
                    normalMap: material.normalMap,
                    roughnessMap: material.roughnessMap,
                    // 保存其他贴图...
                };
                this.originalTextures.set(child.uuid, textures);
            }
        });
    }

    async loadTextureSet(setName) {
        if (this.textureCache.has(setName)) {
            this.applyTextureSet(setName);
            return;
        }

        // 异步加载贴图
        const textures = await this.loadTextures(setName);
        this.textureCache.set(setName, textures);
        this.applyTextureSet(setName);
    }

    async loadTextures(setName) {
        const basePath = `textures/${setName}/`;

        return {
            diffuse: await this.loadTexture(`${basePath}diffuse.jpg`),
            normal: await this.loadTexture(`${basePath}normal.jpg`),
            roughness: await this.loadTexture(`${basePath}roughness.jpg`),
            metalness: await this.loadTexture(`${basePath}metalness.jpg`),
        };
    }

    loadTexture(url) {
        return new Promise((resolve) => {
            textureLoader.load(url, resolve);
        });
    }

    applyTextureSet(setName) {
        const textures = this.textureCache.get(setName);

        this.model.traverse((child) => {
            if (child.isMesh && child.material) {
                const material = child.material;

                // 根据材质名称或类型应用不同的贴图
                if (material.name && material.name.includes('metal')) {
                    material.map = textures.metalness;
                } else {
                    material.map = textures.diffuse;
                    material.normalMap = textures.normal;
                    material.roughnessMap = textures.roughness;
                }

                material.needsUpdate = true;
            }
        });

        this.currentTextureSet = setName;
    }

    resetToOriginal() {
        this.model.traverse((child) => {
            if (child.isMesh && this.originalTextures.has(child.uuid)) {
                const original = this.originalTextures.get(child.uuid);
                Object.assign(child.material, original);
                child.material.needsUpdate = true;
            }
        });
    }
}

// 使用示例
let textureManager;

loader.load('model.gltf', (gltf) => {
    const model = gltf.scene;
    scene.add(model);

    textureManager = new TextureManager(model);

    // 切换贴图集
    document.getElementById('texture1').addEventListener('click', () => {
        textureManager.loadTextureSet('wood');
    });

    document.getElementById('texture2').addEventListener('click', () => {
        textureManager.loadTextureSet('metal');
    });

    document.getElementById('reset').addEventListener('click', () => {
        textureManager.resetToOriginal();
    });
});

1.7. 优化建议

1.7.1. 贴图管理

javascript

// 1. 使用 LoadingManager 管理加载
const loadingManager = new THREE.LoadingManager();
loadingManager.onProgress = (url, loaded, total) => {
    console.log(`Loading: ${loaded}/${total} - ${url}`);
};

const textureLoader = new THREE.TextureLoader(loadingManager);

// 2. 压缩贴图
function loadCompressedTexture(url) {
    return new Promise((resolve, reject) => {
        const loader = new THREE.CompressedTextureLoader();
        loader.load(url, resolve, undefined, reject);
    });
}

// 3. 缓存已加载的贴图
const textureCache = {};
function getCachedTexture(url) {
    if (!textureCache[url]) {
        textureCache[url] = textureLoader.load(url);
    }
    return textureCache[url];
}

// 4. 释放不需要的贴图
function disposeUnusedTextures() {
    Object.keys(textureCache).forEach(key => {
        if (!isTextureInUse(textureCache[key])) {
            textureCache[key].dispose();
            delete textureCache[key];
        }
    });
}

1.8. 常见问题解决

1.8.1. 贴图不显示

javascript

function fixTextureIssues(material) {
    // 1. 检查贴图是否加载
    if (material.map && material.map.image) {
        console.log('贴图已加载:', material.map.image.src);
    }

    // 2. 检查 UV 坐标
    if (material.map) {
        material.map.needsUpdate = true;
    }

    // 3. 检查材质是否需要更新
    material.needsUpdate = true;

    // 4. 检查纹理过滤和包裹方式
    if (material.map) {
        material.map.minFilter = THREE.LinearFilter;
        material.map.magFilter = THREE.LinearFilter;
        material.map.wrapS = THREE.RepeatWrapping;
        material.map.wrapT = THREE.RepeatWrapping;
    }

    // 5. 检查透明材质设置
    if (material.transparent) {
        material.alphaTest = 0.5;
        material.depthWrite = false;
    }
}

// 应用修复
model.traverse((child) => {
    if (child.isMesh && child.material) {
        fixTextureIssues(child.material);
    }
});

1.9. 处理多材质对象

javascript

// GLTF 模型可能有多个材质
function handleMultiMaterial(mesh) {
    if (Array.isArray(mesh.material)) {
        // 多个材质(如 MultiMaterial)
        mesh.material.forEach((material, index) => {
            replaceMaterialTextures(material, index);
        });
    } else {
        // 单个材质
        replaceMaterialTextures(mesh.material);
    }
}

function replaceMaterialTextures(material, materialIndex = 0) {
    // 根据材质索引使用不同的贴图
    const texturePaths = [
        'textures/material_0/diffuse.jpg',
        'textures/material_1/diffuse.jpg',
        // ...
    ];

    if (texturePaths[materialIndex]) {
        material.map = textureLoader.load(texturePaths[materialIndex]);
        material.needsUpdate = true;
    }
}

2. 总结

更换 GLTF 模型贴图的关键步骤:

  1. 正确加载模型:使用 GLTFLoader

  2. 遍历模型结构:使用 traverse() 方法

  3. 识别材质类型:检查材质属性

  4. 更换贴图:根据需要更换特定贴图

  5. 更新材质:设置 material.needsUpdate = true

  6. 优化性能:缓存贴图,管理内存

通过以上方法,你可以灵活地控制和更换 GLTF 模型的任何贴图。