Skip to content

100. 实战:酷家乐装修编辑器(四)

Published:

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

2025-05-21 20.51.51.gif

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

比如墙壁:

2025-05-24 17.18.58.gif

在 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 的场景里把墙渲染出来:

image.png

import * as THREE from 'three';
const scene3DRef = useRef<THREE.Scene>(null);

const { data } = useHouseStore();
scene3DRef.current = scene;

用 useRef 保存 scene。

然后根据 data 把墙画出来,加到 scene 中。

image.png

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 拉伸一下。

画出来的墙是这样的:

image.png

所以需要绕 x 轴旋转 -Math.PI / 2

看下效果:

2025-05-24 19.41.10.gif

这样,两面墙就画好了。

然后再画一下 2D 的:

image.png

image.png

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

image.png

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

看下效果:

2025-05-24 19.45.01.gif

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

image.png

controls.addEventListener('change', () => {
    console.log(controls.target, camera.position);
});

监听 change 可视化调试。

2025-05-24 19.51.59.gif

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

image.png

改了 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);
}

看下效果:

image.png

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

2025-05-24 20.10.21.gif

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

image.png

const controls = new MapControls(camera, renderer.domElement);
controls.enableRotate = false;

看下效果:

2025-05-24 20.12.47.gif

现在就只能拖动、放缩,但不能旋转了。

3D 视图的相机位置也要改下:

image.png

2025-05-24 20.16.12.gif

这样,2D、3D 的墙体绘制就完成了。

案例代码上传了小册仓库

总结

这节我们引入了全局 store,并且画出了 2D、3D 的墙。

用 zustand 实现全局 store,用 json 保存 4 个点的墙的数据,然后 ExtrudeGeometry 拉伸成墙体。

在 2D 视图里则是用 ShapeGeometry 画出形状。

最后我们调了下相机角度。

这样,同一份数据就可以渲染成 2D 的平面户型图,和 3D 的立体房屋。

评论