threejs镜头追踪的实现

120 阅读5分钟

​ 要求:当数据data变化时,模型要实时沿着x轴移动到data的位置,同时镜头要追踪模型的移动,保持模型在镜头中心位置,以确保能够在视线范围内看到模型,避免移出视线

实现思路:使用tween.js库实现模型的移动动画,同时移动镜头的x轴位置并在动画的update回调函数中更新镜头的朝向并以模型为中心,使镜头始终追踪模型的移动

(模型视线调整需要一点点磨)

最开始我的代码如下:我自认为这个代码逻辑是没有问题的,按理说相机会跟着模型平移才对,但事实上模型在移动过程中的效果是不理想的,相机会先向x轴的目标位置偏移,然后模型才会跟着动画移动到目标位置,这就说明相机朝向设置有问题...但是又感觉没毛病啊...

    /** 移动模型 */
	function moveModel() {
		if (group?.children.length > 0) {
			const modelName = getGroupByName(group?.children, 'xxx.fbx'); // 获取移动的模型

			if (modelName) {
				// 动画 modelName
				new TWEEN.Tween(modelName.position)
					.to({ x: data.current }, 2000) // 移动到目标位置
					.easing(TWEEN.Easing.Quadratic.Out)
					.start();

				// 更新相机位置
				new TWEEN.Tween(camera.position)
					.to({ x: data.current + 2800 }, 2000)
					.onUpdate(() => {
						camera.lookAt(data.current + 2800, 300, 700);
						controls.target.set(data.current + 2800, 300, 700);
					})
					.easing(TWEEN.Easing.Quadratic.Out)
					.start();
			}
		}
	}

修改后,正确的的代码如下:

	function moveModel() {
		if (group?.children.length > 0) {
			const modelName = getGroupByName(group?.children, 'xxx.fbx');

			if (modelName) {
				// 动画 modelName
				new TWEEN.Tween(modelName.position)
					.to({ x: data.current }, 2000) // 移动到目标位置
					.easing(TWEEN.Easing.Quadratic.Out)
					.start();

				// 更新相机位置
				new TWEEN.Tween(camera.position)
					.to({ x: data.current + 2800 }, 2000)
					.onUpdate(() => {
						const newX = camera.position.x;
						camera.lookAt(new THREE.Vector3(newX, 300, 700));
						controls.target.set(newX, 300, 700);
						camera.updateProjectionMatrix();
					})
					.easing(TWEEN.Easing.Quadratic.Out)
					.start();
			}
		}
	}

图片展黄色模块会根据data变化左右移动,镜头也会跟着移动,以确保模型在视线范围内

image.png 那么问题出在哪里呢?

camera.lookAt(data.current + 2800, 300, 700) 和使用 camera.lookAt(new THREE.Vector3(newX, targetY, targetZ)) 在效果上是类似的,都是设置相机朝向某个特定的点,但它们在实现方式上有一些细微的区别:

  1. 参数类型:
    camera.lookAt(data.current + 2800, 300, 700):第一种写法传入的三个参数代表目标点的 x, y, z 坐标。
    camera.lookAt(new THREE.Vector3(data.current + 2800, 300, 700)):第二种写法是将目标位置封装在一个 THREE.Vector3 对象中传递。

  2. 变化不同:
    在 camera.lookAt(data.current + 2800, 300, 700) 中,直接给了一个具体的、静态的目标点。这个目标点不随着动画而变化。虽然 data.current 是动态的,但在调用的时候会直接执行,动画更新时不会跟随 data.current 的变化。

在 camera.lookAt(new THREE.Vector3(data.current + 2800, 300, 70)) 中,newX 是相机动画更新时的 camera.position.x,它会随着相机位置的变化而动态更新。通过这种写法,lookAt 的目标点会随相机位置变化而更新。

  1. 效果不同:
    camera.lookAt(data.current + 2800, 300, 700):在动画开始时计算了一次 data.current + 2800,然后将相机锁定朝向这个固定点,动画过程中朝向不会改变,除非你在动画过程中再次调用 camera.lookAt。

camera.lookAt(new THREE.Vector3(data.current + 2800, 300, 70)):在每次 TWEEN 的 onUpdate 中,data.current + 2800 代表了当前相机的 x 位置,它会随着动画更新,从而相机的朝向也会动态地跟随相机位置的变化,形成相机平滑转动的效果。

完整代码如下:

import { useState, useEffect, useRef } from 'react';
import { Spin } from 'antd';
import * as THREE from 'three';
import TWEEN from '@tweenjs/tween.js';
import bg2 from '../../assets/img/pp.jpg';
import { getGroupByName } from '../../utils/method';
import { FBXLoader } from 'three/addons/loaders/FBXLoader.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

function DemoTest(props) {
 const colors = [ 0xffff00];
 const scene = new THREE.Scene();
 const containerRef = useRef(null); // 获取容器的引用
 const width = props.demoSize[0]; // 获取容器宽度
 const height = props.demoSize[1]; // 获取容器高度
 const group = useRef(new THREE.Group()).current;
 const [loading, setLoading] = useState(false);
 const camera = new THREE.PerspectiveCamera(70, width / height, 0.1, 10000000);

 let renderer, controls;
 const data = useRef(null); // 大车位置

 useEffect(() => {
  const interval = setInterval(() => {
   // 模拟数据变化
   (data.current = Math.floor(Math.random() * 5000)), moveModel();
  }, 8000);
  return () => clearInterval(interval); // 清理函数
 }, []);

 useEffect(() => {
  setLoading(true);
  const container = containerRef.current; // 获取容器元素
  if (!container) return;
  initModel();
  return () => {
   scene.clear();
   container.removeChild(renderer.domElement);
  };
 }, []);

 return (
  <Spin spinning={loading}>
   <div
    id='webgl'
    ref={containerRef}
    style={{ width: props.demoSize[0], height: props.demoSize[1] }}
   />
  </Spin>
 );

 /** 初始化 */
 function initModel() {
  scene.add(group);
  const axes = new THREE.AxesHelper(10000000);
  scene.add(axes);
  const loader = new THREE.TextureLoader();
  const backgroundTexture = loader.load(bg2, function (texture) {
   scene.background = texture;
  }); // 设置模型背景
  setupLight();
  setupCamera();
  setupRenderer();

  renderer = new THREE.WebGLRenderer({
   antialias: true, // 抗锯齿属性
   // logarithmicDepthBuffer: true,
  });
  controls = new OrbitControls(camera, renderer.domElement)
  loadModels(group, props.fileName).then(() => {
   setLoading(false);
  });// 加载模型

  render();
 }

 /** 移动模型 */
 function moveModel() {
  if (group?.children.length > 0) {
   const modelName = getGroupByName(group?.children, 'xxx.fbx');
   console.log(data.current, 'moveModel');

   if (modelName) {
    // 动画 modelName
    new TWEEN.Tween(modelName.position)
     .to({ x: data.current }, 2000) // 移动到目标位置
     .easing(TWEEN.Easing.Quadratic.Out)
     .start();

    // 更新相机位置
    new TWEEN.Tween(camera.position)
     .to({ x: data.current + 2800 }, 2000)
     .onUpdate(() => {
      const newX = camera.position.x;
      camera.lookAt(new THREE.Vector3(newX, 300, 700));
      controls.target.set(newX, 300, 700);
      camera.updateProjectionMatrix();
     })
     .easing(TWEEN.Easing.Quadratic.Out)
     .start();
   }
  }
 }

 /** 加载模型 */
 function loadModels(group, modelFiles) {
  return new Promise((resolve) => {
   const loader = new FBXLoader();
   let loadedModels = 0; // 已加载模型的计数
   modelFiles.forEach((fileName, index) => {
    loader.load(fileName, (object) => {
     const modelGroup = new THREE.Group();
     modelGroup.name = fileName;

     object.traverse((child) => {
      if (child.isMesh) {
       const mesh = new THREE.Mesh(
        child.geometry,
        new THREE.MeshPhongMaterial({
         color: colors[index],
         side: THREE.DoubleSide,
        })
       );
       modelGroup.add(mesh);
      }
     });
     group.add(modelGroup); // 将模型组添加到主组
     loadedModels++;
     if (loadedModels === modelFiles.length) {
      setLoading(false);
      resolve(); // 当所有模型都加载完成时,解决 Promise
     }
    });
   });
  });
 }


 /** 渲染 */
 function render() {
  requestAnimationFrame(render);
  renderer.render(scene, camera);
  TWEEN.update();
  controls.update();
 }


 /** 相机 */
 function setupCamera() {
  camera.position.set(2800, 1500, 1500);
  camera.lookAt(2800, 0, 1500);
  camera.up.set(0, 0, 1);
  controls.target.set(2800, 300, 800);
  camera.updateProjectionMatrix();

  renderer.setSize(width, height);
  renderer.setClearColor(0x111);
  containerRef.current.appendChild(renderer.domElement);
 }

 /** 光源 */
 function setupLight() {
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
  const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
  directionalLight.position.set(1000, 1000, 10000); //光源位置
  scene.add(ambientLight, directionalLight);

  // 禁止旋转和缩放
  controls.enableRotate = false;
  controls.enableZoom = false;
 }
}

export default DemoTest;

/** 获取对应name的模型 */
export const getGroupByName = (obj, name) => {
	for (let child of obj) {
		if (child.name === name) {
			return child;
		}
	}
	return null; // 如果未找到,返回 null
};