需求:父模型(P)移动时,子模型(C)相对于父模型的位置也随之变化(P会根据parentData在x轴上移动,C是位于P模型上的一个部件,C根据数据childData在Y轴上移动)
问题出现的原因:局部(相对)坐标和世界坐标系区别 需要修改的是C相对于P的局部坐标系,而不是C的世界坐标系
解决方案:当C模型嵌套在父模型P中时,要修改C模型位置,需要先获取P模型的世界坐标,然后将C模型的目标世界坐标转换为相对于P模型的局部坐标,最后修改C模型的位置(如果只是修改C模型世界坐标或者直接修改C模型位置都会不生效)
完整代码如下
import { useEffect, useState, useRef } from 'react';
import { Spin } from 'antd';
import * as THREE from 'three';
import TWEEN from '@tweenjs/tween.js';
import bg from '../../assets/img/pp.jpg';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
const GLTFModel = (props) => {
let mixer, model, renderer, controls, animationAction;
const width = props.demoSize[0]; // 获取容器宽度
const height = props.demoSize[1]; // 获取容器高度
const [loading, setLoading] = useState(false);
const scene = new THREE.Scene();
const containerRef = useRef(null);
const parentModelRef = useRef(null); // 父模型
const childModelRef = useRef(null); // 子模型
const camera = new THREE.PerspectiveCamera(40, width / height, 1, 1000);
const parentData = useRef(null); // parent位置
const childData = useRef(null); // child位置
const childDirection = useRef(1); // child:1 表示递增,-1 表示递减
const parentDirection = useRef(1); // parent:1 表示递增,-1 表示递减
useEffect(() => {
loadedModels();
addRenderer();
addScene();
addControls();
const axes = new THREE.AxesHelper(10000000);
scene.add(axes);
const onResize = () => {
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
};
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
scene.clear(); // 清空场景
renderer.dispose();
containerRef.current?.removeChild(renderer.domElement);
};
}, []);
useEffect(() => {
const interval = setInterval(() => {
// 更新 childData
if (childData.current >= 20) {
childDirection.current = -1; // 达到最大值
} else if (childData.current <= 0) {
childDirection.current = 1; // 达到最小值
}
childData.current += childDirection.current; // 根据方向更新
// 更新 parentData
if (parentData.current >= 180) {
parentDirection.current = -1; // 达到最大值
} else if (parentData.current <= 0) {
parentDirection.current = 1; // 达到最小值
}
parentData.current += parentDirection.current * 5; // 根据方向更新
moveChild(), moveParent();
}, 1000); // 模拟数据变化
return () => clearInterval(interval);
}, []);
return (
<Spin spinning={loading}>
<div ref={containerRef} style={{ width: '100%', height: '100%' }} />
</Spin>
);
/** 移动child模型 */
function moveChild() {
if (childModelRef.current && parentModelRef.current) {
// 获取当前 parent 的世界坐标 (child嵌套在parent时)
const currentMJWorldPosition = new THREE.Vector3();
parentModelRef.current.getWorldPosition(currentMJWorldPosition);
// 使用 childData.current 更新 child 的世界坐标,保持 x 轴一致,y 轴变动 (因为C是在P上的一个部件,所以要保持x、z轴一致)
const targetWorldPosition = new THREE.Vector3(
currentMJWorldPosition.x, // 保持 parent 的 x 轴位置
childData.current, // 使用 childData 更新 y 轴坐标
currentMJWorldPosition.z // 保持 z 轴位置
);
// 将目标的世界坐标转换为相对父物体 (parent) 的局部坐标
const parent = childModelRef.current.parent;
parent.worldToLocal(targetWorldPosition); // 将目标世界坐标转换为局部坐标
console.log( '新局部坐标:', targetWorldPosition);
// 更新 child 的位置 tween是为了移动时有动画效果
new TWEEN.Tween(childModelRef.current.position)
.to(targetWorldPosition, 10000)
.easing(TWEEN.Easing.Quadratic.Out)
.onUpdate(() => {
childModelRef.current.updateMatrixWorld(true); // 更新世界矩阵
})
.start();
}
}
/** 移动parent模型 */
function moveParent() {
if (parentModelRef.current) {
new TWEEN.Tween(parentModelRef.current.position)
.to({ x: parentData.current }, 5000) // 移动到目标位置
.easing(TWEEN.Easing.Quadratic.Out)
.start();
// new TWEEN.Tween(camera.position)
// .to({ x: parentData.current + 50 }, 3000)
// .onUpdate(() => {
// const newX = camera.position.x;
// controls.target.set(newX, 0, 0);
// camera.updateProjectionMatrix();
// })
// .easing(TWEEN.Easing.Quadratic.Out)
// .start(); // 更新相机位置
}
}
/** 更新渲染 */
function animate() {
requestAnimationFrame(animate);
TWEEN.update();
controls.update();
renderer.render(scene, camera);
}
/** 渲染器 */
function addRenderer() {
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height); // 设置渲染区域范围
containerRef.current.appendChild(renderer.domElement);
}
/** 场景配置 */
function addScene() {
const loader = new THREE.TextureLoader();
loader.load(bg, function (texture) {
scene.background = texture;
}); // 设置模型背景
// 确保renderer已经被正确初始化后再使用PMREMGenerator
const pmremGenerator = new THREE.PMREMGenerator(renderer);
scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.3); // 平行光
directionalLight.position.set(90, 76, -126); // 光源位置
scene.add(directionalLight);
camera.position.set(90, 150, 90);
camera.up.set(0, 0, 1);
}
/** 控制器 */
function addControls() {
controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(90, 0, 20);
controls.enableRotate = false;
controls.enableZoom = false;
controls.update();
}
/** 加载模型 */
function loadedModels() {
setLoading(true);
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('jsm/libs/draco/gltf/');
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
loader.load(
'241204.glb', // 确保路径和模型文件名正确
function (gltf) {
model = gltf.scene;
scene.add(model);
parentModelRef.current = model.getObjectByName('parent'); // 获取父模型
childModelRef.current = parentModelRef.current.getObjectByName('child'); // 获取子模型
animate();
setLoading(false);
},
undefined,
function (e) {
console.error(e);
}
);
}
};
export default GLTFModel;