初始化项目
这里使用 Next.js 作为项目基础框架,这也是目前 React 官方推荐的起步方案之一。
1
| npx create-next-app@latest
|
基本上使用默认选项即可。
安装 Three.js
1 2
| npm install --save three npm install @types/three --save-dev
|
使用 Three.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| "use client"; import { useState, useCallback } from "react"; import * as THREE from "three";
const initScene = (node: HTMLDivElement) => { const width = node.clientWidth; const height = node.clientHeight; const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); const renderer = new THREE.WebGLRenderer(); renderer.setClearColor(0xffffff); renderer.setSize(width, height); node.appendChild(renderer.domElement); camera.position.z = 5;
const geometry = new THREE.BoxGeometry(); const material = new THREE.MeshNormalMaterial(); const cube = new THREE.Mesh(geometry, material); scene.add(cube);
const animate = () => { requestAnimationFrame(animate); cube.rotation.x += 0.01; cube.rotation.y += 0.01; renderer.render(scene, camera); }; animate(); };
export default function Home() { const [initialized, setInitialized] = useState(false);
const threeDivRef = useCallback( (node: HTMLDivElement | null) => { if (node !== null && !initialized) { initScene(node); setInitialized(true); } }, [initialized] );
return ( <main> <div className="flex items-center justify-center h-screen" ref={threeDivRef} ></div> </main> ); }
|
需要注意,这里要使用 useCallback
而不是 useRef
避免不必要的重新渲染。因为在 Hooks 的组件中,state 改变后会引起父组件的重新渲染,每次渲染都会生成一个新函数。useCallback
通过缓存函数,无论渲染多少次,都是同一个函数,可以避免额外的性能和开销。
由于 Next.js 现在默认引入 Tailwind,所以这里直接其定义的样式,如果不使用 Tailwind,可以使用 style
设置样式:
1 2 3 4 5 6
| style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh' }}
|
监听窗口大小变化
由于这里已经有一个动画 animate
,可以在这里监听窗口大小是否发生变化,重新调整渲染器大小来适应变化。当然使用传统的 window.addEventListener
添加 resize
的事件处理也是可以的。
首先确保 canvas
填满 div
,可以在 node.appendChild
之后添加样式:
1 2 3
| renderer.domElement.style.width = "100%"; renderer.domElement.style.height = "100%"; renderer.domElement.style.display = "block";
|
判断宽度和高度是否匹配的辅助函数,可以考虑将它放入到某些类似工具类的通用代码里:
1 2 3 4 5 6 7 8 9 10
| const resizeRendererToDisplaySize = (renderer: THREE.WebGLRenderer) => { const canvas = renderer.domElement; const width = canvas.clientWidth; const height = canvas.clientHeight; const needResize = canvas.width !== width || canvas.height !== height; if (needResize) { renderer.setSize(width, height, false); } return needResize; };
|
在 animate
中判断大小是否发生变化:
1 2 3 4 5 6 7 8 9 10
| const animate = () => { requestAnimationFrame(animate);
if (resizeRendererToDisplaySize(renderer)) { const canvas = renderer.domElement; camera.aspect = canvas.clientWidth / canvas.clientHeight; camera.updateProjectionMatrix(); }
|
鼠标控制场景
使用 Three.js 内置的 OrbitControls
1 2 3 4 5 6 7 8
| import { OrbitControls } from "three/addons/controls/OrbitControls.js";
const controls = new OrbitControls(camera, renderer.domElement); controls.target.set(0, 2, 0);
controls.update();
|
调整数值
推荐使用 Three.js 内置的 lil-gui,而不是 dat.gui。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
const [controls] = useState({ rotationSpeed: 0.01 });
useEffect(() => { const gui = new GUI({ width: 200 }); gui.add(controls, "rotationSpeed", 0, 0.1);
return () => { if (gui) { gui.destroy(); } }; }, []);
|
性能面板
1 2 3 4 5 6 7 8 9
| import Stats from "three/addons/libs/stats.module.js";
export function initStats(type: number, node: HTMLDivElement) { let stats = new Stats(); stats.showPanel(type); node.appendChild(stats.dom); stats.dom.style.position = "absolute"; return stats; }
|