three-base 2.0.172 → 2.0.180

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.
Files changed (127) hide show
  1. package/FPScontrol.d.ts +15 -24
  2. package/FPScontrol.js +63 -87
  3. package/FPScontrol.ts +64 -88
  4. package/Ground1/Ground1.d.ts +4 -2
  5. package/Ground1/Ground1.js +77 -57
  6. package/Ground1/Ground1.ts +77 -57
  7. package/Mirror.js +0 -4
  8. package/Mirror.ts +0 -4
  9. package/MktMesh/MktMesh.d.ts +3 -2
  10. package/MktMesh/MktMesh.js +27 -8
  11. package/MktMesh/MktMesh.ts +24 -8
  12. package/MktMesh/boxLine2.d.ts +42 -0
  13. package/MktMesh/boxLine2.js +185 -0
  14. package/MktMesh/boxLine2.ts +222 -0
  15. package/MktMesh/imagePlane.d.ts +60 -0
  16. package/MktMesh/imagePlane.js +158 -0
  17. package/MktMesh/imagePlane.ts +203 -0
  18. package/MktMesh/imageTileLoader.d.ts +19 -0
  19. package/MktMesh/imageTileLoader.js +55 -0
  20. package/MktMesh/imageTileLoader.ts +64 -0
  21. package/MktMesh/mktMappingShader.d.ts +6 -5
  22. package/MktMesh/mktMappingShader.js +26 -19
  23. package/MktMesh/mktMappingShader.ts +28 -19
  24. package/MktMesh/mktShader.d.ts +127 -58
  25. package/MktMesh/mktShader.js +192 -117
  26. package/MktMesh/mktShader.ts +251 -127
  27. package/MktMesh/modelSlot.d.ts +123 -0
  28. package/MktMesh/modelSlot.js +339 -0
  29. package/MktMesh/modelSlot.ts +383 -0
  30. package/MktMesh/panoSlotRoster.d.ts +2 -2
  31. package/MktMesh/panoSlotRoster.js +9 -7
  32. package/MktMesh/panoSlotRoster.ts +9 -7
  33. package/MktMesh/panoTileStitcher.d.ts +20 -13
  34. package/MktMesh/panoTileStitcher.js +97 -76
  35. package/MktMesh/panoTileStitcher.ts +119 -90
  36. package/PipeMesh/PipeJoint.d.ts +22 -0
  37. package/PipeMesh/PipeJoint.js +33 -0
  38. package/PipeMesh/PipeJoint.ts +38 -0
  39. package/PipeMesh/PipeMesh.d.ts +94 -22
  40. package/PipeMesh/PipeMesh.js +192 -53
  41. package/PipeMesh/PipeMesh.ts +223 -76
  42. package/PipeMesh/PipeStraight.d.ts +19 -0
  43. package/PipeMesh/PipeStraight.js +34 -0
  44. package/PipeMesh/PipeStraight.ts +45 -0
  45. package/PipeMesh/axisSnap.d.ts +52 -0
  46. package/PipeMesh/axisSnap.js +52 -0
  47. package/PipeMesh/axisSnap.ts +93 -0
  48. package/PipeMesh/directRoute.d.ts +85 -0
  49. package/PipeMesh/directRoute.js +126 -0
  50. package/PipeMesh/directRoute.ts +190 -0
  51. package/PipeMesh/index.d.ts +6 -0
  52. package/PipeMesh/index.js +6 -0
  53. package/PipeMesh/index.ts +7 -1
  54. package/PipeMesh/pipeMaterial.d.ts +16 -0
  55. package/PipeMesh/pipeMaterial.js +23 -0
  56. package/PipeMesh/pipeMaterial.ts +26 -0
  57. package/ThreeBase.d.ts +1 -39
  58. package/ThreeBase.js +38 -147
  59. package/ThreeBase.ts +1176 -1302
  60. package/instanced/InstancedBatch.d.ts +14 -1
  61. package/instanced/InstancedBatch.js +53 -3
  62. package/instanced/InstancedBatch.ts +62 -3
  63. package/instanced/InstancedChild.d.ts +24 -0
  64. package/instanced/InstancedChild.js +40 -0
  65. package/instanced/InstancedChild.ts +41 -0
  66. package/instanced/InstancedGroup.d.ts +48 -0
  67. package/instanced/InstancedGroup.js +45 -1
  68. package/instanced/InstancedGroup.ts +85 -0
  69. package/package.json +22 -7
  70. package/pass/debugDepthState.d.ts +16 -8
  71. package/pass/debugDepthState.js +4 -0
  72. package/pass/debugDepthState.ts +27 -9
  73. package/pass/glow_pass.js +11 -94
  74. package/pass/glow_pass.ts +11 -97
  75. package/pass/ssao_pass.d.ts +9 -23
  76. package/pass/ssao_pass.js +41 -128
  77. package/pass/ssao_pass.ts +36 -166
  78. package/pass/stroke_pass.js +3 -0
  79. package/pass/stroke_pass.ts +4 -0
  80. package/shaders/debugDepthCompareShader.d.ts +1 -1
  81. package/shaders/debugDepthCompareShader.js +1 -1
  82. package/shaders/debugDepthCompareShader.ts +1 -1
  83. package/shaders/debugDepthJudgeShader.d.ts +1 -1
  84. package/shaders/debugDepthJudgeShader.js +1 -1
  85. package/shaders/debugDepthJudgeShader.ts +1 -1
  86. package/PipeMesh/PathGeometry.d.ts +0 -44
  87. package/PipeMesh/PathGeometry.js +0 -307
  88. package/PipeMesh/PathGeometry.ts +0 -343
  89. package/PipeMesh/PathPoint.d.ts +0 -17
  90. package/PipeMesh/PathPoint.js +0 -31
  91. package/PipeMesh/PathPoint.ts +0 -36
  92. package/PipeMesh/PathPointList.d.ts +0 -31
  93. package/PipeMesh/PathPointList.js +0 -209
  94. package/PipeMesh/PathPointList.ts +0 -227
  95. package/PipeMesh/PathTubeGeometry.d.ts +0 -25
  96. package/PipeMesh/PathTubeGeometry.js +0 -130
  97. package/PipeMesh/PathTubeGeometry.ts +0 -146
  98. package/WaterFlow/FloatingDuck.d.ts +0 -88
  99. package/WaterFlow/FloatingDuck.js +0 -307
  100. package/WaterFlow/FloatingDuck.ts +0 -445
  101. package/WaterFlow/GPUFluidSimulation.d.ts +0 -56
  102. package/WaterFlow/GPUFluidSimulation.js +0 -785
  103. package/WaterFlow/GPUFluidSimulation.ts +0 -827
  104. package/WaterFlow/ReservoirSystem.d.ts +0 -106
  105. package/WaterFlow/ReservoirSystem.js +0 -621
  106. package/WaterFlow/ReservoirSystem.ts +0 -754
  107. package/WaterFlow/SPHField.d.ts +0 -35
  108. package/WaterFlow/SPHField.js +0 -204
  109. package/WaterFlow/SPHField.ts +0 -228
  110. package/WaterFlow/ScreenSpaceFluidPass.d.ts +0 -69
  111. package/WaterFlow/ScreenSpaceFluidPass.js +0 -736
  112. package/WaterFlow/ScreenSpaceFluidPass.ts +0 -789
  113. package/WaterFlow/ShallowWaterSurface.d.ts +0 -45
  114. package/WaterFlow/ShallowWaterSurface.js +0 -441
  115. package/WaterFlow/ShallowWaterSurface.ts +0 -510
  116. package/WaterFlow/WaterFlow.d.ts +0 -87
  117. package/WaterFlow/WaterFlow.js +0 -171
  118. package/WaterFlow/WaterFlow.ts +0 -219
  119. package/WaterFlow/types.d.ts +0 -161
  120. package/WaterFlow/types.js +0 -1
  121. package/WaterFlow/types.ts +0 -189
  122. package/WaterFlow/waterFlowPass.d.ts +0 -3
  123. package/WaterFlow/waterFlowPass.js +0 -20
  124. package/WaterFlow/waterFlowPass.ts +0 -40
  125. package/shaders/debugExcludedShader.d.ts +0 -15
  126. package/shaders/debugExcludedShader.js +0 -48
  127. package/shaders/debugExcludedShader.ts +0 -50
package/FPScontrol.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Camera, Clock, EventDispatcher, Quaternion, Ray, Raycaster, Scene, Vector3 } from "three";
2
- import { Capsule } from "three/examples/jsm/math/Capsule";
3
2
  import { LineSegments2 } from "three/examples/jsm/lines/LineSegments2.js";
4
3
  import type { FPScontrolEventTypeMap } from "./EventMap";
5
4
  /** 全局 FPScontrol 实例注册表。
@@ -158,7 +157,7 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
158
157
  /** 是否允许跳跃(空格触发)。false 时按空格不跳。默认 true */
159
158
  jumpEnabled: boolean;
160
159
  /** 跳跃初速度(垂直方向,m/s)。
161
- * ⚠️ 2026-09-27 治「跳跃翻墙」:原 15(当时重力 30)→ 顶点 3.75m 远超墙高 2m,跳起后胶囊底越过墙顶
160
+ * ⚠️ 2026-09-27 治「跳跃翻墙」:原 15(当时重力 30)→ 顶点 3.75m 远超墙高 2m,跳起后视点越过墙顶
162
161
  * 造成水平失明窗口,人带着水平速度飞越墙平面、落地在墙外侧被越推越远(探针实测 z=-187)。
163
162
  * 定案 8 ⇒ 顶点 1.07m:仍可跳上 0.8m 高台,且 yBottom 恒低于「墙顶 2.0 − 步高 0.1」,
164
163
  * 与主循环竖直门配合后失明窗口彻底消失。**顶点**才是防翻墙的那条线,不是初速度本身。
@@ -284,14 +283,14 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
284
283
  * 即 R 就是「玩家中心离墙多远开始判定碰撞」的半径。若 R 小于停位需求,人必须**先陷进墙里**
285
284
  * 才会被射线检测到,随后每帧被位置修正弹回一点 → 皇上所见「侧撞弹很远」。
286
285
  * 探针实测(`tmp/TMP_R_side_effect.mjs`,斜 45° 撞墙角 / 正对单墙同口径):
287
- * R=0.30 → 停位垂距 **0.2128**(< 胶囊半径 0.35,**明显穿模**)每帧被推 0.040
286
+ * R=0.30 → 停位垂距 **0.2128**,射线包络过小,容易贴墙。
288
287
  * R=0.50 → 0.3541(贴到墙面) 每帧 0.036
289
- * R=0.60 → 0.4243(勉强 > 胶囊半径)0.034
288
+ * R=0.60 → 0.4243。
290
289
  * R=0.80 → 0.5448 ✓ / R=1.06 → 0.7247 ✓(每帧修正仅 0.002)
291
- * ⇒ 取 **0.6**:保证停位垂距 > 玩家胶囊半径(0.35),不再穿模。
290
+ * ⇒ 取 **0.6**:为射线包络保留足够的贴墙间距。
292
291
  * ⭐ 2026-10-08 皇上旨:下限放宽到 **0.2**、上限收到 **1.0**(编辑框范围 0.2~1.0)。
293
292
  * ⚠️ R<0.6 仍会贴墙穿模(R=0.5 贴到墙面、R=0.3 明显穿模),皇上按需拧;下限放开是给窄通道/贴墙演示留余地。
294
- * ⭐ 2026-10-11 A 档复核(tmp→test/fpsCollideFit.test.ts,正对 1m 墙、胶囊半径 0.35):
293
+ * ⭐ 2026-10-11 A 档复核(tmp→test/fpsCollideFit.test.ts,正对 1m 墙):
295
294
  * 停位现在**逐位等于 R**(眼停在面前 R 处 ⇒ 身体入墙深度 = 0.35 − R,一条直线):
296
295
  * R=0.20 → 入墙 **0.1500m** R=0.30 → **0.0500m** R=0.35 → **0.0000m**
297
296
  * R=0.50 / 0.60 / 1.00 → **0.0000m**(身体不出面)
@@ -345,12 +344,11 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
345
344
  private _horizHit;
346
345
  private _inputDirVec;
347
346
  private _previewRetreat;
348
- /** 开启预览那一刻的胶囊两端(关闭时整体还原回去) */
349
- private _obsSavedStart;
350
- private _obsSavedEnd;
347
+ /** 开启预览那一刻的玩家视点位置(关闭时还原) */
348
+ private _obsSavedPosition;
351
349
  /** 是否已有可还原的暂存(没进过观察模式就没有,避免构造期赋值把人还原到原点) */
352
350
  private _obsHasSave;
353
- /** 进观察模式(回弹预览 / 单步):记下当前胶囊位置,退出时还原 */
351
+ /** 进观察模式(回弹预览 / 单步):记下当前视点位置,退出时还原 */
354
352
  private _saveObsPos;
355
353
  /**
356
354
  * 退观察模式:把位置整份还原到进入那一刻,并清掉水平速度。
@@ -380,18 +378,6 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
380
378
  debugHideSideRays: boolean;
381
379
  private _tempEuler;
382
380
  MAX_ITERATIONS: number;
383
- /**
384
- * 一个**物理小帧**里 while 最多走几轮就落地(皇上 2026-10-07 裁决「加一个每小帧轮数上限」)。
385
- * 默认 500 = 与 `MAX_ITERATIONS` 同档 ⇒ 现状一字不变;拧小才是他要的「一次只修几格、下一小帧再修」。
386
- * ⭐ 为什么拧格长没用(10-07 实测):常态帧会一直迭代到「8 条射线全部脱开」才落地 ⇒
387
- * **每次落地量 = 这一小帧往墙里挤进多深**,与一格多远无关(格长 0.03→0.01 判级一字未变)。
388
- * 一格 = 一轮里每条命中各推的距离 ⇒ 要限制"一次修多少"只能限制**轮数**。
389
- * ⚠️ 代价(不许瞒):出水量 = 轮数 × 每轮票数 × 格长,默认每帧 2 个小帧。拧到 1 轮 + 1cm 格长 ⇒
390
- * 一秒最多挤出约 120cm 的量看着够,但高速冲刺顶墙时「这一帧挤进来的」可能比「两小帧推出去的」多
391
- * ⇒ 越陷越深,格长拧到 0.005 那一档体检台 40 号现场就是这么判 **❌穿墙** 的。
392
- * 所以默认不动,先让他在线上拧着看手感。
393
- */
394
- ROUNDS_PER_SUBSTEP: number;
395
381
  private _frameIterBudget;
396
382
  private _tempQuat;
397
383
  private _raycaster;
@@ -407,6 +393,10 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
407
393
  */
408
394
  private _fitNormal;
409
395
  private _horizNormal;
396
+ /** 本子步实际推离过的不同墙面法线。墙角需要同时裁掉两个朝内速度分量,
397
+ * 不能只用最后一次命中的面,否则两个面会在相邻帧交替抢占速度闸门而抖动。 */
398
+ private _horizContactNormals;
399
+ private _horizContactCount;
410
400
  private _fitVec;
411
401
  private _rhombusAxisX;
412
402
  private _rhombusAxisZ;
@@ -503,7 +493,8 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
503
493
  static markAllCollidersDirty(): void;
504
494
  /** 复位玩家到指定世界坐标(默认原点),清空速度;第一人称 demo 的"回到起点"用得到 */
505
495
  resetPlayer(position?: Vector3): void;
506
- playerCollider: Capsule;
496
+ /** 玩家视点。所有碰撞射线均以此点为局部原点。 */
497
+ playerPosition: Vector3;
507
498
  clock: Clock;
508
499
  prePlayerVelocity: Vector3;
509
500
  playerVelocity: Vector3;
@@ -530,7 +521,7 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
530
521
  * `depthWrite:true`(three-base/Ground1/Ground1.ts:107);哪天地板改回
531
522
  * transparent + depthWrite:false,射线线会重新穿地浮出来。
532
523
  * - 回弹方向线(黄→白)= **false**:「回弹向量线保持穿透显示」。
533
- * 它是「本帧要看的向量」,起点在胶囊内部、方向常指着墙里,开遮挡会被自己的身子
524
+ * 它是「本帧要看的向量」,起点在玩家位置、方向常指着墙里,开遮挡会被墙面
534
525
  * 或墙面吃掉半截 → 观察目的直接落空,故保持常显在最前。
535
526
  */
536
527
  private _createDebugLine;
package/FPScontrol.js CHANGED
@@ -6,11 +6,9 @@
6
6
  */
7
7
  import { Clock, Euler, EventDispatcher, Group, InstancedMesh, Matrix3, Matrix4, Mesh, Quaternion, Ray, Raycaster, Vector3 } from "three";
8
8
  import { MathUtils } from "three";
9
- // (2026-09-28 皇上旨「完完全全使用 xsh-react-three 的碰撞计算方法」:胶囊兜底整条拆除。
10
- // collisionOBB.ts 纯几何模块与其单测保留,但 FPScontrol 不再引用。)
9
+ // 碰撞完全由围绕玩家视点的射线完成;FPScontrol 不使用任何体积碰撞体。
11
10
  const { degToRad, clamp } = MathUtils;
12
11
  // 「矩形射线法」的水平 far 口径:斜弦画长 = R√2(四弦首尾闭合的唯一解,勿再改回)。
13
- import { Capsule } from "three/examples/jsm/math/Capsule";
14
12
  // 粗辅助线:WebGL 下 `LineBasicMaterial.linewidth` 恒被驱动钳成 1px(Chrome/ANGLE 一律 1px,
15
13
  // 调大无效),要真的变粗必须走 `LineSegments2 + LineMaterial`(shader 里按像素铺quad)。
16
14
  // 本仓库先例:src/views/home/homeRender.ts:71-73, 344-358。
@@ -212,7 +210,7 @@ export default class FPScontrol extends EventDispatcher {
212
210
  /** 是否允许跳跃(空格触发)。false 时按空格不跳。默认 true */
213
211
  jumpEnabled = true;
214
212
  /** 跳跃初速度(垂直方向,m/s)。
215
- * ⚠️ 2026-09-27 治「跳跃翻墙」:原 15(当时重力 30)→ 顶点 3.75m 远超墙高 2m,跳起后胶囊底越过墙顶
213
+ * ⚠️ 2026-09-27 治「跳跃翻墙」:原 15(当时重力 30)→ 顶点 3.75m 远超墙高 2m,跳起后视点越过墙顶
216
214
  * 造成水平失明窗口,人带着水平速度飞越墙平面、落地在墙外侧被越推越远(探针实测 z=-187)。
217
215
  * 定案 8 ⇒ 顶点 1.07m:仍可跳上 0.8m 高台,且 yBottom 恒低于「墙顶 2.0 − 步高 0.1」,
218
216
  * 与主循环竖直门配合后失明窗口彻底消失。**顶点**才是防翻墙的那条线,不是初速度本身。
@@ -260,7 +258,7 @@ export default class FPScontrol extends EventDispatcher {
260
258
  // 位置修正沿该面法线一格一格推,每格 = min(RETREAT_SKIN, deficit) ⇒ 大欠距仍走整格,
261
259
  // **收尾那一格按欠距截短** ⇒ 落点正好贴在包络边上(旧写法最后一格也推满 1cm → 过冲 → 来回甩)。
262
260
  // 速度只削「往墙里」那一份(v −(v·n)n),切向保留 ⇒ 贴墙能滑,撞墙不再「加速→清零」锯齿。
263
- // ⇒ 停位仍只由 **R** 一个数决定(R=1.06 → 沿法线 0.749~1.06m);胶囊半径 0.35 ⇒ 身体与墙之间
261
+ // ⇒ 停位仍只由 **R** 一个数决定(R=1.06 → 沿法线 0.749~1.06m);射线包络决定视点与墙之间
264
262
  // 还留 0.40~0.71m 空气隙,这是皇上 2026-09-06 终裁布局自带、且亲眼验收过的量,不要当 bug 修。
265
263
  /**
266
264
  * 单步最大位移(连续碰撞检测用):每帧位移会被切成若干 ≤ 本值的小步,逐步做碰撞检测。
@@ -273,8 +271,8 @@ export default class FPScontrol extends EventDispatcher {
273
271
  MAX_STEP_DISTANCE = 0.1;
274
272
  /** 位移细分的最大段数(防止极端速度/长卡顿把一帧切成几百步拖垮帧率) */
275
273
  MAX_STEP_COUNT = 32;
276
- // 垂直碰撞范围 = 从 playerCollider.end(眼睛)到「该射线所保护的那一面」的距离
277
- // = capsule length + radius = (1.0 - 0.35) + 0.35 = 1.0(几何真值;下方两个常量是**口径**,不是几何)
274
+ // 垂直碰撞范围 = 从玩家视点到「该射线所保护的那一面」的距离
275
+ // = 视点到脚下射线保护面的距离 1.0(下方两个常量是**口径**,不是几何)
278
276
  // 对应 xsh-react-three 中 collideVolume(=3)= 控制器到地面的距离
279
277
  // xsh 中 far = collideVolume, 响应 = collideVolume - distance
280
278
  // 此处 far = DOWN_RANGE/UP_RANGE + SKIN,响应 = DOWN_RANGE/UP_RANGE − distance
@@ -283,9 +281,9 @@ export default class FPScontrol extends EventDispatcher {
283
281
  // 我提醒过这一条常量身兼两职(探测长度 + 站位基准),他追选定案「**直接改站位基准**」,
284
282
  // 所以拆成两条各自独立的常量,**不再**共用 VERTICAL_RANGE=1.0:
285
283
  // · DOWN_RANGE = 1.5:下射线 far = 1.5+SKIN,响应 downPen = 1.5 − distance
286
- // → 落地后**眼睛离地 1.5m**。胶囊脚底在眼下 1.0m ⇒ 站定时脚底离地 **0.5m(悬浮)**。
284
+ // → 落地后**眼睛离地 1.5m**。
287
285
  // · UP_RANGE = 0.3:上射线 far = 0.3+SKIN,响应 upPen = 0.3 − distance
288
- // → 头顶到面时**眼睛离顶 0.3m**。胶囊顶在眼下 0.35m ⇒ 真顶到矮面时头顶穿入面内 **0.05m**,
286
+ // → 头顶到面时**眼睛离顶 0.3m**,由上射线阻止继续上移。
289
287
  // 且 0.3~1.0m 之间的低矮障碍物从此探不到(上射线变短的直接后果)。
290
288
  // ⚠️ 跳跃/重力/台阶吸附的其余口径一字未动,只换这两个基准数。
291
289
  // ⭐ 2026-09-26 皇上旨「能调节的参数都做滚轮输入框」→ 两条改为公共可写(面板「参数旋钮」可拧),
@@ -400,14 +398,14 @@ export default class FPScontrol extends EventDispatcher {
400
398
  * 即 R 就是「玩家中心离墙多远开始判定碰撞」的半径。若 R 小于停位需求,人必须**先陷进墙里**
401
399
  * 才会被射线检测到,随后每帧被位置修正弹回一点 → 皇上所见「侧撞弹很远」。
402
400
  * 探针实测(`tmp/TMP_R_side_effect.mjs`,斜 45° 撞墙角 / 正对单墙同口径):
403
- * R=0.30 → 停位垂距 **0.2128**(< 胶囊半径 0.35,**明显穿模**)每帧被推 0.040
401
+ * R=0.30 → 停位垂距 **0.2128**,射线包络过小,容易贴墙。
404
402
  * R=0.50 → 0.3541(贴到墙面) 每帧 0.036
405
- * R=0.60 → 0.4243(勉强 > 胶囊半径)0.034
403
+ * R=0.60 → 0.4243。
406
404
  * R=0.80 → 0.5448 ✓ / R=1.06 → 0.7247 ✓(每帧修正仅 0.002)
407
- * ⇒ 取 **0.6**:保证停位垂距 > 玩家胶囊半径(0.35),不再穿模。
405
+ * ⇒ 取 **0.6**:为射线包络保留足够的贴墙间距。
408
406
  * ⭐ 2026-10-08 皇上旨:下限放宽到 **0.2**、上限收到 **1.0**(编辑框范围 0.2~1.0)。
409
407
  * ⚠️ R<0.6 仍会贴墙穿模(R=0.5 贴到墙面、R=0.3 明显穿模),皇上按需拧;下限放开是给窄通道/贴墙演示留余地。
410
- * ⭐ 2026-10-11 A 档复核(tmp→test/fpsCollideFit.test.ts,正对 1m 墙、胶囊半径 0.35):
408
+ * ⭐ 2026-10-11 A 档复核(tmp→test/fpsCollideFit.test.ts,正对 1m 墙):
411
409
  * 停位现在**逐位等于 R**(眼停在面前 R 处 ⇒ 身体入墙深度 = 0.35 − R,一条直线):
412
410
  * R=0.20 → 入墙 **0.1500m** R=0.30 → **0.0500m** R=0.35 → **0.0000m**
413
411
  * R=0.50 / 0.60 / 1.00 → **0.0000m**(身体不出面)
@@ -593,19 +591,6 @@ export default class FPScontrol extends EventDispatcher {
593
591
  desc: "⭐ 2026-10-11 皇上 A 档改判:每轮**只沿「欠距最大那一面」的法线**推一格,且这一格取 min(本值, 该面欠距) ⇒ 大欠距仍是一格一格往外走(皇上 10-07「1 个单位 1 个单位的修正」),只有**收尾那一格按欠距截短**,落点贴在包络边上(旧写法每条命中射线各推满一格、最后一格也推满 ⇒ 过冲 ⇒ 贴墙来回甩 = 抖动)。" +
594
592
  "⚠️ 上限以下别贪小:0.03/0.02/0.01 体检台判级完全相同,拧到 0.005 才有改善但「跳跃+冲刺+掉帧」那个现场开始穿墙(格长 = 每帧出水量)。迭代推到全部射线脱开为止",
595
593
  },
596
- {
597
- group: "回弹与手感",
598
- label: "每小帧轮数上限",
599
- key: "ROUNDS_PER_SUBSTEP",
600
- min: 1,
601
- max: 500,
602
- step: 1,
603
- digits: 0,
604
- unit: "轮",
605
- desc: "⭐ 皇上 2026-10-07 裁决加:一个物理小帧里 while 最多走几轮就落地(默认 500 = 与「每帧迭代预算」同档 ⇒ 现状一字不变)。" +
606
- "**这颗才是「一次修多少」的真闸门**:10-07 实测每次落地量 = 这一小帧往墙里挤进多深(2/8/10cm 都这么来),跟一格多远无关 ⇒ 要「一次只修几格、下一小帧再修」只能拧这里。" +
607
- "⚠️ 拧小的代价:一秒能挤出去的总量变小,高速冲刺顶墙时「挤进来的比推出去的快」⇒ 越陷越深(格长拧到 0.005 那档体检台 40 号现场就判 ❌穿墙,同一个机理)。先拧着看手感,别把默认改小",
608
- },
609
594
  // ===== 移动与视角 =====
610
595
  {
611
596
  group: "移动与视角",
@@ -776,7 +761,7 @@ export default class FPScontrol extends EventDispatcher {
776
761
  // ⚠️ 只闸水平+竖向的**位置写入**;削速度、playerOnFloor、事件派发全部保留,
777
762
  // 否则预览模式下重力会一帧帧攒着把人送到地心并触发掉出世界复位。
778
763
  // ⭐⭐ 2026-09-26 皇上七旨「回弹预览开了, 就关不掉」⇒ 本字段改成**带暂存的访问器**:
779
- // 开 = 记下当前胶囊位置;关 = **还原**那个位置 + 清掉水平速度。
764
+ // 开 = 记下当前视点位置;关 = **还原**那个位置 + 清掉水平速度。
780
765
  // 病根(实测,墙面板块 x∈[5.0,6.0]、R=1.06):预览既然不写位置,人就照着自己的
781
766
  // 行走速度(speed=6 ⇒ 终端 37.5m/s ⇒ 每帧 0.62m)一路**穿墙走出去**,120 帧后中心
782
767
  // 已经在 x=138;这一路上只要停在任何一处「两面墙的票互相抵消」的几何里,位置修正就
@@ -784,15 +769,13 @@ export default class FPScontrol extends EventDispatcher {
784
769
  // deficit 恒为 0.5600、位移 0。看着就是「预览关了位置修正也不回来了」。
785
770
  // ⇒ 预览是**观察**用的,那一段时间里的位移本来就不作数,关掉即回到打开前那一刻。
786
771
  _previewRetreat = false;
787
- /** 开启预览那一刻的胶囊两端(关闭时整体还原回去) */
788
- _obsSavedStart = new Vector3();
789
- _obsSavedEnd = new Vector3();
772
+ /** 开启预览那一刻的玩家视点位置(关闭时还原) */
773
+ _obsSavedPosition = new Vector3();
790
774
  /** 是否已有可还原的暂存(没进过观察模式就没有,避免构造期赋值把人还原到原点) */
791
775
  _obsHasSave = false;
792
- /** 进观察模式(回弹预览 / 单步):记下当前胶囊位置,退出时还原 */
776
+ /** 进观察模式(回弹预览 / 单步):记下当前视点位置,退出时还原 */
793
777
  _saveObsPos() {
794
- this._obsSavedStart.copy(this.playerCollider.start);
795
- this._obsSavedEnd.copy(this.playerCollider.end);
778
+ this._obsSavedPosition.copy(this.playerPosition);
796
779
  this._obsHasSave = true;
797
780
  }
798
781
  /**
@@ -803,9 +786,8 @@ export default class FPScontrol extends EventDispatcher {
803
786
  _restoreObsPos() {
804
787
  if (!this._obsHasSave)
805
788
  return;
806
- this.playerCollider.start.copy(this._obsSavedStart);
807
- this.playerCollider.end.copy(this._obsSavedEnd);
808
- this.camera.position.copy(this.playerCollider.end);
789
+ this.playerPosition.copy(this._obsSavedPosition);
790
+ this.camera.position.copy(this.playerPosition);
809
791
  this.playerVelocity.x = 0;
810
792
  this.playerVelocity.z = 0;
811
793
  this._obsHasSave = false;
@@ -826,8 +808,7 @@ export default class FPScontrol extends EventDispatcher {
826
808
  // xsh 的骨架只有一个 while(`FPSControls.tsx` 的 `computerPosition`):
827
809
  // ① 在**候选位置**(真实位置 + 已累计的 positionIncremental)上打水平射线
828
810
  // ② 有命中 ⇒ 沿「离开命中点」的方向推**一小格**(它写死 0.03)并续跑下一轮
829
- // ③ 一轮下来没命中 ⇒ goOnComputer=false ⇒ 收工;整圈上限 = MAX_ITERATIONS(皇上 10-03 旨:500)轮,
830
- // 单趟 while 另有「本轮数上限」闸门(常态帧 = ROUNDS_PER_SUBSTEP,皇上 10-07 裁决加,默认 500 同档 ⇒ 不变)
811
+ // ③ 一轮下来没命中 ⇒ goOnComputer=false ⇒ 收工;整圈上限 = MAX_ITERATIONS(皇上 10-03 旨:500)轮
831
812
  // ④ 循环外**唯一一次** position.add(incremental)
832
813
  // ⇒ 旧的「测试法逐级逼近」(六旨:1 → 0.1 → 0.01 三档、武装/判定/接受/撤销、每帧出水量 ≥
833
814
  // 进水量那套配套保险丝)**整条删除**。位置仍是一格一格挪出去的(皇上要的一步一步看那个形态不变),
@@ -853,18 +834,6 @@ export default class FPScontrol extends EventDispatcher {
853
834
  _tempEuler = new Euler(0, 0, 0, "YXZ");
854
835
  // 最大迭代次数(皇上 2026-10-03 旨:「那计算次数改成不超过 500」⇒ 100 → 500)
855
836
  MAX_ITERATIONS = 500;
856
- /**
857
- * 一个**物理小帧**里 while 最多走几轮就落地(皇上 2026-10-07 裁决「加一个每小帧轮数上限」)。
858
- * 默认 500 = 与 `MAX_ITERATIONS` 同档 ⇒ 现状一字不变;拧小才是他要的「一次只修几格、下一小帧再修」。
859
- * ⭐ 为什么拧格长没用(10-07 实测):常态帧会一直迭代到「8 条射线全部脱开」才落地 ⇒
860
- * **每次落地量 = 这一小帧往墙里挤进多深**,与一格多远无关(格长 0.03→0.01 判级一字未变)。
861
- * 一格 = 一轮里每条命中各推的距离 ⇒ 要限制"一次修多少"只能限制**轮数**。
862
- * ⚠️ 代价(不许瞒):出水量 = 轮数 × 每轮票数 × 格长,默认每帧 2 个小帧。拧到 1 轮 + 1cm 格长 ⇒
863
- * 一秒最多挤出约 120cm 的量看着够,但高速冲刺顶墙时「这一帧挤进来的」可能比「两小帧推出去的」多
864
- * ⇒ 越陷越深,格长拧到 0.005 那一档体检台 40 号现场就是这么判 **❌穿墙** 的。
865
- * 所以默认不动,先让他在线上拧着看手感。
866
- */
867
- ROUNDS_PER_SUBSTEP = 500;
868
837
  // ⚠️ 每帧碰撞迭代总预算(皇上 2026-09-03:墙角偶尔卡住,要求碰撞计算次数不超过 100;
869
838
  // 2026-10-03 同一位皇上把这条上限抬到 500 —— 机制一字未改,只是额度变大):
870
839
  // while 循环原本的上限 100 是「单次 playerCollisions」的,而一个 update() 帧内会调
@@ -892,6 +861,10 @@ export default class FPScontrol extends EventDispatcher {
892
861
  */
893
862
  _fitNormal = new Vector3();
894
863
  _horizNormal = new Vector3();
864
+ /** 本子步实际推离过的不同墙面法线。墙角需要同时裁掉两个朝内速度分量,
865
+ * 不能只用最后一次命中的面,否则两个面会在相邻帧交替抢占速度闸门而抖动。 */
866
+ _horizContactNormals = [new Vector3(), new Vector3(), new Vector3(), new Vector3()];
867
+ _horizContactCount = 0;
895
868
  _fitVec = new Vector3();
896
869
  _rhombusAxisX = new Vector3(1, 0, 0);
897
870
  _rhombusAxisZ = new Vector3(0, 0, 1);
@@ -1190,14 +1163,12 @@ export default class FPScontrol extends EventDispatcher {
1190
1163
  resetPlayer(position = new Vector3(0, 0, 0)) {
1191
1164
  this.playerVelocity.set(0, 0, 0);
1192
1165
  this.prePlayerVelocity.set(0, 0, 0);
1193
- this.playerCollider.start.set(position.x, position.y + 0.35, position.z);
1194
- this.playerCollider.end.set(position.x, position.y + 1, position.z);
1195
- this.playerCollider.radius = 0.35;
1196
- this.camera.position.copy(this.playerCollider.end);
1197
- // ⭐ 复位 = 换现场 ⇒ 把相机/胶囊位置基准挪到新位置。
1166
+ this.playerPosition.set(position.x, position.y + 1, position.z);
1167
+ this.camera.position.copy(this.playerPosition);
1168
+ // ⭐ 复位 = 换现场 ⇒ 把相机/视点位置基准挪到新位置。
1198
1169
  }
1199
- // 玩家胶囊碰撞体
1200
- playerCollider = new Capsule(new Vector3(0, 0.35, 0), new Vector3(0, 1, 0), 0.35);
1170
+ /** 玩家视点。所有碰撞射线均以此点为局部原点。 */
1171
+ playerPosition = new Vector3(0, 1, 0);
1201
1172
  // 时钟对象
1202
1173
  clock = new Clock();
1203
1174
  prePlayerVelocity = new Vector3();
@@ -1242,7 +1213,7 @@ export default class FPScontrol extends EventDispatcher {
1242
1213
  * `depthWrite:true`(three-base/Ground1/Ground1.ts:107);哪天地板改回
1243
1214
  * transparent + depthWrite:false,射线线会重新穿地浮出来。
1244
1215
  * - 回弹方向线(黄→白)= **false**:「回弹向量线保持穿透显示」。
1245
- * 它是「本帧要看的向量」,起点在胶囊内部、方向常指着墙里,开遮挡会被自己的身子
1216
+ * 它是「本帧要看的向量」,起点在玩家位置、方向常指着墙里,开遮挡会被墙面
1246
1217
  * 或墙面吃掉半截 → 观察目的直接落空,故保持常显在最前。
1247
1218
  */
1248
1219
  _createDebugLine(depthTest) {
@@ -1312,7 +1283,7 @@ export default class FPScontrol extends EventDispatcher {
1312
1283
  this._debugRayGroup = null;
1313
1284
  }
1314
1285
  init(canvas) {
1315
- this.playerCollider.end.set(0, 1, 0);
1286
+ this.playerPosition.set(0, 1, 0);
1316
1287
  document.addEventListener("keydown", this.keydown);
1317
1288
  document.addEventListener("keyup", this.keyup);
1318
1289
  canvas.addEventListener("pointerdown", this.mousedown);
@@ -1460,7 +1431,7 @@ export default class FPScontrol extends EventDispatcher {
1460
1431
  this.playerCollisions(s === stepCount - 1);
1461
1432
  }
1462
1433
  // 设置相机位置(第一人称默认值)
1463
- this.camera.position.copy(this.playerCollider.end);
1434
+ this.camera.position.copy(this.playerPosition);
1464
1435
  // 第三人称:偏移相机位置 + 朝向第一人称坐标
1465
1436
  if (this.thirdPerson) {
1466
1437
  const cosPitch = Math.cos(this._thirdPersonAngleX);
@@ -1475,7 +1446,7 @@ export default class FPScontrol extends EventDispatcher {
1475
1446
  Math.cos(this._thirdPersonAngleY) *
1476
1447
  cosPitch *
1477
1448
  this.thirdPersonDistance;
1478
- this.camera.lookAt(this.playerCollider.end);
1449
+ this.camera.lookAt(this.playerPosition);
1479
1450
  }
1480
1451
  // dev 调试模式:在相机本地坐标系叠加偏移(向后上方)
1481
1452
  if (this.dev && !this.thirdPerson) {
@@ -1494,7 +1465,7 @@ export default class FPScontrol extends EventDispatcher {
1494
1465
  // xsh: direction prop → _localRayDirs(归一化)
1495
1466
  // xsh: raycaster.far = getDirection().length() → HORIZONTAL_FAR / downFar / upFar
1496
1467
  // xsh: Group3D rotation={x:-euler.x} → yawQuat(pitch 补偿)
1497
- // xsh: getIntersetResult(positionIncremental) → worldPos = playerCollider.end + positionIncremental
1468
+ // xsh: getIntersetResult(positionIncremental) → worldPos = playerPosition + positionIncremental
1498
1469
  //
1499
1470
  // 现行 10 条布局逐条表 = 上方「★ 矩形射线法 ★」权威规格块(唯一数据源,不在这里重复一遍)。
1500
1471
  /** 输出一条碰撞日志(唯一出口 = 浏览器控制台;皇上:「日志就是浏览器的控制台」) */
@@ -1530,18 +1501,16 @@ export default class FPScontrol extends EventDispatcher {
1530
1501
  // · 竖向保留 faceUp / 天花板三道闸守卫(「掉出世界」血案修复)。
1531
1502
  // · ⭐ 2026-10-11 A 档:**只沿「欠距最大那一面」的法线**推,且最后一格按欠距截短
1532
1503
  // (xsh 是每条命中各推一整格 ⇒ 4 条命中同一面墙一轮推 4cm,退出时必过冲)。
1533
- // 原 deficit / 票面合成(finalDir) / 逐格推离 / 白推保险丝 / 胶囊兜底机器整条拆除。
1504
+ // 原 deficit / 票面合成(finalDir) / 逐格推离 / 白推保险丝等旧分支已拆除。
1534
1505
  // ⚠️ 措辞校正(10-11):上面这句里「deficit」那半已随 A 档**回归**(`_horizFitDeficit()`:
1535
1506
  // 沿命中面法线的菱形可达距离 R·max(|n·x̂|,|n·ẑ|) 减当前垂距);
1536
- // 仍拆除的是「票面合成 finalDir / 白推保险丝 / 胶囊兜底」三件 —— 别照旧文案把它们捡回来。
1507
+ // 仍拆除的是「票面合成 finalDir / 白推保险丝」两件 —— 别照旧文案把它们捡回来。
1537
1508
  const positionIncremental = this._tempVec4.set(0, 0, 0);
1538
1509
  this._refreshInputDir(); // 台账 ix/iz 列还要用(与碰撞计算无关)
1539
1510
  const recordDebug = (this.dev || this.thirdPerson) && isFinalStep;
1540
1511
  let debugRecorded = !recordDebug;
1541
1512
  this._horizHit = false;
1542
- // ⭐ 皇上 2026-10-07 裁决「加一个每小帧轮数上限」:这一趟 while 最多走几轮就落地。
1543
- // 常态帧用旋钮 ROUNDS_PER_SUBSTEP(默认 500 与 MAX_ITERATIONS 同档 ⇒ 现状完全不变)。
1544
- const roundCap = this.ROUNDS_PER_SUBSTEP;
1513
+ this._horizContactCount = 0;
1545
1514
  const downRange = this.DOWN_RANGE;
1546
1515
  const upRange = this.UP_RANGE;
1547
1516
  let i = 0;
@@ -1556,7 +1525,7 @@ export default class FPScontrol extends EventDispatcher {
1556
1525
  // ⭐ 2026-10-02 模拟修正:真实 collider 此刻**还没被本步位移推过**,
1557
1526
  // 所以锚点 = 真实位置 + 本步位移 =「这一步走完会站在哪儿」的**模拟位置**
1558
1527
  // (数值上与旧写法「先 translate 再取 end」完全相同 ⇒ 几何、方向、测试基线都不变)。
1559
- const basePos = this._tempVec3.copy(this.playerCollider.end).add(this._subStep);
1528
+ const basePos = this._tempVec3.copy(this.playerPosition).add(this._subStep);
1560
1529
  // xsh: 射线原点 = 控制器位置 + positionIncremental(getIntersetResult(incremental) 语义)
1561
1530
  const worldPos = this._candPos.copy(basePos).add(positionIncremental);
1562
1531
  // xsh: Group3D rotation={x:-euler.x} 补偿 pitch → 只跟随 yaw
@@ -1615,18 +1584,21 @@ export default class FPScontrol extends EventDispatcher {
1615
1584
  }
1616
1585
  }
1617
1586
  if (bestDeficit > 1e-6) {
1618
- if (i > roundCap) {
1619
- // ⭐ **轮次上限**用完 ⇒ 这一轮起水平一格都不再推。
1620
- // 常态帧 = 皇上 10-07 点头新加的 ROUNDS_PER_SUBSTEP(默认 500 ⇒ 与旧行为同形)。
1621
- // ⚠️ 只拦水平:竖向(踩地/顶头)仍照常,否则人会往地心沉(皇上 09-25 冻结口径)。
1587
+ // 大欠距逐格推离,直到所有水平射线都脱离碰撞面。
1588
+ positionIncremental.addScaledVector(this._horizNormal, Math.min(this.RETREAT_SKIN, bestDeficit));
1589
+ // 一面墙在多轮迭代里会重复命中;只保留方向不同的接触面。
1590
+ let known = false;
1591
+ for (let ni = 0; ni < this._horizContactCount; ni++) {
1592
+ if (this._horizContactNormals[ni].dot(this._horizNormal) > 0.999) {
1593
+ known = true;
1594
+ break;
1595
+ }
1622
1596
  }
1623
- else {
1624
- // ⭐ 皇上 10-07 旨「每执行一次,偏移量是多少」⇒ 一格一行、带累计的形态保留:
1625
- // 大欠距仍是一格一格往外走,只有收尾那一格按欠距截短。
1626
- positionIncremental.addScaledVector(this._horizNormal, Math.min(this.RETREAT_SKIN, bestDeficit));
1627
- goOnComputer = true;
1628
- this._horizHit = true;
1597
+ if (!known && this._horizContactCount < this._horizContactNormals.length) {
1598
+ this._horizContactNormals[this._horizContactCount++].copy(this._horizNormal);
1629
1599
  }
1600
+ goOnComputer = true;
1601
+ this._horizHit = true;
1630
1602
  }
1631
1603
  // ========== 向下射线(索引 0 = upTodwomRay)==========
1632
1604
  const downRay = this._rays[0];
@@ -1739,7 +1711,7 @@ export default class FPScontrol extends EventDispatcher {
1739
1711
  // 第 11 帧眼落到板底之下 0.0135,上射线打到**板底(法线朝下)**,旧口径
1740
1712
  // `positionIncremental.y -= upPen` 一帧把人往下砸 **0.9865m** → 眼落到 −2.0,
1741
1713
  // 从此两条竖射线彻底够不着 → 自由落体到 **−13.161547** → 撞上 camera.y≤−10 的
1742
- // 掉出世界复位(`this.playerCollider.end.set(0, 1, 10)`)→ 「陷进地面很长一段
1714
+ // 掉出世界复位(玩家视点重置到 (0, 1, 10))→ 「陷进地面很长一段
1743
1715
  // 距离然后凭空回到原点」。这就是皇上看到的现象。
1744
1716
  // 新口径:**绝不往下顶**,一步顶到「脚底刚好落在这块面之上」(眼高 = 面 y +
1745
1717
  // downRange + SKIN)。顶完眼只比板底高一个 SKIN → 下一轮迭代下射线立刻
@@ -1833,7 +1805,7 @@ export default class FPScontrol extends EventDispatcher {
1833
1805
  // 位移永远照走(previewRetreat 只关「修正」那份,不关位移)。
1834
1806
  this._appliedMove.set(this._subStep.x + corrX, this._subStep.y + corrY, this._subStep.z + corrZ);
1835
1807
  if (this._appliedMove.lengthSq() > 0) {
1836
- this.playerCollider.translate(this._appliedMove);
1808
+ this.playerPosition.add(this._appliedMove);
1837
1809
  }
1838
1810
  // 本步已消费:归零,面板那两颗直接调 playerCollisions 的按钮不会重复走一步
1839
1811
  this._subStep.set(0, 0, 0);
@@ -1849,9 +1821,15 @@ export default class FPScontrol extends EventDispatcher {
1849
1821
  // ⚠️ 方向与推离用的是**同一个** `_horizNormal`(水平半区那轮算好、就地复用),绝不另算一遍。
1850
1822
  // ⚠️ 竖向 y 一字不动(重力/跳跃照旧);观察模式(回弹预览)不改速度,口径与 10-07 一致。
1851
1823
  if (this._horizHit && !this.previewRetreat) {
1852
- const into = this.playerVelocity.dot(this._horizNormal);
1853
- if (into < 0) {
1854
- this.playerVelocity.addScaledVector(this._horizNormal, -into);
1824
+ // 墙角中两个约束面不一定在同一轮成为“欠距最大面”。依次投影两轮,
1825
+ // 让每个面都得到 v·n >= 0,同时保留不受任何墙面约束的切向速度。
1826
+ for (let pass = 0; pass < 2; pass++) {
1827
+ for (let ni = 0; ni < this._horizContactCount; ni++) {
1828
+ const normal = this._horizContactNormals[ni];
1829
+ const into = this.playerVelocity.dot(normal);
1830
+ if (into < 0)
1831
+ this.playerVelocity.addScaledVector(normal, -into);
1832
+ }
1855
1833
  }
1856
1834
  }
1857
1835
  // ⭐ 十旨:一帧一条的抖动台账。**只由子步循环的最后一步(isFinalStep)记** ——
@@ -1877,10 +1855,8 @@ export default class FPScontrol extends EventDispatcher {
1877
1855
  // 越界传送
1878
1856
  teleportPlayerIfOob = () => {
1879
1857
  if (this.camera.position.y <= -10) {
1880
- this.playerCollider.start.set(0, 0.35, 0);
1881
- this.playerCollider.end.set(0, 1, 10);
1882
- this.playerCollider.radius = 0.35;
1883
- this.camera.position.copy(this.playerCollider.end);
1858
+ this.playerPosition.set(0, 1, 10);
1859
+ this.camera.position.copy(this.playerPosition);
1884
1860
  }
1885
1861
  };
1886
1862
  // 更新