上节初始化了 Three.js,并实现了 2D、3D 的切换:

这节我们来加上全局 store,并且根据 store 来渲染场景。
比如墙壁:

在 3D 的房屋和 2D 的户型图里,墙壁的形状都是一样的。
这明显是根据同一份数据来做的不同渲染。
我们可以把房屋的墙壁、家具等信息存到 json 里,然后 2D、3D 都根据这个 json 来渲染。
我们用 zustand 来做全局状态管理:
npm install --save zustand
写一下 store:
src/store/index.ts
import { create } from "zustand";
interface Wall {
p1: {
x: number,
z: number
},
p2: {
x: number,
z: number
},
p3: {
x: number,
z: number
},
p4: {
x: number,
z: number
}
}
interface State {
data: {
walls: Array<Wall>
}
}
const useHouseStore = create<State>((set, get) => {
return {
data: {
walls: [
{
p1: {x: 0, z: 0},
p2: {x: 500, z: 0},
p3: {x: 500, z: 30},
p4: {x: 0, z: 30}
},
{
p1: {x: 0, z: 0},
p2: {x: 0, z: 500},
p3: {x: 30, z: 500},
p4: {x: 30, z: 0}
}
]
}
}
});
export {
useHouseStore
}
我们先在 store 里放两面墙的数据。
墙只要指定 4 个点的 x、z 就好了,然后沿着 y 轴拉伸一下。
接下来在 2D 和 3D 的场景里把墙渲染出来:

import * as THREE from 'three';
const scene3DRef = useRef<THREE.Scene>(null);
const { data } = useHouseStore();
scene3DRef.current = scene;
用 useRef 保存 scene。
然后根据 data 把墙画出来,加到 scene 中。

useEffect(() => {
const scene = scene3DRef.current!;
const walls = data.walls.map(item => {
const shape = new THREE.Shape();
shape.moveTo(item.p1.x, item.p1.z);
shape.lineTo(item.p2.x, item.p2.z);
shape.lineTo(item.p3.x, item.p3.z);
shape.lineTo(item.p4.x, item.p4.z);
shape.lineTo(item.p1.x, item.p1.z);
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: 500
});
const material = new THREE.MeshPhongMaterial({
color: 'white'
})
const wall = new THREE.Mesh(geometry, material);
wall.rotateX(-Math.PI/2);
return wall;
});
scene.add(...walls);
}, [data]);
用 Shape 把墙的 4 个点连起来,然后用 ExtrudeGeometry 拉伸一下。
画出来的墙是这样的:

所以需要绕 x 轴旋转 -Math.PI / 2
看下效果:

这样,两面墙就画好了。
然后再画一下 2D 的:


const scene2DRef = useRef<THREE.Scene>(null);
scene2DRef.current = scene;

useEffect(() => {
const scene = scene2DRef.current!;
const walls = data.walls.map(item => {
const shape = new THREE.Shape();
shape.moveTo(item.p1.x, item.p1.z);
shape.lineTo(item.p2.x, item.p2.z);
shape.lineTo(item.p3.x, item.p3.z);
shape.lineTo(item.p4.x, item.p4.z);
shape.lineTo(item.p1.x, item.p1.z);
const geometry = new THREE.ShapeGeometry(shape);
const material = new THREE.MeshPhongMaterial({
color: 'white'
})
const wall = new THREE.Mesh(geometry, material);
wall.rotateX(-Math.PI/2);
return wall;
});
scene.add(...walls);
}, [data]);
和前面一样,只不过这次画平面图,用 ShapeGeometry
看下效果:

没啥问题,我们改下相机位置:

controls.addEventListener('change', () => {
console.log(controls.target, camera.position);
});
监听 change 可视化调试。

最后把数值更新到代码里:

改了 camera 的 lookAt 要同时修改 OrbitControls 的 target 才可以,不然会被重置到 0,0,0。
camera.position.set(200, 500, -100);
camera.lookAt(200, 0, -100);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(200, 0, -100);
controls.addEventListener('change', () => {
console.log(controls.target, camera.position);
});
function render() {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(render);
}
看下效果:

但现在还是能随意旋转相机角度的:

我们把它禁用掉,默认 OrbitControls 的鼠标左键旋转,右键拖动,这里换成 MapControls 更合适,它是左键拖动,右键旋转。

const controls = new MapControls(camera, renderer.domElement);
controls.enableRotate = false;
看下效果:

现在就只能拖动、放缩,但不能旋转了。
3D 视图的相机位置也要改下:


这样,2D、3D 的墙体绘制就完成了。
案例代码上传了小册仓库
总结
这节我们引入了全局 store,并且画出了 2D、3D 的墙。
用 zustand 实现全局 store,用 json 保存 4 个点的墙的数据,然后 ExtrudeGeometry 拉伸成墙体。
在 2D 视图里则是用 ShapeGeometry 画出形状。
最后我们调了下相机角度。
这样,同一份数据就可以渲染成 2D 的平面户型图,和 3D 的立体房屋。