three-base 2.0.130 → 2.0.137

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/ApngTexture.d.ts +13 -13
  2. package/ApngTexture.js +61 -61
  3. package/ApngTextureManage.d.ts +18 -18
  4. package/ApngTextureManage.js +87 -87
  5. package/DirectionalLight.d.ts +6 -0
  6. package/DirectionalLight.js +68 -0
  7. package/EventMesh.d.ts +18 -18
  8. package/EventMesh.js +59 -59
  9. package/ExtrudeMesh.d.ts +112 -112
  10. package/ExtrudeMesh.js +366 -366
  11. package/FPScontrol.d.ts +78 -50
  12. package/FPScontrol.js +617 -232
  13. package/Fire/Fire.d.ts +28 -28
  14. package/Fire/Fire.js +79 -79
  15. package/Fire/FireGromtry.d.ts +9 -9
  16. package/Fire/FireGromtry.js +40 -40
  17. package/Fire/FireMaterial.d.ts +28 -28
  18. package/Fire/FireMaterial.js +32 -32
  19. package/Fire/index.d.ts +1 -1
  20. package/Fire/index.js +1 -1
  21. package/Grass/Grass.d.ts +8 -8
  22. package/Grass/Grass.js +38 -38
  23. package/Grass/index.d.ts +1 -1
  24. package/Grass/index.js +1 -1
  25. package/Ground1/Ground1.d.ts +11 -11
  26. package/Ground1/Ground1.js +54 -54
  27. package/Ground1/index.d.ts +1 -1
  28. package/Ground1/index.js +1 -1
  29. package/InstancedMeshBase.d.ts +39 -39
  30. package/InstancedMeshBase.js +293 -293
  31. package/KeyFrameAnimation.d.ts +25 -25
  32. package/KeyFrameAnimation.js +78 -78
  33. package/MeshBase.d.ts +35 -35
  34. package/MeshBase.js +223 -223
  35. package/Mirror.d.ts +25 -25
  36. package/Mirror.js +139 -139
  37. package/PipeMesh/PathGeometry.d.ts +44 -44
  38. package/PipeMesh/PathGeometry.js +300 -300
  39. package/PipeMesh/PathPoint.d.ts +17 -17
  40. package/PipeMesh/PathPoint.js +31 -31
  41. package/PipeMesh/PathPointList.d.ts +31 -31
  42. package/PipeMesh/PathPointList.js +208 -208
  43. package/PipeMesh/PathTubeGeometry.d.ts +25 -25
  44. package/PipeMesh/PathTubeGeometry.js +130 -130
  45. package/PipeMesh/PipeMesh.d.ts +25 -25
  46. package/PipeMesh/PipeMesh.js +63 -63
  47. package/PipeMesh/index.d.ts +1 -1
  48. package/PipeMesh/index.js +1 -1
  49. package/PlaneModal1/PlaneModal1.d.ts +31 -31
  50. package/PlaneModal1/PlaneModal1.js +105 -105
  51. package/PlaneModal1/index.d.ts +1 -1
  52. package/PlaneModal1/index.js +1 -1
  53. package/PlaneText.d.ts +39 -39
  54. package/PlaneText.js +30 -30
  55. package/PlaneWithHtml.d.ts +11 -11
  56. package/PlaneWithHtml.js +63 -68
  57. package/PointLight/PointLight.d.ts +14 -13
  58. package/PointLight/PointLight.js +70 -64
  59. package/PointLight/index.d.ts +1 -1
  60. package/PointLight/index.js +1 -1
  61. package/Sky/Sky.d.ts +9 -9
  62. package/Sky/Sky.js +22 -22
  63. package/Sky/index.d.ts +1 -1
  64. package/Sky/index.js +1 -1
  65. package/SpotLight.d.ts +6 -0
  66. package/SpotLight.js +72 -0
  67. package/SpriteModal1/SpriteModal1.d.ts +30 -30
  68. package/SpriteModal1/SpriteModal1.js +110 -110
  69. package/SpriteModal1/index.d.ts +1 -1
  70. package/SpriteModal1/index.js +1 -1
  71. package/SpriteText.d.ts +45 -45
  72. package/SpriteText.js +163 -163
  73. package/SpriteText2.d.ts +47 -47
  74. package/SpriteText2.js +198 -198
  75. package/SpriteWithHtml.d.ts +7 -7
  76. package/SpriteWithHtml.js +44 -49
  77. package/Text3D/Text3D.d.ts +20 -20
  78. package/Text3D/Text3D.js +62 -62
  79. package/Text3D/fontController.d.ts +59 -59
  80. package/Text3D/fontController.js +199 -199
  81. package/Text3D/index.d.ts +1 -1
  82. package/Text3D/index.js +1 -1
  83. package/ThreeBase.d.ts +111 -81
  84. package/ThreeBase.js +656 -560
  85. package/ThreeMouse.d.ts +40 -40
  86. package/ThreeMouse.js +407 -407
  87. package/Transition.d.ts +40 -40
  88. package/Transition.js +210 -210
  89. package/Water/Water.d.ts +5 -0
  90. package/Water/Water.js +26 -0
  91. package/Water/waternormals.jpg +0 -0
  92. package/declare.d.ts +167 -0
  93. package/index.d.ts +1 -1
  94. package/index.js +2 -2
  95. package/keyContorls.d.ts +25 -25
  96. package/keyContorls.js +98 -98
  97. package/package.json +3 -2
  98. package/pass/bloom_pass.d.ts +2 -2
  99. package/pass/bloom_pass.js +99 -99
  100. package/pass/cover_pass.d.ts +2 -2
  101. package/pass/cover_pass.js +95 -95
  102. package/pass/emissive_ao_pass.d.ts +13 -0
  103. package/pass/emissive_ao_pass.js +370 -0
  104. package/pass/frosted_glass_pass.d.ts +2 -0
  105. package/pass/frosted_glass_pass.js +275 -0
  106. package/pass/fsQuad.d.ts +9 -9
  107. package/pass/fsQuad.js +28 -28
  108. package/pass/glow_pass.d.ts +2 -2
  109. package/pass/glow_pass.js +310 -254
  110. package/pass/ssao_pass.d.ts +13 -0
  111. package/pass/ssao_pass.js +372 -0
  112. package/pass/stroke_pass.d.ts +2 -2
  113. package/pass/stroke_pass.js +199 -199
  114. package/shaders/FXAAShaderMaterial.d.ts +3 -3
  115. package/shaders/FXAAShaderMaterial.js +21 -21
  116. package/shaders/baseTextureToRenderShader.d.ts +2 -2
  117. package/shaders/baseTextureToRenderShader.js +14 -14
  118. package/shaders/copyShader.d.ts +3 -3
  119. package/shaders/copyShader.js +14 -14
  120. package/shaders.d.ts +5 -5
  121. package/shaders.js +42 -42
  122. package/threeLoader.d.ts +34 -34
  123. package/threeLoader.js +527 -527
  124. package/threeUtils.d.ts +52 -52
  125. package/threeUtils.js +330 -330
package/FPScontrol.js CHANGED
@@ -1,232 +1,617 @@
1
- /*
2
- * @Author: xiaosihan
3
- * @Date: 2022-05-04 23:01:36
4
- * @Last Modified by: xiaosihan
5
- * @Last Modified time: 2024-11-07 21:57:39
6
- */
7
- import { Clock, EventDispatcher, Vector3, } from "three";
8
- import { Capsule } from "three/examples/jsm/math/Capsule";
9
- import { Octree } from "three/examples/jsm/math/Octree";
10
- import { clamp, degToRad } from "three/src/math/MathUtils";
11
- /**
12
- * 第一人称控制器
13
- *
14
- * @export
15
- * @class FPScontrol
16
- * @extends {EventDispatcher}
17
- */
18
- export default class FPScontrol extends EventDispatcher {
19
- canvas;
20
- constructor(camera, canvas) {
21
- super();
22
- // 相机对象
23
- this.camera = camera;
24
- this.camera.rotation.order = "YXZ";
25
- this.canvas = canvas;
26
- this.init(canvas);
27
- }
28
- // 是否启用
29
- enable = false;
30
- GRAVITY = 30; // 重力
31
- STEPS_PER_FRAME = 5; // 每帧碰撞计算的次数
32
- speed = 1; // 速度
33
- camera;
34
- // 移动时是否保持惯性 由外部来配置
35
- inertia = true;
36
- // 八叉树数组
37
- octreeMap = new Map();
38
- octree = (() => {
39
- const octree = new Octree();
40
- octree.getCapsuleTriangles = (capsule, triangles) => {
41
- const octrees = Array.from(this.octreeMap.values());
42
- for (let octree of octrees) {
43
- for (let i = 0; i < octree.subTrees.length; i++) {
44
- const subTree = octree.subTrees[i];
45
- if (!capsule.intersectsBox(subTree.box))
46
- continue;
47
- if (subTree.triangles.length > 0) {
48
- for (let j = 0; j < subTree.triangles.length; j++) {
49
- if (triangles.indexOf(subTree.triangles[j]) === -1)
50
- triangles.push(subTree.triangles[j]);
51
- }
52
- }
53
- else {
54
- subTree.getCapsuleTriangles(capsule, triangles);
55
- }
56
- }
57
- }
58
- return triangles;
59
- };
60
- return octree;
61
- })();
62
- addOctree(obj) {
63
- const octree = new Octree();
64
- octree.fromGraphNode(obj);
65
- this.octreeMap.set(obj.id, octree);
66
- return octree;
67
- }
68
- // 用户自己的碰撞体积
69
- playerCollider = new Capsule(new Vector3(0, 0.35, 0), new Vector3(0, 1, 0), 0.35);
70
- // 时钟对象
71
- clock = new Clock();
72
- prePlayerVelocity = new Vector3();
73
- playerVelocity = new Vector3();
74
- // 起跳中
75
- jumping = false;
76
- playerDirection = new Vector3();
77
- prePlayerOnFloor = false;
78
- playerOnFloor = false;
79
- mouseTime = 0;
80
- isMousedown = false;
81
- keyStates = {};
82
- init(canvas) {
83
- this.playerCollider.end.set(0, 1, 0);
84
- document.addEventListener("keydown", this.keydown);
85
- document.addEventListener("keyup", this.keyup);
86
- canvas.addEventListener("pointerdown", this.mousedown);
87
- document.addEventListener("pointerup", this.mouseup);
88
- document.body.addEventListener("pointermove", this.mousemove);
89
- this.enable = true;
90
- }
91
- keydown = (event) => {
92
- this.keyStates[event.code] = true;
93
- };
94
- keyup = (event) => {
95
- this.keyStates[event.code] = false;
96
- };
97
- mousedown = () => {
98
- this.isMousedown = true;
99
- this.mouseTime = performance.now();
100
- };
101
- mouseup = () => {
102
- this.isMousedown = false;
103
- };
104
- mousemove = (event) => {
105
- if (this.enable && this.isMousedown) {
106
- this.camera.rotation.y -= event.movementX / 500;
107
- this.camera.rotation.x -= event.movementY / 500;
108
- }
109
- };
110
- // 获取前进的向量
111
- getForwardVector = () => {
112
- this.camera.getWorldDirection(this.playerDirection);
113
- this.playerDirection.y = 0;
114
- this.playerDirection.normalize();
115
- return this.playerDirection;
116
- };
117
- // 获取 后退的向量
118
- getSideVector = () => {
119
- this.camera.getWorldDirection(this.playerDirection);
120
- this.playerDirection.y = 0;
121
- this.playerDirection.normalize();
122
- this.playerDirection.cross(this.camera.up);
123
- return this.playerDirection;
124
- };
125
- // 玩家移动控制器
126
- controls = (deltaTime) => {
127
- const speedDelta = deltaTime * (this.playerOnFloor ? 25 : 8) * this.speed;
128
- // const speedDelta = this.speed;
129
- if (this.keyStates["KeyW"]) {
130
- this.playerVelocity.add(this.getForwardVector().multiplyScalar(speedDelta));
131
- }
132
- if (this.keyStates["KeyS"]) {
133
- this.playerVelocity.add(this.getForwardVector().multiplyScalar(-speedDelta));
134
- }
135
- if (this.keyStates["KeyA"]) {
136
- this.playerVelocity.add(this.getSideVector().multiplyScalar(-speedDelta));
137
- }
138
- if (this.keyStates["KeyD"]) {
139
- this.playerVelocity.add(this.getSideVector().multiplyScalar(speedDelta));
140
- }
141
- if (this.playerOnFloor &&
142
- !this.keyStates["KeyW"] &&
143
- !this.keyStates["KeyS"] &&
144
- !this.keyStates["KeyA"] &&
145
- !this.keyStates["KeyD"]) {
146
- if (!this.inertia) {
147
- // 关闭惯性时就大幅增加动能的衰减
148
- this.playerVelocity.x *= 0.85;
149
- this.playerVelocity.z *= 0.85;
150
- }
151
- }
152
- if (this.playerOnFloor) {
153
- if (this.keyStates["Space"]) {
154
- this.playerVelocity.y = 15;
155
- this.jumping = true;
156
- this.dispatchEvent({ type: "jump_start" });
157
- }
158
- }
159
- };
160
- // 更新玩家的位置
161
- updatePlayer = (deltaTime) => {
162
- let damping = Math.exp(-4 * deltaTime) - 1;
163
- if (!this.playerOnFloor) {
164
- this.playerVelocity.y -= this.GRAVITY * deltaTime;
165
- damping *= 0.1;
166
- }
167
- this.playerVelocity.addScaledVector(this.playerVelocity, damping);
168
- const deltaPosition = this.playerVelocity.clone().multiplyScalar(deltaTime);
169
- this.playerCollider.translate(deltaPosition);
170
- this.playerCollisions();
171
- this.camera.position.copy(this.playerCollider.end);
172
- this.camera.rotation.x = clamp(this.camera.rotation.x, degToRad(-89), degToRad(89));
173
- };
174
- // 玩家的碰撞检测
175
- playerCollisions() {
176
- this.playerOnFloor = false;
177
- // 遍历 八叉树 做碰撞检测
178
- const result = this.octree.capsuleIntersect(this.playerCollider);
179
- if (result) {
180
- this.playerOnFloor = result.normal.y > 0;
181
- if (!this.playerOnFloor) {
182
- this.playerVelocity.addScaledVector(result.normal, -result.normal.dot(this.playerVelocity));
183
- }
184
- const { x: preX, y: preY, z: preZ } = this.prePlayerVelocity;
185
- const { x, y, z } = this.playerVelocity;
186
- if (Math.abs(preX + preZ) < 0.1 && Math.abs(x + z) > 0.1) {
187
- this.dispatchEvent({ type: "move_start" });
188
- }
189
- else if (Math.abs(preX + preZ) > 0.1 && Math.abs(x + z) < 0.1) {
190
- this.dispatchEvent({ type: "move_end" });
191
- }
192
- this.prePlayerVelocity.copy(this.playerVelocity);
193
- this.playerCollider.translate(result.normal.multiplyScalar(result.depth));
194
- }
195
- if (this.jumping && !this.prePlayerOnFloor && this.playerOnFloor) {
196
- // 分发落地事件
197
- this.jumping = false;
198
- this.dispatchEvent({ type: "jump_end" });
199
- }
200
- this.prePlayerOnFloor = this.playerOnFloor;
201
- }
202
- // 更新控制器的位置
203
- teleportPlayerIfOob = () => {
204
- if (this.camera.position.y <= -10) {
205
- this.playerCollider.start.set(0, 0.35, 0);
206
- this.playerCollider.end.set(0, 1, 10);
207
- this.playerCollider.radius = 0.35;
208
- this.camera.position.copy(this.playerCollider.end);
209
- // this.camera.position.set(0, 3, 0);
210
- }
211
- };
212
- // 更新
213
- update() {
214
- if (this.enable) {
215
- const deltaTime = Math.min(0.05, this.clock.getDelta()) / this.STEPS_PER_FRAME;
216
- for (let i = 0; i < this.STEPS_PER_FRAME; i++) {
217
- this.controls(deltaTime);
218
- this.updatePlayer(deltaTime);
219
- this.teleportPlayerIfOob();
220
- }
221
- }
222
- }
223
- // 销毁事件
224
- disposer() {
225
- this.enable = false;
226
- document.removeEventListener("keydown", this.keydown);
227
- document.removeEventListener("keyup", this.keyup);
228
- this.canvas.removeEventListener("pointerdown", this.mousedown);
229
- document.removeEventListener("pointerup", this.mouseup);
230
- document.body.removeEventListener("pointermove", this.mousemove);
231
- }
232
- }
1
+ /*
2
+ * @Author: xiaosihan
3
+ * @Date: 2022-05-04 23:01:36
4
+ * @Last Modified by: xiaosihan
5
+ * @Last Modified time: 2024-11-07 21:57:39
6
+ */
7
+ import { BufferGeometry, Clock, EventDispatcher, Euler, Float32BufferAttribute, Group, Line, LineBasicMaterial, Mesh, InstancedMesh, Quaternion, Raycaster, Vector3, } from "three";
8
+ import { Capsule } from "three/examples/jsm/math/Capsule";
9
+ import { clamp, degToRad } from "three/src/math/MathUtils";
10
+ import { acceleratedRaycast, computeBoundsTree } from "three-mesh-bvh";
11
+ // 确保 three-mesh-bvh 补丁已应用(含 InstancedMesh)
12
+ if (Mesh.prototype.raycast !== acceleratedRaycast) {
13
+ Mesh.prototype.raycast = acceleratedRaycast;
14
+ }
15
+ if (InstancedMesh.prototype.raycast !== acceleratedRaycast) {
16
+ InstancedMesh.prototype.raycast = acceleratedRaycast;
17
+ }
18
+ /** 第一/三人称控制器(6 射线 + 迭代推离,userData.fpsCollision 标记碰撞体) */
19
+ export default class FPScontrol extends EventDispatcher {
20
+ canvas;
21
+ /**
22
+ * @param camera 相机
23
+ * @param canvas canvas 元素
24
+ * @param scene 场景,用于射线碰撞检测(会递归检测所有子对象)
25
+ */
26
+ constructor(camera, canvas, scene) {
27
+ super();
28
+ this.camera = camera;
29
+ this.camera.rotation.order = "YXZ";
30
+ this.canvas = canvas;
31
+ this.scene = scene;
32
+ this.init(canvas);
33
+ }
34
+ // 是否启用
35
+ enable = false;
36
+ GRAVITY = 30; // 重力
37
+ STEPS_PER_FRAME = 2; // 每帧碰撞计算的次数
38
+ speed = 1; // 速度
39
+ camera;
40
+ // 移动时是否保持惯性 由外部来配置
41
+ inertia = true;
42
+ // 场景引用(用于射线检测)
43
+ scene;
44
+ // ========== dev / 第三人称 ==========
45
+ // 开发模式:打开后显示射线可视化
46
+ dev = false;
47
+ // 调试模式相机偏移(本地坐标系,Y=上,Z=后)
48
+ devCameraOffset = new Vector3(0, 2.5, 12.5);
49
+ // 第三人称模式
50
+ thirdPerson = false;
51
+ // 第三人称参数:相机与第一人称位置的距离和高度偏移
52
+ thirdPersonDistance = 5;
53
+ thirdPersonHeight = 2;
54
+ // 第三人称轨道角
55
+ _thirdPersonAngleY = 0;
56
+ _thirdPersonAngleX = 0.5;
57
+ // 模式切换检测
58
+ _prevThirdPerson = false;
59
+ // ========== 射线碰撞检测 ==========
60
+ // 碰撞体积(对应 xsh-react-three 的 collideVolume,默认 3,此处缩放为 0.5)
61
+ // 用于水平射线的起点偏移和 far 值
62
+ COLLIDE_VOLUME = 0.5;
63
+ // 垂直碰撞范围 = 从 playerCollider.end(眼睛)到脚底的距离
64
+ // = capsule length + radius = (1.0 - 0.35) + 0.35 = 1.0
65
+ // 对应 xsh-react-three 中 collideVolume(=3)= 控制器到地面的距离
66
+ // xsh 中 far = collideVolume, 响应 = collideVolume - distance
67
+ // 此处 far = VERTICAL_RANGE, 响应 = VERTICAL_RANGE - distance
68
+ VERTICAL_RANGE = 1.0;
69
+ // 推离步长(每次迭代推离的距离,与 xsh-react-three 一致:0.03)
70
+ PUSH_DISTANCE = 0.03;
71
+ // 最大迭代次数(与 xsh-react-three 一致:100)
72
+ MAX_ITERATIONS = 100;
73
+ // 临时四元数(复用避免 GC)
74
+ _tempQuat = new Quaternion();
75
+ // 射线检测器
76
+ _raycaster = new Raycaster();
77
+ // 临时向量(复用避免 GC)
78
+ _tempVec = new Vector3();
79
+ _tempVec2 = new Vector3();
80
+ _tempVec3 = new Vector3();
81
+ _tempVec4 = new Vector3();
82
+ // ========== 射线定义(完全参考 xsh-react-three FPSControls.tsx 的 render 方法)==========
83
+ // 射线定义在局部空间,通过 Group3D(rotation={x: -euler.x}) 补偿 pitch
84
+ // 即射线只跟随 yaw 旋转,不跟随 pitch 俯仰
85
+ //
86
+ // xsh-react-three 原始布局(collideVolume=cv):
87
+ // rightToForwardRay: position={x:cv, y:0, z:0}, direction={x:-cv, y:0, z:-cv}
88
+ // leftToForwardRay: position={x:-cv, y:0, z:0}, direction={x:cv, y:0, z:-cv}
89
+ // leftToBackRay: position={x:-cv, y:0, z:0}, direction={x:cv, y:0, z:cv}
90
+ // rightToBackRay: position={x:cv, y:0, z:0}, direction={x:-cv, y:0, z:cv}
91
+ // upTodwomRay: position={x:0, y:0, z:0}, direction={y:-cv}
92
+ // dwomToUpRay: position={x:0, y:0, z:0}, direction={y:cv}
93
+ //
94
+ // 局部空间射线起点(索引 0-5)
95
+ _localRayOrigins = [
96
+ new Vector3(0, 0, 0), // 0: upTodwomRay(下射线)
97
+ new Vector3(0, 0, 0), // 1: dwomToUpRay(上射线)
98
+ new Vector3(1, 0, 0), // 2: rightToForwardRay 起点(右侧)
99
+ new Vector3(-1, 0, 0), // 3: leftToForwardRay 起点(左侧)
100
+ new Vector3(-1, 0, 0), // 4: leftToBackRay 起点(左侧)
101
+ new Vector3(1, 0, 0), // 5: rightToBackRay 起点(右侧)
102
+ ];
103
+ // 局部空间射线方向(对应 direction prop,归一化前为 {x:±cv, y:0, z:±cv})
104
+ // 归一化后与 xsh 的 clampLength(0,1) 效果一致
105
+ _localRayDirs = [
106
+ new Vector3(0, -1, 0), // 0: 下射线方向
107
+ new Vector3(0, 1, 0), // 1: 上射线方向
108
+ new Vector3(-1, 0, -1).normalize(), // 2: rightToForward → 指向 -X -Z(左前方)
109
+ new Vector3(1, 0, -1).normalize(), // 3: leftToForward → 指向 +X -Z(右前方)
110
+ new Vector3(1, 0, 1).normalize(), // 4: leftToBack → 指向 +X +Z(右后方)
111
+ new Vector3(-1, 0, 1).normalize(), // 5: rightToBack → 指向 -X +Z(左后方)
112
+ ];
113
+ // 世界空间射线起点(复用避免 GC)
114
+ _worldRayOrigins = [0, 1, 2, 3, 4, 5].map(() => new Vector3());
115
+ // 世界空间射线方向(复用避免 GC)
116
+ _worldRayDirs = [0, 1, 2, 3, 4, 5].map(() => new Vector3());
117
+ // userData.fpsCollision === true 的碰撞体列表
118
+ _colliders = [];
119
+ // 缓存标记:场景变化时才重建碰撞体列表
120
+ _collidersDirty = true;
121
+ _colliderSceneVersion = 0;
122
+ // 收集 fpsCollision 物体 + 首次构建 BVH
123
+ _getColliders() {
124
+ if (!this._collidersDirty)
125
+ return;
126
+ this._colliders.length = 0;
127
+ this.scene.traverseVisible((obj) => {
128
+ if (obj.isMesh && obj.userData?.fpsCollision) {
129
+ const geo = obj.geometry;
130
+ if (geo && !geo.boundsTree && geo.attributes.position?.count > 0) {
131
+ computeBoundsTree.apply(geo);
132
+ }
133
+ this._colliders.push(obj);
134
+ }
135
+ });
136
+ this._collidersDirty = false;
137
+ }
138
+ // 标记碰撞体列表需要重建(外部在添加/删除碰撞体时调用)
139
+ markCollidersDirty() {
140
+ this._collidersDirty = true;
141
+ }
142
+ // 玩家胶囊碰撞体
143
+ playerCollider = new Capsule(new Vector3(0, 0.35, 0), new Vector3(0, 1, 0), 0.35);
144
+ // 时钟对象
145
+ clock = new Clock();
146
+ prePlayerVelocity = new Vector3();
147
+ playerVelocity = new Vector3();
148
+ // 起跳中
149
+ jumping = false;
150
+ playerDirection = new Vector3();
151
+ prePlayerOnFloor = false;
152
+ playerOnFloor = false;
153
+ mouseTime = 0;
154
+ isMousedown = false;
155
+ keyStates = {};
156
+ // ========== 射线可视化 ==========
157
+ _debugRayGroup = null;
158
+ // 6 条调试线段:0-3 水平射线,4 向下射线,5 向上射线
159
+ _debugLines = [];
160
+ // 懒初始化调试射线组
161
+ _initDebugLines() {
162
+ if (this._debugRayGroup)
163
+ return;
164
+ this._debugRayGroup = new Group();
165
+ this._debugRayGroup.name = "FPSControl_DebugRays";
166
+ for (let i = 0; i < 6; i++) {
167
+ const geo = new BufferGeometry();
168
+ geo.setAttribute("position", new Float32BufferAttribute(new Float32Array(6), 3));
169
+ geo.setAttribute("color", new Float32BufferAttribute(new Float32Array(6), 3));
170
+ const mat = new LineBasicMaterial({
171
+ vertexColors: true,
172
+ linewidth: 1,
173
+ });
174
+ const line = new Line(geo, mat);
175
+ // 让调试线段不被射线检测命中
176
+ line.raycast = () => { };
177
+ line.frustumCulled = false;
178
+ this._debugLines.push(line);
179
+ this._debugRayGroup.add(line);
180
+ }
181
+ this.scene.add(this._debugRayGroup);
182
+ }
183
+ // 更新调试线段位置 + 颜色
184
+ _updateDebugLine(index, origin, farPoint, hit) {
185
+ const line = this._debugLines[index];
186
+ const pos = line.geometry.attributes.position.array;
187
+ const col = line.geometry.attributes.color.array;
188
+ // 位置:起点 → 终点
189
+ pos[0] = origin.x;
190
+ pos[1] = origin.y;
191
+ pos[2] = origin.z;
192
+ pos[3] = farPoint.x;
193
+ pos[4] = farPoint.y;
194
+ pos[5] = farPoint.z;
195
+ // 颜色渐变:起点(近端)绿色/红色 → 终点白色
196
+ const r = hit ? 1.0 : 0.0;
197
+ const g = hit ? 0.0 : 1.0;
198
+ col[0] = r;
199
+ col[1] = g;
200
+ col[2] = 0.0; // 起点
201
+ col[3] = 1.0;
202
+ col[4] = 1.0;
203
+ col[5] = 1.0; // 终点白色
204
+ line.geometry.attributes.position.needsUpdate = true;
205
+ line.geometry.attributes.color.needsUpdate = true;
206
+ }
207
+ // 销毁调试射线
208
+ _disposeDebugLines() {
209
+ if (!this._debugRayGroup)
210
+ return;
211
+ for (const line of this._debugLines) {
212
+ line.geometry.dispose();
213
+ line.material.dispose();
214
+ }
215
+ this._debugLines = [];
216
+ this.scene.remove(this._debugRayGroup);
217
+ this._debugRayGroup = null;
218
+ }
219
+ init(canvas) {
220
+ this.playerCollider.end.set(0, 1, 0);
221
+ document.addEventListener("keydown", this.keydown);
222
+ document.addEventListener("keyup", this.keyup);
223
+ canvas.addEventListener("pointerdown", this.mousedown);
224
+ document.addEventListener("pointerup", this.mouseup);
225
+ document.body.addEventListener("pointermove", this.mousemove);
226
+ this.enable = true;
227
+ }
228
+ keydown = (event) => {
229
+ this.keyStates[event.code] = true;
230
+ };
231
+ keyup = (event) => {
232
+ this.keyStates[event.code] = false;
233
+ };
234
+ mousedown = () => {
235
+ this.isMousedown = true;
236
+ this.mouseTime = performance.now();
237
+ };
238
+ mouseup = () => {
239
+ this.isMousedown = false;
240
+ };
241
+ mousemove = (event) => {
242
+ if (this.enable && this.isMousedown) {
243
+ if (this.thirdPerson) {
244
+ // 第三人称:旋转轨道角
245
+ this._thirdPersonAngleY -= event.movementX / 500;
246
+ this._thirdPersonAngleX -= event.movementY / 500;
247
+ this._thirdPersonAngleX = clamp(this._thirdPersonAngleX, degToRad(-89), degToRad(89));
248
+ }
249
+ else {
250
+ // 第一人称:直接旋转相机
251
+ this.camera.rotation.y -= event.movementX / 500;
252
+ this.camera.rotation.x -= event.movementY / 500;
253
+ }
254
+ }
255
+ };
256
+ // 获取前进的向量
257
+ getForwardVector = () => {
258
+ if (this.thirdPerson) {
259
+ // 第三人称:前进 = 轨道偏航方向
260
+ this.playerDirection.set(Math.sin(this._thirdPersonAngleY), 0, Math.cos(this._thirdPersonAngleY));
261
+ return this.playerDirection;
262
+ }
263
+ this.camera.getWorldDirection(this.playerDirection);
264
+ this.playerDirection.y = 0;
265
+ this.playerDirection.normalize();
266
+ return this.playerDirection;
267
+ };
268
+ // 获取侧向向量
269
+ getSideVector = () => {
270
+ if (this.thirdPerson) {
271
+ // 第三人称:侧向 = 轨道偏航 ±90°
272
+ this.playerDirection.set(Math.sin(this._thirdPersonAngleY + Math.PI / 2), 0, Math.cos(this._thirdPersonAngleY + Math.PI / 2));
273
+ return this.playerDirection;
274
+ }
275
+ this.camera.getWorldDirection(this.playerDirection);
276
+ this.playerDirection.y = 0;
277
+ this.playerDirection.normalize();
278
+ this.playerDirection.cross(this.camera.up);
279
+ return this.playerDirection;
280
+ };
281
+ // 玩家移动控制器
282
+ controls = (deltaTime) => {
283
+ const speedDelta = deltaTime * (this.playerOnFloor ? 25 : 8) * this.speed;
284
+ if (this.keyStates["KeyW"]) {
285
+ this.playerVelocity.add(this.getForwardVector().multiplyScalar(speedDelta));
286
+ }
287
+ if (this.keyStates["KeyS"]) {
288
+ this.playerVelocity.add(this.getForwardVector().multiplyScalar(-speedDelta));
289
+ }
290
+ if (this.keyStates["KeyA"]) {
291
+ this.playerVelocity.add(this.getSideVector().multiplyScalar(-speedDelta));
292
+ }
293
+ if (this.keyStates["KeyD"]) {
294
+ this.playerVelocity.add(this.getSideVector().multiplyScalar(speedDelta));
295
+ }
296
+ if (this.playerOnFloor &&
297
+ !this.keyStates["KeyW"] &&
298
+ !this.keyStates["KeyS"] &&
299
+ !this.keyStates["KeyA"] &&
300
+ !this.keyStates["KeyD"]) {
301
+ if (!this.inertia) {
302
+ this.playerVelocity.x *= 0.85;
303
+ this.playerVelocity.z *= 0.85;
304
+ }
305
+ }
306
+ if (this.playerOnFloor) {
307
+ if (this.keyStates["Space"]) {
308
+ this.playerVelocity.y = 15;
309
+ this.jumping = true;
310
+ this.dispatchEvent({ type: "jump_start" });
311
+ }
312
+ }
313
+ };
314
+ // 更新玩家的位置
315
+ updatePlayer = (deltaTime) => {
316
+ let damping = Math.exp(-4 * deltaTime) - 1;
317
+ if (!this.playerOnFloor) {
318
+ this.playerVelocity.y -= this.GRAVITY * deltaTime;
319
+ damping *= 0.1;
320
+ }
321
+ this.playerVelocity.addScaledVector(this.playerVelocity, damping);
322
+ const deltaPosition = this.playerVelocity.clone().multiplyScalar(deltaTime);
323
+ this.playerCollider.translate(deltaPosition);
324
+ this.playerCollisions();
325
+ // 设置相机位置(第一人称默认值)
326
+ this.camera.position.copy(this.playerCollider.end);
327
+ // 第三人称:偏移相机位置 + 朝向第一人称坐标
328
+ if (this.thirdPerson) {
329
+ const cosPitch = Math.cos(this._thirdPersonAngleX);
330
+ this.camera.position.x +=
331
+ Math.sin(this._thirdPersonAngleY) *
332
+ cosPitch *
333
+ this.thirdPersonDistance;
334
+ this.camera.position.y +=
335
+ Math.sin(this._thirdPersonAngleX) * this.thirdPersonDistance +
336
+ this.thirdPersonHeight;
337
+ this.camera.position.z +=
338
+ Math.cos(this._thirdPersonAngleY) *
339
+ cosPitch *
340
+ this.thirdPersonDistance;
341
+ this.camera.lookAt(this.playerCollider.end);
342
+ }
343
+ // dev 调试模式:在相机本地坐标系叠加偏移(向后上方)
344
+ if (this.dev && !this.thirdPerson) {
345
+ const offset = this.devCameraOffset.clone().applyQuaternion(this.camera.quaternion);
346
+ this.camera.position.add(offset);
347
+ }
348
+ this.camera.rotation.x = clamp(this.camera.rotation.x, degToRad(-89), degToRad(89));
349
+ };
350
+ // 碰撞检测(完全参考 xsh-react-three FPSControls.tsx 的 computerPosition 方法)
351
+ //
352
+ // 射线布局对比(xsh-react-three 的 Raycaster 组件 → 本地实现):
353
+ // xsh: position prop → _localRayOrigins × collideVolume
354
+ // xsh: direction prop → _localRayDirs(归一化后 = clampLength(0,1))
355
+ // xsh: raycaster.far = getDirection().length() → horizontalFar / verticalFar
356
+ // xsh: Group3D rotation={x:-euler.x} → yawQuat(pitch 补偿)
357
+ // xsh: getIntersetResult(positionIncremental) → worldPos = playerCollider.end + positionIncremental
358
+ //
359
+ // 6条射线完整数据(cv = collideVolume, vr = VERTICAL_RANGE):
360
+ // 0 upTodwomRay: origin=(0,0,0)×cv+worldPos, dir=(0,-1,0)×yaw, far=vr, 响应=vr-distance
361
+ // 1 dwomToUpRay: origin=(0,0,0)×cv+worldPos, dir=(0,1,0)×yaw, far=vr, 响应=faceNormal×0.01
362
+ // 2 rightToForward: origin=(cv,0,0)×yaw+worldPos, dir=(-1,0,-1).norm×yaw, far=cv√2
363
+ // 3 leftToForward: origin=(-cv,0,0)×yaw+worldPos, dir=(1,0,-1).norm×yaw, far=cv√2
364
+ // 4 leftToBack: origin=(-cv,0,0)×yaw+worldPos, dir=(1,0,1).norm×yaw, far=cv√2
365
+ // 5 rightToBack: origin=(cv,0,0)×yaw+worldPos, dir=(-1,0,1).norm×yaw, far=cv√2
366
+ playerCollisions() {
367
+ this.playerOnFloor = false;
368
+ this._getColliders();
369
+ const positionIncremental = this._tempVec4.set(0, 0, 0);
370
+ const collideVolume = this.COLLIDE_VOLUME;
371
+ const verticalRange = this.VERTICAL_RANGE;
372
+ let i = 0;
373
+ let goOnComputer = true;
374
+ // 调试数据:按射线索引存储(0=下, 1=上, 2-5=水平),只在第一次迭代记录
375
+ const devRayData = this.dev ? new Array(6).fill(null) : null;
376
+ let debugRecorded = !this.dev;
377
+ while (goOnComputer && i < 100) {
378
+ goOnComputer = false;
379
+ i++;
380
+ // 与 xsh: object3d.getWorldPosition() + positionIncremental 对应
381
+ const worldPos = this._tempVec3
382
+ .copy(this.playerCollider.end)
383
+ .add(positionIncremental);
384
+ // 与 xsh: Group3D rotation={x:-euler.x} 补偿 pitch → 只跟随 yaw
385
+ const yawQuat = this._tempQuat.setFromEuler(new Euler(0, this.camera.rotation.y, 0, "YXZ"));
386
+ // 将局部射线原点和方向转换为世界空间
387
+ for (let ri = 0; ri < 6; ri++) {
388
+ this._worldRayOrigins[ri]
389
+ .copy(this._localRayOrigins[ri])
390
+ .multiplyScalar(collideVolume)
391
+ .applyQuaternion(yawQuat)
392
+ .add(worldPos);
393
+ this._worldRayDirs[ri]
394
+ .copy(this._localRayDirs[ri])
395
+ .applyQuaternion(yawQuat)
396
+ .normalize();
397
+ }
398
+ // far 值
399
+ const horizontalFar = collideVolume * Math.SQRT2;
400
+ const downFar = verticalRange;
401
+ const upFar = verticalRange;
402
+ // ========== 4 条水平射线(索引 2-5)==========
403
+ for (const ri of [2, 3, 4, 5]) {
404
+ this._raycaster.set(this._worldRayOrigins[ri], this._worldRayDirs[ri]);
405
+ this._raycaster.far = horizontalFar;
406
+ const intersets = this._raycaster.intersectObjects(this._colliders, false);
407
+ // 调试:第一次迭代记录射线数据
408
+ if (!debugRecorded) {
409
+ const hasHit = intersets.length > 0;
410
+ let closestDist = Infinity;
411
+ let intersetPoint = null;
412
+ if (hasHit) {
413
+ for (const int of intersets) {
414
+ const d = int.point.distanceTo(worldPos);
415
+ if (d < closestDist) {
416
+ closestDist = d;
417
+ intersetPoint = int.point;
418
+ }
419
+ }
420
+ }
421
+ devRayData[ri] = {
422
+ origin: this._worldRayOrigins[ri].clone(),
423
+ farPoint: hasHit
424
+ ? intersetPoint.clone()
425
+ : this._worldRayOrigins[ri].clone().addScaledVector(this._worldRayDirs[ri], horizontalFar),
426
+ hit: hasHit,
427
+ };
428
+ }
429
+ if (intersets.length > 0) {
430
+ let interset = intersets[0];
431
+ if (intersets.length > 1) {
432
+ for (let j = 1; j < intersets.length; j++) {
433
+ if (intersets[j].point.distanceTo(worldPos) < interset.point.distanceTo(worldPos)) {
434
+ interset = intersets[j];
435
+ }
436
+ }
437
+ }
438
+ // 与 xsh: object3d.getWorldPosition().sub(interset.point).setLength(0.03) 一致
439
+ this._tempVec
440
+ .copy(worldPos)
441
+ .sub(interset.point)
442
+ .setLength(0.03);
443
+ positionIncremental.add(this._tempVec);
444
+ goOnComputer = true;
445
+ }
446
+ }
447
+ // ========== 向下射线(索引 0 = upTodwomRay)==========
448
+ this._raycaster.set(this._worldRayOrigins[0], this._worldRayDirs[0]);
449
+ this._raycaster.far = downFar;
450
+ let downIntersets = this._raycaster.intersectObjects(this._colliders, false);
451
+ // 调试:记录向下射线
452
+ if (!debugRecorded) {
453
+ const hasHit = downIntersets.length > 0;
454
+ let intersetPoint = null;
455
+ if (hasHit) {
456
+ let closestDist = Infinity;
457
+ for (const int of downIntersets) {
458
+ const d = int.point.distanceTo(worldPos);
459
+ if (d < closestDist) {
460
+ closestDist = d;
461
+ intersetPoint = int.point;
462
+ }
463
+ }
464
+ }
465
+ devRayData[0] = {
466
+ origin: this._worldRayOrigins[0].clone(),
467
+ farPoint: hasHit
468
+ ? intersetPoint.clone()
469
+ : this._worldRayOrigins[0].clone().addScaledVector(this._worldRayDirs[0], downFar),
470
+ hit: hasHit,
471
+ };
472
+ }
473
+ if (downIntersets.length > 0) {
474
+ let interset = downIntersets[0];
475
+ if (downIntersets.length > 1) {
476
+ for (let j = 1; j < downIntersets.length; j++) {
477
+ if (downIntersets[j].point.distanceTo(worldPos) < interset.point.distanceTo(worldPos)) {
478
+ interset = downIntersets[j];
479
+ }
480
+ }
481
+ }
482
+ this.playerVelocity.y = 0;
483
+ positionIncremental.y += verticalRange - interset.distance;
484
+ this.playerOnFloor = true;
485
+ goOnComputer = true;
486
+ }
487
+ // ========== 向上射线(索引 1 = dwomToUpRay)==========
488
+ this._raycaster.set(this._worldRayOrigins[1], this._worldRayDirs[1]);
489
+ this._raycaster.far = upFar;
490
+ let upIntersets = this._raycaster.intersectObjects(this._colliders, false);
491
+ // 调试:记录向上射线
492
+ if (!debugRecorded) {
493
+ const hasHit = upIntersets.length > 0;
494
+ let intersetPoint = null;
495
+ if (hasHit) {
496
+ let closestDist = Infinity;
497
+ for (const int of upIntersets) {
498
+ const d = int.point.distanceTo(worldPos);
499
+ if (d < closestDist) {
500
+ closestDist = d;
501
+ intersetPoint = int.point;
502
+ }
503
+ }
504
+ }
505
+ devRayData[1] = {
506
+ origin: this._worldRayOrigins[1].clone(),
507
+ farPoint: hasHit
508
+ ? intersetPoint.clone()
509
+ : this._worldRayOrigins[1].clone().addScaledVector(this._worldRayDirs[1], upFar),
510
+ hit: hasHit,
511
+ };
512
+ debugRecorded = true;
513
+ }
514
+ if (upIntersets.length > 0) {
515
+ let interset = upIntersets[0];
516
+ if (upIntersets.length > 1) {
517
+ for (let j = 1; j < upIntersets.length; j++) {
518
+ if (upIntersets[j].point.distanceTo(worldPos) < interset.point.distanceTo(worldPos)) {
519
+ interset = upIntersets[j];
520
+ }
521
+ }
522
+ }
523
+ this.playerVelocity.y = 0;
524
+ // 与 xsh: face.worldNormal.setLength(0.01) 一致
525
+ if (interset.face) {
526
+ const faceNormal = interset.face.normal.clone();
527
+ const worldQuat = new Quaternion();
528
+ interset.object.getWorldQuaternion(worldQuat);
529
+ faceNormal.applyQuaternion(worldQuat).setLength(0.01);
530
+ positionIncremental.add(faceNormal);
531
+ }
532
+ else {
533
+ positionIncremental.y -= 0.01;
534
+ }
535
+ goOnComputer = true;
536
+ }
537
+ }
538
+ // ========== 更新调试射线(dev 模式)==========
539
+ if (this.dev && devRayData) {
540
+ this._initDebugLines();
541
+ for (let di = 0; di < 6; di++) {
542
+ const data = devRayData[di];
543
+ if (data) {
544
+ this._updateDebugLine(di, data.origin, data.farPoint, data.hit);
545
+ }
546
+ }
547
+ }
548
+ // 应用推离
549
+ if (positionIncremental.lengthSq() > 0) {
550
+ this.playerCollider.translate(positionIncremental);
551
+ }
552
+ // 事件分发
553
+ if (this.jumping && !this.prePlayerOnFloor && this.playerOnFloor) {
554
+ this.jumping = false;
555
+ this.dispatchEvent({ type: "jump_end" });
556
+ }
557
+ const { x: preX, z: preZ } = this.prePlayerVelocity;
558
+ const { x, z } = this.playerVelocity;
559
+ if (Math.abs(preX + preZ) < 0.1 && Math.abs(x + z) > 0.1) {
560
+ this.dispatchEvent({ type: "move_start" });
561
+ }
562
+ else if (Math.abs(preX + preZ) > 0.1 && Math.abs(x + z) < 0.1) {
563
+ this.dispatchEvent({ type: "move_end" });
564
+ }
565
+ this.prePlayerVelocity.copy(this.playerVelocity);
566
+ this.prePlayerOnFloor = this.playerOnFloor;
567
+ }
568
+ // 越界传送
569
+ teleportPlayerIfOob = () => {
570
+ if (this.camera.position.y <= -10) {
571
+ this.playerCollider.start.set(0, 0.35, 0);
572
+ this.playerCollider.end.set(0, 1, 10);
573
+ this.playerCollider.radius = 0.35;
574
+ this.camera.position.copy(this.playerCollider.end);
575
+ }
576
+ };
577
+ // 更新
578
+ update() {
579
+ if (this.enable) {
580
+ // 第三人称模式切换时同步角度
581
+ if (this.thirdPerson && !this._prevThirdPerson) {
582
+ // 从第一人称切换到第三人称:复制相机旋转角
583
+ this._thirdPersonAngleY = this.camera.rotation.y;
584
+ this._thirdPersonAngleX = this.camera.rotation.x;
585
+ }
586
+ else if (!this.thirdPerson && this._prevThirdPerson) {
587
+ // 从第三人称切换回第一人称:还原相机旋转角
588
+ this.camera.rotation.y = this._thirdPersonAngleY;
589
+ this.camera.rotation.x = this._thirdPersonAngleX;
590
+ }
591
+ this._prevThirdPerson = this.thirdPerson;
592
+ const deltaTime = Math.min(0.05, this.clock.getDelta()) / this.STEPS_PER_FRAME;
593
+ for (let i = 0; i < this.STEPS_PER_FRAME; i++) {
594
+ this.controls(deltaTime);
595
+ this.updatePlayer(deltaTime);
596
+ this.teleportPlayerIfOob();
597
+ }
598
+ }
599
+ // dev 模式关闭时隐藏调试射线
600
+ if (!this.dev && this._debugRayGroup && this._debugRayGroup.visible) {
601
+ this._debugRayGroup.visible = false;
602
+ }
603
+ else if (this.dev && this._debugRayGroup && !this._debugRayGroup.visible) {
604
+ this._debugRayGroup.visible = true;
605
+ }
606
+ }
607
+ // 销毁事件
608
+ disposer() {
609
+ this.enable = false;
610
+ document.removeEventListener("keydown", this.keydown);
611
+ document.removeEventListener("keyup", this.keyup);
612
+ this.canvas.removeEventListener("pointerdown", this.mousedown);
613
+ document.removeEventListener("pointerup", this.mouseup);
614
+ document.body.removeEventListener("pointermove", this.mousemove);
615
+ this._disposeDebugLines();
616
+ }
617
+ }