【可视化学习】55-家具编辑器
发表于:2023-12-05 |

前言

今天给大家分享一个家具编辑器实现的小案例,一起来学习吧。

基础设置与物体添加列表

创建基础代码

这里就不多说了,直接贴代码了,之前说了无数次了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 导入threejs
import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
// 导入lil.gui
import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
// 导入hdr加载器
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
// 创建场景
const scene = new THREE.Scene();

// 创建相机
const camera = new THREE.PerspectiveCamera(
45, // 视角
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近平面
1000 // 远平面
);

// 相机位置
camera.position.z = 8;
camera.position.y = 2.5;
camera.position.x = 3;
camera.lookAt(0, 1.2, 0);

// 创建渲染器
const renderer = new THREE.WebGLRenderer({
antialias: true, // 开启抗锯齿
});
renderer.shadowMap.enabled = true;
renderer.toneMapping = THREE.ReinhardToneMapping;
renderer.toneMappingExposure = 1;
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);


// 添加世界坐标辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

// 添加轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置带阻尼的惯性
controls.enableDamping = true;

// 渲染函数
function animate() {
controls.update();
requestAnimationFrame(animate);
// 渲染
renderer.render(scene, camera);
}
animate();

// 监听窗口变化
window.addEventListener("resize", () => {
// 重置渲染器宽高比
renderer.setSize(window.innerWidth, window.innerHeight);
// 重置相机宽高比
camera.aspect = window.innerWidth / window.innerHeight;
// 更新相机投影矩阵
camera.updateProjectionMatrix();
});

加载贴图

1
2
3
4
5
6
7
8
9
10
11
12
// rgbeLoader 加载hdr贴图
let rgbeLoader = new RGBELoader();
rgbeLoader.load("./texture/Alex_Hart-Nature_Lab_Bones_2k.hdr", (envMap) => {
// 设置球形贴图
// envMap.mapping = THREE.EquirectangularReflectionMapping;
envMap.mapping = THREE.EquirectangularRefractionMapping;
// 设置环境贴图
// scene.background = envMap;
scene.background = new THREE.Color(0xcccccc);
// 设置环境贴图
scene.environment = envMap;
});

添加网格

1
2
3
4
5
// 添加网格辅助器
const gridHelper = new THREE.GridHelper(50, 50);
gridHelper.material.opacity = 0.3;
gridHelper.material.transparent = true;
scene.add(gridHelper);

加载贴图

使用GUI动态添加户型模型

导入glb模型的前置工作我就不说了,之前都说过很多次了,如果忘了的话大家可以参考之前需要加载模型的文章
这里我定义了一个basicScene变量,用来存储加载的模型,然后通过GUI动态添加到场景中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 导入gltf加载器
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
// 导入draco解码器
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
// 实例化加载器gltf
const gltfLoader = new GLTFLoader();
// 实例化加载器draco
const dracoLoader = new DRACOLoader();
// 设置draco路径
dracoLoader.setDecoderPath("./draco/");
// 设置gltf加载器draco解码器
gltfLoader.setDRACOLoader(dracoLoader);
// 加载模型
gltfLoader.load(
// 模型路径
"./model/house/house-scene-min.glb",
// 加载完成回调
(gltf) => {
basicScene = gltf.scene;
}
);
let basicScene;
let eventObj = {
addScene: function () {
scene.add(basicScene);
},
};
// 创建GUI
const gui = new GUI();

gui.add(eventObj, "addScene").name("添加户型基础模型");

GUI动态添加家具模型

这里我创建了一个folder,依次往folder里面添加家具模型,然后通过点击按钮的方式添加到场景中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 添加物体目录
let meshList = [
{
name: "盆栽",
path: "./model/house/plants-min.glb",
},
{
name: "单人沙发",
path: "./model/house/sofa_chair_min.glb",
},
];
let folderAddMehs = gui.addFolder("添加物体");
let sceneMeshes = [];

meshList.forEach((item) => {
item.addMesh = function () {
gltfLoader.load(item.path, (gltf) => {
sceneMeshes.push({
...item,
object3d: gltf.scene,
});
let object3d = gltf.scene;
scene.add(object3d);
});
};
folderAddMehs.add(item, "addMesh").name(item.name);
});

使用变换控制器操作物体

导入并创建变换控制器

这里我导入了变换控制器,然后创建了一个变换控制器,然后监听了变换控制器的拖动事件,当拖动物体的时候,禁用轨道控制器,当拖动结束的时候,启用轨道控制器

1
2
3
4
5
6
7
8
9
10
// 导入变换控制器
import { TransformControls } from "three/addons/controls/TransformControls.js";
// 创建变换控制器
let tControls = new TransformControls(camera, renderer.domElement);
tControls.addEventListener("change", animate);
// 监听拖动事件,当拖动物体时候,禁用轨道控制器
tControls.addEventListener("dragging-changed", function (event) {
controls.enabled = !event.value;
});
scene.add(tControls);

物体创建选中

我让物体创建的时候就被选中了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function tControlSelect(mesh) {
tControls.attach(mesh);
}

meshList.forEach((item) => {
item.addMesh = function () {
gltfLoader.load(item.path, (gltf) => {
sceneMeshes.push({
...item,
object3d: gltf.scene,
});
let object3d = gltf.scene;
scene.add(object3d);
tControlSelect(object3d);
});
};
folderAddMehs.add(item, "addMesh").name(item.name);
});

家居列表与控制物体切换

定义家居列表

1
let meshesFolder = gui.addFolder("家居列表");

添加点击选中事件

这里我定义了一个meshOpt对象,然后添加了一个toggleMesh方法,然后通过meshesFolder添加到GUI中,然后通过点击按钮的方式,切换选中的物体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
meshList.forEach((item) => {
item.addMesh = function () {
gltfLoader.load(item.path, (gltf) => {
sceneMeshes.push({
...item,
object3d: gltf.scene,
});
let object3d = gltf.scene;
scene.add(object3d);
tControlSelect(object3d);
let meshOpt = {
toggleMesh: function () {
tControlSelect(object3d);
},
};
meshesFolder
.add(meshOpt, "toggleMesh")
.name(item.name);
});
};
folderAddMehs.add(item, "addMesh").name(item.name);
});

标记数量

上面的视频我们可以看到我们可以切换物体了,但还是有个问题就是我们没法区分物体,所以我们给我们的物体加个序号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let meshesNum = {};
meshList.forEach((item) => {
item.addMesh = function () {
gltfLoader.load(item.path, (gltf) => {
sceneMeshes.push({
...item,
object3d: gltf.scene,
});
let object3d = gltf.scene;
scene.add(object3d);
tControlSelect(object3d);
let meshOpt = {
toggleMesh: function () {
tControlSelect(object3d);
},
};
meshesNum[item.name] = meshesNum[item.name]
? meshesNum[item.name] + 1
: 1;
meshesFolder
.add(meshOpt, "toggleMesh")
.name(item.name + meshesNum[item.name]);
});
};
folderAddMehs.add(item, "addMesh").name(item.name);
});

变换控制器对物体位移、旋转、缩放

添加按钮

这里我们使用变换控制器的setMode属性就可以进行切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let eventObj = {
addScene: function () {
scene.add(basicScene);
},
setTranslate: function () {
tControls.setMode("translate");
},
setRotate: function () {
tControls.setMode("rotate");
},
setScale: function () {
tControls.setMode("scale");
},
};

gui.add(eventObj, "addScene").name("添加户型基础模型");
gui.add(eventObj, "setTranslate").name("位移模式");
gui.add(eventObj, "setRotate").name("旋转模式");
gui.add(eventObj, "setScale").name("缩放模式");

添加切换快捷键

1
2
3
4
5
6
7
8
9
10
11
12
13
// 监听鼠标按键事件
window.addEventListener("keydown", (event) => {
// 判断是否按的是t键
if (event.key === "t") {
eventObj.setTranslate();
}
if (event.key === "r") {
eventObj.setRotate();
}
if (event.key === "s") {
eventObj.setScale();
}
});

取消选择和使用物体本地坐标

取消选择

调用detach方法就可以取消选择了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let eventObj = {
addScene: function () {
scene.add(basicScene);
},
setTranslate: function () {
tControls.setMode("translate");
},
setRotate: function () {
tControls.setMode("rotate");
},
setScale: function () {
tControls.setMode("scale");
},
cancelMesh: function () {
tControls.detach();
},
};
gui.add(eventObj, "cancelMesh").name("取消选择");

本地坐标和世界坐标切换

调整space为local或者world就可以切换了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let eventObj = {
addScene: function () {
scene.add(basicScene);
},
setTranslate: function () {
tControls.setMode("translate");
},
setRotate: function () {
tControls.setMode("rotate");
},
setScale: function () {
tControls.setMode("scale");
},
cancelMesh: function () {
tControls.detach();
},
toggleSpace: function () {
tControls.setSpace(tControls.space === "local" ? "world" : "local");
},
};
gui.add(eventObj, "toggleSpace").name("切换空间模式");

我们可以很明显的看到切换空间的时候,那个箭头指向的方向是不一样的,一个是坐标系,一个是以物体为坐标系

固定缩放位移旋转比例

固定位移比例

我这里定义了一个translateSnapNum变量,然后通过onChange监听变量的变化,然后调用setTranslationSnap方法就可以固定缩放比例了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let eventObj = {
addScene: function () {
scene.add(basicScene);
},
setTranslate: function () {
tControls.setMode("translate");
},
setRotate: function () {
tControls.setMode("rotate");
},
setScale: function () {
tControls.setMode("scale");
},
cancelMesh: function () {
tControls.detach();
},
toggleSpace: function () {
tControls.setSpace(tControls.space === "local" ? "world" : "local");
},
translateSnapNum: null,
};
let snapFolder = gui.addFolder("固定设置");
snapFolder
.add(eventObj, "translateSnapNum", {
不固定: null,
1: 1,
0.1: 0.1,
10: 10,
})
.name("固定位移设置")
.onChange(() => {
tControls.setTranslationSnap(eventObj.translateSnapNum);
});

同样的道理,我们可以设置旋转和缩放的比例

固定旋转比例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let eventObj = {
addScene: function () {
scene.add(basicScene);
},
setTranslate: function () {
tControls.setMode("translate");
},
setRotate: function () {
tControls.setMode("rotate");
},
setScale: function () {
tControls.setMode("scale");
},
cancelMesh: function () {
tControls.detach();
},
toggleSpace: function () {
tControls.setSpace(tControls.space === "local" ? "world" : "local");
},
translateSnapNum: null,
rotateSnapNum: 0,
};
snapFolder
.add(eventObj, "rotateSnapNum", 0, 1)
.step(0.01)
.name("旋转")
.onChange(() => {
tControls.setRotationSnap(eventObj.rotateSnapNum * Math.PI * 2);
});

固定缩放比例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let eventObj = {
addScene: function () {
scene.add(basicScene);
},
setTranslate: function () {
tControls.setMode("translate");
},
setRotate: function () {
tControls.setMode("rotate");
},
setScale: function () {
tControls.setMode("scale");
},
cancelMesh: function () {
tControls.detach();
},
toggleSpace: function () {
tControls.setSpace(tControls.space === "local" ? "world" : "local");
},
translateSnapNum: null,
rotateSnapNum: 0,
scaleSnapNum: 0,
};
snapFolder
.add(eventObj, "scaleSnapNum", 0, 2)
.step(0.1)
.name("缩放")
.onChange(() => {
tControls.setScaleSnap(eventObj.scaleSnapNum);
});

吸附地面与室内灯光开关

吸附地面

这个很容易实现,就是吸附地面开启的时候,把物体的位置y的坐标设置为0就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let eventObj = {
addScene: function () {
scene.add(basicScene);
},
setTranslate: function () {
tControls.setMode("translate");
},
setRotate: function () {
tControls.setMode("rotate");
},
setScale: function () {
tControls.setMode("scale");
},
cancelMesh: function () {
tControls.detach();
},
toggleSpace: function () {
tControls.setSpace(tControls.space === "local" ? "world" : "local");
},
translateSnapNum: null,
rotateSnapNum: 0,
scaleSnapNum: 0,
isClampGroup: false
};
snapFolder.add(eventObj, "isClampGroup").name("是否吸附到地面");
tControls.addEventListener("change", () => {
if (eventObj.isClampGroup) {
tControls.object.position.y = 0;
}
})

灯光操作

因为我们这里的光都是因为我们的贴图,所以只需要调整色调映射的强度即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
let eventObj = {
Fullscreen: function () {
// 全屏
document.body.requestFullscreen();
console.log("全屏");
},
ExitFullscreen: function () {
document.exitFullscreen();
console.log("退出全屏");
},
addScene: function () {
scene.add(basicScene);
},
setTranslate: function () {
tControls.setMode("translate");
},
setRotate: function () {
tControls.setMode("rotate");
},
setScale: function () {
tControls.setMode("scale");
},
toggleSpace: function () {
tControls.setSpace(tControls.space === "local" ? "world" : "local");
},
cancelMesh: function () {
tControls.detach();
},
translateSnapNum: null,
rotateSnapNum: 0,
scaleSnapNum: 0,
isClampGroup: false,
isLight: true,
};
gui
.add(eventObj, "isLight")
.name("是否开启灯光")
.onChange((value) => {
if (value) {
renderer.toneMappingExposure = 1;
} else {
renderer.toneMappingExposure = 0.1;
}
});

总结

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
// 导入threejs
import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
// 导入lil.gui
import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
// 导入hdr加载器
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
// 导入gltf加载器
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
// 导入draco解码器
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
// 导入变换控制器
import { TransformControls } from "three/addons/controls/TransformControls.js";

// 创建场景
const scene = new THREE.Scene();

// 创建相机
const camera = new THREE.PerspectiveCamera(
45, // 视角
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近平面
1000 // 远平面
);

// 创建渲染器
const renderer = new THREE.WebGLRenderer({
antialias: true, // 开启抗锯齿
});
// 设置渲染器允许投射阴影
renderer.shadowMap.enabled = true;
renderer.toneMapping = THREE.ReinhardToneMapping;
renderer.toneMappingExposure = 1;
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 设置相机位置
camera.position.z = 8;
camera.position.y = 2.5;
camera.position.x = 3;
camera.lookAt(0, 1.2, 0);

// 添加世界坐标辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

// 添加网格辅助器
const gridHelper = new THREE.GridHelper(50, 50);
gridHelper.material.opacity = 0.3;
gridHelper.material.transparent = true;
scene.add(gridHelper);

// 添加轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置带阻尼的惯性
controls.enableDamping = true;
// 设置阻尼系数
controls.dampingFactor = 0.05;
// 设置旋转速度
controls.addEventListener("change", () => {
renderer.render(scene, camera);
});

// rgbeLoader 加载hdr贴图
let rgbeLoader = new RGBELoader();
rgbeLoader.load("./texture/Alex_Hart-Nature_Lab_Bones_2k.hdr", (envMap) => {
// 设置球形贴图
// envMap.mapping = THREE.EquirectangularReflectionMapping;
envMap.mapping = THREE.EquirectangularRefractionMapping;
// 设置环境贴图
// scene.background = envMap;
scene.background = new THREE.Color(0xcccccc);
// 设置环境贴图
scene.environment = envMap;
});

// 渲染函数
function animate() {
controls.update();
requestAnimationFrame(animate);
// 渲染
renderer.render(scene, camera);
}
animate();

// 监听窗口变化
window.addEventListener("resize", () => {
// 重置渲染器宽高比
renderer.setSize(window.innerWidth, window.innerHeight);
// 重置相机宽高比
camera.aspect = window.innerWidth / window.innerHeight;
// 更新相机投影矩阵
camera.updateProjectionMatrix();
});

// 实例化加载器gltf
const gltfLoader = new GLTFLoader();
// 实例化加载器draco
const dracoLoader = new DRACOLoader();
// 设置draco路径
dracoLoader.setDecoderPath("./draco/");
// 设置gltf加载器draco解码器
gltfLoader.setDRACOLoader(dracoLoader);
// 加载模型
gltfLoader.load(
// 模型路径
"./model/house/house-scene-min.glb",
// 加载完成回调
(gltf) => {
basicScene = gltf.scene;
}
);

let basicScene;
let eventObj = {
addScene: function () {
scene.add(basicScene);
},
setTranslate: function () {
tControls.setMode("translate");
},
setRotate: function () {
tControls.setMode("rotate");
},
setScale: function () {
tControls.setMode("scale");
},
cancelMesh: function () {
tControls.detach();
},
toggleSpace: function () {
tControls.setSpace(tControls.space === "local" ? "world" : "local");
},
translateSnapNum: null,
rotateSnapNum: 0,
scaleSnapNum: 0,
isClampGroup: false,
isLight: true,
};

// 创建GUI
const gui = new GUI();

gui.add(eventObj, "addScene").name("添加户型基础模型");
gui.add(eventObj, "setTranslate").name("位移模式");
gui.add(eventObj, "setRotate").name("旋转模式");
gui.add(eventObj, "setScale").name("缩放模式");
gui.add(eventObj, "toggleSpace").name("切换空间模式");
gui.add(eventObj, "cancelMesh").name("取消选择");
gui
.add(eventObj, "isLight")
.name("是否开启灯光")
.onChange((value) => {
if (value) {
renderer.toneMappingExposure = 1;
} else {
renderer.toneMappingExposure = 0.1;
}
});

// 监听鼠标按键事件
window.addEventListener("keydown", (event) => {
// 判断是否按的是t键
if (event.key === "t") {
eventObj.setTranslate();
}
if (event.key === "r") {
eventObj.setRotate();
}
if (event.key === "s") {
eventObj.setScale();
}
});

let snapFolder = gui.addFolder("固定设置");
snapFolder
.add(eventObj, "translateSnapNum", {
不固定: null,
1: 1,
0.1: 0.1,
10: 10,
})
.name("固定位移设置")
.onChange(() => {
tControls.setTranslationSnap(eventObj.translateSnapNum);
});


snapFolder
.add(eventObj, "rotateSnapNum", 0, 1)
.step(0.01)
.name("旋转")
.onChange(() => {
tControls.setRotationSnap(eventObj.rotateSnapNum * Math.PI * 2);
});
snapFolder
.add(eventObj, "scaleSnapNum", 0, 2)
.step(0.1)
.name("缩放")
.onChange(() => {
tControls.setScaleSnap(eventObj.scaleSnapNum);
});
snapFolder.add(eventObj, "isClampGroup").name("是否吸附到地面");


// 添加物体目录
let meshList = [
{
name: "盆栽",
path: "./model/house/plants-min.glb",
},
{
name: "单人沙发",
path: "./model/house/sofa_chair_min.glb",
},
];
let folderAddMehs = gui.addFolder("添加物体");
let sceneMeshes = [];
let meshesFolder = gui.addFolder("家居列表");
let meshesNum = {};
meshList.forEach((item) => {
item.addMesh = function () {
gltfLoader.load(item.path, (gltf) => {
sceneMeshes.push({
...item,
object3d: gltf.scene,
});
let object3d = gltf.scene;
scene.add(object3d);
tControlSelect(object3d);
let meshOpt = {
toggleMesh: function () {
tControlSelect(object3d);
},
};
meshesNum[item.name] = meshesNum[item.name]
? meshesNum[item.name] + 1
: 1;
meshesFolder
.add(meshOpt, "toggleMesh")
.name(item.name + meshesNum[item.name]);
});
};
folderAddMehs.add(item, "addMesh").name(item.name);
});

// 创建变换控制器
let tControls = new TransformControls(camera, renderer.domElement);
tControls.addEventListener("change", animate);
// 监听拖动事件,当拖动物体时候,禁用轨道控制器
tControls.addEventListener("dragging-changed", function (event) {
controls.enabled = !event.value;
});
tControls.addEventListener("change", () => {
if (eventObj.isClampGroup) {
tControls.object.position.y = 0;
}
})
scene.add(tControls);

function tControlSelect(mesh) {
tControls.attach(mesh);
}

效果

结语

好了,本篇文章就分享到这里了,更多内容敬请期待~~~~

上一篇:
【可视化学习】56-3D动画实现讲解
下一篇:
【可视化学习】54-回顾灯光与阴影-详解