@pascal-app/editor 0.5.1 → 0.7.0
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/package.json +12 -7
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +75 -7
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +29 -0
- package/src/components/editor/first-person/build-collider-world.ts +365 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +281 -83
- package/src/components/editor/floating-building-action-menu.tsx +4 -3
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +10442 -3275
- package/src/components/editor/index.tsx +270 -20
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/selection-manager.tsx +766 -12
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +350 -157
- package/src/components/editor/use-floorplan-background-placement.ts +257 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
- package/src/components/editor/use-floorplan-scene-data.ts +189 -0
- package/src/components/editor/wall-measurement-label.tsx +377 -58
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
- package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
- package/src/components/editor-2d/svg-paths.ts +119 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +272 -0
- package/src/components/systems/roof/roof-edit-system.tsx +5 -5
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +2 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
- package/src/components/tools/column/column-tool.tsx +97 -0
- package/src/components/tools/column/move-column-tool.tsx +105 -0
- package/src/components/tools/door/door-tool.tsx +19 -0
- package/src/components/tools/door/move-door-tool.tsx +38 -8
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +27 -8
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +438 -0
- package/src/components/tools/fence/move-fence-tool.tsx +102 -27
- package/src/components/tools/item/move-tool.tsx +19 -1
- package/src/components/tools/item/placement-math.ts +44 -7
- package/src/components/tools/item/placement-strategies.ts +111 -33
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +2 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +701 -61
- package/src/components/tools/roof/move-roof-tool.tsx +111 -43
- package/src/components/tools/shared/polygon-editor.tsx +244 -29
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/move-slab-tool.tsx +182 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +2 -0
- package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
- package/src/components/tools/spawn/spawn-tool.tsx +130 -0
- package/src/components/tools/stair/stair-tool.tsx +11 -3
- package/src/components/tools/tool-manager.tsx +30 -3
- package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +423 -0
- package/src/components/tools/wall/move-wall-tool.tsx +356 -0
- package/src/components/tools/wall/wall-drafting.ts +348 -17
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +28 -0
- package/src/components/tools/window/window-tool.tsx +17 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +37 -5
- package/src/components/ui/action-menu/index.tsx +91 -1
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +424 -35
- package/src/components/ui/command-palette/editor-commands.tsx +27 -5
- package/src/components/ui/command-palette/index.tsx +0 -1
- package/src/components/ui/controls/material-picker.tsx +189 -169
- package/src/components/ui/controls/slider-control.tsx +88 -26
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1121 -1219
- package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
- package/src/components/ui/level-duplicate-dialog.tsx +115 -0
- package/src/components/ui/panels/ceiling-panel.tsx +47 -27
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +986 -294
- package/src/components/ui/panels/fence-panel.tsx +55 -12
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
- package/src/components/ui/panels/node-display.ts +39 -0
- package/src/components/ui/panels/paint-panel.tsx +138 -0
- package/src/components/ui/panels/panel-manager.tsx +241 -30
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +243 -9
- package/src/components/ui/panels/roof-panel.tsx +30 -62
- package/src/components/ui/panels/roof-segment-panel.tsx +8 -23
- package/src/components/ui/panels/slab-panel.tsx +46 -24
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +117 -69
- package/src/components/ui/panels/stair-segment-panel.tsx +13 -27
- package/src/components/ui/panels/wall-panel.tsx +71 -17
- package/src/components/ui/panels/window-panel.tsx +665 -146
- package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +138 -56
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +12 -6
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +96 -2
- package/src/components/viewer-overlay.tsx +25 -19
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-contextual-tools.ts +14 -13
- package/src/hooks/use-keyboard.ts +67 -9
- package/src/hooks/use-mobile.ts +12 -12
- package/src/index.tsx +2 -1
- package/src/lib/door-interaction.ts +88 -0
- package/src/lib/floorplan/geometry.ts +263 -0
- package/src/lib/floorplan/index.ts +38 -0
- package/src/lib/floorplan/items.ts +179 -0
- package/src/lib/floorplan/selection-tool.ts +231 -0
- package/src/lib/floorplan/stairs.ts +478 -0
- package/src/lib/floorplan/types.ts +57 -0
- package/src/lib/floorplan/walls.ts +23 -0
- package/src/lib/guide-events.ts +10 -0
- package/src/lib/history.ts +20 -0
- package/src/lib/level-duplication.test.ts +72 -0
- package/src/lib/level-duplication.ts +153 -0
- package/src/lib/local-guide-image.ts +42 -0
- package/src/lib/material-paint.ts +284 -0
- package/src/lib/roof-duplication.ts +214 -0
- package/src/lib/scene-bounds.test.ts +183 -0
- package/src/lib/scene-bounds.ts +169 -0
- package/src/lib/sfx-player.ts +96 -13
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +279 -15
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import '../../../three-types'
|
|
2
|
+
import { TransformControls, useKeyboardControls } from '@react-three/drei'
|
|
3
|
+
import { useFrame, useThree, type ThreeElements } from '@react-three/fiber'
|
|
4
|
+
import {
|
|
5
|
+
Suspense,
|
|
6
|
+
forwardRef,
|
|
7
|
+
useCallback,
|
|
8
|
+
useImperativeHandle,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
} from 'react'
|
|
12
|
+
import type { ReactNode } from 'react'
|
|
13
|
+
import * as THREE from 'three'
|
|
14
|
+
import { clamp } from 'three/src/math/MathUtils.js'
|
|
15
|
+
|
|
16
|
+
export type MovementInput = {
|
|
17
|
+
forward?: boolean
|
|
18
|
+
backward?: boolean
|
|
19
|
+
leftward?: boolean
|
|
20
|
+
rightward?: boolean
|
|
21
|
+
joystick?: { x: number; y: number }
|
|
22
|
+
run?: boolean
|
|
23
|
+
jump?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type CharacterAnimationStatus =
|
|
27
|
+
| 'IDLE'
|
|
28
|
+
| 'WALK'
|
|
29
|
+
| 'RUN'
|
|
30
|
+
| 'JUMP_START'
|
|
31
|
+
| 'JUMP_IDLE'
|
|
32
|
+
| 'JUMP_FALL'
|
|
33
|
+
| 'JUMP_LAND'
|
|
34
|
+
|
|
35
|
+
export type FloatCheckType = 'RAYCAST' | 'SHAPECAST' | 'BOTH'
|
|
36
|
+
|
|
37
|
+
export interface BVHEcctrlApi {
|
|
38
|
+
group: THREE.Group | null
|
|
39
|
+
model: THREE.Group | null
|
|
40
|
+
resetLinVel: () => void
|
|
41
|
+
addLinVel: (v: THREE.Vector3) => void
|
|
42
|
+
setLinVel: (v: THREE.Vector3) => void
|
|
43
|
+
setMovement: (input: MovementInput) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface EcctrlProps extends Omit<ThreeElements['group'], 'ref'> {
|
|
47
|
+
children?: ReactNode
|
|
48
|
+
debug?: boolean
|
|
49
|
+
colliderMeshes?: THREE.Mesh[]
|
|
50
|
+
colliderCapsuleArgs?: [
|
|
51
|
+
radius: number,
|
|
52
|
+
length: number,
|
|
53
|
+
capSegments: number,
|
|
54
|
+
radialSegments: number,
|
|
55
|
+
]
|
|
56
|
+
paused?: boolean
|
|
57
|
+
delay?: number
|
|
58
|
+
gravity?: number
|
|
59
|
+
fallGravityFactor?: number
|
|
60
|
+
maxFallSpeed?: number
|
|
61
|
+
mass?: number
|
|
62
|
+
sleepTimeout?: number
|
|
63
|
+
slowMotionFactor?: number
|
|
64
|
+
turnSpeed?: number
|
|
65
|
+
maxWalkSpeed?: number
|
|
66
|
+
maxRunSpeed?: number
|
|
67
|
+
acceleration?: number
|
|
68
|
+
deceleration?: number
|
|
69
|
+
counterAccFactor?: number
|
|
70
|
+
airDragFactor?: number
|
|
71
|
+
jumpVel?: number
|
|
72
|
+
floatCheckType?: FloatCheckType
|
|
73
|
+
maxSlope?: number
|
|
74
|
+
floatHeight?: number
|
|
75
|
+
floatPullBackHeight?: number
|
|
76
|
+
floatSensorRadius?: number
|
|
77
|
+
floatSpringK?: number
|
|
78
|
+
floatDampingC?: number
|
|
79
|
+
collisionCheckIteration?: number
|
|
80
|
+
collisionPushBackDamping?: number
|
|
81
|
+
collisionPushBackThreshold?: number
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type CharacterStatus = {
|
|
85
|
+
position: THREE.Vector3
|
|
86
|
+
linvel: THREE.Vector3
|
|
87
|
+
quaternion: THREE.Quaternion
|
|
88
|
+
inputDir: THREE.Vector3
|
|
89
|
+
movingDir: THREE.Vector3
|
|
90
|
+
isOnGround: boolean
|
|
91
|
+
isOnMovingPlatform: boolean
|
|
92
|
+
animationStatus: CharacterAnimationStatus
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const characterStatus: CharacterStatus = {
|
|
96
|
+
position: new THREE.Vector3(),
|
|
97
|
+
linvel: new THREE.Vector3(),
|
|
98
|
+
quaternion: new THREE.Quaternion(),
|
|
99
|
+
inputDir: new THREE.Vector3(),
|
|
100
|
+
movingDir: new THREE.Vector3(),
|
|
101
|
+
isOnGround: false,
|
|
102
|
+
isOnMovingPlatform: false,
|
|
103
|
+
animationStatus: 'IDLE',
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const BVHEcctrl = forwardRef<BVHEcctrlApi, EcctrlProps>(
|
|
107
|
+
(
|
|
108
|
+
{
|
|
109
|
+
children,
|
|
110
|
+
debug = false,
|
|
111
|
+
colliderMeshes = [],
|
|
112
|
+
colliderCapsuleArgs = [0.3, 0.6, 4, 8],
|
|
113
|
+
paused = false,
|
|
114
|
+
delay = 1.5,
|
|
115
|
+
gravity = 9.81,
|
|
116
|
+
fallGravityFactor = 4,
|
|
117
|
+
maxFallSpeed = 50,
|
|
118
|
+
mass = 1,
|
|
119
|
+
sleepTimeout = 10,
|
|
120
|
+
slowMotionFactor = 1,
|
|
121
|
+
turnSpeed = 15,
|
|
122
|
+
maxWalkSpeed = 3,
|
|
123
|
+
maxRunSpeed = 5,
|
|
124
|
+
acceleration = 30,
|
|
125
|
+
deceleration = 20,
|
|
126
|
+
counterAccFactor = 0.5,
|
|
127
|
+
airDragFactor = 0.3,
|
|
128
|
+
jumpVel = 5,
|
|
129
|
+
floatCheckType = 'BOTH',
|
|
130
|
+
maxSlope = 1,
|
|
131
|
+
floatHeight = 0.2,
|
|
132
|
+
floatPullBackHeight = 0.25,
|
|
133
|
+
floatSensorRadius = 0.12,
|
|
134
|
+
floatSpringK = 600,
|
|
135
|
+
floatDampingC = 28,
|
|
136
|
+
collisionCheckIteration = 3,
|
|
137
|
+
collisionPushBackDamping = 0.1,
|
|
138
|
+
collisionPushBackThreshold = 0.05,
|
|
139
|
+
...props
|
|
140
|
+
},
|
|
141
|
+
ref,
|
|
142
|
+
) => {
|
|
143
|
+
const { camera } = useThree()
|
|
144
|
+
const capsuleRadius = useMemo(() => colliderCapsuleArgs[0], [colliderCapsuleArgs])
|
|
145
|
+
const capsuleLength = useMemo(() => colliderCapsuleArgs[1], [colliderCapsuleArgs])
|
|
146
|
+
const characterGroupRef = useRef<THREE.Group | null>(null)
|
|
147
|
+
const characterColliderRef = useRef<THREE.Mesh | null>(null)
|
|
148
|
+
const characterModelRef = useRef<THREE.Group | null>(null)
|
|
149
|
+
const debugLineStart = useRef<THREE.Mesh | null>(null)
|
|
150
|
+
const debugLineEnd = useRef<THREE.Mesh | null>(null)
|
|
151
|
+
const debugRaySensorStart = useRef<THREE.Mesh | null>(null)
|
|
152
|
+
const debugRaySensorEnd = useRef<THREE.Mesh | null>(null)
|
|
153
|
+
const standPointRef = useRef<THREE.Mesh | null>(null)
|
|
154
|
+
const lookDirRef = useRef<THREE.Mesh | null>(null)
|
|
155
|
+
const inputDirRef = useRef<THREE.ArrowHelper | null>(null)
|
|
156
|
+
const moveDirRef = useRef<THREE.ArrowHelper | null>(null)
|
|
157
|
+
const elapsedRef = useRef(0)
|
|
158
|
+
|
|
159
|
+
function useIsInsideKeyboardControls() {
|
|
160
|
+
try {
|
|
161
|
+
return !!useKeyboardControls()
|
|
162
|
+
} catch {
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const isInsideKeyboardControls = useIsInsideKeyboardControls()
|
|
168
|
+
const [_, getKeys] = isInsideKeyboardControls ? useKeyboardControls() : [null, null]
|
|
169
|
+
const presetKeys = {
|
|
170
|
+
forward: false,
|
|
171
|
+
backward: false,
|
|
172
|
+
leftward: false,
|
|
173
|
+
rightward: false,
|
|
174
|
+
jump: false,
|
|
175
|
+
run: false,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const upAxis = useRef(new THREE.Vector3(0, 1, 0))
|
|
179
|
+
const localUpAxis = useRef(new THREE.Vector3())
|
|
180
|
+
const gravityDir = useRef(new THREE.Vector3(0, -1, 0))
|
|
181
|
+
const currentLinVel = useRef(new THREE.Vector3())
|
|
182
|
+
const currentLinVelOnPlane = useRef(new THREE.Vector3())
|
|
183
|
+
const isFalling = useRef(false)
|
|
184
|
+
const idleTime = useRef(0)
|
|
185
|
+
const isSleeping = useRef(false)
|
|
186
|
+
const camProjDir = useRef(new THREE.Vector3())
|
|
187
|
+
const camRightDir = useRef(new THREE.Vector3())
|
|
188
|
+
const inputDir = useRef(new THREE.Vector3())
|
|
189
|
+
const inputDirOnPlane = useRef(new THREE.Vector3())
|
|
190
|
+
const movingDir = useRef(new THREE.Vector3())
|
|
191
|
+
const deltaLinVel = useRef(new THREE.Vector3())
|
|
192
|
+
const wantToMoveVel = useRef(new THREE.Vector3())
|
|
193
|
+
const forwardState = useRef(false)
|
|
194
|
+
const backwardState = useRef(false)
|
|
195
|
+
const leftwardState = useRef(false)
|
|
196
|
+
const rightwardState = useRef(false)
|
|
197
|
+
const joystickState = useRef(new THREE.Vector2())
|
|
198
|
+
const runState = useRef(false)
|
|
199
|
+
const jumpState = useRef(false)
|
|
200
|
+
const isOnGround = useRef(false)
|
|
201
|
+
const prevIsOnGround = useRef(false)
|
|
202
|
+
const prevAnimation = useRef<CharacterAnimationStatus>('IDLE')
|
|
203
|
+
const characterModelTargetQuat = useRef(new THREE.Quaternion())
|
|
204
|
+
const characterModelLookMatrix = useRef(new THREE.Matrix4())
|
|
205
|
+
const characterOrigin = useMemo(() => new THREE.Vector3(0, 0, 0), [])
|
|
206
|
+
const contactDepth = useRef(0)
|
|
207
|
+
const contactNormal = useRef(new THREE.Vector3())
|
|
208
|
+
const triContactPoint = useRef(new THREE.Vector3())
|
|
209
|
+
const capsuleContactPoint = useRef(new THREE.Vector3())
|
|
210
|
+
const totalDepth = useRef(0)
|
|
211
|
+
const triangleCount = useRef(0)
|
|
212
|
+
const accumulatedContactNormal = useRef(new THREE.Vector3())
|
|
213
|
+
const accumulatedContactPoint = useRef(new THREE.Vector3())
|
|
214
|
+
const absorbVel = useRef(new THREE.Vector3())
|
|
215
|
+
const pushBackVel = useRef(new THREE.Vector3())
|
|
216
|
+
const characterBbox = useRef(new THREE.Box3())
|
|
217
|
+
const characterSegment = useRef(new THREE.Line3())
|
|
218
|
+
const localCharacterBbox = useRef(new THREE.Box3())
|
|
219
|
+
const localCharacterSegment = useRef(new THREE.Line3())
|
|
220
|
+
const collideInvertMatrix = useRef(new THREE.Matrix4())
|
|
221
|
+
const relativeCollideVel = useRef(new THREE.Vector3())
|
|
222
|
+
const scaledContactRadiusVec = useRef(new THREE.Vector3())
|
|
223
|
+
const deltaDist = useRef(new THREE.Vector3())
|
|
224
|
+
const currSlopeAngle = useRef(0)
|
|
225
|
+
const localMinDistance = useRef(Infinity)
|
|
226
|
+
const localClosestPoint = useRef(new THREE.Vector3())
|
|
227
|
+
const localHitNormal = useRef(new THREE.Vector3())
|
|
228
|
+
const triNormal = useRef(new THREE.Vector3())
|
|
229
|
+
const globalMinDistance = useRef(Infinity)
|
|
230
|
+
const globalClosestPoint = useRef(new THREE.Vector3())
|
|
231
|
+
const triHitPoint = useRef(new THREE.Vector3())
|
|
232
|
+
const segHitPoint = useRef(new THREE.Vector3())
|
|
233
|
+
const floatHitNormal = useRef(new THREE.Vector3())
|
|
234
|
+
const groundFriction = useRef(0.8)
|
|
235
|
+
const floatSensorBbox = useRef(new THREE.Box3())
|
|
236
|
+
const floatSensorBboxExpendPoint = useRef(new THREE.Vector3())
|
|
237
|
+
const floatSensorSegment = useRef(new THREE.Line3())
|
|
238
|
+
const localFloatSensorBbox = useRef(new THREE.Box3())
|
|
239
|
+
const localFloatSensorBboxExpendPoint = useRef(new THREE.Vector3())
|
|
240
|
+
const localFloatSensorSegment = useRef(new THREE.Line3())
|
|
241
|
+
const floatInvertMatrix = useRef(new THREE.Matrix4())
|
|
242
|
+
const floatNormalInverseMatrix = useRef(new THREE.Matrix3())
|
|
243
|
+
const floatNormalMatrix = useRef(new THREE.Matrix3())
|
|
244
|
+
const floatRaycaster = useRef(new THREE.Raycaster())
|
|
245
|
+
const relativeHitPoint = useRef(new THREE.Vector3())
|
|
246
|
+
const totalPlatformDeltaPos = useRef(new THREE.Vector3())
|
|
247
|
+
const isOnMovingPlatform = useRef(false)
|
|
248
|
+
const floatTempPos = useRef(new THREE.Vector3())
|
|
249
|
+
const floatTempQuat = useRef(new THREE.Quaternion())
|
|
250
|
+
const floatTempScale = useRef(new THREE.Vector3())
|
|
251
|
+
const scaledFloatRadiusVec = useRef(new THREE.Vector3())
|
|
252
|
+
const deltaHit = useRef(new THREE.Vector3())
|
|
253
|
+
const rotationDeltaPos = useRef(new THREE.Vector3())
|
|
254
|
+
const yawQuaternion = useRef(new THREE.Quaternion())
|
|
255
|
+
const contactTempPos = useRef(new THREE.Vector3())
|
|
256
|
+
const contactTempQuat = useRef(new THREE.Quaternion())
|
|
257
|
+
const contactTempScale = useRef(new THREE.Vector3())
|
|
258
|
+
|
|
259
|
+
floatRaycaster.current.far = capsuleRadius + floatHeight + floatPullBackHeight
|
|
260
|
+
|
|
261
|
+
const floatRaycastCandidates = useMemo(
|
|
262
|
+
() =>
|
|
263
|
+
colliderMeshes.filter(
|
|
264
|
+
(mesh) => mesh.geometry.boundsTree && !(mesh instanceof THREE.InstancedMesh),
|
|
265
|
+
),
|
|
266
|
+
[colliderMeshes],
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
const applyGravity = useCallback(
|
|
270
|
+
(delta: number) => {
|
|
271
|
+
gravityDir.current.copy(upAxis.current).negate()
|
|
272
|
+
const fallingSpeed = currentLinVel.current.dot(gravityDir.current)
|
|
273
|
+
isFalling.current = fallingSpeed > 0
|
|
274
|
+
if (fallingSpeed < maxFallSpeed) {
|
|
275
|
+
currentLinVel.current.addScaledVector(
|
|
276
|
+
gravityDir.current,
|
|
277
|
+
gravity * (isFalling.current ? fallGravityFactor : 1) * delta,
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
[fallGravityFactor, gravity, maxFallSpeed],
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
const checkCharacterSleep = useCallback(
|
|
285
|
+
(jump: boolean, delta: number) => {
|
|
286
|
+
const moving = currentLinVel.current.lengthSq() > 1e-6
|
|
287
|
+
const platformIsMoving = totalPlatformDeltaPos.current.lengthSq() > 1e-6
|
|
288
|
+
|
|
289
|
+
if (!moving && isOnGround.current && !jump && !isOnMovingPlatform.current && !platformIsMoving) {
|
|
290
|
+
idleTime.current += delta
|
|
291
|
+
if (idleTime.current > sleepTimeout) isSleeping.current = true
|
|
292
|
+
} else {
|
|
293
|
+
idleTime.current = 0
|
|
294
|
+
isSleeping.current = false
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
[sleepTimeout],
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
const setInputDirection = useCallback(
|
|
301
|
+
(dir: {
|
|
302
|
+
forward?: boolean
|
|
303
|
+
backward?: boolean
|
|
304
|
+
leftward?: boolean
|
|
305
|
+
rightward?: boolean
|
|
306
|
+
joystick?: THREE.Vector2
|
|
307
|
+
}) => {
|
|
308
|
+
inputDir.current.set(0, 0, 0)
|
|
309
|
+
|
|
310
|
+
camera.getWorldDirection(camProjDir.current)
|
|
311
|
+
camProjDir.current.projectOnPlane(upAxis.current).normalize()
|
|
312
|
+
camRightDir.current.crossVectors(camProjDir.current, upAxis.current).normalize()
|
|
313
|
+
|
|
314
|
+
if (dir.joystick && dir.joystick.lengthSq() > 0) {
|
|
315
|
+
inputDir.current
|
|
316
|
+
.addScaledVector(camProjDir.current, dir.joystick.y)
|
|
317
|
+
.addScaledVector(camRightDir.current, dir.joystick.x)
|
|
318
|
+
} else {
|
|
319
|
+
if (dir.forward) inputDir.current.add(camProjDir.current)
|
|
320
|
+
if (dir.backward) inputDir.current.sub(camProjDir.current)
|
|
321
|
+
if (dir.leftward) inputDir.current.sub(camRightDir.current)
|
|
322
|
+
if (dir.rightward) inputDir.current.add(camRightDir.current)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
inputDir.current.normalize()
|
|
326
|
+
},
|
|
327
|
+
[camera],
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
const handleCharacterMovement = useCallback(
|
|
331
|
+
(run: boolean, delta: number) => {
|
|
332
|
+
const friction = clamp(groundFriction.current, 0, 1)
|
|
333
|
+
|
|
334
|
+
if (inputDir.current.lengthSq() > 0) {
|
|
335
|
+
if (characterModelRef.current) {
|
|
336
|
+
inputDirOnPlane.current.copy(inputDir.current).projectOnPlane(upAxis.current)
|
|
337
|
+
characterModelLookMatrix.current.lookAt(
|
|
338
|
+
inputDirOnPlane.current,
|
|
339
|
+
characterOrigin,
|
|
340
|
+
upAxis.current,
|
|
341
|
+
)
|
|
342
|
+
characterModelTargetQuat.current.setFromRotationMatrix(characterModelLookMatrix.current)
|
|
343
|
+
characterModelRef.current.quaternion.slerp(characterModelTargetQuat.current, delta * turnSpeed)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const maxSpeed = run ? maxRunSpeed : maxWalkSpeed
|
|
347
|
+
wantToMoveVel.current.copy(inputDir.current).multiplyScalar(maxSpeed)
|
|
348
|
+
const dot = movingDir.current.dot(inputDir.current)
|
|
349
|
+
|
|
350
|
+
deltaLinVel.current.subVectors(wantToMoveVel.current, currentLinVelOnPlane.current)
|
|
351
|
+
deltaLinVel.current.clampLength(
|
|
352
|
+
0,
|
|
353
|
+
(dot <= 0 ? 1 + counterAccFactor : 1) *
|
|
354
|
+
acceleration *
|
|
355
|
+
friction *
|
|
356
|
+
delta *
|
|
357
|
+
(isOnGround.current ? 1 : airDragFactor),
|
|
358
|
+
)
|
|
359
|
+
currentLinVel.current.add(deltaLinVel.current)
|
|
360
|
+
} else if (isOnGround.current) {
|
|
361
|
+
deltaLinVel.current.copy(currentLinVelOnPlane.current).clampLength(0, deceleration * friction * delta)
|
|
362
|
+
currentLinVel.current.sub(deltaLinVel.current)
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
[acceleration, airDragFactor, counterAccFactor, deceleration, maxRunSpeed, maxWalkSpeed, turnSpeed, characterOrigin],
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
const updateSegmentBBox = useCallback(() => {
|
|
369
|
+
if (!characterGroupRef.current) return
|
|
370
|
+
|
|
371
|
+
characterSegment.current.start.set(0, capsuleLength / 2, 0).add(characterGroupRef.current.position)
|
|
372
|
+
characterSegment.current.end.set(0, -capsuleLength / 2, 0).add(characterGroupRef.current.position)
|
|
373
|
+
|
|
374
|
+
characterBbox.current
|
|
375
|
+
.makeEmpty()
|
|
376
|
+
.expandByPoint(characterSegment.current.start)
|
|
377
|
+
.expandByPoint(characterSegment.current.end)
|
|
378
|
+
.expandByScalar(capsuleRadius)
|
|
379
|
+
|
|
380
|
+
floatSensorSegment.current.start.copy(characterSegment.current.end)
|
|
381
|
+
floatSensorSegment.current.end
|
|
382
|
+
.copy(floatSensorSegment.current.start)
|
|
383
|
+
.addScaledVector(gravityDir.current, floatHeight + capsuleRadius)
|
|
384
|
+
floatSensorBboxExpendPoint.current
|
|
385
|
+
.copy(floatSensorSegment.current.end)
|
|
386
|
+
.addScaledVector(gravityDir.current, floatPullBackHeight)
|
|
387
|
+
|
|
388
|
+
floatSensorBbox.current
|
|
389
|
+
.makeEmpty()
|
|
390
|
+
.expandByPoint(floatSensorSegment.current.start)
|
|
391
|
+
.expandByPoint(floatSensorBboxExpendPoint.current)
|
|
392
|
+
.expandByScalar(floatSensorRadius)
|
|
393
|
+
}, [capsuleLength, capsuleRadius, floatHeight, floatPullBackHeight, floatSensorRadius])
|
|
394
|
+
|
|
395
|
+
const collisionCheck = useCallback(
|
|
396
|
+
(mesh: THREE.Mesh, originMatrix: THREE.Matrix4, delta: number) => {
|
|
397
|
+
if (!mesh.visible || !mesh.geometry.boundsTree || mesh.userData.excludeCollisionCheck) return
|
|
398
|
+
|
|
399
|
+
originMatrix.decompose(contactTempPos.current, contactTempQuat.current, contactTempScale.current)
|
|
400
|
+
collideInvertMatrix.current.copy(originMatrix).invert()
|
|
401
|
+
localCharacterSegment.current.copy(characterSegment.current).applyMatrix4(collideInvertMatrix.current)
|
|
402
|
+
|
|
403
|
+
scaledContactRadiusVec.current.set(
|
|
404
|
+
capsuleRadius / contactTempScale.current.x,
|
|
405
|
+
capsuleRadius / contactTempScale.current.y,
|
|
406
|
+
capsuleRadius / contactTempScale.current.z,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
localCharacterBbox.current
|
|
410
|
+
.makeEmpty()
|
|
411
|
+
.expandByPoint(localCharacterSegment.current.start)
|
|
412
|
+
.expandByPoint(localCharacterSegment.current.end)
|
|
413
|
+
localCharacterBbox.current.min.addScaledVector(scaledContactRadiusVec.current, -1)
|
|
414
|
+
localCharacterBbox.current.max.add(scaledContactRadiusVec.current)
|
|
415
|
+
|
|
416
|
+
contactDepth.current = 0
|
|
417
|
+
contactNormal.current.set(0, 0, 0)
|
|
418
|
+
absorbVel.current.set(0, 0, 0)
|
|
419
|
+
pushBackVel.current.set(0, 0, 0)
|
|
420
|
+
totalDepth.current = 0
|
|
421
|
+
triangleCount.current = 0
|
|
422
|
+
accumulatedContactNormal.current.set(0, 0, 0)
|
|
423
|
+
accumulatedContactPoint.current.set(0, 0, 0)
|
|
424
|
+
|
|
425
|
+
mesh.geometry.boundsTree.shapecast({
|
|
426
|
+
intersectsBounds: (box) => box.intersectsBox(localCharacterBbox.current),
|
|
427
|
+
intersectsTriangle: (tri) => {
|
|
428
|
+
tri.closestPointToSegment(
|
|
429
|
+
localCharacterSegment.current,
|
|
430
|
+
triContactPoint.current,
|
|
431
|
+
capsuleContactPoint.current,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
deltaDist.current.copy(triContactPoint.current).sub(capsuleContactPoint.current)
|
|
435
|
+
deltaDist.current.divide(scaledContactRadiusVec.current)
|
|
436
|
+
|
|
437
|
+
if (deltaDist.current.lengthSq() < 1) {
|
|
438
|
+
triContactPoint.current.applyMatrix4(originMatrix)
|
|
439
|
+
capsuleContactPoint.current.applyMatrix4(originMatrix)
|
|
440
|
+
|
|
441
|
+
contactNormal.current
|
|
442
|
+
.copy(capsuleContactPoint.current)
|
|
443
|
+
.sub(triContactPoint.current)
|
|
444
|
+
.normalize()
|
|
445
|
+
contactDepth.current =
|
|
446
|
+
capsuleRadius - capsuleContactPoint.current.distanceTo(triContactPoint.current)
|
|
447
|
+
|
|
448
|
+
accumulatedContactNormal.current.addScaledVector(contactNormal.current, contactDepth.current)
|
|
449
|
+
accumulatedContactPoint.current.add(triContactPoint.current)
|
|
450
|
+
totalDepth.current += contactDepth.current
|
|
451
|
+
triangleCount.current += 1
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
if (triangleCount.current > 0) {
|
|
457
|
+
accumulatedContactNormal.current.normalize()
|
|
458
|
+
accumulatedContactPoint.current.divideScalar(triangleCount.current)
|
|
459
|
+
const avgDepth = totalDepth.current / triangleCount.current
|
|
460
|
+
relativeCollideVel.current.copy(currentLinVel.current)
|
|
461
|
+
const intoSurfaceVel = relativeCollideVel.current.dot(accumulatedContactNormal.current)
|
|
462
|
+
|
|
463
|
+
if (intoSurfaceVel < 0) {
|
|
464
|
+
absorbVel.current
|
|
465
|
+
.copy(accumulatedContactNormal.current)
|
|
466
|
+
.multiplyScalar(-intoSurfaceVel * (1 + (mesh.userData.restitution ?? 0.05)))
|
|
467
|
+
currentLinVel.current.add(absorbVel.current)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (avgDepth > collisionPushBackThreshold) {
|
|
471
|
+
const correction = (collisionPushBackDamping / delta) * avgDepth
|
|
472
|
+
pushBackVel.current.copy(accumulatedContactNormal.current).multiplyScalar(correction)
|
|
473
|
+
currentLinVel.current.add(pushBackVel.current)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
[capsuleRadius, collisionPushBackDamping, collisionPushBackThreshold],
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
const handleCollisionResponse = useCallback(
|
|
481
|
+
(meshes: THREE.Mesh[], delta: number) => {
|
|
482
|
+
if (meshes.length === 0) return
|
|
483
|
+
|
|
484
|
+
for (let iteration = 0; iteration < collisionCheckIteration; iteration += 1) {
|
|
485
|
+
for (const mesh of meshes) {
|
|
486
|
+
collisionCheck(mesh, mesh.matrixWorld, delta)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
[collisionCheck, collisionCheckIteration],
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
const floatingCheck = useCallback(
|
|
494
|
+
(mesh: THREE.Mesh, originMatrix: THREE.Matrix4) => {
|
|
495
|
+
if (!mesh.visible || !mesh.geometry.boundsTree || mesh.userData.excludeFloatHit) return
|
|
496
|
+
|
|
497
|
+
originMatrix.decompose(floatTempPos.current, floatTempQuat.current, floatTempScale.current)
|
|
498
|
+
floatInvertMatrix.current.copy(originMatrix).invert()
|
|
499
|
+
floatNormalInverseMatrix.current.getNormalMatrix(floatInvertMatrix.current)
|
|
500
|
+
floatNormalMatrix.current.getNormalMatrix(originMatrix)
|
|
501
|
+
|
|
502
|
+
localFloatSensorSegment.current.copy(floatSensorSegment.current).applyMatrix4(floatInvertMatrix.current)
|
|
503
|
+
localFloatSensorBboxExpendPoint.current
|
|
504
|
+
.copy(floatSensorBboxExpendPoint.current)
|
|
505
|
+
.applyMatrix4(floatInvertMatrix.current)
|
|
506
|
+
|
|
507
|
+
scaledFloatRadiusVec.current.set(
|
|
508
|
+
floatSensorRadius / floatTempScale.current.x,
|
|
509
|
+
floatSensorRadius / floatTempScale.current.y,
|
|
510
|
+
floatSensorRadius / floatTempScale.current.z,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
localFloatSensorBbox.current
|
|
514
|
+
.makeEmpty()
|
|
515
|
+
.expandByPoint(localFloatSensorSegment.current.start)
|
|
516
|
+
.expandByPoint(localFloatSensorBboxExpendPoint.current)
|
|
517
|
+
localFloatSensorBbox.current.min.addScaledVector(scaledFloatRadiusVec.current, -1)
|
|
518
|
+
localFloatSensorBbox.current.max.add(scaledFloatRadiusVec.current)
|
|
519
|
+
|
|
520
|
+
localMinDistance.current = Infinity
|
|
521
|
+
localClosestPoint.current.set(Infinity, Infinity, Infinity)
|
|
522
|
+
|
|
523
|
+
mesh.geometry.boundsTree.shapecast({
|
|
524
|
+
intersectsBounds: (box) => box.intersectsBox(localFloatSensorBbox.current),
|
|
525
|
+
intersectsTriangle: (tri) => {
|
|
526
|
+
tri.closestPointToSegment(localFloatSensorSegment.current, triHitPoint.current, segHitPoint.current)
|
|
527
|
+
localUpAxis.current.copy(upAxis.current).applyMatrix3(floatNormalInverseMatrix.current).normalize()
|
|
528
|
+
deltaHit.current.subVectors(triHitPoint.current, localFloatSensorSegment.current.start)
|
|
529
|
+
deltaHit.current.divide(scaledFloatRadiusVec.current)
|
|
530
|
+
|
|
531
|
+
const totalLengthSq = deltaHit.current.lengthSq()
|
|
532
|
+
const dot = deltaHit.current.dot(localUpAxis.current)
|
|
533
|
+
const verticalLength = Math.abs(dot) / ((capsuleRadius + floatHeight + floatPullBackHeight) / floatSensorRadius)
|
|
534
|
+
const horizontalLength = Math.sqrt(Math.max(0, totalLengthSq - dot * dot))
|
|
535
|
+
|
|
536
|
+
if (horizontalLength < 1 && verticalLength < 1) {
|
|
537
|
+
tri.getNormal(triNormal.current)
|
|
538
|
+
triNormal.current.applyMatrix3(floatNormalMatrix.current).normalize()
|
|
539
|
+
triHitPoint.current.applyMatrix4(originMatrix)
|
|
540
|
+
|
|
541
|
+
const slopeAngle = triNormal.current.angleTo(upAxis.current)
|
|
542
|
+
if (verticalLength < localMinDistance.current && slopeAngle < maxSlope) {
|
|
543
|
+
localMinDistance.current = verticalLength
|
|
544
|
+
localClosestPoint.current.copy(triHitPoint.current)
|
|
545
|
+
localHitNormal.current.copy(triNormal.current)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
if (localMinDistance.current < globalMinDistance.current) {
|
|
552
|
+
globalMinDistance.current = localMinDistance.current
|
|
553
|
+
globalClosestPoint.current.copy(localClosestPoint.current)
|
|
554
|
+
floatHitNormal.current.copy(localHitNormal.current)
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
[capsuleRadius, floatHeight, floatPullBackHeight, floatSensorRadius, maxSlope],
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
const handleFloatingResponse = useCallback(
|
|
561
|
+
(meshes: THREE.Mesh[], jump: boolean, delta: number) => {
|
|
562
|
+
if (meshes.length === 0) return
|
|
563
|
+
|
|
564
|
+
globalMinDistance.current = Infinity
|
|
565
|
+
globalClosestPoint.current.set(Infinity, Infinity, Infinity)
|
|
566
|
+
floatHitNormal.current.set(0, 1, 0)
|
|
567
|
+
isOnGround.current = false
|
|
568
|
+
totalPlatformDeltaPos.current.set(0, 0, 0)
|
|
569
|
+
isOnMovingPlatform.current = false
|
|
570
|
+
|
|
571
|
+
if (floatCheckType !== 'RAYCAST') {
|
|
572
|
+
for (const mesh of meshes) {
|
|
573
|
+
floatingCheck(mesh, mesh.matrixWorld)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (floatCheckType !== 'SHAPECAST' && floatRaycastCandidates.length > 0 && globalMinDistance.current === Infinity) {
|
|
578
|
+
floatRaycaster.current.ray.origin.copy(floatSensorSegment.current.start)
|
|
579
|
+
floatRaycaster.current.ray.direction.copy(gravityDir.current)
|
|
580
|
+
const hits = floatRaycaster.current.intersectObjects(floatRaycastCandidates, false)
|
|
581
|
+
const hit = hits[0]
|
|
582
|
+
if (hit?.point) {
|
|
583
|
+
globalClosestPoint.current.copy(hit.point)
|
|
584
|
+
if (hit.face) {
|
|
585
|
+
floatHitNormal.current.copy(hit.face.normal).transformDirection(hit.object.matrixWorld).normalize()
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (globalClosestPoint.current.x === Infinity) return
|
|
591
|
+
|
|
592
|
+
relativeHitPoint.current.copy(globalClosestPoint.current).sub(floatSensorSegment.current.start)
|
|
593
|
+
const currentDistance = relativeHitPoint.current.length()
|
|
594
|
+
currSlopeAngle.current = floatHitNormal.current.angleTo(upAxis.current)
|
|
595
|
+
|
|
596
|
+
if (currentDistance < floatHeight + capsuleRadius) {
|
|
597
|
+
isOnGround.current = true
|
|
598
|
+
jump = false
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (!jump) {
|
|
602
|
+
const displacement = floatHeight + capsuleRadius - currentDistance
|
|
603
|
+
const velocityOnHitNormal = currentLinVel.current.dot(floatHitNormal.current)
|
|
604
|
+
const springForce = displacement * floatSpringK
|
|
605
|
+
const dampingForce = -velocityOnHitNormal * floatDampingC
|
|
606
|
+
const totalForce = springForce + dampingForce - mass * gravity
|
|
607
|
+
|
|
608
|
+
currentLinVel.current.addScaledVector(floatHitNormal.current, (totalForce / mass) * delta)
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
[capsuleRadius, floatCheckType, floatDampingC, floatHeight, floatRaycastCandidates, floatSpringK, floatingCheck, gravity, mass],
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
const updateCharacterWithPlatform = useCallback(() => {
|
|
615
|
+
if (!characterGroupRef.current) return
|
|
616
|
+
rotationDeltaPos.current.copy(totalPlatformDeltaPos.current)
|
|
617
|
+
characterGroupRef.current.position.add(rotationDeltaPos.current)
|
|
618
|
+
yawQuaternion.current.setFromUnitVectors(upAxis.current, floatHitNormal.current)
|
|
619
|
+
}, [upAxis])
|
|
620
|
+
|
|
621
|
+
const updateCharacterAnimation = useCallback(
|
|
622
|
+
(run: boolean, jump: boolean): CharacterAnimationStatus => {
|
|
623
|
+
if (prevIsOnGround.current && jump) return 'JUMP_START'
|
|
624
|
+
if (!isOnGround.current && currentLinVel.current.y > 0) return 'JUMP_IDLE'
|
|
625
|
+
if (!isOnGround.current && currentLinVel.current.y <= 0) return 'JUMP_FALL'
|
|
626
|
+
if (!prevIsOnGround.current && isOnGround.current) return 'JUMP_LAND'
|
|
627
|
+
if (inputDir.current.lengthSq() > 0) return run ? 'RUN' : 'WALK'
|
|
628
|
+
return 'IDLE'
|
|
629
|
+
},
|
|
630
|
+
[],
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
const updateCharacterStatus = useCallback(
|
|
634
|
+
(run: boolean, jump: boolean) => {
|
|
635
|
+
characterModelRef.current?.getWorldPosition(characterStatus.position)
|
|
636
|
+
characterModelRef.current?.getWorldQuaternion(characterStatus.quaternion)
|
|
637
|
+
characterStatus.linvel.copy(currentLinVel.current)
|
|
638
|
+
characterStatus.inputDir.copy(inputDir.current)
|
|
639
|
+
characterStatus.movingDir.copy(movingDir.current)
|
|
640
|
+
characterStatus.isOnGround = isOnGround.current
|
|
641
|
+
characterStatus.isOnMovingPlatform = isOnMovingPlatform.current
|
|
642
|
+
characterStatus.animationStatus = updateCharacterAnimation(run, jump)
|
|
643
|
+
prevAnimation.current = characterStatus.animationStatus
|
|
644
|
+
},
|
|
645
|
+
[updateCharacterAnimation],
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
const resetLinVel = useCallback(() => currentLinVel.current.set(0, 0, 0), [])
|
|
649
|
+
const addLinVel = useCallback((velocity: THREE.Vector3) => currentLinVel.current.add(velocity), [])
|
|
650
|
+
const setLinVel = useCallback((velocity: THREE.Vector3) => currentLinVel.current.copy(velocity), [])
|
|
651
|
+
const setMovement = useCallback((movement: MovementInput) => {
|
|
652
|
+
if (movement.forward !== undefined) forwardState.current = movement.forward
|
|
653
|
+
if (movement.backward !== undefined) backwardState.current = movement.backward
|
|
654
|
+
if (movement.leftward !== undefined) leftwardState.current = movement.leftward
|
|
655
|
+
if (movement.rightward !== undefined) rightwardState.current = movement.rightward
|
|
656
|
+
if (movement.joystick) joystickState.current.set(movement.joystick.x, movement.joystick.y)
|
|
657
|
+
if (movement.run !== undefined) runState.current = movement.run
|
|
658
|
+
if (movement.jump !== undefined) jumpState.current = movement.jump
|
|
659
|
+
}, [])
|
|
660
|
+
|
|
661
|
+
useImperativeHandle(
|
|
662
|
+
ref,
|
|
663
|
+
() => ({
|
|
664
|
+
get group() {
|
|
665
|
+
return characterGroupRef.current
|
|
666
|
+
},
|
|
667
|
+
get model() {
|
|
668
|
+
return characterModelRef.current
|
|
669
|
+
},
|
|
670
|
+
resetLinVel,
|
|
671
|
+
addLinVel,
|
|
672
|
+
setLinVel,
|
|
673
|
+
setMovement,
|
|
674
|
+
}),
|
|
675
|
+
[addLinVel, resetLinVel, setLinVel, setMovement],
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
const updateDebugger = useCallback(() => {
|
|
679
|
+
debugLineStart.current?.position.copy(characterSegment.current.start)
|
|
680
|
+
debugLineEnd.current?.position.copy(characterSegment.current.end)
|
|
681
|
+
debugRaySensorStart.current?.position.copy(floatSensorSegment.current.start)
|
|
682
|
+
debugRaySensorEnd.current?.position.copy(floatSensorSegment.current.end)
|
|
683
|
+
standPointRef.current?.position.copy(globalClosestPoint.current)
|
|
684
|
+
if (characterGroupRef.current) {
|
|
685
|
+
lookDirRef.current?.position.copy(characterGroupRef.current.position).addScaledVector(upAxis.current, 0.7)
|
|
686
|
+
}
|
|
687
|
+
lookDirRef.current?.lookAt(lookDirRef.current.position.clone().add(camProjDir.current))
|
|
688
|
+
inputDirRef.current?.position.copy(characterSegment.current.end)
|
|
689
|
+
inputDirRef.current?.setDirection(inputDir.current)
|
|
690
|
+
inputDirRef.current?.setLength(inputDir.current.lengthSq())
|
|
691
|
+
moveDirRef.current?.position.copy(characterSegment.current.end)
|
|
692
|
+
moveDirRef.current?.setDirection(currentLinVel.current)
|
|
693
|
+
moveDirRef.current?.setLength(currentLinVel.current.length() / maxWalkSpeed)
|
|
694
|
+
}, [characterSegment, maxWalkSpeed])
|
|
695
|
+
|
|
696
|
+
useFrame((_, delta) => {
|
|
697
|
+
elapsedRef.current += delta
|
|
698
|
+
if (paused || elapsedRef.current < delay) return
|
|
699
|
+
|
|
700
|
+
const deltaTime = Math.min(1 / 45, delta) * slowMotionFactor
|
|
701
|
+
const keys = isInsideKeyboardControls && getKeys ? getKeys() : presetKeys
|
|
702
|
+
const forward = forwardState.current || keys.forward
|
|
703
|
+
const backward = backwardState.current || keys.backward
|
|
704
|
+
const leftward = leftwardState.current || keys.leftward
|
|
705
|
+
const rightward = rightwardState.current || keys.rightward
|
|
706
|
+
const run = runState.current || keys.run
|
|
707
|
+
const jump = jumpState.current || keys.jump
|
|
708
|
+
|
|
709
|
+
setInputDirection({
|
|
710
|
+
forward,
|
|
711
|
+
backward,
|
|
712
|
+
leftward,
|
|
713
|
+
rightward,
|
|
714
|
+
joystick: joystickState.current,
|
|
715
|
+
})
|
|
716
|
+
handleCharacterMovement(run, deltaTime)
|
|
717
|
+
if (jump && isOnGround.current) currentLinVel.current.y = jumpVel
|
|
718
|
+
movingDir.current.copy(currentLinVel.current).normalize()
|
|
719
|
+
currentLinVelOnPlane.current.copy(currentLinVel.current).projectOnPlane(upAxis.current)
|
|
720
|
+
|
|
721
|
+
checkCharacterSleep(jump, deltaTime)
|
|
722
|
+
if (!isSleeping.current) {
|
|
723
|
+
if (!isOnGround.current) applyGravity(deltaTime)
|
|
724
|
+
|
|
725
|
+
updateSegmentBBox()
|
|
726
|
+
handleCollisionResponse(colliderMeshes, deltaTime)
|
|
727
|
+
handleFloatingResponse(colliderMeshes, jump, deltaTime)
|
|
728
|
+
updateCharacterWithPlatform()
|
|
729
|
+
|
|
730
|
+
if (characterGroupRef.current) {
|
|
731
|
+
characterGroupRef.current.position.addScaledVector(currentLinVel.current, deltaTime)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
updateCharacterStatus(run, jump)
|
|
735
|
+
prevIsOnGround.current = isOnGround.current
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (debug) updateDebugger()
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
return (
|
|
742
|
+
<Suspense fallback={null}>
|
|
743
|
+
<group {...props} ref={characterGroupRef} dispose={null}>
|
|
744
|
+
{debug && (
|
|
745
|
+
<mesh ref={characterColliderRef}>
|
|
746
|
+
<capsuleGeometry args={colliderCapsuleArgs} />
|
|
747
|
+
<meshNormalMaterial wireframe />
|
|
748
|
+
</mesh>
|
|
749
|
+
)}
|
|
750
|
+
<group name="BVHEcctrl-Model" ref={characterModelRef}>
|
|
751
|
+
{children}
|
|
752
|
+
</group>
|
|
753
|
+
</group>
|
|
754
|
+
|
|
755
|
+
{debug && (
|
|
756
|
+
<group>
|
|
757
|
+
<TransformControls object={characterGroupRef.current!} />
|
|
758
|
+
<box3Helper args={[characterBbox.current]} />
|
|
759
|
+
<mesh ref={debugLineStart}>
|
|
760
|
+
<octahedronGeometry args={[0.05, 0]} />
|
|
761
|
+
<meshNormalMaterial />
|
|
762
|
+
</mesh>
|
|
763
|
+
<mesh ref={debugLineEnd}>
|
|
764
|
+
<octahedronGeometry args={[0.05, 0]} />
|
|
765
|
+
<meshNormalMaterial />
|
|
766
|
+
</mesh>
|
|
767
|
+
<box3Helper args={[floatSensorBbox.current]} />
|
|
768
|
+
<mesh ref={debugRaySensorStart}>
|
|
769
|
+
<octahedronGeometry args={[0.1, 0]} />
|
|
770
|
+
<meshBasicMaterial color="yellow" wireframe />
|
|
771
|
+
</mesh>
|
|
772
|
+
<mesh ref={debugRaySensorEnd}>
|
|
773
|
+
<octahedronGeometry args={[0.1, 0]} />
|
|
774
|
+
<meshBasicMaterial color="yellow" wireframe />
|
|
775
|
+
</mesh>
|
|
776
|
+
<mesh ref={lookDirRef} scale={[1, 0.5, 4]}>
|
|
777
|
+
<octahedronGeometry args={[0.1, 0]} />
|
|
778
|
+
<meshNormalMaterial />
|
|
779
|
+
</mesh>
|
|
780
|
+
<arrowHelper ref={inputDirRef} args={[undefined, undefined, undefined, '#00f']} />
|
|
781
|
+
<arrowHelper ref={moveDirRef} args={[undefined, undefined, undefined, '#f00']} />
|
|
782
|
+
<mesh ref={standPointRef}>
|
|
783
|
+
<octahedronGeometry args={[0.12, 0]} />
|
|
784
|
+
<meshBasicMaterial color="red" opacity={0.2} transparent />
|
|
785
|
+
</mesh>
|
|
786
|
+
</group>
|
|
787
|
+
)}
|
|
788
|
+
</Suspense>
|
|
789
|
+
)
|
|
790
|
+
},
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
BVHEcctrl.displayName = 'BVHEcctrl'
|
|
794
|
+
|
|
795
|
+
export default BVHEcctrl
|