// 技术栈:React + Three.js + @react-three/fiber + @react-three/drei

import React, { useRef, useState } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { OrbitControls, Text, Box, Sphere } from '@react-three/drei';

// 一个简单的旋转立方体组件
function RotatingCube({ position, color }) {
  // 使用 useRef 来获取对3D对象(mesh)的直接引用
  const meshRef = useRef();

  // useFrame 是 React Three Fiber 的核心钩子,它在每一帧渲染前被调用
  // 这里我们用它来创建动画效果
  useFrame((state, delta) => {
    // 让立方体绕 Y 轴和 X 轴旋转
    meshRef.current.rotation.y += delta; // delta 是上一帧到当前帧的时间差,使动画速度与刷新率无关
    meshRef.current.rotation.x += delta * 0.5;
  });

  return (
    // Box 是 @react-three/drei 提供的预制几何体,等同于 new THREE.BoxGeometry()
    <Box
      ref={meshRef} // 将引用绑定到该3D对象
      position={position} // 位置坐标 [x, y, z]
      args={[1, 1, 1]} // 尺寸参数,这里是长宽高各为1
    >
      // 定义材质
      <meshStandardMaterial color={color} />
    </Box>
  );
}

// 一个交互式的球体组件
function InteractiveSphere() {
  const sphereRef = useRef();
  const [isHovered, setIsHovered] = useState(false);
  const [isClicked, setIsClicked] = useState(false);

  useFrame(() => {
    // 当鼠标悬停时,球体轻微上下浮动
    if (isHovered) {
      sphereRef.current.position.y = Math.sin(Date.now() * 0.003) * 0.2;
    }
  });

  return (
    <Sphere
      ref={sphereRef}
      args={[0.8, 32, 32]} // 半径,宽度分段,高度分段
      position={[0, 0, 0]}
      // 交互事件处理:完全像处理普通React DOM事件一样
      onPointerOver={(event) => {
        event.stopPropagation(); // 防止事件冒泡到Canvas
        setIsHovered(true);
      }}
      onPointerOut={() => setIsHovered(false)}
      onClick={() => setIsClicked(!isClicked)}
    >
      // 根据状态改变材质颜色
      <meshStandardMaterial
        color={isClicked ? 'hotpink' : (isHovered ? 'orange' : 'green')}
        roughness={0.4}
      />
    </Sphere>
  );
}

// 主场景组件
function Scene() {
  return (
    <>
      {/* 环境光,模拟整体环境照明 */}
      <ambientLight intensity={0.5} />
      {/* 平行光,模拟太阳光,产生阴影和高光 */}
      <directionalLight position={[5, 5, 5]} intensity={1} castShadow />
      
      {/* 使用我们自定义的3D组件 */}
      <RotatingCube position={[-2, 0, 0]} color="royalblue" />
      <InteractiveSphere />
      <RotatingCube position={[2, 0, 0]} color="crimson" />
      
      {/* 添加3D文字 */}
      <Text
        position={[0, 2, 0]}
        fontSize={0.8}
        color="white"
        anchorX="center"
        anchorY="middle"
      >
        Hello, 3D World!
      </Text>
      
      {/* 轨道控制器,允许用户用鼠标拖拽、缩放、旋转整个场景 */}
      <OrbitControls enableDamping dampingFactor={0.05} />
    </>
  );
}

// 根React组件
export default function ThreeJSVisualization() {
  return (
    // Canvas组件创建了一个Three.js渲染上下文和画布(Canvas DOM元素)
    // 它自动处理了渲染循环、尺寸调整、事件等所有底层细节
    <div style={{ width: '100vw', height: '100vh' }}>
      <Canvas
        shadows // 启用阴影
        camera={{ position: [0, 0, 8], fov: 50 }} // 设置相机位置和视野
      >
        <Scene />
      </Canvas>
    </div>
  );
}

一、为什么要把React和Three.js放在一起?

想象一下,你正在用React搭建一个现代化的管理后台或者数据大屏。页面里充满了精致的表格、图表和表单。突然,产品经理走过来,指着屏幕说:“这里,能不能把我们的网络拓扑/分子结构/地理数据,用一个可以交互旋转的3D模型展示出来?”

传统的Three.js开发,就像是在一个巨大的画布上直接用画笔(JavaScript)作画。你需要手动管理每一个点、线、面,自己处理动画循环,自己绑定事件,代码组织起来很容易变成一团乱麻。而React的核心思想是“声明式”和“组件化”。你描述UI应该是什么样子(状态+结构),React负责帮你把它渲染出来并保持同步。

把两者结合,就是用React的“乐高积木”思维来搭建3D世界。一个旋转的立方体、一盏灯、一个控制器,都可以是一个独立的React组件。你可以用useState来控制它的颜色,用useEffect来加载外部模型,用props在组件间传递位置和状态。这让复杂3D场景的代码变得清晰、可维护、易复用。我们上面示例中的RotatingCubeInteractiveSphere就是活生生的例子,它们就是标准的、可复用的React组件。

二、核心工具介绍:React Three Fiber 和 Drei

要实现这种美妙的结合,主要依靠两个社区明星库:

React Three Fiber:它是整个整合的基石。你可以把它理解为一个“翻译官”或“适配层”。它把Three.js里那些用命令式代码创建的对象(比如new THREE.Mesh(...)),转换成了React的声明式组件(比如<mesh>)。它接管了Three.js最繁琐的部分——渲染循环、上下文管理、资源清理,让你可以专心用JSX来描述3D场景。我们示例中的<Canvas>useFrame钩子都来自它。

React Three Drei:这是一个“百宝箱”或“组件库”。Three.js本身提供的是基础原材料(几何体、材质、光源)。而Drei则提供了大量预制的、开箱即用的高级组件和工具函数。比如示例中的<OrbitControls>(轨道控制器)、<Text>(3D文字)、<Box>/<Sphere>(预制几何体),都来自Drei。它极大地提高了开发效率,避免了你重复造轮子。

三、整合过程中的关键技巧与详细示例

掌握了基础,我们来看看一些实现复杂可视化时更高级的技巧。

1. 状态管理与动画:让3D世界“活”起来 3D可视化的核心是动态交互。在React组件中,你可以无缝地使用React的状态和生命周期来控制3D对象。

// 技术栈:React + Three.js + @react-three/fiber + @react-three/drei
import React, { useState, useEffect } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { useGLTF, Html } from '@react-three/drei';

// 一个根据外部数据动态变化的柱状图组件
function DataBar({ index, value, maxValue }) {
  const barRef = useRef();
  const targetHeight = (value / maxValue) * 3; // 根据数值计算目标高度

  // 使用useFrame实现平滑的高度过渡动画
  useFrame(() => {
    // 线性插值,使高度变化更平滑
    barRef.current.scale.y += (targetHeight - barRef.current.scale.y) * 0.1;
  });

  return (
    <mesh ref={barRef} position={[index * 1.2 - 3, 0, 0]}>
      <boxGeometry args={[0.8, 1, 0.8]} /> {/* 初始高度为1 */}
      <meshStandardMaterial color={`hsl(${value * 20}, 70%, 50%)`} />
      {/* 在3D空间内嵌入2D HTML标签,用于显示数值 */}
      <Html position={[0, barRef.current?.scale.y / 2 + 0.3, 0]} center>
        <div style={{ color: 'white', background: '#333', padding: '2px 6px', borderRadius: '4px', fontSize: '12px' }}>
          {value.toFixed(1)}
        </div>
      </Html>
    </mesh>
  );
}

function DynamicChart() {
  const [data, setData] = useState([1, 3, 5, 2, 4, 7]);
  const maxValue = Math.max(...data);

  // 模拟实时数据更新
  useEffect(() => {
    const interval = setInterval(() => {
      setData(prev => prev.map(v => Math.max(0.5, v + (Math.random() - 0.5) * 2))); // 随机波动数据
    }, 2000);
    return () => clearInterval(interval);
  }, []);

  return (
    <>
      <ambientLight intensity={0.6} />
      <directionalLight position={[10, 10, 5]} intensity={1} />
      {data.map((value, idx) => (
        <DataBar key={idx} index={idx} value={value} maxValue={maxValue} />
      ))}
    </>
  );
}
// 在主Canvas中渲染 <DynamicChart /> 即可看到动态更新的3D柱状图

2. 性能优化:复杂场景的生存法则 3D渲染非常消耗资源。在React组件树中,不必要的重渲染会拖垮性能。

  • 使用 React.memo 包裹纯3D组件:如果一个3D子组件的props没有变化,它就不应该随着父组件一起重渲染。
  • 善用 useMemouseCallback:对于复杂的几何体、材质或事件处理函数,用它们进行缓存。
  • 实例化渲染:当需要渲染成千上万个相同的物体(如一片草地、星空)时,使用Three.js的InstancedMesh,并通过Drei的<InstancedMesh>组件来使用,可以极大提升性能。它只调用一次绘制命令,而非成千上万次。
  • 按需渲染:React Three Fiber的Canvas默认是连续渲染的(每帧都渲染)。对于静态或交互不多的场景,可以设置frameloop="demand",只在需要时(例如控制器触发变化时)才渲染。

3. 与外部世界(2D UI)通信 你的3D可视化很少是孤岛,它需要和页面其他部分的按钮、滑块、数据面板联动。

// 技术栈:React + Three.js + @react-three/fiber + @react-three/drei
import React, { useState } from 'react';
import { Canvas } from '@react-three/fiber';
import { Select } from '@react-three/postprocessing'; // 一个用于后期处理选择的库

function SelectableModel({ modelUrl, isSelected, onSelect }) {
  const { scene } = useGLTF(modelUrl); // 加载外部3D模型

  return (
    // Select 组件可以高亮被选中的物体
    <Select enabled={isSelected}>
      <primitive
        object={scene}
        scale={0.5}
        onClick={(event) => {
          event.stopPropagation();
          onSelect(); // 触发外部传入的回调函数,通知父组件这个模型被点了
        }}
        // 根据选中状态改变颜色
        material-color={isSelected ? 'gold' : 'white'}
      />
    </Select>
  );
}

function ModelViewerApp() {
  const [selectedId, setSelectedId] = useState(null);
  const models = ['/robot.glb', '/car.glb', '/tree.glb'];

  // 2D UI面板
  const ModelListPanel = () => (
    <div style={{ position: 'absolute', top: 20, left: 20, background: 'rgba(0,0,0,0.7)', color: 'white', padding: '10px' }}>
      <h4>模型列表</h4>
      {models.map((url, idx) => (
        <div key={idx} style={{ padding: '5px', cursor: 'pointer', background: selectedId === idx ? '#555' : 'transparent' }}>
          <button onClick={() => setSelectedId(idx)}>选择 模型{idx + 1}</button>
        </div>
      ))}
      {selectedId !== null && (
        <p>当前选中: 模型{selectedId + 1} <button onClick={() => setSelectedId(null)}>取消选择</button></p>
      )}
    </div>
  );

  return (
    <div style={{ width: '100%', height: '100vh' }}>
      <ModelListPanel />
      <Canvas>
        {/* 3D场景 */}
        {models.map((url, idx) => (
          <SelectableModel
            key={idx}
            modelUrl={url}
            isSelected={selectedId === idx}
            onSelect={() => setSelectedId(idx)}
            position={[(idx - 1) * 3, 0, 0]} // 简单排列开
          />
        ))}
        <OrbitControls />
      </Canvas>
    </div>
  );
}

四、应用场景、优缺点、注意事项与总结

应用场景: 这种技术组合非常适合需要将3D元素深度集成到复杂Web应用中的场景。例如:产品3D配置器(在线定制汽车、家具)、数据可视化大屏(3D地理信息、网络拓扑、分子结构)、在线展览与数字孪生(博物馆展品、工厂生产线)、教育模拟软件(物理、化学实验模拟)以及交互式图表和报表

技术优点:

  1. 开发效率高:组件化思维让代码组织清晰,复用性强。
  2. 易于集成:3D场景能轻松融入现有的React应用和数据流(如Redux, Context)。
  3. 声明式优势:UI状态与3D渲染状态自动同步,心智模型更简单。
  4. 生态强大:受益于React和Three.js两个庞大的生态系统,工具和资源丰富。

潜在挑战与注意事项:

  1. 学习曲线:需要同时理解React和Three.js的基本概念,初期门槛不低。
  2. 包体积:引入Three.js及其生态会使最终打包体积显著增加,需注意代码分割和懒加载。
  3. 性能敏感:不当的React重渲染会引发严重的3D性能问题,必须关注优化技巧。
  4. 调试复杂度:问题可能出在React逻辑、Three.js渲染或两者交互上,调试需要更多经验。

总结: 将React与Three.js通过React Three Fiber进行整合,是现代Web前端实现复杂3D可视化的一条高效路径。它把命令式的3D绘图API,封装成了声明式的React组件,让开发者能够用熟悉的工具和思维模式去构建交互丰富的3D界面。虽然存在一定的学习成本和性能优化要求,但其带来的开发效率、可维护性以及与现代前端框架的无缝融合能力,使得它在需要深度交互式3D内容的Web项目中极具吸引力。关键在于,你不再是一个“Three.js脚本编写者”,而是一个用React组件“搭建”3D世界的工程师。