minecraft-renderer 0.1.93 → 0.1.94
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/dist/minecraft-renderer.js +64 -64
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +522 -522
- package/package.json +1 -1
- package/src/graphicsBackend/appViewer.ts +4 -3
- package/src/graphicsBackend/types.ts +4 -1
- package/src/lib/worldrendererCommon.ts +2 -2
- package/src/three/entities.ts +302 -33
- package/src/three/entity/EntityMesh.ts +8 -3
- package/src/three/entity/animations.js +37 -25
- package/src/three/entity/animations.test.ts +249 -0
- package/src/three/entity/boatModelRotation.test.ts +29 -0
- package/src/three/entity/boatModelRotation.ts +14 -0
- package/src/three/entity/boatPaddleAnimation.test.ts +108 -0
- package/src/three/entity/boatPaddleAnimation.ts +104 -0
- package/src/three/entity/boatPassengerRendering.ts +20 -0
- package/src/three/entity/boatPassengerRotation.test.ts +166 -0
- package/src/three/entity/boatPassengerRotation.ts +71 -0
- package/src/three/entity/boatRenderSetup.test.ts +236 -0
- package/src/three/entity/boatRenderSetup.ts +214 -0
- package/src/three/entity/entityHeadPose.test.ts +15 -0
- package/src/three/entity/entityHeadPose.ts +22 -0
- package/src/three/entity/horseEntityScale.test.ts +11 -0
- package/src/three/entity/horseEntityScale.ts +9 -0
- package/src/three/entity/horseHeadAnimation.test.ts +159 -0
- package/src/three/entity/horseHeadAnimation.ts +151 -0
- package/src/three/entity/horsePassengerRendering.test.ts +77 -0
- package/src/three/entity/interpolationPolicy.test.ts +259 -0
- package/src/three/entity/interpolationPolicy.ts +187 -0
- package/src/three/entity/localHorseCameraYawLock.test.ts +72 -0
- package/src/three/entity/localVehicleRendering.integration.test.ts +247 -0
- package/src/three/entity/minecartLocalPassengerRendering.test.ts +208 -0
- package/src/three/entity/networkHeadPitchRendering.test.ts +102 -0
- package/src/three/entity/networkHeadPitchRendering.ts +44 -0
- package/src/three/entity/remoteBoatPassengerRotation.integration.test.ts +227 -0
- package/src/three/entity/remoteBoatPassengerRotation.ts +128 -0
- package/src/three/entity/vehiclePassengerProduction.test.ts +214 -0
- package/src/three/entity/vehiclePassengerRendering.ts +56 -0
- package/src/three/entity/vehiclePassengerUpdate.ts +82 -0
- package/src/three/graphicsBackendBase.ts +5 -5
- package/src/three/graphicsBackendOffThread.ts +2 -2
- package/src/three/worldRendererThree.ts +48 -22
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import type * as THREE from 'three'
|
|
3
|
+
import type { PlayerObjectType } from '../../lib/createPlayerObject'
|
|
4
|
+
import { resolveBoatPassengerThirdPersonRotation, shouldApplyBoatPassengerRotation } from './boatPassengerRotation'
|
|
5
|
+
import { getNetworkHeadPitch, restoreGenericRemotePlayerHeadRotation, type NetworkHeadRotationState } from './networkHeadPitchRendering'
|
|
6
|
+
|
|
7
|
+
export type RemoteBoatPassengerEntity = {
|
|
8
|
+
rotation: { y: number; set: (x: number, y: number, z: number) => unknown }
|
|
9
|
+
visible: boolean
|
|
10
|
+
playerObject?: PlayerObjectType
|
|
11
|
+
userData: NetworkHeadRotationState & {
|
|
12
|
+
_passengerVehicleId?: string
|
|
13
|
+
_boatPassengerVehicleId?: string
|
|
14
|
+
_rotTween?: { stop: () => unknown }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type RemoteBoatPassengerVehicle = {
|
|
19
|
+
rotation: { y: number }
|
|
20
|
+
realName?: string
|
|
21
|
+
originalEntity: { name: string }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getAnchoredVehicleId(userData: RemoteBoatPassengerEntity['userData']): string | undefined {
|
|
25
|
+
return userData._passengerVehicleId ?? userData._boatPassengerVehicleId
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function stopRemotePassengerRotationTween(userData: RemoteBoatPassengerEntity['userData']): void {
|
|
29
|
+
userData._rotTween?.stop()
|
|
30
|
+
userData._rotTween = undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function applyRemoteBoatPassengerRotation(params: {
|
|
34
|
+
passenger: RemoteBoatPassengerEntity
|
|
35
|
+
vehicle: RemoteBoatPassengerVehicle
|
|
36
|
+
vehicleYaw: number
|
|
37
|
+
networkHeadYaw: number
|
|
38
|
+
}): void {
|
|
39
|
+
const { passenger, networkHeadYaw, vehicleYaw } = params
|
|
40
|
+
const playerObject = passenger.playerObject
|
|
41
|
+
if (!playerObject) return
|
|
42
|
+
|
|
43
|
+
stopRemotePassengerRotationTween(passenger.userData)
|
|
44
|
+
|
|
45
|
+
const resolved = resolveBoatPassengerThirdPersonRotation({
|
|
46
|
+
cameraYaw: networkHeadYaw,
|
|
47
|
+
cameraPitch: getNetworkHeadPitch(passenger.userData),
|
|
48
|
+
vehicleYaw
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
passenger.rotation.set(0, resolved.bodyYaw, 0)
|
|
52
|
+
playerObject.skin.head.rotation.set(resolved.headPitch, resolved.headYaw, 0)
|
|
53
|
+
passenger.userData._remoteBoatRotationApplied = true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function restoreGenericRemotePassengerRotation(passenger: RemoteBoatPassengerEntity): void {
|
|
57
|
+
const playerObject = passenger.playerObject
|
|
58
|
+
if (!playerObject) return
|
|
59
|
+
|
|
60
|
+
stopRemotePassengerRotationTween(passenger.userData)
|
|
61
|
+
|
|
62
|
+
const networkHeadYaw = passenger.userData._networkHeadYaw
|
|
63
|
+
if (typeof networkHeadYaw === 'number' && Number.isFinite(networkHeadYaw)) {
|
|
64
|
+
passenger.rotation.set(0, networkHeadYaw, 0)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
restoreGenericRemotePlayerHeadRotation(playerObject, passenger.userData)
|
|
68
|
+
passenger.userData._remoteBoatRotationApplied = false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function processRemoteBoatPassengerRotation(params: {
|
|
72
|
+
passenger: RemoteBoatPassengerEntity
|
|
73
|
+
vehicle: RemoteBoatPassengerVehicle | undefined
|
|
74
|
+
syncArmor?: () => void
|
|
75
|
+
}): void {
|
|
76
|
+
const { passenger, vehicle, syncArmor } = params
|
|
77
|
+
if (!passenger.playerObject) return
|
|
78
|
+
|
|
79
|
+
const userData = passenger.userData
|
|
80
|
+
const wasApplied = userData._remoteBoatRotationApplied === true
|
|
81
|
+
const anchoredVehicleId = getAnchoredVehicleId(userData)
|
|
82
|
+
const vehicleName = vehicle?.realName ?? vehicle?.originalEntity.name
|
|
83
|
+
const vehicleYaw = vehicle?.rotation.y
|
|
84
|
+
const gatePasses = shouldApplyBoatPassengerRotation({
|
|
85
|
+
isAnchoredPassenger: anchoredVehicleId != null && vehicle != null,
|
|
86
|
+
vehicleName,
|
|
87
|
+
vehicleYaw
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
if (!gatePasses) {
|
|
91
|
+
if (wasApplied) {
|
|
92
|
+
restoreGenericRemotePassengerRotation(passenger)
|
|
93
|
+
syncArmor?.()
|
|
94
|
+
}
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const networkHeadYaw = userData._networkHeadYaw
|
|
99
|
+
if (typeof networkHeadYaw !== 'number' || !Number.isFinite(networkHeadYaw)) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
applyRemoteBoatPassengerRotation({
|
|
104
|
+
passenger,
|
|
105
|
+
vehicle: vehicle!,
|
|
106
|
+
vehicleYaw: vehicleYaw!,
|
|
107
|
+
networkHeadYaw
|
|
108
|
+
})
|
|
109
|
+
syncArmor?.()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function processRemoteBoatPassengerRotations(params: {
|
|
113
|
+
entities: Record<string, RemoteBoatPassengerEntity | undefined>
|
|
114
|
+
syncArmor: (passenger: RemoteBoatPassengerEntity) => void
|
|
115
|
+
}): void {
|
|
116
|
+
for (const entity of Object.values(params.entities)) {
|
|
117
|
+
if (!entity?.playerObject) continue
|
|
118
|
+
|
|
119
|
+
const anchoredVehicleId = getAnchoredVehicleId(entity.userData)
|
|
120
|
+
const vehicle = anchoredVehicleId != null ? (params.entities[anchoredVehicleId] as RemoteBoatPassengerVehicle | undefined) : undefined
|
|
121
|
+
|
|
122
|
+
processRemoteBoatPassengerRotation({
|
|
123
|
+
passenger: entity,
|
|
124
|
+
vehicle,
|
|
125
|
+
syncArmor: entity.visible ? () => params.syncArmor(entity) : undefined
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import { expect, test } from 'vitest'
|
|
4
|
+
import { SceneOrigin } from '../sceneOrigin'
|
|
5
|
+
import { getBoatPassengerWorldPosition, getMinecartPassengerWorldPosition } from './interpolationPolicy'
|
|
6
|
+
import { anchorVehiclePassengerPosition, shouldSkipLocalPassengerAnchoring } from './vehiclePassengerRendering'
|
|
7
|
+
import { updateVehiclePassengerPositions, type VehiclePassengerSceneEntity, type VehiclePassengerVehicle } from './vehiclePassengerUpdate'
|
|
8
|
+
|
|
9
|
+
const LOCAL_PLAYER_ID = 7
|
|
10
|
+
const REMOTE_PLAYER_ID = 8
|
|
11
|
+
|
|
12
|
+
type PassengerLike = THREE.Group & VehiclePassengerSceneEntity
|
|
13
|
+
|
|
14
|
+
function makePassenger(id: number): PassengerLike {
|
|
15
|
+
const passenger = new THREE.Group() as PassengerLike
|
|
16
|
+
passenger.playerObject = {}
|
|
17
|
+
passenger.originalEntity = { id }
|
|
18
|
+
passenger.userData = {}
|
|
19
|
+
passenger.rotation.y = 0
|
|
20
|
+
return passenger
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeVehicle(id: number, name: string, renderHints: Record<string, unknown>): VehiclePassengerVehicle & THREE.Group {
|
|
24
|
+
const vehicle = new THREE.Group() as VehiclePassengerVehicle & THREE.Group
|
|
25
|
+
vehicle.originalEntity = { id, name, height: name === 'donkey' ? 1.5 : 1.6 }
|
|
26
|
+
vehicle.userData = { renderHints }
|
|
27
|
+
vehicle.realName = name
|
|
28
|
+
vehicle.rotation.y = 0
|
|
29
|
+
return vehicle
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createHarness() {
|
|
33
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
34
|
+
sceneOrigin.update(96, 60, 192)
|
|
35
|
+
|
|
36
|
+
const localPlayer = makePassenger(LOCAL_PLAYER_ID)
|
|
37
|
+
sceneOrigin.track(localPlayer)
|
|
38
|
+
|
|
39
|
+
const entities: Record<string, VehiclePassengerSceneEntity | VehiclePassengerVehicle> = {}
|
|
40
|
+
const cameraWorldPos = { x: 100, y: 65.62, z: 200 }
|
|
41
|
+
|
|
42
|
+
const runUpdate = () =>
|
|
43
|
+
updateVehiclePassengerPositions({
|
|
44
|
+
entities,
|
|
45
|
+
localPlayer,
|
|
46
|
+
getWorldPosition: target => sceneOrigin.getWorldPosition(target as THREE.Object3D)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
return { sceneOrigin, localPlayer, entities, cameraWorldPos, runUpdate }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test('shouldSkipLocalPassengerAnchoring keeps horse on camera path', () => {
|
|
53
|
+
expect(shouldSkipLocalPassengerAnchoring('boat')).toBe(false)
|
|
54
|
+
expect(shouldSkipLocalPassengerAnchoring('minecart')).toBe(false)
|
|
55
|
+
expect(shouldSkipLocalPassengerAnchoring('horse')).toBe(true)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('updateVehiclePassengerPositions anchors local boat and minecart passengers', () => {
|
|
59
|
+
const { sceneOrigin, localPlayer, entities, runUpdate } = createHarness()
|
|
60
|
+
|
|
61
|
+
const boat = makeVehicle(10, 'oak_boat', { passengerLayout: 'boat', passengerIds: [LOCAL_PLAYER_ID] })
|
|
62
|
+
sceneOrigin.track(boat)
|
|
63
|
+
boat.position.set(100, 64, 200)
|
|
64
|
+
entities['10'] = boat
|
|
65
|
+
|
|
66
|
+
runUpdate()
|
|
67
|
+
|
|
68
|
+
const expectedBoatSeat = getBoatPassengerWorldPosition(sceneOrigin.getWorldPosition(boat)!, boat.rotation.y, 0, 1)
|
|
69
|
+
expect(localPlayer.userData._passengerVehicleId).toBe('10')
|
|
70
|
+
expect(sceneOrigin.getWorldPosition(localPlayer)).toEqual(expectedBoatSeat)
|
|
71
|
+
|
|
72
|
+
const minecart = makeVehicle(42, 'minecart', { passengerLayout: 'minecart', passengerIds: [LOCAL_PLAYER_ID] })
|
|
73
|
+
sceneOrigin.track(minecart)
|
|
74
|
+
minecart.position.set(103, 64.5, 205)
|
|
75
|
+
entities['42'] = minecart
|
|
76
|
+
boat.userData.renderHints = { passengerLayout: 'boat', passengerIds: [] }
|
|
77
|
+
|
|
78
|
+
runUpdate()
|
|
79
|
+
|
|
80
|
+
const expectedMinecartSeat = getMinecartPassengerWorldPosition(sceneOrigin.getWorldPosition(minecart)!)
|
|
81
|
+
expect(localPlayer.userData._passengerVehicleId).toBe('42')
|
|
82
|
+
expect(sceneOrigin.getWorldPosition(localPlayer)).toEqual(expectedMinecartSeat)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('updateVehiclePassengerPositions keeps local horse on existing skip path', () => {
|
|
86
|
+
const { sceneOrigin, localPlayer, entities, cameraWorldPos, runUpdate } = createHarness()
|
|
87
|
+
const eyeHeight = 1.62
|
|
88
|
+
localPlayer.position.set(cameraWorldPos.x, cameraWorldPos.y - eyeHeight, cameraWorldPos.z)
|
|
89
|
+
|
|
90
|
+
const horse = makeVehicle(42, 'horse', {
|
|
91
|
+
passengerLayout: 'horse',
|
|
92
|
+
passengerIds: [LOCAL_PLAYER_ID],
|
|
93
|
+
localVehicle: true,
|
|
94
|
+
localVehicleVerticalCameraLock: 'horse'
|
|
95
|
+
})
|
|
96
|
+
sceneOrigin.track(horse)
|
|
97
|
+
horse.position.set(100, 64, 200)
|
|
98
|
+
entities['42'] = horse
|
|
99
|
+
|
|
100
|
+
runUpdate()
|
|
101
|
+
|
|
102
|
+
expect(localPlayer.userData._passengerVehicleId).toBeUndefined()
|
|
103
|
+
expect(sceneOrigin.getWorldPosition(localPlayer)?.y).toBeCloseTo(cameraWorldPos.y - eyeHeight, 5)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('local and remote boat passengers use the same seat algorithm per index', () => {
|
|
107
|
+
const { sceneOrigin, localPlayer, entities, runUpdate } = createHarness()
|
|
108
|
+
const remotePlayer = makePassenger(REMOTE_PLAYER_ID)
|
|
109
|
+
sceneOrigin.track(remotePlayer)
|
|
110
|
+
entities[String(REMOTE_PLAYER_ID)] = remotePlayer
|
|
111
|
+
|
|
112
|
+
const boat = makeVehicle(10, 'oak_boat', { passengerLayout: 'boat', passengerIds: [LOCAL_PLAYER_ID, REMOTE_PLAYER_ID] })
|
|
113
|
+
sceneOrigin.track(boat)
|
|
114
|
+
boat.position.set(100, 64, 200)
|
|
115
|
+
entities['10'] = boat
|
|
116
|
+
|
|
117
|
+
runUpdate()
|
|
118
|
+
|
|
119
|
+
const vehicleWorldPos = sceneOrigin.getWorldPosition(boat)!
|
|
120
|
+
const localWorld = sceneOrigin.getWorldPosition(localPlayer)!
|
|
121
|
+
const remoteWorld = sceneOrigin.getWorldPosition(remotePlayer)!
|
|
122
|
+
expect(localWorld).toEqual(getBoatPassengerWorldPosition(vehicleWorldPos, boat.rotation.y, 0, 2))
|
|
123
|
+
expect(remoteWorld).toEqual(getBoatPassengerWorldPosition(vehicleWorldPos, boat.rotation.y, 1, 2))
|
|
124
|
+
expect(localWorld.y).toBeCloseTo(63.55, 5)
|
|
125
|
+
expect(remoteWorld.y).toBeCloseTo(63.55, 5)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('vehicle movement updates body anchor without horizontal jitter across frames', () => {
|
|
129
|
+
const { sceneOrigin, localPlayer, entities, runUpdate } = createHarness()
|
|
130
|
+
|
|
131
|
+
const boat = makeVehicle(10, 'oak_boat', { passengerLayout: 'boat', passengerIds: [LOCAL_PLAYER_ID], localVehicle: true })
|
|
132
|
+
sceneOrigin.track(boat)
|
|
133
|
+
entities['10'] = boat
|
|
134
|
+
|
|
135
|
+
const xs = [100, 100.4, 100.9, 101.1, 100.8]
|
|
136
|
+
const deltas: number[] = []
|
|
137
|
+
let previousX: number | undefined
|
|
138
|
+
|
|
139
|
+
for (const x of xs) {
|
|
140
|
+
boat.position.set(x, 64, 200)
|
|
141
|
+
runUpdate()
|
|
142
|
+
const world = sceneOrigin.getWorldPosition(localPlayer)!
|
|
143
|
+
if (previousX !== undefined) {
|
|
144
|
+
deltas.push(world.x - previousX)
|
|
145
|
+
}
|
|
146
|
+
previousX = world.x
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const delta of deltas) {
|
|
150
|
+
expect(delta).toBeCloseTo(Math.round(delta * 10) / 10, 5)
|
|
151
|
+
}
|
|
152
|
+
expect(deltas.map(delta => Math.round(delta * 10) / 10)).toEqual([0.4, 0.5, 0.2, -0.3])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('dismount and vehicle removal release local passenger anchor', () => {
|
|
156
|
+
const { sceneOrigin, localPlayer, entities, cameraWorldPos, runUpdate } = createHarness()
|
|
157
|
+
const eyeHeight = 1.62
|
|
158
|
+
localPlayer.position.set(cameraWorldPos.x, cameraWorldPos.y - eyeHeight, cameraWorldPos.z)
|
|
159
|
+
|
|
160
|
+
const boat = makeVehicle(10, 'oak_boat', { passengerLayout: 'boat', passengerIds: [LOCAL_PLAYER_ID] })
|
|
161
|
+
sceneOrigin.track(boat)
|
|
162
|
+
boat.position.set(100, 64, 200)
|
|
163
|
+
entities['10'] = boat
|
|
164
|
+
|
|
165
|
+
runUpdate()
|
|
166
|
+
expect(localPlayer.userData._passengerVehicleId).toBe('10')
|
|
167
|
+
|
|
168
|
+
boat.userData.renderHints = { passengerLayout: 'boat', passengerIds: [] }
|
|
169
|
+
runUpdate()
|
|
170
|
+
expect(localPlayer.userData._passengerVehicleId).toBeUndefined()
|
|
171
|
+
|
|
172
|
+
localPlayer.position.set(cameraWorldPos.x, cameraWorldPos.y - eyeHeight, cameraWorldPos.z)
|
|
173
|
+
const releasedWorld = sceneOrigin.getWorldPosition(localPlayer)!
|
|
174
|
+
expect(releasedWorld.y).toBeCloseTo(cameraWorldPos.y - eyeHeight, 5)
|
|
175
|
+
|
|
176
|
+
delete entities['10']
|
|
177
|
+
runUpdate()
|
|
178
|
+
expect(localPlayer.userData._passengerVehicleId).toBeUndefined()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('scene origin rebase preserves passenger world position', () => {
|
|
182
|
+
const { sceneOrigin, localPlayer, entities, runUpdate } = createHarness()
|
|
183
|
+
|
|
184
|
+
const boat = makeVehicle(10, 'oak_boat', { passengerLayout: 'boat', passengerIds: [LOCAL_PLAYER_ID] })
|
|
185
|
+
sceneOrigin.track(boat)
|
|
186
|
+
boat.position.set(100, 64, 200)
|
|
187
|
+
entities['10'] = boat
|
|
188
|
+
|
|
189
|
+
runUpdate()
|
|
190
|
+
const beforeRebase = { ...sceneOrigin.getWorldPosition(localPlayer)! }
|
|
191
|
+
|
|
192
|
+
sceneOrigin.update(112, 70, 208)
|
|
193
|
+
runUpdate()
|
|
194
|
+
const afterRebase = sceneOrigin.getWorldPosition(localPlayer)!
|
|
195
|
+
|
|
196
|
+
expect(afterRebase).toEqual(beforeRebase)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('non-finite vehicle position does not create passenger anchor', () => {
|
|
200
|
+
const { localPlayer, entities, runUpdate } = createHarness()
|
|
201
|
+
|
|
202
|
+
const boat = makeVehicle(10, 'oak_boat', { passengerLayout: 'boat', passengerIds: [LOCAL_PLAYER_ID] })
|
|
203
|
+
boat.position.set(Number.NaN, 64, 200)
|
|
204
|
+
entities['10'] = boat
|
|
205
|
+
|
|
206
|
+
runUpdate()
|
|
207
|
+
expect(localPlayer.userData._passengerVehicleId).toBeUndefined()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('anchorVehiclePassengerPosition ignores non-finite coordinates', () => {
|
|
211
|
+
const passenger = makePassenger(LOCAL_PLAYER_ID)
|
|
212
|
+
anchorVehiclePassengerPosition(passenger, { x: 1, y: Number.NaN, z: 2 }, '10')
|
|
213
|
+
expect(passenger.userData._passengerVehicleId).toBeUndefined()
|
|
214
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import type { Vec3Like } from './interpolationPolicy'
|
|
3
|
+
|
|
4
|
+
export type PassengerLayout = 'boat' | 'minecart' | 'horse'
|
|
5
|
+
|
|
6
|
+
export function shouldSkipLocalPassengerAnchoring(layout: PassengerLayout): boolean {
|
|
7
|
+
return layout === 'horse'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isFiniteVec3(value: Vec3Like | undefined | null): value is Vec3Like {
|
|
11
|
+
return value != null && Number.isFinite(value.x) && Number.isFinite(value.y) && Number.isFinite(value.z)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type VehiclePassengerRenderState = {
|
|
15
|
+
position: {
|
|
16
|
+
set: (x: number, y: number, z: number) => unknown
|
|
17
|
+
}
|
|
18
|
+
userData: {
|
|
19
|
+
_posTween?: { stop: () => unknown }
|
|
20
|
+
_tweenTarget?: Vec3Like
|
|
21
|
+
_passengerVehicleId?: string
|
|
22
|
+
/** @deprecated Use _passengerVehicleId */
|
|
23
|
+
_boatPassengerVehicleId?: string
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function anchorVehiclePassengerPosition(passenger: VehiclePassengerRenderState, passengerWorldPos: Vec3Like, vehicleId: string): void {
|
|
28
|
+
if (!isFiniteVec3(passengerWorldPos)) return
|
|
29
|
+
passenger.userData._posTween?.stop()
|
|
30
|
+
passenger.userData._posTween = undefined
|
|
31
|
+
passenger.userData._tweenTarget ??= { ...passengerWorldPos }
|
|
32
|
+
Object.assign(passenger.userData._tweenTarget, passengerWorldPos)
|
|
33
|
+
passenger.position.set(passengerWorldPos.x, passengerWorldPos.y, passengerWorldPos.z)
|
|
34
|
+
passenger.userData._passengerVehicleId = vehicleId
|
|
35
|
+
delete passenger.userData._boatPassengerVehicleId
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function releaseVehiclePassengerPosition(passenger: VehiclePassengerRenderState, vehicleId: string, currentWorldPos: Vec3Like | undefined): boolean {
|
|
39
|
+
const anchoredVehicleId = passenger.userData._passengerVehicleId ?? passenger.userData._boatPassengerVehicleId
|
|
40
|
+
if (anchoredVehicleId !== vehicleId) return false
|
|
41
|
+
passenger.userData._posTween?.stop()
|
|
42
|
+
passenger.userData._posTween = undefined
|
|
43
|
+
if (currentWorldPos) {
|
|
44
|
+
passenger.userData._tweenTarget ??= { ...currentWorldPos }
|
|
45
|
+
Object.assign(passenger.userData._tweenTarget, currentWorldPos)
|
|
46
|
+
}
|
|
47
|
+
delete passenger.userData._passengerVehicleId
|
|
48
|
+
delete passenger.userData._boatPassengerVehicleId
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @deprecated Use anchorVehiclePassengerPosition */
|
|
53
|
+
export const anchorBoatPassengerPosition = anchorVehiclePassengerPosition
|
|
54
|
+
|
|
55
|
+
/** @deprecated Use releaseVehiclePassengerPosition */
|
|
56
|
+
export const releaseBoatPassengerPosition = releaseVehiclePassengerPosition
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { isBoatEntityName } from './boatModelRotation'
|
|
3
|
+
import {
|
|
4
|
+
getBoatPassengerWorldPosition,
|
|
5
|
+
getHorsePassengerWorldPosition,
|
|
6
|
+
getMinecartPassengerWorldPosition,
|
|
7
|
+
isRideableHorseEntityName,
|
|
8
|
+
isRideableMinecartEntityName,
|
|
9
|
+
type EntityRenderHints,
|
|
10
|
+
type Vec3Like
|
|
11
|
+
} from './interpolationPolicy'
|
|
12
|
+
import {
|
|
13
|
+
anchorVehiclePassengerPosition,
|
|
14
|
+
isFiniteVec3,
|
|
15
|
+
releaseVehiclePassengerPosition,
|
|
16
|
+
shouldSkipLocalPassengerAnchoring,
|
|
17
|
+
type VehiclePassengerRenderState
|
|
18
|
+
} from './vehiclePassengerRendering'
|
|
19
|
+
|
|
20
|
+
export type VehiclePassengerSceneEntity = VehiclePassengerRenderState & {
|
|
21
|
+
playerObject?: unknown
|
|
22
|
+
originalEntity: { id: number }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type VehiclePassengerVehicle = {
|
|
26
|
+
originalEntity: { id: number | string; name: string; height?: number }
|
|
27
|
+
userData: { renderHints?: EntityRenderHints }
|
|
28
|
+
rotation: { y: number }
|
|
29
|
+
realName?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function updateVehiclePassengerPositions(args: {
|
|
33
|
+
entities: Record<string, VehiclePassengerSceneEntity | VehiclePassengerVehicle>
|
|
34
|
+
localPlayer: VehiclePassengerSceneEntity | null
|
|
35
|
+
getWorldPosition: (target: unknown) => Vec3Like | undefined
|
|
36
|
+
}): Set<VehiclePassengerSceneEntity> {
|
|
37
|
+
const attachedPassengers = new Set<VehiclePassengerSceneEntity>()
|
|
38
|
+
|
|
39
|
+
for (const entity of Object.values(args.entities)) {
|
|
40
|
+
const vehicle = entity as VehiclePassengerVehicle
|
|
41
|
+
const renderHints = vehicle.userData.renderHints
|
|
42
|
+
const passengerIds = renderHints?.passengerIds ?? renderHints?.boatPassengerIds
|
|
43
|
+
if (!Array.isArray(passengerIds) || passengerIds.length === 0) continue
|
|
44
|
+
|
|
45
|
+
const vehicleName = vehicle.realName ?? vehicle.originalEntity.name
|
|
46
|
+
const layout = renderHints?.passengerLayout ?? (isBoatEntityName(vehicleName) ? 'boat' : undefined)
|
|
47
|
+
if (layout !== 'boat' && layout !== 'minecart' && layout !== 'horse') continue
|
|
48
|
+
if (layout === 'boat' && !isBoatEntityName(vehicleName)) continue
|
|
49
|
+
if (layout === 'minecart' && !isRideableMinecartEntityName(vehicleName)) continue
|
|
50
|
+
if (layout === 'horse' && !isRideableHorseEntityName(vehicleName)) continue
|
|
51
|
+
|
|
52
|
+
const vehicleWorldPos = args.getWorldPosition(vehicle)
|
|
53
|
+
if (!isFiniteVec3(vehicleWorldPos)) continue
|
|
54
|
+
const vehicleId = String(vehicle.originalEntity.id)
|
|
55
|
+
|
|
56
|
+
for (const [passengerIndex, passengerId] of passengerIds.entries()) {
|
|
57
|
+
const isLocalPassenger = passengerId === args.localPlayer?.originalEntity.id
|
|
58
|
+
const passenger = isLocalPassenger ? args.localPlayer : (args.entities[passengerId] as VehiclePassengerSceneEntity | undefined)
|
|
59
|
+
if (!passenger?.playerObject) continue
|
|
60
|
+
if (passenger === args.localPlayer && shouldSkipLocalPassengerAnchoring(layout)) continue
|
|
61
|
+
|
|
62
|
+
const passengerWorldPos =
|
|
63
|
+
layout === 'minecart'
|
|
64
|
+
? getMinecartPassengerWorldPosition(vehicleWorldPos)
|
|
65
|
+
: layout === 'horse'
|
|
66
|
+
? getHorsePassengerWorldPosition(vehicleWorldPos, vehicleName, vehicle.originalEntity.height ?? 1.6)
|
|
67
|
+
: getBoatPassengerWorldPosition(vehicleWorldPos, vehicle.rotation.y, passengerIndex, passengerIds.length)
|
|
68
|
+
|
|
69
|
+
if (!isFiniteVec3(passengerWorldPos)) continue
|
|
70
|
+
anchorVehiclePassengerPosition(passenger, passengerWorldPos, vehicleId)
|
|
71
|
+
attachedPassengers.add(passenger)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const passenger of [...Object.values(args.entities), ...(args.localPlayer ? [args.localPlayer] : [])] as VehiclePassengerSceneEntity[]) {
|
|
76
|
+
const vehicleId = passenger.userData._passengerVehicleId ?? (passenger.userData._boatPassengerVehicleId as string | undefined)
|
|
77
|
+
if (vehicleId === undefined || attachedPassengers.has(passenger)) continue
|
|
78
|
+
releaseVehiclePassengerPosition(passenger, vehicleId, args.getWorldPosition(passenger))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return attachedPassengers
|
|
82
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import * as THREE from 'three'
|
|
10
10
|
import { Vec3 } from 'vec3'
|
|
11
|
-
import type { GraphicsBackend, GraphicsInitOptions, DisplayWorldOptions } from '../graphicsBackend'
|
|
11
|
+
import type { GraphicsBackend, GraphicsInitOptions, DisplayWorldOptions, UpdateCameraOptions } from '../graphicsBackend'
|
|
12
12
|
import { createWorkerProxy, restoreTransferred } from '../lib/workerProxy'
|
|
13
13
|
import { ResourcesManager } from '../resourcesManager'
|
|
14
14
|
import { FrameTimingCollector } from '../lib/frameTimingCollector'
|
|
@@ -280,10 +280,10 @@ export const createGraphicsBackendBase = () => {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
}),
|
|
283
|
-
updateCamera(pos: Vec3 | null, yaw: number, pitch: number) {
|
|
283
|
+
updateCamera(pos: Vec3 | null, yaw: number, pitch: number, options?: UpdateCameraOptions) {
|
|
284
284
|
// Mark camera update event for frame timing visualization
|
|
285
285
|
frameTimingCollector?.markCameraUpdate(!pos)
|
|
286
|
-
worldRenderer?.setFirstPersonCamera(pos, yaw, pitch)
|
|
286
|
+
worldRenderer?.setFirstPersonCamera(pos, yaw, pitch, options)
|
|
287
287
|
},
|
|
288
288
|
get soundSystem() {
|
|
289
289
|
return worldRenderer?.soundSystem
|
|
@@ -311,10 +311,10 @@ export const createGraphicsBackendBase = () => {
|
|
|
311
311
|
startWorld,
|
|
312
312
|
disconnect,
|
|
313
313
|
setRendering: backend.setRendering,
|
|
314
|
-
updateCamera(pos, yaw, pitch) {
|
|
314
|
+
updateCamera(pos, yaw, pitch, options?: UpdateCameraOptions) {
|
|
315
315
|
const posVec = pos ? new Vec3(pos.x, pos.y, pos.z) : null
|
|
316
316
|
frameTimingCollector?.markCameraUpdate(!posVec)
|
|
317
|
-
backend.updateCamera(posVec, yaw, pitch)
|
|
317
|
+
backend.updateCamera(posVec, yaw, pitch, options)
|
|
318
318
|
},
|
|
319
319
|
async callBackendMethod<K extends keyof ThreeJsBackendMethods>(
|
|
320
320
|
method: K,
|
|
@@ -154,8 +154,8 @@ export const createGraphicsBackendOffThread: GraphicsBackendLoader = async initO
|
|
|
154
154
|
setRendering(rendering) {
|
|
155
155
|
proxy.setRendering(rendering)
|
|
156
156
|
},
|
|
157
|
-
updateCamera(pos, yaw, pitch) {
|
|
158
|
-
proxy.updateCamera(pos ? { x: pos.x, y: pos.y, z: pos.z } : null, yaw, pitch)
|
|
157
|
+
updateCamera(pos, yaw, pitch, options) {
|
|
158
|
+
proxy.updateCamera(pos ? { x: pos.x, y: pos.y, z: pos.z } : null, yaw, pitch, options)
|
|
159
159
|
},
|
|
160
160
|
soundSystem: undefined,
|
|
161
161
|
backendMethods: backendMethodsProxy
|
|
@@ -16,11 +16,18 @@ import { getMyHand } from './hand'
|
|
|
16
16
|
import { createHoldingBlock } from './holdingBlockFactory'
|
|
17
17
|
import type { IHoldingBlock } from './holdingBlockTypes'
|
|
18
18
|
import { getMesh } from './entity/EntityMesh'
|
|
19
|
+
import { resolveEntityHeadPose } from './entity/entityHeadPose'
|
|
19
20
|
import { armorModel } from './entity/armorModels'
|
|
20
21
|
import { disposeObject, loadThreeJsTextureFromBitmap } from './threeJsUtils'
|
|
21
22
|
import { CursorBlock } from './world/cursorBlock'
|
|
22
23
|
import { getItemUv } from './appShared'
|
|
23
24
|
import { Entities } from './entities'
|
|
25
|
+
import {
|
|
26
|
+
getCameraMovementTweenDurationMs,
|
|
27
|
+
shouldRestartCameraPositionTween,
|
|
28
|
+
type CameraMovementMode,
|
|
29
|
+
type UpdateCameraOptions
|
|
30
|
+
} from './entity/interpolationPolicy'
|
|
24
31
|
import { ThreeJsSound } from './threeJsSound'
|
|
25
32
|
import { CameraShake } from './cameraShake'
|
|
26
33
|
import { ThreeJsMedia } from './threeJsMedia'
|
|
@@ -160,6 +167,8 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
160
167
|
|
|
161
168
|
private currentPosTween?: tweenJs.Tween<{ x: number; y: number; z: number }>
|
|
162
169
|
private currentRotTween?: tweenJs.Tween<{ pitch: number; yaw: number }>
|
|
170
|
+
private currentCameraTarget: { x: number; y: number; z: number } | null = null
|
|
171
|
+
private currentCameraMovementMode: CameraMovementMode | null = null
|
|
163
172
|
|
|
164
173
|
// Pre-allocated objects for getThirdPersonCamera (avoid per-frame allocs)
|
|
165
174
|
private readonly _tpDirection = new THREE.Vector3()
|
|
@@ -492,17 +501,18 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
492
501
|
}
|
|
493
502
|
|
|
494
503
|
updateEntity(e, isPosUpdate = false) {
|
|
504
|
+
const headPose = resolveEntityHeadPose(e)
|
|
495
505
|
const overrides = {
|
|
496
506
|
rotation: {
|
|
497
507
|
head: {
|
|
498
|
-
x:
|
|
499
|
-
y:
|
|
508
|
+
x: headPose.pitch,
|
|
509
|
+
y: headPose.headYaw,
|
|
500
510
|
z: 0
|
|
501
511
|
}
|
|
502
512
|
}
|
|
503
513
|
}
|
|
504
514
|
if (isPosUpdate) {
|
|
505
|
-
this.entities.updateEntityPosition(e, false, overrides)
|
|
515
|
+
this.entities.updateEntityPosition(e, false, overrides, e.headRotationOnly === true)
|
|
506
516
|
} else {
|
|
507
517
|
this.entities.update(e, overrides)
|
|
508
518
|
}
|
|
@@ -993,10 +1003,10 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
993
1003
|
}
|
|
994
1004
|
}
|
|
995
1005
|
|
|
996
|
-
setFirstPersonCamera(pos: Vec3 | null, yaw: number, pitch: number) {
|
|
1006
|
+
setFirstPersonCamera(pos: Vec3 | null, yaw: number, pitch: number, options?: UpdateCameraOptions) {
|
|
997
1007
|
const yOffset = this.playerStateReactive.eyeHeight
|
|
998
1008
|
|
|
999
|
-
this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
|
|
1009
|
+
this.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch, options)
|
|
1000
1010
|
// this.media.tryIntersectMedia()
|
|
1001
1011
|
this.updateCameraSectionPos()
|
|
1002
1012
|
}
|
|
@@ -1152,34 +1162,50 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
1152
1162
|
}
|
|
1153
1163
|
}
|
|
1154
1164
|
|
|
1155
|
-
updateCamera(pos: Vec3 | null, yaw: number, pitch: number): void {
|
|
1165
|
+
updateCamera(pos: Vec3 | null, yaw: number, pitch: number, options?: UpdateCameraOptions): void {
|
|
1156
1166
|
// Skip position/rotation updates if cinematic script is running
|
|
1157
1167
|
if (this.cinimaticScript.running) {
|
|
1158
1168
|
return
|
|
1159
1169
|
}
|
|
1160
1170
|
|
|
1161
|
-
// if (this.freeFlyMode) {
|
|
1162
|
-
// pos = this.freeFlyState.position
|
|
1163
|
-
// pitch = this.freeFlyState.pitch
|
|
1164
|
-
// yaw = this.freeFlyState.yaw
|
|
1165
|
-
// }
|
|
1166
|
-
|
|
1167
1171
|
if (pos) {
|
|
1168
1172
|
if (this.renderer.xr.isPresenting) {
|
|
1169
1173
|
pos.y -= this.camera.position.y // Fix Y position of camera in world
|
|
1170
1174
|
}
|
|
1171
1175
|
|
|
1172
|
-
this.
|
|
1173
|
-
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
.
|
|
1178
|
-
|
|
1176
|
+
const movementMode: CameraMovementMode = this.playerStateUtils.isSpectatingEntity() ? 'spectating' : (options?.movementMode ?? 'local-player')
|
|
1177
|
+
const instant = options?.instant === true || this.displayOptions.inWorldRenderingConfig.instantCameraUpdate === true
|
|
1178
|
+
const target = { x: pos.x, y: pos.y, z: pos.z }
|
|
1179
|
+
const restartTween = shouldRestartCameraPositionTween({
|
|
1180
|
+
target,
|
|
1181
|
+
currentTarget: this.currentCameraTarget,
|
|
1182
|
+
movementMode,
|
|
1183
|
+
previousMovementMode: this.currentCameraMovementMode,
|
|
1184
|
+
instant
|
|
1185
|
+
})
|
|
1186
|
+
const tweenDelay = getCameraMovementTweenDurationMs(movementMode, instant)
|
|
1187
|
+
|
|
1188
|
+
if (restartTween) {
|
|
1189
|
+
this.currentCameraTarget = { ...target }
|
|
1190
|
+
this.currentCameraMovementMode = movementMode
|
|
1191
|
+
this.currentPosTween?.stop()
|
|
1192
|
+
if (instant || tweenDelay === 0) {
|
|
1193
|
+
this.cameraWorldPos.x = target.x
|
|
1194
|
+
this.cameraWorldPos.y = target.y
|
|
1195
|
+
this.cameraWorldPos.z = target.z
|
|
1196
|
+
this.sceneOrigin.update(target.x, target.y, target.z)
|
|
1179
1197
|
this.cameraObject.position.set(0, 0, 0)
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1198
|
+
this.currentPosTween = undefined
|
|
1199
|
+
} else {
|
|
1200
|
+
this.currentPosTween = new tweenJs.Tween(this.cameraWorldPos)
|
|
1201
|
+
.to(target, tweenDelay)
|
|
1202
|
+
.onUpdate(() => {
|
|
1203
|
+
this.sceneOrigin.update(this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z)
|
|
1204
|
+
this.cameraObject.position.set(0, 0, 0)
|
|
1205
|
+
})
|
|
1206
|
+
.start()
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1183
1209
|
}
|
|
1184
1210
|
|
|
1185
1211
|
if (this.playerStateUtils.isSpectatingEntity()) {
|