解决threejs中(gltf)父模型动态移动时修改子模型位置不生效问题

需求:父模型(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;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值