记录一个Three.js项目

木头的喵喵拖孩

记录一下公司的 Three.js 项目,感觉很经典

完整项目地址
预览地址

目录概览

以下加粗字体为核心文件

  • public
    • index.html
    • city.gltf
    • video_point.png
  • src
    • index.js
  • package.json
  • webpack.config.js
  • webpack.dev.config.js
  • webpack.pro.config.js

package.json

简单介绍一下项目依赖和脚本

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
{
"name": "threejsapp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server --config ./webpack.dev.config.js",
"build": "webpack --config ./webpack.pro.config.js"
},
"author": "",
"license": "ISC",
"devDependencies": {
"babel-loader": "^9.1.2",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
"html-webpack-plugin": "^5.5.1",
"webpack": "^5.85.1",
"webpack-cli": "^5.1.3",
"webpack-dev-server": "^4.15.0"
},
"dependencies": {
"three": "^0.153.0"
}
}

webpack.config.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
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
// target: 'node', // 如果需要打包后运行在nodejs环境下,需要设置此值
entry: './src/index.js',
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader'
}
]
},
plugins: [
new CleanWebpackPlugin(), // 先删除打包目标目录再打包,
new HtmlWebpackPlugin({
template: './public/index.html' // 配置模板index.html文件
}),
new CopyWebpackPlugin({
patterns: [
{ from: './public/city.gltf' }, // 把文件(CNAME等)移动到打包目录
{ from: './public/video_point.png' } // 把文件(CNAME等)移动到打包目录
]
})
]
};

webpack.dev.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack.dev.config.js
const path = require('path');
const baseConfig = require('./webpack.config');

module.exports = Object.assign(baseConfig, {
// 开发模式配置
mode: 'development',
devtool: 'source-map',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/'
},
devServer: {
static: {
directory: path.join(__dirname, 'dist') // 开发服务器根目录
},
compress: true,
port: 8888
}
});

webpack.pro.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
// webpack.pro.config.js
const path = require('path');
const baseConfig = require('./webpack.config');

module.exports = Object.assign(baseConfig, {
// 生产模式配置
mode: 'production',
output: {
path: path.resolve(__dirname, 'docs'),
filename: 'scripts/bundle.js',
publicPath: './'
}
});

/public/index.html

源码

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>记录一个Three.js项目</title>
<style>
body {
margin: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#container {
width: 60%;
height: 60%;
}
</style>
</head>
<body>
<div id="container"></div>
</body>
</html>

/src/index.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
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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
import * as Three from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

(function () {
let threeContainer;
let scene;
let camera;
let renderer;
let controls;
let raycaster;
let pointer;
let model; // 要展示的模型
let modelUnits = [];
let selectedModel;
let spriteGroup; // 监控点位,
let isMouseOnCanvas = false;

init();

/**
* 总初始化
*/
async function init() {
threeContainer = initContainer();
scene = initScene();
camera = initCamera();
raycaster = initRaycaster();
renderer = initRenderer(true, true);
controls = initControls();
model = await initModel('./city.gltf');
scene.add(model);
spriteGroup = initMonitor('./video_point.png');
initEvent();
}


/**
* 初始化画布容器
* @returns {HTMLElement}
*/
function initContainer() {
return document.getElementById('container');
}

/**
* 初始化threejs场景
* @returns {Three.Scene}
*/
function initScene() {
let scene = new Three.Scene();

// 设置光源
const directionalLight = new Three.DirectionalLight(0xffffff, 1);
directionalLight.position.set(3000, 5000, -3000);
// 设置阴影
directionalLight.castShadow = true;
// 设置光源阴影相机
const distance = 900;
directionalLight.shadow.camera.left = -distance;
directionalLight.shadow.camera.right = distance;
directionalLight.shadow.camera.top = distance;
directionalLight.shadow.camera.bottom = -distance;
directionalLight.shadow.camera.far = 20000;
directionalLight.shadow.camera.near = 1;

directionalLight.target.position.set(0, 0, 0);
scene.add(directionalLight.target);

// 光源辅助线
// const helper = new Three.DirectionalLightHelper(directionalLight);
// scene.add(helper);

scene.add(directionalLight);
const ambient = new Three.AmbientLight(0xffffff, 0.7);
scene.add(ambient);

return scene;
}

/**
* 初始化threejs相机
* @returns {Three.Camera}
*/
function initCamera() {
let camera = new Three.PerspectiveCamera(20, threeContainer.clientWidth / threeContainer.clientHeight, 1, 20000);
camera.position.set(0, 1700, 2000);
camera.lookAt(0, 0, 0);
return camera;
}

/**
* 初始化threejs的事件射线检测器,
* threejs的各种事件实现的基本前提
* @returns {Three.Raycaster}
*/
function initRaycaster() {
pointer = new Three.Vector2();
let raycaster = new Three.Raycaster();
return raycaster;
}

/**
* 初始化threejs渲染器,
* 这里需要递归调用requestAnimationFrame来递归渲染3D模型,
* 模型能不能动主要是在这个函数里面实现
*
* @param {Boolean} openEvent 是否开启事件功能
* @param {Boolean} openRotate 是否开启模型自旋转
* @returns {Three.Renderer}
*/
function initRenderer(openEvent = false, openRotate = false) {
let renderer = new Three.WebGLRenderer({ alpha: true });
renderer.setSize(threeContainer.clientWidth, threeContainer.clientHeight);
renderer.setClearColor(0xffffff, 0);
renderer.outputColorSpace = Three.SRGBColorSpace;
//阴影
renderer.shadowMap.enabled = true;
// renderer.shadowMap.type = Three.PCFSoftShadowMap;

threeContainer.appendChild(renderer.domElement);

const render = () => {
if (openEvent) {
// 事件射线位置更新
raycaster.setFromCamera(pointer, camera);
}
if (openRotate) {
// 模型旋转角度更新
if (
model
&& spriteGroup
&& !isMouseOnCanvas
) {
let unit = Math.PI / 3000;
model.rotation.y -= unit;
spriteGroup.rotation.y -= unit;
if (Math.abs(model.rotation.y) >= 2 * Math.PI) {// 如果旋转角度绝对值大于等于两个Math.PI(即旋转360度),将y归零,防止内存溢出
model.rotation.y = 0;
spriteGroup.rotation.y = 0;
}
}
}

renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();

window.onresize = () => {
renderer.setSize(threeContainer.clientWidth, threeContainer.clientHeight);
camera.aspect = threeContainer.clientWidth / threeContainer.clientHeight;
camera.updateProjectionMatrix();
}
return renderer;
}

/**
* 初始化threejs控制器,
* 该控制器主要限制相机的运动轨迹,
* 非threejs的核心类
*
* @returns {OrbitControls}
*/
function initControls() {
let controls = new OrbitControls(camera, renderer.domElement);
return controls;
}

/**
* 初始化threejs模型文件
* @param {String} modelPath 模型文件地址
* @returns {Promise<Three.Scene>}
*/
function initModel(modelPath) {

let loader;
if (/\.(glb|gltf)$/.test(modelPath)) {
loader = new GLTFLoader();
} else if (/\.dae$/.test(modelPath)) {
loader = new ColladaLoader();
}
return new Promise((resolve, reject) => {
loader.load(modelPath, obj => {
const material = new Three.MeshStandardMaterial({ color: 0xffffff });
const material2 = new Three.MeshStandardMaterial({ color: 0x999999 });

const ground = [
'地面',
'地面1',
]

// 遍历模型
traversalModelTree(obj.scene.children, modelUnit => {

//设置阴影
modelUnit.castShadow = true;
modelUnit.receiveShadow = true;

// 设置材质
if (ground.find(item => item === modelUnit.name)) {
modelUnit.material = material2;
} else {
modelUnit.material = material;
if (
modelUnit.name === '平面'
|| modelUnit.name === '车库'
) {
// 这里把模型提高3个单位,否者和地面重叠会出现闪烁问题
modelUnit.position.y = 3;
}
}
});

resolve(obj.scene);
}, undefined, err => {
reject(err);
})
})

}
/**
* 递归遍历模型树中每一个最小单位模型,并为其使用回调函数
* @param {Three.Group} models 模型树
* @param {Function} cb 回调函数
*/
function traversalModelTree(models, cb) {
for (let model of models) {
if (cb && cb instanceof Function) cb(model);
modelUnits.push(model);
if (model.children && model.children.length > 0) {
traversalModelTree(model.children, cb);
}
}
}

/**
* 初始化threejs事件
*/
function initEvent() {
const modelUnits = [
// ...model.children,
...spriteGroup.children
]
const handlerLeave = () => {
isMouseOnCanvas = false;
}
const handlerMove = ev => {
isMouseOnCanvas = true;
if (selectedModel) {
selectedModel.object.material.opacity = 1;
}

let style = ev.target.style;
style.cursor = 'default';

pointer.x = (ev.offsetX / threeContainer.scrollWidth) * 2 - 1;
pointer.y = 1 - (ev.offsetY / threeContainer.scrollHeight) * 2;

const intersects = raycaster.intersectObjects(modelUnits, true);
if (intersects[0]) {
style.cursor = 'pointer';
selectedModel = intersects[0];
selectedModel.object.material.opacity = .5;
}

}
const handlerDown = ev => {

pointer.x = (ev.offsetX / threeContainer.scrollWidth) * 2 - 1;
pointer.y = 1 - (ev.offsetY / threeContainer.scrollHeight) * 2;

const intersects = raycaster.intersectObjects(modelUnits, true);
if (intersects[0]) {
selectedModel = intersects[0];
alert('监控点被点击了!')
}
}

threeContainer.onpointerleave = handlerLeave;
threeContainer.onpointermove = handlerMove;
threeContainer.onpointerdown = handlerDown;
}

/**
* 初始化threejs监控点空间坐标
* @returns {Three.Group}
*/
function initMonitor(monitorPath) {
// 当前模型医院四个角的坐标,这里的位置是相对于你正对医院大门时候的位置,参考
// { x: 415, y: 30, z: 675 }, // 右下
// { x: 415, y: 30, z: -670 }, // 右上
// { x: -355, y: 30, z: 675 }, // 左下
// { x: -355, y: 30, z: -670 }, // 左上

// 当前医院大门坐标,参考
// { x: -15, y: 30, z: 675 },
// { x: -205, y: 30, z: 675 },

let monitorInterval = 80.8 // 摄像机间隔
, adjustLongSide = -2 // 长边间隔调整
, adjustShortSide = 4.6; // 短边间隔调整
let monitorHeight = 30; // 摄像机高度
let monitorScale = 30; // 摄像机图片缩放倍数

let monitorPositionRight = []; // 右围栏监控点坐标
for (let i = 0; i < 17; i++) {
monitorPositionRight.push({ x: 415, y: monitorHeight, z: 675 - i * (monitorInterval + adjustLongSide) });
}
let monitorPositionLeft = []; // 左围栏监控点坐标
for (let i = 0; i < 17; i++) {
monitorPositionLeft.push({ x: -355, y: monitorHeight, z: -670 + i * (monitorInterval + adjustLongSide) });
}
let monitorPositionTop = []; // 上围栏监控点坐标
for (let i = 0; i < 9; i++) {
monitorPositionTop.push({ x: 415 - i * (monitorInterval + adjustShortSide), y: monitorHeight, z: -670 });
}
let monitorPositionBottom = []; // 下围栏监控点坐标
for (let i = 0; i < 9; i++) {
let x = -355 + i * (monitorInterval + adjustShortSide);
// 摄像机点位不能在大门上方
if (x < -205 || x > -15) monitorPositionBottom.push({ x, y: monitorHeight, z: 675 });
}
let monitorPosition = [
...monitorPositionLeft,
...monitorPositionRight,
...monitorPositionTop,
...monitorPositionBottom
];
return addMonitorPoint(monitorPath, monitorPosition, monitorScale);
}

/**
*
* @param {{x:Number,y:Number,z:Number}[]} pointPositions 监控点空间坐标数组
* @param {*} scale 摄像机图片缩放倍数
* @returns {Three.Group}
*/
function addMonitorPoint(monitorPath, pointPositions, scale = 1) {
let spriteGroup = new Three.Group();
for (let pointPosition of pointPositions) {
let map = new Three.TextureLoader().load(monitorPath);
let material = new Three.SpriteMaterial({ map });
let sprite = new Three.Sprite(material);
sprite.position.set(pointPosition.x, pointPosition.y, pointPosition.z,);
sprite.scale.set(scale, scale, scale);
spriteGroup.add(sprite);
scene.add(spriteGroup)
}
return spriteGroup;
}
})()
  • 标题: 记录一个Three.js项目
  • 作者: 木头的喵喵拖孩
  • 创建于: 2023-06-06 09:30:13
  • 更新于: 2024-06-05 14:07:08
  • 链接: https://blog.xx-xx.top/2023/06/06/记录一个Three-js项目/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。