three-player-controller 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,856 @@
1
+ // src/playerController.ts
2
+ import * as THREE from "three";
3
+ import { MeshBVH, MeshBVHHelper } from "three-mesh-bvh";
4
+ import { RoundedBoxGeometry } from "three/examples/jsm/geometries/RoundedBoxGeometry.js";
5
+ import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
6
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
7
+ import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js";
8
+ var controllerInstance = null;
9
+ var PlayerController = class {
10
+ // 射线检测时只返回第一个碰撞
11
+ constructor() {
12
+ this.loader = new GLTFLoader();
13
+ this.isFirstPerson = false;
14
+ this.mouseSensity = 5;
15
+ this.boundingBoxMinY = 0;
16
+ // 测试参数
17
+ this.displayPlayer = false;
18
+ this.displayCollider = false;
19
+ this.displayVisualizer = false;
20
+ // 场景对象
21
+ this.collider = null;
22
+ this.visualizer = null;
23
+ this.person = null;
24
+ // 状态开关
25
+ this.playerIsOnGround = false;
26
+ this.isUpdatePlayer = true;
27
+ // 输入状态
28
+ this.fwdPressed = false;
29
+ // w
30
+ this.bkdPressed = false;
31
+ // s
32
+ this.lftPressed = false;
33
+ // a
34
+ this.rgtPressed = false;
35
+ // d
36
+ this.spacePressed = false;
37
+ // 空格
38
+ this.ctPressed = false;
39
+ // ctrl
40
+ this.shiftPressed = false;
41
+ // shift
42
+ this.sustainSpacePressed = false;
43
+ // 空格键是否持续按下
44
+ this.spaceLongPressTimer = null;
45
+ // 第三人称
46
+ this._camCollisionLerp = 0.18;
47
+ // 平滑系数
48
+ this._camEpsilon = 0.35;
49
+ // 摄像机与障碍物之间的安全距离(米)
50
+ this._minCamDistance = 1;
51
+ // 摄像机最小距离
52
+ this._maxCamDistance = 4.4;
53
+ // 摄像机最大距离
54
+ // 物理/运动
55
+ this.playerVelocity = new THREE.Vector3();
56
+ // 玩家速度向量
57
+ this.upVector = new THREE.Vector3(0, 1, 0);
58
+ // 临时复用向量/矩阵
59
+ this.tempVector = new THREE.Vector3();
60
+ this.tempVector2 = new THREE.Vector3();
61
+ this.tempBox = new THREE.Box3();
62
+ this.tempMat = new THREE.Matrix4();
63
+ this.tempSegment = new THREE.Line3();
64
+ // 复用向量:用于相机朝向 / 移动
65
+ this.camDir = new THREE.Vector3();
66
+ this.moveDir = new THREE.Vector3();
67
+ this.targetQuat = new THREE.Quaternion();
68
+ this.targetMat = new THREE.Matrix4();
69
+ this.rotationSpeed = 10;
70
+ this.DIR_FWD = new THREE.Vector3(0, 0, -1);
71
+ this.DIR_BKD = new THREE.Vector3(0, 0, 1);
72
+ this.DIR_LFT = new THREE.Vector3(-1, 0, 0);
73
+ this.DIR_RGT = new THREE.Vector3(1, 0, 0);
74
+ this._personToCam = new THREE.Vector3();
75
+ this._originTmp = new THREE.Vector3();
76
+ this._raycaster = new THREE.Raycaster(
77
+ new THREE.Vector3(),
78
+ new THREE.Vector3(0, -1, 0)
79
+ );
80
+ this._raycasterPersonToCam = new THREE.Raycaster(
81
+ new THREE.Vector3(),
82
+ new THREE.Vector3()
83
+ );
84
+ // 键盘按下事件
85
+ this._boundOnKeydown = async (e) => {
86
+ if (e.ctrlKey && (e.code === "KeyW" || e.code === "KeyA" || e.code === "KeyS" || e.code === "KeyD")) {
87
+ e.preventDefault();
88
+ }
89
+ switch (e.code) {
90
+ case "KeyW":
91
+ this.fwdPressed = true;
92
+ this.setAnimationByPressed();
93
+ break;
94
+ case "KeyS":
95
+ this.bkdPressed = true;
96
+ this.setAnimationByPressed();
97
+ break;
98
+ case "KeyD":
99
+ this.rgtPressed = true;
100
+ this.setAnimationByPressed();
101
+ break;
102
+ case "KeyA":
103
+ this.lftPressed = true;
104
+ this.setAnimationByPressed();
105
+ break;
106
+ case "ShiftLeft":
107
+ this.shiftPressed = true;
108
+ this.setAnimationByPressed();
109
+ break;
110
+ case "Space":
111
+ if (!this.spacePressed) this.spacePressed = true;
112
+ if (!this.spaceLongPressTimer) {
113
+ this.spaceLongPressTimer = setTimeout(() => {
114
+ this.sustainSpacePressed = true;
115
+ }, 2e3);
116
+ }
117
+ break;
118
+ case "ControlLeft":
119
+ this.ctPressed = true;
120
+ break;
121
+ case "KeyV":
122
+ this.changeView();
123
+ break;
124
+ }
125
+ };
126
+ // 键盘抬起事件
127
+ this._boundOnKeyup = (e) => {
128
+ switch (e.code) {
129
+ case "KeyW":
130
+ this.fwdPressed = false;
131
+ this.setAnimationByPressed();
132
+ break;
133
+ case "KeyS":
134
+ this.bkdPressed = false;
135
+ this.setAnimationByPressed();
136
+ break;
137
+ case "KeyD":
138
+ this.rgtPressed = false;
139
+ this.setAnimationByPressed();
140
+ break;
141
+ case "KeyA":
142
+ this.lftPressed = false;
143
+ this.setAnimationByPressed();
144
+ break;
145
+ case "ShiftLeft":
146
+ this.shiftPressed = false;
147
+ this.setAnimationByPressed();
148
+ break;
149
+ case "Space":
150
+ if (this.spaceLongPressTimer) {
151
+ clearTimeout(this.spaceLongPressTimer);
152
+ this.spaceLongPressTimer = null;
153
+ }
154
+ this.spacePressed = false;
155
+ this.sustainSpacePressed = false;
156
+ break;
157
+ case "ControlLeft":
158
+ this.ctPressed = false;
159
+ break;
160
+ }
161
+ };
162
+ // 根据按键设置人物动画
163
+ this.setAnimationByPressed = () => {
164
+ if (this.playerIsOnGround) {
165
+ if (!this.fwdPressed && !this.bkdPressed && !this.lftPressed && !this.rgtPressed) {
166
+ this.playPersonAnimationByName("idle");
167
+ return;
168
+ }
169
+ if (this.fwdPressed) {
170
+ if (this.shiftPressed) {
171
+ this.playPersonAnimationByName("running");
172
+ } else {
173
+ this.playPersonAnimationByName("walking");
174
+ }
175
+ return;
176
+ }
177
+ if (!this.isFirstPerson && (this.lftPressed || this.rgtPressed || this.bkdPressed)) {
178
+ if (this.shiftPressed) {
179
+ this.playPersonAnimationByName("running");
180
+ } else {
181
+ this.playPersonAnimationByName("walking");
182
+ }
183
+ return;
184
+ }
185
+ if (this.lftPressed) {
186
+ this.playPersonAnimationByName("left_walking");
187
+ return;
188
+ }
189
+ if (this.rgtPressed) {
190
+ this.playPersonAnimationByName("right_walking");
191
+ return;
192
+ }
193
+ if (this.bkdPressed) {
194
+ this.playPersonAnimationByName("walking_backward");
195
+ return;
196
+ }
197
+ }
198
+ };
199
+ // 鼠标移动事件
200
+ this._mouseMove = (e) => {
201
+ if (document.pointerLockElement !== document.body) return;
202
+ if (this.isFirstPerson) {
203
+ const yaw = -e.movementX * 1e-4 * this.mouseSensity;
204
+ const pitch = -e.movementY * 1e-4 * this.mouseSensity;
205
+ this.player.rotateY(yaw);
206
+ this.camera.rotation.x = THREE.MathUtils.clamp(
207
+ this.camera.rotation.x + pitch,
208
+ -1.3,
209
+ 1.4
210
+ );
211
+ } else {
212
+ const sensitivity = 1e-4 * this.mouseSensity;
213
+ const deltaX = -e.movementX * sensitivity;
214
+ const deltaY = -e.movementY * sensitivity;
215
+ const target = this.player.position.clone();
216
+ const distance = this.camera.position.distanceTo(target);
217
+ const currentPosition = this.camera.position.clone().sub(target);
218
+ let theta = Math.atan2(currentPosition.x, currentPosition.z);
219
+ let phi = Math.acos(currentPosition.y / distance);
220
+ theta += deltaX;
221
+ phi += deltaY;
222
+ phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
223
+ const newX = distance * Math.sin(phi) * Math.sin(theta);
224
+ const newY = distance * Math.cos(phi);
225
+ const newZ = distance * Math.sin(phi) * Math.cos(theta);
226
+ this.camera.position.set(
227
+ target.x + newX,
228
+ target.y + newY,
229
+ target.z + newZ
230
+ );
231
+ this.camera.lookAt(target);
232
+ }
233
+ };
234
+ this._mouseClick = (e) => {
235
+ if (document.pointerLockElement !== document.body)
236
+ document.body.requestPointerLock();
237
+ };
238
+ this._raycaster.firstHitOnly = true;
239
+ this._raycasterPersonToCam.firstHitOnly = true;
240
+ }
241
+ // 初始化
242
+ async init(opts, callback) {
243
+ this.scene = opts.scene;
244
+ this.camera = opts.camera;
245
+ this.controls = opts.controls;
246
+ this.playerModel = opts.playerModel;
247
+ this.playerModel.scale = opts.playerModel.scale ? opts.playerModel.scale : 1;
248
+ this.initPos = opts.initPos ? opts.initPos : new THREE.Vector3(0, 0, 0);
249
+ this.visualizeDepth = 0 * this.playerModel.scale;
250
+ this.gravity = -2400 * this.playerModel.scale;
251
+ this.jumpHeight = 300 * this.playerModel.scale;
252
+ this.highJumpHeight = 1e3 * this.playerModel.scale;
253
+ this.playerSpeed = 400 * this.playerModel.scale;
254
+ await this.createBVH();
255
+ this.createPlayer();
256
+ await this.loadPersonGLB();
257
+ if (this.isFirstPerson && this.player) {
258
+ this.player.add(this.camera);
259
+ }
260
+ this.onAllEvent();
261
+ this.setCameraPos();
262
+ this.setControls();
263
+ if (callback) callback();
264
+ }
265
+ // 第一/三视角切换
266
+ changeView() {
267
+ this.isFirstPerson = !this.isFirstPerson;
268
+ if (this.isFirstPerson) {
269
+ this.player.attach(this.camera);
270
+ this.camera.position.set(
271
+ 0,
272
+ 40 * this.playerModel.scale,
273
+ 30 * this.playerModel.scale
274
+ );
275
+ this.camera.rotation.set(0, Math.PI, 0);
276
+ document.body.requestPointerLock();
277
+ } else {
278
+ this.scene.attach(this.camera);
279
+ const worldPos = this.player.position.clone();
280
+ const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(
281
+ this.player.quaternion
282
+ );
283
+ const angle = Math.atan2(dir.z, dir.x);
284
+ const offset = new THREE.Vector3(
285
+ Math.cos(angle) * 400 * this.playerModel.scale,
286
+ 200 * this.playerModel.scale,
287
+ Math.sin(angle) * 400 * this.playerModel.scale
288
+ );
289
+ this.camera.position.copy(worldPos).add(offset);
290
+ this.controls.target.copy(worldPos);
291
+ document.body.requestPointerLock();
292
+ }
293
+ }
294
+ // 摄像机/控制器设置
295
+ setCameraPos() {
296
+ if (this.isFirstPerson) {
297
+ this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
298
+ } else {
299
+ const worldPos = this.player.position.clone();
300
+ const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(
301
+ this.player.quaternion
302
+ );
303
+ const angle = Math.atan2(dir.z, dir.x);
304
+ const offset = new THREE.Vector3(
305
+ Math.cos(angle) * 400 * this.playerModel.scale,
306
+ 200 * this.playerModel.scale,
307
+ Math.sin(angle) * 400 * this.playerModel.scale
308
+ );
309
+ this.camera.position.copy(worldPos).add(offset);
310
+ }
311
+ this.camera.updateProjectionMatrix();
312
+ }
313
+ // 设置控制器
314
+ setControls() {
315
+ this.controls.enabled = false;
316
+ this.controls.maxPolarAngle = Math.PI * (230 / 360);
317
+ }
318
+ // 重置控制器
319
+ resetControls() {
320
+ this.controls.enabled = true;
321
+ this.controls.enablePan = true;
322
+ this.controls.maxPolarAngle = Math.PI / 2;
323
+ this.controls.rotateSpeed = 1;
324
+ this.controls.enableZoom = true;
325
+ this.controls.mouseButtons = {
326
+ LEFT: 0,
327
+ MIDDLE: 1,
328
+ RIGHT: 2
329
+ };
330
+ }
331
+ // 初始化加载器
332
+ async initLoader() {
333
+ const dracoLoader = new DRACOLoader();
334
+ dracoLoader.setDecoderPath(
335
+ "https://unpkg.com/three@0.180.0/examples/jsm/libs/draco/gltf/"
336
+ );
337
+ dracoLoader.setDecoderConfig({ type: "js" });
338
+ this.loader.setDRACOLoader(dracoLoader);
339
+ }
340
+ // 人物与动画加载
341
+ async loadPersonGLB() {
342
+ try {
343
+ const gltf = await this.loader.loadAsync(
344
+ this.playerModel.url,
345
+ (xhr) => {
346
+ }
347
+ );
348
+ this.person = gltf.scene;
349
+ this.person.name = "\u89D2\u8272";
350
+ this.person.scale.set(
351
+ 0.9 * this.playerModel.scale,
352
+ 0.9 * this.playerModel.scale,
353
+ 0.9 * this.playerModel.scale
354
+ );
355
+ this.person.position.set(0, -125 * this.playerModel.scale, 0);
356
+ this.player.add(this.person);
357
+ this.reset();
358
+ this.personMixer = new THREE.AnimationMixer(this.person);
359
+ const animations = gltf.animations ?? [];
360
+ this.personActions = /* @__PURE__ */ new Map();
361
+ const findClip = (name) => animations.find((a) => a.name === name);
362
+ const regs = [
363
+ [this.playerModel.idleAnim, "idle"],
364
+ [this.playerModel.walkAnim, "walking"],
365
+ [this.playerModel.leftWalkAnim || this.playerModel.walkAnim, "left_walking"],
366
+ [this.playerModel.rightWalkAnim || this.playerModel.walkAnim, "right_walking"],
367
+ [this.playerModel.backwardAnim || this.playerModel.walkAnim, "walking_backward"],
368
+ [this.playerModel.jumpAnim, "jumping"],
369
+ [this.playerModel.runAnim, "running"]
370
+ ];
371
+ for (const [key, clipName] of regs) {
372
+ const clip = findClip(key);
373
+ if (!clip) continue;
374
+ const action = this.personMixer.clipAction(clip);
375
+ if (clipName === "jumping") {
376
+ action.setLoop(THREE.LoopOnce, 1);
377
+ action.clampWhenFinished = true;
378
+ action.setEffectiveTimeScale(1.2);
379
+ } else {
380
+ action.setLoop(THREE.LoopRepeat, Infinity);
381
+ action.clampWhenFinished = false;
382
+ action.setEffectiveTimeScale(1);
383
+ }
384
+ action.enabled = true;
385
+ action.setEffectiveWeight(0);
386
+ this.personActions.set(clipName, action);
387
+ }
388
+ this.idleAction = this.personActions.get("idle");
389
+ this.walkAction = this.personActions.get("walking");
390
+ this.leftWalkAction = this.personActions.get("left_walking");
391
+ this.rightWalkAction = this.personActions.get("right_walking");
392
+ this.backwardAction = this.personActions.get("walking_backward");
393
+ this.jumpAction = this.personActions.get("jumping");
394
+ this.runAction = this.personActions.get("running");
395
+ this.idleAction.setEffectiveWeight(1);
396
+ this.idleAction.play();
397
+ this.actionState = this.idleAction;
398
+ this.personMixer.addEventListener("finished", (ev) => {
399
+ const finishedAction = ev.action;
400
+ if (finishedAction === this.jumpAction) {
401
+ if (this.fwdPressed) {
402
+ if (this.shiftPressed) this.playPersonAnimationByName("running");
403
+ else this.playPersonAnimationByName("walking");
404
+ return;
405
+ }
406
+ if (this.bkdPressed) {
407
+ this.playPersonAnimationByName("walking_backward");
408
+ return;
409
+ }
410
+ if (this.rgtPressed || this.lftPressed) {
411
+ this.playPersonAnimationByName("walking");
412
+ return;
413
+ }
414
+ this.playPersonAnimationByName("idle");
415
+ }
416
+ });
417
+ } catch (error) {
418
+ }
419
+ }
420
+ // 平滑切换人物动画
421
+ playPersonAnimationByName(name, fade = 0.18) {
422
+ if (!this.personActions) return;
423
+ if (this.ctPressed) return;
424
+ const next = this.personActions.get(name);
425
+ if (!next) return;
426
+ if (this.actionState === next) return;
427
+ const prev = this.actionState;
428
+ next.reset();
429
+ next.setEffectiveWeight(1);
430
+ next.play();
431
+ if (prev && prev !== next) {
432
+ prev.fadeOut(fade);
433
+ next.fadeIn(fade);
434
+ } else {
435
+ next.fadeIn(fade);
436
+ }
437
+ this.actionState = next;
438
+ }
439
+ // 创建玩家胶囊体
440
+ createPlayer() {
441
+ const material = new THREE.MeshStandardMaterial({
442
+ color: new THREE.Color(1, 0, 0),
443
+ shadowSide: THREE.DoubleSide,
444
+ depthTest: false
445
+ });
446
+ material.transparent = true;
447
+ material.opacity = this.displayPlayer ? 0.5 : 0;
448
+ material.wireframe = true;
449
+ this.player = new THREE.Mesh(
450
+ new RoundedBoxGeometry(
451
+ 75 * this.playerModel.scale,
452
+ 180 * this.playerModel.scale,
453
+ 75 * this.playerModel.scale,
454
+ 100 * this.playerModel.scale,
455
+ 75 * this.playerModel.scale
456
+ ),
457
+ material
458
+ );
459
+ this.player.geometry.translate(0, -30 * this.playerModel.scale, 0);
460
+ this.player.capsuleInfo = {
461
+ radius: 25 * this.playerModel.scale,
462
+ segment: new THREE.Line3(
463
+ new THREE.Vector3(),
464
+ new THREE.Vector3(0, -1, 0)
465
+ )
466
+ };
467
+ this.player.name = "\u89D2\u8272\u80F6\u56CA\u4F53";
468
+ this.player.rotateY(Math.PI / 2);
469
+ this.scene.add(this.player);
470
+ this.reset();
471
+ }
472
+ // 每帧更新
473
+ async updatePlayer(delta) {
474
+ if (!this.isUpdatePlayer || !this.player) return;
475
+ delta = Math.min(delta, 1 / 30);
476
+ this.updateMixers(delta);
477
+ if (!this.collider) return;
478
+ this.camera.getWorldDirection(this.camDir);
479
+ let angle = Math.atan2(this.camDir.z, this.camDir.x) + Math.PI / 2;
480
+ angle = 2 * Math.PI - angle;
481
+ this.moveDir.set(0, 0, 0);
482
+ if (this.fwdPressed) this.moveDir.add(this.DIR_FWD);
483
+ if (this.bkdPressed) this.moveDir.add(this.DIR_BKD);
484
+ if (this.lftPressed) this.moveDir.add(this.DIR_LFT);
485
+ if (this.rgtPressed) this.moveDir.add(this.DIR_RGT);
486
+ if (this.spacePressed && this.playerIsOnGround) {
487
+ this.playPersonAnimationByName("jumping");
488
+ setTimeout(() => {
489
+ this.playerVelocity.y = this.jumpHeight;
490
+ this.playerIsOnGround = false;
491
+ this.spacePressed = false;
492
+ this.player.position.addScaledVector(this.playerVelocity, delta);
493
+ this.player.updateMatrixWorld();
494
+ }, 200);
495
+ }
496
+ if (this.sustainSpacePressed && this.playerIsOnGround) {
497
+ this.playPersonAnimationByName("jumping");
498
+ setTimeout(() => {
499
+ this.playerVelocity.y = this.highJumpHeight;
500
+ this.playerIsOnGround = false;
501
+ this.spacePressed = false;
502
+ this.player.position.addScaledVector(this.playerVelocity, delta);
503
+ this.player.updateMatrixWorld();
504
+ }, 200);
505
+ }
506
+ this.playerSpeed = this.shiftPressed ? 900 * this.playerModel.scale : 400 * this.playerModel.scale;
507
+ if (this.moveDir.lengthSq() > 1e-6) {
508
+ this.moveDir.normalize().applyAxisAngle(this.upVector, angle);
509
+ this.player.position.addScaledVector(
510
+ this.moveDir,
511
+ this.playerSpeed * delta
512
+ );
513
+ }
514
+ let playerDistanceFromGround = Infinity;
515
+ this._originTmp.set(
516
+ this.player.position.x,
517
+ this.player.position.y,
518
+ this.player.position.z
519
+ );
520
+ this._raycaster.ray.origin.copy(this._originTmp);
521
+ const intersects = this._raycaster.intersectObject(
522
+ this.collider,
523
+ false
524
+ );
525
+ if (intersects.length > 0) {
526
+ playerDistanceFromGround = this.player.position.y - intersects[0].point.y;
527
+ }
528
+ if (playerDistanceFromGround > 130 * this.playerModel.scale) {
529
+ this.playerVelocity.y += delta * this.gravity;
530
+ this.player.position.addScaledVector(this.playerVelocity, delta);
531
+ } else {
532
+ this.playerIsOnGround = true;
533
+ }
534
+ this.player.updateMatrixWorld();
535
+ const capsuleInfo = this.player.capsuleInfo;
536
+ this.tempBox.makeEmpty();
537
+ this.tempMat.copy(this.collider.matrixWorld).invert();
538
+ this.tempSegment.copy(capsuleInfo.segment);
539
+ this.tempSegment.start.applyMatrix4(this.player.matrixWorld).applyMatrix4(this.tempMat);
540
+ this.tempSegment.end.applyMatrix4(this.player.matrixWorld).applyMatrix4(this.tempMat);
541
+ this.tempBox.expandByPoint(this.tempSegment.start);
542
+ this.tempBox.expandByPoint(this.tempSegment.end);
543
+ this.tempBox.expandByScalar(capsuleInfo.radius);
544
+ const bvh = this.collider?.geometry;
545
+ bvh?.boundsTree?.shapecast({
546
+ // 检测包围盒碰撞
547
+ intersectsBounds: (box) => box.intersectsBox(this.tempBox),
548
+ // 检测三角形碰撞
549
+ intersectsTriangle: (tri) => {
550
+ const triPoint = this.tempVector;
551
+ const capsulePoint = this.tempVector2;
552
+ const distance = tri.closestPointToSegment(
553
+ this.tempSegment,
554
+ triPoint,
555
+ capsulePoint
556
+ );
557
+ if (distance < capsuleInfo.radius) {
558
+ const depth = capsuleInfo.radius - distance;
559
+ const direction = capsulePoint.sub(triPoint).normalize();
560
+ this.tempSegment.start.addScaledVector(direction, depth);
561
+ this.tempSegment.end.addScaledVector(direction, depth);
562
+ }
563
+ }
564
+ });
565
+ const newPosition = this.tempVector.copy(this.tempSegment.start).applyMatrix4(this.collider.matrixWorld);
566
+ const deltaVector = this.tempVector2.subVectors(
567
+ newPosition,
568
+ this.player.position
569
+ );
570
+ const len = deltaVector.length();
571
+ const offset = Math.max(0, len - 1e-5);
572
+ if (offset > 0 && len > 0) {
573
+ const n = deltaVector.multiplyScalar(1 / len);
574
+ this.player.position.addScaledVector(n, offset);
575
+ this.playerVelocity.set(0, 0, 0);
576
+ }
577
+ if (!this.isFirstPerson && this.moveDir.lengthSq() > 0) {
578
+ this.camDir.y = 0;
579
+ this.camDir.normalize();
580
+ this.camDir.negate();
581
+ this.moveDir.normalize();
582
+ this.moveDir.negate();
583
+ const lookTarget = this.player.position.clone().add(this.moveDir);
584
+ this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
585
+ this.targetQuat.setFromRotationMatrix(this.targetMat);
586
+ const alpha = Math.min(1, this.rotationSpeed * delta);
587
+ this.player.quaternion.slerp(this.targetQuat, alpha);
588
+ }
589
+ if (!this.isFirstPerson) {
590
+ const lookTarget = this.player.position.clone();
591
+ lookTarget.y += 30 * this.playerModel.scale;
592
+ this.camera.position.sub(this.controls.target);
593
+ this.controls.target.copy(lookTarget);
594
+ this.camera.position.add(lookTarget);
595
+ this.controls.update();
596
+ this._personToCam.subVectors(this.camera.position, this.player.position);
597
+ const origin = this.player.position.clone().add(new THREE.Vector3(0, 0, 0));
598
+ const direction = this._personToCam.clone().normalize();
599
+ const desiredDist = this._personToCam.length();
600
+ this._raycasterPersonToCam.set(origin, direction);
601
+ this._raycasterPersonToCam.far = desiredDist;
602
+ const intersects2 = this._raycasterPersonToCam.intersectObject(
603
+ this.collider,
604
+ false
605
+ );
606
+ if (intersects2.length > 0) {
607
+ const hit = intersects2[0];
608
+ const safeDist = Math.max(
609
+ hit.distance - this._camEpsilon,
610
+ this._minCamDistance
611
+ );
612
+ const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
613
+ this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
614
+ } else {
615
+ const dis = this.player.position.distanceTo(this.camera.position);
616
+ this._raycasterPersonToCam.far = this._maxCamDistance;
617
+ const intersectsMaxDis = this._raycasterPersonToCam.intersectObject(
618
+ this.collider,
619
+ false
620
+ );
621
+ if (dis < this._maxCamDistance) {
622
+ let safeDist = this._maxCamDistance;
623
+ if (intersectsMaxDis.length) {
624
+ const hitMax = intersectsMaxDis[0];
625
+ safeDist = hitMax.distance - this._camEpsilon;
626
+ }
627
+ const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
628
+ this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
629
+ }
630
+ }
631
+ }
632
+ if (this.player.position.y < this.boundingBoxMinY - 1) {
633
+ this._originTmp.set(
634
+ this.player.position.x,
635
+ 1e4,
636
+ this.player.position.z
637
+ );
638
+ this._raycaster.ray.origin.copy(this._originTmp);
639
+ const intersects2 = this._raycaster.intersectObject(
640
+ this.collider,
641
+ false
642
+ );
643
+ if (intersects2.length > 0) {
644
+ console.log("\u73A9\u5BB6\u4E3Abug\u610F\u5916\u6389\u843D");
645
+ this.reset(
646
+ new THREE.Vector3(
647
+ this.player.position.x,
648
+ intersects2[0].point.y + 5,
649
+ this.player.position.z
650
+ )
651
+ );
652
+ } else {
653
+ console.log("\u73A9\u5BB6\u6B63\u5E38\u6389\u843D");
654
+ this.reset(
655
+ new THREE.Vector3(
656
+ this.player.position.x,
657
+ this.player.position.y + 15,
658
+ this.player.position.z
659
+ )
660
+ );
661
+ }
662
+ }
663
+ }
664
+ // 重置 / 销毁
665
+ reset(position) {
666
+ if (!this.player) return;
667
+ this.playerVelocity.set(0, 0, 0);
668
+ this.player.position.copy(position ? position : this.initPos);
669
+ }
670
+ // 销毁
671
+ destroy() {
672
+ this.offAllEvent();
673
+ if (this.player) {
674
+ this.player.remove(this.camera);
675
+ this.scene.remove(this.player);
676
+ }
677
+ this.player = null;
678
+ if (this.person) {
679
+ this.scene.remove(this.person);
680
+ this.person = null;
681
+ }
682
+ this.resetControls();
683
+ if (this.visualizer) {
684
+ this.scene.remove(this.visualizer);
685
+ this.visualizer = null;
686
+ }
687
+ if (this.collider) {
688
+ this.scene.remove(this.collider);
689
+ this.collider = null;
690
+ }
691
+ controllerInstance = null;
692
+ }
693
+ // 事件绑定
694
+ onAllEvent() {
695
+ this.isUpdatePlayer = true;
696
+ document.body.requestPointerLock();
697
+ window.addEventListener("keydown", this._boundOnKeydown);
698
+ window.addEventListener("keyup", this._boundOnKeyup);
699
+ window.addEventListener("mousemove", this._mouseMove);
700
+ window.addEventListener("click", this._mouseClick);
701
+ }
702
+ // 事件解绑
703
+ offAllEvent() {
704
+ this.isUpdatePlayer = false;
705
+ document.exitPointerLock();
706
+ window.removeEventListener("keydown", this._boundOnKeydown);
707
+ window.removeEventListener("keyup", this._boundOnKeyup);
708
+ window.removeEventListener("mousemove", this._mouseMove);
709
+ window.removeEventListener("click", this._mouseClick);
710
+ }
711
+ // 更新模型动画
712
+ updateMixers(delta) {
713
+ if (this.personMixer) this.personMixer.update(delta);
714
+ if (this.droneMixer) this.droneMixer.update(delta);
715
+ }
716
+ // BVH构建
717
+ async createBVH(meshUrl = "") {
718
+ await this.initLoader();
719
+ const ensureAttributesMinimal = (geom) => {
720
+ if (!geom.attributes.position) {
721
+ return null;
722
+ }
723
+ if (!geom.attributes.normal) geom.computeVertexNormals();
724
+ if (!geom.attributes.uv) {
725
+ const count = geom.attributes.position.count;
726
+ const dummyUV = new Float32Array(count * 2);
727
+ geom.setAttribute("uv", new THREE.BufferAttribute(dummyUV, 2));
728
+ }
729
+ return geom;
730
+ };
731
+ const collected = [];
732
+ if (meshUrl == "") {
733
+ if (this.collider) {
734
+ this.scene.remove(this.collider);
735
+ this.collider = null;
736
+ }
737
+ this.scene.traverse((c) => {
738
+ const mesh = c;
739
+ if (mesh?.isMesh && mesh.geometry && c.name !== "\u89D2\u8272\u80F6\u56CA\u4F53") {
740
+ try {
741
+ let geom = mesh.geometry.clone();
742
+ geom.applyMatrix4(mesh.matrixWorld);
743
+ if (geom.index) geom = geom.toNonIndexed();
744
+ const safe = ensureAttributesMinimal(geom);
745
+ if (safe) collected.push(safe);
746
+ } catch (e) {
747
+ console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
748
+ }
749
+ }
750
+ });
751
+ if (!collected.length) {
752
+ return;
753
+ }
754
+ const attrMap = /* @__PURE__ */ new Map();
755
+ const attrConflict = /* @__PURE__ */ new Set();
756
+ for (const g of collected) {
757
+ for (const name of Object.keys(g.attributes)) {
758
+ const attr = g.attributes[name];
759
+ const ctor = attr.array.constructor;
760
+ const itemSize = attr.itemSize;
761
+ if (!attrMap.has(name)) {
762
+ attrMap.set(name, { itemSize, arrayCtor: ctor, examples: 1 });
763
+ } else {
764
+ const m = attrMap.get(name);
765
+ if (m.itemSize !== itemSize || m.arrayCtor !== ctor)
766
+ attrConflict.add(name);
767
+ else m.examples++;
768
+ }
769
+ }
770
+ }
771
+ if (attrConflict.size) {
772
+ for (const g of collected) {
773
+ for (const name of Array.from(attrConflict)) {
774
+ if (g.attributes[name]) g.deleteAttribute(name);
775
+ }
776
+ }
777
+ for (const name of attrConflict) attrMap.delete(name);
778
+ }
779
+ const attrNames = Array.from(attrMap.keys());
780
+ for (const g of collected) {
781
+ const count = g.attributes.position.count;
782
+ for (const name of attrNames) {
783
+ if (!g.attributes[name]) {
784
+ const meta = attrMap.get(name);
785
+ const len = count * meta.itemSize;
786
+ const array = new meta.arrayCtor(len);
787
+ g.setAttribute(
788
+ name,
789
+ new THREE.BufferAttribute(array, meta.itemSize)
790
+ );
791
+ }
792
+ }
793
+ }
794
+ } else {
795
+ const gltf = await this.loader.loadAsync(meshUrl, (xhr) => {
796
+ });
797
+ const mesh = gltf.scene.children[0];
798
+ mesh.name = "BVH\u52A0\u8F7D\u6A21\u578B";
799
+ let geom = mesh.geometry.clone();
800
+ geom.applyMatrix4(mesh.matrixWorld);
801
+ if (geom.index) geom = geom.toNonIndexed();
802
+ const safe = ensureAttributesMinimal(geom);
803
+ if (safe) collected.push(safe);
804
+ }
805
+ const merged = BufferGeometryUtils.mergeGeometries(collected, false);
806
+ if (!merged) {
807
+ console.error("\u5408\u5E76\u51E0\u4F55\u5931\u8D25");
808
+ return;
809
+ }
810
+ merged.boundsTree = new MeshBVH(merged);
811
+ this.collider = new THREE.Mesh(
812
+ merged,
813
+ new THREE.MeshBasicMaterial({
814
+ color: "red",
815
+ opacity: 0.2,
816
+ transparent: true,
817
+ wireframe: false
818
+ })
819
+ );
820
+ if (this.displayCollider) this.scene.add(this.collider);
821
+ if (this.displayVisualizer) {
822
+ if (this.visualizer) this.scene.remove(this.visualizer);
823
+ this.visualizer = new MeshBVHHelper(this.collider, this.visualizeDepth);
824
+ this.scene.add(this.visualizer);
825
+ }
826
+ this.boundingBoxMinY = this.collider.geometry.boundingBox.min.y;
827
+ console.log("bvh\u52A0\u8F7D\u6A21\u578B\u6210\u529F", this.collider);
828
+ }
829
+ };
830
+ function usePlayer() {
831
+ if (!controllerInstance) controllerInstance = new PlayerController();
832
+ const c = controllerInstance;
833
+ return {
834
+ init: (opts, callback) => c.init(opts, callback),
835
+ changeView: () => c.changeView(),
836
+ createBVH: (url = "") => c.createBVH(url),
837
+ createPlayer: () => c.createPlayer(),
838
+ reset: (pos) => c.reset(pos),
839
+ updatePlayer: (dt) => c.updatePlayer(dt),
840
+ destroy: () => c.destroy()
841
+ };
842
+ }
843
+ function onAllEvent() {
844
+ if (!controllerInstance) controllerInstance = new PlayerController();
845
+ controllerInstance.onAllEvent();
846
+ }
847
+ function offAllEvent() {
848
+ if (!controllerInstance) return;
849
+ controllerInstance.offAllEvent();
850
+ }
851
+ export {
852
+ offAllEvent,
853
+ onAllEvent,
854
+ usePlayer
855
+ };
856
+ //# sourceMappingURL=index.mjs.map