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

Three.js图片旋转木马:Quaternion.angleTo无法区分左右角度求解

Three.js图片旋转木马:精准旋转方向区分与平滑动画实现

问题背景

我正在用Three.js开发图片旋转木马,通过以下代码将图片(Plane平面)以圆形排列添加到组中:

function createObjects(){
    let angleLambda = 360 / 6;
    for (let c = 1; c < 7; c++) {
        let geometry = new THREE.PlaneGeometry(1, 1.5);
        let texture1 = new THREE.TextureLoader().load("pic/0" + c + ".jpg");
        let material = new THREE.MeshBasicMaterial({map: texture1, side: THREE.DoubleSide});

        let plane = new THREE.Mesh(geometry, material);
        plane.rotation.y = -THREE.MathUtils.degToRad(angleLambda * (c-1));  // 负数表示顺时针排列
        plane.translateZ(3.5);

        carousel.add(plane);
    }
}

目前我用临时方法实现了旋转到指定图片后停止的功能:

if(carousel.rotation.y <= THREE.MathUtils.degToRad(360/6*5)){
    carousel.rotation.y += 0.01;
}

但尝试用四元数的方式实现时遇到问题:

if(carousel.rotation.y <= carousel.children[4].quaternion.angleTo(camera.quaternion))
{
    carousel.rotation.y += 0.01;
}

该方法仅对前3张图片有效,目标为第5张时会停在对称的第2张——因为quaternion.angleTo()只返回0到π的角度,无法区分左右旋转方向。

提问

  1. 如何正确区分旋转木马左右侧图片的角度?必须用状态机记录位置吗?
  2. 每次累加0.01更新旋转并判断的方式太繁琐,有没有类似rotateUntil(x)或rotate(45°)的平滑旋转方法?

解决方案

1. 区分旋转方向:无需状态机,用角度差值的最短路径计算

quaternion.angleTo()的局限性在于只返回无方向的夹角,解决思路是直接计算carousel需要旋转到的目标角度,并处理角度循环的差值,判断旋转方向:

步骤1:给每个Plane记录目标旋转角度

在创建Plane时,存储carousel需要转到的目标y轴角度(此时该Plane会正对相机):

function createObjects(){
    let angleLambda = 360 / 6;
    for (let c = 1; c < 7; c++) {
        // ... 原有创建代码
        plane.rotation.y = -THREE.MathUtils.degToRad(angleLambda * (c-1));
        // 存储目标角度:carousel转到该值时,当前Plane正对相机
        plane.targetCarouselRotY = -plane.rotation.y;
        plane.translateZ(3.5);
        carousel.add(plane);
    }
}

步骤2:计算最短旋转路径与方向

在动画循环中,计算当前carousel角度与目标角度的差值,并将差值限制在[-π, π]范围内,通过差值的正负判断旋转方向:

// 假设目标是第5张图(索引4)
const targetAngle = carousel.children[4].targetCarouselRotY;
let angleDiff = targetAngle - carousel.rotation.y;

// 处理角度循环,将差值转换为[-π, π]的最短路径
angleDiff = THREE.MathUtils.euclideanModulo(angleDiff + Math.PI, 2 * Math.PI) - Math.PI;

// 设置阈值防止抖动,持续旋转直到接近目标
if (Math.abs(angleDiff) > 0.01) {
    // 按差值的正负决定旋转方向(正为逆时针,负为顺时针)
    carousel.rotation.y += Math.sign(angleDiff) * 0.01;
} else {
    // 最终校准到目标角度
    carousel.rotation.y = targetAngle;
}

这种方法无需状态机,通过数学计算直接区分左右旋转方向,避免停在对称位置。

2. 平滑旋转:用插值或动画库实现

Three.js没有内置的rotateUntil方法,但可以通过以下两种方式实现平滑旋转:

方式1:用Three.js内置的MathUtils.lerp做线性插值

通过渐进式插值让旋转更平滑,factor值越小,动画越平缓:

const targetAngle = carousel.children[4].targetCarouselRotY;
// 平滑插值,factor建议取0.03-0.1之间
carousel.rotation.y = THREE.MathUtils.lerp(carousel.rotation.y, targetAngle, 0.05);

// 当角度差足够小时,锁定目标角度
const angleDiff = THREE.MathUtils.euclideanModulo(targetAngle - carousel.rotation.y + Math.PI, 2 * Math.PI) - Math.PI;
if (Math.abs(angleDiff) < 0.001) {
    carousel.rotation.y = targetAngle;
}

方式2:用Tween.js实现带缓动的动画

Three.js官方提供的Tween.js可以实现更丰富的缓动效果,比如先快后慢的平滑过渡:

// 引入Tween.js(Three.js自带,路径为three/addons/libs/tween.module.js)
import { Tween, Easing } from 'three/addons/libs/tween.module.js';

// 创建补间动画,1秒内旋转到目标角度
new Tween(carousel.rotation)
    .to({ y: targetAngle }, 1000)
    .easing(Easing.Quadratic.InOut) // 缓动函数,可选线性、指数等
    .onComplete(() => {
        // 动画结束后的回调
        carousel.rotation.y = targetAngle;
    })
    .start();

// 必须在动画循环中更新Tween
function animate() {
    requestAnimationFrame(animate);
    Tween.update();
    renderer.render(scene, camera);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 19:40:57