外观
Three.js(3D 可视化)
本文基于项目实际落地的二三维画布应用(
apps/design)编写。根级依赖版本:three@0.160.0。事实来源为apps/design/src/views/EditorView/EditorView.vue中的renderThreeD与apps/design/src/elements/中各类元素的render3d。
介绍
Three.js 是一个基于 WebGL 的 3D 图形库。它的渲染链路由以下核心角色组成:
Scene:场景容器,承载所有 3D 对象、光源与辅助工具。Camera:观察视角,决定画面从哪个位置、以什么投影方式渲染。本项目使用OrthographicCamera(正交投影,无透视变形,适合工程俯视示意)。Renderer(WebGLRenderer):把场景从相机视角绘制到页面 Canvas。Geometry:几何体(顶点、面片数据),如BoxGeometry。Material:材质,决定几何体的表面如何着色,如MeshLambertMaterial。Mesh:网格,是Geometry与Material的组合,被放置在Scene中。Light:光源。像MeshLambertMaterial这类材质依赖光源才能正确显示明暗。
进入三维视图前,先通过 WebGL.isWebGLAvailable() 检测设备是否支持 WebGL;不支持时展示 getWebGLErrorMessage() 的错误提示并终止渲染。
场景搭建
三维视图的初始化流程如下(与 apps/design 保持一致):
typescript
const scene = new THREE.Scene();
scene.background = new THREE.Color(data.backgroundColor || "#444444");
// 正交相机:left, right, top, bottom, near, far
const k = width / height;
const s = 600;
const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 0.1, 8000);
camera.position.set(1000, 1000, 1000);
camera.lookAt(0, 0, 0);
// 渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.shadowMap.enabled = true;
renderer.autoClear = false;
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
box.appendChild(renderer.domElement);
// 轨道控制器(鼠标旋转 / 缩放视角)
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableZoom = true;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
同时向场景添加辅助工具与光源:
AxesHelper:坐标轴辅助线。GridHelper(网格)与PolarGridHelper(点状径向网格):轴网,帮助观察坐标。- 三个
DirectionalLight定向光:均匀照亮模型。 ViewHelper:右下角的坐标轴指示微缩视图。
二维到三维的对象映射
二维画布的数据来自 Fabric.js(2D 编辑) 序列化的同一份 JSON(对象为 fabric.Object,其上挂有 meta 业务字段)。三维视图遍历 data.objects,按 obj.meta?.type 将二维对象映射为对应的三维几何体:
typescript
objects.forEach((obj) => {
const metaType = obj.meta?.type;
if (metaType === "cube") Cube.render3d(obj, scene);
if (metaType === "cuboid") Cuboid.render3d(obj, scene);
if (metaType === "cone") Cone.render3d(obj, scene);
if (metaType === "tetrahedron") Tetrahedron.render3d(obj, scene);
// sphere / ring / board / capsule / polygon ...
});1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
以立方体(Cube)为核心示例
Cube.render3d 接收一个在二维画布中本质上是 fabric.Rect 的对象,将其尺寸、颜色与离地高度映射为三维网格。
关键映射关系:
- 平面
width * scaleX→ 立方体边长,用于BoxGeometry(正方体三个方向等长)。 - 平面
fill→ 材质颜色,用于MeshLambertMaterial。 - 平面
left→ Three 的 X 轴坐标。 - 平面
top→ Three 的 Z 轴坐标。 meta.depthAboveGround(离地高度)→ Three 的 Y 轴坐标。- 几何体以
position定位其中心,因此各方向加上length / 2偏移。
typescript
const size = (rect.width ?? 0) * (rect.scaleX ?? 1);
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(size, size, size),
new THREE.MeshLambertMaterial({ color: rect.fill as string }),
);
mesh.position.set(
(rect.left ?? 0) + size / 2, // X
rect.meta.depthAboveGround + size / 2, // Y:离地高度
(rect.top ?? 0) + size / 2, // Z
);
scene.add(mesh);1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
这一对应关系源于 Three.js 的右手坐标系:X 轴向右为正、Y 轴向上为正、Z 轴垂直于屏幕向外为正。二维俯视图的横轴(left)映射为三维的 X、纵轴(top)映射为 Z,而离地高度单独占用竖直方向的 Y。
动画循环
三维场景通过持续的 requestAnimationFrame 循环驱动渲染,每次更新轨道控制器、清空并重绘场景,并渲染右下角的 ViewHelper:
typescript
function animate() {
controls.update();
renderer.clear();
renderer.render(scene, camera);
viewHelper.render(renderer as any);
requestAnimationFrame(animate);
}
animate();1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
常见问题与实践建议
以下属于后续维护建议,当前编辑器尚未完全实现,与既有行为分开说明:
- 材质依赖光源:
MeshLambertMaterial需要场景中存在光源才能有明暗效果;移除光源会导致模型显示为纯黑或不可见。 - 设备像素比:
renderer.setPixelRatio(window.devicePixelRatio)保证高分屏清晰,但设备像素比过高时也会带来性能开销。 - 窗口尺寸变化:监听
resize事件同步更新相机宽高比与renderer.setSize,否则画面会拉伸或裁切。 - 切换视图应停止循环并释放资源:从三维切回二维时应取消
requestAnimationFrame停止动画循环,并对不再使用的geometry/material调用dispose()、对renderer调用dispose()并移除其domElement,避免内存占用持续增长。 - 动画循环的启动时机:
animate()目前渲染一次后便永久循环;更稳妥的做法是在创建场景时启动、在销毁时(onUnmounted)停止。