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.
Files changed (42) hide show
  1. package/dist/minecraft-renderer.js +64 -64
  2. package/dist/minecraft-renderer.js.meta.json +1 -1
  3. package/dist/threeWorker.js +522 -522
  4. package/package.json +1 -1
  5. package/src/graphicsBackend/appViewer.ts +4 -3
  6. package/src/graphicsBackend/types.ts +4 -1
  7. package/src/lib/worldrendererCommon.ts +2 -2
  8. package/src/three/entities.ts +302 -33
  9. package/src/three/entity/EntityMesh.ts +8 -3
  10. package/src/three/entity/animations.js +37 -25
  11. package/src/three/entity/animations.test.ts +249 -0
  12. package/src/three/entity/boatModelRotation.test.ts +29 -0
  13. package/src/three/entity/boatModelRotation.ts +14 -0
  14. package/src/three/entity/boatPaddleAnimation.test.ts +108 -0
  15. package/src/three/entity/boatPaddleAnimation.ts +104 -0
  16. package/src/three/entity/boatPassengerRendering.ts +20 -0
  17. package/src/three/entity/boatPassengerRotation.test.ts +166 -0
  18. package/src/three/entity/boatPassengerRotation.ts +71 -0
  19. package/src/three/entity/boatRenderSetup.test.ts +236 -0
  20. package/src/three/entity/boatRenderSetup.ts +214 -0
  21. package/src/three/entity/entityHeadPose.test.ts +15 -0
  22. package/src/three/entity/entityHeadPose.ts +22 -0
  23. package/src/three/entity/horseEntityScale.test.ts +11 -0
  24. package/src/three/entity/horseEntityScale.ts +9 -0
  25. package/src/three/entity/horseHeadAnimation.test.ts +159 -0
  26. package/src/three/entity/horseHeadAnimation.ts +151 -0
  27. package/src/three/entity/horsePassengerRendering.test.ts +77 -0
  28. package/src/three/entity/interpolationPolicy.test.ts +259 -0
  29. package/src/three/entity/interpolationPolicy.ts +187 -0
  30. package/src/three/entity/localHorseCameraYawLock.test.ts +72 -0
  31. package/src/three/entity/localVehicleRendering.integration.test.ts +247 -0
  32. package/src/three/entity/minecartLocalPassengerRendering.test.ts +208 -0
  33. package/src/three/entity/networkHeadPitchRendering.test.ts +102 -0
  34. package/src/three/entity/networkHeadPitchRendering.ts +44 -0
  35. package/src/three/entity/remoteBoatPassengerRotation.integration.test.ts +227 -0
  36. package/src/three/entity/remoteBoatPassengerRotation.ts +128 -0
  37. package/src/three/entity/vehiclePassengerProduction.test.ts +214 -0
  38. package/src/three/entity/vehiclePassengerRendering.ts +56 -0
  39. package/src/three/entity/vehiclePassengerUpdate.ts +82 -0
  40. package/src/three/graphicsBackendBase.ts +5 -5
  41. package/src/three/graphicsBackendOffThread.ts +2 -2
  42. 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: e.headPitch ?? e.pitch,
499
- y: e.headYaw,
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.currentPosTween?.stop()
1173
- // Use instant camera updates (0 delay) in playground mode when camera controls are enabled
1174
- const tweenDelay = this.displayOptions.inWorldRenderingConfig.instantCameraUpdate ? 0 : this.playerStateUtils.isSpectatingEntity() ? 150 : 50
1175
- this.currentPosTween = new tweenJs.Tween(this.cameraWorldPos)
1176
- .to({ x: pos.x, y: pos.y, z: pos.z }, tweenDelay)
1177
- .onUpdate(() => {
1178
- this.sceneOrigin.update(this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z)
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
- .start()
1182
- // this.freeFlyState.position = pos
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()) {