three-base 2.0.137 → 2.0.145
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/DirectionalLight.d.ts +2 -2
- package/DirectionalLight.js +1 -1
- package/DirectionalLight.ts +70 -0
- package/EventMap.d.ts +103 -0
- package/EventMap.js +17 -0
- package/EventMap.ts +139 -0
- package/EventMesh.ts +60 -0
- package/ExtrudeMesh.js +1 -25
- package/ExtrudeMesh.ts +553 -0
- package/FPScontrol.d.ts +448 -15
- package/FPScontrol.js +1448 -266
- package/FPScontrol.ts +1855 -0
- package/Fire/Fire.d.ts +4 -4
- package/Fire/Fire.js +8 -7
- package/Fire/Fire.ts +89 -0
- package/Fire/{FireGromtry.d.ts → FireGeometry.d.ts} +2 -2
- package/Fire/{FireGromtry.js → FireGeometry.js} +1 -1
- package/Fire/FireGeometry.ts +40 -0
- package/Fire/FireMaterial.d.ts +3 -3
- package/Fire/FireMaterial.js +1 -1
- package/Fire/FireMaterial.ts +89 -0
- package/Fire/index.d.ts +1 -0
- package/Fire/index.js +1 -0
- package/Fire/index.ts +2 -0
- package/GLTFDracoLoader.d.ts +71 -0
- package/GLTFDracoLoader.js +266 -0
- package/GLTFDracoLoader.ts +430 -0
- package/Grass/Grass.d.ts +2 -2
- package/Grass/Grass.ts +38 -0
- package/Grass/index.d.ts +1 -0
- package/Grass/index.js +1 -0
- package/Grass/index.ts +2 -0
- package/Ground1/Ground1.d.ts +2 -3
- package/Ground1/Ground1.js +64 -60
- package/Ground1/Ground1.ts +114 -0
- package/Ground1/index.ts +1 -0
- package/MeshBase.d.ts +1 -0
- package/MeshBase.js +37 -13
- package/MeshBase.ts +253 -0
- package/Mirror.d.ts +3 -3
- package/Mirror.js +7 -3
- package/Mirror.ts +203 -0
- package/MktMesh/MktMesh.d.ts +301 -0
- package/MktMesh/MktMesh.js +1064 -0
- package/MktMesh/MktMesh.ts +1096 -0
- package/MktMesh/assetUrl.d.ts +59 -0
- package/MktMesh/assetUrl.js +154 -0
- package/MktMesh/assetUrl.ts +155 -0
- package/MktMesh/mktMappingShader.d.ts +12 -0
- package/MktMesh/mktMappingShader.js +139 -0
- package/MktMesh/mktMappingShader.ts +139 -0
- package/MktMesh/mktShader.d.ts +263 -0
- package/MktMesh/mktShader.js +1248 -0
- package/MktMesh/mktShader.ts +1344 -0
- package/MktMesh/panoBitmapTexture.d.ts +25 -0
- package/MktMesh/panoBitmapTexture.js +52 -0
- package/MktMesh/panoBitmapTexture.ts +54 -0
- package/MktMesh/panoSlotBind.d.ts +29 -0
- package/MktMesh/panoSlotBind.js +106 -0
- package/MktMesh/panoSlotBind.ts +153 -0
- package/MktMesh/panoSlotRoster.d.ts +22 -0
- package/MktMesh/panoSlotRoster.js +169 -0
- package/MktMesh/panoSlotRoster.ts +168 -0
- package/MktMesh/panoTileStitcher.d.ts +119 -0
- package/MktMesh/panoTileStitcher.js +533 -0
- package/MktMesh/panoTileStitcher.ts +616 -0
- package/MktMesh/types.d.ts +75 -0
- package/MktMesh/types.js +1 -0
- package/MktMesh/types.ts +87 -0
- package/MktMesh/wallCapTessellate.d.ts +33 -0
- package/MktMesh/wallCapTessellate.js +97 -0
- package/MktMesh/wallCapTessellate.ts +68 -0
- package/PipeMesh/PathGeometry.js +9 -2
- package/PipeMesh/PathGeometry.ts +343 -0
- package/PipeMesh/PathPoint.ts +36 -0
- package/PipeMesh/PathPointList.js +1 -0
- package/PipeMesh/PathPointList.ts +227 -0
- package/PipeMesh/PathTubeGeometry.ts +146 -0
- package/PipeMesh/PipeMesh.d.ts +1 -1
- package/PipeMesh/PipeMesh.js +1 -1
- package/PipeMesh/PipeMesh.ts +76 -0
- package/PipeMesh/index.ts +1 -0
- package/PlaneModal1/PlaneModal1.d.ts +16 -13
- package/PlaneModal1/PlaneModal1.js +4 -0
- package/PlaneModal1/PlaneModal1.ts +138 -0
- package/PlaneModal1/index.ts +1 -0
- package/PlaneText.d.ts +2 -14
- package/PlaneText.js +7 -2
- package/PlaneText.ts +55 -0
- package/PlaneWithHtml.d.ts +4 -2
- package/PlaneWithHtml.js +2 -1
- package/PlaneWithHtml.ts +70 -0
- package/PointLight/PointLight.d.ts +3 -4
- package/PointLight/PointLight.js +1 -7
- package/PointLight/PointLight.ts +67 -0
- package/PointLight/index.ts +1 -0
- package/README.md +110 -79
- package/Sky/Sky.ts +22 -0
- package/Sky/index.ts +1 -0
- package/SpotLight.d.ts +2 -2
- package/SpotLight.js +1 -1
- package/SpotLight.ts +74 -0
- package/SpriteModal1/SpriteModal1.d.ts +16 -13
- package/SpriteModal1/SpriteModal1.js +8 -2
- package/SpriteModal1/SpriteModal1.ts +146 -0
- package/SpriteModal1/index.ts +1 -0
- package/SpriteText.d.ts +14 -13
- package/SpriteText.js +22 -9
- package/SpriteText.ts +205 -0
- package/SpriteText2.d.ts +9 -8
- package/SpriteText2.js +57 -32
- package/SpriteText2.ts +258 -0
- package/SpriteText3.d.ts +86 -0
- package/SpriteText3.js +519 -0
- package/SpriteText3.ts +599 -0
- package/SpriteWithHtml.d.ts +4 -2
- package/SpriteWithHtml.ts +50 -0
- package/Text3D/Text3D.js +17 -21
- package/Text3D/Text3D.ts +77 -0
- package/Text3D/fontController.d.ts +10 -9
- package/Text3D/fontController.js +17 -50
- package/Text3D/fontController.ts +209 -0
- package/Text3D/index.ts +1 -0
- package/ThreeBase.d.ts +168 -31
- package/ThreeBase.js +663 -117
- package/ThreeBase.ts +1314 -0
- package/ThreeMouse.d.ts +20 -7
- package/ThreeMouse.js +78 -32
- package/ThreeMouse.ts +472 -0
- package/Transition.d.ts +12 -7
- package/Transition.js +26 -27
- package/Transition.ts +242 -0
- package/Water/Water.js +21 -3
- package/Water/Water.ts +45 -0
- package/Water/index.d.ts +2 -0
- package/Water/index.js +2 -0
- package/Water/index.ts +2 -0
- package/WaterFlow/FloatingDuck.d.ts +88 -0
- package/WaterFlow/FloatingDuck.js +307 -0
- package/WaterFlow/FloatingDuck.ts +445 -0
- package/WaterFlow/GPUFluidSimulation.d.ts +56 -0
- package/WaterFlow/GPUFluidSimulation.js +785 -0
- package/WaterFlow/GPUFluidSimulation.ts +827 -0
- package/WaterFlow/ReservoirSystem.d.ts +106 -0
- package/WaterFlow/ReservoirSystem.js +621 -0
- package/WaterFlow/ReservoirSystem.ts +754 -0
- package/WaterFlow/SPHField.d.ts +35 -0
- package/WaterFlow/SPHField.js +204 -0
- package/WaterFlow/SPHField.ts +228 -0
- package/WaterFlow/ScreenSpaceFluidPass.d.ts +69 -0
- package/WaterFlow/ScreenSpaceFluidPass.js +736 -0
- package/WaterFlow/ScreenSpaceFluidPass.ts +789 -0
- package/WaterFlow/ShallowWaterSurface.d.ts +45 -0
- package/WaterFlow/ShallowWaterSurface.js +441 -0
- package/WaterFlow/ShallowWaterSurface.ts +510 -0
- package/WaterFlow/WaterFlow.d.ts +87 -0
- package/WaterFlow/WaterFlow.js +171 -0
- package/WaterFlow/WaterFlow.ts +219 -0
- package/WaterFlow/types.d.ts +161 -0
- package/WaterFlow/types.js +1 -0
- package/WaterFlow/types.ts +189 -0
- package/WaterFlow/waterFlowPass.d.ts +3 -0
- package/WaterFlow/waterFlowPass.js +20 -0
- package/WaterFlow/waterFlowPass.ts +40 -0
- package/collisionOBB.d.ts +42 -0
- package/collisionOBB.js +112 -0
- package/collisionOBB.ts +108 -0
- package/declare.d.ts +25 -167
- package/index.js +0 -1
- package/index.ts +1 -0
- package/instanced/InstancedBatch.d.ts +57 -0
- package/instanced/InstancedBatch.js +348 -0
- package/instanced/InstancedBatch.ts +406 -0
- package/instanced/InstancedChild.d.ts +174 -0
- package/instanced/InstancedChild.js +264 -0
- package/instanced/InstancedChild.ts +327 -0
- package/instanced/InstancedGroup.d.ts +43 -0
- package/instanced/InstancedGroup.js +126 -0
- package/instanced/InstancedGroup.ts +137 -0
- package/{InstancedMeshBase.d.ts → instanced/InstancedMeshBase.d.ts} +6 -7
- package/{InstancedMeshBase.js → instanced/InstancedMeshBase.js} +36 -47
- package/instanced/InstancedMeshBase.ts +292 -0
- package/instanced/InstancedTemplate.d.ts +46 -0
- package/instanced/InstancedTemplate.js +125 -0
- package/instanced/InstancedTemplate.ts +188 -0
- package/keyControls.d.ts +29 -0
- package/{keyContorls.js → keyControls.js} +10 -7
- package/keyControls.ts +107 -0
- package/lensflare/LensFlare.d.ts +39 -0
- package/lensflare/LensFlare.js +373 -0
- package/lensflare/LensFlare.ts +448 -0
- package/lensflare/lensflare0.png +0 -0
- package/lensflare/lensflare3.png +0 -0
- package/lensflare/lensflare_custom.png +0 -0
- package/lensflare/lensflare_hex_ghost.png +0 -0
- package/lensflare/lensflare_hex_main.png +0 -0
- package/package.json +104 -25
- package/pass/bloom_pass.d.ts +1 -1
- package/pass/bloom_pass.js +127 -40
- package/pass/bloom_pass.ts +243 -0
- package/pass/cover_pass.d.ts +1 -1
- package/pass/cover_pass.js +55 -32
- package/pass/cover_pass.ts +182 -0
- package/pass/debugDepthState.d.ts +20 -0
- package/pass/debugDepthState.js +9 -0
- package/pass/debugDepthState.ts +22 -0
- package/pass/depthTestBias.d.ts +12 -0
- package/pass/depthTestBias.js +12 -0
- package/pass/depthTestBias.ts +12 -0
- package/pass/effectUserData.d.ts +27 -0
- package/pass/effectUserData.js +1 -0
- package/pass/effectUserData.ts +46 -0
- package/pass/frosted_glass_pass.d.ts +1 -1
- package/pass/frosted_glass_pass.js +45 -64
- package/pass/frosted_glass_pass.ts +288 -0
- package/pass/fsQuad.d.ts +2 -2
- package/pass/fsQuad.ts +34 -0
- package/pass/glow_pass.d.ts +1 -1
- package/pass/glow_pass.js +288 -72
- package/pass/glow_pass.ts +654 -0
- package/pass/screen_bloom_pass.d.ts +11 -0
- package/pass/screen_bloom_pass.js +235 -0
- package/pass/screen_bloom_pass.ts +263 -0
- package/pass/ssao_pass.d.ts +78 -3
- package/pass/ssao_pass.js +642 -206
- package/pass/ssao_pass.ts +918 -0
- package/pass/stroke_pass.d.ts +1 -1
- package/pass/stroke_pass.js +187 -116
- package/pass/stroke_pass.ts +638 -0
- package/shaders/FXAAShaderMaterial.js +5 -48
- package/shaders/FXAAShaderMaterial.ts +201 -0
- package/shaders/baseDepthVisShader.d.ts +11 -0
- package/shaders/baseDepthVisShader.js +36 -0
- package/shaders/baseDepthVisShader.ts +36 -0
- package/shaders/baseTextureToRenderShader.js +46 -51
- package/shaders/baseTextureToRenderShader.ts +232 -0
- package/shaders/copyShader.js +2 -3
- package/shaders/copyShader.ts +32 -0
- package/shaders/debugDepthCompareShader.d.ts +14 -0
- package/shaders/debugDepthCompareShader.js +49 -0
- package/shaders/debugDepthCompareShader.ts +50 -0
- package/shaders/debugDepthJudgeShader.d.ts +18 -0
- package/shaders/debugDepthJudgeShader.js +52 -0
- package/shaders/debugDepthJudgeShader.ts +54 -0
- package/shaders/debugExcludedShader.d.ts +13 -0
- package/shaders/debugExcludedShader.js +46 -0
- package/shaders/debugExcludedShader.ts +48 -0
- package/spriteTextBg.d.ts +1 -0
- package/spriteTextBg.js +20 -0
- package/spriteTextBg.ts +20 -0
- package/threeLoader.d.ts +8 -7
- package/threeLoader.js +58 -67
- package/threeLoader.ts +525 -0
- package/threeUtils.d.ts +2 -1
- package/threeUtils.js +24 -25
- package/threeUtils.ts +373 -0
- package/types.d.ts +2 -0
- package/types.js +6 -0
- package/types.ts +8 -0
- package/ApngTexture.d.ts +0 -13
- package/ApngTexture.js +0 -61
- package/ApngTextureManage.d.ts +0 -18
- package/ApngTextureManage.js +0 -87
- package/KeyFrameAnimation.d.ts +0 -25
- package/KeyFrameAnimation.js +0 -78
- package/keyContorls.d.ts +0 -25
- package/lib/draco/draco_decoder.js +0 -34
- package/lib/draco/draco_decoder.wasm +0 -0
- package/lib/draco/draco_wasm_wrapper.js +0 -117
- package/pass/emissive_ao_pass.d.ts +0 -13
- package/pass/emissive_ao_pass.js +0 -370
- package/shaders.d.ts +0 -5
- package/shaders.js +0 -93
package/FPScontrol.js
CHANGED
|
@@ -4,18 +4,159 @@
|
|
|
4
4
|
* @Last Modified by: xiaosihan
|
|
5
5
|
* @Last Modified time: 2024-11-07 21:57:39
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { Clock, Euler, EventDispatcher, Group, InstancedMesh, Matrix3, Matrix4, Mesh, Quaternion, Ray, Raycaster, Vector3 } from "three";
|
|
8
|
+
import { MathUtils } from "three";
|
|
9
|
+
// (2026-09-28 皇上旨「完完全全使用 xsh-react-three 的碰撞计算方法」:胶囊兜底整条拆除。
|
|
10
|
+
// collisionOBB.ts 纯几何模块与其单测保留,但 FPScontrol 不再引用。)
|
|
11
|
+
const { degToRad, clamp } = MathUtils;
|
|
12
|
+
// 「矩形射线法」的水平 far 口径:斜弦画长 = R√2(四弦首尾闭合的唯一解,勿再改回)。
|
|
8
13
|
import { Capsule } from "three/examples/jsm/math/Capsule";
|
|
9
|
-
|
|
14
|
+
// 粗辅助线:WebGL 下 `LineBasicMaterial.linewidth` 恒被驱动钳成 1px(Chrome/ANGLE 一律 1px,
|
|
15
|
+
// 调大无效),要真的变粗必须走 `LineSegments2 + LineMaterial`(shader 里按像素铺quad)。
|
|
16
|
+
// 本仓库先例:src/views/home/homeRender.ts:71-73, 344-358。
|
|
17
|
+
import { LineSegments2 } from "three/examples/jsm/lines/LineSegments2.js";
|
|
18
|
+
import { LineSegmentsGeometry } from "three/examples/jsm/lines/LineSegmentsGeometry.js";
|
|
19
|
+
import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
|
|
10
20
|
import { acceleratedRaycast, computeBoundsTree } from "three-mesh-bvh";
|
|
11
|
-
//
|
|
21
|
+
// 确保 three-mesh-bvh 补丁已应用到普通 Mesh 的射线拾取(BVH 加速)。
|
|
22
|
+
// ⚠️ 绝不给 InstancedMesh.prototype.raycast 打该补丁:three-mesh-bvh 的
|
|
23
|
+
// acceleratedRaycast 没有 per-instance 遍历逻辑,对无 boundsTree 的几何会退化成
|
|
24
|
+
// 原生 Mesh.prototype.raycast(只检测几何体一次、不遍历实例),导致 InstancedMesh
|
|
25
|
+
// 的实例完全拾取不到(如 three-base 实例化网格示例的 hover/click 事件失效)。
|
|
26
|
+
// InstancedMesh 必须保留 three 原生 raycast(逐实例检测并返回 instanceId)。
|
|
12
27
|
if (Mesh.prototype.raycast !== acceleratedRaycast) {
|
|
13
28
|
Mesh.prototype.raycast = acceleratedRaycast;
|
|
14
29
|
}
|
|
15
|
-
|
|
16
|
-
|
|
30
|
+
/** 全局 FPScontrol 实例注册表。
|
|
31
|
+
* InstancedBatch 在「异步创建碰撞体 InstancedMesh」后,通过本表通知所有已创建的 FPScontrol
|
|
32
|
+
* 重新收集碰撞体,确保晚于控制器初始化的实例化碰撞体也能被碰撞射线命中(否则会出现"墙建出来了却穿过去"的时序 bug)。 */
|
|
33
|
+
export const fpsControlRegistry = new Set();
|
|
34
|
+
/**
|
|
35
|
+
* 往「只含 1 段」的 LineSegments2 里写端点与顶点色。
|
|
36
|
+
* `LineSegmentsGeometry` 用交错的实例属性存段:instanceStart/instanceEnd 共用一条
|
|
37
|
+
* `instanceStart.array`(长度 6 = 起点 xyz + 终点 xyz),颜色同理。
|
|
38
|
+
* 每帧只写数组 + 置 needsUpdate,绝不调 `setPositions()`(那会重建缓冲、60fps 下白扔 GC)。
|
|
39
|
+
*/
|
|
40
|
+
function writeDebugSegment(line, ax, ay, az, bx, by, bz, c0, c1, c2, c3, c4, c5) {
|
|
41
|
+
const attrs = line.geometry.attributes;
|
|
42
|
+
const pos = attrs.instanceStart.array;
|
|
43
|
+
pos[0] = ax;
|
|
44
|
+
pos[1] = ay;
|
|
45
|
+
pos[2] = az;
|
|
46
|
+
pos[3] = bx;
|
|
47
|
+
pos[4] = by;
|
|
48
|
+
pos[5] = bz;
|
|
49
|
+
const col = attrs.instanceColorStart.array;
|
|
50
|
+
col[0] = c0;
|
|
51
|
+
col[1] = c1;
|
|
52
|
+
col[2] = c2;
|
|
53
|
+
col[3] = c3;
|
|
54
|
+
col[4] = c4;
|
|
55
|
+
col[5] = c5;
|
|
56
|
+
attrs.instanceStart.needsUpdate = true;
|
|
57
|
+
attrs.instanceColorStart.needsUpdate = true;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 单条碰撞射线 + 其专属辅助线的封装(皇上 2026-09-06 旨:每一条射线都必须拥有一个
|
|
61
|
+
* 一一对应、且由射线自身数据驱动的辅助线,杜绝「调试线索引与射线索引错位 / 漂移」)。
|
|
62
|
+
* 继承 THREE.Ray(天然持有 worldOrigin=this.origin、worldDir=this.direction),
|
|
63
|
+
* 额外携带:局部定义(localOrigin/localDir)、本帧碰撞 far、专属 debugLine、本帧命中/响应态。
|
|
64
|
+
*
|
|
65
|
+
* ⭐ 2026-09-21 皇上旨「就在 FPScontrol.ts 加 export」:对外导出以便复用。
|
|
66
|
+
* 本类**零业务耦合**(只依赖 three 的 Ray / Vector3 / LineSegments2),拿到别处可直接独立使用,
|
|
67
|
+
* 四步走:`new FpsRay(localOrigin, localDir, debugLine?, isSideRay?)`
|
|
68
|
+
* → `updateWorld(yawQuat, worldPos)`(每帧世界化)
|
|
69
|
+
* → `applyToRaycaster(raycaster)`(注入碰撞检测)
|
|
70
|
+
* → `recordHit(hitPointOrNull, visualFar)`(冻结本帧命中态 + 辅助线起终点)
|
|
71
|
+
* → `syncDebugLine()`(写入几何 + 三态配色:红=命中 / 青=探测余量 / 白末端)。
|
|
72
|
+
* `debugLine` 由调用方自备:一条**只有 1 段**的 `LineSegments2`(几何由
|
|
73
|
+
* `LineSegmentsGeometry.setPositions/setColors` 各给 6 个 float = 起点+终点;
|
|
74
|
+
* 材质照抄 FPScontrol._createDebugLine:LineMaterial + vertexColors + linewidth(px) +
|
|
75
|
+
* depthTest(射线线 true=正常遮挡 / 向量线 false=保持穿透)+ depthWrite:false +
|
|
76
|
+
* transparent:true + toneMapped:false + renderOrder:9999)。
|
|
77
|
+
* 不传 debugLine 也可纯当「带局部定义与命中态的 Ray」用,syncDebugLine 会自动跳过。
|
|
78
|
+
*
|
|
79
|
+
* ⚠️ 副作用提醒(皇上已悉,定案保留):从本模块 import 会执行文件顶层的 three-mesh-bvh 原型补丁
|
|
80
|
+
* (`Mesh.prototype.raycast = acceleratedRaycast`)。若哪天只要这个类、不想要该全局补丁,
|
|
81
|
+
* 需把 FpsRay 搬到独立文件(如 three-base/FpsRay.ts)。
|
|
82
|
+
*/
|
|
83
|
+
export class FpsRay extends Ray {
|
|
84
|
+
/** 局部定义(随布局冻结,单位就是**世界米**;世界空间 = 跟随 yaw 旋转 + 加玩家位置) */
|
|
85
|
+
localOrigin;
|
|
86
|
+
localDir;
|
|
87
|
+
/** 本帧碰撞 far(由 FPScontrol 按射线类型注入:水平=horizontalFar,下=downFar,上=upFar) */
|
|
88
|
+
far = 0;
|
|
89
|
+
/** 专属辅助线(与射线一一对应;懒初始化,未开调试时为 null) */
|
|
90
|
+
debugLine = null;
|
|
91
|
+
/** ⭐ 是否为「侧向射线」(±X 直射线,原 6/7)。用于调试线绘制默认隐藏开关(debugHideSideRays)。
|
|
92
|
+
* 当前「矩形射线法」10 条布局**无侧向射线**,全部 false;若日后加回 ±X 侧向直射线,给那条置 true。
|
|
93
|
+
* 反向水平斜弦 6-9 不是侧向(是 2-5 的反方向探测),保持 false → 调试线可见(皇上要「看到区别」)。 */
|
|
94
|
+
isSideRay = false;
|
|
95
|
+
/** 本帧调试态:是否命中 / 是否真触发碰撞响应(供三态配色) */
|
|
96
|
+
hit = false;
|
|
97
|
+
responded = false;
|
|
98
|
+
/**
|
|
99
|
+
* 本条线的辅助线是否参与正常深度遮挡(= debugLine 材质的 depthTest)。
|
|
100
|
+
* 2026-09-25 皇上旨「射线辅助线应该是正常的遮挡状态」→ 10 条射线线全为 true;
|
|
101
|
+
* 两条回弹向量线按同日定案保持穿透显示(它们不挂在 FpsRay 上)。
|
|
102
|
+
* 挂在同一 Line 上的 `renderOrder:9999` 不再代表「盖住一切」,故把真值记在这里供核对/回归断言。
|
|
103
|
+
*/
|
|
104
|
+
depthTestEnabled = false;
|
|
105
|
+
/** 辅助线起点(命中记录时按本帧 world origin 冻结,避免迭代间 worldPos 漂移) */
|
|
106
|
+
debugOrigin = new Vector3();
|
|
107
|
+
/** 辅助线终点:恒 = origin+direction*visualFar 画满(命中点不参与绘制,命中态看颜色) */
|
|
108
|
+
debugFarPoint = new Vector3();
|
|
109
|
+
constructor(localOrigin, localDir, debugLine = null, isSideRay = false) {
|
|
110
|
+
super(localOrigin.clone(), localDir.clone().normalize());
|
|
111
|
+
this.localOrigin = localOrigin.clone();
|
|
112
|
+
this.localDir = localDir.clone().normalize();
|
|
113
|
+
this.debugLine = debugLine;
|
|
114
|
+
this.isSideRay = isSideRay;
|
|
115
|
+
}
|
|
116
|
+
/** 每帧:把局部定义转为世界空间(局部已是米,只需跟随 yaw 旋转 + 加玩家位置) */
|
|
117
|
+
updateWorld(yawQuat, worldPos) {
|
|
118
|
+
this.origin.copy(this.localOrigin).applyQuaternion(yawQuat).add(worldPos);
|
|
119
|
+
this.direction.copy(this.localDir).applyQuaternion(yawQuat).normalize();
|
|
120
|
+
}
|
|
121
|
+
/** 把本射线注入射线器(碰撞用) */
|
|
122
|
+
applyToRaycaster(raycaster) {
|
|
123
|
+
raycaster.set(this.origin, this.direction);
|
|
124
|
+
raycaster.far = this.far;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 记录本帧命中态 + 冻结辅助线起终点。
|
|
128
|
+
* ⚠️ 辅助线**恒按 visualFar 画满**:命中点不参与绘制。
|
|
129
|
+
* 2026-09-25 皇上两次旨意(先是「下射线一落地就从 1.5 缩到 1.0,看着像射线变短了」,
|
|
130
|
+
* 后是「拾取到物体后辅助线都变短了」)定案:**长度不再兼职表达命中**,
|
|
131
|
+
* 命中态只由颜色指示(红 = 命中 / 亮青 = 未命中)。附带好处:菱形包络恒闭合。
|
|
132
|
+
* ⚠️ 想让线收在命中点是回退这条口径,动之前先问过皇上。
|
|
133
|
+
*/
|
|
134
|
+
recordHit(intersetPoint, visualFar) {
|
|
135
|
+
this.hit = intersetPoint !== null;
|
|
136
|
+
// 冻结辅助线起点 = 当前 world origin(首轮迭代记录,避免被后续迭代的 worldPos 推离带偏)
|
|
137
|
+
this.debugOrigin.copy(this.origin);
|
|
138
|
+
this.debugFarPoint.copy(this.origin).addScaledVector(this.direction, visualFar);
|
|
139
|
+
}
|
|
140
|
+
/** 把 debugOrigin→debugFarPoint 写入专属辅助线几何 + 三态配色(红=命中 / 青=探测余量 / 白末端) */
|
|
141
|
+
syncDebugLine() {
|
|
142
|
+
const line = this.debugLine;
|
|
143
|
+
if (!line)
|
|
144
|
+
return;
|
|
145
|
+
// ⚠️ 配色与旧 _updateDebugLine 完全一致:命中=红(1,0,0) / 未命中=亮青(0,1,1) / 末端白
|
|
146
|
+
const r = this.hit ? 1.0 : 0.0;
|
|
147
|
+
const g = this.hit ? 0.0 : 1.0;
|
|
148
|
+
const b = this.hit ? 0.0 : 1.0;
|
|
149
|
+
writeDebugSegment(line, this.debugOrigin.x, this.debugOrigin.y, this.debugOrigin.z, this.debugFarPoint.x, this.debugFarPoint.y, this.debugFarPoint.z, r, g, b, 1.0, 1.0, 1.0);
|
|
150
|
+
}
|
|
17
151
|
}
|
|
18
|
-
/**
|
|
152
|
+
/** 第一/三人称控制器(「矩形射线法」10 条射线:4 斜弦 + 0/1 竖向 + 6-9 反向斜弦 + 迭代推离,
|
|
153
|
+
* userData.fpsCollision 标记碰撞体)。详见下方 `_localRayOrigins` 上方的「★矩形射线法★」权威规格块。
|
|
154
|
+
* ⭐ 射线布局 = 矩形射线法 / Rectangle-Ray Method(皇上 2026-09-06 命名,唯一数据源 = _localRayOrigins/_localRayDirs):
|
|
155
|
+
* 0/1 竖向(下/上)+ 2-5 斜弦(菱形,R=1.06,四弦闭合)+ 6-9 反向斜弦(2-5 的反方向、起点 xy 重合、
|
|
156
|
+
* 比 2-5 再低 0.05m —— 皇上 2026-09-26 五旨定案:HORIZ_FWD_RAY_Y = −0.7m、HORIZ_REVERSE_RAY_Y = −0.75m,
|
|
157
|
+
* 即离地 0.8m / 0.75m;此前 0 / −0.5 / −0.7 各版均已作废),
|
|
158
|
+
* 用于反向探测玩家身后墙)。6/7 ±X 侧向与 8-13 共线延长线已于 2026-09-06 精简删除。
|
|
159
|
+
* 以后说「矩形射线法」即可按规格块精确还原(含冻结常量表 / 10 条逐条表 / 局部→世界变换 / 关键不变量)。 */
|
|
19
160
|
export default class FPScontrol extends EventDispatcher {
|
|
20
161
|
canvas;
|
|
21
162
|
/**
|
|
@@ -30,20 +171,47 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
30
171
|
this.canvas = canvas;
|
|
31
172
|
this.scene = scene;
|
|
32
173
|
this.init(canvas);
|
|
174
|
+
// 注册到全局表,供 InstancedBatch 异步创建碰撞体后通知重建碰撞列表
|
|
175
|
+
fpsControlRegistry.add(this);
|
|
33
176
|
}
|
|
34
177
|
// 是否启用
|
|
35
178
|
enable = false;
|
|
36
|
-
|
|
179
|
+
/**
|
|
180
|
+
* 重力加速度。⭐ 皇上 2026-10-03 旨「重力 固定 10m/s 不需要编辑」⇒ 30 → **10**,
|
|
181
|
+
* 并且从面板旋钮表(PARAM_SPECS)与 meta 右侧表单整条删除,从此不是可调参数。
|
|
182
|
+
* ⚠️ 跳跃顶点 = jumpSpeed²/(2×GRAVITY),滞空时间 = 2×jumpSpeed/GRAVITY ⇒ 重力降到 1/3
|
|
183
|
+
* 等于同一初速度跳得高 3 倍、在空里多待 3 倍(jumpSpeed 的默认值要跟这一点连着看)。
|
|
184
|
+
*/
|
|
185
|
+
GRAVITY = 10; // 重力(皇上定案:固定 10,不做编辑项 ⇒ readonly,写它编译就报错)
|
|
37
186
|
STEPS_PER_FRAME = 2; // 每帧碰撞计算的次数
|
|
38
187
|
speed = 1; // 速度
|
|
39
188
|
camera;
|
|
40
189
|
// 移动时是否保持惯性 由外部来配置
|
|
41
190
|
inertia = true;
|
|
191
|
+
// ⭐ 2026-09-06 皇上:跳跃 / 移动惯性 运行时可调(演示面板参数控制)
|
|
192
|
+
/** 是否允许跳跃(空格触发)。false 时按空格不跳。默认 true */
|
|
193
|
+
jumpEnabled = true;
|
|
194
|
+
/** 跳跃初速度(垂直方向,m/s)。
|
|
195
|
+
* ⚠️ 2026-09-27 治「跳跃翻墙」:原 15(当时重力 30)→ 顶点 3.75m 远超墙高 2m,跳起后胶囊底越过墙顶
|
|
196
|
+
* 造成水平失明窗口,人带着水平速度飞越墙平面、落地在墙外侧被越推越远(探针实测 z=-187)。
|
|
197
|
+
* 定案 8 ⇒ 顶点 1.07m:仍可跳上 0.8m 高台,且 yBottom 恒低于「墙顶 2.0 − 步高 0.1」,
|
|
198
|
+
* 与主循环竖直门配合后失明窗口彻底消失。**顶点**才是防翻墙的那条线,不是初速度本身。
|
|
199
|
+
* ⭐ 2026-10-03 皇上旨「重力固定 10」⇒ 顶点 = jumpSpeed²/20,原 8 会把顶点抬到 3.2m(>墙顶 2m)
|
|
200
|
+
* = 09-27 那个翻墙窗口原地复活,所以按「**实测跳跃弧线不变**」倒推出 **5**(5²/20 = 1.25m 理论值,
|
|
201
|
+
* 实测脚底最高 1.57m 与 8@重力30 同一档;离散积分每帧扣 0.167 + 腾空阻尼,比理论值少约 0.15m)。
|
|
202
|
+
* 代价:同一顶点高度下滞空时间 0.53s → 1.0s(重力变 1/3 的直接后果,也就是皇上要的手感)。
|
|
203
|
+
* 调大此值会重新打开翻墙窗口。 */
|
|
204
|
+
jumpSpeed = 5;
|
|
205
|
+
/** 惯性关闭(inertia=false)时,每帧水平速度保留率(×0.85 旧值)。越小停得越快。默认 0.85 */
|
|
206
|
+
inertiaDamp = 0.85;
|
|
42
207
|
// 场景引用(用于射线检测)
|
|
43
208
|
scene;
|
|
44
209
|
// ========== dev / 第三人称 ==========
|
|
45
210
|
// 开发模式:打开后显示射线可视化
|
|
46
211
|
dev = false;
|
|
212
|
+
// ⚠️ 2026-09-25 退役:原 `debugDownRayProbe` + `_downProbeTick` 独立探针(另一套 console.log)。
|
|
213
|
+
// 皇上旨「我说的日志就是浏览器的控制台,而不是你来开发一个控制台」→ 下射线数值已并进【碰撞日志】
|
|
214
|
+
// 的【下射线】段,开关与节流计数随之整条删除。
|
|
47
215
|
// 调试模式相机偏移(本地坐标系,Y=上,Z=后)
|
|
48
216
|
devCameraOffset = new Vector3(0, 2.5, 12.5);
|
|
49
217
|
// 第三人称模式
|
|
@@ -57,80 +225,842 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
57
225
|
// 模式切换检测
|
|
58
226
|
_prevThirdPerson = false;
|
|
59
227
|
// ========== 射线碰撞检测 ==========
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
COLLIDE_VOLUME
|
|
63
|
-
//
|
|
64
|
-
//
|
|
228
|
+
//
|
|
229
|
+
// ⭐⭐ 2026-09-26 皇上旨「把『碰撞包络』『贴墙余量』这些可以去掉的参数都去掉,不要那么复杂」
|
|
230
|
+
// → 旧的 `COLLIDE_VOLUME`(cv=0.5) 整条删除。它在最后一次简化(回退搜索改解析推离)之后
|
|
231
|
+
// 已经**不再决定任何停位**,只剩三件壳:① 斜弦起点写 R/cv、6-9 高度写「局部 × cv」
|
|
232
|
+
// (= 一套没人看的中间单位);② far 的分母;③ 面板上两个看着像旋钮、拧了其实只换尺子的格子。
|
|
233
|
+
// ⇒ 现在**全部直接用世界米**:斜弦起点 (±R,0,0)、6-9 起点 (0, 皇上拧的米数, ±R)、
|
|
234
|
+
// far = R√2。菱形几何一个数没变(R=1.06 → 外接框 2.12×2.12、斜弦 45°、首尾闭合),
|
|
235
|
+
// 只是不再经过「局部单位」这道换算。竖向的 2cm 探测余量见 `VERTICAL_SKIN`。
|
|
236
|
+
//
|
|
237
|
+
// 【停位口径(现行唯一,皇上 2026-09-27 十五旨)】射线在 far 内命中 = 碰撞;
|
|
238
|
+
// 「还差多远才脱开」= 菱形沿墙法线的可达距离 reach = R·max(|n·右|, |n·前|) ∈ [R/√2, R]
|
|
239
|
+
// 减去当前垂距 = deficit,取全轮最大者当读数;位置修正沿回弹方向**一格一格推**(每格
|
|
240
|
+
// RETREAT_SKIN),推到最大穿透 ≤ 阈值 / 全脱开为止 —— 停位带与旧的解析解同宽。
|
|
241
|
+
// ⇒ 停位仍只由 **R** 一个数决定(R=1.06 → 沿法线 0.749~1.06m);胶囊半径 0.35 ⇒ 身体与墙之间
|
|
242
|
+
// 还留 0.40~0.71m 空气隙,这是皇上 2026-09-06 终裁布局自带、且亲眼验收过的量,不要当 bug 修。
|
|
243
|
+
/**
|
|
244
|
+
* 单步最大位移(连续碰撞检测用):每帧位移会被切成若干 ≤ 本值的小步,逐步做碰撞检测。
|
|
245
|
+
* ⚠️ 必须明显小于水平包络的可达距离(R=1.06 时沿法线 0.749~1.06m),否则掉帧时一步就跨过墙体。
|
|
246
|
+
* ⚠️ 本值同时决定「单次碰撞需要修正多深」——子步越小,玩家每次陷进墙里越浅,
|
|
247
|
+
* 迭代要走的格数就越少(一格恒 RETREAT_SKIN,总位移 ≈ 该子步的穿透深度),
|
|
248
|
+
* 不会出现「一下子回弹很多」。
|
|
249
|
+
* 0.25 → 0.1:单次最大穿透从 0.25 降到 0.1,回弹量同比变小(子步数相应上调)。
|
|
250
|
+
*/
|
|
251
|
+
MAX_STEP_DISTANCE = 0.1;
|
|
252
|
+
/** 位移细分的最大段数(防止极端速度/长卡顿把一帧切成几百步拖垮帧率) */
|
|
253
|
+
MAX_STEP_COUNT = 32;
|
|
254
|
+
// 垂直碰撞范围 = 从 playerCollider.end(眼睛)到「该射线所保护的那一面」的距离
|
|
255
|
+
// = capsule length + radius = (1.0 - 0.35) + 0.35 = 1.0(几何真值;下方两个常量是**口径**,不是几何)
|
|
65
256
|
// 对应 xsh-react-three 中 collideVolume(=3)= 控制器到地面的距离
|
|
66
257
|
// xsh 中 far = collideVolume, 响应 = collideVolume - distance
|
|
67
|
-
// 此处 far =
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
|
|
258
|
+
// 此处 far = DOWN_RANGE/UP_RANGE + SKIN,响应 = DOWN_RANGE/UP_RANGE − distance
|
|
259
|
+
//
|
|
260
|
+
// ⭐⭐ 2026-09-25 皇上旨「把下射线的长度改成 1.5,上射线的长度改成 0.3」→
|
|
261
|
+
// 我提醒过这一条常量身兼两职(探测长度 + 站位基准),他追选定案「**直接改站位基准**」,
|
|
262
|
+
// 所以拆成两条各自独立的常量,**不再**共用 VERTICAL_RANGE=1.0:
|
|
263
|
+
// · DOWN_RANGE = 1.5:下射线 far = 1.5+SKIN,响应 downPen = 1.5 − distance
|
|
264
|
+
// → 落地后**眼睛离地 1.5m**。胶囊脚底在眼下 1.0m ⇒ 站定时脚底离地 **0.5m(悬浮)**。
|
|
265
|
+
// · UP_RANGE = 0.3:上射线 far = 0.3+SKIN,响应 upPen = 0.3 − distance
|
|
266
|
+
// → 头顶到面时**眼睛离顶 0.3m**。胶囊顶在眼下 0.35m ⇒ 真顶到矮面时头顶穿入面内 **0.05m**,
|
|
267
|
+
// 且 0.3~1.0m 之间的低矮障碍物从此探不到(上射线变短的直接后果)。
|
|
268
|
+
// ⚠️ 跳跃/重力/台阶吸附的其余口径一字未动,只换这两个基准数。
|
|
269
|
+
// ⭐ 2026-09-26 皇上旨「能调节的参数都做滚轮输入框」→ 两条改为公共可写(面板「参数旋钮」可拧),
|
|
270
|
+
// 但 1.5 / 0.3 仍是皇上定下的**基线**:改公共性 ≠ 改基线,默认值一字未动。
|
|
271
|
+
DOWN_RANGE = 1.5;
|
|
272
|
+
UP_RANGE = 0.3;
|
|
273
|
+
/**
|
|
274
|
+
* 「脚下到底还有没有地面」的深探长度(2026-09-25,仅用于穿地板自救的判据)。
|
|
275
|
+
* 竖向两条射线只够得着 下 (DOWN_RANGE+SKIN)=1.52m / 上 (UP_RANGE+SKIN)=0.32m,
|
|
276
|
+
* 正常跳跃顶点 ≈ jumpSpeed²/(2·GRAVITY) = 5²/20 = 1.25m(实测脚底最高 ≈ 1.5m)
|
|
277
|
+
* → 腾空时脚下射线天然全瞎。
|
|
278
|
+
* 若此时头顶又撞到东西,就分不清
|
|
279
|
+
* 「跳起来顶到天花板」(该往下压)和「穿过地板掉进无底洞」(该顶出来)。
|
|
280
|
+
* 判据:往下再问 VOID_PROBE_DOWN 一段,问得到地面 = 正常腾空/下落,问不到 = 已经穿模。
|
|
281
|
+
* 取 8m ≈ 2 跳多一点;比它更深的地面在 demo 里不存在。
|
|
282
|
+
*/
|
|
283
|
+
VOID_PROBE_DOWN = 8.0;
|
|
284
|
+
/**
|
|
285
|
+
* 竖向两条射线的探测余量(2cm)。⚠️ 它**不是**水平参数:水平侧的「离墙多远」全由 R 决定
|
|
286
|
+
* (见上方停位口径),这里只是给「脚底离地 1.5m / 眼离顶 0.3m」两条基准各留一点富余,
|
|
287
|
+
* 避免落地那一帧量到的距离恰好等于基准 → 判据在阈值上抖动。
|
|
288
|
+
* 2026-09-26 皇上旨「把贴墙余量这类可以去掉的参数都去掉」→ 名字里「贴墙」的语义已不存在
|
|
289
|
+
* (水平不再读它),面板格子一并删除,改私有;数值 0.02 一字未动(竖向已冻结)。
|
|
290
|
+
*/
|
|
291
|
+
VERTICAL_SKIN = 0.02;
|
|
292
|
+
/**
|
|
293
|
+
* ⭐ 皇上 2026-09-06 定案:射线围成的**菱形**(旋转 45° 的正方形)**半对角线 R = 1.06 m**
|
|
294
|
+
* → 外接框 **2.12 × 2.12 m**(皇上原话「横向你也改成 2.12m」+「要正方形不要 X 形状」)。
|
|
295
|
+
*
|
|
296
|
+
* ⭐⭐ 菱形的几何约束:**起点外扩量 a 就是半对角线 R,画长 L = R√2**。
|
|
297
|
+
* 起点 (±a,0,0) + 方向朝内 45° + 长 L → 末端 (a − L/√2, 0, ∓L/√2)
|
|
298
|
+
* 要末端正好落在顶点 (0,0,∓R) 使四弦**首尾闭合**:a = L/√2 = R。
|
|
299
|
+
* ⇒ 起点一旦挪回**玩家中心**(a=0),四弦就从同一点发散 → 变成**X 形状**(皇上否决)。
|
|
300
|
+
* ⇒ 要菱形多大,起点就得外扩多少;两者是同一个数。
|
|
301
|
+
*
|
|
302
|
+
* ⚠️ 臣曾误判:以为起点外扩越大,命中点切向偏移越大、回弹方向越歪。
|
|
303
|
+
* **实测相反**:R=0.5(旧) 平均偏离 17.89°、R=0(中心) 7.88°、**R=1.06 只有 4.20°**(最准)。
|
|
304
|
+
*
|
|
305
|
+
* ⛔⛔ **2026-09-06 19:32 皇上终裁:布局冻结** —— 原话「终于对了,这个布局千万不要改了」。
|
|
306
|
+
* 当日 21:56 再旨删掉 6/7 两条 ±X 侧向射线;2026-09-25 晚否决「均匀放射 N=12」方案、
|
|
307
|
+
* 令回退矩形布局(「我最开始用射线组合的矩形,就是最好的方案」)。
|
|
308
|
+
* **已验收通过,不得再动**(含 R 本身与射线条数)。皇上未发话前,任何调整——
|
|
309
|
+
* 哪怕只是「再调大一点」「再精简一条」——都必须**先请旨**。
|
|
310
|
+
* (几何上要改大小只动 `CHORD_RHOMBUS_R` 一个数,far = R√2 自动跟着闭合。)
|
|
311
|
+
* ⭐ 2026-10-08 皇上旨(明确发话,覆盖本段冻结):默认 R 改 **0.5**、可调范围放宽到 **[0.2, 1.0]**,
|
|
312
|
+
* 见下方 `CHORD_RHOMBUS_R` 默认值、`MIN/MAX_HORIZONTAL_RAY_RADIUS` 与 `PARAM_SPECS` 编辑框。
|
|
313
|
+
*/
|
|
314
|
+
/** 菱形半对角线 R(水平射线半径,单位=世界米)。
|
|
315
|
+
* ⭐ 2026-10-08 皇上旨:默认 **0.5**(覆盖 09-06 终裁的 1.06);运行时可写公共字段,
|
|
316
|
+
* 走 setHorizontalRayRadius(r) 调整,clamp 到 [0.2, 1.0](见 MIN/MAX_HORIZONTAL_RAY_RADIUS)。 */
|
|
317
|
+
CHORD_RHOMBUS_R = 0.5;
|
|
318
|
+
/**
|
|
319
|
+
* 6-9 这组「共线反向」射线的起点高度(**世界米**,相对控制器中心;负 = 低于中心)。
|
|
320
|
+
*
|
|
321
|
+
* ⭐ 2026-09-26 皇上五道旨(当日反复,全部记录在此,别再拿任意一版当定案):
|
|
322
|
+
* ①「平行的射线往下移动一点」⇒ 与 2-5 重合(0);②「2 组不要重合,要整体下移」⇒ −0.5;
|
|
323
|
+
* ③「平行射线高度 改成 −0.7」⇒ −0.7;④「按面板打印的那份日志参数设初始默认值」⇒ 回 −0.5;
|
|
324
|
+
* ⑤**定案:两组 = −0.7(2-5)与 −0.75(6-9)**⇒ 本值 **−0.75m**。
|
|
325
|
+
* 站定时眼在脚底以上 1.5m ⇒ 2-5 离地 0.8m、6-9 离地 0.75m,两组只差 5cm(皇上旨「不用间隔的太宽」)。
|
|
326
|
+
* ⚠️ 副作用(皇上已知):两组都从「与眼同高」降到胸口/腰,矮桌、栏杆、地毯边、
|
|
327
|
+
* 低台阶沿都会变成「撞得上的墙」;1.5m 以上只靠 0/1 竖向射线与顶部判定。
|
|
328
|
+
* ⚠️ 本字段有**两处**消费:`_localRayOrigins` 初值 + `setHorizontalRayRadius()`(面板「水平射线框」
|
|
329
|
+
* 滑动条每次拖动都重写 6-9 起点)。只改初值 = 拖一下滑动条就被悄悄改回去,所以两处必须同源。
|
|
330
|
+
* 旧字段存的是「局部单位」(× cv 才是米),2026-09-26 随 cv 一并废除换算,本字段从此就是米数。
|
|
331
|
+
*/
|
|
332
|
+
HORIZ_REVERSE_RAY_Y = -0.75;
|
|
333
|
+
/**
|
|
334
|
+
* 2-5 这组斜弦的起点高度(**世界米**,相对控制器中心;负 = 低于中心)。
|
|
335
|
+
*
|
|
336
|
+
* ⭐ 2026-09-26 皇上问「平行线的高度怎么只有一组可以调」—— 因为这一组此前**没有字段**,
|
|
337
|
+
* y=0 直接内联在 `_localRayOrigins[2..5]` 与 `setHorizontalRayRadius()` 两处 = 死值。
|
|
338
|
+
* 现在补成字段 + 旋钮,与 6-9 对称。
|
|
339
|
+
* ⚠️ 同样有**两处**消费(初值 + `setHorizontalRayRadius()`),只改一处就是死旋钮。
|
|
340
|
+
* ⭐ 定案见下方 6-9 那条「五道旨」:本值 **−0.7m**(离地 0.8m)。
|
|
341
|
+
* ⚠️ 这一改**动了 09-06 终裁布局里「2-5 与眼同高」那条**(皇上 09-26 五旨点名要 −0.7);
|
|
342
|
+
* 菱形闭合(R / far=R√2 / 停位距离)与此无关,一字未动。
|
|
343
|
+
*/
|
|
344
|
+
HORIZ_FWD_RAY_Y = -0.7;
|
|
345
|
+
/**
|
|
346
|
+
* ⭐ 2026-09-26 皇上旨「2 组平行射线不用间隔的太宽了,你把能调节的参数都做成滚轮输入框」→
|
|
347
|
+
* 6-9 那组的高度从此由皇上自己在面板「参数旋钮」里拧,不再由臣定档位。
|
|
348
|
+
* 参数就是**世界米**(相对控制器中心,负 = 低于中心),字段存什么面板就显示什么。
|
|
349
|
+
* ⚠️ 写入口必须**两处同时写**(`_localRayOrigins[6..9]` + `_rays[6..9].localOrigin`),
|
|
350
|
+
* 少一处 = 拖一下「水平射线框」滑动条就把高度悄悄改回去的死旋钮(皇上多次踩过)。
|
|
351
|
+
* @param worldY 世界米 y 偏移(相对控制器中心)
|
|
352
|
+
*/
|
|
353
|
+
setHorizReverseRayY(worldY) {
|
|
354
|
+
this.HORIZ_REVERSE_RAY_Y = worldY;
|
|
355
|
+
const r = this.CHORD_RHOMBUS_R;
|
|
356
|
+
for (let i = 0; i < 4; i++) {
|
|
357
|
+
const zSign = i < 2 ? -1 : 1; // 6/7 在菱形 −R 顶点,8/9 在 +R 顶点(与 setHorizontalRayRadius 同式)
|
|
358
|
+
this._localRayOrigins[6 + i].set(0, worldY, zSign * r);
|
|
359
|
+
this._rays[6 + i].localOrigin.set(0, worldY, zSign * r);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* ⭐ 2026-09-26 皇上旨「平行线的高度怎么只有一组可以调」→ 2-5 那组也做成可拧的,两处写入口同源。
|
|
364
|
+
* @param worldY 世界米 y 偏移(相对控制器中心);皇上 09-26 五旨定案默认 −0.75(2-5 是 −0.7)
|
|
365
|
+
*/
|
|
366
|
+
setHorizFwdRayY(worldY) {
|
|
367
|
+
this.HORIZ_FWD_RAY_Y = worldY;
|
|
368
|
+
const signs = [1, -1, -1, 1]; // 与 _localRayOrigins[2..5] 对应(右/左/左/右顶点)
|
|
369
|
+
for (let i = 0; i < 4; i++) {
|
|
370
|
+
const x = signs[i] * this.CHORD_RHOMBUS_R;
|
|
371
|
+
this._localRayOrigins[2 + i].set(x, worldY, 0);
|
|
372
|
+
this._rays[2 + i].localOrigin.set(x, worldY, 0);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* 水平射线半径 R 的安全下限(⭐ 2026-09-22 补,皇上滑动条可调后暴露)。
|
|
377
|
+
* **几何事实**:斜弦沿法线的可达距离 = **R**(far = R√2、斜弦 45° → R√2·cos45° = R),
|
|
378
|
+
* 即 R 就是「玩家中心离墙多远开始判定碰撞」的半径。若 R 小于停位需求,人必须**先陷进墙里**
|
|
379
|
+
* 才会被射线检测到,随后每帧被位置修正弹回一点 → 皇上所见「侧撞弹很远」。
|
|
380
|
+
* 探针实测(`tmp/TMP_R_side_effect.mjs`,斜 45° 撞墙角 / 正对单墙同口径):
|
|
381
|
+
* R=0.30 → 停位垂距 **0.2128**(< 胶囊半径 0.35,**明显穿模**)每帧被推 0.040
|
|
382
|
+
* R=0.50 → 0.3541(贴到墙面) 每帧 0.036
|
|
383
|
+
* R=0.60 → 0.4243(勉强 > 胶囊半径)0.034
|
|
384
|
+
* R=0.80 → 0.5448 ✓ / R=1.06 → 0.7247 ✓(每帧修正仅 0.002)
|
|
385
|
+
* ⇒ 取 **0.6**:保证停位垂距 > 玩家胶囊半径(0.35),不再穿模。
|
|
386
|
+
* ⭐ 2026-10-08 皇上旨:下限放宽到 **0.2**、上限收到 **1.0**(编辑框范围 0.2~1.0)。
|
|
387
|
+
* ⚠️ R<0.6 仍会贴墙穿模(R=0.5 贴到墙面、R=0.3 明显穿模),皇上按需拧;下限放开是给窄通道/贴墙演示留余地。
|
|
388
|
+
*/
|
|
389
|
+
static MIN_HORIZONTAL_RAY_RADIUS = 0.2;
|
|
390
|
+
/** 水平射线半径 R 的安全上限(⭐ 2026-10-08 皇上旨收到 1.0:再大包络远大于停位需求、回弹精度已够,白烧射程) */
|
|
391
|
+
static MAX_HORIZONTAL_RAY_RADIUS = 1.0;
|
|
392
|
+
/** 水平射线(斜弦)的 far = R√2 —— 四弦首尾闭合的几何解,随 R 一个数走。 */
|
|
393
|
+
get HORIZONTAL_FAR() {
|
|
394
|
+
return this.CHORD_RHOMBUS_R * Math.SQRT2;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* 运行时设置水平射线(菱形)半对角线半径 R(单位:世界米)。
|
|
398
|
+
* 改变后 4 条斜弦起点 x = ±R、4 条反向弦起点 z = ±R **原地更新**
|
|
399
|
+
* (同时更新碰撞检测用的 `_localRayOrigins` 与调试线用的 `_rays[i].localOrigin`,两处保持一致)。
|
|
400
|
+
* ⚠️ R 越大碰撞包络越大、回弹越准(R=1.06 约 4.20°);R 越小包络越小、贴墙余量越小、回弹偏离越大(R=0.5 旧值约 17.89°)。
|
|
401
|
+
* 整束水平射线的大小只由此一个数决定(far = R√2 自动跟随,菱形闭合不破)。
|
|
402
|
+
*/
|
|
403
|
+
setHorizontalRayRadius(r) {
|
|
404
|
+
// ⭐ 2026-09-22:clamp 到安全区间 —— 越界值会让碰撞包络小于停位需求(贴墙穿模 + 每帧弹回),
|
|
405
|
+
// 见 MIN_HORIZONTAL_RAY_RADIUS 注释里的实测表。Demo 滑动条 / meta 表单的 min 同取此下限。
|
|
406
|
+
const clamped = Math.min(FPScontrol.MAX_HORIZONTAL_RAY_RADIUS, Math.max(FPScontrol.MIN_HORIZONTAL_RAY_RADIUS, r));
|
|
407
|
+
this.CHORD_RHOMBUS_R = clamped;
|
|
408
|
+
const signs = [1, -1, -1, 1]; // 与 _localRayOrigins[2..5] 对应的 x 符号(右/左/左/右顶点)
|
|
409
|
+
for (let i = 0; i < 4; i++) {
|
|
410
|
+
const x = signs[i] * clamped;
|
|
411
|
+
// 2-5 斜弦:起点 y = HORIZ_FWD_RAY_Y
|
|
412
|
+
this._localRayOrigins[2 + i].set(x, this.HORIZ_FWD_RAY_Y, 0); // 碰撞检测用(主循环与停位公式都读此数组)
|
|
413
|
+
this._rays[2 + i].localOrigin.set(x, this.HORIZ_FWD_RAY_Y, 0); // 调试线用(每帧 updateWorld 读 localOrigin)
|
|
414
|
+
// ⭐ 6-9 共线反向射线(皇上 2026-09-14 改):起点 = 2-5 终点(菱形内侧顶点),
|
|
415
|
+
// 6/7 对应 2/3(终点 z=-R)→ local z=-k;8/9 对应 4/5(终点 z=+R)→ local z=+k;
|
|
416
|
+
// x=0、y = HORIZ_REVERSE_RAY_Y(皇上 2026-09-26 五旨定案 −0.75,比 2-5 的 −0.7 再低 5cm;
|
|
417
|
+
// 中途的 −0.5 / −0.7 与旧值 0.2(世界 0.1m)、「归零重合」各版均已作废)。
|
|
418
|
+
// 与 2-5 共线、方向取反 → 同一直线双向探测。
|
|
419
|
+
// ⚠️ 这里重写的是**滑动条路径**:漏改就会让「拖一下 R 就把高度改回去」= 死旋钮。
|
|
420
|
+
const zSign = i < 2 ? -1 : 1;
|
|
421
|
+
this._localRayOrigins[6 + i].set(0, this.HORIZ_REVERSE_RAY_Y, zSign * clamped);
|
|
422
|
+
this._rays[6 + i].localOrigin.set(0, this.HORIZ_REVERSE_RAY_Y, zSign * clamped);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* ⭐⭐ 2026-09-26 皇上旨「2 组平行射线不用间隔的太宽了,你把能调节的参数都给我做的滚轮输入的输入框,
|
|
427
|
+
* 然后一键打印,把参数打印到控制台」—— 全部可调旋钮的**唯一数据源**。
|
|
428
|
+
*
|
|
429
|
+
* 面板「参数旋钮」分组按本表逐行生成滚轮输入框;「打印参数(全量·控制台)」按本表逐行出行;
|
|
430
|
+
* `setParamValue()` 按本表夹范围并走各自的 `set`(该走 setter 的一个都不能漏,否则又是死旋钮)。
|
|
431
|
+
*
|
|
432
|
+
* ⚠️ 表里 `frozen: true` 的行 = 皇上已下旨定死基线(布局冻结 09-06、竖向 09-25、撞墙清零 09-25),
|
|
433
|
+
* 做成输入框只是让皇上自己能拧,**不代表臣可以改默认值**;默认值一律保持基线原样。
|
|
434
|
+
*/
|
|
435
|
+
static PARAM_SPECS = [
|
|
436
|
+
// ===== 射线几何(拧了立刻反映到辅助线上)=====
|
|
437
|
+
{
|
|
438
|
+
group: "射线几何",
|
|
439
|
+
label: "水平射线框 R",
|
|
440
|
+
key: "CHORD_RHOMBUS_R",
|
|
441
|
+
min: FPScontrol.MIN_HORIZONTAL_RAY_RADIUS,
|
|
442
|
+
max: FPScontrol.MAX_HORIZONTAL_RAY_RADIUS,
|
|
443
|
+
step: 0.01,
|
|
444
|
+
digits: 2,
|
|
445
|
+
unit: "m",
|
|
446
|
+
frozen: true,
|
|
447
|
+
desc: "菱形半对角线 R,外接框边长 = 2R(= 离墙多远开始判定碰撞的半径)。越大包络越大、回弹方向越准;R<0.6 仍会贴墙穿模(R=0.5 贴墙面、R=0.3 明显穿模),已放宽到 0.2~1.0 供按需拧",
|
|
448
|
+
get: (fps) => fps.CHORD_RHOMBUS_R,
|
|
449
|
+
set: (fps, v) => fps.setHorizontalRayRadius(v),
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
group: "射线几何",
|
|
453
|
+
label: "平行射线高度·2-5",
|
|
454
|
+
key: "HORIZ_FWD_RAY_Y",
|
|
455
|
+
min: -2.0,
|
|
456
|
+
max: 2.0,
|
|
457
|
+
step: 0.05,
|
|
458
|
+
digits: 2,
|
|
459
|
+
unit: "m",
|
|
460
|
+
desc: "2-5 这组(前方/斜弦)射线的起点高度,相对控制器中心、世界米。皇上 09-26 五旨定案 −0.7 = 离地 0.8m(胸/腰之间,不再与眼同高)。与「平行射线高度」那一行相加/相减才是两组的高差。拧它 = 改 2-5 探的是胸口还是腰,矮障碍/高墙的归属会跟着变",
|
|
461
|
+
set: (fps, v) => fps.setHorizFwdRayY(v),
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
group: "射线几何",
|
|
465
|
+
label: "平行射线高度",
|
|
466
|
+
key: "HORIZ_REVERSE_RAY_Y",
|
|
467
|
+
min: -2.0,
|
|
468
|
+
max: 2.0,
|
|
469
|
+
step: 0.05,
|
|
470
|
+
digits: 2,
|
|
471
|
+
unit: "m",
|
|
472
|
+
desc: "6-9 那组「平行/共线反向」射线的起点高度,相对控制器中心、世界米。与 2-5 的间隔 = |本值 − 平行射线高度·2-5|;皇上 09-26 五旨定案 −0.75 = 比 2-5(−0.7)再低 5cm、离地 0.75m(「2 组平行射线不用间隔的太宽了」)。越低越先探到身后腰下的矮障碍(矮桌/栏杆/地毯边)",
|
|
473
|
+
set: (fps, v) => fps.setHorizReverseRayY(v),
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
group: "射线几何",
|
|
477
|
+
label: "下射线长度",
|
|
478
|
+
key: "DOWN_RANGE",
|
|
479
|
+
min: 0.5,
|
|
480
|
+
max: 3,
|
|
481
|
+
step: 0.05,
|
|
482
|
+
digits: 2,
|
|
483
|
+
unit: "m",
|
|
484
|
+
frozen: true,
|
|
485
|
+
desc: "落地后眼睛离地的高度(站位基准)。1.5 ⇒ 脚底悬浮 0.5m;改小人会往下沉,改大悬浮更高。⚠️ 皇上 2026-09-25 定案基线,改水平碰撞不许牵连它",
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
group: "射线几何",
|
|
489
|
+
label: "上射线长度",
|
|
490
|
+
key: "UP_RANGE",
|
|
491
|
+
min: 0,
|
|
492
|
+
max: 2,
|
|
493
|
+
step: 0.05,
|
|
494
|
+
digits: 2,
|
|
495
|
+
unit: "m",
|
|
496
|
+
frozen: true,
|
|
497
|
+
desc: "头顶到面时「眼睛离顶」的距离。0.3 ⇒ 真顶到矮面时头顶陷入面内 0.05m,且 0.3~1.0m 之间的低矮障碍探不到",
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
group: "射线几何",
|
|
501
|
+
label: "穿地板自救探深",
|
|
502
|
+
key: "VOID_PROBE_DOWN",
|
|
503
|
+
min: 2,
|
|
504
|
+
max: 20,
|
|
505
|
+
step: 0.5,
|
|
506
|
+
digits: 1,
|
|
507
|
+
unit: "m",
|
|
508
|
+
desc: "腾空时再往下问一段,问不到地面 = 已穿模 → 把人顶回来。比它更深的地面在场景里不存在",
|
|
509
|
+
},
|
|
510
|
+
// ===== 回弹与手感(步进 / 预算 / 两条对消阈值)=====
|
|
511
|
+
{
|
|
512
|
+
group: "回弹与手感",
|
|
513
|
+
label: "单步最大位移",
|
|
514
|
+
key: "MAX_STEP_DISTANCE",
|
|
515
|
+
min: 0.02,
|
|
516
|
+
max: 0.4,
|
|
517
|
+
step: 0.01,
|
|
518
|
+
digits: 2,
|
|
519
|
+
unit: "m",
|
|
520
|
+
desc: "连续碰撞检测把每帧位移切成的最大段长。越小单次穿透越浅、回弹越不「一下子弹很远」,但子步更费。⚠️ 必须明显小于水平包络可达距离 R(默认 1.06),超过掉帧时会一步跨过墙",
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
group: "回弹与手感",
|
|
524
|
+
label: "一帧最多切几步",
|
|
525
|
+
key: "MAX_STEP_COUNT",
|
|
526
|
+
min: 4,
|
|
527
|
+
max: 64,
|
|
528
|
+
step: 1,
|
|
529
|
+
digits: 0,
|
|
530
|
+
unit: "步",
|
|
531
|
+
desc: "位移细分的段数上限(防止极端速度/长卡顿把一帧切成几百步拖垮帧率)",
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
group: "回弹与手感",
|
|
535
|
+
label: "每帧碰撞计算次数",
|
|
536
|
+
key: "STEPS_PER_FRAME",
|
|
537
|
+
min: 1,
|
|
538
|
+
max: 4,
|
|
539
|
+
step: 1,
|
|
540
|
+
digits: 0,
|
|
541
|
+
unit: "次",
|
|
542
|
+
desc: "一帧里跑几遍碰撞+回弹循环。1 最省,4 最贴手但每帧多付三倍查询",
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
group: "回弹与手感",
|
|
546
|
+
label: "每帧迭代预算",
|
|
547
|
+
key: "MAX_ITERATIONS",
|
|
548
|
+
min: 10,
|
|
549
|
+
max: 1000,
|
|
550
|
+
step: 10,
|
|
551
|
+
digits: 0,
|
|
552
|
+
unit: "次",
|
|
553
|
+
desc: "一帧内全部子步共享的迭代上限(预算烧完即停,宁可贴墙也不空转)。皇上 2026-10-03 旨:不超过 500",
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
group: "回弹与手感",
|
|
557
|
+
label: "单次推离",
|
|
558
|
+
key: "RETREAT_SKIN",
|
|
559
|
+
min: 0.005,
|
|
560
|
+
max: 0.2,
|
|
561
|
+
step: 0.005,
|
|
562
|
+
digits: 3,
|
|
563
|
+
unit: "m",
|
|
564
|
+
desc: "⭐ 2026-09-28 皇上旨「完完全全用 xsh-react-three 的碰撞法」:每条命中射线每轮沿「命中点→中心」方向推的一小格(xsh 源码里写死 0.03;**皇上 10-07 旨「应该 1 个单位 1 个单位的修正」⇒ 默认改 0.01 = 一格 1cm**)。" +
|
|
565
|
+
"⚠️ 上限以下别贪小:0.03/0.02/0.01 体检台判级完全相同,拧到 0.005 才有改善但「跳跃+冲刺+掉帧」那个现场开始穿墙(格长 = 每帧出水量)。迭代推到全部射线脱开为止",
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
group: "回弹与手感",
|
|
569
|
+
label: "每小帧轮数上限",
|
|
570
|
+
key: "ROUNDS_PER_SUBSTEP",
|
|
571
|
+
min: 1,
|
|
572
|
+
max: 500,
|
|
573
|
+
step: 1,
|
|
574
|
+
digits: 0,
|
|
575
|
+
unit: "轮",
|
|
576
|
+
desc: "⭐ 皇上 2026-10-07 裁决加:一个物理小帧里 while 最多走几轮就落地(默认 500 = 与「每帧迭代预算」同档 ⇒ 现状一字不变)。" +
|
|
577
|
+
"**这颗才是「一次修多少」的真闸门**:10-07 实测每次落地量 = 这一小帧往墙里挤进多深(2/8/10cm 都这么来),跟一格多远无关 ⇒ 要「一次只修几格、下一小帧再修」只能拧这里。" +
|
|
578
|
+
"⚠️ 拧小的代价:一秒能挤出去的总量变小,高速冲刺顶墙时「挤进来的比推出去的快」⇒ 越陷越深(格长拧到 0.005 那档体检台 40 号现场就判 ❌穿墙,同一个机理)。先拧着看手感,别把默认改小",
|
|
579
|
+
},
|
|
580
|
+
// ===== 移动与视角 =====
|
|
581
|
+
{
|
|
582
|
+
group: "移动与视角",
|
|
583
|
+
label: "移动速度",
|
|
584
|
+
key: "speed",
|
|
585
|
+
min: 0.2,
|
|
586
|
+
max: 3,
|
|
587
|
+
step: 0.05,
|
|
588
|
+
digits: 2,
|
|
589
|
+
desc: "第一人称移动速度倍率(面板与右侧表单同一个字段)",
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
group: "移动与视角",
|
|
593
|
+
label: "跳跃初速度",
|
|
594
|
+
key: "jumpSpeed",
|
|
595
|
+
min: 1,
|
|
596
|
+
max: 8,
|
|
597
|
+
step: 0.2,
|
|
598
|
+
digits: 1,
|
|
599
|
+
unit: "m/s",
|
|
600
|
+
desc: "起跳垂直初速度。⚠️ 重力按皇上 10-03 旨固定为 10(已从旋钮表删除,不可编辑)⇒ 顶点 = 初速度²/20、滞空 = 2×初速度/10。当前 5 ⇒ 顶点 1.25m(实测脚底最高与旧 8@重力30 同一档)。上限锁 8:8 ⇒ 顶点 3.2m 越过墙顶 2m,会重开 09-27 治好的「跳跃翻墙 ⇒ 穿墙」窗口",
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
group: "移动与视角",
|
|
604
|
+
label: "惯性阻尼",
|
|
605
|
+
key: "inertiaDamp",
|
|
606
|
+
min: 0.5,
|
|
607
|
+
max: 0.99,
|
|
608
|
+
step: 0.01,
|
|
609
|
+
digits: 2,
|
|
610
|
+
desc: "「移动惯性」关掉时每帧水平速度保留率,越小松键停得越快",
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
group: "移动与视角",
|
|
614
|
+
label: "第三人称距离",
|
|
615
|
+
key: "thirdPersonDistance",
|
|
616
|
+
min: 1,
|
|
617
|
+
max: 15,
|
|
618
|
+
step: 0.1,
|
|
619
|
+
digits: 1,
|
|
620
|
+
unit: "m",
|
|
621
|
+
desc: "第三人称相机在玩家身后多远",
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
group: "移动与视角",
|
|
625
|
+
label: "第三人称高度",
|
|
626
|
+
key: "thirdPersonHeight",
|
|
627
|
+
min: 0,
|
|
628
|
+
max: 10,
|
|
629
|
+
step: 0.1,
|
|
630
|
+
digits: 1,
|
|
631
|
+
unit: "m",
|
|
632
|
+
desc: "第三人称相机高出玩家多少",
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
group: "辅助线与预览",
|
|
636
|
+
label: "辅助线粗细",
|
|
637
|
+
key: "debugLineWidth",
|
|
638
|
+
min: 1,
|
|
639
|
+
max: 8,
|
|
640
|
+
step: 0.5,
|
|
641
|
+
digits: 1,
|
|
642
|
+
unit: "px",
|
|
643
|
+
desc: "射线辅助线像素宽(只有 LineSegments2 才吃这个值,LineBasicMaterial 会被驱动钳成 1px)",
|
|
644
|
+
},
|
|
645
|
+
];
|
|
646
|
+
/** 读某个旋钮的当前值(表里没有该 key 时返回 NaN,面板/打印据此跳过,不会崩) */
|
|
647
|
+
getParamValue(key) {
|
|
648
|
+
const spec = FPScontrol.PARAM_SPECS.find((s) => s.key === key);
|
|
649
|
+
if (!spec) {
|
|
650
|
+
return NaN;
|
|
651
|
+
}
|
|
652
|
+
// 旋钮一律按「字段名」存取:表 = 唯一数据源,面板和打印读的是同一个数
|
|
653
|
+
return spec.get ? spec.get(this) : Number(this[spec.key]);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* 写某个旋钮:先夹到 [min,max],再走该旋钮自己的 `set`(缺省 = 直接写字段)。
|
|
657
|
+
* @returns 真正落进去的值(夹过之后的),面板拿它回显,避免显示的和实际的两张皮
|
|
658
|
+
*/
|
|
659
|
+
setParamValue(key, v) {
|
|
660
|
+
const spec = FPScontrol.PARAM_SPECS.find((s) => s.key === key);
|
|
661
|
+
if (!spec || !Number.isFinite(v)) {
|
|
662
|
+
return this.getParamValue(key);
|
|
663
|
+
}
|
|
664
|
+
const clamped = Math.min(spec.max, Math.max(spec.min, v));
|
|
665
|
+
if (spec.set) {
|
|
666
|
+
spec.set(this, clamped);
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
this[key] = clamped;
|
|
670
|
+
}
|
|
671
|
+
return clamped;
|
|
672
|
+
}
|
|
673
|
+
// ⚠️ 2026-09-06 21:56 皇上撤旨删除 6/7 侧向射线(±X 直墙改由场景保证/接受穿墙)→ SIDE_FAR_K 一并移除。
|
|
674
|
+
// ⚠️⚠️ 2026-09-25 皇上旨「向下的射线有陷入地板」→ 原 `DEBUG_RAY_VISUAL_LEN = 1.5` **作废并删除**。
|
|
675
|
+
// 病根:辅助线起点在脚底上方竖向基准处,却画一个更长的定值 → 线头扎进地板以下,
|
|
676
|
+
// 而上图那段根本不是射线(射线真实只探到 far)。当年 1.5 是为了让竖线不比斜弦短
|
|
677
|
+
// (构图齐平),皇上今日定案改为「**每条线画到自己的真实长度**」:
|
|
678
|
+
// · 下 0 → DOWN_RANGE = 1.5,上 1 → UP_RANGE = 0.3(同日再旨拆开的两条基准;
|
|
679
|
+
// 站定时下射线 distance 恰为 1.5 → 线头正好落在地板面上,不会埋进地板)
|
|
680
|
+
// · 水平 2-9 → 画到碰撞 far = horizontalFar = R√2 ≈ 1.4991
|
|
681
|
+
// 哪日要恢复「竖线比射线长」的观感,得重新请旨,别顺手塞回一个定值。
|
|
682
|
+
/**
|
|
683
|
+
* 辅助线粗细(**像素**)。2026-09-25 皇上旨「辅助线能调粗一点」+ 定案 2px。
|
|
684
|
+
* ⚠️ 这个旋钮只有在 `LineMaterial`(LineSegments2)上才活着:`LineBasicMaterial.linewidth`
|
|
685
|
+
* 被 WebGL 驱动一律钳成 1px,改大完全无效果。像素宽依赖 `resolution` = 画布 CSS 尺寸,
|
|
686
|
+
* 已在 _createDebugLine / _syncDebugLineResolution 里跟上。
|
|
687
|
+
*/
|
|
688
|
+
debugLineWidth = 2;
|
|
689
|
+
// 按键加速度累加临时量(先整块累加再一次进速度,见 controls())
|
|
690
|
+
_accelVec = new Vector3();
|
|
691
|
+
/**
|
|
692
|
+
* 本次调用在**模拟位置**上算出的水平弹出总量(= xsh while 收敛所需的推离长度)。
|
|
693
|
+
* ⭐ 2026-10-02 模拟修正后它不再等于「真实位置陷了多深」—— 真实位置不再先被写进墙里,
|
|
694
|
+
* 它是「这一步要是真走进去、需要弹多少才干净」的模拟读数。
|
|
695
|
+
*/
|
|
696
|
+
// 本迭代是否有水平射线命中(用于循环外统一回弹)
|
|
697
|
+
// ⭐ 2026-09-21 旨「射线命中 = 碰撞 = 立刻做位置修正」:命中即置位,不再要求真穿透。
|
|
698
|
+
// 安全由**推离量本身**保证:deficit=0(只是贴着)→ dist=0 → 一格都不推,
|
|
699
|
+
// 不会「没穿透也放行」而一帧推 1.15 穿墙。
|
|
700
|
+
_horizHit = false;
|
|
701
|
+
// 缓存本帧的按键输入单位方向(水平)。initPlayerInputDir() 每物理步刷新一次;
|
|
702
|
+
// 零向量表示「本帧没按方向键」→ 最终方向退化为纯反弹方向。
|
|
703
|
+
_inputDirVec = new Vector3();
|
|
704
|
+
/**
|
|
705
|
+
* 本帧**进碰撞之前**的水平速率(米/秒)。皇上八旨要求「移动向量的长度保持不变」,
|
|
706
|
+
* 而碰撞会把 `playerVelocity.x/z` 清零,所以必须在 updatePlayer 入口先记一份。
|
|
707
|
+
*/
|
|
708
|
+
_preCollideHorizSpeed = 0;
|
|
709
|
+
// ========== 回弹预览(皇上 2026-09-25 旨:「加个开关可以关闭碰撞修正位置的计算,方便观察回弹向量」)==========
|
|
710
|
+
// 打开后:**不写位置**(见 playerCollisions 末尾唯一的 translate 闸门),但方向/推离量照常算、
|
|
711
|
+
// 黄→白方向线照常画 → 肉眼核对「算出来的方向对不对」而不被人推开。
|
|
712
|
+
// ⚠️ 只闸水平+竖向的**位置写入**;削速度、playerOnFloor、事件派发全部保留,
|
|
713
|
+
// 否则预览模式下重力会一帧帧攒着把人送到地心并触发掉出世界复位。
|
|
714
|
+
// ⭐⭐ 2026-09-26 皇上七旨「回弹预览开了, 就关不掉」⇒ 本字段改成**带暂存的访问器**:
|
|
715
|
+
// 开 = 记下当前胶囊位置;关 = **还原**那个位置 + 清掉水平速度。
|
|
716
|
+
// 病根(实测,墙面板块 x∈[5.0,6.0]、R=1.06):预览既然不写位置,人就照着自己的
|
|
717
|
+
// 行走速度(speed=6 ⇒ 终端 37.5m/s ⇒ 每帧 0.62m)一路**穿墙走出去**,120 帧后中心
|
|
718
|
+
// 已经在 x=138;这一路上只要停在任何一处「两面墙的票互相抵消」的几何里,位置修正就
|
|
719
|
+
// 再也算不出方向 —— 现场实录:人停在墙板正中 x=5.5,连跑 120 帧每帧都命中、
|
|
720
|
+
// deficit 恒为 0.5600、位移 0。看着就是「预览关了位置修正也不回来了」。
|
|
721
|
+
// ⇒ 预览是**观察**用的,那一段时间里的位移本来就不作数,关掉即回到打开前那一刻。
|
|
722
|
+
_previewRetreat = false;
|
|
723
|
+
/** 开启预览那一刻的胶囊两端(关闭时整体还原回去) */
|
|
724
|
+
_obsSavedStart = new Vector3();
|
|
725
|
+
_obsSavedEnd = new Vector3();
|
|
726
|
+
/** 是否已有可还原的暂存(没进过观察模式就没有,避免构造期赋值把人还原到原点) */
|
|
727
|
+
_obsHasSave = false;
|
|
728
|
+
/** 进观察模式(回弹预览 / 单步):记下当前胶囊位置,退出时还原 */
|
|
729
|
+
_saveObsPos() {
|
|
730
|
+
this._obsSavedStart.copy(this.playerCollider.start);
|
|
731
|
+
this._obsSavedEnd.copy(this.playerCollider.end);
|
|
732
|
+
this._obsHasSave = true;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* 退观察模式:把位置整份还原到进入那一刻,并清掉水平速度。
|
|
736
|
+
* ⚠️ 只清 x/z(竖向已冻结):观察模式刻意不削速度 ⇒ 人攒着一身的行走终端速度
|
|
737
|
+
* (speed=6 ⇒ 37.5m/s ⇒ 每帧 0.62m),还原位置后若不清掉,下一帧又是一个大跨步再次穿墙。
|
|
738
|
+
*/
|
|
739
|
+
_restoreObsPos() {
|
|
740
|
+
if (!this._obsHasSave)
|
|
741
|
+
return;
|
|
742
|
+
this.playerCollider.start.copy(this._obsSavedStart);
|
|
743
|
+
this.playerCollider.end.copy(this._obsSavedEnd);
|
|
744
|
+
this.camera.position.copy(this.playerCollider.end);
|
|
745
|
+
this.playerVelocity.x = 0;
|
|
746
|
+
this.playerVelocity.z = 0;
|
|
747
|
+
this._obsHasSave = false;
|
|
748
|
+
}
|
|
749
|
+
get previewRetreat() {
|
|
750
|
+
return this._previewRetreat;
|
|
751
|
+
}
|
|
752
|
+
set previewRetreat(on) {
|
|
753
|
+
if (on === this._previewRetreat)
|
|
754
|
+
return;
|
|
755
|
+
if (on)
|
|
756
|
+
this._saveObsPos();
|
|
757
|
+
else
|
|
758
|
+
this._restoreObsPos();
|
|
759
|
+
this._previewRetreat = on;
|
|
760
|
+
}
|
|
761
|
+
// ========== 水平位置修正 = 迭代推离(皇上 2026-09-27 十五旨:除反弹方向外**完全照 xsh-react-three**)==========
|
|
762
|
+
// xsh 的骨架只有一个 while(`FPSControls.tsx` 的 `computerPosition`):
|
|
763
|
+
// ① 在**候选位置**(真实位置 + 已累计的 positionIncremental)上打水平射线
|
|
764
|
+
// ② 有命中 ⇒ 沿「离开命中点」的方向推**一小格**(它写死 0.03)并续跑下一轮
|
|
765
|
+
// ③ 一轮下来没命中 ⇒ goOnComputer=false ⇒ 收工;整圈上限 = MAX_ITERATIONS(皇上 10-03 旨:500)轮,
|
|
766
|
+
// 单趟 while 另有「本轮数上限」闸门(常态帧 = ROUNDS_PER_SUBSTEP,皇上 10-07 裁决加,默认 500 同档 ⇒ 不变)
|
|
767
|
+
// ④ 循环外**唯一一次** position.add(incremental)
|
|
768
|
+
// ⇒ 旧的「测试法逐级逼近」(六旨:1 → 0.1 → 0.01 三档、武装/判定/接受/撤销、每帧出水量 ≥
|
|
769
|
+
// 进水量那套配套保险丝)**整条删除**。位置仍是一格一格挪出去的(皇上要的一步一步看那个形态不变),
|
|
770
|
+
// 只是格长固定成一档 = 下面这个 RETREAT_SKIN。
|
|
771
|
+
/**
|
|
772
|
+
* 每轮迭代沿回弹方向推出去的**固定格长**(米)。
|
|
773
|
+
* ⭐ 皇上 2026-10-07 旨「这个修正距离一看就不正常,应该 1 个单位 1 个单位的修正」⇒ 默认从 xsh
|
|
774
|
+
* 源码 `setLength(0.03)` 那个写死的数**改成 0.01**(一次一格 = 1cm)。
|
|
775
|
+
* ⚠️ 别往下拧太多:体检台 34 现场实测 0.03 / 0.02 / 0.01 判级完全相同(❌0·⚠️20·✅14),
|
|
776
|
+
* 拧到 0.005 才有 6 个 ⚠️ 转 ✅,但同一趟「跳跃 + 冲刺 + 掉帧」现场判 **❌穿墙**
|
|
777
|
+
* —— 格长就是每帧的出水量,格子越小一帧 500 轮预算能挤出去的总量越小,高速顶墙时抵不过往里挤的位移。
|
|
778
|
+
*/
|
|
779
|
+
RETREAT_SKIN = 0.01;
|
|
780
|
+
/**
|
|
781
|
+
* dev 视图里是否**隐藏**纯 ±X 射线(6/7 侧向,默认 true = 不画)。
|
|
782
|
+
* ⚠️ 皇上 2026-09-03:这几条从玩家中心左右各伸出,连起来是一根**横贯视野中央**的线,
|
|
783
|
+
* 是全场最扎眼的一根(原话「中间的横着的那根线不要」)。
|
|
784
|
+
* ⚠️⚠️ **只是不画,碰撞射线照常发射**——左右方向的探测全靠 6/7(2026-09-06 起延长线已删),
|
|
785
|
+
* 真删掉侧向墙就要等玩家中心陷进去才被挡住(老 bug 立刻回来)。
|
|
786
|
+
* 想恢复显示(例如排查侧向碰撞):本字段置 false。
|
|
787
|
+
*/
|
|
788
|
+
debugHideSideRays = true;
|
|
789
|
+
_tempEuler = new Euler(0, 0, 0, "YXZ");
|
|
790
|
+
// 最大迭代次数(皇上 2026-10-03 旨:「那计算次数改成不超过 500」⇒ 100 → 500)
|
|
791
|
+
MAX_ITERATIONS = 500;
|
|
792
|
+
/**
|
|
793
|
+
* 一个**物理小帧**里 while 最多走几轮就落地(皇上 2026-10-07 裁决「加一个每小帧轮数上限」)。
|
|
794
|
+
* 默认 500 = 与 `MAX_ITERATIONS` 同档 ⇒ 现状一字不变;拧小才是他要的「一次只修几格、下一小帧再修」。
|
|
795
|
+
* ⭐ 为什么拧格长没用(10-07 实测):常态帧会一直迭代到「8 条射线全部脱开」才落地 ⇒
|
|
796
|
+
* **每次落地量 = 这一小帧往墙里挤进多深**,与一格多远无关(格长 0.03→0.01 判级一字未变)。
|
|
797
|
+
* 一格 = 一轮里每条命中各推的距离 ⇒ 要限制"一次修多少"只能限制**轮数**。
|
|
798
|
+
* ⚠️ 代价(不许瞒):出水量 = 轮数 × 每轮票数 × 格长,默认每帧 2 个小帧。拧到 1 轮 + 1cm 格长 ⇒
|
|
799
|
+
* 一秒最多挤出约 120cm 的量看着够,但高速冲刺顶墙时「这一帧挤进来的」可能比「两小帧推出去的」多
|
|
800
|
+
* ⇒ 越陷越深,格长拧到 0.005 那一档体检台 40 号现场就是这么判 **❌穿墙** 的。
|
|
801
|
+
* 所以默认不动,先让他在线上拧着看手感。
|
|
802
|
+
*/
|
|
803
|
+
ROUNDS_PER_SUBSTEP = 500;
|
|
804
|
+
// ⚠️ 每帧碰撞迭代总预算(皇上 2026-09-03:墙角偶尔卡住,要求碰撞计算次数不超过 100;
|
|
805
|
+
// 2026-10-03 同一位皇上把这条上限抬到 500 —— 机制一字未改,只是额度变大):
|
|
806
|
+
// while 循环原本的上限 100 是「单次 playerCollisions」的,而一个 update() 帧内会调
|
|
807
|
+
// STEPS_PER_FRAME(2) × 位移细分(≤32) ≈ 最多 64 次——墙角处若每次都跑满,理论上限
|
|
808
|
+
// 6400 迭代 × 8 射线 ≈ 5 万次射线求交/帧,这才是「墙角偶尔卡住」的真凶(循环个数多,
|
|
809
|
+
// 不是单个循环跑太多)。改为整帧共享预算:一帧内全部子步合计最多迭代 MAX_ITERATIONS(500)
|
|
810
|
+
// 次;每个子步仍保证至少 1 次迭代(推离不能整次跳过,否则穿墙)。
|
|
811
|
+
_frameIterBudget = 500;
|
|
73
812
|
// 临时四元数(复用避免 GC)
|
|
74
813
|
_tempQuat = new Quaternion();
|
|
75
814
|
// 射线检测器
|
|
76
815
|
_raycaster = new Raycaster();
|
|
816
|
+
// 脚下深探(_floorBelow,穿地板自救)专用检测器(**不可**复用 _raycaster:调用方正在同一实例上逐射线取命中)
|
|
817
|
+
_probeRaycaster = new Raycaster();
|
|
77
818
|
// 临时向量(复用避免 GC)
|
|
78
|
-
_tempVec = new Vector3();
|
|
79
|
-
_tempVec2 = new Vector3();
|
|
80
819
|
_tempVec3 = new Vector3();
|
|
820
|
+
// xsh 移植(2026-09-28):候选位置(= 控制器位置 + positionIncremental,射线原点锚)
|
|
821
|
+
// 与推离向量专用临时量(复用避免 GC)
|
|
822
|
+
_candPos = new Vector3();
|
|
823
|
+
_corrVec = new Vector3();
|
|
81
824
|
_tempVec4 = new Vector3();
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
825
|
+
// 位移细分用的临时向量(与 playerCollisions 内的 _tempVec4 分开,避免被覆盖)
|
|
826
|
+
_tempVec5 = new Vector3();
|
|
827
|
+
/**
|
|
828
|
+
* ⭐⭐ 2026-10-02 皇上旨「在修正位置的时候,我希望的是**模拟修正**,而不是**真实的修正**,
|
|
829
|
+
* 在模拟修正中把最终结果计算好了,再应用到真实的第一人称控制器上面」。
|
|
830
|
+
* 本小步(位移细分的一份)**尚未写进真实位置**的位移:playerCollisions 先在
|
|
831
|
+
* 「真实位置 + 本位移」的**模拟位置**上跑 xsh 的 while,算出干净落点后,
|
|
832
|
+
* 才由那唯一一次 translate 落到真实控制器上(见 playerCollisions 末尾)。
|
|
833
|
+
* ⇒ 真实位置不再「先走进墙里 0.04m,再被弹出来 0.06m」= 一个渲染帧两趟来回甩 = 超级抖。
|
|
834
|
+
* 消费掉(落地)后归零,所以面板那颗直接调 playerCollisions 的按钮不会重复走一步。
|
|
835
|
+
*/
|
|
836
|
+
_subStep = new Vector3();
|
|
837
|
+
/** 模拟算完之后本小步真正写进真实位置的那一份(本步位移 + 最终修正)= 唯一一次 translate */
|
|
838
|
+
_appliedMove = new Vector3();
|
|
839
|
+
_wallQuat = new Quaternion();
|
|
840
|
+
// 消速度专用:把面法线从实例局部空间变换到世界空间时,需要算上 instanceMatrix 的旋转/缩放(逆转置)
|
|
841
|
+
_wallMat3 = new Matrix3();
|
|
842
|
+
_wallMatrix4 = new Matrix4();
|
|
843
|
+
// 竖向射线判「命中面朝上还是朝下」用的世界法线(见 _faceNormalWorldY)
|
|
844
|
+
_vertNormal = new Vector3();
|
|
845
|
+
// ========== 射线布局(唯一数据源 = 下方 `_localRayOrigins` / `_localRayDirs`) ==========
|
|
846
|
+
// 射线定义在局部空间,只跟随 yaw 旋转、不跟随 pitch 俯仰(对应 Group3D(rotation={x:-euler.x}) 补偿)。
|
|
847
|
+
// ⚠️ 局部坐标的单位**就是世界米**(2026-09-26 起)。此前要先 × COLLIDE_VOLUME 换算成米,
|
|
848
|
+
// 那个参数连同面板格子一并删除;R / 6-9 高度 / far 的**物理数值一个没变**。
|
|
85
849
|
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
850
|
+
// 布局沿革(勿凭记忆重建,逐条清单见下方「★ 矩形射线法 ★」规格块与 CHORD_RHOMBUS_R 的终裁注释):
|
|
851
|
+
// · 源自 xsh-react-three 的 6 条(4 斜弦 + 上下竖向);斜弦起点外扩到 ±R → 四弦首尾闭合成**菱形**。
|
|
852
|
+
// · 曾扩到 14 条(±X 侧向 + 共线延长线)去补 reach,2026-09-06 皇上终裁精简为 8 条水平 + 2 条竖向。
|
|
853
|
+
// · 「均匀放射 N=12」方案皇上当场否决并令回退:「我最开始用射线组合的矩形,就是最好的方案」。
|
|
854
|
+
// · 防穿墙的判据只有一个:**沿法线 reach > MAX_STEP_DISTANCE**(现行最小 R/√2 = 0.749 > 0.1,富余 7 倍)。
|
|
855
|
+
// 停位不需要 reach 探得到 —— 那是命中之后由解析推离顶出来的。
|
|
856
|
+
// · 反例(只针对均匀放射,不是本布局的问题):起点外扩到**包络圆周**上,玩家陷墙时起点已越过墙面
|
|
857
|
+
// → 朝外射永远打不到墙 → 深穿透失明。本布局斜弦起点在 z=0 中心平面、反向弦落在菱形顶点,天生没这个坑。
|
|
93
858
|
//
|
|
94
|
-
//
|
|
859
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
860
|
+
// ★ 矩形射线法 / Rectangle-Ray Method(皇上 2026-09-06 命名 · 权威布局规格)
|
|
861
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
862
|
+
// ⚠️ 以后若碰撞布局被改坏,只说「矩形射线法」→ AI 按本块精确还原(不要凭记忆重建)。
|
|
863
|
+
// 本块是唯一数据源:改动射线只改下方 `_localRayOrigins` / `_localRayDirs`(务必等长),
|
|
864
|
+
// `this._rays`(FpsRay[])与每条专属调试线自动跟随,不可能错位。
|
|
865
|
+
//
|
|
866
|
+
// 一、冻结常量(逐字还原值;单位一律**世界米**)
|
|
867
|
+
// CHORD_RHOMBUS_R = 1.06 菱形半对角线 R(水平射线半径)
|
|
868
|
+
// 水平 far = R√2 ≈ 1.4991(getter HORIZONTAL_FAR,随 R 自动重算 → 菱形闭合不破)
|
|
869
|
+
// HORIZ_FWD_RAY_Y = −0.7 2-5 起点高度(相对控制器中心;皇上 09-26 五旨定案,离地 0.8m)
|
|
870
|
+
// HORIZ_REVERSE_RAY_Y = −0.75 6-9 起点高度(比 2-5 再低 5cm,离地 0.75m;「不用间隔太宽」)
|
|
871
|
+
// DOWN_RANGE = 1.5 / UP_RANGE = 0.3;竖向 far 各再 + VERTICAL_SKIN(0.02) 探测余量
|
|
872
|
+
// 辅助线绘制长度 竖向 = DOWN_RANGE(1.5)/UP_RANGE(0.3)、水平 = 本条 far
|
|
873
|
+
// (= 射线自身长度,皇上 2026-09-25 定案;原 DEBUG_RAY_VISUAL_LEN=1.5 已作废删除)
|
|
874
|
+
// GRAVITY = 10(皇上 2026-10-03 旨:固定 10、不做编辑项;原 30)
|
|
875
|
+
// jumpSpeed = 5(按「实测跳跃弧线不变」随重力倒推:5²/20 = 1.25m;历史 15 → 8 → 5)
|
|
876
|
+
// inertia = true inertiaDamp = 0.85
|
|
877
|
+
// ⚠️ 2026-09-26 已删除:`COLLIDE_VOLUME`、水平侧的 `COLLIDE_SKIN`、`HORIZONTAL_FAR_K`。
|
|
878
|
+
// 它们只服务于「局部单位 × cv 才是米」这套中间换算,删掉后几何数值一字未动。
|
|
879
|
+
//
|
|
880
|
+
// 二、10 条射线逐条表(localOrigin 单位=米,运行时只跟随 yaw + 加玩家位置)
|
|
881
|
+
// idx origin(米) dir(单位向量) far 角色
|
|
882
|
+
// 0 (0,0,0) (0,-1,0) downFar(=DOWN_RANGE+SKIN) 竖向·下(踩地)
|
|
883
|
+
// 1 (0,0,0) (0,+1,0) upFar(=UP_RANGE+SKIN) 竖向·上(顶头)
|
|
884
|
+
// 2 (R, −0.7, 0) (-1,0,-1)→归一 R√2 斜弦·右顶点→左前
|
|
885
|
+
// 3 (-R,−0.7, 0) (+1,0,-1)→归一 R√2 斜弦·左顶点→右前
|
|
886
|
+
// 4 (-R,−0.7, 0) (+1,0,+1)→归一 R√2 斜弦·左顶点→右后
|
|
887
|
+
// 5 (R, −0.7, 0) (-1,0,+1)→归一 R√2 斜弦·右顶点→左后
|
|
888
|
+
// 6 (0, −0.75, −R) (+1,0,+1)→归一 R√2 共线反向·2(=2 终点→起点)
|
|
889
|
+
// 7 (0, −0.75, −R) (-1,0,+1)→归一 R√2 共线反向·3(=3 终点→起点)
|
|
890
|
+
// 8 (0, −0.75, +R) (-1,0,-1)→归一 R√2 共线反向·4(=4 终点→起点)
|
|
891
|
+
// 9 (0, −0.75, +R) (+1,0,-1)→归一 R√2 共线反向·5(=5 终点→起点)
|
|
892
|
+
// ⭐ 6-9 = 2-5 的**共线反向**射线(皇上 2026-09-14 改:原「起点重合朝外分叉」被否,改为真·重合):
|
|
893
|
+
// 起点 = 2-5 各自的**终点**(菱形内侧顶点 (0,0,∓R)),方向与 2-5 严格取反,
|
|
894
|
+
// → 与 2-5 **共线、同一直线、但再低 5cm**(y = HORIZ_FWD_RAY_Y = −0.7 / HORIZ_REVERSE_RAY_Y = −0.75,
|
|
895
|
+
// 皇上 2026-09-26 五旨「两组 = −0.7 和 −0.75」⇒ 站定时离地 0.8m / 0.75m)。
|
|
896
|
+
// 历史上 y 曾取世界 0.1m(2026-09-14,肉眼区分)→ 09-26 归零重合 → −0.5 → 三旨 −0.7 → 四旨回 −0.5 → 五旨 −0.7/−0.75。
|
|
897
|
+
// 用于「同一直线双向探测」玩家两侧墙。R = CHORD_RHOMBUS_R = 1.06。
|
|
898
|
+
//
|
|
899
|
+
// 三、局部→世界变换(FpsRay.updateWorld,每帧)
|
|
900
|
+
// worldOrigin = localOrigin · yawQuat + playerWorldPos
|
|
901
|
+
// worldDirection = localDir · yawQuat(归一化)
|
|
902
|
+
//
|
|
903
|
+
// 四、关键不变量(还原后必须仍满足,否则布局被破坏)
|
|
904
|
+
// · 菱形闭合:四斜弦末端落在 (0,0,±R),外接框 2.12×2.12;far 恒 = R√2
|
|
905
|
+
// · 沿法线 reach = R·max(|n·右|, |n·前|) ∈ [R/√2, R] = 0.749~1.06 > MAX_STEP_DISTANCE(0.1) → 不穿墙
|
|
906
|
+
// · 回弹方向口径:normalize(Σ_i normalize(控制器中心−碰撞点_i))(水平、等权)
|
|
907
|
+
// · 6-9 反向射线 isSideRay = false(可见,不与已删除的 6/7 侧向历史混淆)
|
|
908
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
909
|
+
// ⭐ 射线定义即唯一数据源:下方 `this._rays`(FpsRay[])由本数组 map 生成,
|
|
910
|
+
// 每条 FpsRay 自带 world 空间 origin/direction(每帧 updateWorld)与专属 debugLine,
|
|
911
|
+
// 增减射线只需改本数组(与 _localRayDirs 务必等长),`this._rays` 自动跟随、调试线一一对应。
|
|
95
912
|
_localRayOrigins = [
|
|
96
913
|
new Vector3(0, 0, 0), // 0: upTodwomRay(下射线)
|
|
97
914
|
new Vector3(0, 0, 0), // 1: dwomToUpRay(上射线)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
915
|
+
// ⭐ 2026-09-06 皇上旨「横向你也改成 2.12m」「不是正方形,是 X 形状」:
|
|
916
|
+
// 斜弦起点 = **±CHORD_RHOMBUS_R(1.06) 米**(局部就是米,不再 ×cv 换算)。
|
|
917
|
+
// 四条弦首尾相接,围成外接框 **2.12×2.12 的菱形**。
|
|
918
|
+
// ⚠️ 臣曾把起点挪回玩家中心 (0,0,0) —— 四弦变成从**同一点**发散的 **X**,被皇上否决。
|
|
919
|
+
// **菱形的半对角线 R 就是起点外扩量**,要够大就必须外扩,不能回中心。
|
|
920
|
+
// ✅ 实测(tmp/fps_rhombus_shape_ab.mjs):八方位停位一致、speed 20 不穿墙、薄墙 0.1m 不穿、
|
|
921
|
+
// 夹缝正常;**回弹方向平均偏离 4.20°**(比中心的 7.88° 更准)。
|
|
922
|
+
new Vector3(this.CHORD_RHOMBUS_R, this.HORIZ_FWD_RAY_Y, 0), // 2: 斜弦起点(菱形右顶点)
|
|
923
|
+
new Vector3(-this.CHORD_RHOMBUS_R, this.HORIZ_FWD_RAY_Y, 0), // 3: 斜弦起点(菱形左顶点)
|
|
924
|
+
new Vector3(-this.CHORD_RHOMBUS_R, this.HORIZ_FWD_RAY_Y, 0), // 4: 斜弦起点(菱形左顶点)
|
|
925
|
+
new Vector3(this.CHORD_RHOMBUS_R, this.HORIZ_FWD_RAY_Y, 0), // 5: 斜弦起点(菱形右顶点)
|
|
926
|
+
// ⭐ 矩形射线法·反向探测(皇上 2026-09-06 加):6-9 = 2-5 的反方向射线,
|
|
927
|
+
// 方向取反 → 探测玩家身后的墙。自动参与碰撞检测(主循环 ri=2..length)且可见(isSideRay=false)。
|
|
928
|
+
// ⭐ 共线反向(皇上 2026-09-14 改):6-9 起点 = 2-5 终点(菱形内侧顶点),方向取反 → 与 2-5 共线。
|
|
929
|
+
// 2026-09-26 皇上旨「平行的射线往下移动一点」→ 先归零与 2-5 同高;同日再旨「2 组射线不要重合在一起了,
|
|
930
|
+
// 要整体下移」⇒ −0.5;三旨「改成 −0.7」;四旨「按面板日志定初始值」⇒ 回 −0.5;
|
|
931
|
+
// 五旨(定案)「两组 = −0.7 和 −0.75」⇒ y = HORIZ_FWD_RAY_Y −0.7 / HORIZ_REVERSE_RAY_Y −0.75(四条一起降)。
|
|
932
|
+
new Vector3(0, this.HORIZ_REVERSE_RAY_Y, -this.CHORD_RHOMBUS_R), // 6: 共线反向·2(终点(0,0,-R)→右顶点)
|
|
933
|
+
new Vector3(0, this.HORIZ_REVERSE_RAY_Y, -this.CHORD_RHOMBUS_R), // 7: 共线反向·3(终点(0,0,-R)→左顶点)
|
|
934
|
+
new Vector3(0, this.HORIZ_REVERSE_RAY_Y, this.CHORD_RHOMBUS_R), // 8: 共线反向·4(终点(0,0,+R)→左顶点)
|
|
935
|
+
new Vector3(0, this.HORIZ_REVERSE_RAY_Y, this.CHORD_RHOMBUS_R), // 9: 共线反向·5(终点(0,0,+R)→右顶点)
|
|
102
936
|
];
|
|
103
|
-
//
|
|
104
|
-
// 归一化后与 xsh 的 clampLength(0,1) 效果一致
|
|
937
|
+
// 局部空间射线方向(单位向量)
|
|
105
938
|
_localRayDirs = [
|
|
106
939
|
new Vector3(0, -1, 0), // 0: 下射线方向
|
|
107
940
|
new Vector3(0, 1, 0), // 1: 上射线方向
|
|
108
|
-
new Vector3(-1, 0, -1).normalize(), // 2:
|
|
109
|
-
new Vector3(1, 0, -1).normalize(), // 3:
|
|
110
|
-
new Vector3(1, 0, 1).normalize(), // 4:
|
|
111
|
-
new Vector3(-1, 0, 1).normalize(), // 5:
|
|
941
|
+
new Vector3(-1, 0, -1).normalize(), // 2: 右顶点→左前
|
|
942
|
+
new Vector3(1, 0, -1).normalize(), // 3: 左顶点→右前
|
|
943
|
+
new Vector3(1, 0, 1).normalize(), // 4: 左顶点→右后
|
|
944
|
+
new Vector3(-1, 0, 1).normalize(), // 5: 右顶点→左后
|
|
945
|
+
// ⭐ 矩形射线法·共线反向(皇上 2026-09-14 改):6-9 = 2-5 严格取反,起点改为 2-5 终点(菱形内侧顶点),与 2-5 共线同一直线(见 _localRayOrigins)
|
|
946
|
+
new Vector3(1, 0, 1).normalize(), // 6: 反向·2(右顶点→右后)
|
|
947
|
+
new Vector3(-1, 0, 1).normalize(), // 7: 反向·3(左顶点→左后)
|
|
948
|
+
new Vector3(-1, 0, -1).normalize(), // 8: 反向·4(左顶点→左前)
|
|
949
|
+
new Vector3(1, 0, -1).normalize(), // 9: 反向·5(右顶点→右前)
|
|
112
950
|
];
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
|
|
951
|
+
// ⭐ 每条射线一个 FpsRay 封装:局部定义 + 每帧 world 空间 + 本帧 far + 专属辅助线。
|
|
952
|
+
// world 空间 origin/direction 由 FpsRay.updateWorld 每帧重算(不再单独维护 _worldRayOrigins/_worldRayDirs),
|
|
953
|
+
// 辅助线由 FpsRay.debugLine 一对一持有 → 杜绝「调试线索引与射线索引错位 / 漂移」。
|
|
954
|
+
_rays = this._localRayOrigins.map((origin, i) => new FpsRay(origin, this._localRayDirs[i]));
|
|
117
955
|
// userData.fpsCollision === true 的碰撞体列表
|
|
118
956
|
_colliders = [];
|
|
119
957
|
// 缓存标记:场景变化时才重建碰撞体列表
|
|
120
958
|
_collidersDirty = true;
|
|
121
959
|
_colliderSceneVersion = 0;
|
|
960
|
+
/**
|
|
961
|
+
* 把命中面的法线变换到**世界空间**(含 InstancedMesh 的实例旋转/缩放;
|
|
962
|
+
* 非均匀缩放下必须用 3×3 逆转置,不能直接 applyMatrix3)。
|
|
963
|
+
* ⚠️ 原样返回:不抹平 y、不归一化、不翻向 —— 都由调用方按需处理。
|
|
964
|
+
* ⚠️ 只用 _wallQuat/_wallMatrix4/_wallMat3 这三个专用临时量,可安全嵌套调用其它射线流程。
|
|
965
|
+
* @returns false = 无 face 或法线是零向量(调用方须自行兜底)
|
|
966
|
+
*/
|
|
967
|
+
_transformFaceNormalWorld(interset, outN) {
|
|
968
|
+
if (!interset.face)
|
|
969
|
+
return false;
|
|
970
|
+
outN.copy(interset.face.normal);
|
|
971
|
+
// 1) object 自身世界旋转(InstancedMesh 整体;对普通 Mesh 就是它自己的旋转)
|
|
972
|
+
interset.object.getWorldQuaternion(this._wallQuat);
|
|
973
|
+
outN.applyQuaternion(this._wallQuat);
|
|
974
|
+
// 2) 若是 InstancedMesh,再叠加上该实例的旋转/缩放
|
|
975
|
+
const obj = interset.object;
|
|
976
|
+
if (obj.isInstancedMesh && interset.instanceId !== undefined) {
|
|
977
|
+
const mesh = interset.object;
|
|
978
|
+
mesh.getMatrixAt(interset.instanceId, this._wallMatrix4);
|
|
979
|
+
const e = this._wallMatrix4.elements;
|
|
980
|
+
// instanceMatrix 是列主序 4×4,左上 3×3 转成行主序 Matrix3
|
|
981
|
+
this._wallMat3.set(e[0], e[4], e[8], e[1], e[5], e[9], e[2], e[6], e[10]);
|
|
982
|
+
this._wallMat3.invert().transpose(); // 逆转置 = 正确法线变换
|
|
983
|
+
outN.applyMatrix3(this._wallMat3);
|
|
984
|
+
}
|
|
985
|
+
return outN.lengthSq() > 1e-12;
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* ⭐ 2026-09-27 射线为主方案恢复(与 HEAD 旧射线口径一字不差,内部复用 _transformFaceNormalWorld):
|
|
989
|
+
* 解析一个射线命中点的「墙法线」与「沿法线的垂距」。
|
|
990
|
+
* @param worldPos 玩家中心(本子步候选位置)
|
|
991
|
+
* @param interset 射线命中点
|
|
992
|
+
* @param pushDir 输出「命中点→玩家」的水平推离向量(未归一化;退化时被换成 fallbackDir)
|
|
993
|
+
* @param outN 输出「指向玩家一侧」的单位水平墙面法线(退化时同样有效)
|
|
994
|
+
* @param fallbackDir 射线反方向(已水平归一化),退化(命中点≈玩家中心)时兜底推离方向
|
|
995
|
+
* @returns 垂距 perpDist(≥0)。退化时返回 0(视为最深穿透),并置 _perpDegenerate
|
|
996
|
+
*/
|
|
997
|
+
/**
|
|
998
|
+
* 竖向(上/下)射线专用:取命中面**世界法线的 y 分量**(已归一化)。
|
|
999
|
+
* > 0 = 朝上的面(可行走顶面,如 Ground1 地板、箱体顶面)
|
|
1000
|
+
* < 0 = 朝下的面(天花板 / 箱体底面)
|
|
1001
|
+
* @returns null = 拿不到法线(无 face 或零向量)→ 调用方按「维持旧行为」处理
|
|
1002
|
+
*/
|
|
1003
|
+
_faceNormalWorldY(interset) {
|
|
1004
|
+
const n = this._vertNormal;
|
|
1005
|
+
if (!this._transformFaceNormalWorld(interset, n))
|
|
1006
|
+
return null;
|
|
1007
|
+
return n.normalize().y;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* 脚下深探:往下问 VOID_PROBE_DOWN 这么长一段,有没有地面(2026-09-25,穿地板自救用)。
|
|
1011
|
+
* @returns true = 下方有地面 → 只是正常腾空/下落,不是穿模
|
|
1012
|
+
*/
|
|
1013
|
+
_floorBelow(worldPos) {
|
|
1014
|
+
this._probeRaycaster.ray.origin.copy(worldPos);
|
|
1015
|
+
this._probeRaycaster.ray.direction.set(0, -1, 0);
|
|
1016
|
+
this._probeRaycaster.far = this.VOID_PROBE_DOWN;
|
|
1017
|
+
return this._probeRaycaster.intersectObjects(this._colliders, false).length > 0;
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* ⭐⭐ 刷新本帧的**按键输入方向**(水平单位向量)—— 皇上 2026-09-21 旨:
|
|
1021
|
+
* 「移动方向」取**按键输入方向**(WASD 相对相机),用于与回弹方向相加。
|
|
1022
|
+
* 算法与 `controls()` 里的加速度合成**同源**(同用 getForwardVector / getSideVector),
|
|
1023
|
+
* 保证「输入方向」与「实际加速方向」严格一致(不会因两处各写一遍而漂移)。
|
|
1024
|
+
* ⚠️ 与 `controls()` 的区别:这里**只要方向**,不乘 speedDelta、不进速度 ——
|
|
1025
|
+
* 取的是「玩家意图往哪走」这个纯方向。
|
|
1026
|
+
* 无按键 / 前后或左右互相抵消 → 零向量(调用方据此退化为纯回弹方向)。
|
|
1027
|
+
* 每物理步调用一次(在 `playerCollisions` 入口)。
|
|
1028
|
+
*/
|
|
1029
|
+
_refreshInputDir() {
|
|
1030
|
+
this._inputDirVec.set(0, 0, 0);
|
|
1031
|
+
if (this.keyStates["KeyW"])
|
|
1032
|
+
this._inputDirVec.add(this.getForwardVector());
|
|
1033
|
+
if (this.keyStates["KeyS"])
|
|
1034
|
+
this._inputDirVec.sub(this.getForwardVector());
|
|
1035
|
+
if (this.keyStates["KeyA"])
|
|
1036
|
+
this._inputDirVec.sub(this.getSideVector());
|
|
1037
|
+
if (this.keyStates["KeyD"])
|
|
1038
|
+
this._inputDirVec.add(this.getSideVector());
|
|
1039
|
+
// 只取水平分量:回弹方向恒在水平面内,混入 y 会让两者相加后偏离水平面
|
|
1040
|
+
this._inputDirVec.y = 0;
|
|
1041
|
+
if (this._inputDirVec.lengthSq() > 1e-8)
|
|
1042
|
+
this._inputDirVec.normalize();
|
|
1043
|
+
else
|
|
1044
|
+
this._inputDirVec.set(0, 0, 0);
|
|
1045
|
+
}
|
|
122
1046
|
// 收集 fpsCollision 物体 + 首次构建 BVH
|
|
123
1047
|
_getColliders() {
|
|
124
1048
|
if (!this._collidersDirty)
|
|
125
1049
|
return;
|
|
126
1050
|
this._colliders.length = 0;
|
|
127
|
-
this.scene.traverseVisible((
|
|
128
|
-
if (
|
|
129
|
-
const geo =
|
|
1051
|
+
this.scene.traverseVisible((child) => {
|
|
1052
|
+
if (child instanceof Mesh && child.userData?.fpsCollision) {
|
|
1053
|
+
const geo = child.geometry;
|
|
130
1054
|
if (geo && !geo.boundsTree && geo.attributes.position?.count > 0) {
|
|
131
1055
|
computeBoundsTree.apply(geo);
|
|
132
1056
|
}
|
|
133
|
-
|
|
1057
|
+
// ⚠️ InstancedMesh 的包围球必须强制重算:three 只在 boundingSphere === null 时才计算,
|
|
1058
|
+
// 而它首次被计算时 count 往往还是 0(模板异步加载),算出的是「空球」radius = -1,
|
|
1059
|
+
// 之后实例数量/矩阵变化都不会刷新 → raycast 的包围球剔除永远失败 → 碰撞与点击全部失效。
|
|
1060
|
+
if (child instanceof InstancedMesh) {
|
|
1061
|
+
child.boundingSphere = null;
|
|
1062
|
+
}
|
|
1063
|
+
this._colliders.push(child);
|
|
134
1064
|
}
|
|
135
1065
|
});
|
|
136
1066
|
this._collidersDirty = false;
|
|
@@ -139,6 +1069,22 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
139
1069
|
markCollidersDirty() {
|
|
140
1070
|
this._collidersDirty = true;
|
|
141
1071
|
}
|
|
1072
|
+
/** 通知所有已创建的 FPScontrol 重新收集碰撞体(InstancedBatch 异步创建碰撞 InstancedMesh 后调用) */
|
|
1073
|
+
static markAllCollidersDirty() {
|
|
1074
|
+
for (const c of fpsControlRegistry) {
|
|
1075
|
+
c.markCollidersDirty();
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
/** 复位玩家到指定世界坐标(默认原点),清空速度;第一人称 demo 的"回到起点"用得到 */
|
|
1079
|
+
resetPlayer(position = new Vector3(0, 0, 0)) {
|
|
1080
|
+
this.playerVelocity.set(0, 0, 0);
|
|
1081
|
+
this.prePlayerVelocity.set(0, 0, 0);
|
|
1082
|
+
this.playerCollider.start.set(position.x, position.y + 0.35, position.z);
|
|
1083
|
+
this.playerCollider.end.set(position.x, position.y + 1, position.z);
|
|
1084
|
+
this.playerCollider.radius = 0.35;
|
|
1085
|
+
this.camera.position.copy(this.playerCollider.end);
|
|
1086
|
+
// ⭐ 复位 = 换现场 ⇒ 把相机/胶囊位置基准挪到新位置。
|
|
1087
|
+
}
|
|
142
1088
|
// 玩家胶囊碰撞体
|
|
143
1089
|
playerCollider = new Capsule(new Vector3(0, 0.35, 0), new Vector3(0, 1, 0), 0.35);
|
|
144
1090
|
// 时钟对象
|
|
@@ -154,65 +1100,103 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
154
1100
|
isMousedown = false;
|
|
155
1101
|
keyStates = {};
|
|
156
1102
|
// ========== 射线可视化 ==========
|
|
1103
|
+
// ⭐ 每条射线(this._rays[i])自带一条专属辅助线 ray.debugLine;
|
|
157
1104
|
_debugRayGroup = null;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
//
|
|
1105
|
+
/** 全部调试线的材质(含 10 条射线线),只为统一同步 `resolution`(粗细按像素算,必须知道画布尺寸) */
|
|
1106
|
+
_debugLineMats = [];
|
|
1107
|
+
// 懒初始化调试射线组(每条 FpsRay 一条专属线)
|
|
161
1108
|
_initDebugLines() {
|
|
162
1109
|
if (this._debugRayGroup)
|
|
163
1110
|
return;
|
|
164
1111
|
this._debugRayGroup = new Group();
|
|
165
1112
|
this._debugRayGroup.name = "FPSControl_DebugRays";
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
});
|
|
174
|
-
const line = new Line(geo, mat);
|
|
175
|
-
// 让调试线段不被射线检测命中
|
|
176
|
-
line.raycast = () => { };
|
|
177
|
-
line.frustumCulled = false;
|
|
178
|
-
this._debugLines.push(line);
|
|
1113
|
+
// ① 每条射线一条专属辅助线(与 this._rays 一一对应,杜绝索引错位)
|
|
1114
|
+
// 射线线 = 开深度测试(正常遮挡,皇上 2026-09-25)
|
|
1115
|
+
for (const ray of this._rays) {
|
|
1116
|
+
const line = this._createDebugLine(true);
|
|
1117
|
+
ray.debugLine = line;
|
|
1118
|
+
// 材质真值抄到射线上(线对象上没有这个概念,断言/核对只看材质要 cast)
|
|
1119
|
+
ray.depthTestEnabled = line.material.depthTest === true;
|
|
179
1120
|
this._debugRayGroup.add(line);
|
|
180
1121
|
}
|
|
181
1122
|
this.scene.add(this._debugRayGroup);
|
|
182
1123
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
1124
|
+
/**
|
|
1125
|
+
* 创建一条标准调试线(几何/材质统一,debugLine 复用);不被射线检测命中。
|
|
1126
|
+
* @param depthTest 是否参与正常深度遮挡 —— **两种口径按皇上 2026-09-25 逐条定案**:
|
|
1127
|
+
* - 射线辅助线(10 条)= **true**:「射线辅助线 / 下射线的辅助线应该是正常的遮挡状态」。
|
|
1128
|
+
* 旧值 false + renderOrder:9999 是当年为治 2026-09-03「贴地辅助线被地面挡掉看不见」而定,
|
|
1129
|
+
* 代价是穿透一切浮在最前 —— 下射线画满 1.5 后埋进地里那半截被直接画在地板之上,
|
|
1130
|
+
* 就是皇上所见「像没开深度测试」。能挡住它们的前提:地面 Ground1 已是不透明且
|
|
1131
|
+
* `depthWrite:true`(three-base/Ground1/Ground1.ts:107);哪天地板改回
|
|
1132
|
+
* transparent + depthWrite:false,射线线会重新穿地浮出来。
|
|
1133
|
+
* - 回弹方向线(黄→白)= **false**:「回弹向量线保持穿透显示」。
|
|
1134
|
+
* 它是「本帧要看的向量」,起点在胶囊内部、方向常指着墙里,开遮挡会被自己的身子
|
|
1135
|
+
* 或墙面吃掉半截 → 观察目的直接落空,故保持常显在最前。
|
|
1136
|
+
*/
|
|
1137
|
+
_createDebugLine(depthTest) {
|
|
1138
|
+
// 一条线 = 一段(2 端点)。LineSegmentsGeometry 用实例属性存段,先塞 6 个 0 占位,
|
|
1139
|
+
// 之后每帧只改数组(见 writeDebugSegment),不重复建缓冲。
|
|
1140
|
+
const geo = new LineSegmentsGeometry();
|
|
1141
|
+
geo.setPositions(new Float32Array(6));
|
|
1142
|
+
geo.setColors(new Float32Array(6));
|
|
1143
|
+
const mat = new LineMaterial({
|
|
1144
|
+
vertexColors: true,
|
|
1145
|
+
// ⭐ 2026-09-25 皇上旨「辅助线调粗一点」+ 定案 2px:`linewidth` 在 LineMaterial 里
|
|
1146
|
+
// 是**真的像素宽**(shader 铺 quad),不像 LineBasicMaterial 被驱动钳成 1px。
|
|
1147
|
+
linewidth: this.debugLineWidth,
|
|
1148
|
+
depthTest,
|
|
1149
|
+
// 关深度写入:调试线只可见、不参与后续深度/后处理重建,避免被当背景
|
|
1150
|
+
depthWrite: false,
|
|
1151
|
+
// 进透明队列:renderOrder 9999 让它排在场景几何之后画(亮色也不被 alpha 混合削淡)
|
|
1152
|
+
transparent: true,
|
|
1153
|
+
// 后处理链下不走色调映射,避免亮色被压暗
|
|
1154
|
+
toneMapped: false,
|
|
1155
|
+
});
|
|
1156
|
+
// resolution 必须等于画布 CSS 尺寸,否则像素宽会按 1×1 折算 = 粗到糊满屏幕。
|
|
1157
|
+
// 尺寸变化由 _syncDebugLineResolution() 每帧跟上。
|
|
1158
|
+
mat.resolution.set(this._canvasCssWidth(), this._canvasCssHeight());
|
|
1159
|
+
this._debugLineMats.push(mat);
|
|
1160
|
+
const line = new LineSegments2(geo, mat);
|
|
1161
|
+
// 让调试线段不被射线检测命中
|
|
1162
|
+
line.raycast = () => { };
|
|
1163
|
+
line.frustumCulled = false;
|
|
1164
|
+
// 同队列内的绘制先后。⚠️ 它不再等于「盖住一切」——是否被挡住由上面的 depthTest 说话。
|
|
1165
|
+
line.renderOrder = 9999;
|
|
1166
|
+
return line;
|
|
1167
|
+
}
|
|
1168
|
+
/** 画布 CSS 宽(LineMaterial 的像素宽按它折算;DOM 缺席时兜 1,绝不喂 NaN/0) */
|
|
1169
|
+
_canvasCssWidth() {
|
|
1170
|
+
return this.canvas?.clientWidth || 1;
|
|
1171
|
+
}
|
|
1172
|
+
/** 画布 CSS 高,口径同 _canvasCssWidth */
|
|
1173
|
+
_canvasCssHeight() {
|
|
1174
|
+
return this.canvas?.clientHeight || 1;
|
|
1175
|
+
}
|
|
1176
|
+
/** 把画布当前尺寸同步进所有调试线材质(resize / 改粗细都要走一次) */
|
|
1177
|
+
_syncDebugLineResolution() {
|
|
1178
|
+
const w = this._canvasCssWidth();
|
|
1179
|
+
const h = this._canvasCssHeight();
|
|
1180
|
+
for (const mat of this._debugLineMats) {
|
|
1181
|
+
if (mat.resolution.x !== w || mat.resolution.y !== h)
|
|
1182
|
+
mat.resolution.set(w, h);
|
|
1183
|
+
if (mat.linewidth !== this.debugLineWidth)
|
|
1184
|
+
mat.linewidth = this.debugLineWidth;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
// 销毁调试射线(遍历每条射线自己的 debugLine + 2 条回弹线)
|
|
208
1188
|
_disposeDebugLines() {
|
|
209
1189
|
if (!this._debugRayGroup)
|
|
210
1190
|
return;
|
|
211
|
-
for (const
|
|
212
|
-
line.
|
|
213
|
-
line
|
|
1191
|
+
for (const ray of this._rays) {
|
|
1192
|
+
const line = ray.debugLine;
|
|
1193
|
+
if (line) {
|
|
1194
|
+
line.geometry.dispose();
|
|
1195
|
+
line.material.dispose();
|
|
1196
|
+
ray.debugLine = null;
|
|
1197
|
+
}
|
|
214
1198
|
}
|
|
215
|
-
this.
|
|
1199
|
+
this._debugLineMats.length = 0;
|
|
216
1200
|
this.scene.remove(this._debugRayGroup);
|
|
217
1201
|
this._debugRayGroup = null;
|
|
218
1202
|
}
|
|
@@ -223,9 +1207,20 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
223
1207
|
canvas.addEventListener("pointerdown", this.mousedown);
|
|
224
1208
|
document.addEventListener("pointerup", this.mouseup);
|
|
225
1209
|
document.body.addEventListener("pointermove", this.mousemove);
|
|
226
|
-
|
|
1210
|
+
// ⚠️ 不要在这里把 enable 置 true:enable 的语义是「由调用方显式开启」。
|
|
1211
|
+
// 字段默认 false(见上方声明),调用方需自行 fps.enable = true 才驱动物理/相机。
|
|
1212
|
+
// 否则默认即开启会让「不希望 FPS 接管相机」的场景(如主页用 OrbitControls)被迫再写一行 false 抵消。
|
|
227
1213
|
}
|
|
228
1214
|
keydown = (event) => {
|
|
1215
|
+
// ⭐ 2026-09-26 皇上旨「参数做成滚轮输入的输入框」连带补的一条:
|
|
1216
|
+
// 焦点在输入框里时按键不该再驱动人物(空格起跳、W/S 前后窜),
|
|
1217
|
+
// 否则「一改参数人就乱动」。keyup 不拦:只放行不清会让按键卡在 true。
|
|
1218
|
+
const t = event.target;
|
|
1219
|
+
if (t &&
|
|
1220
|
+
(t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT" ||
|
|
1221
|
+
t.isContentEditable)) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
229
1224
|
this.keyStates[event.code] = true;
|
|
230
1225
|
};
|
|
231
1226
|
keyup = (event) => {
|
|
@@ -243,7 +1238,8 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
243
1238
|
if (this.thirdPerson) {
|
|
244
1239
|
// 第三人称:旋转轨道角
|
|
245
1240
|
this._thirdPersonAngleY -= event.movementX / 500;
|
|
246
|
-
|
|
1241
|
+
// 俯仰与第一人称同向:鼠标下移 → 视角下压 → 相机升高俯视(故用 +=)
|
|
1242
|
+
this._thirdPersonAngleX += event.movementY / 500;
|
|
247
1243
|
this._thirdPersonAngleX = clamp(this._thirdPersonAngleX, degToRad(-89), degToRad(89));
|
|
248
1244
|
}
|
|
249
1245
|
else {
|
|
@@ -254,10 +1250,13 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
254
1250
|
}
|
|
255
1251
|
};
|
|
256
1252
|
// 获取前进的向量
|
|
1253
|
+
// ⚠️ 第三人称必须取「远离相机的方向」,与相机轨道偏移(见 updatePlayer)严格互为反向:
|
|
1254
|
+
// 相机偏移 = +(sin(a), ·, cos(a))·dist,故前进 = -(sin(a), 0, cos(a)) = (-sin a, 0, -cos a)。
|
|
1255
|
+
// 写成 +(sin, cos) 会让 W 键把玩家推向镜头(视觉上"前进像后退"),即"方向反了"的 bug。
|
|
257
1256
|
getForwardVector = () => {
|
|
258
1257
|
if (this.thirdPerson) {
|
|
259
|
-
// 第三人称:前进 =
|
|
260
|
-
this.playerDirection.set(Math.sin(this._thirdPersonAngleY), 0, Math.cos(this._thirdPersonAngleY));
|
|
1258
|
+
// 第三人称:前进 = 轨道偏航方向(相机在身后,故取轨道偏移的反方向)
|
|
1259
|
+
this.playerDirection.set(-Math.sin(this._thirdPersonAngleY), 0, -Math.cos(this._thirdPersonAngleY));
|
|
261
1260
|
return this.playerDirection;
|
|
262
1261
|
}
|
|
263
1262
|
this.camera.getWorldDirection(this.playerDirection);
|
|
@@ -281,31 +1280,39 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
281
1280
|
// 玩家移动控制器
|
|
282
1281
|
controls = (deltaTime) => {
|
|
283
1282
|
const speedDelta = deltaTime * (this.playerOnFloor ? 25 : 8) * this.speed;
|
|
1283
|
+
// ⭐ 2026-09-26 皇上旨「碰撞就是按射线拾取算的,不用搞太复杂」:
|
|
1284
|
+
// 旧的「接触法线缓存 + 加速度保速投影」整块删除 —— 撞墙的手感由位置侧负责
|
|
1285
|
+
// (命中即 playerVelocity.x/z = 0,再走迭代推格修正位置,皇上 2026-09-25 定案)。
|
|
1286
|
+
// ⭐ 2026-09-26 皇上八旨给这条加了个尾巴:位置修正判完之后,若本帧算出了方向且没和
|
|
1287
|
+
// 输入方向对消,水平速度会整条换成 normalize(输入 + 回弹) × 碰撞前速率(贴墙不再走不动),
|
|
1288
|
+
// 见 playerCollisions 末尾那段。清零本身照旧。
|
|
1289
|
+
const accel = this._accelVec.set(0, 0, 0);
|
|
284
1290
|
if (this.keyStates["KeyW"]) {
|
|
285
|
-
|
|
1291
|
+
accel.addScaledVector(this.getForwardVector(), speedDelta);
|
|
286
1292
|
}
|
|
287
1293
|
if (this.keyStates["KeyS"]) {
|
|
288
|
-
|
|
1294
|
+
accel.addScaledVector(this.getForwardVector(), -speedDelta);
|
|
289
1295
|
}
|
|
290
1296
|
if (this.keyStates["KeyA"]) {
|
|
291
|
-
|
|
1297
|
+
accel.addScaledVector(this.getSideVector(), -speedDelta);
|
|
292
1298
|
}
|
|
293
1299
|
if (this.keyStates["KeyD"]) {
|
|
294
|
-
|
|
1300
|
+
accel.addScaledVector(this.getSideVector(), speedDelta);
|
|
295
1301
|
}
|
|
1302
|
+
this.playerVelocity.add(accel);
|
|
296
1303
|
if (this.playerOnFloor &&
|
|
297
1304
|
!this.keyStates["KeyW"] &&
|
|
298
1305
|
!this.keyStates["KeyS"] &&
|
|
299
1306
|
!this.keyStates["KeyA"] &&
|
|
300
1307
|
!this.keyStates["KeyD"]) {
|
|
301
1308
|
if (!this.inertia) {
|
|
302
|
-
this.playerVelocity.x *=
|
|
303
|
-
this.playerVelocity.z *=
|
|
1309
|
+
this.playerVelocity.x *= this.inertiaDamp;
|
|
1310
|
+
this.playerVelocity.z *= this.inertiaDamp;
|
|
304
1311
|
}
|
|
305
1312
|
}
|
|
306
|
-
if (this.playerOnFloor) {
|
|
1313
|
+
if (this.playerOnFloor && this.jumpEnabled) {
|
|
307
1314
|
if (this.keyStates["Space"]) {
|
|
308
|
-
this.playerVelocity.y =
|
|
1315
|
+
this.playerVelocity.y = this.jumpSpeed;
|
|
309
1316
|
this.jumping = true;
|
|
310
1317
|
this.dispatchEvent({ type: "jump_start" });
|
|
311
1318
|
}
|
|
@@ -319,9 +1326,25 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
319
1326
|
damping *= 0.1;
|
|
320
1327
|
}
|
|
321
1328
|
this.playerVelocity.addScaledVector(this.playerVelocity, damping);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
this.
|
|
1329
|
+
// ⭐ 2026-09-26 皇上八旨「位置修正结束后…移动向量的长度保持不变」:本帧**进碰撞前**的
|
|
1330
|
+
// 水平速率快照。碰撞会把速度清零,子步跑完再想要「原来的长度」就没有了,必须在入口取。
|
|
1331
|
+
this._preCollideHorizSpeed = Math.hypot(this.playerVelocity.x, this.playerVelocity.z);
|
|
1332
|
+
// 位移细分(连续碰撞):把本帧位移切成若干 ≤ MAX_STEP_DISTANCE 的小步,逐步推进并检测。
|
|
1333
|
+
// 不细分的话,掉帧(deltaTime 被 clamp 到 0.05)时单步位移可达 25×0.025 ≈ 0.6,
|
|
1334
|
+
// 远大于射线的检测余量(沿法线最小 reach R/√2 = 0.749 − 子步 0.1)→ 直接跨过墙体(穿墙)。
|
|
1335
|
+
const deltaPosition = this._tempVec5.copy(this.playerVelocity).multiplyScalar(deltaTime);
|
|
1336
|
+
const stepCount = Math.min(this.MAX_STEP_COUNT, Math.max(1, Math.ceil(deltaPosition.length() / this.MAX_STEP_DISTANCE)));
|
|
1337
|
+
deltaPosition.divideScalar(stepCount);
|
|
1338
|
+
for (let s = 0; s < stepCount; s++) {
|
|
1339
|
+
// ⭐⭐ 2026-10-02 皇上旨「模拟修正,不是真实修正」:**这里不再 translate 真实位置**。
|
|
1340
|
+
// 本小步的位移交给 playerCollisions,在「真实位置 + 本位移」的模拟位置上把 xsh 的
|
|
1341
|
+
// while 跑完(含循环内的竖向读数),由它末尾唯一一次 translate 落地。
|
|
1342
|
+
// 旧写法 = 先 translate 进墙 0.039m 再被修正弹出来 0.06m,一个渲染帧两趟 ⇒ 肉眼「超级抖」
|
|
1343
|
+
// (皇上 2026-10-02 台账:③位移折返 24 次 / ⑤单条位移峰 0.0565m / pen=corr=0.0600 恒等)。
|
|
1344
|
+
this._subStep.copy(deltaPosition);
|
|
1345
|
+
// 只有最后一步记录调试射线 + 分发移动/落地事件(细分只是碰撞中间过程,不应重复派发)
|
|
1346
|
+
this.playerCollisions(s === stepCount - 1);
|
|
1347
|
+
}
|
|
325
1348
|
// 设置相机位置(第一人称默认值)
|
|
326
1349
|
this.camera.position.copy(this.playerCollider.end);
|
|
327
1350
|
// 第三人称:偏移相机位置 + 朝向第一人称坐标
|
|
@@ -347,106 +1370,137 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
347
1370
|
}
|
|
348
1371
|
this.camera.rotation.x = clamp(this.camera.rotation.x, degToRad(-89), degToRad(89));
|
|
349
1372
|
};
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
//
|
|
1373
|
+
/**
|
|
1374
|
+
* 碰撞检测(完全参考 xsh-react-three FPSControls.tsx 的 computerPosition 方法)
|
|
1375
|
+
* @param isFinalStep 是否为位移细分的最后一步:
|
|
1376
|
+
* 只有最后一步才记录调试射线数据、分发移动/跳跃落地事件,中间小步只做推离。
|
|
1377
|
+
*/
|
|
1378
|
+
// 与 xsh-react-three 的对应关系(本地实现逐条对照):
|
|
1379
|
+
// xsh: position prop → _localRayOrigins(本地已直接用米,不再 ×cv)
|
|
1380
|
+
// xsh: direction prop → _localRayDirs(归一化)
|
|
1381
|
+
// xsh: raycaster.far = getDirection().length() → HORIZONTAL_FAR / downFar / upFar
|
|
356
1382
|
// xsh: Group3D rotation={x:-euler.x} → yawQuat(pitch 补偿)
|
|
357
1383
|
// xsh: getIntersetResult(positionIncremental) → worldPos = playerCollider.end + positionIncremental
|
|
358
1384
|
//
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
// 3 leftToForward: origin=(-cv,0,0)×yaw+worldPos, dir=(1,0,-1).norm×yaw, far=cv√2
|
|
364
|
-
// 4 leftToBack: origin=(-cv,0,0)×yaw+worldPos, dir=(1,0,1).norm×yaw, far=cv√2
|
|
365
|
-
// 5 rightToBack: origin=(cv,0,0)×yaw+worldPos, dir=(-1,0,1).norm×yaw, far=cv√2
|
|
366
|
-
playerCollisions() {
|
|
1385
|
+
// 现行 10 条布局逐条表 = 上方「★ 矩形射线法 ★」权威规格块(唯一数据源,不在这里重复一遍)。
|
|
1386
|
+
/** 输出一条碰撞日志(唯一出口 = 浏览器控制台;皇上:「日志就是浏览器的控制台」) */
|
|
1387
|
+
/** 第一人称碰撞主入口:xsh `computerPosition` 移植的迭代推离主循环。 */
|
|
1388
|
+
playerCollisions(isFinalStep = true) {
|
|
367
1389
|
this.playerOnFloor = false;
|
|
368
1390
|
this._getColliders();
|
|
1391
|
+
// ⭐⭐⭐ 2026-09-28 皇上旨(原话):「你还是完完全全使用 xsh-react-three 项目里面的
|
|
1392
|
+
// 第一人称控制器的碰撞计算的方法吧」。
|
|
1393
|
+
// ⇒ 本方法整体 = xsh `FPSControls.tsx → computerPosition()` 的忠实移植,骨架只有:
|
|
1394
|
+
// ① while 循环(皇上 09-03 定「一帧合计不超过 100」,10-03 旨改到 500;骨架与 xsh 的 i<100 同形);
|
|
1395
|
+
// ② 每轮在「候选位置」(真实位置 + positionIncremental)上打 8 条水平斜弦射线,
|
|
1396
|
+
// 每条命中 ⇒ 沿「控制器中心 − 命中点」方向推一小格(xsh 源码写死 0.03;皇上 10-07 旨
|
|
1397
|
+
// 「应该 1 个单位 1 个单位的修正」⇒ 本项默认改成 RETREAT_SKIN = 0.01,一格 1cm)
|
|
1398
|
+
// 累加进 positionIncremental,并 goOnComputer=true 续命下一轮(xsh 各票天然抵消);
|
|
1399
|
+
// ③ 下射线命中 ⇒ 竖直速度清零 + y 钉到「命中面之上 DOWN_RANGE」(xsh: cv − distance);
|
|
1400
|
+
// 上射线命中 ⇒ 竖直速度清零 + 修正(含三道闸天花板的血案守卫);
|
|
1401
|
+
// ④ 某一轮全部射线脱开 ⇒ 收工;循环外唯一一次 translate 落地(血案护栏不变)。
|
|
1402
|
+
// ⭐⭐ 2026-10-02 皇上旨「模拟修正,不是真实修正,把最终结果算好了再应用到真实的第一人称
|
|
1403
|
+
// 控制器上面」⇒ 那个 translate 落的是**「本步位移 + 允许的修正」一次到位**:
|
|
1404
|
+
// while 全程跑在模拟位置(真实 end + `_subStep` + 增量)上,真实 collider 在此之前**一动不动**
|
|
1405
|
+
// (旧写法先把它 translate 进墙里再由修正弹出来 ⇒ 一帧两趟来回甩 = 「超级抖」)。
|
|
1406
|
+
// ⭐ 10-02 同日曾加过「真实落地只许弹本步朝墙挤进的那一份」的上限 ⇒ **实测穿墙、当场作废**,
|
|
1407
|
+
// 代码已整条删除(详见循环末尾那一段的实录,禁止复活护栏在 AI_INDEX.md 登记)。
|
|
1408
|
+
// 与 xsh 的必要偏差(均为已验收修复,非自作主张):
|
|
1409
|
+
// · 水平修正压平 y —— xsh 水平射线在中心高度(y=0)修正天然水平;
|
|
1410
|
+
// 本布局射线在 y=−0.7/−0.75(皇上 09-26 五旨),不压平会把 y 混进推离;
|
|
1411
|
+
// · 水平射线 8 条(皇上 09-06/09-14 亲加的 6-9 共线反向组),xsh 原版 4 条;
|
|
1412
|
+
// · 竖向保留 faceUp / 天花板三道闸守卫(「掉出世界」血案修复)。
|
|
1413
|
+
// 原 deficit / 票面合成(finalDir) / 逐格推离 / 白推保险丝 / 胶囊兜底机器整条拆除。
|
|
369
1414
|
const positionIncremental = this._tempVec4.set(0, 0, 0);
|
|
370
|
-
|
|
371
|
-
const
|
|
1415
|
+
this._refreshInputDir(); // 台账 ix/iz 列还要用(与碰撞计算无关)
|
|
1416
|
+
const recordDebug = (this.dev || this.thirdPerson) && isFinalStep;
|
|
1417
|
+
let debugRecorded = !recordDebug;
|
|
1418
|
+
this._horizHit = false;
|
|
1419
|
+
// ⭐ 皇上 2026-10-07 裁决「加一个每小帧轮数上限」:这一趟 while 最多走几轮就落地。
|
|
1420
|
+
// 常态帧用旋钮 ROUNDS_PER_SUBSTEP(默认 500 与 MAX_ITERATIONS 同档 ⇒ 现状完全不变)。
|
|
1421
|
+
const roundCap = this.ROUNDS_PER_SUBSTEP;
|
|
1422
|
+
const downRange = this.DOWN_RANGE;
|
|
1423
|
+
const upRange = this.UP_RANGE;
|
|
372
1424
|
let i = 0;
|
|
373
1425
|
let goOnComputer = true;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
1426
|
+
while (goOnComputer && (i === 0 || this._frameIterBudget > 0)) {
|
|
1427
|
+
// ⚠️ 整帧共享预算(皇上 09-03「碰撞计算次数不超过 100」→ 10-03 旨改到 500):每个子步保证至少 1 轮。
|
|
1428
|
+
if (i > 0)
|
|
1429
|
+
this._frameIterBudget--;
|
|
378
1430
|
goOnComputer = false;
|
|
379
1431
|
i++;
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
.applyQuaternion(yawQuat)
|
|
392
|
-
.add(worldPos);
|
|
393
|
-
this._worldRayDirs[ri]
|
|
394
|
-
.copy(this._localRayDirs[ri])
|
|
395
|
-
.applyQuaternion(yawQuat)
|
|
396
|
-
.normalize();
|
|
1432
|
+
// xsh: object3d.getWorldPosition() —— 修正方向的锚点(循环内不变,不含增量)
|
|
1433
|
+
// ⭐ 2026-10-02 模拟修正:真实 collider 此刻**还没被本步位移推过**,
|
|
1434
|
+
// 所以锚点 = 真实位置 + 本步位移 =「这一步走完会站在哪儿」的**模拟位置**
|
|
1435
|
+
// (数值上与旧写法「先 translate 再取 end」完全相同 ⇒ 几何、方向、测试基线都不变)。
|
|
1436
|
+
const basePos = this._tempVec3.copy(this.playerCollider.end).add(this._subStep);
|
|
1437
|
+
// xsh: 射线原点 = 控制器位置 + positionIncremental(getIntersetResult(incremental) 语义)
|
|
1438
|
+
const worldPos = this._candPos.copy(basePos).add(positionIncremental);
|
|
1439
|
+
// xsh: Group3D rotation={x:-euler.x} 补偿 pitch → 只跟随 yaw
|
|
1440
|
+
const yawQuat = this._tempQuat.setFromEuler(this._tempEuler.set(0, this.camera.rotation.y, 0, "YXZ"));
|
|
1441
|
+
for (const ray of this._rays) {
|
|
1442
|
+
ray.updateWorld(yawQuat, worldPos);
|
|
397
1443
|
}
|
|
398
|
-
// far
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
1444
|
+
const horizontalFar = this.HORIZONTAL_FAR; // = R√2(xsh: far = direction.length())
|
|
1445
|
+
const downFar = downRange + this.VERTICAL_SKIN;
|
|
1446
|
+
const upFar = upRange + this.VERTICAL_SKIN;
|
|
1447
|
+
const devVisualFarD = downRange;
|
|
1448
|
+
const devVisualFarU = upRange;
|
|
1449
|
+
// ========== 水平半区(xsh 原样:每条命中射线各推一小格)==========
|
|
1450
|
+
for (let ri = 2; ri < this._rays.length; ri++) {
|
|
1451
|
+
const ray = this._rays[ri];
|
|
1452
|
+
ray.far = horizontalFar;
|
|
1453
|
+
ray.applyToRaycaster(this._raycaster);
|
|
406
1454
|
const intersets = this._raycaster.intersectObjects(this._colliders, false);
|
|
407
|
-
// 调试:第一次迭代记录射线数据
|
|
408
1455
|
if (!debugRecorded) {
|
|
409
|
-
|
|
410
|
-
let closestDist = Infinity;
|
|
1456
|
+
// ⭐ 铁律(09-05 定案):辅助线指示真实探测方向——命中画到最近命中点、未命中画满 far
|
|
411
1457
|
let intersetPoint = null;
|
|
412
|
-
if (
|
|
1458
|
+
if (intersets.length > 0) {
|
|
1459
|
+
let closestDist = Infinity;
|
|
413
1460
|
for (const int of intersets) {
|
|
414
|
-
const d = int.point.distanceTo(
|
|
1461
|
+
const d = int.point.distanceTo(basePos);
|
|
415
1462
|
if (d < closestDist) {
|
|
416
1463
|
closestDist = d;
|
|
417
1464
|
intersetPoint = int.point;
|
|
418
1465
|
}
|
|
419
1466
|
}
|
|
420
1467
|
}
|
|
421
|
-
|
|
422
|
-
origin: this._worldRayOrigins[ri].clone(),
|
|
423
|
-
farPoint: hasHit
|
|
424
|
-
? intersetPoint.clone()
|
|
425
|
-
: this._worldRayOrigins[ri].clone().addScaledVector(this._worldRayDirs[ri], horizontalFar),
|
|
426
|
-
hit: hasHit,
|
|
427
|
-
};
|
|
1468
|
+
ray.recordHit(intersetPoint, horizontalFar);
|
|
428
1469
|
}
|
|
429
|
-
if (intersets.length
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
1470
|
+
if (intersets.length === 0)
|
|
1471
|
+
continue;
|
|
1472
|
+
// 最近命中(xsh: 离控制器位置最近的那个交点)
|
|
1473
|
+
let interset = intersets[0];
|
|
1474
|
+
for (let j = 1; j < intersets.length; j++) {
|
|
1475
|
+
if (intersets[j].point.distanceTo(basePos) < interset.point.distanceTo(basePos)) {
|
|
1476
|
+
interset = intersets[j];
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
// ⭐ xsh 原文:vector3Temp = worldPos − interset.point; setLength(0.03);
|
|
1480
|
+
// positionIncremental.add(vector3Temp); goOnComputer = true;
|
|
1481
|
+
// ⚠️ 锚点 = basePos(xsh 的 getWorldPosition,循环内不含增量)。
|
|
1482
|
+
const corr = this._corrVec.copy(basePos).sub(interset.point);
|
|
1483
|
+
corr.y = 0; // 偏差①:本布局射线在 y=−0.7/−0.75,不压平会把 y 混进推离(xsh 射线在 y=0 天然水平)
|
|
1484
|
+
if (corr.lengthSq() > 1e-12) {
|
|
1485
|
+
if (i > roundCap) {
|
|
1486
|
+
// ⭐ **轮次上限**用完 ⇒ 这一轮起水平一格都不再推。
|
|
1487
|
+
// 常态帧 = 皇上 10-07 点头新加的 ROUNDS_PER_SUBSTEP(默认 500 ⇒ 与旧行为同形)。
|
|
1488
|
+
// ⚠️ 只拦水平:竖向(踩地/顶头)仍照常,否则人会往地心沉(皇上 09-25 冻结口径)。
|
|
1489
|
+
}
|
|
1490
|
+
else {
|
|
1491
|
+
corr.setLength(this.RETREAT_SKIN);
|
|
1492
|
+
positionIncremental.add(corr);
|
|
1493
|
+
goOnComputer = true;
|
|
1494
|
+
this._horizHit = true;
|
|
1495
|
+
// 方向线/台账的「修正方向」快照 = 最后一次命中的推离方向
|
|
1496
|
+
// ⭐ 皇上 10-07 旨「每执行一次,偏移量是多少」⇒ 按钮那一次:一格一行,带累计
|
|
437
1497
|
}
|
|
438
|
-
// 与 xsh: object3d.getWorldPosition().sub(interset.point).setLength(0.03) 一致
|
|
439
|
-
this._tempVec
|
|
440
|
-
.copy(worldPos)
|
|
441
|
-
.sub(interset.point)
|
|
442
|
-
.setLength(0.03);
|
|
443
|
-
positionIncremental.add(this._tempVec);
|
|
444
|
-
goOnComputer = true;
|
|
445
1498
|
}
|
|
446
1499
|
}
|
|
447
1500
|
// ========== 向下射线(索引 0 = upTodwomRay)==========
|
|
448
|
-
|
|
449
|
-
|
|
1501
|
+
const downRay = this._rays[0];
|
|
1502
|
+
downRay.far = downFar;
|
|
1503
|
+
downRay.applyToRaycaster(this._raycaster);
|
|
450
1504
|
let downIntersets = this._raycaster.intersectObjects(this._colliders, false);
|
|
451
1505
|
// 调试:记录向下射线
|
|
452
1506
|
if (!debugRecorded) {
|
|
@@ -462,13 +1516,9 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
462
1516
|
}
|
|
463
1517
|
}
|
|
464
1518
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
? intersetPoint.clone()
|
|
469
|
-
: this._worldRayOrigins[0].clone().addScaledVector(this._worldRayDirs[0], downFar),
|
|
470
|
-
hit: hasHit,
|
|
471
|
-
};
|
|
1519
|
+
// ⭐ 2026-09-25 皇上两条旨:竖向线命中不缩短、也不许往地板里多画 → 画到下射线自身长度
|
|
1520
|
+
downRay.recordHit(intersetPoint, devVisualFarD);
|
|
1521
|
+
downRay.responded = hasHit;
|
|
472
1522
|
}
|
|
473
1523
|
if (downIntersets.length > 0) {
|
|
474
1524
|
let interset = downIntersets[0];
|
|
@@ -479,91 +1529,206 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
479
1529
|
}
|
|
480
1530
|
}
|
|
481
1531
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
intersetPoint = int.point;
|
|
502
|
-
}
|
|
1532
|
+
const downPen = downRange - interset.distance;
|
|
1533
|
+
// ⭐⭐ 2026-09-25 皇上旨「顶出来,不穿模」:命中面朝向决定这份修正能不能吃。
|
|
1534
|
+
// downPen > 0 = 面在脚底之上 = 脚底陷进地面 → 顶出来(原有行为,保留)。
|
|
1535
|
+
// downPen < 0 = 面在脚底之下 = 原有的「吸附到地面」(下楼梯/斜坡不悬空) ——
|
|
1536
|
+
// 但只有**朝上的可行走面**才许吸:脚下是一块「朝下的面」(板底,或从实体
|
|
1537
|
+
// 内部看到的底面)时吸下去 = 直接把人流送进板子里,从此两条竖射线全瞎。
|
|
1538
|
+
const faceUp = (this._faceNormalWorldY(interset) ?? 1) > 0;
|
|
1539
|
+
if (downPen >= 0 || faceUp) {
|
|
1540
|
+
this.playerVelocity.y = 0;
|
|
1541
|
+
positionIncremental.y += downPen;
|
|
1542
|
+
this.playerOnFloor = true;
|
|
1543
|
+
// ⚠️ 皇上:脚下踩实时 downPen≈0(玩家刚好停在距地 downRange 处),
|
|
1544
|
+
// 若无条件置 goOnComputer=true,while 循环会每帧跑满 MAX_ITERATIONS 次
|
|
1545
|
+
// (每次都重新检测到地面、再置 true)→ 站地面时碰撞开销暴涨 ~MAX_ITERATIONS×,FPS 骤降、
|
|
1546
|
+
// 鼠标视角卡顿;起跳离地后脚下射线不命中、循环 1 次即退出 → 反而流畅。
|
|
1547
|
+
// 只有「真实发生了有意义的垂直修正」才需要重跑循环重新校验水平碰撞。
|
|
1548
|
+
// (向上射线早就有 `if (upPen > 0)` 这层保护,唯独向下射线漏了 → 这就是根因。)
|
|
1549
|
+
if (Math.abs(downPen) > 1e-4) {
|
|
1550
|
+
goOnComputer = true;
|
|
503
1551
|
}
|
|
504
1552
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
1553
|
+
// ⭐ 2026-09-25 皇上旨「我说的日志就是浏览器的控制台,不是你开发一个控制台」:
|
|
1554
|
+
// 原来这段是独立 console.log 探针(自带节流),现改为**只备料**,
|
|
1555
|
+
// 文本并进下方【碰撞日志】同一次输出里 → 皇上抄一次就同时拿到水平方向 + 下射线全部数值。
|
|
1556
|
+
// ⚠️ 取样闸门 = `i === 1`(本帧第一次迭代;`i++` 在循环体最前面,故日志头「迭代轮次」首轮印 1)。
|
|
1557
|
+
// **不能**沿用旧探针的 `!debugRecorded`:`debugRecorded = !recordDebug`,而 `recordDebug`
|
|
1558
|
+
// 跟着面板「碰撞射线」(dev) 走 → dev 关着时恒 true → 整段被跳过,
|
|
1559
|
+
// 这正是皇上「在控制台里根本看不到下射线数值」的原因。
|
|
1560
|
+
// ========== 向上射线(索引 1 = dwomToUpRay)==========
|
|
1561
|
+
const upRay = this._rays[1];
|
|
1562
|
+
upRay.far = upFar;
|
|
1563
|
+
upRay.applyToRaycaster(this._raycaster);
|
|
1564
|
+
let upIntersets = this._raycaster.intersectObjects(this._colliders, false);
|
|
1565
|
+
// 调试:记录向上射线
|
|
1566
|
+
if (!debugRecorded) {
|
|
1567
|
+
const hasHit = upIntersets.length > 0;
|
|
1568
|
+
let intersetPoint = null;
|
|
1569
|
+
if (hasHit) {
|
|
1570
|
+
let closestDist = Infinity;
|
|
1571
|
+
for (const int of upIntersets) {
|
|
1572
|
+
const d = int.point.distanceTo(worldPos);
|
|
1573
|
+
if (d < closestDist) {
|
|
1574
|
+
closestDist = d;
|
|
1575
|
+
intersetPoint = int.point;
|
|
1576
|
+
}
|
|
520
1577
|
}
|
|
521
1578
|
}
|
|
1579
|
+
upRay.recordHit(intersetPoint, devVisualFarU);
|
|
1580
|
+
upRay.responded = hasHit;
|
|
1581
|
+
debugRecorded = true;
|
|
522
1582
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
1583
|
+
if (upIntersets.length > 0) {
|
|
1584
|
+
let interset = upIntersets[0];
|
|
1585
|
+
if (upIntersets.length > 1) {
|
|
1586
|
+
for (let j = 1; j < upIntersets.length; j++) {
|
|
1587
|
+
if (upIntersets[j].point.distanceTo(worldPos) < interset.point.distanceTo(worldPos)) {
|
|
1588
|
+
interset = upIntersets[j];
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
// 按距离解算(与下射线一致):头顶离天花板 interset.distance,需保持 upRange → 下推 (upRange - distance)
|
|
1593
|
+
const upPen = upRange - interset.distance;
|
|
1594
|
+
if (upPen > 0 && downIntersets.length === 0
|
|
1595
|
+
&& this.playerVelocity.y <= 0 && !this._floorBelow(worldPos)) {
|
|
1596
|
+
// ⭐⭐ 2026-09-25 皇上旨「顶出来,不穿模」(「下射线会陷入地面很长一个距离」的正解)
|
|
1597
|
+
// 上射线打到东西、**脚下却没有任何地面**、且人不正在往上蹿 → 我已经在这块面的
|
|
1598
|
+
// 下方/内部,再往下顶就是把它当「天花板」往下钻 = 死亡路径:
|
|
1599
|
+
// 实测脚底 −1.5 起手(1m 厚地板,tmp/TMP_vert_trace.mjs)——
|
|
1600
|
+
// 前 10 帧人在板内、FrontSide 看不到背面 → 上下射线全瞎 → 重力累加上坠;
|
|
1601
|
+
// 第 11 帧眼落到板底之下 0.0135,上射线打到**板底(法线朝下)**,旧口径
|
|
1602
|
+
// `positionIncremental.y -= upPen` 一帧把人往下砸 **0.9865m** → 眼落到 −2.0,
|
|
1603
|
+
// 从此两条竖射线彻底够不着 → 自由落体到 **−13.161547** → 撞上 camera.y≤−10 的
|
|
1604
|
+
// 掉出世界复位(`this.playerCollider.end.set(0, 1, 10)`)→ 「陷进地面很长一段
|
|
1605
|
+
// 距离然后凭空回到原点」。这就是皇上看到的现象。
|
|
1606
|
+
// 新口径:**绝不往下顶**,一步顶到「脚底刚好落在这块面之上」(眼高 = 面 y +
|
|
1607
|
+
// downRange + SKIN)。顶完眼只比板底高一个 SKIN → 下一轮迭代下射线立刻
|
|
1608
|
+
// 看得到板顶(distance≈SKIN)→ downPen≈downRange 把人整段托上板面站稳,
|
|
1609
|
+
// 上下射线互为 backup,全程 ≤ 十几帧、不再掉出世界。
|
|
1610
|
+
// ⚠️ 三道闸缺一不可:
|
|
1611
|
+
// · `downIntersets.length===0`:站在下层地板上、头顶真天花板时,往下鸭/躲矮洞
|
|
1612
|
+
// 是正确行为,不能把人吸到上层去;
|
|
1613
|
+
// · `playerVelocity.y <= 0`:正往上跳、头顶撞到真天花板必须仍按普通天花板往下顶,
|
|
1614
|
+
// 否则「跳一下 → 被传上天花板」;
|
|
1615
|
+
// · `!_floorBelow()`:竖向射线只够 downFar(1.52)/upFar(0.32),而跳跃顶点 ≈1.06m 加上
|
|
1616
|
+
// 脚底悬浮 0.5m 已经超出下射线够得着的范围 →
|
|
1617
|
+
// 腾空时脚下本来就是瞎的,只靠上面两道闸会把「高空跳到天花板下方」也误判成穿模。往下深探
|
|
1618
|
+
// VOID_PROBE_DOWN(8m) 问得到地面 = 正常腾空/下落 → 维持旧的天花板下压。
|
|
1619
|
+
// ⚠️ 已知残留(实测 tmp/TMP_ceiling_ab.mjs ⑦):板**厚度 > DOWN_RANGE(1.5m)** 时,
|
|
1620
|
+
// 顶到「板底之上」后人仍在板内 → 再次全瞎下坠 → 在板底往复。
|
|
1621
|
+
// HEAD 版同一场景是「掉到 −10.9 → 凭空复位」,现在收敛为有界往复(不再掉出世界),
|
|
1622
|
+
// 彻底根治要加**背面探针**(FrontSide 看不到内部,量不到板顶)。
|
|
1623
|
+
this.playerVelocity.y = 0;
|
|
1624
|
+
const liftTo = interset.point.y + downRange + this.VERTICAL_SKIN - worldPos.y;
|
|
1625
|
+
if (liftTo > 1e-4) {
|
|
1626
|
+
positionIncremental.y += liftTo;
|
|
1627
|
+
this.playerOnFloor = true;
|
|
1628
|
+
goOnComputer = true;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
else if (upPen > 0) {
|
|
1632
|
+
this.playerVelocity.y = 0;
|
|
1633
|
+
positionIncremental.y -= upPen;
|
|
1634
|
+
goOnComputer = true;
|
|
1635
|
+
}
|
|
534
1636
|
}
|
|
535
|
-
goOnComputer = true;
|
|
536
1637
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
1638
|
+
// ========== 更新调试射线(dev 模式的最后一步)==========
|
|
1639
|
+
if (recordDebug) {
|
|
1640
|
+
this._initDebugLines();
|
|
1641
|
+
// 像素宽依赖画布尺寸:resize / 改 debugLineWidth 都在这里跟上
|
|
1642
|
+
this._syncDebugLineResolution();
|
|
1643
|
+
// ⭐ 每条 FpsRay 自带 debugLine,直接 syncDebugLine(起点/终点由 ray.recordHit 冻结,
|
|
1644
|
+
// 三态配色由 ray.syncDebugLine 写入)。射线与辅助线一一对应,不可能再错位。
|
|
1645
|
+
for (let di = 0; di < this._rays.length; di++) {
|
|
1646
|
+
// ⚠️ 皇上 2026-09-03:横贯中央的 ±X 侧向射线默认不画(debugHideSideRays 开关)。
|
|
1647
|
+
// (原 6/7 侧向已删;当前 6-9 是反向水平斜弦,isSideRay=false → 不受本开关影响,可见。
|
|
1648
|
+
// 皇上要「看到区别」→ 反向射线必须画出来。)若日后加回 ±X 侧向直射线,给那条 isSideRay 置 true 即自动隐藏。
|
|
1649
|
+
if (this.debugHideSideRays && this._rays[di].isSideRay)
|
|
1650
|
+
continue;
|
|
1651
|
+
const ray = this._rays[di];
|
|
1652
|
+
if (ray.debugLine)
|
|
1653
|
+
ray.syncDebugLine();
|
|
545
1654
|
}
|
|
546
1655
|
}
|
|
1656
|
+
// 应用推离
|
|
1657
|
+
// ⚠️⚠️ 这一段**只能出现一次**(2026-09-21 血案:重构时误插入第二份,
|
|
1658
|
+
// 位置推离量直接翻倍 → 陷 0.42m 被推 0.84m、垂距冲到 0.94(该停在 0.52)
|
|
1659
|
+
// → 皇上看到的「从侧面撞墙角一下子弹很远」)。改动本文件时务必确认
|
|
1660
|
+
// `positionIncremental` 全文件只被 `translate` 应用一次。
|
|
1661
|
+
// ⭐⭐ 2026-09-25 皇上旨「加个开关可以关闭碰撞修正位置的计算,方便观察回弹向量」:
|
|
1662
|
+
// 唯一的**位置写入口**就是这一行 → 在这一个闸门上关,才是真的「不修正位置」。
|
|
1663
|
+
// 原先 `previewRetreat`(面板「回弹预览」)只跳过了上方 `!previewRetreat` 那个**水平**
|
|
1664
|
+
// 修正块,竖向(下射线 `y += downPen` / 上射线 `y ± upPen`)照样从这里落地 →
|
|
1665
|
+
// 标签写着「不修正位置」却仍在被顶,且陷进地面时每轮迭代累加一份 downPen,
|
|
1666
|
+
// 预览模式下人被竖直弹飞。
|
|
1667
|
+
// ⚠️ 只跳「写位置」,不跳「记账」:`playerVelocity.y = 0`、`playerOnFloor = true`
|
|
1668
|
+
// 与削速度/方向票全部保留 → 观察时不会一帧帧攒重力陷到地心,也不会触发
|
|
1669
|
+
// camera.y ≤ −10 的掉出世界复位。
|
|
547
1670
|
}
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
1671
|
+
// ⭐⭐ 2026-10-02 皇上旨(原话):「在修正位置的时候,我希望的是**模拟修正**,而不是**真实的
|
|
1672
|
+
// 修正**,在模拟修正中把最终结果计算好了,再应用到真实的第一人称控制器上面」。
|
|
1673
|
+
// ⇒ 上面那个 while 全在**模拟位置**(真实位置 + 本步位移 `_subStep`)上跑;真实 collider
|
|
1674
|
+
// 在 while 期间**一动不动**,只在下面那**唯一一次** translate 上落到「本步位移 + 算好的
|
|
1675
|
+
// 最终修正」。旧写法每小帧先 translate 进墙 0.04m、再被弹出来 0.06m ⇒ 一个渲染帧两趟。
|
|
1676
|
+
// ⚠️⚠️ 10-02 同日实测作废的那条「把弹出扣在本步挤进量以内」上限(cap = inward + 欠账)
|
|
1677
|
+
// **不许复活**:模拟算出的修正方向会在一小步之间换向(墙角 / 8 条斜弦 / 高速子步),
|
|
1678
|
+
// 「每步沿自己的方向净位移 ≤ 0」加起来 = 沿着墙往几何体里漂。体检台实测:40 号
|
|
1679
|
+
// 「跳跃+冲刺+掉帧」被扣住的弹出累计 3.12m ⇒ 人穿到墙背面,末位 x 从 3.9 跑到 162
|
|
1680
|
+
// ⇒ ❌穿墙红线(HEAD 基线 ❌0)。模拟位置是**保证脱开**的,全额落地才不会陷进去。
|
|
1681
|
+
// ⚠️ 竖向(y)一律真实落地 —— 皇上 2026-09-25 冻结,本段一个 y 都不改口径。
|
|
1682
|
+
let corrX = positionIncremental.x;
|
|
1683
|
+
let corrZ = positionIncremental.z;
|
|
1684
|
+
let corrY = positionIncremental.y;
|
|
1685
|
+
if (this.previewRetreat) {
|
|
1686
|
+
// 「回弹预览」= 只走路、任何修正都不落地(连竖向都不落,2026-09-25 定的:
|
|
1687
|
+
// 陷地面时每轮累加一份 downPen 会把人竖直弹飞)。
|
|
1688
|
+
corrX = 0;
|
|
1689
|
+
corrZ = 0;
|
|
1690
|
+
corrY = 0;
|
|
551
1691
|
}
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
1692
|
+
// ⚠️⚠️ 全文件**唯一**的水平位置写入口(2026-09-21 血案:误插第二份 ⇒ 推离量翻倍 ⇒
|
|
1693
|
+
// 陷 0.42m 被推 0.84m ⇒ 「从侧面撞墙角一下子弹很远」)。
|
|
1694
|
+
// 模拟算完 → 本步位移 + 允许的修正 → 一次应用到真实控制器(皇上 10-02 旨)。
|
|
1695
|
+
// ⚠️ 本步位移在这里一起落地:走路从旧 updatePlayer 循环外的 translate 挪到了这一行,
|
|
1696
|
+
// 位移永远照走(previewRetreat 只关「修正」那份,不关位移)。
|
|
1697
|
+
this._appliedMove.set(this._subStep.x + corrX, this._subStep.y + corrY, this._subStep.z + corrZ);
|
|
1698
|
+
if (this._appliedMove.lengthSq() > 0) {
|
|
1699
|
+
this.playerCollider.translate(this._appliedMove);
|
|
556
1700
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
1701
|
+
// 本步已消费:归零,面板那两颗直接调 playerCollisions 的按钮不会重复走一步
|
|
1702
|
+
this._subStep.set(0, 0, 0);
|
|
1703
|
+
// ⭐⭐ 2026-10-07 皇上旨「撞到墙后,加速度置为 0」:
|
|
1704
|
+
// 抖动根因 = 撞墙后位置被推回包络边,但 playerVelocity 的水平分量(朝墙的冲量)一直保留,
|
|
1705
|
+
// 下一帧 controls() 又叠加输入加速度 → 再次朝墙冲 → 再次被推回 → 来回甩 = 肉眼抖动。
|
|
1706
|
+
// ⇒ 本子步水平命中(_horizHit)即把 playerVelocity.x/z 清零:人贴墙即停,
|
|
1707
|
+
// 不再被惯性反复往墙里拽、被推离反复弹回。(观察模式:单步/回弹预览不改速度,
|
|
1708
|
+
// 免得不落地的观察帧把速度抹掉、干扰手感判断;竖向 y 一字不动——重力/跳跃照旧。)
|
|
1709
|
+
if (this._horizHit && !this.previewRetreat) {
|
|
1710
|
+
this.playerVelocity.x = 0;
|
|
1711
|
+
this.playerVelocity.z = 0;
|
|
561
1712
|
}
|
|
562
|
-
|
|
563
|
-
|
|
1713
|
+
// ⭐ 十旨:一帧一条的抖动台账。**只由子步循环的最后一步(isFinalStep)记** ——
|
|
1714
|
+
// 中间小步是碰撞细分的过渡状态,记了就变成「一帧好几条」,翻向/跳变的口径全乱。
|
|
1715
|
+
// 事件分发(只在位移细分的最后一步执行,避免一次移动派发多组 move_start/move_end)
|
|
1716
|
+
if (isFinalStep) {
|
|
1717
|
+
if (this.jumping && !this.prePlayerOnFloor && this.playerOnFloor) {
|
|
1718
|
+
this.jumping = false;
|
|
1719
|
+
this.dispatchEvent({ type: "jump_end" });
|
|
1720
|
+
}
|
|
1721
|
+
const { x: preX, z: preZ } = this.prePlayerVelocity;
|
|
1722
|
+
const { x, z } = this.playerVelocity;
|
|
1723
|
+
if (Math.abs(preX + preZ) < 0.1 && Math.abs(x + z) > 0.1) {
|
|
1724
|
+
this.dispatchEvent({ type: "move_start" });
|
|
1725
|
+
}
|
|
1726
|
+
else if (Math.abs(preX + preZ) > 0.1 && Math.abs(x + z) < 0.1) {
|
|
1727
|
+
this.dispatchEvent({ type: "move_end" });
|
|
1728
|
+
}
|
|
1729
|
+
this.prePlayerVelocity.copy(this.playerVelocity);
|
|
1730
|
+
this.prePlayerOnFloor = this.playerOnFloor;
|
|
564
1731
|
}
|
|
565
|
-
this.prePlayerVelocity.copy(this.playerVelocity);
|
|
566
|
-
this.prePlayerOnFloor = this.playerOnFloor;
|
|
567
1732
|
}
|
|
568
1733
|
// 越界传送
|
|
569
1734
|
teleportPlayerIfOob = () => {
|
|
@@ -589,6 +1754,8 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
589
1754
|
this.camera.rotation.x = this._thirdPersonAngleX;
|
|
590
1755
|
}
|
|
591
1756
|
this._prevThirdPerson = this.thirdPerson;
|
|
1757
|
+
// 每帧重置碰撞迭代预算(整帧共享,见 _frameIterBudget 注释)
|
|
1758
|
+
this._frameIterBudget = this.MAX_ITERATIONS;
|
|
592
1759
|
const deltaTime = Math.min(0.05, this.clock.getDelta()) / this.STEPS_PER_FRAME;
|
|
593
1760
|
for (let i = 0; i < this.STEPS_PER_FRAME; i++) {
|
|
594
1761
|
this.controls(deltaTime);
|
|
@@ -596,17 +1763,32 @@ export default class FPScontrol extends EventDispatcher {
|
|
|
596
1763
|
this.teleportPlayerIfOob();
|
|
597
1764
|
}
|
|
598
1765
|
}
|
|
599
|
-
// dev
|
|
600
|
-
|
|
601
|
-
|
|
1766
|
+
// 调试射线可见性:dev / 第三人称 任一开启即把整组放出来(碰撞射线跟随 dev / 第三人称)。
|
|
1767
|
+
const showRays = this.dev || this.thirdPerson;
|
|
1768
|
+
if (this._debugRayGroup && this._debugRayGroup.visible !== showRays) {
|
|
1769
|
+
this._debugRayGroup.visible = showRays;
|
|
602
1770
|
}
|
|
603
|
-
|
|
604
|
-
this.
|
|
1771
|
+
if (this._debugRayGroup && this._rays.length > 0 && this._rays[0].debugLine) {
|
|
1772
|
+
const showBase = this.dev || this.thirdPerson;
|
|
1773
|
+
// ⭐ 射线辅助线可见性直接遍历 this._rays(与射线一一对应)
|
|
1774
|
+
for (let di = 0; di < this._rays.length; di++) {
|
|
1775
|
+
// ⚠️ 皇上 2026-09-03:原 ±X 侧向射线(ri=6/7)默认不画(太扎眼)。
|
|
1776
|
+
// ⚠️ 只改**可见性**,射线本身照常发射 —— 见 debugHideSideRays 注释,别去删射线。
|
|
1777
|
+
// 现 di 最大=9(6-9 是反向水平斜弦,isSideRay=false → 不受本开关影响,可见);
|
|
1778
|
+
// 若日后加回 ±X 侧向直射线,给那条 isSideRay=true 即自动隐藏(与 L1750 守卫一致)。
|
|
1779
|
+
const dl = this._rays[di].debugLine;
|
|
1780
|
+
if (dl)
|
|
1781
|
+
dl.visible = showBase && !(this.debugHideSideRays && this._rays[di].isSideRay);
|
|
1782
|
+
}
|
|
1783
|
+
// 方向线的显示由 playerCollisions 里「本帧是否真算出回弹方向」决定,这里不再覆盖
|
|
605
1784
|
}
|
|
1785
|
+
// 诊断日志已撤(皇上 2026-09-03):[FPS-TP]/[FPS-CAM] 属当年第三人称排查的一次性诊断,
|
|
1786
|
+
// 链路已验证正常;射线辅助线(画面可见)仍保留,仅去掉 console 输出。
|
|
606
1787
|
}
|
|
607
1788
|
// 销毁事件
|
|
608
1789
|
disposer() {
|
|
609
1790
|
this.enable = false;
|
|
1791
|
+
fpsControlRegistry.delete(this);
|
|
610
1792
|
document.removeEventListener("keydown", this.keydown);
|
|
611
1793
|
document.removeEventListener("keyup", this.keyup);
|
|
612
1794
|
this.canvas.removeEventListener("pointerdown", this.mousedown);
|