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.
- package/FPScontrol.d.ts +55 -30
- package/FPScontrol.js +278 -225
- package/FPScontrol.ts +177 -127
- package/Ground1/Ground1.d.ts +4 -2
- package/Ground1/Ground1.js +77 -57
- package/Ground1/Ground1.ts +77 -57
- package/Mirror.js +0 -4
- package/Mirror.ts +0 -4
- package/MktMesh/MktMesh.d.ts +8 -3
- package/MktMesh/MktMesh.js +43 -14
- package/MktMesh/MktMesh.ts +40 -14
- package/MktMesh/mktMappingShader.d.ts +6 -5
- package/MktMesh/mktMappingShader.js +26 -19
- package/MktMesh/mktMappingShader.ts +28 -19
- package/MktMesh/mktShader.d.ts +127 -58
- package/MktMesh/mktShader.js +192 -117
- package/MktMesh/mktShader.ts +251 -127
- package/MktMesh/panoSlotRoster.d.ts +2 -2
- package/MktMesh/panoSlotRoster.js +9 -7
- package/MktMesh/panoSlotRoster.ts +9 -7
- package/MktMesh/panoTileStitcher.d.ts +20 -13
- package/MktMesh/panoTileStitcher.js +97 -76
- package/MktMesh/panoTileStitcher.ts +119 -90
- package/MktMesh/types.d.ts +5 -1
- package/MktMesh/types.ts +5 -1
- package/PipeMesh/PipeJoint.d.ts +22 -0
- package/PipeMesh/PipeJoint.js +33 -0
- package/PipeMesh/PipeJoint.ts +38 -0
- package/PipeMesh/PipeMesh.d.ts +94 -22
- package/PipeMesh/PipeMesh.js +192 -53
- package/PipeMesh/PipeMesh.ts +223 -76
- package/PipeMesh/PipeStraight.d.ts +19 -0
- package/PipeMesh/PipeStraight.js +34 -0
- package/PipeMesh/PipeStraight.ts +45 -0
- package/PipeMesh/axisSnap.d.ts +52 -0
- package/PipeMesh/axisSnap.js +52 -0
- package/PipeMesh/axisSnap.ts +93 -0
- package/PipeMesh/directRoute.d.ts +85 -0
- package/PipeMesh/directRoute.js +126 -0
- package/PipeMesh/directRoute.ts +190 -0
- package/PipeMesh/index.d.ts +6 -0
- package/PipeMesh/index.js +6 -0
- package/PipeMesh/index.ts +7 -1
- package/PipeMesh/pipeMaterial.d.ts +16 -0
- package/PipeMesh/pipeMaterial.js +23 -0
- package/PipeMesh/pipeMaterial.ts +26 -0
- package/ThreeBase.d.ts +2 -49
- package/ThreeBase.js +43 -164
- package/ThreeBase.ts +1176 -1314
- package/instanced/InstancedBatch.d.ts +14 -1
- package/instanced/InstancedBatch.js +53 -3
- package/instanced/InstancedBatch.ts +62 -3
- package/instanced/InstancedChild.d.ts +24 -0
- package/instanced/InstancedChild.js +40 -0
- package/instanced/InstancedChild.ts +41 -0
- package/instanced/InstancedGroup.d.ts +48 -0
- package/instanced/InstancedGroup.js +45 -1
- package/instanced/InstancedGroup.ts +85 -0
- package/package.json +2 -7
- package/pass/bloom_pass.js +7 -14
- package/pass/bloom_pass.ts +7 -16
- package/pass/cover_pass.js +7 -15
- package/pass/cover_pass.ts +7 -17
- package/pass/debugDepthState.d.ts +16 -8
- package/pass/debugDepthState.js +4 -0
- package/pass/debugDepthState.ts +27 -9
- package/pass/glow_pass.js +19 -110
- package/pass/glow_pass.ts +19 -115
- package/pass/ssao_pass.d.ts +14 -59
- package/pass/ssao_pass.js +66 -171
- package/pass/ssao_pass.ts +63 -207
- package/pass/stroke_pass.js +13 -16
- package/pass/stroke_pass.ts +14 -18
- package/shaders/baseDepthVisShader.d.ts +1 -1
- package/shaders/baseDepthVisShader.js +1 -1
- package/shaders/baseDepthVisShader.ts +1 -1
- package/shaders/debugDepthCompareShader.d.ts +2 -2
- package/shaders/debugDepthCompareShader.js +2 -2
- package/shaders/debugDepthCompareShader.ts +2 -2
- package/shaders/debugDepthJudgeShader.d.ts +1 -1
- package/shaders/debugDepthJudgeShader.js +1 -1
- package/shaders/debugDepthJudgeShader.ts +1 -1
- package/PipeMesh/PathGeometry.d.ts +0 -44
- package/PipeMesh/PathGeometry.js +0 -307
- package/PipeMesh/PathGeometry.ts +0 -343
- package/PipeMesh/PathPoint.d.ts +0 -17
- package/PipeMesh/PathPoint.js +0 -31
- package/PipeMesh/PathPoint.ts +0 -36
- package/PipeMesh/PathPointList.d.ts +0 -31
- package/PipeMesh/PathPointList.js +0 -209
- package/PipeMesh/PathPointList.ts +0 -227
- package/PipeMesh/PathTubeGeometry.d.ts +0 -25
- package/PipeMesh/PathTubeGeometry.js +0 -130
- package/PipeMesh/PathTubeGeometry.ts +0 -146
- package/WaterFlow/FloatingDuck.d.ts +0 -88
- package/WaterFlow/FloatingDuck.js +0 -307
- package/WaterFlow/FloatingDuck.ts +0 -445
- package/WaterFlow/GPUFluidSimulation.d.ts +0 -56
- package/WaterFlow/GPUFluidSimulation.js +0 -785
- package/WaterFlow/GPUFluidSimulation.ts +0 -827
- package/WaterFlow/ReservoirSystem.d.ts +0 -106
- package/WaterFlow/ReservoirSystem.js +0 -621
- package/WaterFlow/ReservoirSystem.ts +0 -754
- package/WaterFlow/SPHField.d.ts +0 -35
- package/WaterFlow/SPHField.js +0 -204
- package/WaterFlow/SPHField.ts +0 -228
- package/WaterFlow/ScreenSpaceFluidPass.d.ts +0 -69
- package/WaterFlow/ScreenSpaceFluidPass.js +0 -736
- package/WaterFlow/ScreenSpaceFluidPass.ts +0 -789
- package/WaterFlow/ShallowWaterSurface.d.ts +0 -45
- package/WaterFlow/ShallowWaterSurface.js +0 -441
- package/WaterFlow/ShallowWaterSurface.ts +0 -510
- package/WaterFlow/WaterFlow.d.ts +0 -87
- package/WaterFlow/WaterFlow.js +0 -171
- package/WaterFlow/WaterFlow.ts +0 -219
- package/WaterFlow/types.d.ts +0 -161
- package/WaterFlow/types.js +0 -1
- package/WaterFlow/types.ts +0 -189
- package/WaterFlow/waterFlowPass.d.ts +0 -3
- package/WaterFlow/waterFlowPass.js +0 -20
- package/WaterFlow/waterFlowPass.ts +0 -40
- package/shaders/debugExcludedShader.d.ts +0 -13
- package/shaders/debugExcludedShader.js +0 -46
- package/shaders/debugExcludedShader.ts +0 -48
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
|
-
//
|
|
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
|
* 与主循环竖直门配合后失明窗口彻底消失。**顶点**才是防翻墙的那条线,不是初速度本身。
|
|
@@ -254,25 +252,27 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
254
252
|
// far = R√2。菱形几何一个数没变(R=1.06 → 外接框 2.12×2.12、斜弦 45°、首尾闭合),
|
|
255
253
|
// 只是不再经过「局部单位」这道换算。竖向的 2cm 探测余量见 `VERTICAL_SKIN`。
|
|
256
254
|
//
|
|
257
|
-
// 【停位口径(现行唯一,皇上 2026-
|
|
258
|
-
//
|
|
259
|
-
// 减去当前垂距 = deficit
|
|
260
|
-
//
|
|
261
|
-
// ⇒
|
|
255
|
+
// 【停位口径(现行唯一,皇上 2026-10-11 A 档改判 = 「解析贴合 + 只削法向速度」)】
|
|
256
|
+
// 射线在 far 内命中 = 碰撞;「还差多远才脱开」= 菱形沿墙法线的可达距离
|
|
257
|
+
// reach = R·max(|n·右|, |n·前|) ∈ [R/√2, R] 减去当前垂距 = deficit,取全轮**最大**者当读数;
|
|
258
|
+
// 位置修正沿该面法线一格一格推,每格 = min(RETREAT_SKIN, deficit) ⇒ 大欠距仍走整格,
|
|
259
|
+
// **收尾那一格按欠距截短** ⇒ 落点正好贴在包络边上(旧写法最后一格也推满 1cm → 过冲 → 来回甩)。
|
|
260
|
+
// 速度只削「往墙里」那一份(v −(v·n)n),切向保留 ⇒ 贴墙能滑,撞墙不再「加速→清零」锯齿。
|
|
261
|
+
// ⇒ 停位仍只由 **R** 一个数决定(R=1.06 → 沿法线 0.749~1.06m);射线包络决定视点与墙之间
|
|
262
262
|
// 还留 0.40~0.71m 空气隙,这是皇上 2026-09-06 终裁布局自带、且亲眼验收过的量,不要当 bug 修。
|
|
263
263
|
/**
|
|
264
264
|
* 单步最大位移(连续碰撞检测用):每帧位移会被切成若干 ≤ 本值的小步,逐步做碰撞检测。
|
|
265
265
|
* ⚠️ 必须明显小于水平包络的可达距离(R=1.06 时沿法线 0.749~1.06m),否则掉帧时一步就跨过墙体。
|
|
266
266
|
* ⚠️ 本值同时决定「单次碰撞需要修正多深」——子步越小,玩家每次陷进墙里越浅,
|
|
267
|
-
*
|
|
267
|
+
* 迭代要走的格数就越少(一格 ≤ RETREAT_SKIN,总位移 ≈ 该子步的穿透深度),
|
|
268
268
|
* 不会出现「一下子回弹很多」。
|
|
269
269
|
* 0.25 → 0.1:单次最大穿透从 0.25 降到 0.1,回弹量同比变小(子步数相应上调)。
|
|
270
270
|
*/
|
|
271
271
|
MAX_STEP_DISTANCE = 0.1;
|
|
272
272
|
/** 位移细分的最大段数(防止极端速度/长卡顿把一帧切成几百步拖垮帧率) */
|
|
273
273
|
MAX_STEP_COUNT = 32;
|
|
274
|
-
// 垂直碰撞范围 =
|
|
275
|
-
// =
|
|
274
|
+
// 垂直碰撞范围 = 从玩家视点到「该射线所保护的那一面」的距离
|
|
275
|
+
// = 视点到脚下射线保护面的距离 1.0(下方两个常量是**口径**,不是几何)
|
|
276
276
|
// 对应 xsh-react-three 中 collideVolume(=3)= 控制器到地面的距离
|
|
277
277
|
// xsh 中 far = collideVolume, 响应 = collideVolume - distance
|
|
278
278
|
// 此处 far = DOWN_RANGE/UP_RANGE + SKIN,响应 = DOWN_RANGE/UP_RANGE − distance
|
|
@@ -281,9 +281,9 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
281
281
|
// 我提醒过这一条常量身兼两职(探测长度 + 站位基准),他追选定案「**直接改站位基准**」,
|
|
282
282
|
// 所以拆成两条各自独立的常量,**不再**共用 VERTICAL_RANGE=1.0:
|
|
283
283
|
// · DOWN_RANGE = 1.5:下射线 far = 1.5+SKIN,响应 downPen = 1.5 − distance
|
|
284
|
-
// → 落地后**眼睛离地 1.5m
|
|
284
|
+
// → 落地后**眼睛离地 1.5m**。
|
|
285
285
|
// · UP_RANGE = 0.3:上射线 far = 0.3+SKIN,响应 upPen = 0.3 − distance
|
|
286
|
-
// → 头顶到面时**眼睛离顶 0.3m
|
|
286
|
+
// → 头顶到面时**眼睛离顶 0.3m**,由上射线阻止继续上移。
|
|
287
287
|
// 且 0.3~1.0m 之间的低矮障碍物从此探不到(上射线变短的直接后果)。
|
|
288
288
|
// ⚠️ 跳跃/重力/台阶吸附的其余口径一字未动,只换这两个基准数。
|
|
289
289
|
// ⭐ 2026-09-26 皇上旨「能调节的参数都做滚轮输入框」→ 两条改为公共可写(面板「参数旋钮」可拧),
|
|
@@ -398,13 +398,19 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
398
398
|
* 即 R 就是「玩家中心离墙多远开始判定碰撞」的半径。若 R 小于停位需求,人必须**先陷进墙里**
|
|
399
399
|
* 才会被射线检测到,随后每帧被位置修正弹回一点 → 皇上所见「侧撞弹很远」。
|
|
400
400
|
* 探针实测(`tmp/TMP_R_side_effect.mjs`,斜 45° 撞墙角 / 正对单墙同口径):
|
|
401
|
-
* R=0.30 → 停位垂距 **0.2128
|
|
401
|
+
* R=0.30 → 停位垂距 **0.2128**,射线包络过小,容易贴墙。
|
|
402
402
|
* R=0.50 → 0.3541(贴到墙面) 每帧 0.036
|
|
403
|
-
* R=0.60 → 0.4243
|
|
403
|
+
* R=0.60 → 0.4243。
|
|
404
404
|
* R=0.80 → 0.5448 ✓ / R=1.06 → 0.7247 ✓(每帧修正仅 0.002)
|
|
405
|
-
* ⇒ 取 **0.6
|
|
405
|
+
* ⇒ 取 **0.6**:为射线包络保留足够的贴墙间距。
|
|
406
406
|
* ⭐ 2026-10-08 皇上旨:下限放宽到 **0.2**、上限收到 **1.0**(编辑框范围 0.2~1.0)。
|
|
407
407
|
* ⚠️ R<0.6 仍会贴墙穿模(R=0.5 贴到墙面、R=0.3 明显穿模),皇上按需拧;下限放开是给窄通道/贴墙演示留余地。
|
|
408
|
+
* ⭐ 2026-10-11 A 档复核(tmp→test/fpsCollideFit.test.ts,正对 1m 墙):
|
|
409
|
+
* 停位现在**逐位等于 R**(眼停在面前 R 处 ⇒ 身体入墙深度 = 0.35 − R,一条直线):
|
|
410
|
+
* R=0.20 → 入墙 **0.1500m** R=0.30 → **0.0500m** R=0.35 → **0.0000m**
|
|
411
|
+
* R=0.50 / 0.60 / 1.00 → **0.0000m**(身体不出面)
|
|
412
|
+
* ⇒ 抖不抖与 R 无关(A 档后全档抖动 0.0000m);R<0.35 只剩「身体陷进墙多少」这一件事,
|
|
413
|
+
* 皇上 10-11 定「旋钮不动,只把数字写明」⇒ 要真焊死得改这里的下限,别在推离公式里偷偷加夹取。
|
|
408
414
|
*/
|
|
409
415
|
static MIN_HORIZONTAL_RAY_RADIUS = 0.2;
|
|
410
416
|
/** 水平射线半径 R 的安全上限(⭐ 2026-10-08 皇上旨收到 1.0:再大包络远大于停位需求、回弹精度已够,白烧射程) */
|
|
@@ -464,7 +470,8 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
464
470
|
digits: 2,
|
|
465
471
|
unit: "m",
|
|
466
472
|
frozen: true,
|
|
467
|
-
desc: "菱形半对角线 R,外接框边长 = 2R(= 离墙多远开始判定碰撞的半径)。越大包络越大、回弹方向越准;R<0.6 仍会贴墙穿模(R=0.5 贴墙面、R=0.3 明显穿模),已放宽到 0.2~1.0
|
|
473
|
+
desc: "菱形半对角线 R,外接框边长 = 2R(= 离墙多远开始判定碰撞的半径)。越大包络越大、回弹方向越准;R<0.6 仍会贴墙穿模(R=0.5 贴墙面、R=0.3 明显穿模),已放宽到 0.2~1.0 供按需拧。" +
|
|
474
|
+
"⭐ 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),旋钮范围按皇上旨不动",
|
|
468
475
|
get: (fps) => fps.CHORD_RHOMBUS_R,
|
|
469
476
|
set: (fps, v) => fps.setHorizontalRayRadius(v),
|
|
470
477
|
},
|
|
@@ -581,22 +588,9 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
581
588
|
step: 0.005,
|
|
582
589
|
digits: 3,
|
|
583
590
|
unit: "m",
|
|
584
|
-
desc: "⭐ 2026-
|
|
591
|
+
desc: "⭐ 2026-10-11 皇上 A 档改判:每轮**只沿「欠距最大那一面」的法线**推一格,且这一格取 min(本值, 该面欠距) ⇒ 大欠距仍是一格一格往外走(皇上 10-07「1 个单位 1 个单位的修正」),只有**收尾那一格按欠距截短**,落点贴在包络边上(旧写法每条命中射线各推满一格、最后一格也推满 ⇒ 过冲 ⇒ 贴墙来回甩 = 抖动)。" +
|
|
585
592
|
"⚠️ 上限以下别贪小:0.03/0.02/0.01 体检台判级完全相同,拧到 0.005 才有改善但「跳跃+冲刺+掉帧」那个现场开始穿墙(格长 = 每帧出水量)。迭代推到全部射线脱开为止",
|
|
586
593
|
},
|
|
587
|
-
{
|
|
588
|
-
group: "回弹与手感",
|
|
589
|
-
label: "每小帧轮数上限",
|
|
590
|
-
key: "ROUNDS_PER_SUBSTEP",
|
|
591
|
-
min: 1,
|
|
592
|
-
max: 500,
|
|
593
|
-
step: 1,
|
|
594
|
-
digits: 0,
|
|
595
|
-
unit: "轮",
|
|
596
|
-
desc: "⭐ 皇上 2026-10-07 裁决加:一个物理小帧里 while 最多走几轮就落地(默认 500 = 与「每帧迭代预算」同档 ⇒ 现状一字不变)。" +
|
|
597
|
-
"**这颗才是「一次修多少」的真闸门**:10-07 实测每次落地量 = 这一小帧往墙里挤进多深(2/8/10cm 都这么来),跟一格多远无关 ⇒ 要「一次只修几格、下一小帧再修」只能拧这里。" +
|
|
598
|
-
"⚠️ 拧小的代价:一秒能挤出去的总量变小,高速冲刺顶墙时「挤进来的比推出去的快」⇒ 越陷越深(格长拧到 0.005 那档体检台 40 号现场就判 ❌穿墙,同一个机理)。先拧着看手感,别把默认改小",
|
|
599
|
-
},
|
|
600
594
|
// ===== 移动与视角 =====
|
|
601
595
|
{
|
|
602
596
|
group: "移动与视角",
|
|
@@ -761,18 +755,13 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
761
755
|
// 缓存本帧的按键输入单位方向(水平)。initPlayerInputDir() 每物理步刷新一次;
|
|
762
756
|
// 零向量表示「本帧没按方向键」→ 最终方向退化为纯反弹方向。
|
|
763
757
|
_inputDirVec = new Vector3();
|
|
764
|
-
/**
|
|
765
|
-
* 本帧**进碰撞之前**的水平速率(米/秒)。皇上八旨要求「移动向量的长度保持不变」,
|
|
766
|
-
* 而碰撞会把 `playerVelocity.x/z` 清零,所以必须在 updatePlayer 入口先记一份。
|
|
767
|
-
*/
|
|
768
|
-
_preCollideHorizSpeed = 0;
|
|
769
758
|
// ========== 回弹预览(皇上 2026-09-25 旨:「加个开关可以关闭碰撞修正位置的计算,方便观察回弹向量」)==========
|
|
770
759
|
// 打开后:**不写位置**(见 playerCollisions 末尾唯一的 translate 闸门),但方向/推离量照常算、
|
|
771
760
|
// 黄→白方向线照常画 → 肉眼核对「算出来的方向对不对」而不被人推开。
|
|
772
761
|
// ⚠️ 只闸水平+竖向的**位置写入**;削速度、playerOnFloor、事件派发全部保留,
|
|
773
762
|
// 否则预览模式下重力会一帧帧攒着把人送到地心并触发掉出世界复位。
|
|
774
763
|
// ⭐⭐ 2026-09-26 皇上七旨「回弹预览开了, 就关不掉」⇒ 本字段改成**带暂存的访问器**:
|
|
775
|
-
// 开 =
|
|
764
|
+
// 开 = 记下当前视点位置;关 = **还原**那个位置 + 清掉水平速度。
|
|
776
765
|
// 病根(实测,墙面板块 x∈[5.0,6.0]、R=1.06):预览既然不写位置,人就照着自己的
|
|
777
766
|
// 行走速度(speed=6 ⇒ 终端 37.5m/s ⇒ 每帧 0.62m)一路**穿墙走出去**,120 帧后中心
|
|
778
767
|
// 已经在 x=138;这一路上只要停在任何一处「两面墙的票互相抵消」的几何里,位置修正就
|
|
@@ -780,15 +769,13 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
780
769
|
// deficit 恒为 0.5600、位移 0。看着就是「预览关了位置修正也不回来了」。
|
|
781
770
|
// ⇒ 预览是**观察**用的,那一段时间里的位移本来就不作数,关掉即回到打开前那一刻。
|
|
782
771
|
_previewRetreat = false;
|
|
783
|
-
/**
|
|
784
|
-
|
|
785
|
-
_obsSavedEnd = new Vector3();
|
|
772
|
+
/** 开启预览那一刻的玩家视点位置(关闭时还原) */
|
|
773
|
+
_obsSavedPosition = new Vector3();
|
|
786
774
|
/** 是否已有可还原的暂存(没进过观察模式就没有,避免构造期赋值把人还原到原点) */
|
|
787
775
|
_obsHasSave = false;
|
|
788
|
-
/** 进观察模式(回弹预览 /
|
|
776
|
+
/** 进观察模式(回弹预览 / 单步):记下当前视点位置,退出时还原 */
|
|
789
777
|
_saveObsPos() {
|
|
790
|
-
this.
|
|
791
|
-
this._obsSavedEnd.copy(this.playerCollider.end);
|
|
778
|
+
this._obsSavedPosition.copy(this.playerPosition);
|
|
792
779
|
this._obsHasSave = true;
|
|
793
780
|
}
|
|
794
781
|
/**
|
|
@@ -799,9 +786,8 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
799
786
|
_restoreObsPos() {
|
|
800
787
|
if (!this._obsHasSave)
|
|
801
788
|
return;
|
|
802
|
-
this.
|
|
803
|
-
this.
|
|
804
|
-
this.camera.position.copy(this.playerCollider.end);
|
|
789
|
+
this.playerPosition.copy(this._obsSavedPosition);
|
|
790
|
+
this.camera.position.copy(this.playerPosition);
|
|
805
791
|
this.playerVelocity.x = 0;
|
|
806
792
|
this.playerVelocity.z = 0;
|
|
807
793
|
this._obsHasSave = false;
|
|
@@ -822,8 +808,7 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
822
808
|
// xsh 的骨架只有一个 while(`FPSControls.tsx` 的 `computerPosition`):
|
|
823
809
|
// ① 在**候选位置**(真实位置 + 已累计的 positionIncremental)上打水平射线
|
|
824
810
|
// ② 有命中 ⇒ 沿「离开命中点」的方向推**一小格**(它写死 0.03)并续跑下一轮
|
|
825
|
-
// ③ 一轮下来没命中 ⇒ goOnComputer=false ⇒ 收工;整圈上限 = MAX_ITERATIONS(皇上 10-03 旨:500
|
|
826
|
-
// 单趟 while 另有「本轮数上限」闸门(常态帧 = ROUNDS_PER_SUBSTEP,皇上 10-07 裁决加,默认 500 同档 ⇒ 不变)
|
|
811
|
+
// ③ 一轮下来没命中 ⇒ goOnComputer=false ⇒ 收工;整圈上限 = MAX_ITERATIONS(皇上 10-03 旨:500)轮
|
|
827
812
|
// ④ 循环外**唯一一次** position.add(incremental)
|
|
828
813
|
// ⇒ 旧的「测试法逐级逼近」(六旨:1 → 0.1 → 0.01 三档、武装/判定/接受/撤销、每帧出水量 ≥
|
|
829
814
|
// 进水量那套配套保险丝)**整条删除**。位置仍是一格一格挪出去的(皇上要的一步一步看那个形态不变),
|
|
@@ -849,18 +834,6 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
849
834
|
_tempEuler = new Euler(0, 0, 0, "YXZ");
|
|
850
835
|
// 最大迭代次数(皇上 2026-10-03 旨:「那计算次数改成不超过 500」⇒ 100 → 500)
|
|
851
836
|
MAX_ITERATIONS = 500;
|
|
852
|
-
/**
|
|
853
|
-
* 一个**物理小帧**里 while 最多走几轮就落地(皇上 2026-10-07 裁决「加一个每小帧轮数上限」)。
|
|
854
|
-
* 默认 500 = 与 `MAX_ITERATIONS` 同档 ⇒ 现状一字不变;拧小才是他要的「一次只修几格、下一小帧再修」。
|
|
855
|
-
* ⭐ 为什么拧格长没用(10-07 实测):常态帧会一直迭代到「8 条射线全部脱开」才落地 ⇒
|
|
856
|
-
* **每次落地量 = 这一小帧往墙里挤进多深**,与一格多远无关(格长 0.03→0.01 判级一字未变)。
|
|
857
|
-
* 一格 = 一轮里每条命中各推的距离 ⇒ 要限制"一次修多少"只能限制**轮数**。
|
|
858
|
-
* ⚠️ 代价(不许瞒):出水量 = 轮数 × 每轮票数 × 格长,默认每帧 2 个小帧。拧到 1 轮 + 1cm 格长 ⇒
|
|
859
|
-
* 一秒最多挤出约 120cm 的量看着够,但高速冲刺顶墙时「这一帧挤进来的」可能比「两小帧推出去的」多
|
|
860
|
-
* ⇒ 越陷越深,格长拧到 0.005 那一档体检台 40 号现场就是这么判 **❌穿墙** 的。
|
|
861
|
-
* 所以默认不动,先让他在线上拧着看手感。
|
|
862
|
-
*/
|
|
863
|
-
ROUNDS_PER_SUBSTEP = 500;
|
|
864
837
|
// ⚠️ 每帧碰撞迭代总预算(皇上 2026-09-03:墙角偶尔卡住,要求碰撞计算次数不超过 100;
|
|
865
838
|
// 2026-10-03 同一位皇上把这条上限抬到 500 —— 机制一字未改,只是额度变大):
|
|
866
839
|
// while 循环原本的上限 100 是「单次 playerCollisions」的,而一个 update() 帧内会调
|
|
@@ -877,10 +850,24 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
877
850
|
_probeRaycaster = new Raycaster();
|
|
878
851
|
// 临时向量(复用避免 GC)
|
|
879
852
|
_tempVec3 = new Vector3();
|
|
880
|
-
// xsh 移植(2026-09-28):候选位置(= 控制器位置 + positionIncremental
|
|
881
|
-
// 与推离向量专用临时量(复用避免 GC)
|
|
853
|
+
// xsh 移植(2026-09-28):候选位置(= 控制器位置 + positionIncremental,射线原点锚)临时量
|
|
882
854
|
_candPos = new Vector3();
|
|
883
|
-
|
|
855
|
+
/**
|
|
856
|
+
* ⭐ 2026-10-11 A 档(解析贴合)专用临时量,全部复用避免 GC:
|
|
857
|
+
* · `_fitNormal` = 助手里「本轮这条命中面」指向玩家一侧的单位水平法线;
|
|
858
|
+
* · `_horizNormal` = 本轮**欠距最大**那面的法线(推离方向 + 速度闸门共用一个方向,绝不各算一遍);
|
|
859
|
+
* · `_fitVec` = (候选中心 − 命中点) 临时量;
|
|
860
|
+
* · `_rhombusAxisX/_rhombusAxisZ` = 本帧菱形两条对角轴的世界方向(随 yaw 转,恒水平)。
|
|
861
|
+
*/
|
|
862
|
+
_fitNormal = new Vector3();
|
|
863
|
+
_horizNormal = new Vector3();
|
|
864
|
+
/** 本子步实际推离过的不同墙面法线。墙角需要同时裁掉两个朝内速度分量,
|
|
865
|
+
* 不能只用最后一次命中的面,否则两个面会在相邻帧交替抢占速度闸门而抖动。 */
|
|
866
|
+
_horizContactNormals = [new Vector3(), new Vector3(), new Vector3(), new Vector3()];
|
|
867
|
+
_horizContactCount = 0;
|
|
868
|
+
_fitVec = new Vector3();
|
|
869
|
+
_rhombusAxisX = new Vector3(1, 0, 0);
|
|
870
|
+
_rhombusAxisZ = new Vector3(0, 0, 1);
|
|
884
871
|
_tempVec4 = new Vector3();
|
|
885
872
|
// 位移细分用的临时向量(与 playerCollisions 内的 _tempVec4 分开,避免被覆盖)
|
|
886
873
|
_tempVec5 = new Vector3();
|
|
@@ -1066,6 +1053,43 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1066
1053
|
return null;
|
|
1067
1054
|
return n.normalize().y;
|
|
1068
1055
|
}
|
|
1056
|
+
/**
|
|
1057
|
+
* ⭐⭐ 2026-10-11 皇上改判 A 档「解析贴合」:一条水平命中射线**沿本面法线还差多少才脱开**。
|
|
1058
|
+
* 同时把该面的单位水平法线(指向玩家所在的一侧)写进 `this._fitNormal` 供调用方取用。
|
|
1059
|
+
*
|
|
1060
|
+
* 为什么要它:旧写法每轮对每条命中射线**整格**推 `RETREAT_SKIN`(1cm),而退出判据是「8 条射线
|
|
1061
|
+
* 全部脱开」⇒ 落点必然**过冲最多一格**(实测弹回 1.05cm),下一帧再从过冲处走回来 =
|
|
1062
|
+
* 皇上所见「顶进去又弹回来一直抖」(无头复现:抖峰峰 0.0193m,60fps 下 34 次往复/40 帧,
|
|
1063
|
+
* 与 R / 墙厚 / 入射角 / 帧率全无关)。按欠距推到「刚好脱开」就不再有过冲。
|
|
1064
|
+
*
|
|
1065
|
+
* 判据(恒等式,不是拍的):碰撞包络是顶点在 ±R·x̂、±R·ẑ 的**菱形**,它沿方向 n 的支持距离
|
|
1066
|
+
* req = R·max(|n·x̂|, |n·ẑ|) ∈ [R/√2, R]。候选中心沿 n 到命中面的垂距为 perp(在实体内为负),
|
|
1067
|
+
* 那么「还差 deficit = req − perp」推满就正好贴在包络边上。
|
|
1068
|
+
*
|
|
1069
|
+
* 法线取向:**与射线行进方向相反**的一侧(three 对 FrontSide 只回正面命中,所以常态天然满足;
|
|
1070
|
+
* DoubleSide 拿到背面时按此翻号)。⚠️ 不能按「指向中心」翻号——中心已经陷进 1m 厚墙板时,
|
|
1071
|
+
* 那样翻会把人往墙体更深处推(旧写法正是靠两侧票互相抵消才卡在板正中)。
|
|
1072
|
+
*
|
|
1073
|
+
* @returns deficit(米,≤0 = 已经脱开)。退化场景返回 0:
|
|
1074
|
+
* · 拿不到面法线(无 face / 零向量)
|
|
1075
|
+
* · 命中面近乎水平(法线压平后是零向量,如贴着地面/台面掠射)⇒ 不该把它当墙侧推
|
|
1076
|
+
*/
|
|
1077
|
+
_horizFitDeficit(worldPos, rayDir, interset) {
|
|
1078
|
+
const n = this._fitNormal;
|
|
1079
|
+
if (!this._transformFaceNormalWorld(interset, n))
|
|
1080
|
+
return 0;
|
|
1081
|
+
n.y = 0; // 本布局水平射线在 y=−0.7/−0.75(皇上 09-26 五旨),推离必须纯水平(同旧「偏差①」)
|
|
1082
|
+
if (n.lengthSq() < 1e-8)
|
|
1083
|
+
return 0;
|
|
1084
|
+
n.normalize();
|
|
1085
|
+
// 指向「射线来的那一侧」= 逆着射线行进方向,即离开实体的方向
|
|
1086
|
+
if (n.dot(rayDir) > 0)
|
|
1087
|
+
n.negate();
|
|
1088
|
+
const perp = n.dot(this._fitVec.copy(worldPos).sub(interset.point));
|
|
1089
|
+
const ax = Math.abs(n.dot(this._rhombusAxisX));
|
|
1090
|
+
const az = Math.abs(n.dot(this._rhombusAxisZ));
|
|
1091
|
+
return this.CHORD_RHOMBUS_R * Math.max(ax, az) - perp;
|
|
1092
|
+
}
|
|
1069
1093
|
/**
|
|
1070
1094
|
* 脚下深探:往下问 VOID_PROBE_DOWN 这么长一段,有没有地面(2026-09-25,穿地板自救用)。
|
|
1071
1095
|
* @returns true = 下方有地面 → 只是正常腾空/下落,不是穿模
|
|
@@ -1139,14 +1163,12 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1139
1163
|
resetPlayer(position = new Vector3(0, 0, 0)) {
|
|
1140
1164
|
this.playerVelocity.set(0, 0, 0);
|
|
1141
1165
|
this.prePlayerVelocity.set(0, 0, 0);
|
|
1142
|
-
this.
|
|
1143
|
-
this.
|
|
1144
|
-
|
|
1145
|
-
this.camera.position.copy(this.playerCollider.end);
|
|
1146
|
-
// ⭐ 复位 = 换现场 ⇒ 把相机/胶囊位置基准挪到新位置。
|
|
1166
|
+
this.playerPosition.set(position.x, position.y + 1, position.z);
|
|
1167
|
+
this.camera.position.copy(this.playerPosition);
|
|
1168
|
+
// ⭐ 复位 = 换现场 ⇒ 把相机/视点位置基准挪到新位置。
|
|
1147
1169
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1170
|
+
/** 玩家视点。所有碰撞射线均以此点为局部原点。 */
|
|
1171
|
+
playerPosition = new Vector3(0, 1, 0);
|
|
1150
1172
|
// 时钟对象
|
|
1151
1173
|
clock = new Clock();
|
|
1152
1174
|
prePlayerVelocity = new Vector3();
|
|
@@ -1191,7 +1213,7 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1191
1213
|
* `depthWrite:true`(three-base/Ground1/Ground1.ts:107);哪天地板改回
|
|
1192
1214
|
* transparent + depthWrite:false,射线线会重新穿地浮出来。
|
|
1193
1215
|
* - 回弹方向线(黄→白)= **false**:「回弹向量线保持穿透显示」。
|
|
1194
|
-
*
|
|
1216
|
+
* 它是「本帧要看的向量」,起点在玩家位置、方向常指着墙里,开遮挡会被墙面
|
|
1195
1217
|
* 或墙面吃掉半截 → 观察目的直接落空,故保持常显在最前。
|
|
1196
1218
|
*/
|
|
1197
1219
|
_createDebugLine(depthTest) {
|
|
@@ -1261,7 +1283,7 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1261
1283
|
this._debugRayGroup = null;
|
|
1262
1284
|
}
|
|
1263
1285
|
init(canvas) {
|
|
1264
|
-
this.
|
|
1286
|
+
this.playerPosition.set(0, 1, 0);
|
|
1265
1287
|
document.addEventListener("keydown", this.keydown);
|
|
1266
1288
|
document.addEventListener("keyup", this.keyup);
|
|
1267
1289
|
canvas.addEventListener("pointerdown", this.mousedown);
|
|
@@ -1348,11 +1370,10 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1348
1370
|
controls = (deltaTime) => {
|
|
1349
1371
|
const speedDelta = deltaTime * (this.playerOnFloor ? 25 : 8) * this.speed;
|
|
1350
1372
|
// ⭐ 2026-09-26 皇上旨「碰撞就是按射线拾取算的,不用搞太复杂」:
|
|
1351
|
-
// 旧的「接触法线缓存 + 加速度保速投影」整块删除 ——
|
|
1352
|
-
//
|
|
1353
|
-
//
|
|
1354
|
-
//
|
|
1355
|
-
// 见 playerCollisions 末尾那段。清零本身照旧。
|
|
1373
|
+
// 旧的「接触法线缓存 + 加速度保速投影」整块删除 —— 这里**只管往输入方向加速**,
|
|
1374
|
+
// 撞墙的手感全部由 playerCollisions 负责:位置侧「解析贴合」(欠距最大那面推格、
|
|
1375
|
+
// 最后一格截短),速度侧「只削往墙里那一份」(⚠️ 2026-10-11 A 档改判,此处不再是
|
|
1376
|
+
// playerVelocity.x/z 整条清零 ⇒ 贴墙能沿墙滑行,见 playerCollisions 末尾那段)。
|
|
1356
1377
|
const accel = this._accelVec.set(0, 0, 0);
|
|
1357
1378
|
if (this.keyStates["KeyW"]) {
|
|
1358
1379
|
accel.addScaledVector(this.getForwardVector(), speedDelta);
|
|
@@ -1393,9 +1414,6 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1393
1414
|
damping *= 0.1;
|
|
1394
1415
|
}
|
|
1395
1416
|
this.playerVelocity.addScaledVector(this.playerVelocity, damping);
|
|
1396
|
-
// ⭐ 2026-09-26 皇上八旨「位置修正结束后…移动向量的长度保持不变」:本帧**进碰撞前**的
|
|
1397
|
-
// 水平速率快照。碰撞会把速度清零,子步跑完再想要「原来的长度」就没有了,必须在入口取。
|
|
1398
|
-
this._preCollideHorizSpeed = Math.hypot(this.playerVelocity.x, this.playerVelocity.z);
|
|
1399
1417
|
// 位移细分(连续碰撞):把本帧位移切成若干 ≤ MAX_STEP_DISTANCE 的小步,逐步推进并检测。
|
|
1400
1418
|
// 不细分的话,掉帧(deltaTime 被 clamp 到 0.05)时单步位移可达 25×0.025 ≈ 0.6,
|
|
1401
1419
|
// 远大于射线的检测余量(沿法线最小 reach R/√2 = 0.749 − 子步 0.1)→ 直接跨过墙体(穿墙)。
|
|
@@ -1413,7 +1431,7 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1413
1431
|
this.playerCollisions(s === stepCount - 1);
|
|
1414
1432
|
}
|
|
1415
1433
|
// 设置相机位置(第一人称默认值)
|
|
1416
|
-
this.camera.position.copy(this.
|
|
1434
|
+
this.camera.position.copy(this.playerPosition);
|
|
1417
1435
|
// 第三人称:偏移相机位置 + 朝向第一人称坐标
|
|
1418
1436
|
if (this.thirdPerson) {
|
|
1419
1437
|
const cosPitch = Math.cos(this._thirdPersonAngleX);
|
|
@@ -1428,7 +1446,7 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1428
1446
|
Math.cos(this._thirdPersonAngleY) *
|
|
1429
1447
|
cosPitch *
|
|
1430
1448
|
this.thirdPersonDistance;
|
|
1431
|
-
this.camera.lookAt(this.
|
|
1449
|
+
this.camera.lookAt(this.playerPosition);
|
|
1432
1450
|
}
|
|
1433
1451
|
// dev 调试模式:在相机本地坐标系叠加偏移(向后上方)
|
|
1434
1452
|
if (this.dev && !this.thirdPerson) {
|
|
@@ -1447,7 +1465,7 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1447
1465
|
// xsh: direction prop → _localRayDirs(归一化)
|
|
1448
1466
|
// xsh: raycaster.far = getDirection().length() → HORIZONTAL_FAR / downFar / upFar
|
|
1449
1467
|
// xsh: Group3D rotation={x:-euler.x} → yawQuat(pitch 补偿)
|
|
1450
|
-
// xsh: getIntersetResult(positionIncremental) → worldPos =
|
|
1468
|
+
// xsh: getIntersetResult(positionIncremental) → worldPos = playerPosition + positionIncremental
|
|
1451
1469
|
//
|
|
1452
1470
|
// 现行 10 条布局逐条表 = 上方「★ 矩形射线法 ★」权威规格块(唯一数据源,不在这里重复一遍)。
|
|
1453
1471
|
/** 输出一条碰撞日志(唯一出口 = 浏览器控制台;皇上:「日志就是浏览器的控制台」) */
|
|
@@ -1460,11 +1478,15 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1460
1478
|
// ⇒ 本方法整体 = xsh `FPSControls.tsx → computerPosition()` 的忠实移植,骨架只有:
|
|
1461
1479
|
// ① while 循环(皇上 09-03 定「一帧合计不超过 100」,10-03 旨改到 500;骨架与 xsh 的 i<100 同形);
|
|
1462
1480
|
// ② 每轮在「候选位置」(真实位置 + positionIncremental)上打 8 条水平斜弦射线,
|
|
1463
|
-
//
|
|
1464
|
-
//
|
|
1465
|
-
//
|
|
1481
|
+
// ⭐ 2026-10-11 皇上 A 档改判:每条命中算「沿本面法线还差多少才脱开」(deficit),
|
|
1482
|
+
// **只取欠距最大的那一面**,沿它的法线推 min(RETREAT_SKIN, deficit) ——
|
|
1483
|
+
// 一格一格的外形保留(皇上 10-07 旨「应该 1 个单位 1 个单位的修正」⇒ RETREAT_SKIN=0.01),
|
|
1484
|
+
// 但收尾那一格按欠距截短 ⇒ 落点贴包络边,不再整格过冲(旧 xsh 各票各推一整格 ⇒ 必过冲);
|
|
1485
|
+
// 累加进 positionIncremental,并 goOnComputer=true 续命下一轮;
|
|
1466
1486
|
// ③ 下射线命中 ⇒ 竖直速度清零 + y 钉到「命中面之上 DOWN_RANGE」(xsh: cv − distance);
|
|
1467
|
-
// 上射线命中 ⇒ 竖直速度清零 +
|
|
1487
|
+
// 上射线命中 ⇒ 竖直速度清零 + 修正(含三道闸天花板的血案守卫)
|
|
1488
|
+
// —— ⭐ 10-11 修好少括号的 bug 后,上射线**每轮都测**(旧代码被关进「下射线命中」分支里
|
|
1489
|
+
// ⇒ 腾空时跳起来顶不到天花板);
|
|
1468
1490
|
// ④ 某一轮全部射线脱开 ⇒ 收工;循环外唯一一次 translate 落地(血案护栏不变)。
|
|
1469
1491
|
// ⭐⭐ 2026-10-02 皇上旨「模拟修正,不是真实修正,把最终结果算好了再应用到真实的第一人称
|
|
1470
1492
|
// 控制器上面」⇒ 那个 translate 落的是**「本步位移 + 允许的修正」一次到位**:
|
|
@@ -1477,15 +1499,18 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1477
1499
|
// 本布局射线在 y=−0.7/−0.75(皇上 09-26 五旨),不压平会把 y 混进推离;
|
|
1478
1500
|
// · 水平射线 8 条(皇上 09-06/09-14 亲加的 6-9 共线反向组),xsh 原版 4 条;
|
|
1479
1501
|
// · 竖向保留 faceUp / 天花板三道闸守卫(「掉出世界」血案修复)。
|
|
1480
|
-
//
|
|
1502
|
+
// · ⭐ 2026-10-11 A 档:**只沿「欠距最大那一面」的法线**推,且最后一格按欠距截短
|
|
1503
|
+
// (xsh 是每条命中各推一整格 ⇒ 4 条命中同一面墙一轮推 4cm,退出时必过冲)。
|
|
1504
|
+
// 原 deficit / 票面合成(finalDir) / 逐格推离 / 白推保险丝等旧分支已拆除。
|
|
1505
|
+
// ⚠️ 措辞校正(10-11):上面这句里「deficit」那半已随 A 档**回归**(`_horizFitDeficit()`:
|
|
1506
|
+
// 沿命中面法线的菱形可达距离 R·max(|n·x̂|,|n·ẑ|) 减当前垂距);
|
|
1507
|
+
// 仍拆除的是「票面合成 finalDir / 白推保险丝」两件 —— 别照旧文案把它们捡回来。
|
|
1481
1508
|
const positionIncremental = this._tempVec4.set(0, 0, 0);
|
|
1482
1509
|
this._refreshInputDir(); // 台账 ix/iz 列还要用(与碰撞计算无关)
|
|
1483
1510
|
const recordDebug = (this.dev || this.thirdPerson) && isFinalStep;
|
|
1484
1511
|
let debugRecorded = !recordDebug;
|
|
1485
1512
|
this._horizHit = false;
|
|
1486
|
-
|
|
1487
|
-
// 常态帧用旋钮 ROUNDS_PER_SUBSTEP(默认 500 与 MAX_ITERATIONS 同档 ⇒ 现状完全不变)。
|
|
1488
|
-
const roundCap = this.ROUNDS_PER_SUBSTEP;
|
|
1513
|
+
this._horizContactCount = 0;
|
|
1489
1514
|
const downRange = this.DOWN_RANGE;
|
|
1490
1515
|
const upRange = this.UP_RANGE;
|
|
1491
1516
|
let i = 0;
|
|
@@ -1500,7 +1525,7 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1500
1525
|
// ⭐ 2026-10-02 模拟修正:真实 collider 此刻**还没被本步位移推过**,
|
|
1501
1526
|
// 所以锚点 = 真实位置 + 本步位移 =「这一步走完会站在哪儿」的**模拟位置**
|
|
1502
1527
|
// (数值上与旧写法「先 translate 再取 end」完全相同 ⇒ 几何、方向、测试基线都不变)。
|
|
1503
|
-
const basePos = this._tempVec3.copy(this.
|
|
1528
|
+
const basePos = this._tempVec3.copy(this.playerPosition).add(this._subStep);
|
|
1504
1529
|
// xsh: 射线原点 = 控制器位置 + positionIncremental(getIntersetResult(incremental) 语义)
|
|
1505
1530
|
const worldPos = this._candPos.copy(basePos).add(positionIncremental);
|
|
1506
1531
|
// xsh: Group3D rotation={x:-euler.x} 补偿 pitch → 只跟随 yaw
|
|
@@ -1508,12 +1533,21 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1508
1533
|
for (const ray of this._rays) {
|
|
1509
1534
|
ray.updateWorld(yawQuat, worldPos);
|
|
1510
1535
|
}
|
|
1536
|
+
// ⭐ 2026-10-11 A 档:菱形两条对角轴的世界方向(只随 yaw,恒水平)——贴合量 req 要用它
|
|
1537
|
+
this._rhombusAxisX.set(1, 0, 0).applyQuaternion(yawQuat);
|
|
1538
|
+
this._rhombusAxisZ.set(0, 0, 1).applyQuaternion(yawQuat);
|
|
1511
1539
|
const horizontalFar = this.HORIZONTAL_FAR; // = R√2(xsh: far = direction.length())
|
|
1512
1540
|
const downFar = downRange + this.VERTICAL_SKIN;
|
|
1513
1541
|
const upFar = upRange + this.VERTICAL_SKIN;
|
|
1514
1542
|
const devVisualFarD = downRange;
|
|
1515
1543
|
const devVisualFarU = upRange;
|
|
1516
|
-
// ==========
|
|
1544
|
+
// ========== 水平半区(⭐ 2026-10-11 皇上改判 A 档 = 解析贴合,替代「每条命中各推一整格」)==========
|
|
1545
|
+
// 每轮:所有命中射线各算「沿本面法线还差多少才脱开」(deficit),**只取欠距最大的那一面**,
|
|
1546
|
+
// 沿它的法线推 `min(RETREAT_SKIN, deficit)` —— 一格 1cm 的上限照旧(皇上 10-07「1 个单位
|
|
1547
|
+
// 1 个单位的修正」),但**最后一格只推欠距本身**,落点贴在包络边上,不再整格过冲。
|
|
1548
|
+
// · 为什么不再各票相加:4 条命中同一面墙时旧写法一轮推 4cm,退出时必然过头;
|
|
1549
|
+
// · 为什么只取最大:墙角两面墙各欠 3cm 时,先脱开深的那面,下一轮再补另一面(≤2~3 轮)。
|
|
1550
|
+
let bestDeficit = 0;
|
|
1517
1551
|
for (let ri = 2; ri < this._rays.length; ri++) {
|
|
1518
1552
|
const ray = this._rays[ri];
|
|
1519
1553
|
ray.far = horizontalFar;
|
|
@@ -1543,26 +1577,28 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1543
1577
|
interset = intersets[j];
|
|
1544
1578
|
}
|
|
1545
1579
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
this._horizHit = true;
|
|
1562
|
-
// 方向线/台账的「修正方向」快照 = 最后一次命中的推离方向
|
|
1563
|
-
// ⭐ 皇上 10-07 旨「每执行一次,偏移量是多少」⇒ 按钮那一次:一格一行,带累计
|
|
1580
|
+
const deficit = this._horizFitDeficit(worldPos, ray.direction, interset);
|
|
1581
|
+
if (deficit > bestDeficit) {
|
|
1582
|
+
bestDeficit = deficit;
|
|
1583
|
+
this._horizNormal.copy(this._fitNormal); // 与推离方向同一个数,绝不分两处算
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
if (bestDeficit > 1e-6) {
|
|
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;
|
|
1564
1595
|
}
|
|
1565
1596
|
}
|
|
1597
|
+
if (!known && this._horizContactCount < this._horizContactNormals.length) {
|
|
1598
|
+
this._horizContactNormals[this._horizContactCount++].copy(this._horizNormal);
|
|
1599
|
+
}
|
|
1600
|
+
goOnComputer = true;
|
|
1601
|
+
this._horizHit = true;
|
|
1566
1602
|
}
|
|
1567
1603
|
// ========== 向下射线(索引 0 = upTodwomRay)==========
|
|
1568
1604
|
const downRay = this._rays[0];
|
|
@@ -1624,117 +1660,123 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1624
1660
|
// **不能**沿用旧探针的 `!debugRecorded`:`debugRecorded = !recordDebug`,而 `recordDebug`
|
|
1625
1661
|
// 跟着面板「碰撞射线」(dev) 走 → dev 关着时恒 true → 整段被跳过,
|
|
1626
1662
|
// 这正是皇上「在控制台里根本看不到下射线数值」的原因。
|
|
1627
|
-
//
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
//
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1663
|
+
// ⭐ 2026-10-11 皇上旨「少括号的 bug 一起修」:这个 `}` 是 `if (downIntersets.length > 0)` 的
|
|
1664
|
+
// 闭合括号,历史上被吃掉了 ⇒ 下面整段「向上射线」被关进「下射线命中」分支里。
|
|
1665
|
+
// 症状:腾空时脚下射线打不到地面 → 上射线**一次都不测** → 跳起来顶不到天花板(穿板顶)。
|
|
1666
|
+
// 顺带的第二个坑:while 的闭合括号跟着后移到「应用推离」注释之后(原 1843 行那个 `}`),
|
|
1667
|
+
// 于是「更新调试射线」那段也在每轮迭代里跑一遍 —— 删掉那个多余括号后它回到循环外,
|
|
1668
|
+
// 正好对上它自己的标签「dev 模式的**最后一步**」,且每帧只做一次。
|
|
1669
|
+
}
|
|
1670
|
+
// ========== 向上射线(索引 1 = dwomToUpRay)==========
|
|
1671
|
+
const upRay = this._rays[1];
|
|
1672
|
+
upRay.far = upFar;
|
|
1673
|
+
upRay.applyToRaycaster(this._raycaster);
|
|
1674
|
+
let upIntersets = this._raycaster.intersectObjects(this._colliders, false);
|
|
1675
|
+
// 调试:记录向上射线
|
|
1676
|
+
if (!debugRecorded) {
|
|
1677
|
+
const hasHit = upIntersets.length > 0;
|
|
1678
|
+
let intersetPoint = null;
|
|
1679
|
+
if (hasHit) {
|
|
1680
|
+
let closestDist = Infinity;
|
|
1681
|
+
for (const int of upIntersets) {
|
|
1682
|
+
const d = int.point.distanceTo(worldPos);
|
|
1683
|
+
if (d < closestDist) {
|
|
1684
|
+
closestDist = d;
|
|
1685
|
+
intersetPoint = int.point;
|
|
1644
1686
|
}
|
|
1645
1687
|
}
|
|
1646
|
-
upRay.recordHit(intersetPoint, devVisualFarU);
|
|
1647
|
-
upRay.responded = hasHit;
|
|
1648
|
-
debugRecorded = true;
|
|
1649
1688
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
const upPen = upRange - interset.distance;
|
|
1661
|
-
if (upPen > 0 && downIntersets.length === 0
|
|
1662
|
-
&& this.playerVelocity.y <= 0 && !this._floorBelow(worldPos)) {
|
|
1663
|
-
// ⭐⭐ 2026-09-25 皇上旨「顶出来,不穿模」(「下射线会陷入地面很长一个距离」的正解)
|
|
1664
|
-
// 上射线打到东西、**脚下却没有任何地面**、且人不正在往上蹿 → 我已经在这块面的
|
|
1665
|
-
// 下方/内部,再往下顶就是把它当「天花板」往下钻 = 死亡路径:
|
|
1666
|
-
// 实测脚底 −1.5 起手(1m 厚地板,tmp/TMP_vert_trace.mjs)——
|
|
1667
|
-
// 前 10 帧人在板内、FrontSide 看不到背面 → 上下射线全瞎 → 重力累加上坠;
|
|
1668
|
-
// 第 11 帧眼落到板底之下 0.0135,上射线打到**板底(法线朝下)**,旧口径
|
|
1669
|
-
// `positionIncremental.y -= upPen` 一帧把人往下砸 **0.9865m** → 眼落到 −2.0,
|
|
1670
|
-
// 从此两条竖射线彻底够不着 → 自由落体到 **−13.161547** → 撞上 camera.y≤−10 的
|
|
1671
|
-
// 掉出世界复位(`this.playerCollider.end.set(0, 1, 10)`)→ 「陷进地面很长一段
|
|
1672
|
-
// 距离然后凭空回到原点」。这就是皇上看到的现象。
|
|
1673
|
-
// 新口径:**绝不往下顶**,一步顶到「脚底刚好落在这块面之上」(眼高 = 面 y +
|
|
1674
|
-
// downRange + SKIN)。顶完眼只比板底高一个 SKIN → 下一轮迭代下射线立刻
|
|
1675
|
-
// 看得到板顶(distance≈SKIN)→ downPen≈downRange 把人整段托上板面站稳,
|
|
1676
|
-
// 上下射线互为 backup,全程 ≤ 十几帧、不再掉出世界。
|
|
1677
|
-
// ⚠️ 三道闸缺一不可:
|
|
1678
|
-
// · `downIntersets.length===0`:站在下层地板上、头顶真天花板时,往下鸭/躲矮洞
|
|
1679
|
-
// 是正确行为,不能把人吸到上层去;
|
|
1680
|
-
// · `playerVelocity.y <= 0`:正往上跳、头顶撞到真天花板必须仍按普通天花板往下顶,
|
|
1681
|
-
// 否则「跳一下 → 被传上天花板」;
|
|
1682
|
-
// · `!_floorBelow()`:竖向射线只够 downFar(1.52)/upFar(0.32),而跳跃顶点 ≈1.06m 加上
|
|
1683
|
-
// 脚底悬浮 0.5m 已经超出下射线够得着的范围 →
|
|
1684
|
-
// 腾空时脚下本来就是瞎的,只靠上面两道闸会把「高空跳到天花板下方」也误判成穿模。往下深探
|
|
1685
|
-
// VOID_PROBE_DOWN(8m) 问得到地面 = 正常腾空/下落 → 维持旧的天花板下压。
|
|
1686
|
-
// ⚠️ 已知残留(实测 tmp/TMP_ceiling_ab.mjs ⑦):板**厚度 > DOWN_RANGE(1.5m)** 时,
|
|
1687
|
-
// 顶到「板底之上」后人仍在板内 → 再次全瞎下坠 → 在板底往复。
|
|
1688
|
-
// HEAD 版同一场景是「掉到 −10.9 → 凭空复位」,现在收敛为有界往复(不再掉出世界),
|
|
1689
|
-
// 彻底根治要加**背面探针**(FrontSide 看不到内部,量不到板顶)。
|
|
1690
|
-
this.playerVelocity.y = 0;
|
|
1691
|
-
const liftTo = interset.point.y + downRange + this.VERTICAL_SKIN - worldPos.y;
|
|
1692
|
-
if (liftTo > 1e-4) {
|
|
1693
|
-
positionIncremental.y += liftTo;
|
|
1694
|
-
this.playerOnFloor = true;
|
|
1695
|
-
goOnComputer = true;
|
|
1689
|
+
upRay.recordHit(intersetPoint, devVisualFarU);
|
|
1690
|
+
upRay.responded = hasHit;
|
|
1691
|
+
debugRecorded = true;
|
|
1692
|
+
}
|
|
1693
|
+
if (upIntersets.length > 0) {
|
|
1694
|
+
let interset = upIntersets[0];
|
|
1695
|
+
if (upIntersets.length > 1) {
|
|
1696
|
+
for (let j = 1; j < upIntersets.length; j++) {
|
|
1697
|
+
if (upIntersets[j].point.distanceTo(worldPos) < interset.point.distanceTo(worldPos)) {
|
|
1698
|
+
interset = upIntersets[j];
|
|
1696
1699
|
}
|
|
1697
1700
|
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
+
}
|
|
1702
|
+
// 按距离解算(与下射线一致):头顶离天花板 interset.distance,需保持 upRange → 下推 (upRange - distance)
|
|
1703
|
+
const upPen = upRange - interset.distance;
|
|
1704
|
+
if (upPen > 0 && downIntersets.length === 0
|
|
1705
|
+
&& this.playerVelocity.y <= 0 && !this._floorBelow(worldPos)) {
|
|
1706
|
+
// ⭐⭐ 2026-09-25 皇上旨「顶出来,不穿模」(「下射线会陷入地面很长一个距离」的正解)
|
|
1707
|
+
// 上射线打到东西、**脚下却没有任何地面**、且人不正在往上蹿 → 我已经在这块面的
|
|
1708
|
+
// 下方/内部,再往下顶就是把它当「天花板」往下钻 = 死亡路径:
|
|
1709
|
+
// 实测脚底 −1.5 起手(1m 厚地板,tmp/TMP_vert_trace.mjs)——
|
|
1710
|
+
// 前 10 帧人在板内、FrontSide 看不到背面 → 上下射线全瞎 → 重力累加上坠;
|
|
1711
|
+
// 第 11 帧眼落到板底之下 0.0135,上射线打到**板底(法线朝下)**,旧口径
|
|
1712
|
+
// `positionIncremental.y -= upPen` 一帧把人往下砸 **0.9865m** → 眼落到 −2.0,
|
|
1713
|
+
// 从此两条竖射线彻底够不着 → 自由落体到 **−13.161547** → 撞上 camera.y≤−10 的
|
|
1714
|
+
// 掉出世界复位(玩家视点重置到 (0, 1, 10))→ 「陷进地面很长一段
|
|
1715
|
+
// 距离然后凭空回到原点」。这就是皇上看到的现象。
|
|
1716
|
+
// 新口径:**绝不往下顶**,一步顶到「脚底刚好落在这块面之上」(眼高 = 面 y +
|
|
1717
|
+
// downRange + SKIN)。顶完眼只比板底高一个 SKIN → 下一轮迭代下射线立刻
|
|
1718
|
+
// 看得到板顶(distance≈SKIN)→ downPen≈downRange 把人整段托上板面站稳,
|
|
1719
|
+
// 上下射线互为 backup,全程 ≤ 十几帧、不再掉出世界。
|
|
1720
|
+
// ⚠️ 三道闸缺一不可:
|
|
1721
|
+
// · `downIntersets.length===0`:站在下层地板上、头顶真天花板时,往下鸭/躲矮洞
|
|
1722
|
+
// 是正确行为,不能把人吸到上层去;
|
|
1723
|
+
// · `playerVelocity.y <= 0`:正往上跳、头顶撞到真天花板必须仍按普通天花板往下顶,
|
|
1724
|
+
// 否则「跳一下 → 被传上天花板」;
|
|
1725
|
+
// · `!_floorBelow()`:竖向射线只够 downFar(1.52)/upFar(0.32),而跳跃顶点 ≈1.06m 加上
|
|
1726
|
+
// 脚底悬浮 0.5m 已经超出下射线够得着的范围 →
|
|
1727
|
+
// 腾空时脚下本来就是瞎的,只靠上面两道闸会把「高空跳到天花板下方」也误判成穿模。往下深探
|
|
1728
|
+
// VOID_PROBE_DOWN(8m) 问得到地面 = 正常腾空/下落 → 维持旧的天花板下压。
|
|
1729
|
+
// ⚠️ 已知残留(实测 tmp/TMP_ceiling_ab.mjs ⑦):板**厚度 > DOWN_RANGE(1.5m)** 时,
|
|
1730
|
+
// 顶到「板底之上」后人仍在板内 → 再次全瞎下坠 → 在板底往复。
|
|
1731
|
+
// HEAD 版同一场景是「掉到 −10.9 → 凭空复位」,现在收敛为有界往复(不再掉出世界),
|
|
1732
|
+
// 彻底根治要加**背面探针**(FrontSide 看不到内部,量不到板顶)。
|
|
1733
|
+
this.playerVelocity.y = 0;
|
|
1734
|
+
const liftTo = interset.point.y + downRange + this.VERTICAL_SKIN - worldPos.y;
|
|
1735
|
+
if (liftTo > 1e-4) {
|
|
1736
|
+
positionIncremental.y += liftTo;
|
|
1737
|
+
this.playerOnFloor = true;
|
|
1701
1738
|
goOnComputer = true;
|
|
1702
1739
|
}
|
|
1703
1740
|
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
// 像素宽依赖画布尺寸:resize / 改 debugLineWidth 都在这里跟上
|
|
1709
|
-
this._syncDebugLineResolution();
|
|
1710
|
-
// ⭐ 每条 FpsRay 自带 debugLine,直接 syncDebugLine(起点/终点由 ray.recordHit 冻结,
|
|
1711
|
-
// 三态配色由 ray.syncDebugLine 写入)。射线与辅助线一一对应,不可能再错位。
|
|
1712
|
-
for (let di = 0; di < this._rays.length; di++) {
|
|
1713
|
-
// ⚠️ 皇上 2026-09-03:横贯中央的 ±X 侧向射线默认不画(debugHideSideRays 开关)。
|
|
1714
|
-
// (原 6/7 侧向已删;当前 6-9 是反向水平斜弦,isSideRay=false → 不受本开关影响,可见。
|
|
1715
|
-
// 皇上要「看到区别」→ 反向射线必须画出来。)若日后加回 ±X 侧向直射线,给那条 isSideRay 置 true 即自动隐藏。
|
|
1716
|
-
if (this.debugHideSideRays && this._rays[di].isSideRay)
|
|
1717
|
-
continue;
|
|
1718
|
-
const ray = this._rays[di];
|
|
1719
|
-
if (ray.debugLine)
|
|
1720
|
-
ray.syncDebugLine();
|
|
1741
|
+
else if (upPen > 0) {
|
|
1742
|
+
this.playerVelocity.y = 0;
|
|
1743
|
+
positionIncremental.y -= upPen;
|
|
1744
|
+
goOnComputer = true;
|
|
1721
1745
|
}
|
|
1722
1746
|
}
|
|
1723
|
-
// 应用推离
|
|
1724
|
-
// ⚠️⚠️ 这一段**只能出现一次**(2026-09-21 血案:重构时误插入第二份,
|
|
1725
|
-
// 位置推离量直接翻倍 → 陷 0.42m 被推 0.84m、垂距冲到 0.94(该停在 0.52)
|
|
1726
|
-
// → 皇上看到的「从侧面撞墙角一下子弹很远」)。改动本文件时务必确认
|
|
1727
|
-
// `positionIncremental` 全文件只被 `translate` 应用一次。
|
|
1728
|
-
// ⭐⭐ 2026-09-25 皇上旨「加个开关可以关闭碰撞修正位置的计算,方便观察回弹向量」:
|
|
1729
|
-
// 唯一的**位置写入口**就是这一行 → 在这一个闸门上关,才是真的「不修正位置」。
|
|
1730
|
-
// 原先 `previewRetreat`(面板「回弹预览」)只跳过了上方 `!previewRetreat` 那个**水平**
|
|
1731
|
-
// 修正块,竖向(下射线 `y += downPen` / 上射线 `y ± upPen`)照样从这里落地 →
|
|
1732
|
-
// 标签写着「不修正位置」却仍在被顶,且陷进地面时每轮迭代累加一份 downPen,
|
|
1733
|
-
// 预览模式下人被竖直弹飞。
|
|
1734
|
-
// ⚠️ 只跳「写位置」,不跳「记账」:`playerVelocity.y = 0`、`playerOnFloor = true`
|
|
1735
|
-
// 与削速度/方向票全部保留 → 观察时不会一帧帧攒重力陷到地心,也不会触发
|
|
1736
|
-
// camera.y ≤ −10 的掉出世界复位。
|
|
1737
1747
|
}
|
|
1748
|
+
// ========== 更新调试射线(dev 模式的最后一步)==========
|
|
1749
|
+
if (recordDebug) {
|
|
1750
|
+
this._initDebugLines();
|
|
1751
|
+
// 像素宽依赖画布尺寸:resize / 改 debugLineWidth 都在这里跟上
|
|
1752
|
+
this._syncDebugLineResolution();
|
|
1753
|
+
// ⭐ 每条 FpsRay 自带 debugLine,直接 syncDebugLine(起点/终点由 ray.recordHit 冻结,
|
|
1754
|
+
// 三态配色由 ray.syncDebugLine 写入)。射线与辅助线一一对应,不可能再错位。
|
|
1755
|
+
for (let di = 0; di < this._rays.length; di++) {
|
|
1756
|
+
// ⚠️ 皇上 2026-09-03:横贯中央的 ±X 侧向射线默认不画(debugHideSideRays 开关)。
|
|
1757
|
+
// (原 6/7 侧向已删;当前 6-9 是反向水平斜弦,isSideRay=false → 不受本开关影响,可见。
|
|
1758
|
+
// 皇上要「看到区别」→ 反向射线必须画出来。)若日后加回 ±X 侧向直射线,给那条 isSideRay 置 true 即自动隐藏。
|
|
1759
|
+
if (this.debugHideSideRays && this._rays[di].isSideRay)
|
|
1760
|
+
continue;
|
|
1761
|
+
const ray = this._rays[di];
|
|
1762
|
+
if (ray.debugLine)
|
|
1763
|
+
ray.syncDebugLine();
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
// 应用推离
|
|
1767
|
+
// ⚠️⚠️ 这一段**只能出现一次**(2026-09-21 血案:重构时误插入第二份,
|
|
1768
|
+
// 位置推离量直接翻倍 → 陷 0.42m 被推 0.84m、垂距冲到 0.94(该停在 0.52)
|
|
1769
|
+
// → 皇上看到的「从侧面撞墙角一下子弹很远」)。改动本文件时务必确认
|
|
1770
|
+
// `positionIncremental` 全文件只被 `translate` 应用一次。
|
|
1771
|
+
// ⭐⭐ 2026-09-25 皇上旨「加个开关可以关闭碰撞修正位置的计算,方便观察回弹向量」:
|
|
1772
|
+
// 唯一的**位置写入口**就是这一行 → 在这一个闸门上关,才是真的「不修正位置」。
|
|
1773
|
+
// 原先 `previewRetreat`(面板「回弹预览」)只跳过了上方 `!previewRetreat` 那个**水平**
|
|
1774
|
+
// 修正块,竖向(下射线 `y += downPen` / 上射线 `y ± upPen`)照样从这里落地 →
|
|
1775
|
+
// 标签写着「不修正位置」却仍在被顶,且陷进地面时每轮迭代累加一份 downPen,
|
|
1776
|
+
// 预览模式下人被竖直弹飞。
|
|
1777
|
+
// ⚠️ 只跳「写位置」,不跳「记账」:`playerVelocity.y = 0`、`playerOnFloor = true`
|
|
1778
|
+
// 与削速度/方向票全部保留 → 观察时不会一帧帧攒重力陷到地心,也不会触发
|
|
1779
|
+
// camera.y ≤ −10 的掉出世界复位。
|
|
1738
1780
|
// ⭐⭐ 2026-10-02 皇上旨(原话):「在修正位置的时候,我希望的是**模拟修正**,而不是**真实的
|
|
1739
1781
|
// 修正**,在模拟修正中把最终结果计算好了,再应用到真实的第一人称控制器上面」。
|
|
1740
1782
|
// ⇒ 上面那个 while 全在**模拟位置**(真实位置 + 本步位移 `_subStep`)上跑;真实 collider
|
|
@@ -1763,19 +1805,32 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1763
1805
|
// 位移永远照走(previewRetreat 只关「修正」那份,不关位移)。
|
|
1764
1806
|
this._appliedMove.set(this._subStep.x + corrX, this._subStep.y + corrY, this._subStep.z + corrZ);
|
|
1765
1807
|
if (this._appliedMove.lengthSq() > 0) {
|
|
1766
|
-
this.
|
|
1808
|
+
this.playerPosition.add(this._appliedMove);
|
|
1767
1809
|
}
|
|
1768
1810
|
// 本步已消费:归零,面板那两颗直接调 playerCollisions 的按钮不会重复走一步
|
|
1769
1811
|
this._subStep.set(0, 0, 0);
|
|
1770
|
-
// ⭐⭐ 2026-10-
|
|
1771
|
-
//
|
|
1772
|
-
//
|
|
1773
|
-
//
|
|
1774
|
-
//
|
|
1775
|
-
//
|
|
1812
|
+
// ⭐⭐ 2026-10-11 皇上旨「撞墙会进入墙体然后反弹回来,一直抖动」→ A 档改判:
|
|
1813
|
+
// **只削掉「往墙里」那一份速度**(沿命中面法线的负分量),切向分量原样保留。
|
|
1814
|
+
// 旧口径(2026-10-07「撞到墙后,加速度置为 0」)是 x/z 双双归零,两处毛病:
|
|
1815
|
+
// ① 斜着撞墙 = 连「沿墙滑行」那份一起抹掉 → 手感像被墙吸停,且人是沿坐标轴方向被清,
|
|
1816
|
+
// 不是沿墙法线;速度被清零的方向与实际推离方向不共线 → 推离与加速度交替占上风;
|
|
1817
|
+
// ② 全清 ⇒ 每帧「输入重新加速 → 立刻清零」的锯齿,正是他看到的抖动的另一半来源
|
|
1818
|
+
// (实测峰峰 0.0193m、60fps 下约 17 次/秒翻向)。
|
|
1819
|
+
// 新口径:`into = v·n`(n = 本轮欠距最大那面的**外法线**,指向玩家一侧),只在 into<0
|
|
1820
|
+
// (= 正在往墙里冲)时减掉 `into·n` 那份 ⇒ 撞后 v·n = 0(非穿透),切向 v−(v·n)n 完整保留。
|
|
1821
|
+
// ⚠️ 方向与推离用的是**同一个** `_horizNormal`(水平半区那轮算好、就地复用),绝不另算一遍。
|
|
1822
|
+
// ⚠️ 竖向 y 一字不动(重力/跳跃照旧);观察模式(回弹预览)不改速度,口径与 10-07 一致。
|
|
1776
1823
|
if (this._horizHit && !this.previewRetreat) {
|
|
1777
|
-
|
|
1778
|
-
|
|
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
|
+
}
|
|
1833
|
+
}
|
|
1779
1834
|
}
|
|
1780
1835
|
// ⭐ 十旨:一帧一条的抖动台账。**只由子步循环的最后一步(isFinalStep)记** ——
|
|
1781
1836
|
// 中间小步是碰撞细分的过渡状态,记了就变成「一帧好几条」,翻向/跳变的口径全乱。
|
|
@@ -1800,10 +1855,8 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
1800
1855
|
// 越界传送
|
|
1801
1856
|
teleportPlayerIfOob = () => {
|
|
1802
1857
|
if (this.camera.position.y <= -10) {
|
|
1803
|
-
this.
|
|
1804
|
-
this.
|
|
1805
|
-
this.playerCollider.radius = 0.35;
|
|
1806
|
-
this.camera.position.copy(this.playerCollider.end);
|
|
1858
|
+
this.playerPosition.set(0, 1, 10);
|
|
1859
|
+
this.camera.position.copy(this.playerPosition);
|
|
1807
1860
|
}
|
|
1808
1861
|
};
|
|
1809
1862
|
// 更新
|