【Threejs】——案例实践笔记

该文章已生成可运行项目,

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>
本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值