Skip to content

102. 实战:酷家乐装修编辑器(六)

Published:

上节把洞挖好了:

2025-06-16 16.14.16.gif

我们加载一个窗户的模型放在这个位置,调整下大小。

从 sketchfab.com 来找:

https://sketchfab.com/3d-models/window-af3ba48119bc482a8d0077f0f265cd4a#download

image.png

下载下来放在 public 目录下:

image.png

代码里加载进来:

image.png

let winModel: { model: THREE.Group, size: THREE.Vector3} | null = null;

async function loadWindow() {
    if(winModel !== null) {
        return winModel;
    } else {
        const group = new THREE.Group();
        const loader = new GLTFLoader();
        const gltf = await loader.loadAsync("./window.glb");
        group.add(gltf.scene);
        
        const box = new THREE.Box3();
        box.expandByObject(gltf.scene);

        const size = box.getSize(new THREE.Vector3());
        console.log('size', size)
        winModel =  {
            model: group,
            size
        };
        return winModel;
    }
}

我们用一个全局变量来存储窗户模型,这样全局只加载一次。

使用 loadAsync 和 async await 来实现同步加载之后返回。

因为我们要加载完模型后计算大小再返回。

用 Box3 包围盒来拿到模型大小。

先加载看下效果:

image.png

注意要给函数加上 async

const { model, size} = await loadWindow();
model.scale.setScalar(200);
scene.add(model);

先放大 200 倍

image.png

可以看到,窗户加载出来了,size 也计算了出来。

那如何设置窗户的大小、位置呢?

自然是根据窗户数据的宽高来计算:

image.png

model.position.x = (item.right.x - item.left.x) / 2;
model.position.y = item.height / 2;
model.scale.set(win.width / size.x, win.height / size.y, 1);

位置就是墙的宽度 /2 高度 /2 的位置。

宽高根据窗口宽高来放大。

看下效果:

2025-06-18 08.03.00.gif

这样窗户就绘制出来了。

然后我们多加一面墙试试:

不过在那之前继续优化下存储结构:

image.png

之前记录了墙的左右端点的,好像也没啥必要,直接记录 width 和 position 就行。

有了 width 和 position 自然可以计算出左右端点坐标。

image.png

image.png

position: { x: 0, y: 0, z: 0},
width: 800,
position: { x: number, y: number, z: number},
width: number,

绘制的代码自然也要改一下:

image.png

shape.moveTo(0,0);
shape.lineTo(0, item.height);
shape.lineTo(item.width, item.height);
shape.lineTo(item.width, 0);
shape.lineTo(0, 0);

还是按照左下 -> 右下 -> 右上 -> 左上 -> 左下 转一圈的顺序。

直接根据 width、height 来画就行。

计算窗户位置也换成 item.width

image.png

model.position.x = item.width / 2;

还要设置下墙的位置:

image.png

wall.position.set(item.position.x, item.position.y, item.position.z);

多加几面墙试一下:

image.png

{
    position: { x: 0, y: 0, z: 800},
    width: 800,
    height: 500,
    depth: 30,
    windows: [
        {
            leftBottomPosition: {
                x: 100,
                z: 100
            },
            width: 600,
            height: 300
        }
    ]
}

复制了一份墙体数据,改了下 z

看一下:

image.png

墙的位置是对了,但是窗户没出来。

设置下 z 轴位置:

image.png

model.position.z = item.position.z;

image.png

现在相机的位置有点低了,调整下相机位置:

image.png

camera.position.set(1500, 1500, 1000);

2025-06-18 08.41.44.gif

此外墙的颜色不大对,这个是灯光的原因。

调节下灯光的位置,让它在房屋正上方,然后加大环境光的强度。

image.png

directionalLight.position.set(0, 1500, 0);
const ambientLight = new THREE.AmbientLight(0xffffff, 2);

image.png

这样就好多了。

最后我们再画另外两面墙。

但现在墙的旋转角度是固定的,这个也应该不一样,把它加到 json 里:

image.png

rotationY?: number,

绘制的时候设置下:

image.png

if(item.rotationY) {
    wall.rotation.y = item.rotationY;
}

让第一面墙旋转 60 度:

image.png

看下效果:

image.png

可以看到,墙旋转了,但是窗户没有跟着一起旋转。

这是以为我们没把窗户添加到 wall 的 group 里。

image.png

这样墙和窗户是独立的,自然不会一起旋转。

我们把窗户添加到墙的 group 里:

image.png

这样也就不用单独设置 z 了。

2025-06-18 08.59.31.gif

完成。

我们改下数据,绘制完 4 面墙。

const useHouseStore = create<State>((set, get) => {
    return {
        data: {
            walls: [
                {
                    position: { x: 0, y: 0, z: 0},
                    width: 800,
                    height: 500,
                    depth: 30,
                    windows: [
                        {
                            leftBottomPosition: {
                                x: 100,
                                z: 100
                            },
                            width: 600,
                            height: 300
                        }
                    ]
                },
                {
                    position: { x: 0, y: 0, z: 800},
                    width: 800,
                    height: 500,
                    depth: 30,
                    windows: [
                        {
                            leftBottomPosition: {
                                x: 100,
                                z: 100
                            },
                            width: 600,
                            height: 300
                        }
                    ]
                },
                {
                    position: { x: 0, y: 0, z: 0},
                    width: 800,
                    height: 500,
                    depth: 30,
                    rotationY: -Math.PI / 2,
                    windows: [
                    ]
                },
                {
                    position: { x: 800, y: 0, z: 0},
                    width: 800,
                    height: 500,
                    depth: 30,
                    rotationY: -Math.PI / 2,
                    windows: [
                    ]
                }
            ]
        }
    }
});

2025-06-18 17.50.47.gif

案例代码上传了小册仓库

总结

这节我们加载了窗户模型,并给它设置了正确的大小、位置。

首先,我们继续优化了墙的存储结构,保存 position、rotation、width、height、depth 信息,根据这些来绘制墙。

然后加载窗户模型后,根据包围盒计算出模型的尺寸,再根据窗户的尺寸来设置 scale 就好了。

注意要把窗户添加到墙的 group 下,这样才会作为一个整体一起旋转、位移。

此外,我们加强了环境光,现在看起来就是白墙了。

下节我们继续来绘制门。

评论