Three.js立方体动画失效问题修复及旋转速度设置咨询
修复Three.js立方体旋转异常并设置旋转速度
我来帮你解决这个问题,你的代码里有两个核心问题导致旋转异常,同时我也会告诉你如何稳定控制旋转速度:
一、异常原因分析
- 重复的动画循环:每次点击按钮都会调用
animate(),如果之前的动画还没结束,就会开启新的requestAnimationFrame循环,多个循环同时修改立方体的四元数,导致旋转混乱,出现跳转现象。 - 四元数相等判断的精度问题:
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
相关产品推荐
相关产品推荐

