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

Three.js立方体动画失效问题修复及旋转速度设置咨询

修复Three.js立方体旋转异常并设置旋转速度

我来帮你解决这个问题,你的代码里有两个核心问题导致旋转异常,同时我也会告诉你如何稳定控制旋转速度:

一、异常原因分析

  1. 重复的动画循环:每次点击按钮都会调用animate(),如果之前的动画还没结束,就会开启新的requestAnimationFrame循环,多个循环同时修改立方体的四元数,导致旋转混乱,出现跳转现象。
  2. 四元数相等判断的精度问题:quaternion.equals()是严格的浮点数相等比较,由于动画中的旋转是增量式的,浮点数精度误差会导致这个条件几乎永远不会触发,或者偶尔因为帧率突变(比如页面卡顿)一次旋转过度,出现跳转。

二、修复方案

1. 避免重复动画循环

维护一个动画ID变量,每次触发新的旋转前,先取消之前的动画循环,确保同一时间只有一个动画在运行。

2. 替换四元数相等判断逻辑

用四元数的点积(dot方法)来判断旋转是否接近目标:两个四元数的点积绝对值越接近1,说明它们表示的旋转角度差越小。设置一个阈值(比如0.9999),当点积绝对值超过这个值时,就认为旋转到位,直接将立方体的四元数设置为目标值,然后停止动画。

3. 稳定控制旋转速度

定义一个基于时间的速度变量,比如rotationSpeed = Math.PI(表示每秒旋转180度),然后在rotateTowards中使用rotationSpeed * delta作为每帧允许旋转的最大角度,这样速度不会受帧率影响,始终保持稳定。

三、修正后的代码

Vue集成版

new Vue({ 
    el: '#app', 
    data () { 
        return { 
            camera: null, 
            scene: null, 
            renderer: null, 
            cube: null, 
            angle: null,
            clock: null,
            animationId: null, // 新增:保存动画ID
            rotationSpeed: Math.PI // 新增:旋转速度,每秒π弧度(180度)
        } 
    }, 
    methods: { 
        init: function () { 
            this.camera = new THREE.PerspectiveCamera(1, 1); 
            this.camera.position.z = 200; 
            // 创建场景
            this.scene = new THREE.Scene(); 
            this.clock = new THREE.Clock(); 
            this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); 
            let container = document.getElementById('container') 
            this.renderer.setSize(container.offsetWidth, container.offsetHeight); 
            container.appendChild(this.renderer.domElement); 
            let cube2 = this.createCube() 
            cube2.name = "cube2" 
            this.scene.add(cube2); 
            this.cube = cube2; // 新增:直接保存立方体引用,避免每次查找children[0]
        }, 
        createCube: function () { 
            // 几何体
            // 1. 创建空几何体
            let geometry = new THREE.Geometry(); 
            // 2. 向几何体添加顶点
            geometry.vertices.push( 
                // 顶点[0-3]在+z轴方向
                new THREE.Vector3(-1, 1, 1), 
                new THREE.Vector3(-1, -1, 1), 
                new THREE.Vector3(1, -1, 1), 
                new THREE.Vector3(1, 1, 1), 
                // 顶点[4-7]在-z轴方向
                new THREE.Vector3(-1, 1, -1), 
                new THREE.Vector3(-1, -1, -1), 
                new THREE.Vector3(1, -1, -1), 
                new THREE.Vector3(1, 1, -1), 
            ); 
            // 3. 按指定顺序连接顶点构建面
            let b = 0x1db0ec 
            let y = 0xffef3a 
            let r = 0xea353d 
            let w = 0xffffff 
            // 设置半面
            geometry.faces.push(new THREE.Face3(0, 1, 2)); // 蓝色
            geometry.faces.push(new THREE.Face3(0, 2, 3)); // 黄色
            geometry.faces.push(new THREE.Face3(5, 4, 6)); // 白色
            geometry.faces.push(new THREE.Face3(6, 4, 7)); // 红色
            // 设置全幅面
            geometry.faces.push(new THREE.Face3(1, 0, 5)); // 蓝色
            geometry.faces.push(new THREE.Face3(5, 0, 4));
            geometry.faces.push(new THREE.Face3(1, 5, 2)); // 白色
            geometry.faces.push(new THREE.Face3(5, 6, 2));
            geometry.faces.push(new THREE.Face3(2, 6, 3)); // 红色
            geometry.faces.push(new THREE.Face3(3, 6, 7));
            geometry.faces.push(new THREE.Face3(0, 3, 4)); // 黄色
            geometry.faces.push(new THREE.Face3(3, 7, 4));
            // 设置面颜色
            geometry.faces[0].color.setHex(b); // 半面
            geometry.faces[1].color.setHex(y);
            geometry.faces[2].color.setHex(w);
            geometry.faces[3].color.setHex(r);
            geometry.faces[4].color.setHex(b); // 全幅面
            geometry.faces[5].color.setHex(b);
            geometry.faces[6].color.setHex(w);
            geometry.faces[7].color.setHex(w);
            geometry.faces[8].color.setHex(r);
            geometry.faces[9].color.setHex(r);
            geometry.faces[10].color.setHex(y);
            geometry.faces[11].color.setHex(y);
            // 材质
            // 创建材质
            let material = new THREE.MeshBasicMaterial({ 
                vertexColors: THREE.FaceColors, 
                wireframe: false, 
            }); 
            // 网格
            let cube = new THREE.Mesh(geometry, material); 
            return cube 
        }, 
        rotateTo: function (face) { 
            // 新增:先取消之前的动画
            if (this.animationId) {
                cancelAnimationFrame(this.animationId);
                this.animationId = null;
            }
            // 重置时钟,避免delta过大
            this.clock.reset();

            if (face == 'yellow') this.angle = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI / 2, Math.PI / 2)); 
            else if (face == 'red') this.angle = new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI / 2, 0, Math.PI / 2)); 
            else if (face == 'blue') this.angle = new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI / 2, 0, - Math.PI / 2)); 
            else if (face == 'white') this.angle = new THREE.Quaternion().setFromEuler(new THREE.Euler(- Math.PI / 2, Math.PI / 2, 0)); 
            else if (face == 'yb') this.angle = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0)); 
            else if (face == 'rw') this.angle = new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI, 0, 0)); 
            this.animate() 
        }, 
        animate: function () { 
            this.animationId = requestAnimationFrame(this.animate.bind(this)); // 绑定this,保存动画ID
            let delta = this.clock.getDelta(); 
            // 使用自定义的旋转速度
            this.cube.quaternion.rotateTowards(this.angle, this.rotationSpeed * delta); 
            this.renderer.render(this.scene, this.camera); 
            // 使用点积判断旋转是否到位
            const dotProduct = this.cube.quaternion.dot(this.angle);
            if (Math.abs(dotProduct) > 0.9999) { 
                // 直接设置为目标四元数,消除精度误差
                this.cube.quaternion.copy(this.angle);
                cancelAnimationFrame(this.animationId); 
                this.animationId = null;
                this.renderer.render(this.scene, this.camera); // 最后渲染一次确保正确
            } 
        } 
    }, 
    mounted () { 
        this.init(); 
        this.renderer.render(this.scene, this.camera); 
    } 
}) 
#container { 
    background-color: #aaa; 
    width: 20em; 
    height: 20em; 
} 
<script src="https://unpkg.com/vue"></script> 
<script src="https://threejs.org/build/three.min.js"></script> 
<div id="app"> 
    <div> 
        <button v-on:click="rotateTo('yellow')">yellow</button> 
        <button v-on:click="rotateTo('red')">red</button> 
        <button v-on:click="rotateTo('blue')">blue</button> 
        <button v-on:click="rotateTo('white')">white</button> 
        <button v-on:click="rotateTo('yb')">yellow/blue</button> 
        <button v-on:click="rotateTo('rw')">red/white</button> 
    </div> 
    <div id="container"></div> 
</div> 

独立Three.js版

var camera = null; 
var scene = null; 
var renderer = null; 
var cube = null; 
var angle = null; 
var clock = null;
var animationId = null; // 新增:保存动画ID
var rotationSpeed = Math.PI; // 新增:旋转速度,每秒π弧度(180度)

init(); 
renderer.render(scene, camera); 

function init() { 
    camera = new THREE.PerspectiveCamera(1, 1); 
    camera.position.z = 200; 
    // 创建场景
    scene = new THREE.Scene(); 
    clock = new THREE.Clock(); 
    renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); 
    let container = document.getElementById('container'); 
    renderer.setSize(container.offsetWidth, container.offsetHeight); 
    container.appendChild(renderer.domElement); 
    let cube2 = createCube(); 
    cube2.name = "cube2"; 
    scene.add(cube2); 
    cube = cube2; // 新增:直接保存立方体引用
} 

function createCube() { 
    // 几何体
    // 1. 创建空几何体
    let geometry = new THREE.Geometry(); 
    // 2. 向几何体添加顶点
    geometry.vertices.push( 
        // 顶点[0-3]在+z轴方向
        new THREE.Vector3(-1, 1, 1), 
        new THREE.Vector3(-1, -1, 1), 
        new THREE.Vector3(1, -1, 1), 
        new THREE.Vector3(1, 1, 1), 
        // 顶点[4-7]在-z轴方向
        new THREE.Vector3(-1, 1, -1), 
        new THREE.Vector3(-1, -1, -1), 
        new THREE.Vector3(1, -1, -1), 
        new THREE.Vector3(1, 1, -1), 
    ); 
    // 3. 按指定顺序连接顶点构建面
    let b = 0x1db0ec; 
    let y = 0xffef3a; 
    let r = 0xea353d; 
    let w = 0xffffff; 
    // 设置半面
    geometry.faces.push(new THREE.Face3(0, 1, 2)); // 蓝色
    geometry.faces.push(new THREE.Face3(0, 2, 3)); // 黄色
    geometry.faces.push(new THREE.Face3(5, 4, 6)); // 白色
    geometry.faces.push(new THREE.Face3(6, 4, 7)); // 红色
    // 设置全幅面
    geometry.faces.push(new THREE.Face3(1, 0, 5)); // 蓝色
    geometry.faces.push(new THREE.Face3(5, 0, 4));
    geometry.faces.push(new THREE.Face3(1, 5, 2)); // 白色
    geometry.faces.push(new THREE.Face3(5, 6, 2));
    geometry.faces.push(new THREE.Face3(2, 6, 3)); // 红色
    geometry.faces.push(new THREE.Face3(3, 6, 7));
    geometry.faces.push(new THREE.Face3(0, 3, 4)); // 黄色
    geometry.faces.push(new THREE.Face3(3, 7, 4));
    // 设置面颜色
    geometry.faces[0].color.setHex(b); // 半面
    geometry.faces[1].color.setHex(y);
    geometry.faces[2].color.setHex(w);
    geometry.faces[3].color.setHex(r);
    geometry.faces[4].color.setHex(b); // 全幅面
    geometry.faces[5].color.setHex(b);
    geometry.faces[6].color.setHex(w);
    geometry.faces[7].color.setHex(w);
    geometry.faces[8].color.setHex(r);
    geometry.faces[9].color.setHex(r);
    geometry.faces[10].color.setHex(y);
    geometry.faces[11].color.setHex(y);
    // 材质
    // 创建材质
    let material = new THREE.MeshBasicMaterial({ 
        vertexColors: THREE.FaceColors, 
        wireframe: false, 
    }); 
    // 网格
    let cube = new THREE.Mesh(geometry, material); 
    return cube; 
} 

function rotateTo(face) { 
    // 新增:先取消之前的动画
    if (animationId) {
        cancelAnimationFrame(animationId);
        animationId = null;
    }
    // 重置时钟
    clock.reset();

    if (face == 'yellow') angle = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI / 2, Math.PI / 2)); 
    else if (face == 'red') angle = new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI / 2, 0, Math.PI / 2)); 
    else if (face == 'blue') angle = new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI / 2, 0, -Math.PI / 2)); 
    else if (face == 'white') angle = new THREE.Quaternion().setFromEuler(new THREE.Euler(-Math.PI / 2, Math.PI / 2, 0)); 
    else if (face == 'yb') angle = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0)); 
    else if (face == 'rw') angle = new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI, 0, 0)); 
    animate(); 
} 

function animate() { 
    animationId = requestAnimationFrame(animate); 
    let delta = clock.getDelta(); 
    // 使用自定义旋转速度
    cube.quaternion.rotateTowards(angle, rotationSpeed * delta); 
    renderer.render(scene, camera); 
    // 使用点积判断旋转是否到位
    const dotProduct = cube.quaternion.dot(angle);
    if (Math.abs(dotProduct) > 0.9999) { 
        // 直接设置为目标四元数
        cube.quaternion.copy(angle);
        cancelAnimationFrame(animationId); 
        animationId = null;
        renderer.render(scene, camera); // 最后渲染一次
    } 
} 
#container { 
    background-color: #aaa; 
    width: 20em; 
    height: 20em; 
} 
<script src="https://threejs.org/build/three.min.js"></script> 
<div> 
    <button onclick="rotateTo('yellow')">yellow</button> 
    <button onclick="rotateTo('red')">red</button> 
    <button onclick="rotateTo('blue')">blue</button> 
    <button onclick="rotateTo('white')">white</button> 
    <button onclick="rotateTo('yb')">yellow/blue</button> 
    <button onclick="rotateTo('rw')">red/white</button> 
</div> 
<div id="container"></div> 

额外说明

  • 旋转速度调整:修改rotationSpeed的值即可,比如设置为Math.PI * 2就是每秒旋转360度,Math.PI / 2就是每秒旋转90度。
  • 保存立方体引用:直接把创建的立方体赋值给变量,避免每次从scene.children[0]查找,代码更高效也更可靠。
  • 重置时钟:每次触发新旋转时重置时钟,避免页面隐藏后回到页面时delta值过大,导致一次旋转过度。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 09:34:57