基于React搭建threejs[最流行的3D库]环境
基于Create React App 环境体验three.js库。
threejs 是一个 3D JavaScript库,用于在网页上呈现3D内容。它是一个开源项目,旨在创建一个易于使用、轻量级、跨浏览器、通用的3D库。
当前版本包括WebGL(Web图形库)渲染器和JavaScript API,可无需插件用在任何兼容渲染交互式2D和3D图形的Web浏览器中。现代浏览器广泛支持WebGL。
WebGL是一个绘制点、线和三角形的底册API。要使用WebGL做任何有用的事情,它需要相当多的代码,这时产生了threejs。它处理高级功能,如场景、灯光、阴影、材质、纹理、3D数学等。
threejs还支持其他渲染器,如WebGPU、SVG和CSS3D。官方示例 (opens new window)显示了高级用法。
作为一篇入门threejs的文章,我们快速了解它是什么以及如何使用它。
# 官方立方体旋转动画
上面的图像由官方动画立方体代码渲染,该代码列在threejs的README文件中。下面是代码:
import * as THREE from 'three';
// init
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
camera.position.z = 2;
const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animation);
document.body.appendChild(renderer.domElement);
// animation
function animation(time) {
mesh.rotation.x = time / 2000;
mesh.rotation.y = time / 1000;
renderer.render(scene, camera);
}
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
接下来分析它是如何工作的:
在上面代码第一行, THREE 被导入;
import * as THREE from 'three';
在代码第4行, PerspectiveCamera
【透视摄像机】被创建
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
PerspectiveCamera
的构造函数需要4个参数,具体如下:
export class PerspectiveCamera extends Camera {
/**
* @param [fov=50] Camera frustum vertical field of view. Default value is 50.
* @param [aspect=1] Camera frustum aspect ratio. Default value is 1.
* @param [near=0.1] Camera frustum near plane. Default value is 0.1.
* @param [far=2000] Camera frustum far plane. Default value is 2000.
*/
constructor(fov?: number, aspect?: number, near?: number, far?: number);
}
2
3
4
5
6
7
8
9
在本例中,fov(视角)在垂直维度上设置为70度,纵横比设置为DOM窗口的纵横比(window.innerWidth/window.inerHeight)。
near和far表示将要渲染的摄影机前面的空间。
任何在 near 之前或 far 之后的东西都会被摄像机剪掉。相机前面的范围设置为[0.01,10]。平截头体是一种3D形状,它像一个顶端被切掉的金字塔。
在官方代码第5行, carmera.position 是 三维向量。
camera.position.z = 2;
相机位于[0,0,2]。+Y 和 -Y 轴位于绿色正方形的中间。
表示摄像机的可视范围。
以下是摄像机的类型:阵列相机、相机、立方体相机、正交相机、透视相机和立体相机。
在官方代码行7中,实例化了一个场景
const scene = new THREE.Scene();
场景 scene 是存放对象、灯光和相机的地方。以下都是可用的场景类型:Fog, FogExp2, and Scene.
在第9行代码中,一个 BoxGeometry 被实例化。
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
几何体 Geometry 定义对象的形状。BoxGeometry定义长方体尺寸-宽度、高度和深度。在该示例中,宽度、高度和深度设置为0.2。
export class BoxGeometry extends BufferGeometry {
/**
* @param [width=1] — Width of the sides on the X axis.
* @param [height=1] — Height of the sides on the Y axis.
* @param [depth=1] — Depth of the sides on the Z axis.
* @param [widthSegments=1] — Number of segmented faces along the width of the sides.
* @param [heightSegments=1] — Number of segmented faces along the height of the sides.
* @param [depthSegments=1] — Number of segmented faces along the depth of the sides.
*/
constructor(
width?: number,
height?: number,
depth?: number,
widthSegments?: number,
heightSegments?: number,
depthSegments?: number,
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
立方体的构造函数还可以定义沿每条边的分段面。默认情况下,每条边都有一个分段面。每边的细节越多,需要的分割面就越多。以下是4 x 5 x 10的分段面示意图:
这些是可用的几何类型:BoxGeometry、CapsuleGeometry,CircleGeomety、ConeGeometry和CylinderGeometry,和 WireframeGeometry。
在代码第10行,创建一个 MeshNormalMaterial 对象
const material = new THREE.MeshNormalMaterial();
材质【MaterialParameters】对象的参数-有光泽、平坦、颜色、纹理等-并采用以下参数:
export interface MaterialParameters {
alphaTest?: number | undefined;
alphaToCoverage?: boolean | undefined;
blendDst?: BlendingDstFactor | undefined;
blendDstAlpha?: number | undefined;
blendEquation?: BlendingEquation | undefined;
blendEquationAlpha?: number | undefined;
blending?: Blending | undefined;
blendSrc?: BlendingSrcFactor | BlendingDstFactor | undefined;
blendSrcAlpha?: number | undefined;
clipIntersection?: boolean | undefined;
clippingPlanes?: Plane[] | undefined;
clipShadows?: boolean | undefined;
colorWrite?: boolean | undefined;
defines?: any;
depthFunc?: DepthModes | undefined;
depthTest?: boolean | undefined;
depthWrite?: boolean | undefined;
name?: string | undefined;
opacity?: number | undefined;
polygonOffset?: boolean | undefined;
polygonOffsetFactor?: number | undefined;
polygonOffsetUnits?: number | undefined;
precision?: 'highp' | 'mediump' | 'lowp' | null | undefined;
premultipliedAlpha?: boolean | undefined;
dithering?: boolean | undefined;
side?: Side | undefined;
shadowSide?: Side | undefined;
toneMapped?: boolean | undefined;
transparent?: boolean | undefined;
vertexColors?: boolean | undefined;
visible?: boolean | undefined;
format?: PixelFormat | undefined;
stencilWrite?: boolean | undefined;
stencilFunc?: StencilFunc | undefined;
stencilRef?: number | undefined;
stencilWriteMask?: number | undefined;
stencilFuncMask?: number | undefined;
stencilFail?: StencilOp | undefined;
stencilZFail?: StencilOp | undefined;
stencilZPass?: StencilOp | undefined;
userData?: any;
}
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
网格材质:MeshNormalMaterialParameters 有如下参数
export interface MeshNormalMaterialParameters extends MaterialParameters {
bumpMap?: Texture | null | undefined;
bumpScale?: number | undefined;
normalMap?: Texture | null | undefined;
normalMapType?: NormalMapTypes | undefined;
normalScale?: Vector2 | undefined;
displacementMap?: Texture | null | undefined;
displacementScale?: number | undefined;
displacementBias?: number | undefined;
wireframe?: boolean | undefined;
wireframeLinewidth?: number | undefined;
flatShading?: boolean | undefined;
}
2
3
4
5
6
7
8
9
10
11
12
13
以下是可以使用的材质类型:ShadowMaterial, SpriteMaterial, RawShaderMaterial, ShaderMaterial, PointsMaterial, MeshPhysicalMaterial, MeshStandardMaterial, MeshPhongMaterial, MeshToonMaterial, MeshNormalMaterial, MeshLambertMaterial, MeshDepthMaterial, MeshDistanceMaterial, MeshBasicMaterial, MeshMatcapMaterial, LineDashedMaterial, LineBasicMaterial, and Material.
在代码第12行,创建了几何体形状和 网格材质,然后添加到 场景中:
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
2
网格是构成三维对象图形的骨架,它由几何体(材质)、材质(曲面)和场景 组成。
在官方代码行15–18中,WebGL呈现被实例化。它被设置为DOM窗口大小,配置了一个动画循环,其domElement被附加到DOM主体。
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animation);
document.body.appendChild(renderer.domElement);
2
3
4
在官方代码行21–26中,动画 animation 函数被创建。
function animation(time) {
mesh.rotation.x = time / 2000;
mesh.rotation.y = time / 1000;
renderer.render(scene, camera);
}
2
3
4
5
参数 time 是自渲染器以来的时间,调用setAnimationLoop(动画)。时间单位为毫秒。由于在每个轴上旋转一次需要2π,因此上述动画函数绕x轴旋转约12.56秒,绕y轴旋转约6.28秒。
setAnimationLoop(animation)是启动动画的请求。如果动画为空,它将停止任何正在进行的动画。setAnimationLoop是requestAnimationFrame的替换。
renderer.render(scene, camera) 渲染并更新数据;
# 使用react 脚手架搭建threejs环境
我们使用Create React App (opens new window)来了解三种情况。使用以下命令创建React项目:
$ npx create-react-app react-three
$ cd react-three
2
安装 three.js 依赖。
npm i three
// 或者
yarn add three
2
3
之后可以在package.json中看到
"dependencies": {
"three": "^0.145.0"
}
2
3
这样就把 three 添加到项目中。
使用下面代码,替换 src/App.js 文件
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
function App() {
const divRef = useRef();
useEffect(() => {
// init
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
camera.position.z = 2;
const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animation);
divRef.current.appendChild(renderer.domElement);
// animation
function animation(time) {
mesh.rotation.x = time / 2000;
mesh.rotation.y = time / 1000;
renderer.render(scene, camera);
}
}, []);
return <div ref={divRef} />;
}
export default App;
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
以上代码几乎和官方代码相同,除了以下几点:
- 初始化代码封装在useEffect(第7–32行)中,并初始化一次(第32行的useEfect依赖列表设置为空数组[])。
- 渲染器有所不同,原来 renderer.domElement 是添加到 document.body 中,现在它被添加到divRef.current中。
执行命令 npm start
可以看到立方体旋转动画。
以上代码存在些可改进的方法:
- 不应该依赖DOM窗口的大小,因为我们可以实现 three.js 在一个组件内。
- 该应用不能随着浏览器缩放而缩放。
- 与大多数JavaScript库不同,three.js不会自动清理资源。它依靠浏览器在用户离开页面时进行清理。最佳实践是在对象不再使用时及时释放内存。
改良后的 src/app.js 文件
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
function App() {
const divRef = useRef();
useEffect(() => {
// init
let width = divRef.current.clientWidth;
let height = divRef.current.clientHeight;
const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 10);
camera.position.z = 2;
const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setAnimationLoop(animation);
const divCurrent = divRef.current;
divCurrent.appendChild(renderer.domElement);
window.addEventListener('resize', handleResize);
// handle window resize
function handleResize() {
width = divRef.current.clientWidth;
height = divRef.current.clientHeight;
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.render(scene, camera);
}
// animation
function animation(time) {
mesh.rotation.x = time / 2000;
mesh.rotation.y = time / 1000;
renderer.render(scene, camera);
}
return () => {
renderer.setAnimationLoop(null);
window.removeEventListener('resize', handleResize);
divCurrent.removeChild(renderer.domElement);
scene.remove(mesh);
geometry.dispose();
material.dispose();
};
}, []);
return <div ref={divRef} style={{ height: '100vh' }} />;
}
export default App;
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
53
54
55
56
57
58
59
60
61
62
在第59行,div元素的高度设置为视口高度的100%。
在第9-10行,我们将div元素的宽度和高度保存为变量,以便在第11行和第23行重用。
在第26行,divRef。divRef.current
保存为divCurrent。这允许在第27行添加到divCurrent的子元素在第52行被删除。
在第29行,事件监听器使用回调函数handleResize侦听resize事件。
在第32–39行,handleResize函数检索新的宽度和高度,并使用它们更新渲染和相机。在第38行,渲染器。渲染(场景、摄影机)重新渲染更新的数据。
在第49–56行,返回cleanup函数以停止动画、删除窗口侦听器并释放三个资源。
# 使用@react-three/fiber包创建应用
react-tree-fiber是一个用于threejs的react渲染器。它允许我们用JSX写threejs更具声明性。threejs的所有工作都将在react-tree-fiber中完成。
把jsx元素直接转换为threejs对象,没有其余额外的消耗,例如将 <mesh/>转换为新的THREE.mesh()
安装包 @react-three/fiber
npm i @react-three/fiber
添加到package.json中
"dependencies": {
"@react-three/fiber": "^8.0.19",
"three": "^0.145.0"
}
2
3
4
使用react-three/fiber后,src/App.js更简洁,看起来更像React。
mport { useRef } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
function Box() {
const meshRef = useRef();
useFrame((state, delta) => {
if (meshRef.current) {
meshRef.current.rotation.x += delta / 2;
meshRef.current.rotation.y += delta;
}
});
return (
<mesh ref={meshRef}>
<boxGeometry args={[0.2, 0.2, 0.2]} />
<meshNormalMaterial />
</mesh>
);
}
function App() {
return (
<Canvas
camera={{ fov: 70, near: 0.01, far: 100, position: [0, 0, 2] }}
style={{ height: '100vh', backgroundColor: 'black' }}
>
<Box />
</Canvas>
);
}
export default App;
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
在第4–18行,定义了Box组件。它为网格元素定义meshRef,该元素由钩子useFrame在第6–11行使用。
function useFrame(
callback: (state: RootState, delta: number, frame?: THREE.XRFrame) => void,
renderPriority?: number
): null;
2
3
4
对每帧调用useFrame。参数state包括所有三个状态信息,包括gl(WebGL)、相机、时钟、场景等。
参数delta是以秒为单位的时钟delta。它用于在第8–9行设置动画。
renderPriority是自动切换渲染开关的高级参数。
在 react-three-fiber 中,mesh 对象总是属于threejs的对象,成为一个全局组件。我们创建 mesh组件元素【在13-16行】,包括 boxGeometry 和 meshNormalMaterial。
这盒子元素被放置在
Canvas是three.js的基础。它渲染three组件。Canvas的道具包括gl(WebGL)、相机、光线投射器等。
export declare type RenderProps<TCanvas extends Element> = {
gl?: GLProps;
size?: Size;
shadows?: boolean | Partial<THREE.WebGLShadowMap>;
legacy?: boolean;
linear?: boolean;
flat?: boolean;
orthographic?: boolean;
frameloop?: 'always' | 'demand' | 'never';
performance?: Partial<Omit<Performance, 'regress'>>;
dpr?: Dpr;
raycaster?: Partial<THREE.Raycaster>;
camera?: (Camera | Partial<ReactThreeFiber.Object3DNode<THREE.Camera,
typeof THREE.Camera> & ReactThreeFiber.Object3DNode<THREE.PerspectiveCamera,
typeof THREE.PerspectiveCamera> & ReactThreeFiber.Object3DNode<THREE.OrthographicCamera,
typeof THREE.OrthographicCamera>>) & {
manual?: boolean;
};
events?: (store: UseBoundStore<RootState>) => EventManager<HTMLElement>;
onCreated?: (state: RootState) => void;
onPointerMissed?: (event: MouseEvent) => void;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在第23行,摄像机的道具被定义为{fov:70,near:0.01,far:100,position:[0,0,2]}。
在第24行,样式定义为{height:“100vh”,backgroundColor:“black”}。对于宽度,画布会自动拉伸到100%。
这个简洁的代码和其他代码一样有效。
你是否注意到,我们并没有调用 object.dispose() !
React知道对象的生命周期,React-tree-fiber将尝试通过调用对象来释放资源。dispose(),如果存在,则对所有未装入的对象执行。通过将 dispose={ null }放置在网格、材质等上,甚至放置在父容器(如组)上,可以关闭dispose尝试。
本文是翻译文章:[原文链接](https://betterprogramming.pub/working-with-three-js-the-popular-3d-javascript-library-bd2e9b03c95a)