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

如何在同一页面同时运行多个Three.js动画对象,解决新增对象致原动画失效问题

问题根因分析
  1. 全局变量/函数重名覆盖:两段脚本的animate、onMouseMove函数,以及particles、renderingParent、animProps、myTween、mouseX、mouseY等变量都定义在全局作用域下,后加载的第二段脚本的同名内容会直接覆盖第一段的定义,导致第一个实例的动画逻辑被替换、变量指向第二个实例的内容,自然无法正常运行。
  2. 渲染画布遮挡:两个WebGLRenderer都默认生成了占满全屏的canvas元素,后插入到body的canvas层级更高,会完全遮挡住第一个canvas,视觉上也会出现第一个对象消失的效果。
  3. 事件监听重复绑定:两段脚本都绑定了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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 08:36:06