Skip to content

cover

ThreeJs实现物理小球

最近在重温了 Three.js,一直想找个实战项目练练手,加深对 3D 场景搭建、物理模拟以及交互功能实现的理解。在网上寻觅许久后,终于发现了一个非常契合我需求的项目 —— 一个能够生成若干具有物理特性的球体,并支持鼠标交互的酷炫案例。然而,当我满怀期待地打开代码时,却被眼前的压缩混淆代码泼了一盆冷水。密密麻麻的字符,几乎没有任何可读性,变量名也全是无意义的单字符,这无疑给我的学习之路设置了巨大的障碍。

但越是有挑战,我越是充满斗志。我经过不懈努力,一步一步将代码还原(点击查看效果),过程中不断加深了对Three.js使用的熟练程度,也学到了很多有用的技巧,下面就为大家分享这个项目的核心实现逻辑以及重要步骤代码。

封装核心功能

javascript
class ThreeWrapper {
  constructor(options) {
    this.options = {...options };
    this.initCamera();
    this.initScene();
    this.initRenderer();
    this.resize();
    this.initObservers();
  }
  // 其他方法...
}

ThreeWrapper 类对 Three.js 的核心功能进行了封装。在构造函数中,依次初始化相机、场景、渲染器,并设置初始尺寸和各种事件观察者。它就像是整个 3D 场景的大管家,负责处理场景的初始化、尺寸调整、渲染控制以及元素可见性变化等一系列重要操作。

实现物理模拟

javascript
class PhysicsObject {
  update(frameData) {
    const { config, center, positionData, sizeData, velocityData } = this;
    // 控制球体处理
    let startIndex = 0;
    if (config.controlSphere0) {
      startIndex = 1;
      controlSpherePosition.fromArray(positionData, 0);
      controlSpherePosition.lerp(center, 0.1).toArray(positionData, 0);
      tempVector1.set(0, 0, 0).toArray(velocityData, 0);
    }
    // 位置和速度更新
    for (let index = startIndex; index < config.count; index++) {
      const offset = 3 * index;
      tempVector2.fromArray(positionData, offset);
      tempVector3.fromArray(velocityData, offset);
      tempVector3.y -= frameData.delta * config.gravity * sizeData[index];
      tempVector3.multiplyScalar(config.friction);
      tempVector3.clampLength(0, config.maxVelocity);
      tempVector2.add(tempVector3);
      tempVector2.toArray(positionData, offset);
      tempVector3.toArray(velocityData, offset);
    }
    // 碰撞检测和处理
    for (let index = startIndex; index < config.count; index++) {
      const offset = 3 * index;
      tempVector2.fromArray(positionData, offset);
      tempVector3.fromArray(velocityData, offset);
      const size = sizeData[index];
      for (let otherIndex = index + 1; otherIndex < config.count; otherIndex++) {
        const otherOffset = 3 * otherIndex;
        tempVector4.fromArray(positionData, otherOffset);
        tempVector5.fromArray(velocityData, otherOffset);
        const otherSize = sizeData[otherIndex];
        tempVector6.copy(tempVector4).sub(tempVector2);
        const distance = tempVector6.length();
        const combinedSize = size + otherSize;
        if (distance < combinedSize) {
          const correction = combinedSize - distance;
          tempVector7.copy(tempVector6).normalize().multiplyScalar(0.5 * correction);
          tempVector8.copy(tempVector7).multiplyScalar(Math.max(tempVector3.length(), 1));
          tempVector9.copy(tempVector7).multiplyScalar(Math.max(tempVector5.length(), 1));
          tempVector2.sub(tempVector7);
          tempVector3.sub(tempVector8);
          tempVector2.toArray(positionData, offset);
          tempVector3.toArray(velocityData, offset);
          tempVector4.add(tempVector7);
          tempVector5.add(tempVector9);
          tempVector4.toArray(positionData, otherOffset);
          tempVector5.toArray(velocityData, otherOffset);
        }
      }
      // 边界处理
      if (Math.abs(tempVector2.x) + size > config.maxX) {
        tempVector2.x = Math.sign(tempVector2.x) * (config.maxX - size);
        tempVector3.x = -tempVector3.x * config.wallBounce;
      }
      // 其他轴边界处理类似...
      tempVector2.toArray(positionData, offset);
      tempVector3.toArray(velocityData, offset);
    }
  }
}

PhysicsObject 类的 update 方法是实现物理模拟的核心。它根据时间帧数据,首先处理控制球体的位置(如果启用),然后依次更新每个球体的位置和速度,考虑重力、摩擦力和最大速度的影响。接着,通过嵌套循环检测球体之间的碰撞,一旦发生碰撞,就计算校正向量来调整球体的位置和速度。最后,对球体进行边界检测,确保它们不会超出设定的边界范围,实现了较为真实的物理行为模拟。

实现鼠标交互

javascript
function createInteractiveElement(options) {
  const element = {
    position: new Vector2(),
    normalizedPosition: new Vector2(),
    isHovering: false,
    onEnter() { },
    onMove() { },
    onClick() { },
    onLeave() { },
    ...options
  };
  function innerFunction(domElement, elementOptions) {
    if (!elementMap.has(domElement)) {
      elementMap.set(domElement, elementOptions);
      if (!isListening) {
        document.body.addEventListener("pointermove", handlePointerMove);
        document.body.addEventListener("pointerleave", handlePointerLeave);
        document.body.addEventListener("click", handleClick);
        isListening = true;
      }
    }
  }
  innerFunction(options.domElement, element);
  element.dispose = () => {
    const domElement = options.domElement;
    elementMap.delete(domElement);
    if (elementMap.size === 0) {
      document.body.removeEventListener("pointermove", handlePointerMove);
      document.body.removeEventListener("pointerleave", handlePointerLeave);
      document.body.removeEventListener("click", handleClick);
      isListening = false;
    }
  };
  return element;
}

createInteractiveElement 函数用于创建交互式元素。它通过监听鼠标的移动、离开和点击事件,来实现与 3D 场景中的对象进行交互。当鼠标移动到特定区域时,触发相应的事件处理函数,例如在本项目中,通过射线投射器检测鼠标与场景中平面的交点,从而控制物理球体的中心位置,实现了有趣的交互效果。

通过这次代码还原和学习,我不仅完成了一个很棒的 Three.js 项目实践,也积累了宝贵的经验。在面对复杂代码时,不要畏惧,合理利用工具,结合自己的思考和分析,总能找到解决问题的办法。希望我的这段经历和分享,能够对同样在学习 Three.js 的小伙伴有所帮助,让我们一起在 3D 开发的世界里不断探索前行!

完整代码:spherePacking