猴子也能实现three.js加载小米su7模型(gltf模型版)并实现车换色

说实话现在AI发展的足够让我这只猴子不用去阅读复杂的threejs文档就实现这个功能( 英文太烂,没那个功夫, 懒狗一条),重要的是知道他怎么实现就可以了。

效果展示

请添加图片描述

在这里插入图片描述

前情提要

因为有这么个需求,所以调研了下相关技术。

1. three
作用:核心3D引擎,提供WebGL图形渲染、场景管理、摄像机和灯光等功能,是业界最流行、生态丰富的Web 3D开发基础库。

2. postprocessing
作用:高级3D渲染后处理库。在three.js中使用,可以非常方便地添加模糊、泛光(bloom)、景深、色彩校正等视觉特效,提升画面表现力。

依赖关系:基于three.js使用,并与其生态无缝集成。

3. lil-gui
作用:轻量级GUI面板库,类似 dat.gui。用于在网页端实时调整参数和调试变量,极大提升开发、调优与演示的效率。

依赖关系:可独立用于各种JS项目,常与three.js、postprocessing搭配,用于动态切换特效、灯光、材质等参数。

4. lygia
作用:丰富的GLSL着色器工具库。为自定义shader提供大量高性能、可组合的算法片段(如噪声、滤镜、数学函数)。专门用于提升WebGL/three.js中的视觉创意和自定义后处理能力。

依赖关系:常与three.js和postprocessing配合,实现复杂shader效果。

5. kokomi.js
作用:基于three.js的扩展性工具库,通常用于快速开发3D项目、场景管理或整合各种常用插件。可能封装了如场景切换、后处理、交互等高级功能,进一步简化three.js项目开发。

依赖关系:以three.js为基础,集成或规范化多种3D项目常用功能,加快开发速度。

6. howler
作用:流行的网页音频库,专注于高兼容性、性能最优的音效、音乐播放与管理。常用于需要有音效/背景音乐的3D或可视化交互项目。

依赖关系:独立于three.js,用于音频播放控制,增强多媒体体验。

上面都是AI给解释的奥,因为我们只是加载个模型 还用不着着色器一类的所以本文没有使用到postprocessing lygia
反正都是AI写无所谓了,知道有这么个东西就行,用的时候再去问AI。

正文

先让AI装了一下相关依赖,给他下达指令让他干活。

奶奶滴,AI好像不认识kokomi 根本不知道咋用代码胡编乱造,跑都跑不起来。给👴气的脑溢血了。本身我想让他 只是用kokomi实现这个功能的这脑瘫AI似乎理解不了,加载模型还捣鼓他那破threejs,本身kokomi 已经封装了threejs,算了算了,我们只用kokomi初始化场景完事了,模型加载还是让脑瘫AI用threejs去加载吧。

代码展示【AI写的】

老演员占位div,这时候不太聪明的猴子就要问了为啥叫id="sketch" 不能自定义吗。
当然可以自定义,因为这代码是脑瘫AI写的他理解不了,而kokomi默认找的就是这个,所以我就这样写了。

 <div ref="container" class="car-model-container" id="sketch">

初始化代码

onMounted(() => {
  initKokomiApp();
  loadCarModel();
});
// 继承 kokomi.Base 类
class CarSketch extends kokomi.Base {
  create() {
    // 设置场景背景
    this.scene.background = new THREE.Color(0x87ceeb);

    // 添加环境光 - 降低强度避免冲淡材质颜色
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
    this.scene.add(ambientLight);

    // 添加方向光 - 主要光源
    const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
    directionalLight.position.set(10, 10, 5);
    directionalLight.castShadow = true;
    directionalLight.shadow.mapSize.width = 2048;
    directionalLight.shadow.mapSize.height = 2048;
    this.scene.add(directionalLight);

    // 添加补光 - 减少阴影过暗
    const fillLight = new THREE.DirectionalLight(0xffffff, 0.4);
    fillLight.position.set(-10, 5, -5);
    this.scene.add(fillLight);

    // 添加地面
    const groundGeometry = new THREE.PlaneGeometry(20, 20);
    const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x90ee90 });
    const ground = new THREE.Mesh(groundGeometry, groundMaterial);
    ground.rotation.x = -Math.PI / 2;
    ground.receiveShadow = true;
    this.scene.add(ground);

    // 启用阴影
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    this.renderer.outputColorSpace = THREE.SRGBColorSpace;

    // 设置相机位置
    this.camera.position.set(5, 3, 5);
    this.camera.lookAt(0, 0, 0);

    // 添加轨道控制器
    new kokomi.OrbitControls(this);

    // 添加自定义动画
    this.update((time) => {
      if (carModel) {
        carModel.rotation.y += 0.005;
      }
    });
  }
}

const initKokomiApp = () => {
  // 创建继承自 kokomi.Base 的实例
  sketch = new CarSketch();
  sketch.create();
};

const loadCarModel = () => {
  const loader = new GLTFLoader();

  // 设置 meshopt 解码器(如果需要的话)
  loader.setMeshoptDecoder(MeshoptDecoder);
  loadModel();

  function loadModel() {
    loader.load(
      "/sm_car.gltf",
      (gltf) => {
        carModel = gltf.scene;

        // 收集所有汽车网格并设置阴影
        carMeshes = [];
        carModel.traverse((child) => {
          if (child.isMesh) {
            child.castShadow = true;
            child.receiveShadow = true;
            carMeshes.push(child);
          }
        });

        // 调整模型大小和位置
        carModel.scale.set(1, 1, 1);
        carModel.position.set(0, 0, 0);

        sketch.scene.add(carModel);

        // 自动调整相机位置以适应模型
        const box = new THREE.Box3().setFromObject(carModel);
        const center = box.getCenter(new THREE.Vector3());
        const size = box.getSize(new THREE.Vector3());

        const maxDim = Math.max(size.x, size.y, size.z);
        const fov = sketch.camera.fov * (Math.PI / 180);
        let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));

        sketch.camera.position.set(
          center.x + cameraZ,
          center.y + cameraZ * 0.5,
          center.z + cameraZ
        );
        sketch.camera.lookAt(center);

        // 模型加载完成
        isLoaded.value = true;
        loadingProgress.value = 100;

        // 延迟初始化GUI,确保模型已加载
        setTimeout(() => {
          initGUI();
        }, 500);

        console.log("汽车模型加载完成!");
      },
      (progress) => {
        if (progress.total > 0) {
          loadingProgress.value = (progress.loaded / progress.total) * 100;
        }
        console.log("加载进度:", loadingProgress.value + "%");
      },
      (error) => {
        console.error("加载模型时出错:", error);
        // 显示错误信息
        isLoaded.value = true;
        loadingProgress.value = 0;
      }
    );
  }
};

就这样汽车已经渲染出来了,是不是很简单,接下来实现换色,逻辑就是加载模型时把模型的网格存储起来。
然后遍历网格设置颜色就可以实现换色

// 改变汽车颜色的函数
const changeCarColor = (color) => {
  currentColor.value = color;
  const threeColor = new THREE.Color(color);

  carMeshes.forEach((mesh) => {
    if (mesh.material) {
      // 如果是数组材质
      if (Array.isArray(mesh.material)) {
        mesh.material.forEach((mat) => {
          if (
            mat.isMeshStandardMaterial ||
            mat.isMeshLambertMaterial ||
            mat.isMeshPhongMaterial
          ) {
            mat.color = threeColor;
          }
        });
      } else {
        // 单个材质
        if (
          mesh.material.isMeshStandardMaterial ||
          mesh.material.isMeshLambertMaterial ||
          mesh.material.isMeshPhongMaterial
        ) {
          mesh.material.color = threeColor;
        }
      }
    }
  });

  console.log("汽车颜色已更改为:", color);
};

OK! 非常好的代码,使我的su7旋转
在这里插入图片描述

完整的答辩代码请直接复制粘贴

<template>
  <div ref="container" class="car-model-container" id="sketch">
    <!-- Loading 页面 -->
    <div v-if="!isLoaded" class="loading-overlay">
      <div class="loading-content">
        <div class="loading-spinner"></div>
        <div class="loading-text">正在加载汽车模型...</div>
        <div class="loading-progress" v-if="loadingProgress > 0">
          加载进度: {{ Math.round(loadingProgress) }}%
        </div>
      </div>
    </div>

    <!-- 颜色选择器 -->
    <div v-if="isLoaded" class="color-picker">
      <div class="color-picker-title">点击选择颜色</div>
      <div class="color-options">
        <div
          v-for="color in carColors"
          :key="color.name"
          class="color-option"
          :style="{ backgroundColor: color.value }"
          @click="changeCarColor(color.value)"
          :class="{ active: currentColor === color.value }"
        >
          <span class="color-name">{{ color.name }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import * as kokomi from "kokomi.js";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
import GUI from "lil-gui";

const container = ref(null);
const isLoaded = ref(false);
const loadingProgress = ref(0);
const currentColor = ref("#ff0000");
let sketch,
  carModel,
  gui,
  carMeshes = [];

// 汽车颜色选项
const carColors = [
  { name: "红色", value: "#ff0000" },
  { name: "蓝色", value: "#0066ff" },
  { name: "绿色", value: "#00ff00" },
  { name: "黄色", value: "#ffff00" },
  { name: "紫色", value: "#9900ff" },
  { name: "橙色", value: "#ff6600" },
  { name: "粉色", value: "#ff66cc" },
  { name: "白色", value: "#ffffff" },
  { name: "黑色", value: "#000000" },
  { name: "银色", value: "#c0c0c0" },
  { name: "金色", value: "#ffd700" },
  { name: "深蓝", value: "#000080" },
];

onMounted(() => {
  initKokomiApp();
  loadCarModel();
});

onUnmounted(() => {
  if (sketch) {
    sketch.dispose();
  }
  if (gui) {
    gui.destroy();
  }
});

// 继承 kokomi.Base 类
class CarSketch extends kokomi.Base {
  create() {
    // 设置场景背景
    this.scene.background = new THREE.Color(0x87ceeb);

    // 添加环境光 - 降低强度避免冲淡材质颜色
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
    this.scene.add(ambientLight);

    // 添加方向光 - 主要光源
    const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
    directionalLight.position.set(10, 10, 5);
    directionalLight.castShadow = true;
    directionalLight.shadow.mapSize.width = 2048;
    directionalLight.shadow.mapSize.height = 2048;
    this.scene.add(directionalLight);

    // 添加补光 - 减少阴影过暗
    const fillLight = new THREE.DirectionalLight(0xffffff, 0.4);
    fillLight.position.set(-10, 5, -5);
    this.scene.add(fillLight);

    // 添加地面
    const groundGeometry = new THREE.PlaneGeometry(20, 20);
    const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x90ee90 });
    const ground = new THREE.Mesh(groundGeometry, groundMaterial);
    ground.rotation.x = -Math.PI / 2;
    ground.receiveShadow = true;
    this.scene.add(ground);

    // 启用阴影
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    this.renderer.outputColorSpace = THREE.SRGBColorSpace;

    // 设置相机位置
    this.camera.position.set(5, 3, 5);
    this.camera.lookAt(0, 0, 0);

    // 添加轨道控制器
    new kokomi.OrbitControls(this);

    // 添加自定义动画
    this.update((time) => {
      if (carModel) {
        // carModel.rotation.y += 0.005;
      }
    });
  }
}

const initKokomiApp = () => {
  // 创建继承自 kokomi.Base 的实例
  sketch = new CarSketch();
  sketch.create();
};

const loadCarModel = () => {
  const loader = new GLTFLoader();

  // 设置 meshopt 解码器(如果需要的话)
  loader.setMeshoptDecoder(MeshoptDecoder);
  loadModel();

  function loadModel() {
    loader.load(
      "/sm_car.gltf",
      (gltf) => {
        carModel = gltf.scene;

        // 收集所有汽车网格并设置阴影
        carMeshes = [];
        carModel.traverse((child) => {
          if (child.isMesh) {
            child.castShadow = true;
            child.receiveShadow = true;
            carMeshes.push(child);
          }
        });

        // 调整模型大小和位置
        carModel.scale.set(1, 1, 1);
        carModel.position.set(0, 0, 0);

        sketch.scene.add(carModel);

        // 自动调整相机位置以适应模型
        const box = new THREE.Box3().setFromObject(carModel);
        const center = box.getCenter(new THREE.Vector3());
        const size = box.getSize(new THREE.Vector3());

        const maxDim = Math.max(size.x, size.y, size.z);
        const fov = sketch.camera.fov * (Math.PI / 180);
        let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));

        sketch.camera.position.set(
          center.x + cameraZ,
          center.y + cameraZ * 0.5,
          center.z + cameraZ
        );
        sketch.camera.lookAt(center);

        // 模型加载完成
        isLoaded.value = true;
        loadingProgress.value = 100;

        // 延迟初始化GUI,确保模型已加载
        setTimeout(() => {
          initGUI();
        }, 500);

        console.log("汽车模型加载完成!");
      },
      (progress) => {
        if (progress.total > 0) {
          loadingProgress.value = (progress.loaded / progress.total) * 100;
        }
        console.log("加载进度:", loadingProgress.value + "%");
      },
      (error) => {
        console.error("加载模型时出错:", error);
        // 显示错误信息
        isLoaded.value = true;
        loadingProgress.value = 0;
      }
    );
  }
};

// 改变汽车颜色的函数
const changeCarColor = (color) => {
  currentColor.value = color;
  const threeColor = new THREE.Color(color);

  carMeshes.forEach((mesh) => {
    if (mesh.material) {
      // 如果是数组材质
      if (Array.isArray(mesh.material)) {
        mesh.material.forEach((mat) => {
          if (
            mat.isMeshStandardMaterial ||
            mat.isMeshLambertMaterial ||
            mat.isMeshPhongMaterial
          ) {
            mat.color = threeColor;
          }
        });
      } else {
        // 单个材质
        if (
          mesh.material.isMeshStandardMaterial ||
          mesh.material.isMeshLambertMaterial ||
          mesh.material.isMeshPhongMaterial
        ) {
          mesh.material.color = threeColor;
        }
      }
    }
  });

  console.log("汽车颜色已更改为:", color);
};

const initGUI = () => {
  gui = new GUI();

  // 确保 carModel 存在
  if (!carModel) {
    console.warn("Car model not loaded yet, skipping GUI initialization");
    return;
  }

  // 模型控制
  const modelFolder = gui.addFolder("模型控制");

  // 创建可控制的旋转对象
  const rotationControl = {
    x: carModel.rotation.x,
    y: carModel.rotation.y,
    z: carModel.rotation.z,
  };

  modelFolder
    .add(rotationControl, "x", -Math.PI, Math.PI)
    .name("旋转X")
    .onChange((value) => {
      carModel.rotation.x = value;
    });
  modelFolder
    .add(rotationControl, "y", -Math.PI, Math.PI)
    .name("旋转Y")
    .onChange((value) => {
      carModel.rotation.y = value;
    });
  modelFolder
    .add(rotationControl, "z", -Math.PI, Math.PI)
    .name("旋转Z")
    .onChange((value) => {
      carModel.rotation.z = value;
    });

  // 创建可控制的缩放对象
  const scaleControl = {
    scale: 1,
  };
  modelFolder
    .add(scaleControl, "scale", 0.1, 3)
    .name("缩放")
    .onChange((value) => {
      carModel.scale.set(value, value, value);
    });

  // 颜色控制
  const colorFolder = gui.addFolder("颜色控制");
  const colorParams = {
    color: currentColor.value,
  };
  colorFolder
    .addColor(colorParams, "color")
    .name("汽车颜色")
    .onChange((value) => {
      changeCarColor(value);
    });

  // 相机控制
  const cameraFolder = gui.addFolder("相机控制");
  cameraFolder.add(sketch.camera.position, "x", -20, 20).name("相机X");
  cameraFolder.add(sketch.camera.position, "y", -20, 20).name("相机Y");
  cameraFolder.add(sketch.camera.position, "z", -20, 20).name("相机Z");

  // 光照控制
  const lightFolder = gui.addFolder("光照控制");
  const ambientLight = sketch.scene.children.find(
    (child) => child.type === "AmbientLight"
  );
  if (ambientLight) {
    lightFolder.add(ambientLight, "intensity", 0, 1).name("环境光强度");
  }

  const directionalLight = sketch.scene.children.find(
    (child) => child.type === "DirectionalLight" && child.position.x > 0
  );
  if (directionalLight) {
    lightFolder.add(directionalLight, "intensity", 0, 2).name("主光源强度");
    lightFolder.add(directionalLight.position, "x", -20, 20).name("主光源X");
    lightFolder.add(directionalLight.position, "y", -20, 20).name("主光源Y");
    lightFolder.add(directionalLight.position, "z", -20, 20).name("主光源Z");
  }

  const fillLight = sketch.scene.children.find(
    (child) => child.type === "DirectionalLight" && child.position.x < 0
  );
  if (fillLight) {
    lightFolder.add(fillLight, "intensity", 0, 1).name("补光强度");
    lightFolder.add(fillLight.position, "x", -20, 20).name("补光X");
    lightFolder.add(fillLight.position, "y", -20, 20).name("补光Y");
    lightFolder.add(fillLight.position, "z", -20, 20).name("补光Z");
  }

  // 背景颜色控制
  const backgroundFolder = gui.addFolder("背景控制");
  const backgroundParams = {
    color: "#87ceeb",
  };
  backgroundFolder
    .addColor(backgroundParams, "color")
    .onChange((value) => {
      sketch.scene.background = new THREE.Color(value);
    })
    .name("背景颜色");

  // 地面颜色控制
  const groundFolder = gui.addFolder("地面控制");
  const ground = sketch.scene.children.find(
    (child) => child.geometry && child.geometry.type === "PlaneGeometry"
  );
  if (ground) {
    const groundParams = {
      color: "#90EE90",
    };
    groundFolder
      .addColor(groundParams, "color")
      .onChange((value) => {
        ground.material.color = new THREE.Color(value);
      })
      .name("地面颜色");
  }
};
</script>

<style scoped>
.car-model-container {
  width: 100%;
  height: 100vh;
  overflow: hidden;
  position: relative;
}

.loading-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
  transition: opacity 0.5s ease-out;
}

.loading-content {
  text-align: center;
  color: white;
}

.loading-spinner {
  width: 60px;
  height: 60px;
  border: 4px solid rgba(255, 255, 255, 0.3);
  border-top: 4px solid white;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: 0 auto 20px;
}

.loading-text {
  font-size: 18px;
  font-weight: 500;
  margin-bottom: 10px;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}

.loading-progress {
  font-size: 14px;
  opacity: 0.8;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}

/* 颜色选择器样式 */
.color-picker {
  position: absolute;
  top: 20px;
  right: 20px;
  background: rgba(0, 0, 0, 0.8);
  padding: 15px;
  border-radius: 10px;
  backdrop-filter: blur(10px);
  z-index: 1001;
  min-width: 200px;
}

.color-picker-title {
  color: white;
  font-size: 14px;
  font-weight: 500;
  margin-bottom: 10px;
  text-align: center;
}

.color-options {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.color-option {
  width: 40px;
  height: 40px;
  border-radius: 8px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid transparent;
  transition: all 0.3s ease;
  position: relative;
}

.color-option:hover {
  transform: scale(1.1);
  border-color: white;
}

.color-option.active {
  border-color: #fff;
  box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
}

.color-name {
  font-size: 10px;
  color: white;
  text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
  font-weight: bold;
  text-align: center;
  line-height: 1;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

/* 当加载完成时,淡出loading页面 */
.loading-overlay.fade-out {
  opacity: 0;
  pointer-events: none;
}
</style>

在这里插入图片描述

DLC 让车轮滚起来

      if (carModel) {
        // carModel.rotation.y += 0.005;
        if (wheelMeshes && wheelMeshes.length > 0) {
          wheelMeshes.forEach((wheel) => {
            // 轮子围绕其局部 Z 轴旋转以实现前进效果
            wheel.rotation.z += 0.1; // 调整旋转速度
          });
        }
      }
...
        carModel.traverse((child) => {
          if (child.isMesh) {
            child.castShadow = true;
            child.receiveShadow = true;
            carMeshes.push(child);
            // 识别轮子并添加到 sketch.wheelMeshes 数组
            if (child.name.toLowerCase().includes("wheel")) {
              // 假设轮子名称包含 'wheel'
              wheelMeshes.push(child);
            }
          }
        });

其他功能没有了,因为我的AI已经rate Limit 了,哭惹。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值