ThreeJS 点击改变模型内的子元素颜色与镜头跟随

108 阅读4分钟

效果

TheeJS跟随移动与点击变色.gif

完整代码


<template>
  <div class="home">
    <div class="convas_container" ref="canvasDom" style="">

    </div>


  </div>
</template>

<script setup>
// 
import * as THREE from 'three'
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'

import { Water } from 'three/examples/jsm/objects/Water'
import gsap from 'gsap'
// 导入dat.gui

import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader' // 引入加载器
// 引入控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { onMounted, reactive, ref } from 'vue';
// 添加反射
import { Reflector } from 'three/examples/jsm/objects/Reflector'
// 相机跟踪
import * as TWEEN from '@tweenjs/tween.js';

const canvasDom = ref(null)
let controls = null // 控制器

// 初始化场景
const scene = new THREE.Scene()

// 设置时钟
const clock = new THREE.Clock()

// 创建纹理加载器
const textureLoader = new THREE.TextureLoader();

// 初始化相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
// 设置相机的位置
camera.position.set(0, 10, 6)

// 初始化渲染器
const renderer = new THREE.WebGLRenderer({
  antialias: true,
  // 设置光照
})
renderer.setSize(window.innerWidth, window.innerHeight)
// 设置色调映射
renderer.toneMapping = THREE.ACESFilmicToneMapping
// 色调亮度
renderer.toneMappingExposure = 2
// 加阴影
renderer.toneMappingExposure = 0.5
renderer.shadowMap.enabled = true
renderer.physicallyCorrectLights = true
document.body.appendChild(renderer.domElement)
const render = () => {
  camera.aspect = window.innerWidth / window.innerHeight

  camera.updateProjectionMatrix()
  controls && controls.update()
  renderer.render(scene, camera);
  requestAnimationFrame(render)
}
onMounted(() => {
  canvasDom.value.appendChild(renderer.domElement)
  // 初始化渲染器  渲染背景
  renderer.setClearColor('#000')
  render()
  // // 添加轨道控制器
  controls = new OrbitControls(camera, canvasDom.value)
  controls.enableDamping = true // 添加阻尼
  controls.update()
})
// 添加辅助坐标轴
const axes = new THREE.AxesHelper(5)
scene.add(axes)

// 创建rgbe加载器
let hdrloader = new RGBELoader()
hdrloader.load('./model/satara_night_no_lamps_1k.hdr', (texture) => {
  texture.mapping = THREE.EquirectangularReflectionMapping
  scene.background = texture
  scene.environment = texture
})

// 添加模型


const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('./draco/gltf/')
const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)
// 加载地图模型

const doorColorTexture = textureLoader.load('./model/1.jpg') // 纹理路径
// 材质
const basicMaterial = new THREE.MeshBasicMaterial({
  color: '#c8c8c8',
  map: doorColorTexture,// 添加纹理(贴图)
})


let model;
gltfLoader.load('./model/shelves.glb', function (glb) {
  model = glb.scene;
  // 遍历模型的子元素
  model.traverse(function (child) {
    if (child.isMesh) {
      // 给子元素添加纹理
      child.material = child.material.clone();
      child.material.map = doorColorTexture;
      child.material.needsUpdate = true;

      child.material.transparent = true;
      child.material.opacity = 0.2;
      child.material.color.set(0xffcccc);
    }
  });
  //将模型添加至场景
  scene.add(glb.scene)
  model.position.set(10,2,10)
  //设置模型位置
  camera.position.set(20,10, 20)
})






// 添加灯管
const light = new THREE.DirectionalLight(0xffffff, 100)
light.position.set(10, 10, 10)
scene.add(light)
const light1 = new THREE.DirectionalLight(0xffffff, 100)
light1.position.set(-10, 10, 10)
scene.add(light1)
const light2 = new THREE.DirectionalLight(0xffffff, 100)
light2.position.set(20, 10, 10)
scene.add(light2)


// 添加环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 10);
scene.add(ambientLight);

// 添加光阵
// let video = document.createElement('video')
// video.src = './model/v天空.mp4'
// video.loop = true
// video.muted = true
// video.play()
// let videoTexture = new THREE.VideoTexture(video)
// const videoPlane = new THREE.PlaneBufferGeometry(30, 30)
// const videoMaterial = new THREE.MeshBasicMaterial({
//   map: videoTexture,
//   transparent: true,
//   side: THREE.DoubleSide,
//   alphaMap: videoTexture
// })
// const videoMesh = new THREE.Mesh(videoPlane, videoMaterial)
// videoMesh.position.set(0, 0.2, 0)
// videoMesh.rotation.set(-Math.PI / 2, 0, 0)
// scene.add(videoMesh)

// 添加镜面反射
// let reflectorGeometry = new THREE.PlaneBufferGeometry(1000, 1000)
// let reflectorPlane = new Reflector(reflectorGeometry, {
//   textureWidth: window.innerWidth,
//   textureHeight: window.innerHeight,
//   color: 0xc8c8c8,
//   recursion: 1,
// })
// reflectorPlane.rotation.x = -Math.PI / 2
// scene.add(reflectorPlane)



// 设置Raycaster和鼠标事件处理
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

function onMouseClick(event) {
  // 将鼠标点击位置转换为标准化设备坐标 (-1 to +1)
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;

  // 更新射线
  raycaster.setFromCamera(mouse, camera);
  // 计算物体和射线的交集
  if (model) {
    const intersects = raycaster.intersectObject(model, true);
    if (intersects.length > 0) {
      const intersectedObject = intersects[0].object;
      if (intersectedObject.name.indexOf('Cube') != -1) return
      // 将所有子元素设为透明
      model.traverse((child) => {
        if (child.isMesh) {
          if (child.name == intersectedObject.name) {
            intersectedObject.material.transparent = false;
            intersectedObject.material.opacity = 1.0;
            intersectedObject.material.color.set(0xff0000);
            moveCameraToObject(child);
          } else {
            child.material.transparent = true;
            child.material.opacity = 0.2;
            child.material.color.set(0xffcccc);
          }
        }
      });
    }
  }
}

window.addEventListener('click', onMouseClick, false);


function moveCameraToObject(target) {
  const targetPosition = new THREE.Vector3();
  target.getWorldPosition(targetPosition);

  // 使用Tween.js进行平滑过渡
  new TWEEN.Tween(camera.position)
    .to({ x: targetPosition.x, y: targetPosition.y + 10, z: targetPosition.z + 10 }, 1000 * 1)
    .easing(TWEEN.Easing.Quadratic.InOut)
    .start();

  // 更新相机在过渡期间的lookAt
  new TWEEN.Tween(camera.lookAt)
    .to({ x: targetPosition.x, y: targetPosition.y + 10, z: targetPosition.z + 10 }, 1000 * 1)
    .easing(TWEEN.Easing.Quadratic.InOut)
    .start();


}


// 设置渲染帧率
const fps = 20;
const interval = 1000 / fps;
let lastTime = Date.now();

function animate() {
  requestAnimationFrame(animate);

  const currentTime = Date.now();
  const deltaTime = currentTime - lastTime;

  if (deltaTime > interval) {
    // 更新时间戳
    lastTime = currentTime - (deltaTime % interval);

    // 更新Tween动画
    TWEEN.update();

    // 渲染场景
    renderer.render(scene, camera);
  }
}

animate();
</script>

<style>
* {
  margin: 0;
  padding: 0;
}

canvas {
  width: 100vw;
  height: 100vh;

}

.convas_container {
  width: 100vw;
  height: 100vh;
}
</style>

点击变色代码

先给模型的子元素赋值一个纹理,这里需要遍历每个子元素,不然相同的子元素会公用同一个纹理


let model;
gltfLoader.load('./model/shelves.glb', function (glb) {
  model = glb.scene;
  // 遍历模型的子元素
  model.traverse(function (child) {
    if (child.isMesh) {
      // 给子元素添加纹理
      child.material = child.material.clone();
      child.material.map = doorColorTexture;
      child.material.needsUpdate = true;

      child.material.transparent = true;
      child.material.opacity = 0.2;
      child.material.color.set(0xffcccc);
    }
  });
  //将模型添加至场景
  scene.add(glb.scene)
  model.position.set(10, 2, 10)
  //设置模型位置
  camera.position.set(20, 10, 20)
})

点击子模型时,做条件判断,符合则进行更改颜色

// 设置Raycaster和鼠标事件处理
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

function onMouseClick(event) {
  // 将鼠标点击位置转换为标准化设备坐标 (-1 to +1)
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;

  // 更新射线
  raycaster.setFromCamera(mouse, camera);
  // 计算物体和射线的交集
  if (model) {
    const intersects = raycaster.intersectObject(model, true);
    if (intersects.length > 0) {
      const intersectedObject = intersects[0].object;
      if (intersectedObject.name.indexOf('Cube') != -1) return
      // 将所有子元素设为透明
      model.traverse((child) => {
        if (child.isMesh) {
          if (child.name == intersectedObject.name) {
            intersectedObject.material.transparent = false;
            intersectedObject.material.opacity = 1.0;
            intersectedObject.material.color.set(0xff0000);
            moveCameraToObject(child);
          } else {
            child.material.transparent = true;
            child.material.opacity = 0.2;
            child.material.color.set(0xffcccc);
          }
        }
      });
    }
  }
}

window.addEventListener('click', onMouseClick, false);

镜头跟随

引入模块

// 相机跟踪
import * as TWEEN from '@tweenjs/tween.js';


function moveCameraToObject(target) {
  const targetPosition = new THREE.Vector3();
  target.getWorldPosition(targetPosition);

  // 使用Tween.js进行平滑过渡
  new TWEEN.Tween(camera.position)
    .to({ x: targetPosition.x, y: targetPosition.y + 10, z: targetPosition.z + 10 }, 1000 * 1)
    .easing(TWEEN.Easing.Quadratic.InOut)
    .start();

  // 更新相机在过渡期间的lookAt
  new TWEEN.Tween(camera.lookAt)
    .to({ x: targetPosition.x, y: targetPosition.y + 10, z: targetPosition.z + 10 }, 1000 * 1)
    .easing(TWEEN.Easing.Quadratic.InOut)
    .start();


}


// 设置渲染帧率
const fps = 20;
const interval = 1000 / fps;
let lastTime = Date.now();

function animate() {
  requestAnimationFrame(animate);

  const currentTime = Date.now();
  const deltaTime = currentTime - lastTime;

  if (deltaTime > interval) {
    // 更新时间戳
    lastTime = currentTime - (deltaTime % interval);

    // 更新Tween动画
    TWEEN.update();

    // 渲染场景
    renderer.render(scene, camera);
  }
}

animate();

项目所需的npm包

版本号视项目而定

"@tweenjs/tween.js": "^23.1.2",
"core-js": "^3.6.5",
"gsap": "^3.11.4",
"three": "^0.150.1",
"tree": "^0.1.3",
"vue": "^3.0.0"