前面我们都是 html 引入 Three.js 来跑的,但实际的前端项目更多是在某个前端页面里嵌入了 3D 场景。
页面的其他部分,是通过 Vue、React 这种前端框架渲染的。
所以我们要 Three.js 和 Vue、React 集成来写页面。
怎么集成呢?
其实也很简单:

创建了 WebGLRenderer 之后,它的 domElement 属性就是一个 canvas 元素,我们把它挂载到了 body。
之后的渲染都是在这个 canvas 上。
而 Vue、React 这些前端框架都是状态修改之后去渲染更新 dom。
我们只要在某个 dom 上 append 这个 canvas 元素就好了。
前端框架更新 dom,Three.js 在这个 canvas 上绘制,互不影响。
如果想相互调用怎么办呢?
比如点击了页面的标签,来更新 3D 场景的某个部分,或者 3D 场景中的一些交互,会更新页面的某些部分。
这种也很容易想到,导出方法给对方用就好了。
我们来试一下:
npx create-vite react-three-app

创建项目,前端框架选择 react。
进入项目,安装下依赖:
npm install
npm install --save three
npm install --save-dev @types/three
先跑一下:
npm run dev


我们先写一下布局:

注释掉 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
看下效果:

然后我们让 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:

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

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

还有 resize 的时候:

最后可以返回 scene 等对象:

在 App.jsx 引入:

useEffect(() => {
const dom = document.getElementById('content');
init(dom);
return () => {
dom.innerHTML = '';
}
}, []);
useEffect 也就是 dom 渲染完之后 init 一下 three.js。
组件销毁的时候清空 dom 的内容,也就是删掉那个 canvas 元素。
看下效果:

这样 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 引入下:


然后先来实现 React 点击按钮的时候和 3D 场景交互。
比如点击红绿蓝按钮的时候,分别让对应的方块跳一下:
这里引入 tween.js 做动画:
npm install --save @tweenjs/tween.js


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 方法:

调用下:

const { jump } = init(dom);
jump('red');

然后我们希望点击按钮的时候执行这个 jump 方法。
用 ref 来保存这个方法:

const jumpRef = useRef(() => {});
jumpRef.current = jump;
点击按钮的时候调用下:

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

这样,从 react 到 Three.js 的调用就完成了。
那 Three.js 场景中想调用 react 应用的一些方法呢?
可以在 init 的时候传入方法:

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

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


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

这样就实现了 Three.js 场景里调用 react 组件的一些方法。
vue 项目里也是大同小异,同样的思路。
案例代码上传了小册仓库。
总结
这节我们实现了 react 和 three.js 的集成。
three.js 的 renderer 渲染出 canvas 元素,把它挂载到 react 应用的某个 dom 下就好了。
three.js 在这个 canvas 元素渲染,react 则是渲染整个 dom 树,互不影响。
互相调用的话就是通过参数返回值传递一些函数,在这些函数里实现调用的功能就好了。
我们只测试了 Three.js 和 React 项目的集成,但 Vue 项目也是同一个思路,没啥区别。
后面的项目如果需要写页面的部分,就可以用 Three.js 和前端框架集成来搞。