代码改变世界

实用指南:Threejs案例实践笔记

2025-09-21 21:41  tlnshuju  阅读(18)  评论(0)    收藏  举报

1. 圣诞贺卡

1.1. 项目搭建

  • 安装库

    1. 安装threejs

      npm install three 或 yarn add three 或 pnpm install three
    2. 安装gsap

      npm install gsap 或 yarn add gsap 或 pnpm install gsap
  • 项目初始化

    准备:

    1. 准备场景模型scene.glb,并放置相应目录

    2. 准备解压器文件夹draco,并放置相应目录

    3. 初始代码

      • 导入依赖,定义变量关联元素

        import { ref, onMounted
        } from 'vue'
        import * as THREE from "three"
        import gsap from 'gsap' // 导入动画gsap
        import { OrbitControls
        } from "three/examples/jsm/controls/OrbitControls.js" // 轨道控制器
        import { DRACOLoader
        } from 'three/examples/jsm/loaders/DRACOLoader.js' // 导入Draco加载器
        import { GLTFLoader
        } from 'three/examples/jsm/loaders/GLTFLoader.js' // 导入gltf加载器
        const demo = ref(null)
      • 静态页面配置

        <template>
          <div ref="demo" />
            <
            /template>
            <style scoped>
              * {
              margin: 0;
              padding: 0;
              }
              canvas {
              width: 100%;
              height: 100%;
              position: fixed;
              left: 0;
              top: 0;
              }
              <
              /style>
      • threejs初始化配置

        // 1. --- 创建场景 ---
        const scene = new THREE.Scene()
        // 2. --- 创建相机 ---
        const camera = new THREE.PerspectiveCamera(
        75,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
        )
        camera.position.set(-3.23, 2.98, 4) // 相机位置
        camera.updateProjectionMatrix()
        // 3. --- 创建渲染器 ---
        const renderer = new THREE.WebGLRenderer({
        antialias: true, // 开启抗锯齿
        })
        renderer.setSize(window.innerWidth, window.innerHeight)
        demo.value.appendChild(renderer.domElement)
        // 4. --- 轨道控制器 ---
        const controls = new OrbitControls(camera, renderer.domElement)
        controls.enableDamping = true // 设置阻尼
        controls.dampingFactor = 0.05
        // 5. --- 模型加载 ---
        const dracoLoader = new DRACOLoader() // 实例化draco加载器
        dracoLoader.setDecoderPath("./draco/") // 设置draco解压器路径
        const gltfLoader = new GLTFLoader() // 实例化gltf加载器
        gltfLoader.setDRACOLoader(dracoLoader) // gltf加载器关联draco解压器
        gltfLoader.load(
        "../public/model/scene.glb",
        (gltf) => scene.add(gltf.scene)
        )
        // 6. --- 添加光源 ---
        const light = new THREE.DirectionalLight(0xffffff, 1)
        light.position.set(0, 50, 0)
        scene.add(light)
        // 7. --- 渲染 ---
        const render = () =>
        {
        requestAnimationFrame(render) // 请求动画帧
        renderer.render(scene, camera)
        controls.update()
        }
        render()
        // 窗口大小调整
        window.addEventListener("resize", () =>
        {
        renderer.setSize(window.innerWidth, window.innerHeight)
        camera.aspect = window.innerWidth / window.innerHeight
        camera.updateProjectionMatrix()
        })
        onMounted(() =>
        {
        demo.value.appendChild(renderer.domElement)
        })

        效果图:

        在这里插入图片描述

1.2. 水面与天空

  • 准备环境纹理贴图sky.hdr,并放置相应目录

  • 加载环境纹理

    import { RGBELoader
    } from 'three/examples/jsm/Addons.js' // 导入加载器
    ...
    // 加载环境纹理
    const rgbeLoader = new RGBELoader()
    rgbeLoader.load(
    '/texture/hdr/sky.hdr',
    (texture) =>
    {
    texture.mapping = THREE.EquirectangularReflectionMapping // 设置纹理映射
    scene.background = texture
    scene.environment = texture
    },
    )
    // 设置色调映射
    renderer.toneMapping = THREE.ACESFilmicToneMapping
    controls.enableDamping = true

    效果图:

    在这里插入图片描述

  • 设置水面效果

    import { Water
    } from 'three/examples/jsm/objects/Water2.js' // 导入自带水面库
    ...
    // 设置水面效果
    const waterGeometry = new THREE.CircleGeometry(300, 32)
    const water = new Water(waterGeometry, {
    textureWidth: 1024,
    textureHeight: 1024,
    color: 0xeeeeff,
    flowDirection: new THREE.Vector2(1, 1), // waves direction
    scale: 100, // wavesize
    })
    water.rotation.x = -Math.PI / 2
    water.position.y = -0.4
    scene.add(water)

    效果图:

    在这里插入图片描述

1.3. 场景光源

  • 给小木屋添加点光源

    注意开启阴影要对渲染器、物体、光源分别进行设置

    // 设置色调映射
    ...
    renderer.shadowMap.enabled = true;
    // 1.渲染器开启阴影
    renderer.physicallyCorrectLights = true;
    ...
    gltfLoader.load("/model/scene.glb",
    (gltf) =>
    {
    const model = gltf.scene
    model.traverse((child) =>
    {
    ...
    // 2.物体允许接收后投射阴影
    if (child.isMesh) {
    child.castShadow = true;
    child.receiveShadow = true;
    }
    })
    ...
    }
    )
    // 点光源
    const pointLight = new THREE.PointLight(0xffffff, 10)
    pointLight.position.set(0.1, 2.4, 0)
    pointLight.castShadow = true // 3.点光源开启阴影
    scene.add(pointLight)

    效果图:

    在这里插入图片描述

  • 循环创建发光小球并绑定点光源,组成点光源组

    // 创建点光源
    const pointLight = new THREE.PointLight(0xffffff, 50)
    pointLight.position.set(0.1, 2.4, 0)
    pointLight.castShadow = true
    // 创建点光源组
    const pointLightGroup = new THREE.Group()
    pointLightGroup.position.set(-8, 2.5, -1.5)
    const radius = 3
    const pointLightList = []
    for (let i = 0; i <
    3; i++) {
    // 创建球体用于绑定光源 
    const sphereGeometry = new THREE.SphereGeometry(0.2, 32, 32)
    const sphereMaterial = new THREE.MeshStandardMaterial({
    color: 0xffffff,
    emissive: 0xffffff,
    emissiveIntensity: 10 // 亮度
    })
    const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
    pointLightList.push(sphere);
    sphere.position.set(
    radius * Math.cos(i * 2 * Math.PI / 3),
    Math.cos(i * 2 * Math.PI / 3),
    radius * Math.sin(i * 2 * Math.PI / 3),
    )
    const pointLight = new THREE.PointLight(0xffffff, 50)
    sphere.add(pointLight) // 绑定点光源
    pointLightGroup.add(sphere)
    }
    scene.add(pointLightGroup)

    这里来解释这段代码:

    • 代码:radius * Math.cos(i * 2 * Math.PI / 3)

      圆为2π( 即2 * Math.PI),我们有三个小球,那么每个小球就应该相隔 (2 / 3)π, 那么每个小球的位置应该是i * (2 /3)π,即分别为0,(2 / 3)π ,(4 /3)* π

    • pointLightList是干啥用的?:

      这里只是往pointLightList添加小球,小球绑定了点光源,那么后面的gsap动画就直接操作小球就可以了(这里threejs应该在底层做了优化, 不然 pointLightList.push(sphere),应该写在最后一行的)

      在这里插入图片描述

  • 使点光源组运动

    // 使用补间函数,从0到2π,使小球旋转运动
    const options = {
    angle: 0,
    };
    gsap.to(options, {
    angle: Math.PI * 2,
    duration: 10,
    repeat: -1,
    ease: "linear", // 这里要用双引号,不然要报错
    onUpdate: () =>
    {
    pointLightGroup.rotation.y = options.angle;
    // 让球上下摆动
    pointLightList.forEach((item, index) =>
    {
    item.position.set(
    radius * Math.cos(index * 2 * Math.PI / 3),
    Math.cos(index * 2 * Math.PI / 3 + options.angle * 5),
    radius * Math.sin(index * 2 * Math.PI / 3)
    )
    });
    },
    })

    效果图:

    光源组运动

1.4. 相机与文字

  • 监听鼠标滚轮事件

    const index = ref(0)
    window.addEventListener('wheel', (e) =>
    {
    if (e.deltaY >
    0) {
    index.value++
    if (index.value >= scenes.length) {
    index.value = 0
    }
    }
    scenes[index.value].callback()
    },
    false
    )
  • 利用数组存储数据即交互逻辑

    const scenes = [
    {
    text: '《认真的白学》',
    callback: () =>
    {
    // 切换对应位置
    translateCamera(
    new THREE.Vector3(-3.23, 3, 4.06),
    new THREE.Vector3(-8, 2, 0)
    )
    console.log(111);
    }
    }, {
    text: '学下得那么深',
    callback: () =>
    {
    // 切换对应位置
    translateCamera(
    new THREE.Vector3(7, 0, 23),
    new THREE.Vector3(0, 0, 0)
    )
    console.log(222);
    }
    }, {
    text: '学得那么认真',
    callback: () =>
    {
    // 切换对应位置
    translateCamera(
    new THREE.Vector3(10, 3, 0),
    new THREE.Vector3(5, 2, 0)
    )
    console.log(333);
    }
    }, {
    text: '倒映出我躺在学中的伤痕',
    callback: () =>
    {
    // 切换对应位置
    translateCamera(
    new THREE.Vector3(7, 0, 23),
    new THREE.Vector3(0, 0, 0)
    )
    console.log(444);
    }
    }, {
    text: '完',
    callback: () =>
    {
    // 切换对应位置
    translateCamera(
    new THREE.Vector3(-20, 1.3, 6.6),
    new THREE.Vector3(5, 2, 0)
    )
    console.log(555);
    }
    }
    ]
  • 微调一下结构样式,添加相应文字

    <template>
      <div ref="demo">
        <h1 style="padding: 100px 50px; font-size: 50px; color: #fff" class=
        "text">
        {
        { scenes[index].text
        }
        }
        <
        /h1>
        <
        /div>
        <
        /template>
        <style scoped>
          * {
          margin: 0;
          padding: 0;
          }
          canvas {
          width: 100%;
          height: 100%;
          position: fixed;
          left: 0;
          top: 0;
          z-index: 1;
          }
          .text {
          position: fixed;
          z-index: 2;
          }
          <
          /style>
  • 使用补间动画移动相机

    let timeLine1 = gsap.timeline()
    let timeLine2 = gsap.timeline()
    // 相机的移动(position:移动的目标位置, target:看向的位置)
    const translateCamera = (position, target) =>
    {
    timeLine1.to(camera.position, {
    x: position.x,
    y: position.y,
    z: position.z,
    duration: 1,
    ease: "power2.inOut",
    })
    timeLine2.to(controls.target, {
    x: target.x,
    y: target.y,
    z: target.z,
    duration: 1,
    ease: "power2.inOut",
    })
    }

1.5. 星与心

  • 创建星实例并随即分布到天上

    // 实例化满天星
    const starsInstance = new THREE.InstancedMesh(
    new THREE.SphereGeometry(0.1, 32, 32),
    sphereMaterial,
    100
    )
    // 随机分布到天上
    const starsArr = []
    const endArr = []
    for (let i = 0; i <
    100; i++) {
    const x = (Math.random() - 0.5) * 100
    const y = (Math.random() - 0.5) * 100
    const z = (Math.random() - 0.5) * 100
    starsArr.push(new THREE.Vector3(x, y, z))
    const matrix = new THREE.Matrix4()
    matrix.setPosition(x, y, z)
    starsInstance.setMatrixAt(i, matrix)
    }
    scene.add(starsInstance)
  • 创建爱心路径并获取点

    // 创建爱心路径
    const heartPath = new THREE.Shape()
    heartPath.moveTo(25, 25)
    heartPath.bezierCurveTo(25, 25, 20, 0, 0, 0)
    heartPath.bezierCurveTo(-30, 0, -30, 35, -30, 35)
    heartPath.bezierCurveTo(-30, 55, -10, 77, 25, 95)
    heartPath.bezierCurveTo(60, 77, 80, 55, 80, 35)
    heartPath.bezierCurveTo(80, 35, 80, 0, 50, 0)
    heartPath.bezierCurveTo(35, 0, 25, 25, 25, 25)
    // 根据爱心路径获取点
    const center = new THREE.Vector3(0, 2, 10)
    for (let i = 0; i <
    100; i++) {
    const point = heartPath.getPoint(i / 100)
    endArr.push(new THREE.Vector3(
    point.x * 0.1 + center.x,
    point.y * 0.1 + center.y,
    center.z))
    }
  • 创建满天星和爱心的切换动画

    // 创建爱心动画
    const makeHeart = () =>
    {
    const params = {
    time: 0
    }
    gsap.to(params, {
    time: 1,
    duration: 1,
    onUpdate: () =>
    {
    for (let i = 0; i <
    100; i++) {
    const x = starsArr[i].x + (endArr[i].x - starsArr[i].x) * params.time
    const y = starsArr[i].y + (endArr[i].y - starsArr[i].y) * params.time
    const z = starsArr[i].z + (endArr[i].z - starsArr[i].z) * params.time
    const matrix = new THREE.Matrix4()
    matrix.setPosition(x, y, z)
    starsInstance.setMatrixAt(i, matrix)
    }
    starsInstance.instanceMatrix.needsUpdate = true
    }
    })
    }
    // 爱心复原
    const restoreHeart = () =>
    {
    const params = {
    time: 0,
    }
    gsap.to(params, {
    time: 1,
    duration: 1,
    onUpdate: () =>
    {
    for (let i = 0; i <
    100; i++) {
    const x = endArr[i].x + (starsArr[i].x - endArr[i].x) * params.time
    const y = endArr[i].y + (starsArr[i].y - endArr[i].y) * params.time
    const z = endArr[i].z + (starsArr[i].z - endArr[i].z) * params.time
    const matrix = new THREE.Matrix4()
    matrix.setPosition(x, y, z)
    starsInstance.setMatrixAt(i, matrix)
    }
    starsInstance.instanceMatrix.needsUpdate = true
    }
    })
    }

1.6. 代码奉上

  • 代码差异
    1. 资源路径可能不同,按照实际路径修改几款
    2. 部分静态页面实现不同,我的文字使用固定定位和层级处理,切换相机没有动画
    3. 我的水面代码好像有问题,没有波光粼粼的感觉

1.6.1. 最终效果

1.6.2. 我的代码

<script setup>
  import { ref, onMounted
  } from 'vue'
  import * as THREE from "three"
  import gsap from 'gsap' // 导入动画gsap
  import { OrbitControls
  } from "three/examples/jsm/controls/OrbitControls.js" // 轨道控制器
  import { DRACOLoader
  } from 'three/examples/jsm/loaders/DRACOLoader.js' // 导入Draco加载器
  import { GLTFLoader
  } from 'three/examples/jsm/loaders/GLTFLoader.js' // 导入gltf加载器
  import { RGBELoader
  } from 'three/examples/jsm/loaders/RGBELoader'
  import { Water
  } from 'three/examples/jsm/objects/Water2.js'
  const demo = ref(null)
  // 1. --- 创建场景 ---
  const scene = new THREE.Scene()
  // 2. --- 创建相机 ---
  const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
  )
  camera.position.set(-3.23, 2.98, 4) // 相机位置
  camera.updateProjectionMatrix()
  // 3. --- 创建渲染器 ---
  const renderer = new THREE.WebGLRenderer({
  antialias: true, // 开启抗锯齿
  })
  renderer.setSize(window.innerWidth, window.innerHeight)
  // 设置色调映射
  renderer.outputEncoding = THREE.sRGBEncoding;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 0.5;
  renderer.shadowMap.enabled = true;
  // 渲染器开启阴影
  renderer.physicallyCorrectLights = true;
  // 4. --- 轨道控制器 ---
  const controls = new OrbitControls(camera, renderer.domElement)
  // controls.enableDamping = true // 设置阻尼
  // 5. --- 模型加载 ---
  const dracoLoader = new DRACOLoader() // 实例化draco加载器
  dracoLoader.setDecoderPath("./draco/") // 设置draco解压器路径
  const gltfLoader = new GLTFLoader() // 实例化gltf加载器
  gltfLoader.setDRACOLoader(dracoLoader) // gltf加载器关联draco解压器
  gltfLoader.load("/model/scene.glb",
  (gltf) =>
  {
  const model = gltf.scene
  model.traverse((child) =>
  {
  if (child.name === "Plane") {
  child.visible = false
  }
  // 物体允许接收后投射阴影
  if (child.isMesh) {
  child.castShadow = true;
  child.receiveShadow = true;
  }
  })
  scene.add(model)
  }
  )
  // 加载环境纹理
  const rgbeLoader = new RGBELoader()
  rgbeLoader.load(
  '/texture/hdr/sky.hdr',
  (texture) =>
  {
  texture.mapping = THREE.EquirectangularReflectionMapping // 设置纹理映射
  scene.background = texture
  scene.environment = texture
  },
  )
  // 设置水面效果
  const waterGeometry = new THREE.CircleGeometry(300, 32)
  const water = new Water(waterGeometry, {
  textureWidth: 1024,
  textureHeight: 1024,
  color: 0xeeeeff,
  flowDirection: new THREE.Vector2(1, 1), // waves direction
  scale: 100, // wavesize
  })
  water.rotation.x = -Math.PI / 2
  water.position.y = -0.4
  scene.add(water)
  // 6. --- 添加光源 ---
  const light = new THREE.DirectionalLight(0xffffff, 1)
  light.position.set(0, 50, 0)
  scene.add(light)
  // 创建点光源
  const pointLight = new THREE.PointLight(0xffffff, 50)
  pointLight.position.set(0.1, 2.4, 0)
  pointLight.castShadow = true
  // 创建点光源组
  const pointLightGroup = new THREE.Group()
  pointLightGroup.position.set(-8, 2.5, -1.5)
  const radius = 3
  const pointLightList = []
  // 创建球体用于绑定光源 
  const sphereGeometry = new THREE.SphereGeometry(0.2, 32, 32)
  const sphereMaterial = new THREE.MeshStandardMaterial({
  color: 0xffffff,
  emissive: 0xffffff,
  emissiveIntensity: 10 // 亮度
  })
  for (let i = 0; i <
  3; i++) {
  const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
  pointLightList.push(sphere);
  sphere.position.set(
  radius * Math.cos(i * 2 * Math.PI / 3),
  Math.cos(i * 2 * Math.PI / 3),
  radius * Math.sin(i * 2 * Math.PI / 3),
  )
  const pointLight = new THREE.PointLight(0xffffff, 50)
  sphere.add(pointLight) // 绑定点光源
  pointLightGroup.add(sphere)
  }
  scene.add(pointLightGroup)
  // 使用补间函数,从0到2π,使小球旋转运动
  const options = {
  angle: 0,
  };
  gsap.to(options, {
  angle: Math.PI * 2,
  duration: 10,
  repeat: -1,
  ease: "linear", // 这里要用双引号,不然要报错
  onUpdate: () =>
  {
  pointLightGroup.rotation.y = options.angle;
  // 让球上下摆动
  pointLightList.forEach((item, index) =>
  {
  item.position.set(
  radius * Math.cos(index * 2 * Math.PI / 3),
  Math.cos(index * 2 * Math.PI / 3 + options.angle * 5),
  radius * Math.sin(index * 2 * Math.PI / 3)
  )
  });
  },
  })
  // 7. --- 渲染 ---
  const render = () =>
  {
  requestAnimationFrame(render) // 请求动画帧
  renderer.render(scene, camera)
  controls.update()
  }
  render()
  // 数组存储数据即交互逻辑
  const scenes = [
  {
  text: '《认真的白学》',
  callback: () =>
  {
  // 切换对应位置
  translateCamera(
  new THREE.Vector3(-3.23, 3, 4.06),
  new THREE.Vector3(-8, 2, 0)
  )
  }
  }, {
  text: '学下得那么深',
  callback: () =>
  {
  // 切换对应位置
  translateCamera(
  new THREE.Vector3(7, 0, 23),
  new THREE.Vector3(0, 0, 0)
  )
  }
  }, {
  text: '学得那么认真',
  callback: () =>
  {
  // 切换对应位置
  translateCamera(
  new THREE.Vector3(10, 3, 0),
  new THREE.Vector3(5, 2, 0)
  )
  }
  }, {
  text: '倒映出我躺在学中的伤痕',
  callback: () =>
  {
  // 切换对应位置
  translateCamera(
  new THREE.Vector3(7, 0, 23),
  new THREE.Vector3(0, 0, 0)
  )
  // 爱心
  makeHeart()
  }
  }, {
  text: '完',
  callback: () =>
  {
  // 切换对应位置
  translateCamera(
  new THREE.Vector3(-20, 1.3, 6.6),
  new THREE.Vector3(5, 2, 0)
  )
  }
  }
  ]
  const index = ref(0)
  // 监听鼠标滚轮事件
  window.addEventListener('wheel', (e) =>
  {
  if (e.deltaY >
  0) {
  index.value++
  if (index.value >= scenes.length) {
  index.value = 0
  restoreHeart() // 复原
  }
  }
  scenes[index.value].callback()
  },
  false
  )
  // 使用补间动画移动相机
  let timeLine1 = gsap.timeline()
  let timeLine2 = gsap.timeline()
  // 相机的移动(position:移动的目标位置, target:看向的位置)
  const translateCamera = (position, target) =>
  {
  timeLine1.to(camera.position, {
  x: position.x,
  y: position.y,
  z: position.z,
  duration: 1,
  ease: "power2.inOut",
  })
  timeLine2.to(controls.target, {
  x: target.x,
  y: target.y,
  z: target.z,
  duration: 1,
  ease: "power2.inOut",
  })
  }
  console.log(scenes);
  // 实例化满天星
  const starsInstance = new THREE.InstancedMesh(
  new THREE.SphereGeometry(0.1, 32, 32),
  sphereMaterial,
  100
  )
  // 随机分布到天上
  const starsArr = []
  const endArr = []
  for (let i = 0; i <
  100; i++) {
  const x = (Math.random() - 0.5) * 100
  const y = (Math.random() - 0.5) * 100
  const z = (Math.random() - 0.5) * 100
  starsArr.push(new THREE.Vector3(x, y, z))
  const matrix = new THREE.Matrix4()
  matrix.setPosition(x, y, z)
  starsInstance.setMatrixAt(i, matrix)
  }
  scene.add(starsInstance)
  // 创建爱心路径
  const heartPath = new THREE.Shape()
  heartPath.moveTo(25, 25)
  heartPath.bezierCurveTo(25, 25, 20, 0, 0, 0)
  heartPath.bezierCurveTo(-30, 0, -30, 35, -30, 35)
  heartPath.bezierCurveTo(-30, 55, -10, 77, 25, 95)
  heartPath.bezierCurveTo(60, 77, 80, 55, 80, 35)
  heartPath.bezierCurveTo(80, 35, 80, 0, 50, 0)
  heartPath.bezierCurveTo(35, 0, 25, 25, 25, 25)
  // 根据爱心路径获取点
  const center = new THREE.Vector3(0, 2, 10)
  for (let i = 0; i <
  100; i++) {
  const point = heartPath.getPoint(i / 100)
  endArr.push(new THREE.Vector3(
  point.x * 0.1 + center.x,
  point.y * 0.1 + center.y,
  center.z))
  }
  // 创建爱心动画
  const makeHeart = () =>
  {
  const params = {
  time: 0
  }
  gsap.to(params, {
  time: 1,
  duration: 1,
  onUpdate: () =>
  {
  for (let i = 0; i <
  100; i++) {
  const x = starsArr[i].x + (endArr[i].x - starsArr[i].x) * params.time
  const y = starsArr[i].y + (endArr[i].y - starsArr[i].y) * params.time
  const z = starsArr[i].z + (endArr[i].z - starsArr[i].z) * params.time
  const matrix = new THREE.Matrix4()
  matrix.setPosition(x, y, z)
  starsInstance.setMatrixAt(i, matrix)
  }
  starsInstance.instanceMatrix.needsUpdate = true
  }
  }
  )
  }
  // 爱心复原
  const restoreHeart = () =>
  {
  const params = {
  time: 0,
  };
  gsap.to(params,{
  time: 1,
  duration: 1,
  onUpdate: () =>
  {
  for (let i = 0; i <
  100; i++) {
  const x = starsArr[i].x + (starsArr[i].x - endArr[i].x) * params.time
  const y = starsArr[i].y + (starsArr[i].y - endArr[i].y) * params.time
  const z = starsArr[i].z + (starsArr[i].z - endArr[i].z) * params.time
  const matrix = new THREE.Matrix4()
  matrix.setPosition(x, y, z)
  starsInstance.setMatrixAt(i, matrix)
  }
  starsInstance.instanceMatrix.needsUpdate = true
  }
  })
  }
  onMounted(() =>
  {
  demo.value.appendChild(renderer.domElement)
  })
  <
  /script>
  <template>
    <div ref="demo">
      <h1 style="padding: 100px 50px; font-size: 50px; color: #fff" class=
      "text">
      {
      { scenes[index].text
      }
      }
      <
      /h1>
      <
      /div>
      <
      /template>
      <style scoped>
        * {
        margin: 0;
        padding: 0;
        }
        canvas {
        width: 100%;
        height: 100%;
        position: fixed;
        left: 0;
        top: 0;
        z-index: 1;
        }
        .text {
        position: fixed;
        z-index: 2;
        }
        <
        /style>

1.6.3. 老陈打码

<template>
  <div class=
  "scenes" style="
  position: fixed;
  left: 0;
  top: 0;
  z-index: 10;
  pointer-events: none;
  transition: all 1s;
  " :style="{
  transform: `translate3d(0, ${-index * 100
  }vh, 0)`,
  }">
  <div v-for="item in scenes" style="width: 100vw; height: 100vh">
    <h1 style="padding: 100px 50px; font-size: 50px; color: #fff">
      {
      { item.text
      }
      }
      <
      /h1>
      <
      /div>
      <
      /div>
      <
      /template>
      <script setup>
        import * as THREE from "three";
        import { OrbitControls
        } from "three/examples/jsm/controls/OrbitControls";
        import { GLTFLoader
        } from "three/examples/jsm/loaders/GLTFLoader";
        import { DRACOLoader
        } from "three/examples/jsm/loaders/DRACOLoader";
        import { RGBELoader
        } from "three/examples/jsm/loaders/RGBELoader";
        import { Water
        } from "three/examples/jsm/objects/Water2";
        import gsap from "gsap";
        import { ref
        } from "vue";
        // 初始化场景
        const scene = new THREE.Scene();
        // 初始化相机
        const camera = new THREE.PerspectiveCamera(
        75,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
        );
        camera.position.set(-3.23, 2.98, 4.06);
        camera.updateProjectionMatrix();
        // 初始化渲染器
        const renderer = new THREE.WebGLRenderer({
        // 设置抗锯齿
        antialias: true,
        });
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);
        // 设置色调映射
        renderer.outputEncoding = THREE.sRGBEncoding;
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.toneMappingExposure = 0.5;
        renderer.shadowMap.enabled = true;
        renderer.physicallyCorrectLights = true;
        // 设置水面效果
        // 初始化控制器
        const controls = new OrbitControls(camera, renderer.domElement);
        controls.target.set(-8, 2, 0);
        controls.enableDamping = true;
        // 初始化loader
        const dracoLoader = new DRACOLoader();
        dracoLoader.setDecoderPath("./draco/");
        const gltfLoader = new GLTFLoader();
        gltfLoader.setDRACOLoader(dracoLoader);
        // 加载环境纹理
        let rgbeLoader = new RGBELoader();
        rgbeLoader.load("./textures/sky.hdr", (texture) =>
        {
        texture.mapping = THREE.EquirectangularReflectionMapping;
        scene.background = texture;
        scene.environment = texture;
        });
        // 加载模型
        gltfLoader.load("./model/scene.glb", (gltf) =>
        {
        const model = gltf.scene;
        model.traverse((child) =>
        {
        if (child.name === "Plane") {
        child.visible = false;
        }
        if (child.isMesh) {
        child.castShadow = true;
        child.receiveShadow = true;
        }
        });
        scene.add(model);
        });
        // 创建水面
        const waterGeometry = new THREE.CircleGeometry(300, 32);
        const water = new Water(waterGeometry, {
        textureWidth: 1024,
        textureHeight: 1024,
        color: 0xeeeeff,
        flowDirection: new THREE.Vector2(1, 1),
        scale: 100,
        });
        water.rotation.x = -Math.PI / 2;
        water.position.y = -0.4;
        scene.add(water);
        // 添加平行光
        const light = new THREE.DirectionalLight(0xffffff, 1);
        light.position.set(0, 50, 0);
        scene.add(light);
        // 添加点光源
        const pointLight = new THREE.PointLight(0xffffff, 50);
        pointLight.position.set(0.1, 2.4, 0);
        pointLight.castShadow = true;
        scene.add(pointLight);
        // 创建点光源组
        const pointLightGroup = new THREE.Group();
        pointLightGroup.position.set(-8, 2.5, -1.5);
        let radius = 3;
        let pointLightArr = [];
        for (let i = 0; i <
        3; i++) {
        // 创建球体当灯泡
        const sphereGeometry = new THREE.SphereGeometry(0.2, 32, 32);
        const sphereMaterial = new THREE.MeshStandardMaterial({
        color: 0xffffff,
        emissive: 0xffffff,
        emissiveIntensity: 10,
        });
        const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
        pointLightArr.push(sphere);
        sphere.position.set(
        radius * Math.cos((i * 2 * Math.PI) / 3),
        Math.cos((i * 2 * Math.PI) / 3),
        radius * Math.sin((i * 2 * Math.PI) / 3)
        );
        let pointLight = new THREE.PointLight(0xffffff, 50);
        sphere.add(pointLight);
        pointLightGroup.add(sphere);
        }
        scene.add(pointLightGroup);
        // 使用补间函数,从0到2π,使灯泡旋转
        let options = {
        angle: 0,
        };
        gsap.to(options, {
        angle: Math.PI * 2,
        duration: 10,
        repeat: -1,
        ease: "linear",
        onUpdate: () =>
        {
        pointLightGroup.rotation.y = options.angle;
        pointLightArr.forEach((item, index) =>
        {
        item.position.set(
        radius * Math.cos((index * 2 * Math.PI) / 3),
        Math.cos((index * 2 * Math.PI) / 3 + options.angle * 5),
        radius * Math.sin((index * 2 * Math.PI) / 3)
        );
        });
        },
        });
        function render() {
        requestAnimationFrame(render);
        renderer.render(scene, camera);
        controls.update();
        }
        render();
        // 使用补间动画移动相机
        let timeLine1 = gsap.timeline();
        let timeline2 = gsap.timeline();
        // 定义相机移动函数
        function translateCamera(position, target) {
        timeLine1.to(camera.position, {
        x: position.x,
        y: position.y,
        z: position.z,
        duration: 1,
        ease: "power2.inOut",
        });
        timeline2.to(controls.target, {
        x: target.x,
        y: target.y,
        z: target.z,
        duration: 1,
        ease: "power2.inOut",
        });
        }
        let scenes = [
        {
        text: "圣诞快乐",
        callback: () =>
        {
        // 执行函数切换位置
        translateCamera(
        new THREE.Vector3(-3.23, 3, 4.06),
        new THREE.Vector3(-8, 2, 0)
        );
        },
        },
        {
        text: "感谢在这么大的世界里遇见了你",
        callback: () =>
        {
        // 执行函数切
        translateCamera(new THREE.Vector3(7, 0, 23), new THREE.Vector3(0, 0, 0));
        },
        },
        {
        text: "愿与你探寻世界的每一个角落",
        callback: () =>
        {
        // 执行函数切
        translateCamera(new THREE.Vector3(10, 3, 0), new THREE.Vector3(5, 2, 0));
        },
        },
        {
        text: "愿将天上的星星送给你",
        callback: () =>
        {
        // 执行函数切
        translateCamera(new THREE.Vector3(7, 0, 23), new THREE.Vector3(0, 0, 0));
        makeHeart();
        },
        },
        {
        text: "愿疫情结束,大家健康快乐!",
        callback: () =>
        {
        // 执行函数切
        translateCamera(
        new THREE.Vector3(-20, 1.3, 6.6),
        new THREE.Vector3(5, 2, 0)
        );
        },
        },
        ];
        let index = ref(0);
        let isAnimate = false;
        // 监听鼠标滚轮事件
        window.addEventListener(
        "wheel",
        (e) =>
        {
        if (isAnimate) return;
        isAnimate = true;
        if (e.deltaY >
        0) {
        index.value++;
        if (index.value > scenes.length - 1) {
        index.value = 0;
        restoreHeart();
        }
        }
        scenes[index.value].callback();
        setTimeout(() =>
        {
        isAnimate = false;
        }, 1000);
        },
        false
        );
        // 实例化创建漫天星星
        let starsInstance = new THREE.InstancedMesh(
        new THREE.SphereGeometry(0.1, 32, 32),
        new THREE.MeshStandardMaterial({
        color: 0xffffff,
        emissive: 0xffffff,
        emissiveIntensity: 10,
        }),
        100
        );
        // 星星随机到天上
        let starsArr = [];
        let endArr = [];
        for (let i = 0; i <
        100; i++) {
        let x = Math.random() * 100 - 50;
        let y = Math.random() * 100 - 50;
        let z = Math.random() * 100 - 50;
        starsArr.push(new THREE.Vector3(x, y, z));
        let matrix = new THREE.Matrix4();
        matrix.setPosition(x, y, z);
        starsInstance.setMatrixAt(i, matrix);
        }
        scene.add(starsInstance);
        // 创建爱心路径
        let heartShape = new THREE.Shape();
        heartShape.moveTo(25, 25);
        heartShape.bezierCurveTo(25, 25, 20, 0, 0, 0);
        heartShape.bezierCurveTo(-30, 0, -30, 35, -30, 35);
        heartShape.bezierCurveTo(-30, 55, -10, 77, 25, 95);
        heartShape.bezierCurveTo(60, 77, 80, 55, 80, 35);
        heartShape.bezierCurveTo(80, 35, 80, 0, 50, 0);
        heartShape.bezierCurveTo(35, 0, 25, 25, 25, 25);
        // 根据爱心路径获取点
        let center = new THREE.Vector3(0, 2, 10);
        for (let i = 0; i <
        100; i++) {
        let point = heartShape.getPoint(i / 100);
        endArr.push(
        new THREE.Vector3(
        point.x * 0.1 + center.x,
        point.y * 0.1 + center.y,
        center.z
        )
        );
        }
        // 创建爱心动画
        function makeHeart() {
        let params = {
        time: 0,
        };
        gsap.to(params, {
        time: 1,
        duration: 1,
        onUpdate: () =>
        {
        for (let i = 0; i <
        100; i++) {
        let x = starsArr[i].x + (endArr[i].x - starsArr[i].x) * params.time;
        let y = starsArr[i].y + (endArr[i].y - starsArr[i].y) * params.time;
        let z = starsArr[i].z + (endArr[i].z - starsArr[i].z) * params.time;
        let matrix = new THREE.Matrix4();
        matrix.setPosition(x, y, z);
        starsInstance.setMatrixAt(i, matrix);
        }
        starsInstance.instanceMatrix.needsUpdate = true;
        },
        });
        }
        function restoreHeart() {
        let params = {
        time: 0,
        };
        gsap.to(params, {
        time: 1,
        duration: 1,
        onUpdate: () =>
        {
        for (let i = 0; i <
        100; i++) {
        let x = endArr[i].x + (starsArr[i].x - endArr[i].x) * params.time;
        let y = endArr[i].y + (starsArr[i].y - endArr[i].y) * params.time;
        let z = endArr[i].z + (starsArr[i].z - endArr[i].z) * params.time;
        let matrix = new THREE.Matrix4();
        matrix.setPosition(x, y, z);
        starsInstance.setMatrixAt(i, matrix);
        }
        starsInstance.instanceMatrix.needsUpdate = true;
        },
        });
        }
        <
        /script>
        <style>
          * {
          margin: 0;
          padding: 0;
          }
          canvas {
          width: 100vw;
          height: 100vh;
          position: fixed;
          left: 0;
          top: 0;
          }
          <
          /style>

2. 汽车展示

2.1. 项目搭建

  • 基础代码

    <script setup>
      import { ref, onMounted
      } from 'vue'
      import * as THREE from "three"
      import { OrbitControls
      } from "three/examples/jsm/controls/OrbitControls.js" // 轨道控制器
      const demo = ref(null)
      // 1. --- 创建场景 ---
      const scene = new THREE.Scene()
      // 2. --- 创建相机 ---
      const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
      )
      camera.position.set(0, 2, 6) // 相机位置
      camera.updateProjectionMatrix() // 更新投影矩阵
      // 3. --- 创建渲染器 ---
      const renderer = new THREE.WebGLRenderer({
      antialias: true, // 开启抗锯齿
      })
      renderer.setSize(window.innerWidth, window.innerHeight)
      // 渲染函数
      const render = () =>
      {
      renderer.render(scene, camera)
      controls && controls.update()
      requestAnimationFrame(render) // 渲染下一帧
      }
      // 挂载到dom中
      onMounted(() =>
      {
      demo.value.appendChild(renderer.domElement) // 将渲染器添加到场景中
      renderer.setClearColor("#000")
      scene.background = new THREE.Color("#ccc")
      scene.environment = new THREE.Color("#ccc")
      const grid = new THREE.GridHelper(10, 10)// 添加网格地面 
      grid.material.transparent = true
      grid.material.opacity = 0.2
      scene.add(grid)
      render() // 调用渲染函数
      })
      // 4. --- 添加控制器 ---
      const controls = new OrbitControls(camera, renderer.domElement)
      controls.update()
      // 5. ---窗口大小调整 ---
      window.addEventListener("resize", () =>
      {
      renderer.setSize(window.innerWidth, window.innerHeight)
      camera.aspect = window.innerWidth / window.innerHeight
      camera.updateProjectionMatrix()
      })
      <
      /script>
      <template>
        <div ref="demo">
          <
          /div>
          <
          /template>
          <style scoped>
            * {
            margin: 0;
            padding: 0;
            }
            canvas {
            width: 100%;
            height: 100%;
            position: fixed;
            left: 0;
            top: 0;
            z-index: 1;
            }
            <
            /style>
  • 模型准备

    1. 准备draco文件夹,将其复制到public文件夹下
    2. 准备汽车模型car.glb,放到相应文件夹下

    效果图:

    在这里插入图片描述

2.2. 模型与灯光

  • 模型加载

    import { DRACOLoader
    } from 'three/examples/jsm/loaders/DRACOLoader.js' // 导入Draco加载器
    import { GLTFLoader
    } from 'three/examples/jsm/loaders/GLTFLoader.js' // 导入gltf加载器
    ...
    // 模型载入
    const loader = new GLTFLoader()
    const dracoLoader = new DRACOLoader()
    dracoLoader.setDecoderPath("/draco/") // 设置draco解码器路径
    loader.setDRACOLoader(dracoLoader) // 绑定draco解码器
    loader.load("/model/car.glb", (gltf) =>
    {
    scene.add(gltf.scene)
    })

    效果图:

    在这里插入图片描述

    因为模型为物理材质,需要光才能看见模型颜色

  • 添加灯光

    // 添加灯光
    const light1 = new THREE.DirectionalLight(0xffffff, 1)
    light1.position.set(0, 0, 10)
    const light2 = new THREE.DirectionalLight(0xffffff, 1)
    light2.position.set(0, 0, -10)
    const light3 = new THREE.DirectionalLight(0xffffff, 1)
    light3.position.set(10, 0, 0)
    const light4 = new THREE.DirectionalLight(0xffffff, 1)
    light4.position.set(-10, 0, 0)
    const light5 = new THREE.DirectionalLight(0xffffff, 1)
    light5.position.set(0, 10, 0)
    const light6 = new THREE.DirectionalLight(0xffffff, 1)
    light6.position.set(5, 10, 0)
    const light7 = new THREE.DirectionalLight(0xffffff, 1)
    light7.position.set(0, 10, 5)
    const light8 = new THREE.DirectionalLight(0xffffff, 1)
    light8.position.set(0, 10,-5)
    const light9 = new THREE.DirectionalLight(0xffffff, 1)
    light9.position.set(-5, 10, 0)
    scene.add(light1, light2, light3, light4, light5, light6, light7, light8, light9)

    效果图:

    在这里插入图片描述

2.3. 车身材质

  • 模型组成

    打印gltf.scene,发现汽车是由66个模型组成,接下来我们对这些物体进行设置,来完成对模型的操作

    loader.load("/model/car.glb", (gltf) =>
    {
    scene.add(gltf.scene)
    console.log(gltf);
    })

    在这里插入图片描述

  • 获取模型名字(仅作为参考,最好在3D建模软件导出时就取好名字)

    loader.load("/model/car.glb", (gltf) =>
    {
    const car = gltf.scene
    car.traverse((child) =>
    {
    if (child.isMesh) {
    console.log(child.name);
    }
    })
    scene.add(car)
    })

    在这里插入图片描述

  • 创建变量存储材质

    // - 创建材质 -
    // 车身材质
    const bodyMaterial = new THREE.MeshPhysicalMaterial({
    color: 0xff0000,
    metalness: 1,
    roughness: 0.5,
    clearcoat: 1,
    clearcoatRoughness: 0
    })
    // 前部车身材质
    const frontMaterial = new THREE.MeshPhysicalMaterial({
    color: 0xff0000,
    metalness: 1,
    roughness: 0.5,
    clearcoat: 1,
    clearcoatRoughness: 0
    })
    // 车的轮毂
    const wheelMaterial = new THREE.MeshPhysicalMaterial({
    color: 0xff0000,
    metalness: 1,
    roughness: 0.1,
    })
    // 车玻璃
    const glassMaterial = new THREE.MeshPhysicalMaterial({
    color: 0xffffff,
    metalness: 0,
    roughness: 0,
    transmission: 1,
    transparent: true
    })
    // 引擎盖
    const hoodMaterial = new THREE.MeshPhysicalMaterial({
    color: 0xff0000,
    metalness: 1,
    roughness: 0.5,
    clearcoat: 1,
    clearcoatRoughness: 0
    })
  • 对车的各个部位模型进行赋值

    loader.load("/model/car.glb", (gltf) =>
    {
    const car = gltf.scene
    car.traverse((child) =>
    {
    if (child.isMesh) {
    // 判断是否是轮毂
    if (child.name.includes("轮毂")) {
    child.material = wheelMaterial
    wheels.push(child)
    }
    // 判断是否是车身主体
    else if (child.name.includes("Mesh002")) {
    carBody = child
    carBody.material = bodyMaterial
    }
    // 判断是否是前部车身
    else if (child.name.includes("前脸")) {
    frontCar = child
    frontCar.material = frontMaterial
    }
    // 是否是引擎盖
    else if (child.name.includes("引擎盖_1")) {
    hoodCar = child
    hoodCar.material = hoodMaterial
    }
    // 是否是挡风玻璃
    else if (child.name.includes("挡风玻璃")) {
    glassCar = child
    glassCar.material = glassMaterial
    }
    }
    })
    scene.add(car)
    })

    效果图:

    在这里插入图片描述

2.4. 材质切换

  • 静态页面改变(我用了scss,如果要像我这样使用,请安装相应依赖)

    <template>
      <div ref="demo">
        <div class=
        "menu">
        <div class=
        "title">
        <h1>汽车展示与选配<
          /h1>
          <
          /div>
          <
          !-- 车身颜色 -->
          <h2>请选择车身颜色<
            /h2>
            <div class=
            "select">
            <div class=
            "option" v-for="(item, index) in colors" :key="index" @click="selectColor(index)">
            <div class=
            "option-color" :style="{ background: item }" />
            <
            /div>
            <
            /div>
            <
            !-- 贴膜材质 -->
            <h2>请选择贴膜材质<
              /h2>
              <div class=
              "select">
              <div class=
              "option" v-for="(item, index) in materials" :key="index" @click="selectMaterial(index)">
              <button class=
              "option-material" :style="{ background: item }">
              {
              { item.name
              }
              }<
              /button>
              <
              /div>
              <
              /div>
              <
              /div>
              <
              /div>
              <
              /template>
              <style scoped lang="scss">
                * {
                margin: 0;
                padding: 0;
                }
                canvas {
                width: 100%;
                height: 100%;
                position: fixed;
                left: 0;
                top: 0;
                z-index: 1;
                }
                .menu {
                position: fixed;
                top: 20px;
                right: 20px;
                z-index: 2;
                .select {
                display: inline-block;
                display: flex;
                .option {
                display: flex;
                .option-color {
                width: 30px;
                height: 30px;
                border: 1px solid #ccc;
                margin: 10px;
                display: inline-block;
                cursor: pointer;
                border-radius: 10px;
                }
                }
                }
                }
                <
                /style>
  • 颜色切换

    // 设置颜色数组
    const colors = ["red", "yellow", "blue", "green",
    "orange", "pink", "purple", "white", "black", "gray"]
    ...
    // 颜色选择函数
    const selectColor = (index) =>
    {
    bodyMaterial.color.set(colors[index])
    frontMaterial.color.set(colors[index])
    glassMaterial.color.set(colors[index])
    hoodMaterial.color.set(colors[index])
    wheelMaterial.color.set(colors[index])
    }
  • 贴膜材质切换

    // 设置贴膜数组
    const materials = [
    { name: '磨砂', value: 1
    },
    { name: '冰晶', value: 0
    }
    ]
    ...
    // 材质选择函数
    const selectMaterial = (index) =>
    {
    bodyMaterial.clearcoatRoughness = materials[index].value
    frontMaterial.clearcoatRoughness = materials[index].value
    hoodMaterial.clearcoatRoughness = materials[index].value
    wheelMaterial.clearcoatRoughness = materials[index].value
    }

    效果图:

    • 黄色冰晶

      在这里插入图片描述

    • 绿色磨砂

      在这里插入图片描述

2.5. 源码奉上

2.5.1.我的代码

<script setup>
  import { ref, onMounted
  } from 'vue'
  import * as THREE from "three"
  import { OrbitControls
  } from "three/examples/jsm/controls/OrbitControls.js" // 轨道控制器
  import { DRACOLoader
  } from 'three/examples/jsm/loaders/DRACOLoader.js' // 导入Draco加载器
  import { GLTFLoader
  } from 'three/examples/jsm/loaders/GLTFLoader.js' // 导入gltf加载器
  const demo = ref(null)
  // 1. --- 创建场景 ---
  const scene = new THREE.Scene()
  // - 模型 -
  const wheels = [] // 轮子
  let carBody, // 主体车身
  frontCar, // 前部车身
  hoodCar, // 引擎盖
  glassCar // 挡风玻璃
  // - 创建材质 -
  // 车身材质
  const bodyMaterial = new THREE.MeshPhysicalMaterial({
  color: 0xff0000,
  metalness: 1,
  roughness: 0.5,
  clearcoat: 1,
  clearcoatRoughness: 0
  })
  // 设置颜色数组
  const colors = ["red", "yellow", "blue", "green",
  "orange", "pink", "purple", "white", "black", "gray"]
  // 设置贴膜数组
  const materials = [
  { name: '磨砂', value: 1
  },
  { name: '冰晶', value: 0
  }
  ]
  // 前部车身材质
  const frontMaterial = new THREE.MeshPhysicalMaterial({
  color: 0xff0000,
  metalness: 1,
  roughness: 0.5,
  clearcoat: 1,
  clearcoatRoughness: 0
  })
  // 车的轮毂
  const wheelMaterial = new THREE.MeshPhysicalMaterial({
  color: 0xff0000,
  metalness: 1,
  roughness: 0.1,
  })
  // 车玻璃
  const glassMaterial = new THREE.MeshPhysicalMaterial({
  color: 0xffffff,
  metalness: 0,
  roughness: 0,
  transmission: 1,
  transparent: true
  })
  // 引擎盖
  const hoodMaterial = new THREE.MeshPhysicalMaterial({
  color: 0xff0000,
  metalness: 1,
  roughness: 0.5,
  clearcoat: 1,
  clearcoatRoughness: 0
  })
  // 2. --- 创建相机 ---
  const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
  )
  camera.position.set(0, 2, 6) // 相机位置
  camera.updateProjectionMatrix() // 更新投影矩阵
  // 3. --- 创建渲染器 ---
  const renderer = new THREE.WebGLRenderer({
  antialias: true, // 开启抗锯齿
  })
  renderer.setSize(window.innerWidth, window.innerHeight)
  // 渲染函数
  const render = () =>
  {
  renderer.render(scene, camera)
  controls && controls.update()
  requestAnimationFrame(render) // 渲染下一帧
  }
  // 挂载到dom中
  onMounted(() =>
  {
  demo.value.appendChild(renderer.domElement) // 将渲染器添加到场景中
  renderer.setClearColor("#000")
  scene.background = new THREE.Color("#ccc")
  scene.environment = new THREE.Color("#ccc")
  const grid = new THREE.GridHelper(10, 10)// 添加网格地面 
  grid.material.transparent = true
  grid.material.opacity = 0.2
  scene.add(grid)
  render() // 调用渲染函数
  })
  // 4. --- 添加控制器 ---
  const controls = new OrbitControls(camera, renderer.domElement)
  controls.update()
  // 模型载入
  const loader = new GLTFLoader()
  const dracoLoader = new DRACOLoader()
  dracoLoader.setDecoderPath("/draco/") // 设置draco解码器路径
  loader.setDRACOLoader(dracoLoader) // 绑定draco解码器
  loader.load("/model/car.glb", (gltf) =>
  {
  const car = gltf.scene
  car.traverse((child) =>
  {
  if (child.isMesh) {
  // 判断是否是轮毂
  if (child.name.includes("轮毂")) {
  child.material = wheelMaterial
  wheels.push(child)
  }
  // 判断是否是车身主体
  else if (child.name.includes("Mesh002")) {
  carBody = child
  carBody.material = bodyMaterial
  }
  // 判断是否是前部车身
  else if (child.name.includes("前脸")) {
  frontCar = child
  frontCar.material = frontMaterial
  }
  // 是否是引擎盖
  else if (child.name.includes("引擎盖_1")) {
  hoodCar = child
  hoodCar.material = hoodMaterial
  }
  // 是否是挡风玻璃
  else if (child.name.includes("挡风玻璃")) {
  glassCar = child
  glassCar.material = glassMaterial
  }
  }
  })
  scene.add(car)
  })
  // 添加灯光
  const light1 = new THREE.DirectionalLight(0xffffff, 1)
  light1.position.set(0, 0, 10)
  const light2 = new THREE.DirectionalLight(0xffffff, 1)
  light2.position.set(0, 0, -10)
  const light3 = new THREE.DirectionalLight(0xffffff, 1)
  light3.position.set(10, 0, 0)
  const light4 = new THREE.DirectionalLight(0xffffff, 1)
  light4.position.set(-10, 0, 0)
  const light5 = new THREE.DirectionalLight(0xffffff, 1)
  light5.position.set(0, 10, 0)
  const light6 = new THREE.DirectionalLight(0xffffff, 1)
  light6.position.set(5, 10, 0)
  const light7 = new THREE.DirectionalLight(0xffffff, 1)
  light7.position.set(0, 10, 5)
  const light8 = new THREE.DirectionalLight(0xffffff, 1)
  light8.position.set(0, 10, -5)
  const light9 = new THREE.DirectionalLight(0xffffff, 1)
  light9.position.set(-5, 10, 0)
  scene.add(light1, light2, light3, light4, light5, light6, light7, light8, light9)
  // 5. ---窗口大小调整 
  window.addEventListener("resize", () =>
  {
  renderer.setSize(window.innerWidth, window.innerHeight)
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  })
  // 颜色选择函数
  const selectColor = (index) =>
  {
  bodyMaterial.color.set(colors[index])
  frontMaterial.color.set(colors[index])
  glassMaterial.color.set(colors[index])
  hoodMaterial.color.set(colors[index])
  wheelMaterial.color.set(colors[index])
  }
  // 材质选择函数
  const selectMaterial = (index) =>
  {
  bodyMaterial.clearcoatRoughness = materials[index].value
  frontMaterial.clearcoatRoughness = materials[index].value
  hoodMaterial.clearcoatRoughness = materials[index].value
  wheelMaterial.clearcoatRoughness = materials[index].value
  }
  <
  /script>
  <template>
    <div ref="demo">
      <div class=
      "menu">
      <div class=
      "title">
      <h1>汽车展示与选配<
        /h1>
        <
        /div>
        <
        !-- 车身颜色 -->
        <h2>请选择车身颜色<
          /h2>
          <div class=
          "select">
          <div class=
          "option" v-for="(item, index) in colors" :key="index" @click="selectColor(index)">
          <div class=
          "option-color" :style="{ background: item }" />
          <
          /div>
          <
          /div>
          <
          !-- 贴膜材质 -->
          <h2>请选择贴膜材质<
            /h2>
            <div class=
            "select">
            <div class=
            "option" v-for="(item, index) in materials" :key="index" @click="selectMaterial(index)">
            <button class=
            "option-material" :style="{ background: item }">
            {
            { item.name
            }
            }<
            /button>
            <
            /div>
            <
            /div>
            <
            /div>
            <
            /div>
            <
            /template>
            <style scoped lang="scss">
              * {
              margin: 0;
              padding: 0;
              }
              canvas {
              width: 100%;
              height: 100%;
              position: fixed;
              left: 0;
              top: 0;
              z-index: 1;
              }
              .menu {
              position: fixed;
              top: 20px;
              right: 20px;
              z-index: 2;
              .title {
              }
              .select {
              display: inline-block;
              display: flex;
              .option {
              display: flex;
              .option-color {
              width: 30px;
              height: 30px;
              border: 1px solid #ccc;
              margin: 10px;
              display: inline-block;
              cursor: pointer;
              border-radius: 10px;
              }
              }
              }
              }
              <
              /style>

2.5.2.老陈打码

<template>
  <div class=
  "home">
  <div class=
  "canvas-container" ref="canvasDom">
  <
  /div>
  <div class=
  "home-content">
  <div class=
  "home-content-title">
  <h1>汽车展示与选配<
    /h1>
    <
    /div>
    <h2>选择车身颜色<
      /h2>
      <div class=
      "select">
      <div
      class=
      "select-item"
      v-for="(item, index) in colors"
      :key="index"
      @click="selectColor(index)"
      >
      <div
      class=
      "select-item-color"
      :style="{ backgroundColor: item }"
      >
      <
      /div>
      <
      /div>
      <
      /div>
      <h2>选择贴膜材质<
        /h2>
        <div class=
        "select">
        <div
        class=
        "select-item"
        v-for="(item, index) in materials"
        :key="index"
        @click="selectMaterial(index)"
        >
        <div class=
        "select-item-text">
        {
        { item.name
        }
        }<
        /div>
        <
        /div>
        <
        /div>
        <
        /div>
        <
        /div>
        <
        /template>
        <script setup>
          import * as THREE from "three";
          import { onMounted, reactive, ref
          } from "vue";
          import { OrbitControls
          } from "three/examples/jsm/controls/OrbitControls";
          import { GLTFLoader
          } from "three/examples/jsm/loaders/GLTFLoader";
          import { DRACOLoader
          } from "three/examples/jsm/loaders/DRACOLoader";
          let controls;
          let canvasDom = ref(null);
          // 创建场景
          const scene = new THREE.Scene();
          // 创建相机
          const camera = new THREE.PerspectiveCamera(
          75,
          window.innerWidth / window.innerHeight,
          0.1,
          1000
          );
          camera.position.set(0, 2, 6);
          // 创建渲染器
          const renderer = new THREE.WebGLRenderer({
          // 抗锯齿
          antialias: true,
          });
          renderer.setSize(window.innerWidth, window.innerHeight);
          const render = () =>
          {
          renderer.render(scene, camera);
          controls && controls.update();
          requestAnimationFrame(render);
          };
          let wheels = [];
          let carBody, frontCar, hoodCar, glassCar;
          // 创建材质
          const bodyMaterial = new THREE.MeshPhysicalMaterial({
          color: 0xff0000,
          metalness: 1,
          roughness: 0.5,
          clearcoat: 1,
          clearcoatRoughness: 0,
          });
          const frontMaterial = new THREE.MeshPhysicalMaterial({
          color: 0xff0000,
          metalness: 1,
          roughness: 0.5,
          clearcoat: 1,
          clearcoatRoughness: 0,
          });
          const hoodMaterial = new THREE.MeshPhysicalMaterial({
          color: 0xff0000,
          metalness: 1,
          roughness: 0.5,
          clearcoat: 1,
          clearcoatRoughness: 0,
          });
          const wheelsMaterial = new THREE.MeshPhysicalMaterial({
          color: 0xff0000,
          metalness: 1,
          roughness: 0.1,
          });
          const glassMaterial = new THREE.MeshPhysicalMaterial({
          color: 0xffffff,
          metalness: 0,
          roughness: 0,
          transmission: 1,
          transparent: true,
          });
          let colors = ["red", "blue", "green", "gray", "orange", "purple"];
          let selectColor = (index) =>
          {
          bodyMaterial.color.set(colors[index]);
          frontMaterial.color.set(colors[index]);
          hoodMaterial.color.set(colors[index]);
          wheelsMaterial.color.set(colors[index]);
          // glassMaterial.color.set(colors[index]);
          };
          let materials = [
          { name: "磨砂", value: 1
          },
          { name: "冰晶", value: 0
          },
          ];
          let selectMaterial = (index) =>
          {
          bodyMaterial.clearcoatRoughness = materials[index].value;
          frontMaterial.clearcoatRoughness = materials[index].value;
          hoodMaterial.clearcoatRoughness = materials[index].value;
          };
          onMounted(() =>
          {
          // 把渲染器插入到dom中
          // console.log(canvasDom.value);
          canvasDom.value.appendChild(renderer.domElement);
          // 初始化渲染器,渲染背景
          renderer.setClearColor("#000");
          scene.background = new THREE.Color("#ccc");
          scene.environment = new THREE.Color("#ccc");
          render();
          // 添加网格地面
          const gridHelper = new THREE.GridHelper(10, 10);
          gridHelper.material.opacity = 0.2;
          gridHelper.material.transparent = true;
          scene.add(gridHelper);
          // 添加控制器
          controls = new OrbitControls(camera, renderer.domElement);
          controls.update();
          // 添加gltf汽车模型
          const loader = new GLTFLoader();
          const dracoLoader = new DRACOLoader();
          dracoLoader.setDecoderPath("./draco/gltf/");
          loader.setDRACOLoader(dracoLoader);
          loader.load("./model/bmw01.glb", (gltf) =>
          {
          const bmw = gltf.scene;
          // console.log(gltf);
          bmw.traverse((child) =>
          {
          if (child.isMesh) {
          console.log(child.name);
          }
          // 判断是否是轮毂
          if (child.isMesh && child.name.includes("轮毂")) {
          child.material = wheelsMaterial;
          wheels.push(child);
          }
          // 判断是否是车身
          if (child.isMesh && child.name.includes("Mesh002")) {
          carBody = child;
          carBody.material = bodyMaterial;
          }
          // 判断是否是前脸
          if (child.isMesh && child.name.includes("前脸")) {
          child.material = frontMaterial;
          frontCar = child;
          }
          // 判断是否是引擎盖
          if (child.isMesh && child.name.includes("引擎盖_1")) {
          child.material = hoodMaterial;
          hoodCar = child;
          }
          // 判断是否是挡风玻璃
          if (child.isMesh && child.name.includes("挡风玻璃")) {
          child.material = glassMaterial;
          glassCar = child;
          }
          });
          scene.add(bmw);
          });
          // 添加灯光
          const light1 = new THREE.DirectionalLight(0xffffff, 1);
          light1.position.set(0, 0, 10);
          scene.add(light1);
          const light2 = new THREE.DirectionalLight(0xffffff, 1);
          light2.position.set(0, 0, -10);
          scene.add(light2);
          const light3 = new THREE.DirectionalLight(0xffffff, 1);
          light3.position.set(10, 0, 0);
          scene.add(light3);
          const light4 = new THREE.DirectionalLight(0xffffff, 1);
          light4.position.set(-10, 0, 0);
          scene.add(light4);
          const light5 = new THREE.DirectionalLight(0xffffff, 1);
          light5.position.set(0, 10, 0);
          scene.add(light5);
          const light6 = new THREE.DirectionalLight(0xffffff, 0.3);
          light6.position.set(5, 10, 0);
          scene.add(light6);
          const light7 = new THREE.DirectionalLight(0xffffff, 0.3);
          light7.position.set(0, 10, 5);
          scene.add(light7);
          const light8 = new THREE.DirectionalLight(0xffffff, 0.3);
          light8.position.set(0, 10, -5);
          scene.add(light8);
          const light9 = new THREE.DirectionalLight(0xffffff, 0.3);
          light9.position.set(-5, 10, 0);
          scene.add(light9);
          });
          <
          /script>
          <style>
            * {
            margin: 0;
            padding: 0;
            }
            .home-content {
            position: fixed;
            top: 0;
            right: 20px;
            }
            .select-item-color {
            width: 50px;
            height: 50px;
            border: 1px solid #ccc;
            margin: 10px;
            display: inline-block;
            cursor: pointer;
            border-radius: 10px;
            }
            .select {
            display: flex;
            }
            <
            /style>