【可视化学习】35-根据已有模型渲染智慧园区
发表于:2023-09-02 |

前言

本文不讲述建模部分,这个我自己也不是很熟练,等以后有机会了再统一出个建模的文章。

初始化项目

这块已经讲述过很多了,这里就不多赘述了

设置全局样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
*{
margin: 0;
padding: 0;
}
ul,li{
list-style: none;
}

html{
font-size:calc(100vw / 19.2);
}

body{
font-size: 16px;
}

基础目录结构和实现逻辑

这个和我之前那篇智慧城市的文章一样,就使用那个拆分就可以了

导入组件

首先在App.vue中

1
2
3
4
5
6
7
8
9
<template>
<div class="home">
<Scene></Scene>
</div>
</template>

<script setup>
import Scene from "@/components/Scene.vue";
</script>

添加事件总览

新建utils文件夹,新建eventHub.js

1
2
3
4
5
import Mitt from "mitt";

const eventHub = new Mitt();

export default eventHub;

组件导入目录

Scene组件

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
<template>
<div class="scene" ref="sceneDiv"></div>
</template>

<script setup>
import { onMounted, ref} from "vue";
// 导入场景
import scene from "@/three/scene";

import camera from "@/three/camera";
// 导入辅助坐标轴
import axesHelper from "@/three/axesHelper";
// 导入渲染器
import renderer from "@/three/renderer";
// 初始化调整屏幕
import "@/three/init";
// 导入添加物体函数
import createMesh from "@/three/createMesh";

// 导入每一帧的执行函数
import animate from "@/three/animate";

// 场景元素div
let sceneDiv = ref(null);
// 添加相机
scene.add(camera);
// 添加辅助坐标轴
scene.add(axesHelper);
// 创建物体
createMesh();

onMounted(() => {
sceneDiv.value.appendChild(renderer.domElement);
animate();
});
</script>

<style>
.scene {
width: 100vw;
height: 100vh;
position: fixed;
z-index: 100;
left: 0;
top: 0;
}
</style>

新建/src/three文件夹

在这个文件夹下面初始化我们的基础文件,初始化的代码就不解释了,之前的文章都有讲过

camera.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import * as THREE from "three";
// 创建透视相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerHeight / window.innerHeight,
1,
100000
);
// 设置相机位置
// object3d具有position,属性是1个3维的向量
camera.position.set(1000, 1000, 1000);

export default camera;

renderer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import * as THREE from "three";
// 初始化渲染器
const renderer = new THREE.WebGLRenderer({
// 设置抗锯齿
antialias: true,
// depthbuffer
logarithmicDepthBuffer: true,
physicallyCorrectLights: true,
});
// 设置渲染尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.5;

export default renderer;

init.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import camera from "./camera";
import renderer from "./renderer";

// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();

// 监听屏幕大小改变的变化,设置渲染的尺寸
window.addEventListener("resize", () => {
// console.log("resize");
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();

// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比例
renderer.setPixelRatio(window.devicePixelRatio);
});

axesHelper.js

1
2
3
4
5
import * as THREE from "three";
// 加入辅助轴,帮助我们查看3维坐标轴
const axesHelper = new THREE.AxesHelper(5);

export default axesHelper;

controls.js

1
2
3
4
5
6
7
8
9
10
11
12
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import camera from "./camera";
import renderer from "./renderer";

// 初始化控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼
controls.enableDamping = true;
// 设置自动旋转
// controls.autoRotate = true;

export default controls;

animate.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import camera from "./camera";
import renderer from "./renderer";
import controls from "./controls";
import scene from "./scene";

function animate() {
controls.update();
requestAnimationFrame(animate);
// 使用渲染器渲染相机看这个场景的内容渲染出来
renderer.render(scene, camera);
}

export default animate;

scene.js

这里我们导入一个纹理图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import * as THREE from "three";
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
// 初始化场景
const scene = new THREE.Scene();

// 导入hdr纹理
const hdrLoader = new RGBELoader();
hdrLoader.loadAsync("./textures/023.hdr").then((texture) => {
scene.background = texture;
scene.environment = texture;
scene.environment.mapping = THREE.EquirectangularReflectionMapping;
});

// 添加平行光
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(10, 100, 10);
scene.add(light);

export default scene;

createMesh.js

1
2
3
4
5
6
7
8
9
10
11
import scene from "./scene";
import City from "./mesh/City";
let city;
export default function createMesh() {
// 创建城市
city = new City(scene);
}

export function updateMesh(time) {
city.update(time);
}

纹理贴图效果

导入模型

City.js
注意:DRACOLoader是用来解析glb文件的,然后这个文件是需要从node_modules\three\examples\js\libs文件夹下面拷贝出来的,然后放到public文件夹下面,这样才能正常解析
DRACOLoader放置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";

export default class City {
constructor(scene) {
// 载入模型
this.scene = scene;
this.loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("./draco/");
this.loader.setDRACOLoader(dracoLoader);
this.loader.load("./model/city4.glb", (gltf) => {
console.log(gltf);
scene.add(gltf.scene);
});
}
}

模型导入效果图

添加热气球动画

模型里面已经设置了动画,在这里只需要调用即可,不用自己绘制动画

animate.js

调用updateMesh,传入time

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { updateMesh } from "@/three/createMesh";
import * as THREE from "three";
import camera from "./camera";
import renderer from "./renderer";
import controls from "./controls";
import scene from "./scene";

const clock = new THREE.Clock();
function animate() {
const time = clock.getDelta();
controls.update(time);
updateMesh(time);
requestAnimationFrame(animate);
// 使用渲染器渲染相机看这个场景的内容渲染出来
renderer.render(scene, camera);
}

export default animate;

City.js

导入glb中的动画

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
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import * as THREE from "three";
export default class City {
constructor(scene) {
// 载入模型
this.scene = scene;
this.loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("./draco/");
this.loader.setDRACOLoader(dracoLoader);
this.loader.load("./model/city4.glb", (gltf) => {
console.log(gltf);
scene.add(gltf.scene);
// 场景子元素遍历
this.gltf = gltf;
gltf.scene.traverse((child) => {
if (child.name === "热气球") {
// console.log(child);
this.mixer = new THREE.AnimationMixer(child);
this.clip = gltf.animations[1];
this.action = this.mixer.clipAction(this.clip);
this.action.play();
}
});
});
}

update(time) {
if (this.mixer) {
this.mixer.update(time);
}
}
}

动画效果

仔细看,可以看到热气球是在飘动的

添加汽车动画

City.js

在模型里面,已经设置了汽车的轨迹,我们只需要根据轨迹来设置汽车的位置即可

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
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import * as THREE from "three";
import gsap from "gsap";
export default class City {
constructor(scene) {
// 载入模型
this.scene = scene;
this.loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("./draco/");
this.loader.setDRACOLoader(dracoLoader);
this.loader.load("./model/city4.glb", (gltf) => {
console.log(gltf);
scene.add(gltf.scene);
// 场景子元素遍历
this.gltf = gltf;
gltf.scene.traverse((child) => {
if (child.name === "热气球") {
// console.log(child);
this.mixer = new THREE.AnimationMixer(child);
this.clip = gltf.animations[1];
this.action = this.mixer.clipAction(this.clip);
this.action.play();
}
if (child.name === "汽车园区轨迹") {
// console.log(child);
const line = child;
line.visible = false;
// 根据点创建曲线
const points = [];
for (
let i = line.geometry.attributes.position.count - 1;
i >= 0;
i--
) {
points.push(
new THREE.Vector3(
line.geometry.attributes.position.getX(i),
line.geometry.attributes.position.getY(i),
line.geometry.attributes.position.getZ(i)
)
);
}

this.curve = new THREE.CatmullRomCurve3(points);
this.curveProgress = 0;
this.carAnimation();
}
if (child.name === "redcar") {
console.log(child);
this.redcar = child;
}
});
});
}

update(time) {
if (this.mixer) {
this.mixer.update(time);
}
}
carAnimation() {
gsap.to(this, {
curveProgress: 0.999,
duration: 10,
repeat: -1,
onUpdate: () => {
const point = this.curve.getPoint(this.curveProgress);
this.redcar.position.set(point.x, point.y, point.z);
if (this.curveProgress + 0.001 < 1) {
const point = this.curve.getPoint(this.curveProgress + 0.001);
this.redcar.lookAt(point);
}
},
});
}
}

动画效果

仔细看,可以看到汽车是在运动的

设置不同视角观看以及热气球动画

模型视角

在建模的时候已经给了我们多个视角,我们只需要对应切换即可
模型视角
依旧使用之前智慧城市的控制屏幕代码
并添加事件

BigScreen.vue

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
<template>
<div id="bigScreen">
<div class="header">智慧城市管理系统平台</div>
<div class="main">
<div class="left">
<div class="cityEvent">
<h3>
<span>热气球控制</span>
</h3>
<h1 @click="toggleAction(0)">
<img class="icon" src="../assets/bg/bar.svg" alt="" />
<span>设置热气球以横穿园区的动画显示</span>
</h1>
<h1 @click="toggleAction(1)">
<img class="icon" src="../assets/bg/bar.svg" alt="" />
<span>设置热气球以环绕园区进行运动</span>
</h1>

<div class="footerBorder"></div>
</div>

<div class="cityEvent">
<h3>
<span>相机控制</span>
</h3>
<h1 @click="toggleCamera('default')">
<img class="icon" src="../assets/bg/bar.svg" alt="" />
<span>默认的相机视角</span>
</h1>
<h1 @click="toggleCamera('carcamera_Orientation')">
<img class="icon" src="../assets/bg/bar.svg" alt="" />
<span>设置相机追随汽车导览园区</span>
</h1>
<h1 @click="toggleCamera('rightcamera_Orientation')">
<img class="icon" src="../assets/bg/bar.svg" alt="" />
<span>查看汽车司机视角</span>
</h1>

<div class="footerBorder"></div>
</div>
</div>
</div>
</div>
</template>

<script setup>
import eventHub from "@/utils/eventHub";

const toggleAction = (i) => {
eventHub.emit("actionClick", i);
};

const toggleCamera = (name) => {
eventHub.emit("toggleCamera", name);
};

</script>

<style>
#bigScreen {
width: 100vw;
height: 100vh;
position: fixed;
z-index: 100;

left: 0;
top: 0;
pointer-events: none;
display: flex;
flex-direction: column;
}

.header {
/* width: 1920px;
height: 100px; */

width: 19.2rem;
height: 1rem;
background-image: url(@/assets/bg/title.png);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
text-align: center;
color: rgb(226, 226, 255);
font-size: 0.4rem;
}

.main {
flex: 1;
width: 19.2rem;
display: flex;
justify-content: space-between;
}

.left {
width: 4rem;
/* background-color: rgb(255,255,255,0.5); */
background-image: url(@/assets/bg/line_img.png);
background-repeat: no-repeat;
background-size: contain;
background-position: right center;
display: flex;
flex-direction: column;
align-items: center;
padding: 0.4rem 0;
}

.right {
width: 4rem;
/* background-color: rgb(255,255,255,0.5); */
background-image: url(@/assets/bg/line_img.png);
background-repeat: no-repeat;
background-size: contain;
background-position: left center;
display: flex;
flex-direction: column;
align-items: center;
padding: 0.4rem 0;
}

.cityEvent {
position: relative;
width: 3.5rem;
/* height: 3rem; */
margin-bottom: 0.5rem;
background-image: url(@/assets/bg/bg_img03.png);
background-repeat: repeat;
pointer-events: auto;
}

.cityEvent::before {
width: 0.4rem;
height: 0.4rem;
position: absolute;
left: 0;
top: 0;
border-top: 4px solid rgb(34, 133, 247);
border-left: 4px solid rgb(34, 133, 247);
content: "";
display: block;
}

.cityEvent::after {
width: 0.4rem;
height: 0.4rem;
position: absolute;
right: 0;
top: 0;
border-top: 4px solid rgb(34, 133, 247);
border-right: 4px solid rgb(34, 133, 247);
content: "";
display: block;
}
.footerBorder {
position: absolute;
bottom: 0;
bottom: 0;
width: 3.5rem;
height: 0.4rem;
}
.footerBorder::before {
width: 0.4rem;
height: 0.4rem;
position: absolute;
left: 0;
top: 0;
border-bottom: 4px solid rgb(34, 133, 247);
border-left: 4px solid rgb(34, 133, 247);
content: "";
display: block;
}

.footerBorder::after {
width: 0.4rem;
height: 0.4rem;
position: absolute;
right: 0;
top: 0;
border-bottom: 4px solid rgb(34, 133, 247);
border-right: 4px solid rgb(34, 133, 247);
content: "";
display: block;
}

.icon {
width: 40px;
height: 40px;
}

.cityEvent h1 span {
flex: 1;
}

h1 {
color: #fff;
display: flex;
align-items: center;
padding: 0 0.3rem 0.3rem;
justify-content: space-between;
font-size: 0.25rem;
}
h3 {
color: #fff;
display: flex;
align-items: center;
padding: 0.3rem 0.3rem;
}

h1 > div {
display: flex;
align-items: center;
}
h1 span.time {
font-size: 0.2rem;
font-weight: normal;
}

.cityEvent li > p {
color: #eee;
padding: 0rem 0.3rem 0.3rem;
}
.list h1 {
padding: 0.1rem 0.3rem;
}
.cityEvent.list ul {
pointer-events: auto;
cursor: pointer;
}

.cityEvent li.active h1 {
color: red;
}
.cityEvent li.active p {
color: red;
}
</style>

App.vue

在App.vue中导入并使用

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="home">
<Scene></Scene>
<BigScreen></BigScreen>
</div>
</template>

<script setup>
import Scene from "@/components/Scene.vue";
import BigScreen from "@/components/BigScreen.vue";
</script>

Camera.js

根据不同的事件切换不同的相机视角

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
import * as THREE from "three";
import eventHub from "@/utils/eventHub";
// 创建透视相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerHeight / window.innerHeight,
1,
100000
);
// 设置相机位置
// object3d具有position,属性是1个3维的向量
camera.position.set(1000, 1000, 1000);

class CameraModule {
constructor() {
this.activeCamera = camera;
this.collection = {
default: camera,
};

eventHub.on("toggleCamera", (name) => {
this.setActive(name);
});
}
add(name, camera) {
this.collection[name] = camera;
}
setActive(name) {
this.activeCamera = this.collection[name];
}
}

export default new CameraModule();

City.js

在City.js中添加相机视角

1
2
3
4
import cameraModule from "../camera";
gltf.cameras.forEach((camera) => {
cameraModule.add(camera.name, camera);
});

在City.js中切换热气球动画
1
2
3
4
5
6
7
eventHub.on("actionClick", (i) => {
console.log(i);
this.action.reset();
this.clip = this.gltf.animations[i];
this.action = this.mixer.clipAction(this.clip);
this.action.play();
});

其他文件

在其他使用到camera的地方全部切换

  • animate.js
    1
    2
    import cameraModule from "./camera";
    renderer.render(scene, cameraModule.activeCamera);
  • controls.js

    1
    2
    3
    4
    5
    import cameraModule from "./camera";
    const controls = new OrbitControls(
    cameraModule.activeCamera,
    renderer.domElement
    );
  • init.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import cameraModule from "./camera";
    import renderer from "./renderer";

    // 更新摄像头
    cameraModule.activeCamera.aspect = window.innerWidth / window.innerHeight;
    // 更新摄像机的投影矩阵
    cameraModule.activeCamera.updateProjectionMatrix();

    // 监听屏幕大小改变的变化,设置渲染的尺寸
    window.addEventListener("resize", () => {
    // console.log("resize");
    // 更新摄像头
    cameraModule.activeCamera.aspect = window.innerWidth / window.innerHeight;
    // 更新摄像机的投影矩阵
    cameraModule.activeCamera.updateProjectionMatrix();

    // 更新渲染器
    renderer.setSize(window.innerWidth, window.innerHeight);
    // 设置渲染器的像素比例
    renderer.setPixelRatio(window.devicePixelRatio);
    });

  • Scene.vue

    1
    2
    3
    import cameraModule from "@/three/camera";
    // 添加相机
    scene.add(cameraModule.activeCamera);

切换视角效果

切换观览模式

BigScreen.vue

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
<div class="right">
<div class="cityEvent list">
<h3>
<span>切换园区观览模式</span>
</h3>
<ul>
<li @click="toggleControls('Orbit')">
<h1>
<div>
<img class="icon" src="../assets/bg/dianli.svg" alt="" />
<span> 轨道观览 </span>
</div>
</h1>
<p>可以锁定目标建筑和园区进行轨道式360°查看</p>
</li>
<li @click="toggleControls('Fly')">
<h1>
<div>
<img class="icon" src="../assets/bg/dianli.svg" alt="" />
<span> 飞行观览 </span>
</div>
</h1>
<p>可以使用飞行模式进行园区进行观览</p>
</li>
<li @click="toggleControls('FirstPerson')">
<h1>
<div>
<img class="icon" src="../assets/bg/dianli.svg" alt="" />
<span> 第一人称 </span>
</div>
</h1>
<p>可以使用第一人称模式进行园区进行观览</p>
</li>
</ul>

<div class="footerBorder"></div>
</div>
</div>
1
2
3
4
const toggleControls = (name) => {
// console.log(i);
eventHub.emit("toggleControls", name);
};

controls.js

切换控制器,具体的控制器代码可以去threejs官网查看,这里使用了FlyControls和FirstPersonControls

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
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { FlyControls } from "three/examples/jsm/controls/FlyControls";
import { FirstPersonControls } from "three/examples/jsm/controls/FirstPersonControls";
import cameraModule from "./camera";
import renderer from "./renderer";
import eventHub from "@/utils/eventHub";

class ControlsModule {
constructor() {
this.setOrbitControls();
eventHub.on("toggleControls", (name) => {
this[`set${name}Controls`]();
});
}
setOrbitControls() {
// 初始化控制器
this.controls = new OrbitControls(
cameraModule.activeCamera,
renderer.domElement
);
// 设置控制器阻尼
this.controls.enableDamping = true;
// 设置自动旋转
// controls.autoRotate = true;

this.controls.maxPolarAngle = Math.PI / 2;
this.controls.minPolarAngle = 0;
}
setFlyControls() {
this.controls = new FlyControls(
cameraModule.activeCamera,
renderer.domElement
);
this.controls.movementSpeed = 100;
this.controls.rollSpeed = Math.PI / 60;
}
setFirstPersonControls() {
this.controls = new FirstPersonControls(
cameraModule.activeCamera,
renderer.domElement
);
this.controls.movementSpeed = 100;
this.controls.rollSpeed = Math.PI / 60;
}
}

export default new ControlsModule();

animate.js

切换控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import * as THREE from "three";
import cameraModule from "./camera";
import renderer from "./renderer";
import controlsModule from "./controls";
import scene from "./scene";
import { updateMesh } from "@/three/createMesh";

const clock = new THREE.Clock();
function animate(t) {
const time = clock.getDelta();
controlsModule.controls.update(time);
updateMesh(time);
requestAnimationFrame(animate);
// 使用渲染器渲染相机看这个场景的内容渲染出来
renderer.render(scene, cameraModule.activeCamera);
}

export default animate;

切换控制器效果

结语

本篇文章就简单阐述到这里了,每天学习一点点,做大做强!

上一篇:
【可视化学习】36-cesium基础学习(一)
下一篇:
submenu在结合路由使用时刷新页面,导致submenu的展开状态丢失