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

ThreeJS中GLTF模型旋转不一致问题求助

ThreeJS GLTF模型旋转不一致问题排查与修复

从Blender导入的gltf.scene对象旋转表现不可预测且不一致,期望实现鼠标X轴控制旋转:鼠标右移时逆时针旋转,左移时顺时针旋转,类似芭蕾舞者的旋转效果。

代码中包含两处旋转操作:

  • 导入模型时设置初始旋转
  • 动画tick函数中根据鼠标位置实现动态旋转

核心问题:页面加载有时完全符合预期,但刷新后可能出现对象无法旋转的情况,再次刷新又恢复正常。怀疑是万向锁导致,但不知道如何修复代码消除旋转不一致问题。另外,用ThreeJS直接创建的imageMesh对象按相同逻辑旋转从未出现此问题。

模型初始加载代码

// Load GLTF Model
gltfLoader.load(
    '/Trial8.glb',
    (gltf) => {
        modelRef.current = gltf.scene;
        gltf.scene.traverse((child) => {
            if (child.isMesh) {
                child.geometry.center(); // Centers the geometry relative to its local origin
                child.material = bakedMaterial;
                child.castShadow = true;
                console.log('Model material:', child.material.constructor.name);
            }
        });
        gltf.scene.position.set(1.5, -7.54, 3);

        // Set initial rotation using Quaternion to avoid Euler issues
        const initialQuaternion = new THREE.Quaternion();
        initialQuaternion.setFromEuler(new THREE.Euler(0, -Math.PI / 2, Math.PI / 2, 'XYZ'));
        gltf.scene.quaternion.copy(initialQuaternion);
        gltf.scene.scale.set(0.5, 0.5, 0.5);
        scene.add(gltf.scene);

        // Add AxesHelper to visualize origin
        const axesHelper = new THREE.AxesHelper(1); // 1 unit long, scaled with model (0.5)
        gltf.scene.add(axesHelper);
        
        const originMarker = new THREE.Mesh(
            new THREE.SphereGeometry(0.1),
            new THREE.MeshBasicMaterial({ color: 0xff0000 })
        );
        originMarker.position.set(0, 0, 0);
        modelRef.current.add(originMarker);
    },
    (progress) => {
        console.log(`Loading: ${(progress.loaded / progress.total * 100).toFixed(2)}%`);
    },
    (error) => {
        console.error('Error loading GLTF:', error);
    }
);

Tick函数中的代码

// Animation loop
const clock = new THREE.Clock();
const tick = () => {
    const elapsedTime = clock.getElapsedTime();

    if (cameraRef.current) {
        const targetX = mouse.x * moveRange;
        const targetZ = -mouse.y;
        cameraRef.current.position.x = THREE.MathUtils.lerp(
            cameraRef.current.position.x,
            targetX,
            dampingFactor
        );
        cameraRef.current.position.z = THREE.MathUtils.lerp(
            cameraRef.current.position.z,
            targetZ,
            dampingFactor
        );
        cameraRef.current.lookAt(
            cameraRef.current.position.x,
            0,
            cameraRef.current.position.z
        );
    }

    // Add rotation for models based on mouse X
    const targetRotationZ = mouse.x * Math.PI; // Adjust rotation range as needed
    if (modelRef.current) {
        // Define target rotation based on mouse.x
        const targetRotationZ = mouse.x * Math.PI; // Same rotation range as before
        const targetQuaternion = new THREE.Quaternion();
        // Combine initial rotation with mouse-driven Y-axis rotation
        targetQuaternion.setFromEuler(
            new THREE.Euler(0, -Math.PI / 2 + targetRotationZ, Math.PI / 2, 'XYZ')
        );
        // Interpolate current quaternion to target quaternion
        modelRef.current.quaternion.slerp(targetQuaternion, dampingFactor);
    }
    imageMesh.rotation.z = THREE.MathUtils.lerp(
        imageMesh.rotation.z,
        targetRotationZ,
        dampingFactor
    );

    // Update directional light's x-position based on mouse.x
    const lightTargetX = initialLightX + mouse.x * lightMoveRange;
    directionalLight.position.x = THREE.MathUtils.lerp(
        directionalLight.position.x,
        lightTargetX,
        dampingFactor
    );

    renderer.render(scene, camera);
    requestAnimationFrame(tick);
};
tick();

问题根源分析

  1. 旋转叠加逻辑错误:每次tick都基于固定欧拉角创建目标四元数,未保留模型初始旋转的独立性,GLTF导入时的隐藏变换或异步加载时序差异会导致初始状态不稳定。
  2. 异步加载状态竞争:tick函数可能在模型未完全加载时就执行,modelRef.current的初始状态不确定,刷新时的时序波动引发问题。
  3. 变量冗余与命名错误:tick函数中重复定义targetRotationZ,且变量命名不符合实际绕Y轴旋转的逻辑,易引发混乱。

修复方案

1. 分离初始旋转与动态旋转

模型加载完成时保存初始四元数,动态旋转时基于初始状态叠加:

// 模型加载回调中添加:
modelRef.current.initialQuaternion = modelRef.current.quaternion.clone();

// tick函数中修改旋转逻辑:
if (modelRef.current && modelRef.current.initialQuaternion) {
    const targetRotationY = mouse.x * Math.PI;
    // 创建仅包含动态旋转的四元数
    const rotationQuaternion = new THREE.Quaternion();
    rotationQuaternion.setFromEuler(new THREE.Euler(0, targetRotationY, 0));
    // 初始旋转 × 动态旋转 = 最终目标旋转
    const targetQuaternion = modelRef.current.initialQuaternion.clone().multiply(rotationQuaternion);
    modelRef.current.quaternion.slerp(targetQuaternion, dampingFactor);
}

2. 确保模型加载完成后再执行旋转

在tick中增加判断,仅当模型初始状态已保存时执行旋转逻辑,避免未加载完成时的无效操作。

3. 清理冗余代码

删除tick中重复的targetRotationZ定义,统一变量命名为targetRotationY(符合绕Y轴旋转的实际逻辑)。

4. 规范GLTF导出设置

打开Blender,选中模型执行Ctrl+A应用旋转和缩放,确保导出的模型初始旋转为(0,0,0)、缩放为(1,1,1),消除导入后的隐藏变换干扰。

为什么imageMesh没有问题?

imageMesh是直接创建的,初始旋转明确为0,没有GLTF导入带来的隐藏变换,且旋转逻辑是直接修改局部Z轴,坐标系方向固定,因此不会出现状态波动。

内容的提问来源于stack exchange,提问作者Han-Jiun Ke

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 00:44:51