three-base 2.0.134 → 2.0.137

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/FPScontrol.d.ts CHANGED
@@ -17,6 +17,7 @@ export default class FPScontrol extends EventDispatcher {
17
17
  inertia: boolean;
18
18
  scene: Scene;
19
19
  dev: boolean;
20
+ devCameraOffset: Vector3;
20
21
  thirdPerson: boolean;
21
22
  thirdPersonDistance: number;
22
23
  thirdPersonHeight: number;
@@ -24,18 +25,24 @@ export default class FPScontrol extends EventDispatcher {
24
25
  private _thirdPersonAngleX;
25
26
  private _prevThirdPerson;
26
27
  COLLIDE_VOLUME: number;
28
+ private readonly VERTICAL_RANGE;
27
29
  private readonly PUSH_DISTANCE;
28
30
  private readonly MAX_ITERATIONS;
31
+ private _tempQuat;
29
32
  private _raycaster;
30
33
  private _tempVec;
31
34
  private _tempVec2;
32
35
  private _tempVec3;
33
36
  private _tempVec4;
34
- private readonly HORIZONTAL_RAY_DIRECTIONS;
35
- private readonly VERTICAL_DOWN;
36
- private readonly VERTICAL_UP;
37
+ private readonly _localRayOrigins;
38
+ private readonly _localRayDirs;
39
+ private _worldRayOrigins;
40
+ private _worldRayDirs;
37
41
  private _colliders;
42
+ private _collidersDirty;
43
+ private _colliderSceneVersion;
38
44
  private _getColliders;
45
+ markCollidersDirty(): void;
39
46
  playerCollider: Capsule;
40
47
  clock: Clock;
41
48
  prePlayerVelocity: Vector3;
package/FPScontrol.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * @Last Modified by: xiaosihan
5
5
  * @Last Modified time: 2024-11-07 21:57:39
6
6
  */
7
- import { BufferGeometry, Clock, EventDispatcher, Float32BufferAttribute, Group, Line, LineBasicMaterial, Mesh, InstancedMesh, Raycaster, Vector3, } from "three";
7
+ import { BufferGeometry, Clock, EventDispatcher, Euler, Float32BufferAttribute, Group, Line, LineBasicMaterial, Mesh, InstancedMesh, Quaternion, Raycaster, Vector3, } from "three";
8
8
  import { Capsule } from "three/examples/jsm/math/Capsule";
9
9
  import { clamp, degToRad } from "three/src/math/MathUtils";
10
10
  import { acceleratedRaycast, computeBoundsTree } from "three-mesh-bvh";
@@ -34,7 +34,7 @@ export default class FPScontrol extends EventDispatcher {
34
34
  // 是否启用
35
35
  enable = false;
36
36
  GRAVITY = 30; // 重力
37
- STEPS_PER_FRAME = 5; // 每帧碰撞计算的次数
37
+ STEPS_PER_FRAME = 2; // 每帧碰撞计算的次数
38
38
  speed = 1; // 速度
39
39
  camera;
40
40
  // 移动时是否保持惯性 由外部来配置
@@ -44,6 +44,8 @@ export default class FPScontrol extends EventDispatcher {
44
44
  // ========== dev / 第三人称 ==========
45
45
  // 开发模式:打开后显示射线可视化
46
46
  dev = false;
47
+ // 调试模式相机偏移(本地坐标系,Y=上,Z=后)
48
+ devCameraOffset = new Vector3(0, 2.5, 12.5);
47
49
  // 第三人称模式
48
50
  thirdPerson = false;
49
51
  // 第三人称参数:相机与第一人称位置的距离和高度偏移
@@ -55,12 +57,21 @@ export default class FPScontrol extends EventDispatcher {
55
57
  // 模式切换检测
56
58
  _prevThirdPerson = false;
57
59
  // ========== 射线碰撞检测 ==========
58
- // 碰撞体积(胶囊体半径)
60
+ // 碰撞体积(对应 xsh-react-three 的 collideVolume,默认 3,此处缩放为 0.5)
61
+ // 用于水平射线的起点偏移和 far 值
59
62
  COLLIDE_VOLUME = 0.5;
60
- // 推离步长(每次迭代推离的距离)
63
+ // 垂直碰撞范围 = 从 playerCollider.end(眼睛)到脚底的距离
64
+ // = capsule length + radius = (1.0 - 0.35) + 0.35 = 1.0
65
+ // 对应 xsh-react-three 中 collideVolume(=3)= 控制器到地面的距离
66
+ // xsh 中 far = collideVolume, 响应 = collideVolume - distance
67
+ // 此处 far = VERTICAL_RANGE, 响应 = VERTICAL_RANGE - distance
68
+ VERTICAL_RANGE = 1.0;
69
+ // 推离步长(每次迭代推离的距离,与 xsh-react-three 一致:0.03)
61
70
  PUSH_DISTANCE = 0.03;
62
- // 最大迭代次数
71
+ // 最大迭代次数(与 xsh-react-three 一致:100)
63
72
  MAX_ITERATIONS = 100;
73
+ // 临时四元数(复用避免 GC)
74
+ _tempQuat = new Quaternion();
64
75
  // 射线检测器
65
76
  _raycaster = new Raycaster();
66
77
  // 临时向量(复用避免 GC)
@@ -68,20 +79,50 @@ export default class FPScontrol extends EventDispatcher {
68
79
  _tempVec2 = new Vector3();
69
80
  _tempVec3 = new Vector3();
70
81
  _tempVec4 = new Vector3();
71
- // 水平射线方向(4 个 45° 斜向)
72
- HORIZONTAL_RAY_DIRECTIONS = [
73
- new Vector3(-1, 0, -1).normalize(), // 右→前
74
- new Vector3(1, 0, -1).normalize(), // 左→前
75
- new Vector3(1, 0, 1).normalize(), // 左→后
76
- new Vector3(-1, 0, 1).normalize(), // 右→后
82
+ // ========== 射线定义(完全参考 xsh-react-three FPSControls.tsx 的 render 方法)==========
83
+ // 射线定义在局部空间,通过 Group3D(rotation={x: -euler.x}) 补偿 pitch
84
+ // 即射线只跟随 yaw 旋转,不跟随 pitch 俯仰
85
+ //
86
+ // xsh-react-three 原始布局(collideVolume=cv):
87
+ // rightToForwardRay: position={x:cv, y:0, z:0}, direction={x:-cv, y:0, z:-cv}
88
+ // leftToForwardRay: position={x:-cv, y:0, z:0}, direction={x:cv, y:0, z:-cv}
89
+ // leftToBackRay: position={x:-cv, y:0, z:0}, direction={x:cv, y:0, z:cv}
90
+ // rightToBackRay: position={x:cv, y:0, z:0}, direction={x:-cv, y:0, z:cv}
91
+ // upTodwomRay: position={x:0, y:0, z:0}, direction={y:-cv}
92
+ // dwomToUpRay: position={x:0, y:0, z:0}, direction={y:cv}
93
+ //
94
+ // 局部空间射线起点(索引 0-5)
95
+ _localRayOrigins = [
96
+ new Vector3(0, 0, 0), // 0: upTodwomRay(下射线)
97
+ new Vector3(0, 0, 0), // 1: dwomToUpRay(上射线)
98
+ new Vector3(1, 0, 0), // 2: rightToForwardRay 起点(右侧)
99
+ new Vector3(-1, 0, 0), // 3: leftToForwardRay 起点(左侧)
100
+ new Vector3(-1, 0, 0), // 4: leftToBackRay 起点(左侧)
101
+ new Vector3(1, 0, 0), // 5: rightToBackRay 起点(右侧)
77
102
  ];
78
- // 垂直射线方向
79
- VERTICAL_DOWN = new Vector3(0, -1, 0); // 上→下
80
- VERTICAL_UP = new Vector3(0, 1, 0); // 下→上
103
+ // 局部空间射线方向(对应 direction prop,归一化前为 {x:±cv, y:0, z:±cv})
104
+ // 归一化后与 xsh 的 clampLength(0,1) 效果一致
105
+ _localRayDirs = [
106
+ new Vector3(0, -1, 0), // 0: 下射线方向
107
+ new Vector3(0, 1, 0), // 1: 上射线方向
108
+ new Vector3(-1, 0, -1).normalize(), // 2: rightToForward → 指向 -X -Z(左前方)
109
+ new Vector3(1, 0, -1).normalize(), // 3: leftToForward → 指向 +X -Z(右前方)
110
+ new Vector3(1, 0, 1).normalize(), // 4: leftToBack → 指向 +X +Z(右后方)
111
+ new Vector3(-1, 0, 1).normalize(), // 5: rightToBack → 指向 -X +Z(左后方)
112
+ ];
113
+ // 世界空间射线起点(复用避免 GC)
114
+ _worldRayOrigins = [0, 1, 2, 3, 4, 5].map(() => new Vector3());
115
+ // 世界空间射线方向(复用避免 GC)
116
+ _worldRayDirs = [0, 1, 2, 3, 4, 5].map(() => new Vector3());
81
117
  // userData.fpsCollision === true 的碰撞体列表
82
118
  _colliders = [];
83
- // 收集 fpsCollision 物体 + 首次构建 BVH(参照 ThreeMouse dispatchEvent)
119
+ // 缓存标记:场景变化时才重建碰撞体列表
120
+ _collidersDirty = true;
121
+ _colliderSceneVersion = 0;
122
+ // 收集 fpsCollision 物体 + 首次构建 BVH
84
123
  _getColliders() {
124
+ if (!this._collidersDirty)
125
+ return;
85
126
  this._colliders.length = 0;
86
127
  this.scene.traverseVisible((obj) => {
87
128
  if (obj.isMesh && obj.userData?.fpsCollision) {
@@ -92,6 +133,11 @@ export default class FPScontrol extends EventDispatcher {
92
133
  this._colliders.push(obj);
93
134
  }
94
135
  });
136
+ this._collidersDirty = false;
137
+ }
138
+ // 标记碰撞体列表需要重建(外部在添加/删除碰撞体时调用)
139
+ markCollidersDirty() {
140
+ this._collidersDirty = true;
95
141
  }
96
142
  // 玩家胶囊碰撞体
97
143
  playerCollider = new Capsule(new Vector3(0, 0.35, 0), new Vector3(0, 1, 0), 0.35);
@@ -294,143 +340,209 @@ export default class FPScontrol extends EventDispatcher {
294
340
  this.thirdPersonDistance;
295
341
  this.camera.lookAt(this.playerCollider.end);
296
342
  }
343
+ // dev 调试模式:在相机本地坐标系叠加偏移(向后上方)
344
+ if (this.dev && !this.thirdPerson) {
345
+ const offset = this.devCameraOffset.clone().applyQuaternion(this.camera.quaternion);
346
+ this.camera.position.add(offset);
347
+ }
297
348
  this.camera.rotation.x = clamp(this.camera.rotation.x, degToRad(-89), degToRad(89));
298
349
  };
299
- // 碰撞检测:6 射线迭代推离(水平 4 + 垂直 2)
350
+ // 碰撞检测(完全参考 xsh-react-three FPSControls.tsx 的 computerPosition 方法)
351
+ //
352
+ // 射线布局对比(xsh-react-three 的 Raycaster 组件 → 本地实现):
353
+ // xsh: position prop → _localRayOrigins × collideVolume
354
+ // xsh: direction prop → _localRayDirs(归一化后 = clampLength(0,1))
355
+ // xsh: raycaster.far = getDirection().length() → horizontalFar / verticalFar
356
+ // xsh: Group3D rotation={x:-euler.x} → yawQuat(pitch 补偿)
357
+ // xsh: getIntersetResult(positionIncremental) → worldPos = playerCollider.end + positionIncremental
358
+ //
359
+ // 6条射线完整数据(cv = collideVolume, vr = VERTICAL_RANGE):
360
+ // 0 upTodwomRay: origin=(0,0,0)×cv+worldPos, dir=(0,-1,0)×yaw, far=vr, 响应=vr-distance
361
+ // 1 dwomToUpRay: origin=(0,0,0)×cv+worldPos, dir=(0,1,0)×yaw, far=vr, 响应=faceNormal×0.01
362
+ // 2 rightToForward: origin=(cv,0,0)×yaw+worldPos, dir=(-1,0,-1).norm×yaw, far=cv√2
363
+ // 3 leftToForward: origin=(-cv,0,0)×yaw+worldPos, dir=(1,0,-1).norm×yaw, far=cv√2
364
+ // 4 leftToBack: origin=(-cv,0,0)×yaw+worldPos, dir=(1,0,1).norm×yaw, far=cv√2
365
+ // 5 rightToBack: origin=(cv,0,0)×yaw+worldPos, dir=(-1,0,1).norm×yaw, far=cv√2
300
366
  playerCollisions() {
301
367
  this.playerOnFloor = false;
302
- // 收集 fpsCollision 物体
303
368
  this._getColliders();
304
- // 累积位移增量(用于迭代推离)
305
369
  const positionIncremental = this._tempVec4.set(0, 0, 0);
306
- // dev 模式:记录射线数据
307
- const devRayData = this.dev ? [] : null;
308
- let iterationCount = 0;
309
- let hasCollision = true;
310
- // ========== 迭代推离 ==========
311
- while (hasCollision && iterationCount < this.MAX_ITERATIONS) {
312
- hasCollision = false;
313
- iterationCount++;
314
- // 最后 2 次迭代记录调试数据
315
- const recordDebug = this.dev && iterationCount >= this.MAX_ITERATIONS - 2;
316
- // 获取胶囊体中心的世界位置
317
- const centerPos = this._tempVec3
318
- .addVectors(this.playerCollider.start, this.playerCollider.end)
319
- .multiplyScalar(0.5);
320
- // ========== 水平射线(4 根 45°)==========
321
- for (let ri = 0; ri < this.HORIZONTAL_RAY_DIRECTIONS.length; ri++) {
322
- const rayDir = this.HORIZONTAL_RAY_DIRECTIONS[ri];
323
- // 起点:胶囊体中心向射线方向偏移 COLLIDE_VOLUME
324
- const rayOrigin = this._tempVec2
325
- .copy(centerPos)
326
- .addScaledVector(rayDir, -this.COLLIDE_VOLUME);
327
- this._raycaster.set(rayOrigin, rayDir.clone());
328
- this._raycaster.far = this.COLLIDE_VOLUME * 2;
329
- const hits = this._raycaster.intersectObjects(this._colliders, false);
330
- if (recordDebug) {
331
- // 记录调试射线数据
332
- const farPoint = new Vector3().copy(rayOrigin).addScaledVector(rayDir, this.COLLIDE_VOLUME * 2);
333
- const hasHit = hits.length > 0 && hits[0].distance < this.COLLIDE_VOLUME * 2;
334
- devRayData.push({
335
- origin: rayOrigin.clone(),
336
- farPoint: hasHit ? hits[0].point.clone() : farPoint,
370
+ const collideVolume = this.COLLIDE_VOLUME;
371
+ const verticalRange = this.VERTICAL_RANGE;
372
+ let i = 0;
373
+ let goOnComputer = true;
374
+ // 调试数据:按射线索引存储(0=下, 1=上, 2-5=水平),只在第一次迭代记录
375
+ const devRayData = this.dev ? new Array(6).fill(null) : null;
376
+ let debugRecorded = !this.dev;
377
+ while (goOnComputer && i < 100) {
378
+ goOnComputer = false;
379
+ i++;
380
+ // 与 xsh: object3d.getWorldPosition() + positionIncremental 对应
381
+ const worldPos = this._tempVec3
382
+ .copy(this.playerCollider.end)
383
+ .add(positionIncremental);
384
+ // 与 xsh: Group3D rotation={x:-euler.x} 补偿 pitch → 只跟随 yaw
385
+ const yawQuat = this._tempQuat.setFromEuler(new Euler(0, this.camera.rotation.y, 0, "YXZ"));
386
+ // 将局部射线原点和方向转换为世界空间
387
+ for (let ri = 0; ri < 6; ri++) {
388
+ this._worldRayOrigins[ri]
389
+ .copy(this._localRayOrigins[ri])
390
+ .multiplyScalar(collideVolume)
391
+ .applyQuaternion(yawQuat)
392
+ .add(worldPos);
393
+ this._worldRayDirs[ri]
394
+ .copy(this._localRayDirs[ri])
395
+ .applyQuaternion(yawQuat)
396
+ .normalize();
397
+ }
398
+ // far 值
399
+ const horizontalFar = collideVolume * Math.SQRT2;
400
+ const downFar = verticalRange;
401
+ const upFar = verticalRange;
402
+ // ========== 4 条水平射线(索引 2-5)==========
403
+ for (const ri of [2, 3, 4, 5]) {
404
+ this._raycaster.set(this._worldRayOrigins[ri], this._worldRayDirs[ri]);
405
+ this._raycaster.far = horizontalFar;
406
+ const intersets = this._raycaster.intersectObjects(this._colliders, false);
407
+ // 调试:第一次迭代记录射线数据
408
+ if (!debugRecorded) {
409
+ const hasHit = intersets.length > 0;
410
+ let closestDist = Infinity;
411
+ let intersetPoint = null;
412
+ if (hasHit) {
413
+ for (const int of intersets) {
414
+ const d = int.point.distanceTo(worldPos);
415
+ if (d < closestDist) {
416
+ closestDist = d;
417
+ intersetPoint = int.point;
418
+ }
419
+ }
420
+ }
421
+ devRayData[ri] = {
422
+ origin: this._worldRayOrigins[ri].clone(),
423
+ farPoint: hasHit
424
+ ? intersetPoint.clone()
425
+ : this._worldRayOrigins[ri].clone().addScaledVector(this._worldRayDirs[ri], horizontalFar),
337
426
  hit: hasHit,
338
- });
427
+ };
339
428
  }
340
- if (hits.length > 0) {
341
- const hit = hits[0];
342
- // 推离向量:交点 → 当前位置,长度 PUSH_DISTANCE
343
- const pushVector = this._tempVec
344
- .copy(centerPos)
345
- .sub(hit.point)
346
- .setLength(this.PUSH_DISTANCE);
347
- // 累加推离量到位移增量
348
- positionIncremental.add(pushVector);
349
- // 继续循环,因为推离后可能还会碰撞
350
- hasCollision = true;
429
+ if (intersets.length > 0) {
430
+ let interset = intersets[0];
431
+ if (intersets.length > 1) {
432
+ for (let j = 1; j < intersets.length; j++) {
433
+ if (intersets[j].point.distanceTo(worldPos) < interset.point.distanceTo(worldPos)) {
434
+ interset = intersets[j];
435
+ }
436
+ }
437
+ }
438
+ // 与 xsh: object3d.getWorldPosition().sub(interset.point).setLength(0.03) 一致
439
+ this._tempVec
440
+ .copy(worldPos)
441
+ .sub(interset.point)
442
+ .setLength(0.03);
443
+ positionIncremental.add(this._tempVec);
444
+ goOnComputer = true;
351
445
  }
352
446
  }
353
- // 重置 far(垂直射线各自设 far)
354
- this._raycaster.far = Infinity;
355
- // 垂直射线
356
- // 向下 → 地面检测
357
- const downRayOrigin = centerPos.clone();
358
- this._raycaster.set(downRayOrigin, this.VERTICAL_DOWN.clone());
359
- this._raycaster.far = this.COLLIDE_VOLUME + 0.5; // 适当延长检测距离
360
- const downHits = this._raycaster.intersectObjects(this._colliders, false);
361
- if (downHits.length > 0) {
362
- const hit = downHits[0];
363
- // 脚下 COLLIDE_VOLUME 内 = 站在地面
364
- if (hit.distance < this.COLLIDE_VOLUME + 0.1) {
365
- this.playerOnFloor = true;
366
- // 如果正在下落,归零垂直速度
367
- if (this.playerVelocity.y < 0) {
368
- this.playerVelocity.y = 0;
369
- }
370
- // 上移站位
371
- const adjustY = this.COLLIDE_VOLUME + 0.1 - hit.distance;
372
- if (adjustY > 0) {
373
- positionIncremental.y += adjustY;
447
+ // ========== 向下射线(索引 0 = upTodwomRay)==========
448
+ this._raycaster.set(this._worldRayOrigins[0], this._worldRayDirs[0]);
449
+ this._raycaster.far = downFar;
450
+ let downIntersets = this._raycaster.intersectObjects(this._colliders, false);
451
+ // 调试:记录向下射线
452
+ if (!debugRecorded) {
453
+ const hasHit = downIntersets.length > 0;
454
+ let intersetPoint = null;
455
+ if (hasHit) {
456
+ let closestDist = Infinity;
457
+ for (const int of downIntersets) {
458
+ const d = int.point.distanceTo(worldPos);
459
+ if (d < closestDist) {
460
+ closestDist = d;
461
+ intersetPoint = int.point;
462
+ }
374
463
  }
375
464
  }
376
- else if (hit.distance < this.COLLIDE_VOLUME + 0.3) {
377
- // 接近地面缓冲
378
- this.playerOnFloor = true;
379
- if (this.playerVelocity.y < 0) {
380
- this.playerVelocity.y = 0;
465
+ devRayData[0] = {
466
+ origin: this._worldRayOrigins[0].clone(),
467
+ farPoint: hasHit
468
+ ? intersetPoint.clone()
469
+ : this._worldRayOrigins[0].clone().addScaledVector(this._worldRayDirs[0], downFar),
470
+ hit: hasHit,
471
+ };
472
+ }
473
+ if (downIntersets.length > 0) {
474
+ let interset = downIntersets[0];
475
+ if (downIntersets.length > 1) {
476
+ for (let j = 1; j < downIntersets.length; j++) {
477
+ if (downIntersets[j].point.distanceTo(worldPos) < interset.point.distanceTo(worldPos)) {
478
+ interset = downIntersets[j];
479
+ }
381
480
  }
382
481
  }
383
- hasCollision = true;
384
- }
385
- if (recordDebug) {
386
- // 向下射线调试数据
387
- const downFar = new Vector3().copy(downRayOrigin).addScaledVector(this.VERTICAL_DOWN, this.COLLIDE_VOLUME + 0.5);
388
- const downHit = downHits.length > 0;
389
- devRayData.push({
390
- origin: downRayOrigin.clone(),
391
- farPoint: downHit ? downHits[0].point.clone() : downFar,
392
- hit: downHit,
393
- });
482
+ this.playerVelocity.y = 0;
483
+ positionIncremental.y += verticalRange - interset.distance;
484
+ this.playerOnFloor = true;
485
+ goOnComputer = true;
394
486
  }
395
- // 向上 → 头顶碰撞
396
- const upRayOrigin = centerPos.clone();
397
- this._raycaster.set(upRayOrigin, this.VERTICAL_UP.clone());
398
- this._raycaster.far = this.COLLIDE_VOLUME + 0.2;
399
- const upHits = this._raycaster.intersectObjects(this._colliders, false);
400
- if (upHits.length > 0) {
401
- const hit = upHits[0];
402
- // 头顶碰撞:归零向上速度 + 下移
403
- if (hit.distance < this.COLLIDE_VOLUME + 0.2) {
404
- // 归零向上的速度
405
- if (this.playerVelocity.y > 0) {
406
- this.playerVelocity.y = 0;
487
+ // ========== 向上射线(索引 1 = dwomToUpRay)==========
488
+ this._raycaster.set(this._worldRayOrigins[1], this._worldRayDirs[1]);
489
+ this._raycaster.far = upFar;
490
+ let upIntersets = this._raycaster.intersectObjects(this._colliders, false);
491
+ // 调试:记录向上射线
492
+ if (!debugRecorded) {
493
+ const hasHit = upIntersets.length > 0;
494
+ let intersetPoint = null;
495
+ if (hasHit) {
496
+ let closestDist = Infinity;
497
+ for (const int of upIntersets) {
498
+ const d = int.point.distanceTo(worldPos);
499
+ if (d < closestDist) {
500
+ closestDist = d;
501
+ intersetPoint = int.point;
502
+ }
407
503
  }
408
- // 计算需要下移的距离
409
- const adjustY = this.COLLIDE_VOLUME + 0.2 - hit.distance;
410
- if (adjustY > 0) {
411
- positionIncremental.y -= Math.min(adjustY, 0.01); // 限制每次调整量
412
- }
413
- hasCollision = true;
414
504
  }
505
+ devRayData[1] = {
506
+ origin: this._worldRayOrigins[1].clone(),
507
+ farPoint: hasHit
508
+ ? intersetPoint.clone()
509
+ : this._worldRayOrigins[1].clone().addScaledVector(this._worldRayDirs[1], upFar),
510
+ hit: hasHit,
511
+ };
512
+ debugRecorded = true;
415
513
  }
416
- if (recordDebug) {
417
- // 向上射线调试数据
418
- const upFar = new Vector3().copy(upRayOrigin).addScaledVector(this.VERTICAL_UP, this.COLLIDE_VOLUME + 0.2);
419
- const upHit = upHits.length > 0;
420
- devRayData.push({
421
- origin: upRayOrigin.clone(),
422
- farPoint: upHit ? upHits[0].point.clone() : upFar,
423
- hit: upHit,
424
- });
514
+ if (upIntersets.length > 0) {
515
+ let interset = upIntersets[0];
516
+ if (upIntersets.length > 1) {
517
+ for (let j = 1; j < upIntersets.length; j++) {
518
+ if (upIntersets[j].point.distanceTo(worldPos) < interset.point.distanceTo(worldPos)) {
519
+ interset = upIntersets[j];
520
+ }
521
+ }
522
+ }
523
+ this.playerVelocity.y = 0;
524
+ // 与 xsh: face.worldNormal.setLength(0.01) 一致
525
+ if (interset.face) {
526
+ const faceNormal = interset.face.normal.clone();
527
+ const worldQuat = new Quaternion();
528
+ interset.object.getWorldQuaternion(worldQuat);
529
+ faceNormal.applyQuaternion(worldQuat).setLength(0.01);
530
+ positionIncremental.add(faceNormal);
531
+ }
532
+ else {
533
+ positionIncremental.y -= 0.01;
534
+ }
535
+ goOnComputer = true;
425
536
  }
426
537
  }
427
538
  // ========== 更新调试射线(dev 模式)==========
428
539
  if (this.dev && devRayData) {
429
540
  this._initDebugLines();
430
- for (let i = 0; i < devRayData.length && i < 6; i++) {
431
- const data = devRayData[i];
432
- // 跳过未收集到的(可能迭代次数不够)
433
- this._updateDebugLine(i, data.origin, data.farPoint, data.hit);
541
+ for (let di = 0; di < 6; di++) {
542
+ const data = devRayData[di];
543
+ if (data) {
544
+ this._updateDebugLine(di, data.origin, data.farPoint, data.hit);
545
+ }
434
546
  }
435
547
  }
436
548
  // 应用推离
package/ThreeBase.d.ts CHANGED
@@ -78,13 +78,27 @@ export default class ThreeBase extends EventDispatcher {
78
78
  needRender: boolean;
79
79
  ssao: {
80
80
  enable: boolean;
81
+ debug: boolean;
81
82
  radius: number;
83
+ falloff: number;
82
84
  bias: number;
83
85
  intensity: number;
84
86
  sampleCount: number;
85
87
  blurRadius: number;
86
88
  strength: number;
87
89
  };
90
+ emissiveAO: {
91
+ enable: boolean;
92
+ debug: boolean;
93
+ radius: number;
94
+ falloff: number;
95
+ bias: number;
96
+ intensity: number;
97
+ sampleCount: number;
98
+ blurRadius: number;
99
+ strength: number;
100
+ };
101
+ emissiveRenderTarget: WebGLRenderTarget<Texture>;
88
102
  stopRender(): void;
89
103
  render(): void;
90
104
  private renderBindThis;
package/ThreeBase.js CHANGED
@@ -16,6 +16,7 @@ import frosted_glass_pass from "./pass/frosted_glass_pass";
16
16
  import glow_pass from "./pass/glow_pass";
17
17
  import stroke_pass from "./pass/stroke_pass";
18
18
  import ssao_pass from "./pass/ssao_pass";
19
+ import emissive_ao_pass from "./pass/emissive_ao_pass";
19
20
  import { copyShader } from "./shaders";
20
21
  import { baseTextureToRenderShader } from "./shaders/baseTextureToRenderShader";
21
22
  import threeLoader from "./threeLoader";
@@ -454,13 +455,33 @@ export default class ThreeBase extends EventDispatcher {
454
455
  // SSAO 配置
455
456
  ssao = {
456
457
  enable: false,
457
- radius: 0.5,
458
- bias: 0.025,
459
- intensity: 1.5,
460
- sampleCount: 16,
458
+ debug: false,
459
+ radius: 2.0, // 世界空间遮蔽半径
460
+ falloff: 5.0, // 世界空间衰减距离
461
+ bias: 0.3, // 切面距离阈值(过滤平坦面)
462
+ intensity: 1.0, // 遮蔽强度
463
+ sampleCount: 16, // 采样数
461
464
  blurRadius: 2.0,
462
465
  strength: 1.0,
463
466
  };
467
+ // Emissive AO 配置 - 发光物体照亮周围
468
+ emissiveAO = {
469
+ enable: false,
470
+ debug: false,
471
+ radius: 3.0, // 世界空间光照半径
472
+ falloff: 8.0, // 世界空间衰减距离
473
+ bias: 0.2, // 切面距离阈值
474
+ intensity: 1.0, // 光照强度
475
+ sampleCount: 16, // 采样数
476
+ blurRadius: 2.0,
477
+ strength: 1.0,
478
+ };
479
+ // 发光纹理渲染目标(用于存储发光物体的颜色)
480
+ emissiveRenderTarget = new WebGLRenderTarget(128, 128, {
481
+ minFilter: LinearFilter,
482
+ magFilter: LinearFilter,
483
+ format: RGBAFormat,
484
+ });
464
485
  // 停止渲染
465
486
  stopRender() {
466
487
  threeUtils.removeEventListener("requestAnimationFrame", this.renderBindThis);
@@ -530,6 +551,20 @@ export default class ThreeBase extends EventDispatcher {
530
551
  this.renderer.setRenderTarget(this.depthRenderTarget);
531
552
  this.renderer.clear();
532
553
  this.renderer.render(this.scene, camera);
554
+ // 如果启用 Emissive AO,渲染发光物体到专门的渲染目标
555
+ if (this.emissiveAO.enable) {
556
+ this.emissiveRenderTarget.setSize(this.size.x * Math.max(window.devicePixelRatio, this.minDevicePixelRatio), this.size.y * Math.max(window.devicePixelRatio, this.minDevicePixelRatio));
557
+ this.renderer.setRenderTarget(this.emissiveRenderTarget);
558
+ this.renderer.clear();
559
+ const emissiveScene = new Scene();
560
+ this.scene.traverseVisible((obj) => {
561
+ const material = obj.material;
562
+ if (material && material.emissive && material.emissiveIntensity > 0) {
563
+ emissiveScene.add(obj.clone());
564
+ }
565
+ });
566
+ this.renderer.render(emissiveScene, camera);
567
+ }
533
568
  // 切回 baseRenderTarget,后处理 pass 需要把效果叠加到颜色渲染目标上
534
569
  this.renderer.setRenderTarget(this.baseRenderTarget);
535
570
  // 后处理特效渲染(描边,发光,颜色覆盖,bloom,磨砂玻璃)
@@ -538,21 +573,29 @@ export default class ThreeBase extends EventDispatcher {
538
573
  glow_pass(this.renderer, camera, obj, this.baseDepthTexture, Math.max(window.devicePixelRatio, this.minDevicePixelRatio));
539
574
  bloom_pass(this.renderer, camera, obj, this.baseRenderTarget, this.baseDepthTexture, Math.max(window.devicePixelRatio, this.minDevicePixelRatio));
540
575
  });
576
+ // 渲染场景到屏幕
541
577
  this.renderer.setRenderTarget(null);
542
578
  this.renderer.clear();
543
579
  this.renderRendererTarget(this.baseRenderTarget);
544
- this.scene.traverseVisible((obj) => {
545
- cover_pass(this.renderer, camera, obj, this.baseDepthTexture, Math.max(window.devicePixelRatio, this.minDevicePixelRatio));
546
- });
547
- // 一次性处理所有支持毛玻璃特效的对象
548
- frosted_glass_pass(this.renderer, camera, this.scene, this.baseRenderTarget, this.baseDepthTexture, Math.max(window.devicePixelRatio, this.minDevicePixelRatio));
549
- // SSAO 放在最后,叠加在所有特效之上
580
+ // SSAO 后处理(基于深度计算遮蔽,乘法叠加到屏幕)
550
581
  if (this.ssao.enable) {
551
582
  ssao_pass(this.renderer, camera, this.baseDepthTexture, Math.max(window.devicePixelRatio, this.minDevicePixelRatio), {
552
583
  ...this.ssao,
553
584
  cameraFar: camera.far,
554
585
  });
555
586
  }
587
+ // Emissive AO 后处理(发光物体照亮周围)
588
+ if (this.emissiveAO.enable) {
589
+ emissive_ao_pass(this.renderer, camera, this.baseDepthTexture, this.emissiveRenderTarget.texture, Math.max(window.devicePixelRatio, this.minDevicePixelRatio), {
590
+ ...this.emissiveAO,
591
+ cameraFar: camera.far,
592
+ });
593
+ }
594
+ this.scene.traverseVisible((obj) => {
595
+ cover_pass(this.renderer, camera, obj, this.baseDepthTexture, Math.max(window.devicePixelRatio, this.minDevicePixelRatio));
596
+ });
597
+ // 一次性处理所有支持毛玻璃特效的对象
598
+ frosted_glass_pass(this.renderer, camera, this.scene, this.baseRenderTarget, this.baseDepthTexture, Math.max(window.devicePixelRatio, this.minDevicePixelRatio));
556
599
  this.dispatchEvent({ type: "afterRender" });
557
600
  }
558
601
  renderBindThis = this.render.bind(this);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-base",
3
- "version": "2.0.134",
3
+ "version": "2.0.137",
4
4
  "description": "three 3d three-base",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -45,6 +45,7 @@
45
45
  "vite": "*",
46
46
  "jszip": "^3.10.1",
47
47
  "html2canvas": "^1.4.1",
48
- "three-mesh-bvh": "^0.8.3"
48
+ "three-mesh-bvh": "^0.8.3",
49
+ "@zumer/snapdom": "^1.9.14"
49
50
  }
50
51
  }
@@ -0,0 +1,13 @@
1
+ import { Camera, DepthTexture, WebGLRenderer } from "three";
2
+ export default function emissive_ao_pass(renderer: WebGLRenderer, camera: Camera, depthTexture: DepthTexture, emissiveTexture: any, devicePixelRatio: number, options: {
3
+ enable: boolean;
4
+ debug?: boolean;
5
+ radius?: number;
6
+ falloff?: number;
7
+ bias?: number;
8
+ intensity?: number;
9
+ sampleCount?: number;
10
+ blurRadius?: number;
11
+ strength?: number;
12
+ cameraFar?: number;
13
+ }): void;