You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Three.js中无人机螺旋桨无法绕自身中心旋转的问题求助

问题:Three.js中GLTF螺旋桨模型绕外部轴旋转而非自身中心

问题描述

在Three.js中实现无人机螺旋桨旋转效果,螺旋桨来自GLTF模型,已将其放置在pivot枢轴组后加入总组,但螺旋桨并未绕自身中心旋转,而是绕外部轴转动。控制台显示螺旋桨mesh在pivot中的位置为(0,0,0),尝试调整枢轴位置等方案后问题仍未解决。

加载代码

const loader = new THREE.GLTFLoader();

const p1Meshes = [];
const p2Meshes = [];
const mMesh = [];

const models = [
    {
        path: 'zep-main-1.gltf',
        center: new THREE.Vector3(40, -80, 250),
        scale: 100,
        materialProps: {
            color: 0x999966,
            transparent: true,
            opacity: 0.96,
            roughness: 0.8,
            metalness: 0.4,
            side: THREE.DoubleSide,
        },
        reflection: false,
        reflectionX: 0,
        pivot: false,
    },
    {
        path: 'zep-p1.gltf',
        center: new THREE.Vector3(40-4.5, -80, 250),
        scale: 100,
        materialProps: {
            color: 0x448888,
            transparent: true,
            opacity: 0.98,
            roughness: 0.2,
            metalness: 0.2,
            side: THREE.DoubleSide,
        },
        reflection: true,
        reflectionX: 41,
        pivot: true,
    }
    // 其余模型配置省略
];

const compositeModel = new THREE.Group();

function loadModel(loader, path, center, scale, materialProps, reflection = false, reflectionX = 0, pivot=false) {
    return new Promise((resolve, reject) => {
        loader.load(
            path,
            function (gltf) {
                const model = gltf.scene;

                camera.lookAt(center);
                if(pivot){
                    var box = new THREE.Box3().setFromObject( model );
                    box.getCenter( model.position );
                    model.position.multiplyScalar( - 1 );
                    const pivot = new THREE.Group();
                    pivot.position.sub(center);
                    compositeModel.add(pivot);
                    pivot.add(model)
                } else {
                    model.position.sub(center);
                    compositeModel.add(model);
                }

                model.scale.set(scale, scale, scale);

                model.traverse(function (child) {
                    if (child.isMesh) {
                        child.material = new THREE.MeshStandardMaterial(materialProps);
                    }
                });

                if (path.includes('zep-p1')) {
                    p1Meshes.push(model);
                } else if (path.includes('zep-p2')) {
                    p2Meshes.push(model);
                }

                if(path.includes('zep-main-1')|| path.includes('zep-mw')) mMesh.push(model);

                if (reflection) {
                    if(pivot){
                        const reflectedModel = model.clone();
                        reflectedModel.scale.x = -scale;
                        var box = new THREE.Box3().setFromObject( reflectedModel );
                        box.getCenter( reflectedModel.position );
                        reflectedModel.position.multiplyScalar( - 1 );
                        const rpivot = pivot.clone();
                        rpivot.scale.x = -scale;
                        rpivot.position.x -= reflectionX;
                        compositeModel.add(rpivot);
                        rpivot.add(reflectedModel)
                    } else {
                        const reflectedModel = model.clone();
                        reflectedModel.scale.x = -scale;
                        reflectedModel.position.x -= reflectionX;
                        compositeModel.add(reflectedModel);
                    }

                    if (path.includes('zep-p1') ) {
                        p1Meshes.push(reflectedModel);
                    } else if (path.includes('zep-p2')) {
                        p2Meshes.push(reflectedModel);
                    }
                }

                resolve();
            },
            function (xhr) {
                console.log((xhr.loaded / xhr.total * 100) + '% loaded');
            },
            function (error) {
                console.error('An error happened', error);
                reject(error);
            }
        );
    });
}

旋转代码

function startAirship() {
    var rotationSpeed = 0.1;

    function animateAirshipProps() {
      requestAnimationFrame(animateAirshipProps);

      mMesh.forEach( m => {
        console.log(m.position);
      })

      p1Meshes.forEach(mesh => {
        console.log("mesh position before: ", mesh.position, mesh.parent.position, mesh.parent.parent, mesh);
        mesh.rotation.z=rotationSpeed; 
        rotationSpeed = rotationSpeed + 0.01;
      });

      p2Meshes.forEach(mesh => {
        //mesh.position.sub(point);
        //mesh.rotation.z -= rotationSpeed;
        //mesh.position.add(point);
      });

      renderer.render(scene, camera);
    }

    animateAirshipProps();
  }

解决方案

核心问题分析

  1. 变换顺序错误:Three.js中变换优先级为「缩放→旋转→平移」,原代码先调整模型位置再应用缩放,导致模型中心点被缩放偏移。
  2. 枢轴位置计算错误:原代码中pivot.position.sub(center)会将枢轴定位到错误位置,而非模型的目标中心。
  3. 反射模型枢轴克隆问题:克隆原枢轴会继承错误的变换属性,导致反射模型旋转中心偏移。

修改后的关键代码

1. 修正loadModel中的枢轴与缩放逻辑

if(pivot){
    // 先应用缩放,再计算包围盒
    model.scale.set(scale, scale, scale);
    const box = new THREE.Box3().setFromObject(model);
    const modelCenter = new THREE.Vector3();
    box.getCenter(modelCenter);
    
    // 将模型原点移到自身几何中心
    model.position.sub(modelCenter);
    
    // 创建枢轴并定位到目标位置
    const pivot = new THREE.Group();
    pivot.position.copy(center);
    compositeModel.add(pivot);
    pivot.add(model);
} else {
    model.scale.set(scale, scale, scale);
    model.position.sub(center);
    compositeModel.add(model);
}

2. 修正反射模型的枢轴创建逻辑

if (reflection) {
    if(pivot){
        const reflectedModel = model.clone();
        reflectedModel.scale.set(scale, scale, scale);
        reflectedModel.scale.x = -scale;
        
        const box = new THREE.Box3().setFromObject(reflectedModel);
        const modelCenter = new THREE.Vector3();
        box.getCenter(modelCenter);
        reflectedModel.position.sub(modelCenter);
        
        // 重新创建枢轴,避免克隆原枢轴的错误变换
        const rpivot = new THREE.Group();
        rpivot.position.copy(center);
        rpivot.position.x += reflectionX; // 调整反射后的X轴位置
        compositeModel.add(rpivot);
        rpivot.add(reflectedModel);
    } else {
        const reflectedModel = model.clone();
        reflectedModel.scale.set(scale, scale, scale);
        reflectedModel.scale.x = -scale;
        reflectedModel.position.x -= reflectionX;
        compositeModel.add(reflectedModel);
    }
}

3. 优化旋转逻辑

避免直接赋值旋转角度,改为累加保证持续旋转:

p1Meshes.forEach(mesh => {
    mesh.rotation.z += 0.1; // 固定速度累加旋转角度
});

内容的提问来源于stack exchange,提问作者Sean

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 17:44:52