继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

🚀 使用 React Three Fiber 构建一个互动的 3D 火箭复活节彩蛋

ibeautiful
关注TA
已关注
手记 514
粉丝 108
获赞 529

当我们在中国领英上达到1000个关注者时,我们知道必须以一种特别的方式来庆祝!受到 Vercel 在 使用 React Three Fiber 构建互动3D活动徽章 这篇文章的启发,我们想要将这种感觉带入自己的有趣项目中。那么,还有什么比在我们新网站 zerodays.dev 上发布一个3D火箭复活节彩蛋更好的庆祝方式呢?

这是一件很有趣的事情,就像数字版的指尖陀螺一样,但要酷得多。我们在开发它的时候玩得很开心,今天我们要带你们看看幕后的情况。

让我们直接进入正题。

🚀 目录:

  1. 🏁 设置
  • 🛠️ 构建我们3D火箭的工具

  • 🎬 设置3D场景

  • 🎥 摄像头选择:为何选择正交相机

  • 💡 快速提示:使用eventSource处理事件

  1. 🚀 主角:我们的互动火箭
  • 主要功能

  • 火箭跟随移动:跟踪鼠标

  • 💡 快速提示:处理 <group> 上的事件

  • 🚀 总结 - 火箭

  1. 💥 粒子系统:为火箭的尾焰供能
  • 关键概念

  • 💥 总结 - 粒子

  1. 🪨 SpaceRocks: 带有物理效果和分裂的陨石
  • 主要功能

  • 🪨 概要 - SpaceRocks

  1. 🌌 SpaceEnvironment: 浸入式太空背景
  • 主要功能

  • 💡 小贴士:使用 window.devicePixelRatio 实现一致的着色器渲染

  • 🌌 概要 - SpaceEnvironment

  1. 🏎️ 性能提升:优化流畅的火箭体验
  • 🚀 1. 帧循环控制:“Always” vs. “Demand”

  • 🚀 2. 自定义 useViewportSize 钩子

  • 🚀 3. 行星的网格实例化

  • 🚀 4. 使用 Refs 存储不重新渲染的值

  • 🚀 5. 通用的 React-Three-Fiber 性能技巧

  • 💡 快速提示:某些设备上的画布尺寸限制

  1. 🚀 总结:最后的倒计时

🏁 设置:

🛠️ 构建我们的3D火箭的工具

我们保持了精简但强大的技术栈,让这个复活节彩蛋 起来。

🎬 设置3D场景

第一步是为我们的火箭设置基本的3D场景。我们使用了react-three-fiber来处理在React组件内渲染场景,并使用了react-three-drei来处理一些辅助功能,比如相机和光照。我们还集成了react-three-rapier来处理物理和碰撞检测。

这里是一个初始设置的简化版本:

    'use client';
    
    import { Canvas } from '@react-three/fiber';
    import { OrthographicCamera } from '@react-three/drei';
    import { Physics } from '@react-three/rapier';
    import Rocket from '@/components/three/rocket';
    import SpaceEnvironment from '@/components/three/space-environment';
    import SpaceRocks from '@/components/three/space-rocks';
    
    const RocketScene = () => {
      return (
        <Canvas>
          <OrthographicCamera makeDefault position={[0, 0, 0]} zoom={1} />
          <ambientLight intensity={0.4} />
          <directionalLight position={[-30, 100, 0]} intensity={0.8} />
    
          <Physics gravity={[0, 0, 0]}>
            <Rocket />
            <SpaceRocks />
          </Physics>
    
          <SpaceEnvironment />
        </Canvas>
      );
    };
    
    export default RocketScene;
    

进入全屏模式 退出全屏模式

这为我们设置了3D世界,包括:

  • Rocket : 展示的主角。
  • SpaceEnvironment : 一个酷炫的太空背景。
  • SpaceRocks : 浮动的太空碎片,增添了额外的魅力和危险!火箭可以与这些碎片碰撞,导致它们分裂开来,从而带来更动态和互动的体验。
  • Lighting : 用于营造宇宙光芒的环境光和定向光。
  • Physics : 处理物体之间的碰撞和交互。

🎥 摄像机选择:为何选择正交摄像机

我们选择了一个正交相机来构建我们的场景,因为它能保持一切比例一致,无论火箭飞到哪里。由于我们的火箭场景贯穿整个页面(高度由可滚动内容决定),正交相机让我们能够平滑地上下飞行火箭,而不会出现任何奇怪的透视失真。它确保了在您滚动页面并与火箭互动时,始终提供一致且友好的用户体验。

💡 快速提示:使用eventSource处理事件

为了确保3D火箭场景仍然可以响应指针事件(例如鼠标或触摸交互),即使其他页面内容覆盖了它,我们在Canvas组件中使用了eventSource属性。这告诉react-three-fiber在哪里监听事件。通过将eventSource设置为根元素(#root),火箭可以在其他内容层之上接收到事件。

这里是我们的设置方式:

    <Canvas
      eventSource={document.getElementById('root')}  // 指定监听指针事件的DOM元素
      eventPrefix="page"         // 转换为画布指针x/y事件的前缀
    />
    

进入全屏模式 退出全屏模式

🚀 主角:我们的互动火箭

火箭是整个展示的主角——与环境互动,响应鼠标移动,并在页面上发射。为了使其真正动态,我们使用了 react-three-fiber 进行渲染和 react-three-rapier 进行物理模拟。让我们来分解核心功能。

主要功能:

  • 状态机:处理火箭的不同状态——空闲、发射、跟随(追踪鼠标)和重置。
  • 碰撞和物理:使用CapsuleColliderRoundConeCollider,火箭可以与太空岩石发生碰撞。物理还帮助模拟火箭发射时的运动和冲量。
  • 悬停和点击交互:火箭在鼠标悬停时会放大,点击则会发射或重置其位置。
  • 平滑移动:火箭追踪鼠标,使用向量计算距离并施加力,同时在useFrame()中平滑旋转以跟随其飞行方向。
  • 粒子:在火箭移动时添加动态排气粒子。

这里是一个关键片段:

    const Rocket = memo(() => {
        // 引用和钩子
        const rocketHovered = useRef(false);
        ...
    
        // 状态机
        const { value: rocketState, transitionTo } = useStateMachine({
          initial: 'idle',
          states: {
            idle: {},
            launching: { onEnter: () => { setTimeout(() => transitionTo('following'), 500); } },
            following: {},
            resetting: { onEnter: async () => { await resetRocket(); transitionTo('idle'); } },
          },
        });
    
        // 火箭重置逻辑
        const resetRocket = useCallback(() => {
          ... // 重置火箭的位置、速度、旋转
        }, [viewportSize]);
    
        // 帧逻辑(每帧执行)
        useFrame((state, delta) => {
          ...
          // 当悬停时放大火箭
          rocketHovered.current
            ? meshRef.current?.scale.lerp(new Vector3(1.1, 1.1, 1.1), delta * 5)
            : meshRef.current?.scale.lerp(new Vector3(1, 1, 1), delta * 5);
    
          // 执行状态特定逻辑
          switch (rocketState) {
            case 'idle': resetRocket(); break;
            case 'launching': rocket.applyImpulse(..., true); break;
            case 'following': { ... } // 让火箭朝指针方向移动,应用旋转
          }
        });
    
        return (
          <group>
            <RigidBody ref={rigidBodyRef}>
              <CapsuleCollider />
              <RoundConeCollider />
              <group
                ref={meshRef}
                onPointerEnter={() => { rocketHovered.current = true; }}
                onPointerLeave={() => { rocketHovered.current = false; }}
                onClick={() => rocketState === 'following' ? transitionTo('resetting') : transitionTo('launching')}
              >
                <RocketModel />
              </group>
            </RigidBody>
    
            {/* 我们自己的火箭尾气云粒子效果组件 */}
            <Particles
              visible={rocketState === 'launching' || rocketState === 'following'}
              objectRef={exhaustMeshRef}
              config={{ ... }}
            />
          </group>
        );
    });
    

进入全屏模式 退出全屏模式

飞船跟随移动:跟踪鼠标位置

我们火箭最酷的部分之一就是它能够在飞行过程中跟随鼠标移动。这得益于 react-three-fiber 中的 useFrame 钩子,它允许我们在每一帧更新火箭的位置和旋转。这里展示了我们如何处理火箭在追踪用户指针时的平滑移动和旋转。

关键点:

  • 鼠标跟踪:我们将指针的归一化设备坐标(NDC)转换为视口坐标,并将火箭移动到该点。
  • 距离计算:我们计算火箭当前位置与目标之间的距离,并据此施加正确的方向力。
  • 旋转:火箭平滑地旋转以面向其移动的方向,提供真实的飞行体验。
这是我们简化后的实现方法:
    useFrame((state, delta) => {
      const rocket = rigidBodyRef.current;
      if (!rocket || !viewportSize) return;
    
      // 将指针坐标转换为视口空间
      const x = (state.pointer.x * viewportSize.width) / 2;
      const y = (state.pointer.y * viewportSize.height) / 2;
    
      // 计算当前火箭位置与目标位置之间的距离
      const currentPos = rocket.translation() as Vector3;
      const targetPos = new Vector3(x, y, currentPos.z);
      const distance = targetPos.distanceTo(currentPos);
    
      // 应用冲量使火箭朝目标移动
      direction.current.copy(targetPos).sub(currentPos).normalize().multiplyScalar(delta * 1000 * distance);
      rocket.applyImpulse(direction.current, true);
    
      // 计算旋转角度并平滑旋转火箭
      const rotationAngle = Math.atan2(y - currentPos.y, x - currentPos.x);
      targetQuaternion.current.setFromAxisAngle(rotationAxis.current, rotationAngle - Math.PI / 2);
      slerpedQuaternion.current.slerpQuaternions(rocket.rotation(), targetQuaternion.current, 0.08);
      rocket.setRotation(slerpedQuaternion.current, true);
    });
    

进入全屏模式 退出全屏模式

💡 快速提示:处理 <group> 上的事件

当处理 three.js 和光线投射(raycasting)时,通常会在一个 <group> 内包含多个网格(mesh)或子网格。这会导致每个子网格被光线投射器击中时,会触发多个 onClickonPointerEnter 事件。为了避免这种情况,我们可以使用 e.stopPropagation() 来防止多个事件触发。

这里有一个快速示例:

    <group
      name="火箭"
      onClick={(e) => {
        e.stopPropagation(); // 阻止多次 onClick 调用
    
      }}
    >
      <RocketModel />
      <pointLight />
      <mesh ... />
    </group>
    

进入全屏模式 退出全屏模式

🚀 概要 - 火箭:

  • 状态机:控制火箭的不同模式——空闲发射跟随(跟踪鼠标)和重置。这确保了状态之间平滑的过渡和交互。

  • 物理和碰撞:使用 CapsuleColliderRoundConeCollider,火箭可以与太空岩石等物体发生碰撞。基于物理的运动和冲量处理确保了碰撞的真实响应。

  • 鼠标交互:当鼠标悬停在火箭上时,火箭会变大,这是由于缩放效果。点击火箭会发射火箭或将其重置为初始状态,为用户增添了趣味的交互体验。

  • 平滑鼠标追踪:火箭平滑地追踪鼠标位置,根据指针的距离计算并施加力。它朝其移动的方向旋转,给人一种真正的飞行感觉。

  • 粒子效果 : 动态的尾气粒子跟随火箭移动,为火箭发射和移动过程增添了视觉效果。

💥 粒子系统:为火箭尾焰供能

我们的火箭如果没有一些壮观的尾气粒子效果,就不完整了!我们使用 three.js 构建了一个自定义粒子系统,并通过着色器进行控制,以实现那种动态效果。该系统相当灵活,允许我们控制从粒子生命周期到速度和湍流的各个方面。

关键概念:

  1. 自定义着色器材质:我们使用ShaderMaterial来控制粒子的外观,例如大小和颜色。片段着色器中包含根据粒子年龄使其淡出的逻辑。
      uniforms: {
        color: { value: new Color('青色') },
        pointSize: { value: 1.0 },
        dpr: { value: window.devicePixelRatio },
      },
      vertexShader: `
        attribute float age;
        varying float vAge;
        void main() {
          vAge = age;
          gl_PointSize = ...;
          gl_Position = ...;
        }
      `,
      fragmentShader: `
        varying float vAge;
        void main() {
          float alpha = 1.0 - vAge; // 根据年龄淡出
          gl_FragColor = vec4(color, alpha);
        }
      `,
    });
    
  1. 粒子初始化:粒子以随机的位置和速度初始化,创造出排气的真实扩散效果。每个粒子具有位置、年龄和大小等属性。
    function 初始化粒子(...) {
      const 位置 = [], 速度 = [], 年龄 = [], 大小 = [];
      for (let i = 0; i < 数量; i++) {
        位置.push(...); // 设置初始位置
        速度.push(new Vector3(...)); // 随机速度
        年龄.push(-i / 发射速率); // 交错粒子发射
        大小.push(大小 + 大小变化 * (Math.random() - 0.5) * 2);
      }
      return { 位置, 速度, 年龄, 大小 };
    }
    
  1. 每帧更新:每帧,我们根据粒子的速度更新粒子的位置,并施加重力和湍流。随着粒子老化,它们会逐渐淡出,并在达到生命周期限制时重置。
useFrame((state, delta) => {
  // 每一帧更新粒子的位置和年龄
  for (let i = 0; i < config.maxParticles; i++) {
    if (ages[i] >= 1.0) resetParticle(i); // 重置已过期的粒子
    else updateParticle(i, delta);
  }
});
  1. 动态粒子重置:当粒子的年龄达到其限制时,它会被重置到一个新的位置、速度和年龄,使其准备好进入下一个发射周期。
    function resetParticle(index) {
      // 将粒子重置为新的随机位置和速度
      positions[index * 3] = ...;
      velocities[index] = new Vector3(...);
      ages[index] = 0;
    }
    

组件结构:

粒子系统以 <points> 网格的形式渲染,并使用缓冲属性来控制位置、年龄和大小。一个自定义的 ShaderMaterial 控制粒子的外观和行为。

    <points visible={visible} ref={meshRef}>
        <bufferGeometry attach="geometry">
          <bufferAttribute attach="attributes-position" {...positions} />
          <bufferAttribute attach="attributes-age" array={ages} itemSize={1} />
          <bufferAttribute attach="attributes-particleSize" {...sizes} itemSize={1} />
        </bufferGeometry>
        <primitive attach="material" object={ParticleShaderMaterial} transparent />
    </points>
    

进入全屏模式 退出全屏模式

💥 概要 - 粒子:

  • 自定义着色器材质 : 我们使用 ShaderMaterial 来管理粒子的外观,处理大小、颜色和淡出效果。粒子根据其年龄淡出,提供逼真的排气效果。

  • 粒子初始化 : 每个粒子被随机初始化为具有位置、速度、年龄和大小。这种随机性使得火箭移动时尾气呈现出自然的扩散效果。

  • 每帧更新:每帧,系统根据粒子的速度更新粒子的位置,并应用重力和湍流等效果。随着粒子老化,它们会逐渐淡出,并在其生命周期结束时重置。

  • 动态粒子重置 : 当一个粒子失效时,它会被重置为具有新的随机属性(位置、速度等),确保持续发射而无需从头生成新的粒子。

  • 高效结构 : 系统利用带有缓冲属性的 <points> 网格高效处理粒子数据。ShaderMaterial 负责渲染和视觉效果,确保一切保持高性能。

🪨 SpaceRocks: 带有物理效果和分裂的太空岩石

SpaceRocks 组件通过生成类似小行星的岩石来为我们的场景增添活力,这些岩石会在周围漂浮,并在碰撞时分裂。使用 Rapier 物理引擎,这些岩石会四处弹跳,与火箭互动,并在受到足够大的撞击时动态分裂。

主要功能:

  1. 岩石几何:每个岩石都是使用由随机顶点组成的**ConvexGeometry** 创建的。这会产生不规则的岩石状形状。
const generateRockGeometry = () => {
  const vertices: Vector3[] = [];
  for (let i = 0; i < 50; i++) {
    vertices.push(new Vector3((Math.random() - 0.5) * 3, ...));
  }
  const geometry = new ConvexGeometry(vertices);
  geometry.scale(5, 5, 5);
  return geometry;
};
    
  1. 岩石分裂:当岩石以足够的力量发生碰撞时,它会被分裂成两个较小的岩石。分裂过程通过计算沿定义平面的交点,然后从原始岩石生成两个新的岩石来处理。
    const splitRock = (rockGeometry: ConvexGeometry) => {
      const verticesA: Vector3[] = [], verticesB: Vector3[] = [];
      // 沿一个平面将几何体分割开
      const plane = new Plane(new Vector3(1, 0, 0), 0);
      const geometryA = new ConvexGeometry(verticesA), geometryB = new ConvexGeometry(verticesB);
      return [geometryA, geometryB];
    };
    
  1. 碰撞处理:当岩石与火箭或其他物体发生碰撞时,我们计算碰撞力。如果碰撞力超过阈值,岩石会分裂。
onContactForce={(payload) => {
    const forceMag = payload.totalForceMagnitude / 100000;
    if (forceMag > 80) handleCollision(key, forceVec); // 只有当力足够强时才进行碰撞处理
}}

岩石生成:

  • 岩石被随机放置在一个网格中并赋予初始速度。它们被赋予了诸如速度、角速度以及在碰撞时分裂的能力等属性。
    for (let i = 0; i < 10; i++) {
      const rockId = `${idPrefix}_rock_${i + 1}`;
      rockMap.set(rockId, {
        ref: createRef<RapierRigidBody>(),
        position: gridCells[i],
        velocity: new Vector3((Math.random() - 0.5) * 10, ...),
        geometry: generateRockGeometry(),
        canSplit: true,
        scale: 3 + Math.random() * 4,
      });
    }
    

组件结构:

每个岩石都是一个 RigidBody,具有诸如恢复系数(弹跳性)和摩擦力之类的属性。这些岩石会动态地相互作用,弹跳并撞击环境,碰撞时还会分裂。

    <RigidBody
        ref={rock.ref}
        position={rock.position}
        linearVelocity={rock.velocity.toArray()}
        restitution={0.9} // 弹性碰撞
        friction={0.1}
        onContactForce={(payload) => handleCollision(key, forceVec)} // 处理岩石分裂
    >
        {/* 外壳 - 自动为凸几何体生成网格碰撞器 */}
        <MeshCollider type="hull">
        <mesh geometry={rock.geometry} scale={rock.scale}>
            <primitive attach="material" object={rockMaterial} />
        </mesh>
        </MeshCollider>
    </RigidBody>
    

进入全屏模式 退出全屏模式

🪨 概要 - SpaceRocks:

  • 动态几何体:岩石使用ConvexGeometry随机生成,形状不规则。
  • 碰撞与分裂:岩石在受到高力度碰撞时会分裂成更小的岩石,从而产生动态交互。
  • 物理集成:使用Rapier实现真实的运动、摩擦和弹跳碰撞,使岩石在场景中的表现更加逼真。

这个系统为火箭在太空中航行增添了更多的互动性和乐趣!

🌌 SpaceEnvironment: 沉浸式太空背景

SpaceEnvironment 组件为我们的火箭场景带来了动态的背景。它包括一个星场和散布于太空中的行星集合。这里展示了我们如何使用 three.jsreact-three-fiber 和着色器来实现自定义效果。

主要功能:

  1. 动态星场:星星在圆柱形体积(高度 = 页面高度)中随机生成,并通过一个自定义着色器控制大小和透明度,使其闪烁。星星似乎在闪烁和渐隐,模拟出一个生动、动态的太空场景。
      uniforms: { color: { value: new Color('white') }, opacity: { value: 0 }, time: { value: 0 }, dpr: { value: 1.0 }},
      vertexShader: `
        varying float vTwinkle; 
        uniform float time;
        void main() {
          vTwinkle = 0.5 + 0.5 * sin(time + position.x * 10.0);
          gl_PointSize = ...;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
      fragmentShader: `
        varying float vTwinkle;
        void main() {
          gl_FragColor = vec4(color, opacity * vTwinkle);
        }
      `,
      transparent: true,
    });
    
  1. 实例化星球:我们使用实例化高效地渲染位于太空随机位置的星球。每个星球都有随机的颜色、大小和位置,使场景充满多样性。
const 行星 = useMemo(() => {
  return Array.from({ length: 20 }).map((_, 索引) => (
    <Instance
      key={索引}
      position={getRandomInCylinder(半径, 高度, 内边距)}
      scale={Math.random() * 100}
      color={行星颜色[索引 % 行星颜色.length]}
    />
  ));
}, [半径, 高度, 内边距]);
    
  1. 旋转的空间环境:整个空间环境缓慢旋转,使得背景中的星星和行星看起来在移动。
useFrame((state, delta) => {
  environmentRef.current.rotation.y += delta * 0.015;
  StarShaderMaterial.uniforms.time.value = state.clock.getElapsedTime();
});

💡 快速提示:使用 window.devicePixelRatio 实现一致的着色器渲染

当处理涉及 点大小(如星场中的星星)的着色器时,必须考虑不同的屏幕分辨率和像素密度。通过结合使用 window.devicePixelRatio,你可以确保着色器适应不同的屏幕,从而在各种设备上保持一致的渲染效果。

这里是我们应用这种方法的方式:

    const StarShaderMaterial = new ShaderMaterial({
      uniforms: {
        dpr: { value: window.devicePixelRatio }, // 确保在不同设备上点的大小一致
      },
      vertexShader: `
        uniform float dpr;
        void main() {
          gl_PointSize = gl_PointSize * dpr; // 根据设备像素比调整点的大小
        }
      `,
    });
    

进入全屏模式 退出全屏模式

提醒: 当屏幕发生变化时,比如将窗口拖动到分辨率不同的显示器上,记得更新 useFrame 或相关钩子中的 dpr 值!

    useFrame(() => {
      StarShaderMaterial.uniforms.dpr.value = window.devicePixelRatio;
    });
    

进入全屏模式 退出全屏模式

组件结构:

星星和行星是在一个 <group> 元素内部渲染的。星星使用 <points> 渲染,而行星则使用 Instances 进行高效渲染。

    <group>
      {/* 实例化的星星 */}
      <points position={[0, 0, 0]}>
        <bufferGeometry>
          <bufferAttribute attach="attributes-position" {...stars} />
        </bufferGeometry>
        <primitive attach="material" object={StarShaderMaterial} />
      </points>
    
      {/* 实例化的行星 */}
      <Instances limit={100} material={PlanetMaterial}>
        <sphereGeometry args={[1, 32, 32]} />
        {planets}
      </Instances>
    </group>
    

进入全屏模式 退出全屏模式

🌌 总结 - SpaceEnvironment:

  • 动态星场 : 随机散落在圆柱空间中的闪烁星星为环境增添了活力。
  • 实例化行星 : 随机位置和大小的行星为场景提供了多样性和深度。
  • 旋转环境 : 整个背景的缓慢旋转使空间感觉广阔而生动。
  • 平滑过渡 : 当环境显现或隐藏时,星星和行星会无缝地淡入淡出。

这个太空环境为火箭的冒险创造了一个沉浸式、动态的背景,增强了场景中的运动感和深度。

🏎️ 性能提升:优化流畅的火箭体验

对于像我们的火箭场景这样的互动体验,保持性能处于顶级状态至关重要——特别是在处理3D渲染和物理计算时。让我们来看看我们是如何榨取出那些额外的帧并保持流畅性的:

🚀 1. 帧循环控制: “Always” vs. “Demand”

为了防止火箭不在视野内时不必要的GPU使用,我们在网页内容下方创建了一个触发元素。这个开关在 "always""demand" 之间切换 帧循环,确保浏览器仅在必要时重新渲染。

  • "Always" : 当火箭可见(已滚动到视图中)或正在动画时使用,确保性能流畅。
  • "Demand" : 当火箭静止且不在视图中时使用,这意味着场景仅在特定事件发生时重新渲染。

这项技术有助于减少GPU负担,并优化移动和笔记本用户的电池使用。

    <Canvas frameloop={isRocketVisibleOrFlying ? 'always' : 'demand'} />

进入全屏模式 退出全屏模式

🚀 2. 自定义 useViewportSize 钩子

我们选择了一个自定义的 useViewportSize() 钩子来跟踪视口变化,同时避免触发过多的重新渲染。虽然 useThree(){ viewport/size } 会在每次滚动时触发重新渲染,但我们对视口大小的检查进行了节流,以避免性能下降。

    const useViewportSize = () => {
      const [size, setSize] = useState(null);
    
      useFrame((state) => {
        if (state.clock.elapsedTime % throttleTime > 0.01) return;
    
        const { width, height } = state.size;
        if (!size || size.width !== width || size.height !== height) {
          setSize({ width, height });
        }
      });
    
      return size;
    };
    

进入全屏模式 退出全屏模式

🚀 3. 行星的网格实例化

为了提高效率,我们使用了实例化网格来处理行星的渲染。实例化允许你渲染多个相同几何体的副本,同时复用相同的材质,这大大减少了绘制场景中每个单独行星的开销。

    <Instances limit={100} material={PlanetMaterial}>
      <sphereGeometry args={[1, 32, 32]} />
      {planets}
    </Instances>
    

进入全屏模式 退出全屏模式

🚀 4. 使用 Refs 存储不重新渲染的值

当在 useFrame() 循环中处理动态更新时,我们将经常变化的值(如位置或速度)存储在 Refs 中。这样可以直接操作这些值,而不会触发组件的重新渲染,从而节省 CPU 周期。

🚀 5. 通用 React-Three-Fiber 性能技巧

最后,我们遵循了 React Three Fiber 文档 中的最佳实践以避免性能陷阱。其中一些包括:

  • 避免过度使用useFrame(),除非绝对必要。
  • 通过在动画循环中少量使用setState() 来批量更新。
  • 减少在循环中创建新对象,以防止垃圾回收。

对于更高级的性能技巧,请务必查看官方的 React Three Fiber 常见陷阱指南性能优化技巧

💡 快速提示:某些设备上的画布大小限制

在处理大型画布时,请记住某些设备(如Android ChromeFirefox on Desktop)存在尺寸限制,如果画布高度超过4096px,可能无法正常渲染。始终考虑这些限制,并考虑为大型视口进行动态缩放或禁用某些功能!

    if (高度 > 4096 && 浏览器名称 === 'Firefox' && 是桌面端) {
      设置大小(null);
      返回;
    }
    

进入全屏模式 退出全屏模式

通过实施这些性能优化,我们确保了流畅、沉浸式的体验,不会拖累用户的设备——无论是手机还是桌面电脑。🖥️📱

🚀 最后的倒计时:收尾工作

庆祝我们在 LinkedIn 上获得 1,000 位关注者,用这个互动的 3D 火箭真是非常有趣!从构建一个动态的、鼠标控制的火箭,到添加分裂的太空岩石、闪烁的星星以及性能优化——这个项目真的是一段有趣的旅程。

正是这样的项目让我们想起了我们热爱这份工作的原因:将创意、技术与一点点火箭科学(好吧,其实是很多火箭科学)结合起来。希望你喜欢阅读我们如何将这一切整合在一起的过程,并且也许还能从中获得一些建议,用于你自己的互动网页项目。

欢迎查看 zerodays.dev 上的火箭复活节彩蛋,并且当然了,继续关注我们构建更多酷炫事物和推动网页开发可能性边界的更多精彩互动体验。

这里为下一个里程碑干杯——也许还有下一次火箭发射!🚀

感谢阅读并跟随教程!

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP