Skip to content

76. Vue、React 项目如何集成 Three.js

Published:

前面我们都是 html 引入 Three.js 来跑的,但实际的前端项目更多是在某个前端页面里嵌入了 3D 场景。

页面的其他部分,是通过 Vue、React 这种前端框架渲染的。

所以我们要 Three.js 和 Vue、React 集成来写页面。

怎么集成呢?

其实也很简单:

image.png

创建了 WebGLRenderer 之后,它的 domElement 属性就是一个 canvas 元素,我们把它挂载到了 body。

之后的渲染都是在这个 canvas 上。

而 Vue、React 这些前端框架都是状态修改之后去渲染更新 dom。

我们只要在某个 dom 上 append 这个 canvas 元素就好了。

前端框架更新 dom,Three.js 在这个 canvas 上绘制,互不影响。

如果想相互调用怎么办呢?

比如点击了页面的标签,来更新 3D 场景的某个部分,或者 3D 场景中的一些交互,会更新页面的某些部分。

这种也很容易想到,导出方法给对方用就好了。

我们来试一下:

npx create-vite react-three-app

image.png

创建项目,前端框架选择 react。

进入项目,安装下依赖:

npm install
npm install --save three
npm install --save-dev @types/three

先跑一下:

npm run dev

image.png

2025-04-10 23.37.09.gif

我们先写一下布局:

image.png

注释掉 index.css 和 StrictMode

改一下 App.jsx:

import './App.css'

function App() {

  return <div>
    <div id="header">
    React 和 Three.js 
    </div>
    <div id="main">
      <div id="content">
      </div>
      <div id="operate">
        <button>红色</button>
        <button>绿色</button>
        <button>蓝色</button>
      </div>
    </div>
  </div>
}

export default App

写下样式:

body {
  margin: 0;
}
#header {
  height: 80px;
  font-size: 32px;
  line-height: 80px;
  font-weight: bold;
  border-bottom: 1px solid #000;
  padding-left: 20px;
}
#main {
  display: flex;
  flex-direction: row;
  height: calc(100vh - 80px);
}
#content {
  width: 1000px;
  border-right: 1px solid #000;
}
#operate {
}
#operate button{
  margin: 10px;
  padding: 10px;
}

就是上面有一个 header 固定 80px 的高度

下面的高度是 100vh - 80px

下面分左右两边,用 flex 布局,左边是 1000px

看下效果:

image.png

然后我们让 Three.js 的 canvas 挂在左边这个 #content 的 div 下。

创建 src/3d-init.js

import * as THREE from 'three';
import {
    OrbitControls
} from 'three/addons/controls/OrbitControls.js';

export function init(dom) {
    const scene = new THREE.Scene();

    const axesHelper = new THREE.AxesHelper(500);
    scene.add(axesHelper);

    const directionalLight = new THREE.DirectionalLight(0xffffff);
    directionalLight.position.set(500, 400, 300);
    scene.add(directionalLight);

    const ambientLight = new THREE.AmbientLight(0xffffff);
    scene.add(ambientLight);

    const width = 1000;
    const height = window.innerHeight - 80;

    const camera = new THREE.PerspectiveCamera(60, width / height, 1, 10000);
    camera.position.set(500, 500, 500);
    camera.lookAt(0, 0, 0);

    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(width, height)

    function render() {
        renderer.render(scene, camera);
        requestAnimationFrame(render);
    }

    render();

    dom.append(renderer.domElement);

    window.onresize = function () {
        const width = 1000;
        const height = window.innerHeight - 80;

        renderer.setSize(width,height);

        camera.aspect = width / height;
        camera.updateProjectionMatrix();
    };
    
    const controls = new OrbitControls(camera, renderer.domElement);

    return {
        scene,
        camera,
        renderer
    }
}

封装一个 init 方法,传入 dom:

image.png

把 renderer 返回的 canvas 元素挂载到这个 dom:

image.png

这里宽高也不再是整个窗口了,也要改一下:

image.png

还有 resize 的时候:

image.png

最后可以返回 scene 等对象:

image.png

在 App.jsx 引入:

image.png

useEffect(() => {
    const dom = document.getElementById('content');
    init(dom);
    return () => {
      dom.innerHTML = '';
    }
}, []);

useEffect 也就是 dom 渲染完之后 init 一下 three.js。

组件销毁的时候清空 dom 的内容,也就是删掉那个 canvas 元素。

看下效果:

2025-04-11 00.06.06.gif

这样 Three.js 的场景就渲染出来了,resize 也没问题。

我们画 3 个立方体:

创建 src/mesh.js

import * as THREE from 'three';

const group = new THREE.Group();

function createBox(color, x) {
    const geometry = new THREE.BoxGeometry(100, 100, 100);
    const material = new THREE.MeshPhongMaterial({
        color: color
    });
    const mesh =  new THREE.Mesh(geometry, material);
    mesh.position.x = x;
    mesh.name = color;
    return mesh;
}

group.add(createBox('red', 0));
group.add(createBox('blue', 300));
group.add(createBox('green', -300));

export default group;

创建 3 个立方体,放在不同的位置,给它们不同的 name。

在 3d-init.js 引入下:

image.png

image.png

然后先来实现 React 点击按钮的时候和 3D 场景交互。

比如点击红绿蓝按钮的时候,分别让对应的方块跳一下:

这里引入 tween.js 做动画:

npm install --save @tweenjs/tween.js

image.png

image.png

import { Easing, Group, Tween } from '@tweenjs/tween.js';
const tweenGroup = new Group();
function jump(color) {
    const box = mesh.getObjectByName(color);
    const tween= new Tween(box.position).to({
        ...box.position,
        y: 100
    }, 1000)
    .easing(Easing.Quadratic.InOut)
    .repeat(0)
    .start()
    .onComplete(() => {
        tweenGroup.remove(tween);
    })
    tweenGroup.add(tween);
}

function render(time) {
    tweenGroup.update(time);
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

这里创建一个 tween 的 group

定义 jump 方法,找到对应 color 的 box,执行 tween 缓动动画,把它放到 group 里,执行完成从 group 里删掉

每次渲染调用 group 的 update 来更新。

导出这个 jump 方法:

image.png

调用下:

image.png

const { jump } = init(dom);

jump('red');

2025-04-11 00.35.44.gif

然后我们希望点击按钮的时候执行这个 jump 方法。

用 ref 来保存这个方法:

image.png

const jumpRef = useRef(() => {});
jumpRef.current = jump;

点击按钮的时候调用下:

image.png

<button onClick={()=> {jumpRef.current('red')}}>红色</button>
<button onClick={()=> {jumpRef.current('green')}}>绿色</button>
<button onClick={()=> {jumpRef.current('blue')}}>蓝色</button>

2025-04-11 00.41.10.gif

这样,从 react 到 Three.js 的调用就完成了。

那 Three.js 场景中想调用 react 应用的一些方法呢?

可以在 init 的时候传入方法:

image.png

const [str, setStr] = useState('');

image.png

创建了一个 state,在页面上显示,然后把 setState 方法传入 init。

image.png

image.png

在动画完成的时候调用下。

2025-04-11 00.48.49.gif

这样就实现了 Three.js 场景里调用 react 组件的一些方法。

vue 项目里也是大同小异,同样的思路。

案例代码上传了小册仓库

总结

这节我们实现了 react 和 three.js 的集成。

three.js 的 renderer 渲染出 canvas 元素,把它挂载到 react 应用的某个 dom 下就好了。

three.js 在这个 canvas 元素渲染,react 则是渲染整个 dom 树,互不影响。

互相调用的话就是通过参数返回值传递一些函数,在这些函数里实现调用的功能就好了。

我们只测试了 Three.js 和 React 项目的集成,但 Vue 项目也是同一个思路,没啥区别。

后面的项目如果需要写页面的部分,就可以用 Three.js 和前端框架集成来搞。

评论