three-base 2.0.169 → 2.0.175

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 (124) hide show
  1. package/FPScontrol.d.ts +55 -30
  2. package/FPScontrol.js +278 -225
  3. package/FPScontrol.ts +177 -127
  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 +8 -3
  10. package/MktMesh/MktMesh.js +43 -14
  11. package/MktMesh/MktMesh.ts +40 -14
  12. package/MktMesh/mktMappingShader.d.ts +6 -5
  13. package/MktMesh/mktMappingShader.js +26 -19
  14. package/MktMesh/mktMappingShader.ts +28 -19
  15. package/MktMesh/mktShader.d.ts +127 -58
  16. package/MktMesh/mktShader.js +192 -117
  17. package/MktMesh/mktShader.ts +251 -127
  18. package/MktMesh/panoSlotRoster.d.ts +2 -2
  19. package/MktMesh/panoSlotRoster.js +9 -7
  20. package/MktMesh/panoSlotRoster.ts +9 -7
  21. package/MktMesh/panoTileStitcher.d.ts +20 -13
  22. package/MktMesh/panoTileStitcher.js +97 -76
  23. package/MktMesh/panoTileStitcher.ts +119 -90
  24. package/MktMesh/types.d.ts +5 -1
  25. package/MktMesh/types.ts +5 -1
  26. package/PipeMesh/PipeJoint.d.ts +22 -0
  27. package/PipeMesh/PipeJoint.js +33 -0
  28. package/PipeMesh/PipeJoint.ts +38 -0
  29. package/PipeMesh/PipeMesh.d.ts +94 -22
  30. package/PipeMesh/PipeMesh.js +192 -53
  31. package/PipeMesh/PipeMesh.ts +223 -76
  32. package/PipeMesh/PipeStraight.d.ts +19 -0
  33. package/PipeMesh/PipeStraight.js +34 -0
  34. package/PipeMesh/PipeStraight.ts +45 -0
  35. package/PipeMesh/axisSnap.d.ts +52 -0
  36. package/PipeMesh/axisSnap.js +52 -0
  37. package/PipeMesh/axisSnap.ts +93 -0
  38. package/PipeMesh/directRoute.d.ts +85 -0
  39. package/PipeMesh/directRoute.js +126 -0
  40. package/PipeMesh/directRoute.ts +190 -0
  41. package/PipeMesh/index.d.ts +6 -0
  42. package/PipeMesh/index.js +6 -0
  43. package/PipeMesh/index.ts +7 -1
  44. package/PipeMesh/pipeMaterial.d.ts +16 -0
  45. package/PipeMesh/pipeMaterial.js +23 -0
  46. package/PipeMesh/pipeMaterial.ts +26 -0
  47. package/ThreeBase.d.ts +2 -49
  48. package/ThreeBase.js +43 -164
  49. package/ThreeBase.ts +1176 -1314
  50. package/instanced/InstancedBatch.d.ts +14 -1
  51. package/instanced/InstancedBatch.js +53 -3
  52. package/instanced/InstancedBatch.ts +62 -3
  53. package/instanced/InstancedChild.d.ts +24 -0
  54. package/instanced/InstancedChild.js +40 -0
  55. package/instanced/InstancedChild.ts +41 -0
  56. package/instanced/InstancedGroup.d.ts +48 -0
  57. package/instanced/InstancedGroup.js +45 -1
  58. package/instanced/InstancedGroup.ts +85 -0
  59. package/package.json +2 -7
  60. package/pass/bloom_pass.js +7 -14
  61. package/pass/bloom_pass.ts +7 -16
  62. package/pass/cover_pass.js +7 -15
  63. package/pass/cover_pass.ts +7 -17
  64. package/pass/debugDepthState.d.ts +16 -8
  65. package/pass/debugDepthState.js +4 -0
  66. package/pass/debugDepthState.ts +27 -9
  67. package/pass/glow_pass.js +19 -110
  68. package/pass/glow_pass.ts +19 -115
  69. package/pass/ssao_pass.d.ts +14 -59
  70. package/pass/ssao_pass.js +66 -171
  71. package/pass/ssao_pass.ts +63 -207
  72. package/pass/stroke_pass.js +13 -16
  73. package/pass/stroke_pass.ts +14 -18
  74. package/shaders/baseDepthVisShader.d.ts +1 -1
  75. package/shaders/baseDepthVisShader.js +1 -1
  76. package/shaders/baseDepthVisShader.ts +1 -1
  77. package/shaders/debugDepthCompareShader.d.ts +2 -2
  78. package/shaders/debugDepthCompareShader.js +2 -2
  79. package/shaders/debugDepthCompareShader.ts +2 -2
  80. package/shaders/debugDepthJudgeShader.d.ts +1 -1
  81. package/shaders/debugDepthJudgeShader.js +1 -1
  82. package/shaders/debugDepthJudgeShader.ts +1 -1
  83. package/PipeMesh/PathGeometry.d.ts +0 -44
  84. package/PipeMesh/PathGeometry.js +0 -307
  85. package/PipeMesh/PathGeometry.ts +0 -343
  86. package/PipeMesh/PathPoint.d.ts +0 -17
  87. package/PipeMesh/PathPoint.js +0 -31
  88. package/PipeMesh/PathPoint.ts +0 -36
  89. package/PipeMesh/PathPointList.d.ts +0 -31
  90. package/PipeMesh/PathPointList.js +0 -209
  91. package/PipeMesh/PathPointList.ts +0 -227
  92. package/PipeMesh/PathTubeGeometry.d.ts +0 -25
  93. package/PipeMesh/PathTubeGeometry.js +0 -130
  94. package/PipeMesh/PathTubeGeometry.ts +0 -146
  95. package/WaterFlow/FloatingDuck.d.ts +0 -88
  96. package/WaterFlow/FloatingDuck.js +0 -307
  97. package/WaterFlow/FloatingDuck.ts +0 -445
  98. package/WaterFlow/GPUFluidSimulation.d.ts +0 -56
  99. package/WaterFlow/GPUFluidSimulation.js +0 -785
  100. package/WaterFlow/GPUFluidSimulation.ts +0 -827
  101. package/WaterFlow/ReservoirSystem.d.ts +0 -106
  102. package/WaterFlow/ReservoirSystem.js +0 -621
  103. package/WaterFlow/ReservoirSystem.ts +0 -754
  104. package/WaterFlow/SPHField.d.ts +0 -35
  105. package/WaterFlow/SPHField.js +0 -204
  106. package/WaterFlow/SPHField.ts +0 -228
  107. package/WaterFlow/ScreenSpaceFluidPass.d.ts +0 -69
  108. package/WaterFlow/ScreenSpaceFluidPass.js +0 -736
  109. package/WaterFlow/ScreenSpaceFluidPass.ts +0 -789
  110. package/WaterFlow/ShallowWaterSurface.d.ts +0 -45
  111. package/WaterFlow/ShallowWaterSurface.js +0 -441
  112. package/WaterFlow/ShallowWaterSurface.ts +0 -510
  113. package/WaterFlow/WaterFlow.d.ts +0 -87
  114. package/WaterFlow/WaterFlow.js +0 -171
  115. package/WaterFlow/WaterFlow.ts +0 -219
  116. package/WaterFlow/types.d.ts +0 -161
  117. package/WaterFlow/types.js +0 -1
  118. package/WaterFlow/types.ts +0 -189
  119. package/WaterFlow/waterFlowPass.d.ts +0 -3
  120. package/WaterFlow/waterFlowPass.js +0 -20
  121. package/WaterFlow/waterFlowPass.ts +0 -40
  122. package/shaders/debugExcludedShader.d.ts +0 -13
  123. package/shaders/debugExcludedShader.js +0 -46
  124. package/shaders/debugExcludedShader.ts +0 -48
package/FPScontrol.ts CHANGED
@@ -6,12 +6,10 @@
6
6
  */
7
7
  import { BufferAttribute, Camera, Clock, Euler, EventDispatcher, Group, InstancedMesh, Intersection, Matrix3, Matrix4, Mesh, Object3D, Quaternion, Ray, Raycaster, Scene, 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
 
13
12
  // 「矩形射线法」的水平 far 口径:斜弦画长 = R√2(四弦首尾闭合的唯一解,勿再改回)。
14
- import { Capsule } from "three/examples/jsm/math/Capsule";
15
13
  // 粗辅助线:WebGL 下 `LineBasicMaterial.linewidth` 恒被驱动钳成 1px(Chrome/ANGLE 一律 1px,
16
14
  // 调大无效),要真的变粗必须走 `LineSegments2 + LineMaterial`(shader 里按像素铺quad)。
17
15
  // 本仓库先例:src/views/home/homeRender.ts:71-73, 344-358。
@@ -263,7 +261,7 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
263
261
  /** 是否允许跳跃(空格触发)。false 时按空格不跳。默认 true */
264
262
  public jumpEnabled: boolean = true;
265
263
  /** 跳跃初速度(垂直方向,m/s)。
266
- * ⚠️ 2026-09-27 治「跳跃翻墙」:原 15(当时重力 30)→ 顶点 3.75m 远超墙高 2m,跳起后胶囊底越过墙顶
264
+ * ⚠️ 2026-09-27 治「跳跃翻墙」:原 15(当时重力 30)→ 顶点 3.75m 远超墙高 2m,跳起后视点越过墙顶
267
265
  * 造成水平失明窗口,人带着水平速度飞越墙平面、落地在墙外侧被越推越远(探针实测 z=-187)。
268
266
  * 定案 8 ⇒ 顶点 1.07m:仍可跳上 0.8m 高台,且 yBottom 恒低于「墙顶 2.0 − 步高 0.1」,
269
267
  * 与主循环竖直门配合后失明窗口彻底消失。**顶点**才是防翻墙的那条线,不是初速度本身。
@@ -305,25 +303,27 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
305
303
  // far = R√2。菱形几何一个数没变(R=1.06 → 外接框 2.12×2.12、斜弦 45°、首尾闭合),
306
304
  // 只是不再经过「局部单位」这道换算。竖向的 2cm 探测余量见 `VERTICAL_SKIN`。
307
305
  //
308
- // 【停位口径(现行唯一,皇上 2026-09-27 十五旨)】射线在 far 内命中 = 碰撞;
309
- // 「还差多远才脱开」= 菱形沿墙法线的可达距离 reach = R·max(|n·右|, |n·前|) ∈ [R/√2, R]
310
- // 减去当前垂距 = deficit,取全轮最大者当读数;位置修正沿回弹方向**一格一格推**(每格
311
- // RETREAT_SKIN),推到最大穿透 ≤ 阈值 / 全脱开为止 —— 停位带与旧的解析解同宽。
312
- // ⇒ 停位仍只由 **R** 一个数决定(R=1.06 → 沿法线 0.749~1.06m);胶囊半径 0.35 ⇒ 身体与墙之间
306
+ // 【停位口径(现行唯一,皇上 2026-10-11 A 档改判 = 「解析贴合 + 只削法向速度」)】
307
+ // 射线在 far 内命中 = 碰撞;「还差多远才脱开」= 菱形沿墙法线的可达距离
308
+ // reach = R·max(|n·右|, |n·前|) ∈ [R/√2, R] 减去当前垂距 = deficit,取全轮**最大**者当读数;
309
+ // 位置修正沿该面法线一格一格推,每格 = min(RETREAT_SKIN, deficit) ⇒ 大欠距仍走整格,
310
+ // **收尾那一格按欠距截短** ⇒ 落点正好贴在包络边上(旧写法最后一格也推满 1cm → 过冲 → 来回甩)。
311
+ // 速度只削「往墙里」那一份(v −(v·n)n),切向保留 ⇒ 贴墙能滑,撞墙不再「加速→清零」锯齿。
312
+ // ⇒ 停位仍只由 **R** 一个数决定(R=1.06 → 沿法线 0.749~1.06m);射线包络决定视点与墙之间
313
313
  // 还留 0.40~0.71m 空气隙,这是皇上 2026-09-06 终裁布局自带、且亲眼验收过的量,不要当 bug 修。
314
314
  /**
315
315
  * 单步最大位移(连续碰撞检测用):每帧位移会被切成若干 ≤ 本值的小步,逐步做碰撞检测。
316
316
  * ⚠️ 必须明显小于水平包络的可达距离(R=1.06 时沿法线 0.749~1.06m),否则掉帧时一步就跨过墙体。
317
317
  * ⚠️ 本值同时决定「单次碰撞需要修正多深」——子步越小,玩家每次陷进墙里越浅,
318
- * 迭代要走的格数就越少(一格恒 RETREAT_SKIN,总位移 ≈ 该子步的穿透深度),
318
+ * 迭代要走的格数就越少(一格 ≤ RETREAT_SKIN,总位移 ≈ 该子步的穿透深度),
319
319
  * 不会出现「一下子回弹很多」。
320
320
  * 0.25 → 0.1:单次最大穿透从 0.25 降到 0.1,回弹量同比变小(子步数相应上调)。
321
321
  */
322
322
  public MAX_STEP_DISTANCE: number = 0.1;
323
323
  /** 位移细分的最大段数(防止极端速度/长卡顿把一帧切成几百步拖垮帧率) */
324
324
  public MAX_STEP_COUNT: number = 32;
325
- // 垂直碰撞范围 = 从 playerCollider.end(眼睛)到「该射线所保护的那一面」的距离
326
- // = capsule length + radius = (1.0 - 0.35) + 0.35 = 1.0(几何真值;下方两个常量是**口径**,不是几何)
325
+ // 垂直碰撞范围 = 从玩家视点到「该射线所保护的那一面」的距离
326
+ // = 视点到脚下射线保护面的距离 1.0(下方两个常量是**口径**,不是几何)
327
327
  // 对应 xsh-react-three 中 collideVolume(=3)= 控制器到地面的距离
328
328
  // xsh 中 far = collideVolume, 响应 = collideVolume - distance
329
329
  // 此处 far = DOWN_RANGE/UP_RANGE + SKIN,响应 = DOWN_RANGE/UP_RANGE − distance
@@ -332,9 +332,9 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
332
332
  // 我提醒过这一条常量身兼两职(探测长度 + 站位基准),他追选定案「**直接改站位基准**」,
333
333
  // 所以拆成两条各自独立的常量,**不再**共用 VERTICAL_RANGE=1.0:
334
334
  // · DOWN_RANGE = 1.5:下射线 far = 1.5+SKIN,响应 downPen = 1.5 − distance
335
- // → 落地后**眼睛离地 1.5m**。胶囊脚底在眼下 1.0m ⇒ 站定时脚底离地 **0.5m(悬浮)**。
335
+ // → 落地后**眼睛离地 1.5m**。
336
336
  // · UP_RANGE = 0.3:上射线 far = 0.3+SKIN,响应 upPen = 0.3 − distance
337
- // → 头顶到面时**眼睛离顶 0.3m**。胶囊顶在眼下 0.35m ⇒ 真顶到矮面时头顶穿入面内 **0.05m**,
337
+ // → 头顶到面时**眼睛离顶 0.3m**,由上射线阻止继续上移。
338
338
  // 且 0.3~1.0m 之间的低矮障碍物从此探不到(上射线变短的直接后果)。
339
339
  // ⚠️ 跳跃/重力/台阶吸附的其余口径一字未动,只换这两个基准数。
340
340
  // ⭐ 2026-09-26 皇上旨「能调节的参数都做滚轮输入框」→ 两条改为公共可写(面板「参数旋钮」可拧),
@@ -449,13 +449,19 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
449
449
  * 即 R 就是「玩家中心离墙多远开始判定碰撞」的半径。若 R 小于停位需求,人必须**先陷进墙里**
450
450
  * 才会被射线检测到,随后每帧被位置修正弹回一点 → 皇上所见「侧撞弹很远」。
451
451
  * 探针实测(`tmp/TMP_R_side_effect.mjs`,斜 45° 撞墙角 / 正对单墙同口径):
452
- * R=0.30 → 停位垂距 **0.2128**(< 胶囊半径 0.35,**明显穿模**)每帧被推 0.040
452
+ * R=0.30 → 停位垂距 **0.2128**,射线包络过小,容易贴墙。
453
453
  * R=0.50 → 0.3541(贴到墙面) 每帧 0.036
454
- * R=0.60 → 0.4243(勉强 > 胶囊半径)0.034
454
+ * R=0.60 → 0.4243。
455
455
  * R=0.80 → 0.5448 ✓ / R=1.06 → 0.7247 ✓(每帧修正仅 0.002)
456
- * ⇒ 取 **0.6**:保证停位垂距 > 玩家胶囊半径(0.35),不再穿模。
456
+ * ⇒ 取 **0.6**:为射线包络保留足够的贴墙间距。
457
457
  * ⭐ 2026-10-08 皇上旨:下限放宽到 **0.2**、上限收到 **1.0**(编辑框范围 0.2~1.0)。
458
458
  * ⚠️ R<0.6 仍会贴墙穿模(R=0.5 贴到墙面、R=0.3 明显穿模),皇上按需拧;下限放开是给窄通道/贴墙演示留余地。
459
+ * ⭐ 2026-10-11 A 档复核(tmp→test/fpsCollideFit.test.ts,正对 1m 墙):
460
+ * 停位现在**逐位等于 R**(眼停在面前 R 处 ⇒ 身体入墙深度 = 0.35 − R,一条直线):
461
+ * R=0.20 → 入墙 **0.1500m** R=0.30 → **0.0500m** R=0.35 → **0.0000m**
462
+ * R=0.50 / 0.60 / 1.00 → **0.0000m**(身体不出面)
463
+ * ⇒ 抖不抖与 R 无关(A 档后全档抖动 0.0000m);R<0.35 只剩「身体陷进墙多少」这一件事,
464
+ * 皇上 10-11 定「旋钮不动,只把数字写明」⇒ 要真焊死得改这里的下限,别在推离公式里偷偷加夹取。
459
465
  */
460
466
  public static readonly MIN_HORIZONTAL_RAY_RADIUS: number = 0.2;
461
467
  /** 水平射线半径 R 的安全上限(⭐ 2026-10-08 皇上旨收到 1.0:再大包络远大于停位需求、回弹精度已够,白烧射程) */
@@ -518,7 +524,8 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
518
524
  digits: 2,
519
525
  unit: "m",
520
526
  frozen: true,
521
- desc: "菱形半对角线 R,外接框边长 = 2R(= 离墙多远开始判定碰撞的半径)。越大包络越大、回弹方向越准;R<0.6 仍会贴墙穿模(R=0.5 贴墙面、R=0.3 明显穿模),已放宽到 0.2~1.0 供按需拧",
527
+ desc: "菱形半对角线 R,外接框边长 = 2R(= 离墙多远开始判定碰撞的半径)。越大包络越大、回弹方向越准;R<0.6 仍会贴墙穿模(R=0.5 贴墙面、R=0.3 明显穿模),已放宽到 0.2~1.0 供按需拧。" +
528
+ "⭐ 2026-10-11 A 档实测:停位逐位等于 R ⇒ 身体入墙深度 = 0.35 − R(R=0.2 → 0.15m、R=0.3 → 0.05m、R≥0.35 → 0m);抖动与 R 无关(全档 0.0000m),旋钮范围按皇上旨不动",
522
529
  get: (fps) => fps.CHORD_RHOMBUS_R,
523
530
  set: (fps, v) => fps.setHorizontalRayRadius(v),
524
531
  },
@@ -635,22 +642,9 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
635
642
  step: 0.005,
636
643
  digits: 3,
637
644
  unit: "m",
638
- desc: "⭐ 2026-09-28 皇上旨「完完全全用 xsh-react-three 的碰撞法」:每条命中射线每轮沿「命中点→中心」方向推的一小格(xsh 源码里写死 0.03;**皇上 10-07 旨「应该 1 个单位 1 个单位的修正」⇒ 默认改 0.01 = 一格 1cm**)。" +
645
+ desc: "⭐ 2026-10-11 皇上 A 档改判:每轮**只沿「欠距最大那一面」的法线**推一格,且这一格取 min(本值, 该面欠距) ⇒ 大欠距仍是一格一格往外走(皇上 10-07「1 个单位 1 个单位的修正」),只有**收尾那一格按欠距截短**,落点贴在包络边上(旧写法每条命中射线各推满一格、最后一格也推满 ⇒ 过冲 ⇒ 贴墙来回甩 = 抖动)。" +
639
646
  "⚠️ 上限以下别贪小:0.03/0.02/0.01 体检台判级完全相同,拧到 0.005 才有改善但「跳跃+冲刺+掉帧」那个现场开始穿墙(格长 = 每帧出水量)。迭代推到全部射线脱开为止",
640
647
  },
641
- {
642
- group: "回弹与手感",
643
- label: "每小帧轮数上限",
644
- key: "ROUNDS_PER_SUBSTEP",
645
- min: 1,
646
- max: 500,
647
- step: 1,
648
- digits: 0,
649
- unit: "轮",
650
- desc: "⭐ 皇上 2026-10-07 裁决加:一个物理小帧里 while 最多走几轮就落地(默认 500 = 与「每帧迭代预算」同档 ⇒ 现状一字不变)。" +
651
- "**这颗才是「一次修多少」的真闸门**:10-07 实测每次落地量 = 这一小帧往墙里挤进多深(2/8/10cm 都这么来),跟一格多远无关 ⇒ 要「一次只修几格、下一小帧再修」只能拧这里。" +
652
- "⚠️ 拧小的代价:一秒能挤出去的总量变小,高速冲刺顶墙时「挤进来的比推出去的快」⇒ 越陷越深(格长拧到 0.005 那档体检台 40 号现场就判 ❌穿墙,同一个机理)。先拧着看手感,别把默认改小",
653
- },
654
648
  // ===== 移动与视角 =====
655
649
  {
656
650
  group: "移动与视角",
@@ -814,18 +808,13 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
814
808
  // 缓存本帧的按键输入单位方向(水平)。initPlayerInputDir() 每物理步刷新一次;
815
809
  // 零向量表示「本帧没按方向键」→ 最终方向退化为纯反弹方向。
816
810
  private _inputDirVec: Vector3 = new Vector3();
817
- /**
818
- * 本帧**进碰撞之前**的水平速率(米/秒)。皇上八旨要求「移动向量的长度保持不变」,
819
- * 而碰撞会把 `playerVelocity.x/z` 清零,所以必须在 updatePlayer 入口先记一份。
820
- */
821
- private _preCollideHorizSpeed: number = 0;
822
811
  // ========== 回弹预览(皇上 2026-09-25 旨:「加个开关可以关闭碰撞修正位置的计算,方便观察回弹向量」)==========
823
812
  // 打开后:**不写位置**(见 playerCollisions 末尾唯一的 translate 闸门),但方向/推离量照常算、
824
813
  // 黄→白方向线照常画 → 肉眼核对「算出来的方向对不对」而不被人推开。
825
814
  // ⚠️ 只闸水平+竖向的**位置写入**;削速度、playerOnFloor、事件派发全部保留,
826
815
  // 否则预览模式下重力会一帧帧攒着把人送到地心并触发掉出世界复位。
827
816
  // ⭐⭐ 2026-09-26 皇上七旨「回弹预览开了, 就关不掉」⇒ 本字段改成**带暂存的访问器**:
828
- // 开 = 记下当前胶囊位置;关 = **还原**那个位置 + 清掉水平速度。
817
+ // 开 = 记下当前视点位置;关 = **还原**那个位置 + 清掉水平速度。
829
818
  // 病根(实测,墙面板块 x∈[5.0,6.0]、R=1.06):预览既然不写位置,人就照着自己的
830
819
  // 行走速度(speed=6 ⇒ 终端 37.5m/s ⇒ 每帧 0.62m)一路**穿墙走出去**,120 帧后中心
831
820
  // 已经在 x=138;这一路上只要停在任何一处「两面墙的票互相抵消」的几何里,位置修正就
@@ -833,15 +822,13 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
833
822
  // deficit 恒为 0.5600、位移 0。看着就是「预览关了位置修正也不回来了」。
834
823
  // ⇒ 预览是**观察**用的,那一段时间里的位移本来就不作数,关掉即回到打开前那一刻。
835
824
  private _previewRetreat: boolean = false;
836
- /** 开启预览那一刻的胶囊两端(关闭时整体还原回去) */
837
- private _obsSavedStart: Vector3 = new Vector3();
838
- private _obsSavedEnd: Vector3 = new Vector3();
825
+ /** 开启预览那一刻的玩家视点位置(关闭时还原) */
826
+ private _obsSavedPosition: Vector3 = new Vector3();
839
827
  /** 是否已有可还原的暂存(没进过观察模式就没有,避免构造期赋值把人还原到原点) */
840
828
  private _obsHasSave: boolean = false;
841
- /** 进观察模式(回弹预览 / 单步):记下当前胶囊位置,退出时还原 */
829
+ /** 进观察模式(回弹预览 / 单步):记下当前视点位置,退出时还原 */
842
830
  private _saveObsPos(): void {
843
- this._obsSavedStart.copy(this.playerCollider.start);
844
- this._obsSavedEnd.copy(this.playerCollider.end);
831
+ this._obsSavedPosition.copy(this.playerPosition);
845
832
  this._obsHasSave = true;
846
833
  }
847
834
  /**
@@ -851,9 +838,8 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
851
838
  */
852
839
  private _restoreObsPos(): void {
853
840
  if (!this._obsHasSave) return;
854
- this.playerCollider.start.copy(this._obsSavedStart);
855
- this.playerCollider.end.copy(this._obsSavedEnd);
856
- this.camera.position.copy(this.playerCollider.end);
841
+ this.playerPosition.copy(this._obsSavedPosition);
842
+ this.camera.position.copy(this.playerPosition);
857
843
  this.playerVelocity.x = 0;
858
844
  this.playerVelocity.z = 0;
859
845
  this._obsHasSave = false;
@@ -871,8 +857,7 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
871
857
  // xsh 的骨架只有一个 while(`FPSControls.tsx` 的 `computerPosition`):
872
858
  // ① 在**候选位置**(真实位置 + 已累计的 positionIncremental)上打水平射线
873
859
  // ② 有命中 ⇒ 沿「离开命中点」的方向推**一小格**(它写死 0.03)并续跑下一轮
874
- // ③ 一轮下来没命中 ⇒ goOnComputer=false ⇒ 收工;整圈上限 = MAX_ITERATIONS(皇上 10-03 旨:500)轮,
875
- // 单趟 while 另有「本轮数上限」闸门(常态帧 = ROUNDS_PER_SUBSTEP,皇上 10-07 裁决加,默认 500 同档 ⇒ 不变)
860
+ // ③ 一轮下来没命中 ⇒ goOnComputer=false ⇒ 收工;整圈上限 = MAX_ITERATIONS(皇上 10-03 旨:500)轮
876
861
  // ④ 循环外**唯一一次** position.add(incremental)
877
862
  // ⇒ 旧的「测试法逐级逼近」(六旨:1 → 0.1 → 0.01 三档、武装/判定/接受/撤销、每帧出水量 ≥
878
863
  // 进水量那套配套保险丝)**整条删除**。位置仍是一格一格挪出去的(皇上要的一步一步看那个形态不变),
@@ -898,18 +883,6 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
898
883
  private _tempEuler: Euler = new Euler(0, 0, 0, "YXZ");
899
884
  // 最大迭代次数(皇上 2026-10-03 旨:「那计算次数改成不超过 500」⇒ 100 → 500)
900
885
  public MAX_ITERATIONS: number = 500;
901
- /**
902
- * 一个**物理小帧**里 while 最多走几轮就落地(皇上 2026-10-07 裁决「加一个每小帧轮数上限」)。
903
- * 默认 500 = 与 `MAX_ITERATIONS` 同档 ⇒ 现状一字不变;拧小才是他要的「一次只修几格、下一小帧再修」。
904
- * ⭐ 为什么拧格长没用(10-07 实测):常态帧会一直迭代到「8 条射线全部脱开」才落地 ⇒
905
- * **每次落地量 = 这一小帧往墙里挤进多深**,与一格多远无关(格长 0.03→0.01 判级一字未变)。
906
- * 一格 = 一轮里每条命中各推的距离 ⇒ 要限制"一次修多少"只能限制**轮数**。
907
- * ⚠️ 代价(不许瞒):出水量 = 轮数 × 每轮票数 × 格长,默认每帧 2 个小帧。拧到 1 轮 + 1cm 格长 ⇒
908
- * 一秒最多挤出约 120cm 的量看着够,但高速冲刺顶墙时「这一帧挤进来的」可能比「两小帧推出去的」多
909
- * ⇒ 越陷越深,格长拧到 0.005 那一档体检台 40 号现场就是这么判 **❌穿墙** 的。
910
- * 所以默认不动,先让他在线上拧着看手感。
911
- */
912
- public ROUNDS_PER_SUBSTEP: number = 500;
913
886
  // ⚠️ 每帧碰撞迭代总预算(皇上 2026-09-03:墙角偶尔卡住,要求碰撞计算次数不超过 100;
914
887
  // 2026-10-03 同一位皇上把这条上限抬到 500 —— 机制一字未改,只是额度变大):
915
888
  // while 循环原本的上限 100 是「单次 playerCollisions」的,而一个 update() 帧内会调
@@ -926,10 +899,24 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
926
899
  private _probeRaycaster: Raycaster = new Raycaster();
927
900
  // 临时向量(复用避免 GC)
928
901
  private _tempVec3: Vector3 = new Vector3();
929
- // xsh 移植(2026-09-28):候选位置(= 控制器位置 + positionIncremental,射线原点锚)
930
- // 与推离向量专用临时量(复用避免 GC)
902
+ // xsh 移植(2026-09-28):候选位置(= 控制器位置 + positionIncremental,射线原点锚)临时量
931
903
  private _candPos: Vector3 = new Vector3();
932
- private _corrVec: Vector3 = new Vector3();
904
+ /**
905
+ * ⭐ 2026-10-11 A 档(解析贴合)专用临时量,全部复用避免 GC:
906
+ * · `_fitNormal` = 助手里「本轮这条命中面」指向玩家一侧的单位水平法线;
907
+ * · `_horizNormal` = 本轮**欠距最大**那面的法线(推离方向 + 速度闸门共用一个方向,绝不各算一遍);
908
+ * · `_fitVec` = (候选中心 − 命中点) 临时量;
909
+ * · `_rhombusAxisX/_rhombusAxisZ` = 本帧菱形两条对角轴的世界方向(随 yaw 转,恒水平)。
910
+ */
911
+ private _fitNormal: Vector3 = new Vector3();
912
+ private _horizNormal: Vector3 = new Vector3();
913
+ /** 本子步实际推离过的不同墙面法线。墙角需要同时裁掉两个朝内速度分量,
914
+ * 不能只用最后一次命中的面,否则两个面会在相邻帧交替抢占速度闸门而抖动。 */
915
+ private _horizContactNormals: Vector3[] = [new Vector3(), new Vector3(), new Vector3(), new Vector3()];
916
+ private _horizContactCount: number = 0;
917
+ private _fitVec: Vector3 = new Vector3();
918
+ private _rhombusAxisX: Vector3 = new Vector3(1, 0, 0);
919
+ private _rhombusAxisZ: Vector3 = new Vector3(0, 0, 1);
933
920
  private _tempVec4: Vector3 = new Vector3();
934
921
  // 位移细分用的临时向量(与 playerCollisions 内的 _tempVec4 分开,避免被覆盖)
935
922
  private _tempVec5: Vector3 = new Vector3();
@@ -1119,6 +1106,40 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1119
1106
  if (!this._transformFaceNormalWorld(interset, n)) return null;
1120
1107
  return n.normalize().y;
1121
1108
  }
1109
+ /**
1110
+ * ⭐⭐ 2026-10-11 皇上改判 A 档「解析贴合」:一条水平命中射线**沿本面法线还差多少才脱开**。
1111
+ * 同时把该面的单位水平法线(指向玩家所在的一侧)写进 `this._fitNormal` 供调用方取用。
1112
+ *
1113
+ * 为什么要它:旧写法每轮对每条命中射线**整格**推 `RETREAT_SKIN`(1cm),而退出判据是「8 条射线
1114
+ * 全部脱开」⇒ 落点必然**过冲最多一格**(实测弹回 1.05cm),下一帧再从过冲处走回来 =
1115
+ * 皇上所见「顶进去又弹回来一直抖」(无头复现:抖峰峰 0.0193m,60fps 下 34 次往复/40 帧,
1116
+ * 与 R / 墙厚 / 入射角 / 帧率全无关)。按欠距推到「刚好脱开」就不再有过冲。
1117
+ *
1118
+ * 判据(恒等式,不是拍的):碰撞包络是顶点在 ±R·x̂、±R·ẑ 的**菱形**,它沿方向 n 的支持距离
1119
+ * req = R·max(|n·x̂|, |n·ẑ|) ∈ [R/√2, R]。候选中心沿 n 到命中面的垂距为 perp(在实体内为负),
1120
+ * 那么「还差 deficit = req − perp」推满就正好贴在包络边上。
1121
+ *
1122
+ * 法线取向:**与射线行进方向相反**的一侧(three 对 FrontSide 只回正面命中,所以常态天然满足;
1123
+ * DoubleSide 拿到背面时按此翻号)。⚠️ 不能按「指向中心」翻号——中心已经陷进 1m 厚墙板时,
1124
+ * 那样翻会把人往墙体更深处推(旧写法正是靠两侧票互相抵消才卡在板正中)。
1125
+ *
1126
+ * @returns deficit(米,≤0 = 已经脱开)。退化场景返回 0:
1127
+ * · 拿不到面法线(无 face / 零向量)
1128
+ * · 命中面近乎水平(法线压平后是零向量,如贴着地面/台面掠射)⇒ 不该把它当墙侧推
1129
+ */
1130
+ private _horizFitDeficit(worldPos: Vector3, rayDir: Vector3, interset: Intersection): number {
1131
+ const n = this._fitNormal;
1132
+ if (!this._transformFaceNormalWorld(interset, n)) return 0;
1133
+ n.y = 0; // 本布局水平射线在 y=−0.7/−0.75(皇上 09-26 五旨),推离必须纯水平(同旧「偏差①」)
1134
+ if (n.lengthSq() < 1e-8) return 0;
1135
+ n.normalize();
1136
+ // 指向「射线来的那一侧」= 逆着射线行进方向,即离开实体的方向
1137
+ if (n.dot(rayDir) > 0) n.negate();
1138
+ const perp = n.dot(this._fitVec.copy(worldPos).sub(interset.point));
1139
+ const ax = Math.abs(n.dot(this._rhombusAxisX));
1140
+ const az = Math.abs(n.dot(this._rhombusAxisZ));
1141
+ return this.CHORD_RHOMBUS_R * Math.max(ax, az) - perp;
1142
+ }
1122
1143
  /**
1123
1144
  * 脚下深探:往下问 VOID_PROBE_DOWN 这么长一段,有没有地面(2026-09-25,穿地板自救用)。
1124
1145
  * @returns true = 下方有地面 → 只是正常腾空/下落,不是穿模
@@ -1186,14 +1207,12 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1186
1207
  resetPlayer(position: Vector3 = new Vector3(0, 0, 0)): void {
1187
1208
  this.playerVelocity.set(0, 0, 0);
1188
1209
  this.prePlayerVelocity.set(0, 0, 0);
1189
- this.playerCollider.start.set(position.x, position.y + 0.35, position.z);
1190
- this.playerCollider.end.set(position.x, position.y + 1, position.z);
1191
- this.playerCollider.radius = 0.35;
1192
- this.camera.position.copy(this.playerCollider.end);
1193
- // ⭐ 复位 = 换现场 ⇒ 把相机/胶囊位置基准挪到新位置。
1210
+ this.playerPosition.set(position.x, position.y + 1, position.z);
1211
+ this.camera.position.copy(this.playerPosition);
1212
+ // ⭐ 复位 = 换现场 ⇒ 把相机/视点位置基准挪到新位置。
1194
1213
  }
1195
- // 玩家胶囊碰撞体
1196
- playerCollider: Capsule = new Capsule(new Vector3(0, 0.35, 0), new Vector3(0, 1, 0), 0.35);
1214
+ /** 玩家视点。所有碰撞射线均以此点为局部原点。 */
1215
+ playerPosition: Vector3 = new Vector3(0, 1, 0);
1197
1216
  // 时钟对象
1198
1217
  clock: Clock = new Clock();
1199
1218
  prePlayerVelocity: Vector3 = new Vector3();
@@ -1240,7 +1259,7 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1240
1259
  * `depthWrite:true`(three-base/Ground1/Ground1.ts:107);哪天地板改回
1241
1260
  * transparent + depthWrite:false,射线线会重新穿地浮出来。
1242
1261
  * - 回弹方向线(黄→白)= **false**:「回弹向量线保持穿透显示」。
1243
- * 它是「本帧要看的向量」,起点在胶囊内部、方向常指着墙里,开遮挡会被自己的身子
1262
+ * 它是「本帧要看的向量」,起点在玩家位置、方向常指着墙里,开遮挡会被墙面
1244
1263
  * 或墙面吃掉半截 → 观察目的直接落空,故保持常显在最前。
1245
1264
  */
1246
1265
  private _createDebugLine(depthTest: boolean): LineSegments2 {
@@ -1308,7 +1327,7 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1308
1327
  this._debugRayGroup = null;
1309
1328
  }
1310
1329
  init(canvas: HTMLCanvasElement): void {
1311
- this.playerCollider.end.set(0, 1, 0);
1330
+ this.playerPosition.set(0, 1, 0);
1312
1331
  document.addEventListener("keydown", this.keydown);
1313
1332
  document.addEventListener("keyup", this.keyup);
1314
1333
  canvas.addEventListener("pointerdown", this.mousedown);
@@ -1397,11 +1416,10 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1397
1416
  controls = (deltaTime: number): void => {
1398
1417
  const speedDelta = deltaTime * (this.playerOnFloor ? 25 : 8) * this.speed;
1399
1418
  // ⭐ 2026-09-26 皇上旨「碰撞就是按射线拾取算的,不用搞太复杂」:
1400
- // 旧的「接触法线缓存 + 加速度保速投影」整块删除 —— 撞墙的手感由位置侧负责
1401
- // (命中即 playerVelocity.x/z = 0,再走迭代推格修正位置,皇上 2026-09-25 定案)。
1402
- // ⭐ 2026-09-26 皇上八旨给这条加了个尾巴:位置修正判完之后,若本帧算出了方向且没和
1403
- // 输入方向对消,水平速度会整条换成 normalize(输入 + 回弹) × 碰撞前速率(贴墙不再走不动),
1404
- // 见 playerCollisions 末尾那段。清零本身照旧。
1419
+ // 旧的「接触法线缓存 + 加速度保速投影」整块删除 —— 这里**只管往输入方向加速**,
1420
+ // 撞墙的手感全部由 playerCollisions 负责:位置侧「解析贴合」(欠距最大那面推格、
1421
+ // 最后一格截短),速度侧「只削往墙里那一份」(⚠️ 2026-10-11 A 档改判,此处不再是
1422
+ // playerVelocity.x/z 整条清零 ⇒ 贴墙能沿墙滑行,见 playerCollisions 末尾那段)。
1405
1423
  const accel = this._accelVec.set(0, 0, 0);
1406
1424
  if (this.keyStates["KeyW"]) {
1407
1425
  accel.addScaledVector(this.getForwardVector(), speedDelta);
@@ -1442,9 +1460,6 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1442
1460
  damping *= 0.1;
1443
1461
  }
1444
1462
  this.playerVelocity.addScaledVector(this.playerVelocity, damping);
1445
- // ⭐ 2026-09-26 皇上八旨「位置修正结束后…移动向量的长度保持不变」:本帧**进碰撞前**的
1446
- // 水平速率快照。碰撞会把速度清零,子步跑完再想要「原来的长度」就没有了,必须在入口取。
1447
- this._preCollideHorizSpeed = Math.hypot(this.playerVelocity.x, this.playerVelocity.z);
1448
1463
  // 位移细分(连续碰撞):把本帧位移切成若干 ≤ MAX_STEP_DISTANCE 的小步,逐步推进并检测。
1449
1464
  // 不细分的话,掉帧(deltaTime 被 clamp 到 0.05)时单步位移可达 25×0.025 ≈ 0.6,
1450
1465
  // 远大于射线的检测余量(沿法线最小 reach R/√2 = 0.749 − 子步 0.1)→ 直接跨过墙体(穿墙)。
@@ -1465,7 +1480,7 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1465
1480
  this.playerCollisions(s === stepCount - 1);
1466
1481
  }
1467
1482
  // 设置相机位置(第一人称默认值)
1468
- this.camera.position.copy(this.playerCollider.end);
1483
+ this.camera.position.copy(this.playerPosition);
1469
1484
  // 第三人称:偏移相机位置 + 朝向第一人称坐标
1470
1485
  if (this.thirdPerson) {
1471
1486
  const cosPitch = Math.cos(this._thirdPersonAngleX);
@@ -1480,7 +1495,7 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1480
1495
  Math.cos(this._thirdPersonAngleY) *
1481
1496
  cosPitch *
1482
1497
  this.thirdPersonDistance;
1483
- this.camera.lookAt(this.playerCollider.end);
1498
+ this.camera.lookAt(this.playerPosition);
1484
1499
  }
1485
1500
  // dev 调试模式:在相机本地坐标系叠加偏移(向后上方)
1486
1501
  if (this.dev && !this.thirdPerson) {
@@ -1499,7 +1514,7 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1499
1514
  // xsh: direction prop → _localRayDirs(归一化)
1500
1515
  // xsh: raycaster.far = getDirection().length() → HORIZONTAL_FAR / downFar / upFar
1501
1516
  // xsh: Group3D rotation={x:-euler.x} → yawQuat(pitch 补偿)
1502
- // xsh: getIntersetResult(positionIncremental) → worldPos = playerCollider.end + positionIncremental
1517
+ // xsh: getIntersetResult(positionIncremental) → worldPos = playerPosition + positionIncremental
1503
1518
  //
1504
1519
  // 现行 10 条布局逐条表 = 上方「★ 矩形射线法 ★」权威规格块(唯一数据源,不在这里重复一遍)。
1505
1520
  /** 输出一条碰撞日志(唯一出口 = 浏览器控制台;皇上:「日志就是浏览器的控制台」) */
@@ -1512,11 +1527,15 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1512
1527
  // ⇒ 本方法整体 = xsh `FPSControls.tsx → computerPosition()` 的忠实移植,骨架只有:
1513
1528
  // ① while 循环(皇上 09-03 定「一帧合计不超过 100」,10-03 旨改到 500;骨架与 xsh 的 i<100 同形);
1514
1529
  // ② 每轮在「候选位置」(真实位置 + positionIncremental)上打 8 条水平斜弦射线,
1515
- // 每条命中 ⇒ 沿「控制器中心 − 命中点」方向推一小格(xsh 源码写死 0.03;皇上 10-07 旨
1516
- // 「应该 1 个单位 1 个单位的修正」⇒ 本项默认改成 RETREAT_SKIN = 0.01,一格 1cm)
1517
- // 累加进 positionIncremental,并 goOnComputer=true 续命下一轮(xsh 各票天然抵消);
1530
+ // ⭐ 2026-10-11 皇上 A 档改判:每条命中算「沿本面法线还差多少才脱开」(deficit),
1531
+ // **只取欠距最大的那一面**,沿它的法线推 min(RETREAT_SKIN, deficit) ——
1532
+ // 一格一格的外形保留(皇上 10-07 旨「应该 1 个单位 1 个单位的修正」⇒ RETREAT_SKIN=0.01),
1533
+ // 但收尾那一格按欠距截短 ⇒ 落点贴包络边,不再整格过冲(旧 xsh 各票各推一整格 ⇒ 必过冲);
1534
+ // 累加进 positionIncremental,并 goOnComputer=true 续命下一轮;
1518
1535
  // ③ 下射线命中 ⇒ 竖直速度清零 + y 钉到「命中面之上 DOWN_RANGE」(xsh: cv − distance);
1519
- // 上射线命中 ⇒ 竖直速度清零 + 修正(含三道闸天花板的血案守卫);
1536
+ // 上射线命中 ⇒ 竖直速度清零 + 修正(含三道闸天花板的血案守卫)
1537
+ // —— ⭐ 10-11 修好少括号的 bug 后,上射线**每轮都测**(旧代码被关进「下射线命中」分支里
1538
+ // ⇒ 腾空时跳起来顶不到天花板);
1520
1539
  // ④ 某一轮全部射线脱开 ⇒ 收工;循环外唯一一次 translate 落地(血案护栏不变)。
1521
1540
  // ⭐⭐ 2026-10-02 皇上旨「模拟修正,不是真实修正,把最终结果算好了再应用到真实的第一人称
1522
1541
  // 控制器上面」⇒ 那个 translate 落的是**「本步位移 + 允许的修正」一次到位**:
@@ -1529,15 +1548,18 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1529
1548
  // 本布局射线在 y=−0.7/−0.75(皇上 09-26 五旨),不压平会把 y 混进推离;
1530
1549
  // · 水平射线 8 条(皇上 09-06/09-14 亲加的 6-9 共线反向组),xsh 原版 4 条;
1531
1550
  // · 竖向保留 faceUp / 天花板三道闸守卫(「掉出世界」血案修复)。
1532
- // 原 deficit / 票面合成(finalDir) / 逐格推离 / 白推保险丝 / 胶囊兜底机器整条拆除。
1551
+ // · ⭐ 2026-10-11 A 档:**只沿「欠距最大那一面」的法线**推,且最后一格按欠距截短
1552
+ // (xsh 是每条命中各推一整格 ⇒ 4 条命中同一面墙一轮推 4cm,退出时必过冲)。
1553
+ // 原 deficit / 票面合成(finalDir) / 逐格推离 / 白推保险丝等旧分支已拆除。
1554
+ // ⚠️ 措辞校正(10-11):上面这句里「deficit」那半已随 A 档**回归**(`_horizFitDeficit()`:
1555
+ // 沿命中面法线的菱形可达距离 R·max(|n·x̂|,|n·ẑ|) 减当前垂距);
1556
+ // 仍拆除的是「票面合成 finalDir / 白推保险丝」两件 —— 别照旧文案把它们捡回来。
1533
1557
  const positionIncremental = this._tempVec4.set(0, 0, 0);
1534
1558
  this._refreshInputDir(); // 台账 ix/iz 列还要用(与碰撞计算无关)
1535
1559
  const recordDebug = (this.dev || this.thirdPerson) && isFinalStep;
1536
1560
  let debugRecorded = !recordDebug;
1537
1561
  this._horizHit = false;
1538
- // ⭐ 皇上 2026-10-07 裁决「加一个每小帧轮数上限」:这一趟 while 最多走几轮就落地。
1539
- // 常态帧用旋钮 ROUNDS_PER_SUBSTEP(默认 500 与 MAX_ITERATIONS 同档 ⇒ 现状完全不变)。
1540
- const roundCap = this.ROUNDS_PER_SUBSTEP;
1562
+ this._horizContactCount = 0;
1541
1563
  const downRange = this.DOWN_RANGE;
1542
1564
  const upRange = this.UP_RANGE;
1543
1565
  let i = 0;
@@ -1551,7 +1573,7 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1551
1573
  // ⭐ 2026-10-02 模拟修正:真实 collider 此刻**还没被本步位移推过**,
1552
1574
  // 所以锚点 = 真实位置 + 本步位移 =「这一步走完会站在哪儿」的**模拟位置**
1553
1575
  // (数值上与旧写法「先 translate 再取 end」完全相同 ⇒ 几何、方向、测试基线都不变)。
1554
- const basePos = this._tempVec3.copy(this.playerCollider.end).add(this._subStep);
1576
+ const basePos = this._tempVec3.copy(this.playerPosition).add(this._subStep);
1555
1577
  // xsh: 射线原点 = 控制器位置 + positionIncremental(getIntersetResult(incremental) 语义)
1556
1578
  const worldPos = this._candPos.copy(basePos).add(positionIncremental);
1557
1579
  // xsh: Group3D rotation={x:-euler.x} 补偿 pitch → 只跟随 yaw
@@ -1561,12 +1583,21 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1561
1583
  for (const ray of this._rays) {
1562
1584
  ray.updateWorld(yawQuat, worldPos);
1563
1585
  }
1586
+ // ⭐ 2026-10-11 A 档:菱形两条对角轴的世界方向(只随 yaw,恒水平)——贴合量 req 要用它
1587
+ this._rhombusAxisX.set(1, 0, 0).applyQuaternion(yawQuat);
1588
+ this._rhombusAxisZ.set(0, 0, 1).applyQuaternion(yawQuat);
1564
1589
  const horizontalFar = this.HORIZONTAL_FAR; // = R√2(xsh: far = direction.length())
1565
1590
  const downFar = downRange + this.VERTICAL_SKIN;
1566
1591
  const upFar = upRange + this.VERTICAL_SKIN;
1567
1592
  const devVisualFarD = downRange;
1568
1593
  const devVisualFarU = upRange;
1569
- // ========== 水平半区(xsh 原样:每条命中射线各推一小格)==========
1594
+ // ========== 水平半区(⭐ 2026-10-11 皇上改判 A 档 = 解析贴合,替代「每条命中各推一整格」)==========
1595
+ // 每轮:所有命中射线各算「沿本面法线还差多少才脱开」(deficit),**只取欠距最大的那一面**,
1596
+ // 沿它的法线推 `min(RETREAT_SKIN, deficit)` —— 一格 1cm 的上限照旧(皇上 10-07「1 个单位
1597
+ // 1 个单位的修正」),但**最后一格只推欠距本身**,落点贴在包络边上,不再整格过冲。
1598
+ // · 为什么不再各票相加:4 条命中同一面墙时旧写法一轮推 4cm,退出时必然过头;
1599
+ // · 为什么只取最大:墙角两面墙各欠 3cm 时,先脱开深的那面,下一轮再补另一面(≤2~3 轮)。
1600
+ let bestDeficit = 0;
1570
1601
  for (let ri = 2; ri < this._rays.length; ri++) {
1571
1602
  const ray = this._rays[ri];
1572
1603
  ray.far = horizontalFar;
@@ -1595,26 +1626,29 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1595
1626
  interset = intersets[j];
1596
1627
  }
1597
1628
  }
1598
- // ⭐ xsh 原文:vector3Temp = worldPos − interset.point; setLength(0.03);
1599
- // positionIncremental.add(vector3Temp); goOnComputer = true;
1600
- // ⚠️ 锚点 = basePos(xsh 的 getWorldPosition,循环内不含增量)。
1601
- const corr = this._corrVec.copy(basePos).sub(interset.point);
1602
- corr.y = 0; // 偏差①:本布局射线在 y=−0.7/−0.75,不压平会把 y 混进推离(xsh 射线在 y=0 天然水平)
1603
- if (corr.lengthSq() > 1e-12) {
1604
- if (i > roundCap) {
1605
- // ⭐ **轮次上限**用完 ⇒ 这一轮起水平一格都不再推。
1606
- // 常态帧 = 皇上 10-07 点头新加的 ROUNDS_PER_SUBSTEP(默认 500 ⇒ 与旧行为同形)。
1607
- // ⚠️ 只拦水平:竖向(踩地/顶头)仍照常,否则人会往地心沉(皇上 09-25 冻结口径)。
1608
- } else {
1609
- corr.setLength(this.RETREAT_SKIN);
1610
- positionIncremental.add(corr);
1611
- goOnComputer = true;
1612
- this._horizHit = true;
1613
- // 方向线/台账的「修正方向」快照 = 最后一次命中的推离方向
1614
- // ⭐ 皇上 10-07 旨「每执行一次,偏移量是多少」⇒ 按钮那一次:一格一行,带累计
1615
- }
1629
+ const deficit = this._horizFitDeficit(worldPos, ray.direction, interset);
1630
+ if (deficit > bestDeficit) {
1631
+ bestDeficit = deficit;
1632
+ this._horizNormal.copy(this._fitNormal); // 与推离方向同一个数,绝不分两处算
1616
1633
  }
1617
1634
  }
1635
+ if (bestDeficit > 1e-6) {
1636
+ // 大欠距逐格推离,直到所有水平射线都脱离碰撞面。
1637
+ positionIncremental.addScaledVector(this._horizNormal, Math.min(this.RETREAT_SKIN, bestDeficit));
1638
+ // 一面墙在多轮迭代里会重复命中;只保留方向不同的接触面。
1639
+ let known = false;
1640
+ for (let ni = 0; ni < this._horizContactCount; ni++) {
1641
+ if (this._horizContactNormals[ni].dot(this._horizNormal) > 0.999) {
1642
+ known = true;
1643
+ break;
1644
+ }
1645
+ }
1646
+ if (!known && this._horizContactCount < this._horizContactNormals.length) {
1647
+ this._horizContactNormals[this._horizContactCount++].copy(this._horizNormal);
1648
+ }
1649
+ goOnComputer = true;
1650
+ this._horizHit = true;
1651
+ }
1618
1652
  // ========== 向下射线(索引 0 = upTodwomRay)==========
1619
1653
  const downRay = this._rays[0];
1620
1654
  downRay.far = downFar;
@@ -1675,6 +1709,13 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1675
1709
  // **不能**沿用旧探针的 `!debugRecorded`:`debugRecorded = !recordDebug`,而 `recordDebug`
1676
1710
  // 跟着面板「碰撞射线」(dev) 走 → dev 关着时恒 true → 整段被跳过,
1677
1711
  // 这正是皇上「在控制台里根本看不到下射线数值」的原因。
1712
+ // ⭐ 2026-10-11 皇上旨「少括号的 bug 一起修」:这个 `}` 是 `if (downIntersets.length > 0)` 的
1713
+ // 闭合括号,历史上被吃掉了 ⇒ 下面整段「向上射线」被关进「下射线命中」分支里。
1714
+ // 症状:腾空时脚下射线打不到地面 → 上射线**一次都不测** → 跳起来顶不到天花板(穿板顶)。
1715
+ // 顺带的第二个坑:while 的闭合括号跟着后移到「应用推离」注释之后(原 1843 行那个 `}`),
1716
+ // 于是「更新调试射线」那段也在每轮迭代里跑一遍 —— 删掉那个多余括号后它回到循环外,
1717
+ // 正好对上它自己的标签「dev 模式的**最后一步**」,且每帧只做一次。
1718
+ }
1678
1719
  // ========== 向上射线(索引 1 = dwomToUpRay)==========
1679
1720
  const upRay = this._rays[1];
1680
1721
  upRay.far = upFar;
@@ -1721,7 +1762,7 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1721
1762
  // 第 11 帧眼落到板底之下 0.0135,上射线打到**板底(法线朝下)**,旧口径
1722
1763
  // `positionIncremental.y -= upPen` 一帧把人往下砸 **0.9865m** → 眼落到 −2.0,
1723
1764
  // 从此两条竖射线彻底够不着 → 自由落体到 **−13.161547** → 撞上 camera.y≤−10 的
1724
- // 掉出世界复位(`this.playerCollider.end.set(0, 1, 10)`)→ 「陷进地面很长一段
1765
+ // 掉出世界复位(玩家视点重置到 (0, 1, 10))→ 「陷进地面很长一段
1725
1766
  // 距离然后凭空回到原点」。这就是皇上看到的现象。
1726
1767
  // 新口径:**绝不往下顶**,一步顶到「脚底刚好落在这块面之上」(眼高 = 面 y +
1727
1768
  // downRange + SKIN)。顶完眼只比板底高一个 SKIN → 下一轮迭代下射线立刻
@@ -1786,7 +1827,6 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1786
1827
  // ⚠️ 只跳「写位置」,不跳「记账」:`playerVelocity.y = 0`、`playerOnFloor = true`
1787
1828
  // 与削速度/方向票全部保留 → 观察时不会一帧帧攒重力陷到地心,也不会触发
1788
1829
  // camera.y ≤ −10 的掉出世界复位。
1789
- }
1790
1830
  // ⭐⭐ 2026-10-02 皇上旨(原话):「在修正位置的时候,我希望的是**模拟修正**,而不是**真实的
1791
1831
  // 修正**,在模拟修正中把最终结果计算好了,再应用到真实的第一人称控制器上面」。
1792
1832
  // ⇒ 上面那个 while 全在**模拟位置**(真实位置 + 本步位移 `_subStep`)上跑;真实 collider
@@ -1819,20 +1859,32 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1819
1859
  this._subStep.z + corrZ
1820
1860
  );
1821
1861
  if (this._appliedMove.lengthSq() > 0) {
1822
- this.playerCollider.translate(this._appliedMove);
1862
+ this.playerPosition.add(this._appliedMove);
1823
1863
  }
1824
1864
  // 本步已消费:归零,面板那两颗直接调 playerCollisions 的按钮不会重复走一步
1825
1865
  this._subStep.set(0, 0, 0);
1826
1866
 
1827
- // ⭐⭐ 2026-10-07 皇上旨「撞到墙后,加速度置为 0」:
1828
- // 抖动根因 = 撞墙后位置被推回包络边,但 playerVelocity 的水平分量(朝墙的冲量)一直保留,
1829
- // 下一帧 controls() 又叠加输入加速度 → 再次朝墙冲 → 再次被推回 → 来回甩 = 肉眼抖动。
1830
- // ⇒ 本子步水平命中(_horizHit)即把 playerVelocity.x/z 清零:人贴墙即停,
1831
- // 不再被惯性反复往墙里拽、被推离反复弹回。(观察模式:单步/回弹预览不改速度,
1832
- // 免得不落地的观察帧把速度抹掉、干扰手感判断;竖向 y 一字不动——重力/跳跃照旧。)
1867
+ // ⭐⭐ 2026-10-11 皇上旨「撞墙会进入墙体然后反弹回来,一直抖动」→ A 档改判:
1868
+ // **只削掉「往墙里」那一份速度**(沿命中面法线的负分量),切向分量原样保留。
1869
+ // 旧口径(2026-10-07「撞到墙后,加速度置为 0」)是 x/z 双双归零,两处毛病:
1870
+ // ① 斜着撞墙 = 连「沿墙滑行」那份一起抹掉 → 手感像被墙吸停,且人是沿坐标轴方向被清,
1871
+ // 不是沿墙法线;速度被清零的方向与实际推离方向不共线 → 推离与加速度交替占上风;
1872
+ // ② 全清 ⇒ 每帧「输入重新加速 → 立刻清零」的锯齿,正是他看到的抖动的另一半来源
1873
+ // (实测峰峰 0.0193m、60fps 下约 17 次/秒翻向)。
1874
+ // 新口径:`into = v·n`(n = 本轮欠距最大那面的**外法线**,指向玩家一侧),只在 into<0
1875
+ // (= 正在往墙里冲)时减掉 `into·n` 那份 ⇒ 撞后 v·n = 0(非穿透),切向 v−(v·n)n 完整保留。
1876
+ // ⚠️ 方向与推离用的是**同一个** `_horizNormal`(水平半区那轮算好、就地复用),绝不另算一遍。
1877
+ // ⚠️ 竖向 y 一字不动(重力/跳跃照旧);观察模式(回弹预览)不改速度,口径与 10-07 一致。
1833
1878
  if (this._horizHit && !this.previewRetreat) {
1834
- this.playerVelocity.x = 0;
1835
- this.playerVelocity.z = 0;
1879
+ // 墙角中两个约束面不一定在同一轮成为“欠距最大面”。依次投影两轮,
1880
+ // 让每个面都得到 v·n >= 0,同时保留不受任何墙面约束的切向速度。
1881
+ for (let pass = 0; pass < 2; pass++) {
1882
+ for (let ni = 0; ni < this._horizContactCount; ni++) {
1883
+ const normal = this._horizContactNormals[ni];
1884
+ const into = this.playerVelocity.dot(normal);
1885
+ if (into < 0) this.playerVelocity.addScaledVector(normal, -into);
1886
+ }
1887
+ }
1836
1888
  }
1837
1889
  // ⭐ 十旨:一帧一条的抖动台账。**只由子步循环的最后一步(isFinalStep)记** ——
1838
1890
  // 中间小步是碰撞细分的过渡状态,记了就变成「一帧好几条」,翻向/跳变的口径全乱。
@@ -1857,10 +1909,8 @@ export default class FPScontrol extends EventDispatcher<FPScontrolEventTypeMap>
1857
1909
  // 越界传送
1858
1910
  teleportPlayerIfOob = (): void => {
1859
1911
  if (this.camera.position.y <= -10) {
1860
- this.playerCollider.start.set(0, 0.35, 0);
1861
- this.playerCollider.end.set(0, 1, 10);
1862
- this.playerCollider.radius = 0.35;
1863
- this.camera.position.copy(this.playerCollider.end);
1912
+ this.playerPosition.set(0, 1, 10);
1913
+ this.camera.position.copy(this.playerPosition);
1864
1914
  }
1865
1915
  };
1866
1916
  // 更新
@@ -1,10 +1,12 @@
1
- import { Matrix4, Mesh, PlaneGeometry, ShaderMaterial } from "three";
2
- /** 装饰地板(随机渐变的深灰、不透明) */
1
+ import { Clock, BufferGeometry, Camera, Group, Material, Matrix4, Mesh, PlaneGeometry, Scene, ShaderMaterial, WebGLRenderer } from "three";
2
+ /** 装饰地板(科技感格子贴图 + 紫色扩散动画,半透明可透视外观) */
3
3
  export default class Ground1 extends Mesh {
4
4
  tempMatrix: Matrix4;
5
5
  constructor();
6
6
  renderOrder: number;
7
7
  geometry: PlaneGeometry;
8
8
  setSize(width: number, height: number): void;
9
+ clock: Clock;
10
+ onBeforeRender: (renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void;
9
11
  material: ShaderMaterial;
10
12
  }