上节把洞挖好了:

我们加载一个窗户的模型放在这个位置,调整下大小。
从 sketchfab.com 来找:
https://sketchfab.com/3d-models/window-af3ba48119bc482a8d0077f0f265cd4a#download

下载下来放在 public 目录下:

代码里加载进来:

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 包围盒来拿到模型大小。
先加载看下效果:

注意要给函数加上 async
const { model, size} = await loadWindow();
model.scale.setScalar(200);
scene.add(model);
先放大 200 倍

可以看到,窗户加载出来了,size 也计算了出来。
那如何设置窗户的大小、位置呢?
自然是根据窗户数据的宽高来计算:

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 的位置。
宽高根据窗口宽高来放大。
看下效果:

这样窗户就绘制出来了。
然后我们多加一面墙试试:
不过在那之前继续优化下存储结构:

之前记录了墙的左右端点的,好像也没啥必要,直接记录 width 和 position 就行。
有了 width 和 position 自然可以计算出左右端点坐标。


position: { x: 0, y: 0, z: 0},
width: 800,
position: { x: number, y: number, z: number},
width: number,
绘制的代码自然也要改一下:

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

model.position.x = item.width / 2;
还要设置下墙的位置:

wall.position.set(item.position.x, item.position.y, item.position.z);
多加几面墙试一下:

{
position: { x: 0, y: 0, z: 800},
width: 800,
height: 500,
depth: 30,
windows: [
{
leftBottomPosition: {
x: 100,
z: 100
},
width: 600,
height: 300
}
]
}
复制了一份墙体数据,改了下 z
看一下:

墙的位置是对了,但是窗户没出来。
设置下 z 轴位置:

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

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

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

此外墙的颜色不大对,这个是灯光的原因。
调节下灯光的位置,让它在房屋正上方,然后加大环境光的强度。

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

这样就好多了。
最后我们再画另外两面墙。
但现在墙的旋转角度是固定的,这个也应该不一样,把它加到 json 里:

rotationY?: number,
绘制的时候设置下:

if(item.rotationY) {
wall.rotation.y = item.rotationY;
}
让第一面墙旋转 60 度:

看下效果:

可以看到,墙旋转了,但是窗户没有跟着一起旋转。
这是以为我们没把窗户添加到 wall 的 group 里。

这样墙和窗户是独立的,自然不会一起旋转。
我们把窗户添加到墙的 group 里:

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

完成。
我们改下数据,绘制完 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: [
]
}
]
}
}
});

案例代码上传了小册仓库
总结
这节我们加载了窗户模型,并给它设置了正确的大小、位置。
首先,我们继续优化了墙的存储结构,保存 position、rotation、width、height、depth 信息,根据这些来绘制墙。
然后加载窗户模型后,根据包围盒计算出模型的尺寸,再根据窗户的尺寸来设置 scale 就好了。
注意要把窗户添加到墙的 group 下,这样才会作为一个整体一起旋转、位移。
此外,我们加强了环境光,现在看起来就是白墙了。
下节我们继续来绘制门。