要求:当数据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变化左右移动,镜头也会跟着移动,以确保模型在视线范围内
那么问题出在哪里呢?
camera.lookAt(data.current + 2800, 300, 700) 和使用 camera.lookAt(new THREE.Vector3(newX, targetY, targetZ)) 在效果上是类似的,都是设置相机朝向某个特定的点,但它们在实现方式上有一些细微的区别:
-
参数类型:
camera.lookAt(data.current + 2800, 300, 700):第一种写法传入的三个参数代表目标点的 x, y, z 坐标。
camera.lookAt(new THREE.Vector3(data.current + 2800, 300, 700)):第二种写法是将目标位置封装在一个 THREE.Vector3 对象中传递。 -
变化不同:
在 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 的目标点会随相机位置变化而更新。
- 效果不同:
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
};