如何在同一页面同时运行多个Three.js动画对象,解决新增对象致原动画失效问题
问题根因分析
- 全局变量/函数重名覆盖:两段脚本的
animate、onMouseMove函数,以及particles、renderingParent、animProps、myTween、mouseX、mouseY等变量都定义在全局作用域下,后加载的第二段脚本的同名内容会直接覆盖第一段的定义,导致第一个实例的动画逻辑被替换、变量指向第二个实例的内容,自然无法正常运行。 - 渲染画布遮挡:两个
WebGLRenderer都默认生成了占满全屏的canvas元素,后插入到body的canvas层级更高,会完全遮挡住第一个canvas,视觉上也会出现第一个对象消失的效果。 - 事件监听重复绑定:两段脚本都绑定了
mousemove事件,后绑定的onMouseMove覆盖了之前的逻辑,第一个实例的鼠标交互逻辑完全失效。
排查步骤
- 打开浏览器开发者工具的控制台面板,查看是否有变量重定义、属性未定义、Three.js版本兼容类报错
- 切换到元素面板,查看body节点下是否生成了2个canvas元素,检查两个canvas的尺寸、定位、z-index属性,确认是否存在遮挡
- 在控制台打印第一个实例的独有变量(如
renderer2、scene2),查看变量是否被覆盖、动画函数是否正常调用 - 逐段注释脚本验证:先注释第二段脚本的所有代码,确认第一个实例可正常运行;再注释第一段脚本确认第二个实例可正常运行,排除单段脚本本身的逻辑错误
解决方案
核心思路是隔离每个Three.js实例的作用域,避免全局污染,同时调整渲染画布的位置避免遮挡。
方案1:用立即执行函数(IIFE)封装每个实例的代码
将每段Three.js代码包裹在独立的IIFE中,所有变量、函数都成为局部作用域内容,不会互相覆盖。如果需要两个实例并排展示,给对应canvas添加样式调整位置即可。
示例修改后的代码:
// 第一个Three.js实例 (function() { var scene2 = new THREE.Scene(); var camera2 = new THREE.PerspectiveCamera( 40, window.innerWidth/2 / window.innerHeight, 0.1, 1000 ); var mouseX; var mouseY; var renderer2 = new THREE.WebGLRenderer(); renderer2.setSize( window.innerWidth/2, window.innerHeight ); // 给第一个canvas加定位,放在页面左侧 renderer2.domElement.style.cssText = 'position: absolute; left: 0; top: 0;'; document.body.appendChild( renderer2.domElement ); window.addEventListener("resize", function() { camera2.aspect = (window.innerWidth/2) / window.innerHeight; camera2.updateProjectionMatrix(); renderer2.setSize( window.innerWidth/2, window.innerHeight ); }); var distance = Math.min(200, (window.innerWidth/2) / 4); // 高版本Three.js已弃用Geometry,可替换为BufferGeometry var geometry = new THREE.Geometry(); for (var i = 0; i < 1000; i++) { var vertex = new THREE.Vector3(); var theta = THREE.Math.randFloatSpread(360); var phi = THREE.Math.randFloatSpread(360); vertex.x = distance * Math.sin(theta) * Math.cos(phi); vertex.y = distance * Math.sin(theta) * Math.sin(phi); vertex.z = distance * Math.cos(theta); geometry.vertices.push(vertex); } var particles = new THREE.Points(geometry, new THREE.PointsMaterial({color: 0xFFFF00, size: 2})); particles.boundingSphere = 50; var renderingParent = new THREE.Group(); renderingParent.add(particles); var resizeContainer = new THREE.Group(); resizeContainer.add(renderingParent); scene2.add(resizeContainer); camera2.position.z = 400; var animate = function () { requestAnimationFrame( animate ); renderer2.render( scene2, camera2 ); }; var myTween; function onMouseMove(event) { if(myTween) myTween.kill(); mouseX = ( event.clientX / (window.innerWidth/2) ) * 2 - 1; mouseY = - ( event.clientY / window.innerHeight ) * 2 + 1; myTween = gsap.to(particles.rotation, {duration: 0.1, x: mouseY*-1, y: mouseX}); } document.addEventListener( 'mousemove', onMouseMove, false ); animate(); var animProps = {scale: 0.5, xRot: 0, yRot: 0}; gsap.to(animProps, {duration: 20, scale: 1.5, repeat: -1, yoyo: true, ease: "sine", onUpdate: function() { renderingParent.scale.set(animProps.scale,animProps.scale,animProps.scale); }}); gsap.to(animProps, {duration: 120, xRot: Math.PI * 2, yRot: Math.PI * 4, repeat: -1, yoyo: true, ease: "none", onUpdate: function() { renderingParent.rotation.set(animProps.xRot,animProps.yRot,0); }}); })(); // 第二个Three.js实例 (function() { var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera( 40, window.innerWidth/2 / window.innerHeight, 0.1, 1000 ); var mouseX; var mouseY; var renderer = new THREE.WebGLRenderer(); renderer.setSize( window.innerWidth/2, window.innerHeight ); // 给第二个canvas加定位,放在页面右侧 renderer.domElement.style.cssText = 'position: absolute; right: 0; top: 0;'; document.body.appendChild( renderer.domElement ); window.addEventListener("resize", function() { camera.aspect = (window.innerWidth/2) / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize( window.innerWidth/2, window.innerHeight ); }); var distance = Math.min(200, (window.innerWidth/2) / 4); var geometry = new THREE.Geometry(); for (var i = 0; i < 1000; i++) { var vertex = new THREE.Vector3(); var theta = THREE.Math.randFloatSpread(360); var phi = THREE.Math.randFloatSpread(360); vertex.x = distance * Math.sin(theta) * Math.cos(phi); vertex.y = distance * Math.sin(theta) * Math.sin(phi); vertex.z = distance * Math.cos(theta); geometry.vertices.push(vertex); } // 修改第二个实例的粒子颜色方便区分 var particles = new THREE.Points(geometry, new THREE.PointsMaterial({color: 0xff00ff, size: 2})); particles.boundingSphere = 50; var renderingParent = new THREE.Group(); renderingParent.add(particles); var resizeContainer = new THREE.Group(); resizeContainer.add(renderingParent); scene.add(resizeContainer); camera.position.z = 400; var animate = function () { requestAnimationFrame( animate ); renderer.render( scene, camera ); }; var myTween; function onMouseMove(event) { if(myTween) myTween.kill(); mouseX = ( (event.clientX - window.innerWidth/2) / (window.innerWidth/2) ) * 2 - 1; mouseY = - ( event.clientY / window.innerHeight ) * 2 + 1; myTween = gsap.to(particles.rotation, {duration: 0.1, x: mouseY*-1, y: mouseX}); } document.addEventListener( 'mousemove', onMouseMove, false ); animate(); var animProps = {scale: 0.5, xRot: 0, yRot: 0}; gsap.to(animProps, {duration: 20, scale: 1.5, repeat: -1, yoyo: true, ease: "sine", onUpdate: function() { renderingParent.scale.set(animProps.scale,animProps.scale,animProps.scale); }}); gsap.to(animProps, {duration: 120, xRot: Math.PI * 2, yRot: Math.PI * 4, repeat: -1, yoyo: true, ease: "none", onUpdate: function() { renderingParent.rotation.set(animProps.xRot,animProps.yRot,0); }}); })();
方案2:单个Renderer渲染多个场景
如果需要多个Three.js内容叠加展示,不需要创建多个Renderer,同一个Renderer按顺序渲染多个场景即可,仅需要给不同场景设置不同的视口(viewport),性能更优。
注意事项
- 所有和单个Three.js实例相关的变量、函数都要放到独立作用域,避免挂载到全局window对象
- 事件监听如果需要共用,可以统一绑定后分别触发每个实例的处理逻辑,不要重复绑定同名全局处理函数
- 多Renderer场景下要主动设置canvas的样式,避免层级遮挡、尺寸冲突
- 长期维护的项目推荐使用ESModule模块化写法,每个Three.js实例对应独立模块,天然具备作用域隔离能力,更易维护
内容的提问来源于stack exchange,提问作者a s
相关产品推荐
相关产品推荐

