The color of the displayed model is inconsistent with the color of the original model

import React, { useRef, useEffect } from 'react';
import * as THREE from 'three';
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import JSZip from 'jszip';

const ModelViewer = ({ zipUrl }) => {
  const mountRef = useRef(null);

  useEffect(() => {
    const initScene = () => {
      const width = mountRef.current.clientWidth;
      const height = mountRef.current.clientHeight;

      const scene = new THREE.Scene();
      // 设置背景颜色为白色
      // scene.background = new THREE.Color(0xffffff);
      const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
      const renderer = new THREE.WebGLRenderer({ antialias: true });

      renderer.setSize(width, height);
      mountRef.current.appendChild(renderer.domElement);

      const controls = new OrbitControls(camera, renderer.domElement);

      // Ambient Light with higher intensity
      const ambientLight = new THREE.AmbientLight(0x404040, 2); // higher intensity
      scene.add(ambientLight);

      // Directional Light to simulate sunlight
      const directionalLight = new THREE.DirectionalLight(0xffffff, 2); // higher intensity
      directionalLight.position.set(5, 10, 7.5);
      scene.add(directionalLight);

      // Point Light to add some more brightness
      // const pointLight = new THREE.PointLight(0xffffff, 2, 100); // higher intensity
      // pointLight.position.set(0, 10, 10);
      // scene.add(pointLight);

      camera.position.z = 5;
      controls.update();

      // return { scene, camera, renderer, controls };
      return { scene, camera, renderer };
    };

    const { scene, camera, renderer } = initScene();

    const animate = (object) => {
      // let animationId;
      const render = function () {
        requestAnimationFrame(render);
        // animationId = requestAnimationFrame(render);
        object.rotation.y += 0.01;
        renderer.render(scene, camera);
      };
      render();
      // 监听鼠标点击事件,停止自动旋转
      // document.addEventListener('click', function() {
      //   cancelAnimationFrame(animationId);
      // });
    };

    const loadModel = async (file, fileType, textureBlob) => {
      let loader;
      const fileBlob = await file.async("blob");
      const fileUrl = URL.createObjectURL(fileBlob);

      switch (fileType) {
        case 'fbx':
          loader = new FBXLoader();
          break;
        case 'obj':
          const mtlFileUrl = getMtlFileUrl(fileUrl); // 自定义函数获取MTL文件URL
          const mtlBlob = await fetchMtlBlob(mtlFileUrl); // 获取MTL文件的Blob
          const mtlUrl = URL.createObjectURL(mtlBlob);

          const mtlLoader = new MTLLoader();
          mtlLoader.load(mtlUrl, (materials) => {
            materials.preload(); // 预加载材质
            const objLoader = new OBJLoader();
            objLoader.setMaterials(materials); // 设置材质
            objLoader.load(fileUrl, (object) => {
              applyTexture(object, textureBlob);
              // scene.add(object);
              sizeAndAddModel(object, scene);
              animate(object);
            });
          });
          return; // 由于我们在内部加载,所以直接返回
        case 'gltf':
        case 'glb':
          loader = new GLTFLoader();
          break;
        default:
          console.error(`Unsupported file type: ${fileType}`);
          return;
      }

      loader.load(fileUrl, (object) => {
        applyTexture(object, textureBlob);
        // scene.add(object);
        sizeAndAddModel(object, scene);
        animate(object);
      });
    };

    const sizeAndAddModel = (object, scene) => {
      // 计算模型边界框
      const box = new THREE.Box3().setFromObject(object);
      const size = box.getSize(new THREE.Vector3());
      const targetSize = 4; // 目标大小
      const scale = targetSize / Math.max(size.x, size.y, size.z);

      object.scale.set(scale, scale, scale); // 设置缩放
      scene.add(object);
    };

    // 获取MTL文件的Blob示例函数
    const fetchMtlBlob = async (mtlFileUrl) => {
      const response = await fetch(mtlFileUrl);
      return await response.blob();
    };

    // 应用自定义纹理的通用函数
    const applyTexture = (object, textureBlob) => {
      if (textureBlob) {
          const textureLoader = new THREE.TextureLoader();
          const textureUrl = URL.createObjectURL(textureBlob);
          textureLoader.load(textureUrl, (loadedTexture) => {
              object.traverse((child) => {
                  if (child.isMesh) {
                      child.material.map = loadedTexture;
                      child.material.needsUpdate = true; // 通知材质需要更新
                  }
              });
          });
      }
    };

    // 获取MTL文件URL的示例函数,可以根据需求自定义
    const getMtlFileUrl = (objFileUrl) => {
      return objFileUrl.replace('.obj', '.mtl'); // 假设MTL文件和OBJ文件同名
    };

    const fetchAndUnzip = async (url) => {
      const response = await fetch(url.replace(/http:\/\//g, 'https://'));
      const arrayBuffer = await response.arrayBuffer();
      const zip = await JSZip.loadAsync(arrayBuffer);

      let textureFile;
      const modelFiles = [];

      zip.forEach((relativePath, file) => {
        const ext = relativePath.split('.').pop().toLowerCase();
        // 检查并保存纹理文件
        if (ext === 'png' || ext === 'jpg' || ext === 'jpeg') {
          textureFile = file;
        // 检查并保存模型文件
        } else if (ext === 'fbx' || ext === 'obj' || ext === 'glb' || ext === 'gltf') {
          modelFiles.push({ file, fileType: ext });
        // 检查并保存MTL文件
        } else if (ext === 'mtl') {
          modelFiles.push({ file, fileType: 'mtl' });
        }
      });

      const textureBlob = textureFile ? await textureFile.async("blob") : null;

      for (const { file, fileType } of modelFiles) {
        await loadModel(file, fileType, textureBlob);
      }
    };

    fetchAndUnzip(zipUrl);

    return () => {
      mountRef.current.innerHTML = '';
    };
  }, [zipUrl]);

  return <div ref={mountRef} style={{ width: 800, height: 600 }} />;
};

export default ModelViewer;

1-18.zip (3.0 MB)
The original model is blue and purple


Shown in yellow

There’s a lot going on in this code – it looks like you’re grabbing some texture from the .ZIP and adding it to the model, but using the wrong texture? I might suggest copying from one of the official OBJ examples, and perhaps not using a .ZIP, the code will be much simpler.

Okay, thanks. I checked again and it seems that only one texture file in the zip is loaded.

That model has 4 textures all together, listed at the bottom of your MTL file:

map_Kd texture_diffuse.png
map_Bump -bm 1.000000 texture_normal.png
map_Pr texture_roughness.png
map_Pm texture_metallic.png

You would also need to apply each of these textures to the proper slot of the material:

  • map_Kd is equivalent to child.material.map
  • map_Bump is equivalent to child.material.bumpMap
  • map_Pm is equivalent to child.material.metalnessMap
  • map_Pr is equivalent to child.material.roughnessMap

The last 2 maps require either the use of THREE.MeshStandardMaterial or THREE.MeshPhysicalMaterial.

The official MTL Loader should be using THREE.MeshPhongMaterial only, see this line in the loader, so you might not be able to get all the effects.

Maybe try switching to using GLTF format instead.

Also, if you are loading from the zip file then maybe try removing the _MACOSX folder from it, since it also has some files inside that might confuse the loader.

If MeshStandardMaterial is not used, will mtlloader not automatically load texture files?

Did you create the model in Blender or can you convert it to a Blender model?
If so, you could export the textured model from Blender to a glb file and all your loading problems should be solved and the loading process would be much much simpler.
Just a thought.
p.s. I don’t want that in my aquarium!

MTL Loader will only process the lines it can understand and skip all other lines - it can understand map_Kd and map_Bump entries but it does not know what map_Pm and map_Pr are for.

Your applyTexture function is specifically applying a texture to the child.material.map slot, without even checking whether that texture is designated for the map slot.

This is probably why you should switch to GLB format and eliminate the code for loading textures.

Since you are using React then I am not sure if this might help you further, in case if you decide to stick with using OBJ+MTL models, but check this topic for my version of OBJ+MTL viewers.

The standalone viewer has custom MTL Loader and OBJ Loader embedded and it can load your model as a zip file but first remove that _MACOSX folder from it or just use the zip attached below for a test.

1-18.zip (3.0 MB)

Here is also a picture of that viewer with your model loaded.