minecraft-renderer 0.1.94 → 0.1.96

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.94",
3
+ "version": "0.1.96",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,8 @@
1
1
  //@ts-nocheck
2
2
  /** Shared geometry export shapes (worker bridge + main-thread viewer). */
3
3
 
4
+ import { SHADER_CUBES_FORMAT_VERSION } from './shaderCubeFormat'
5
+
4
6
  export interface ExportedSection {
5
7
  key: string
6
8
  position: { x: number; y: number; z: number }
@@ -25,7 +27,7 @@ export interface ExportedSection {
25
27
  shaderCubes?: {
26
28
  words: Uint32Array
27
29
  count: number
28
- formatVersion: 3
30
+ formatVersion: typeof SHADER_CUBES_FORMAT_VERSION
29
31
  }
30
32
  visibilitySet?: number
31
33
  }
@@ -0,0 +1,3 @@
1
+ //@ts-nocheck
2
+ export const SHADER_CUBES_FORMAT_VERSION = 4 as const
3
+ export const SHADER_CUBES_WORDS_PER_FACE = 4 as const
@@ -1,5 +1,6 @@
1
1
  //@ts-nocheck
2
2
  import { BlockType } from '../playground/shared'
3
+ import { SHADER_CUBES_FORMAT_VERSION } from './shaderCubeFormat'
3
4
 
4
5
  export const SECTION_HEIGHT = 16
5
6
 
@@ -80,7 +81,7 @@ export type MesherGeometryOutput = {
80
81
  shaderCubes?: {
81
82
  words: Uint32Array
82
83
  count: number
83
- formatVersion: 3
84
+ formatVersion: typeof SHADER_CUBES_FORMAT_VERSION
84
85
  }
85
86
  /** Packed VisibilitySet (36 bits) for section occlusion / smart cull. */
86
87
  visibilitySet?: number
@@ -447,9 +447,7 @@ export class Entities {
447
447
  }
448
448
 
449
449
  private setSceneAttached(attached: boolean, entity: THREE.Object3D | null = null): void {
450
- const targets = entity
451
- ? [entity]
452
- : [...Object.values(this.entities), ...(this.playerEntity ? [this.playerEntity] : [])]
450
+ const targets = entity ? [entity] : [...Object.values(this.entities), ...(this.playerEntity ? [this.playerEntity] : [])]
453
451
  for (const ent of targets) {
454
452
  if (attached) {
455
453
  if (!this.worldRenderer.scene.children.includes(ent)) this.worldRenderer.scene.add(ent)
@@ -655,7 +653,8 @@ export class Entities {
655
653
  applyVehiclePassengerPositions({
656
654
  entities: this.entities,
657
655
  localPlayer: this.playerEntity,
658
- getWorldPosition: target => this.worldRenderer.sceneOrigin.getWorldPosition(target as SceneEntity)
656
+ getWorldPosition: target => this.worldRenderer.sceneOrigin.getWorldPosition(target as SceneEntity),
657
+ version: this.worldRenderer.version
659
658
  })
660
659
  }
661
660
 
@@ -10,6 +10,7 @@ import {
10
10
  getHorsePassengerWorldPosition,
11
11
  getLocalVehicleWorldPosition,
12
12
  getMinecartPassengerWorldPosition,
13
+ getVehiclePassengerFeetOffsetY,
13
14
  resolveLocalVehicleWorldPosition,
14
15
  samePosition,
15
16
  shouldRestartCameraPositionTween,
@@ -244,7 +245,28 @@ test('horse vertical camera lock falls back to raw Y when result is non-finite',
244
245
  })
245
246
 
246
247
  test('minecart passenger feet Y is vehicleY - 0.35', () => {
247
- expect(getMinecartPassengerWorldPosition({ x: 0, y: 64, z: 0 }).y).toBeCloseTo(63.65, 5)
248
+ expect(getMinecartPassengerWorldPosition({ x: 0, y: 64, z: 0 }, '1.17.1', 'minecart', 0.7).y).toBeCloseTo(63.65, 5)
249
+ })
250
+
251
+ test.each([
252
+ ['1.17.1', 'boat', -0.45],
253
+ ['1.20.1', 'boat', -0.45],
254
+ ['1.20.2', 'boat', -0.4125],
255
+ ['1.21.4', 'boat', -0.4125],
256
+ ['1.17.1', 'minecart', -0.35],
257
+ ['1.20.1', 'minecart', -0.35],
258
+ ['1.20.2', 'minecart', -0.4125],
259
+ ['1.21.4', 'minecart', -0.4125],
260
+ ['1.21.4', 'bamboo_raft', -0.1]
261
+ ])('vehicle %s %s seat offset matches vanilla', (version, vehicleName, expected) => {
262
+ const layout = vehicleName === 'minecart' ? 'minecart' : 'boat'
263
+ const height = layout === 'boat' ? 0.5625 : 0.7
264
+ expect(getVehiclePassengerFeetOffsetY(layout, version, vehicleName, height)).toBeCloseTo(expected, 5)
265
+ })
266
+
267
+ test('vehicle seat offset falls back to the default vehicle height', () => {
268
+ expect(getVehiclePassengerFeetOffsetY('boat', '1.20.2', 'oak_boat', undefined)).toBeCloseTo(-0.4125, 5)
269
+ expect(getVehiclePassengerFeetOffsetY('minecart', '1.20.2', 'minecart', undefined)).toBeCloseTo(-0.4125, 5)
248
270
  })
249
271
 
250
272
  test.each([
@@ -1,4 +1,6 @@
1
1
  //@ts-nocheck
2
+ import { versionToNumber } from '../../lib/utils'
3
+
2
4
  export const LOCAL_MOVEMENT_TWEEN_DURATION_MS = 50
3
5
  export const ENTITY_TWEEN_DURATION_MS = 120
4
6
  export const SPECTATING_CAMERA_TWEEN_DURATION_MS = 150
@@ -116,11 +118,6 @@ export function resolveLocalVehicleWorldPosition(args: {
116
118
  return getLocalVehicleWorldPosition(cameraWorldPos, rawVehicleY)
117
119
  }
118
120
 
119
- const BOAT_PASSENGER_RIDING_OFFSET_Y = -0.1
120
- const PLAYER_RIDING_OFFSET_Y = -0.35
121
- /** Vanilla 1.17.1 AbstractMinecart#getPassengersRidingOffset() */
122
- const MINECART_PASSENGER_RIDING_OFFSET_Y = 0
123
-
124
121
  const RIDEABLE_MINECART_ENTITY_NAMES = new Set([
125
122
  'minecart',
126
123
  'chest_minecart',
@@ -148,21 +145,79 @@ export function getBoatPassengerSeatOffset(passengerIndex: number, passengerCoun
148
145
  return passengerIndex === 0 ? 0.2 : -0.6
149
146
  }
150
147
 
151
- /** Vanilla 1.17.1 Boat#positionRider position, without applying the passenger pose. */
152
- export function getBoatPassengerWorldPosition(boatWorldPos: Vec3Like, boatYaw: number, passengerIndex: number, passengerCount: number): Vec3Like {
148
+ const DEFAULT_BOAT_HEIGHT = 0.5625
149
+ const DEFAULT_MINECART_HEIGHT = 0.7
150
+
151
+ // <= 1.20.1: Entity#positionRider = y + vehicle.getPassengersRidingOffset() + passenger.getMyRidingOffset()
152
+ // Vanilla 1.17.1 sources: vehicle/Boat#getPassengersRidingOffset() = -0.1,
153
+ // vehicle/AbstractMinecart#getPassengersRidingOffset() = 0, player/Player#getMyRidingOffset() = -0.35.
154
+ const LEGACY_PLAYER_RIDING_OFFSET_Y = -0.35 // Player#getMyRidingOffset(), <= 1.20.1
155
+ const LEGACY_BOAT_RIDING_OFFSET_Y = -0.1 // Boat#getPassengersRidingOffset(), <= 1.20.1
156
+ const LEGACY_MINECART_RIDING_OFFSET_Y = 0 // AbstractMinecart#getPassengersRidingOffset(), <= 1.20.1
157
+
158
+ // >= 1.20.2: y is based on the vehicle's PASSENGER attachment point.
159
+ // The player's contribution is numerically the same, but the mechanism changed:
160
+ // 1.20.2-1.20.4: PASSENGER.y + Player#ridingOffset() (-0.6)
161
+ // >= 1.20.5: PASSENGER.y - Player.DEFAULT_VEHICLE_ATTACHMENT.y (0.6)
162
+ // Vanilla sources: Entity#positionRider, Boat#getPassengerAttachmentPoint,
163
+ // EntityType.*_MINECART#passengerAttachments and Player#ridingOffset / DEFAULT_VEHICLE_ATTACHMENT.
164
+ const MODERN_PLAYER_RIDING_OFFSET_Y = -0.6
165
+ const MODERN_MINECART_PASSENGER_ATTACHMENT_Y = 0.1875 // EntityType.*_MINECART.passengerAttachments()
166
+ const MODERN_BOAT_RIDE_HEIGHT_FACTOR = 1 / 3 // (Abstract)Boat#rideHeight(), >= 1.20.2
167
+ const MODERN_RAFT_RIDE_HEIGHT_FACTOR = 0.8888889 // Raft#rideHeight(), >= 1.21.2
168
+
169
+ /**
170
+ * Vanilla seat offset for a player passenger in a boat or minecart.
171
+ * Rafts before 1.21.2 are intentionally treated as boats because their entity
172
+ * name is 'boat' and their metadata variant is not read by this project.
173
+ */
174
+ export function getVehiclePassengerFeetOffsetY(
175
+ layout: 'boat' | 'minecart',
176
+ version: string,
177
+ vehicleName: string | undefined,
178
+ vehicleHeight: number | undefined
179
+ ): number {
180
+ const height = Number.isFinite(vehicleHeight) ? vehicleHeight! : layout === 'boat' ? DEFAULT_BOAT_HEIGHT : DEFAULT_MINECART_HEIGHT
181
+
182
+ if (versionToNumber(version) >= versionToNumber('1.20.2')) {
183
+ const isRaft = typeof vehicleName === 'string' && vehicleName.toLowerCase().endsWith('_raft')
184
+ const passengerAttachmentY =
185
+ layout === 'boat' ? height * (isRaft ? MODERN_RAFT_RIDE_HEIGHT_FACTOR : MODERN_BOAT_RIDE_HEIGHT_FACTOR) : MODERN_MINECART_PASSENGER_ATTACHMENT_Y
186
+ return passengerAttachmentY + MODERN_PLAYER_RIDING_OFFSET_Y
187
+ }
188
+
189
+ const vehicleRidingOffset = layout === 'boat' ? LEGACY_BOAT_RIDING_OFFSET_Y : LEGACY_MINECART_RIDING_OFFSET_Y
190
+ return vehicleRidingOffset + LEGACY_PLAYER_RIDING_OFFSET_Y
191
+ }
192
+
193
+ /** Vanilla Boat#positionRider position, without applying the passenger pose. */
194
+ export function getBoatPassengerWorldPosition(
195
+ boatWorldPos: Vec3Like,
196
+ boatYaw: number,
197
+ passengerIndex: number,
198
+ passengerCount: number,
199
+ version: string,
200
+ vehicleName: string | undefined,
201
+ vehicleHeight: number | undefined
202
+ ): Vec3Like {
153
203
  const seatOffset = getBoatPassengerSeatOffset(passengerIndex, passengerCount)
154
204
  return {
155
205
  x: boatWorldPos.x - Math.sin(boatYaw) * seatOffset,
156
- y: boatWorldPos.y + BOAT_PASSENGER_RIDING_OFFSET_Y + PLAYER_RIDING_OFFSET_Y,
206
+ y: boatWorldPos.y + getVehiclePassengerFeetOffsetY('boat', version, vehicleName, vehicleHeight),
157
207
  z: boatWorldPos.z + Math.cos(boatYaw) * seatOffset
158
208
  }
159
209
  }
160
210
 
161
- /** Vanilla 1.17.1 minecart positionRider for a centered player passenger. */
162
- export function getMinecartPassengerWorldPosition(minecartWorldPos: Vec3Like): Vec3Like {
211
+ /** Vanilla minecart positionRider for a centered player passenger. */
212
+ export function getMinecartPassengerWorldPosition(
213
+ minecartWorldPos: Vec3Like,
214
+ version: string,
215
+ vehicleName: string | undefined,
216
+ vehicleHeight: number | undefined
217
+ ): Vec3Like {
163
218
  return {
164
219
  x: minecartWorldPos.x,
165
- y: minecartWorldPos.y + MINECART_PASSENGER_RIDING_OFFSET_Y + PLAYER_RIDING_OFFSET_Y,
220
+ y: minecartWorldPos.y + getVehiclePassengerFeetOffsetY('minecart', version, vehicleName, vehicleHeight),
166
221
  z: minecartWorldPos.z
167
222
  }
168
223
  }
@@ -14,6 +14,10 @@ import {
14
14
 
15
15
  type Vec3 = { x: number; y: number; z: number }
16
16
 
17
+ const TEST_VERSION = '1.17.1'
18
+ const BOAT_HEIGHT = 0.5625
19
+ const MINECART_HEIGHT = 0.7
20
+
17
21
  function staleOffsetVehiclePosition(cameraWorldPos: Vec3, vehiclePosition: Vec3, stalePassengerPosition: Vec3): Vec3 {
18
22
  return {
19
23
  x: cameraWorldPos.x + (vehiclePosition.x - stalePassengerPosition.x),
@@ -74,23 +78,23 @@ test('render frames between press and release keep zero horizontal delta to came
74
78
  test('boat passenger positions use vanilla 1.17.1 riding and seat offsets', () => {
75
79
  const boat = { x: 10, y: 64, z: 20 }
76
80
 
77
- expect(getBoatPassengerWorldPosition(boat, 0, 0, 1)).toEqual({
81
+ expect(getBoatPassengerWorldPosition(boat, 0, 0, 1, TEST_VERSION, 'oak_boat', BOAT_HEIGHT)).toEqual({
78
82
  x: 10,
79
83
  y: 63.55,
80
84
  z: 20
81
85
  })
82
- expect(getBoatPassengerWorldPosition(boat, 0, 0, 2)).toEqual({
86
+ expect(getBoatPassengerWorldPosition(boat, 0, 0, 2, TEST_VERSION, 'oak_boat', BOAT_HEIGHT)).toEqual({
83
87
  x: 10,
84
88
  y: 63.55,
85
89
  z: 20.2
86
90
  })
87
- expect(getBoatPassengerWorldPosition(boat, 0, 1, 2)).toEqual({
91
+ expect(getBoatPassengerWorldPosition(boat, 0, 1, 2, TEST_VERSION, 'oak_boat', BOAT_HEIGHT)).toEqual({
88
92
  x: 10,
89
93
  y: 63.55,
90
94
  z: 19.4
91
95
  })
92
96
 
93
- const rotated = getBoatPassengerWorldPosition(boat, Math.PI / 2, 0, 2)
97
+ const rotated = getBoatPassengerWorldPosition(boat, Math.PI / 2, 0, 2, TEST_VERSION, 'oak_boat', BOAT_HEIGHT)
94
98
  expect(rotated.x).toBeCloseTo(9.8)
95
99
  expect(rotated.y).toBe(63.55)
96
100
  expect(rotated.z).toBeCloseTo(20)
@@ -110,7 +114,15 @@ test('remote player follows the tracked boat each frame and releases from an emp
110
114
  sceneOrigin.track(passenger)
111
115
  passenger.position.set(90, 64, 190)
112
116
 
113
- const firstPassengerPosition = getBoatPassengerWorldPosition(sceneOrigin.getWorldPosition(boat)!, boat.rotation.y, 0, 1)
117
+ const firstPassengerPosition = getBoatPassengerWorldPosition(
118
+ sceneOrigin.getWorldPosition(boat)!,
119
+ boat.rotation.y,
120
+ 0,
121
+ 1,
122
+ TEST_VERSION,
123
+ 'oak_boat',
124
+ BOAT_HEIGHT
125
+ )
114
126
  anchorVehiclePassengerPosition(passenger, firstPassengerPosition, '10')
115
127
 
116
128
  expect(stopPositionTween).toHaveBeenCalledOnce()
@@ -119,7 +131,15 @@ test('remote player follows the tracked boat each frame and releases from an emp
119
131
 
120
132
  boat.position.set(102, 64.2, 203)
121
133
  boat.rotation.y = Math.PI / 2
122
- const movedPassengerPosition = getBoatPassengerWorldPosition(sceneOrigin.getWorldPosition(boat)!, boat.rotation.y, 0, 1)
134
+ const movedPassengerPosition = getBoatPassengerWorldPosition(
135
+ sceneOrigin.getWorldPosition(boat)!,
136
+ boat.rotation.y,
137
+ 0,
138
+ 1,
139
+ TEST_VERSION,
140
+ 'oak_boat',
141
+ BOAT_HEIGHT
142
+ )
123
143
  anchorVehiclePassengerPosition(passenger, movedPassengerPosition, '10')
124
144
  expect(sceneOrigin.getWorldPosition(passenger)?.x).toBe(102)
125
145
  expect(sceneOrigin.getWorldPosition(passenger)?.y).toBeCloseTo(63.75)
@@ -142,7 +162,7 @@ test('remote player follows the tracked boat each frame and releases from an emp
142
162
 
143
163
  test('minecart passenger uses vanilla 1.17.1 riding Y offset', () => {
144
164
  const minecart = { x: 10, y: 64, z: 20 }
145
- expect(getMinecartPassengerWorldPosition(minecart)).toEqual({
165
+ expect(getMinecartPassengerWorldPosition(minecart, TEST_VERSION, 'minecart', MINECART_HEIGHT)).toEqual({
146
166
  x: 10,
147
167
  y: 63.65,
148
168
  z: 20
@@ -163,13 +183,13 @@ test('minecart passenger anchor follows vehicle world position without inheritin
163
183
  sceneOrigin.track(passenger)
164
184
  passenger.position.set(90, 64, 190)
165
185
 
166
- const firstPosition = getMinecartPassengerWorldPosition(sceneOrigin.getWorldPosition(minecart)!)
186
+ const firstPosition = getMinecartPassengerWorldPosition(sceneOrigin.getWorldPosition(minecart)!, TEST_VERSION, 'minecart', MINECART_HEIGHT)
167
187
  anchorVehiclePassengerPosition(passenger, firstPosition, '42')
168
188
  const anchoredYaw = passenger.rotation.y
169
189
 
170
190
  minecart.position.set(103, 64.5, 205)
171
191
  minecart.rotation.y = Math.PI
172
- const movedPosition = getMinecartPassengerWorldPosition(sceneOrigin.getWorldPosition(minecart)!)
192
+ const movedPosition = getMinecartPassengerWorldPosition(sceneOrigin.getWorldPosition(minecart)!, TEST_VERSION, 'minecart', MINECART_HEIGHT)
173
193
  anchorVehiclePassengerPosition(passenger, movedPosition, '42')
174
194
 
175
195
  expect(sceneOrigin.getWorldPosition(passenger)).toEqual({
@@ -234,7 +254,7 @@ test('local minecart passenger anchors to camera-synced vehicle position', () =>
234
254
  for (const camera of cameraFrames) {
235
255
  const syncedMinecart = getLocalVehicleWorldPosition(camera, vehicleY)
236
256
  minecart.position.set(syncedMinecart.x, syncedMinecart.y, syncedMinecart.z)
237
- const seatPosition = getMinecartPassengerWorldPosition(syncedMinecart)
257
+ const seatPosition = getMinecartPassengerWorldPosition(syncedMinecart, TEST_VERSION, 'minecart', MINECART_HEIGHT)
238
258
  anchorVehiclePassengerPosition(passenger, seatPosition, '42')
239
259
  const passengerWorld = sceneOrigin.getWorldPosition(passenger)!
240
260
 
@@ -10,6 +10,8 @@ const REMOTE_PLAYER_ID = 8
10
10
  const MINECART_ID = '42'
11
11
  const EYE_HEIGHT = 1.62
12
12
  const PLAYER_HEIGHT = 1.8
13
+ const TEST_VERSION = '1.17.1'
14
+ const MINECART_HEIGHT = 0.7
13
15
 
14
16
  type PassengerLike = THREE.Group & {
15
17
  playerObject: Record<string, unknown>
@@ -42,7 +44,7 @@ function anchorMinecartPassengers(
42
44
  remotePlayers: Record<number, PassengerLike>
43
45
  ) {
44
46
  const vehicleWorldPos = sceneOrigin.getWorldPosition(minecart)!
45
- const seatPosition = getMinecartPassengerWorldPosition(vehicleWorldPos)
47
+ const seatPosition = getMinecartPassengerWorldPosition(vehicleWorldPos, TEST_VERSION, 'minecart', MINECART_HEIGHT)
46
48
  for (const passengerId of passengerIds) {
47
49
  const passenger = resolveMinecartPassenger(passengerId, localPlayer, remotePlayers)
48
50
  if (!passenger?.playerObject) continue
@@ -196,7 +198,7 @@ test('local boat passenger uses the same anchor as remote passenger', () => {
196
198
  sceneOrigin.track(localPlayer)
197
199
  sceneOrigin.track(remotePlayer)
198
200
 
199
- const boatSeat = getBoatPassengerWorldPosition(sceneOrigin.getWorldPosition(boat)!, boat.rotation.y, 0, 1)
201
+ const boatSeat = getBoatPassengerWorldPosition(sceneOrigin.getWorldPosition(boat)!, boat.rotation.y, 0, 1, TEST_VERSION, 'oak_boat', 0.5625)
200
202
  if (shouldAnchorPassenger(localPlayer, 'boat', true)) {
201
203
  anchorVehiclePassengerPosition(localPlayer, boatSeat, '10')
202
204
  }
@@ -74,7 +74,7 @@ test('minecart passenger anchor preserves head rotation', () => {
74
74
  storeNetworkHeadPitch(userData, 0.45)
75
75
  applyNetworkHeadPitch(playerObject, userData)
76
76
 
77
- const seat = getMinecartPassengerWorldPosition({ x: 10, y: 64, z: 20 })
77
+ const seat = getMinecartPassengerWorldPosition({ x: 10, y: 64, z: 20 }, '1.17.1', 'minecart', 0.7)
78
78
  anchorVehiclePassengerPosition(passenger, seat, '42')
79
79
  simulateAnimationFrames(playerObject, userData, 4)
80
80
 
@@ -21,6 +21,7 @@ const BOAT_YAW = -0.5645049299419149
21
21
  const REMOTE_PLAYER_A = 8
22
22
  const REMOTE_PLAYER_B = 9
23
23
  const BOAT_ID = 10
24
+ const TEST_VERSION = '1.17.1'
24
25
 
25
26
  function makePlayerObject(): PlayerObjectType {
26
27
  return new PlayerObject() as PlayerObjectType
@@ -71,7 +72,8 @@ function createHarness(passengerIds: number[]) {
71
72
  updateVehiclePassengerPositions({
72
73
  entities: entities as Parameters<typeof updateVehiclePassengerPositions>[0]['entities'],
73
74
  localPlayer: null,
74
- getWorldPosition: target => sceneOrigin.getWorldPosition(target as THREE.Object3D)
75
+ getWorldPosition: target => sceneOrigin.getWorldPosition(target as THREE.Object3D),
76
+ version: TEST_VERSION
75
77
  })
76
78
 
77
79
  const runRemoteRotation = () =>
@@ -8,6 +8,7 @@ import { updateVehiclePassengerPositions, type VehiclePassengerSceneEntity, type
8
8
 
9
9
  const LOCAL_PLAYER_ID = 7
10
10
  const REMOTE_PLAYER_ID = 8
11
+ const TEST_VERSION = '1.17.1'
11
12
 
12
13
  type PassengerLike = THREE.Group & VehiclePassengerSceneEntity
13
14
 
@@ -43,7 +44,8 @@ function createHarness() {
43
44
  updateVehiclePassengerPositions({
44
45
  entities,
45
46
  localPlayer,
46
- getWorldPosition: target => sceneOrigin.getWorldPosition(target as THREE.Object3D)
47
+ getWorldPosition: target => sceneOrigin.getWorldPosition(target as THREE.Object3D),
48
+ version: TEST_VERSION
47
49
  })
48
50
 
49
51
  return { sceneOrigin, localPlayer, entities, cameraWorldPos, runUpdate }
@@ -65,7 +67,15 @@ test('updateVehiclePassengerPositions anchors local boat and minecart passengers
65
67
 
66
68
  runUpdate()
67
69
 
68
- const expectedBoatSeat = getBoatPassengerWorldPosition(sceneOrigin.getWorldPosition(boat)!, boat.rotation.y, 0, 1)
70
+ const expectedBoatSeat = getBoatPassengerWorldPosition(
71
+ sceneOrigin.getWorldPosition(boat)!,
72
+ boat.rotation.y,
73
+ 0,
74
+ 1,
75
+ TEST_VERSION,
76
+ 'oak_boat',
77
+ boat.originalEntity.height
78
+ )
69
79
  expect(localPlayer.userData._passengerVehicleId).toBe('10')
70
80
  expect(sceneOrigin.getWorldPosition(localPlayer)).toEqual(expectedBoatSeat)
71
81
 
@@ -77,7 +87,12 @@ test('updateVehiclePassengerPositions anchors local boat and minecart passengers
77
87
 
78
88
  runUpdate()
79
89
 
80
- const expectedMinecartSeat = getMinecartPassengerWorldPosition(sceneOrigin.getWorldPosition(minecart)!)
90
+ const expectedMinecartSeat = getMinecartPassengerWorldPosition(
91
+ sceneOrigin.getWorldPosition(minecart)!,
92
+ TEST_VERSION,
93
+ 'minecart',
94
+ minecart.originalEntity.height
95
+ )
81
96
  expect(localPlayer.userData._passengerVehicleId).toBe('42')
82
97
  expect(sceneOrigin.getWorldPosition(localPlayer)).toEqual(expectedMinecartSeat)
83
98
  })
@@ -119,8 +134,8 @@ test('local and remote boat passengers use the same seat algorithm per index', (
119
134
  const vehicleWorldPos = sceneOrigin.getWorldPosition(boat)!
120
135
  const localWorld = sceneOrigin.getWorldPosition(localPlayer)!
121
136
  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))
137
+ expect(localWorld).toEqual(getBoatPassengerWorldPosition(vehicleWorldPos, boat.rotation.y, 0, 2, TEST_VERSION, 'oak_boat', boat.originalEntity.height))
138
+ expect(remoteWorld).toEqual(getBoatPassengerWorldPosition(vehicleWorldPos, boat.rotation.y, 1, 2, TEST_VERSION, 'oak_boat', boat.originalEntity.height))
124
139
  expect(localWorld.y).toBeCloseTo(63.55, 5)
125
140
  expect(remoteWorld.y).toBeCloseTo(63.55, 5)
126
141
  })
@@ -33,6 +33,7 @@ export function updateVehiclePassengerPositions(args: {
33
33
  entities: Record<string, VehiclePassengerSceneEntity | VehiclePassengerVehicle>
34
34
  localPlayer: VehiclePassengerSceneEntity | null
35
35
  getWorldPosition: (target: unknown) => Vec3Like | undefined
36
+ version: string
36
37
  }): Set<VehiclePassengerSceneEntity> {
37
38
  const attachedPassengers = new Set<VehiclePassengerSceneEntity>()
38
39
 
@@ -61,10 +62,18 @@ export function updateVehiclePassengerPositions(args: {
61
62
 
62
63
  const passengerWorldPos =
63
64
  layout === 'minecart'
64
- ? getMinecartPassengerWorldPosition(vehicleWorldPos)
65
+ ? getMinecartPassengerWorldPosition(vehicleWorldPos, args.version, vehicleName, vehicle.originalEntity.height)
65
66
  : layout === 'horse'
66
67
  ? getHorsePassengerWorldPosition(vehicleWorldPos, vehicleName, vehicle.originalEntity.height ?? 1.6)
67
- : getBoatPassengerWorldPosition(vehicleWorldPos, vehicle.rotation.y, passengerIndex, passengerIds.length)
68
+ : getBoatPassengerWorldPosition(
69
+ vehicleWorldPos,
70
+ vehicle.rotation.y,
71
+ passengerIndex,
72
+ passengerIds.length,
73
+ args.version,
74
+ vehicleName,
75
+ vehicle.originalEntity.height
76
+ )
68
77
 
69
78
  if (!isFiniteVec3(passengerWorldPos)) continue
70
79
  anchorVehiclePassengerPosition(passenger, passengerWorldPos, vehicleId)
@@ -7,6 +7,64 @@ export type { BlockLightmapParams }
7
7
  // Face order: UP=0, DOWN=1, EAST=2, WEST=3, SOUTH=4, NORTH=5
8
8
  // matches WASM mesher face order (mesher.rs FACE_NAMES)
9
9
 
10
+ /** Term of tile-local UV in quad-corner coordinates (u, v ∈ {0,1}). */
11
+ type FaceUvTerm = 'u' | '1-u' | 'v' | '1-v'
12
+
13
+ /**
14
+ * Canonical tile-local UV per face for su>0, sv>0. Matches legacy elemFaces
15
+ * (+ implicit +180° on down; see render-from-wasm.ts `r += 180`).
16
+ * Index = faceId: UP, DOWN, EAST, WEST, SOUTH, NORTH.
17
+ * Single source of truth — GLSL and shaderCubeFaceUv() are generated from this.
18
+ */
19
+ export const FACE_UV_TERMS: ReadonlyArray<readonly [FaceUvTerm, FaceUvTerm]> = [
20
+ ['u', '1-v'], // UP
21
+ ['1-u', 'v'], // DOWN
22
+ ['v', 'u'], // EAST
23
+ ['v', 'u'], // WEST
24
+ ['u', 'v'], // SOUTH
25
+ ['1-u', 'v'] // NORTH
26
+ ]
27
+
28
+ const glslTerm = (t: FaceUvTerm) =>
29
+ ({
30
+ u: 'u',
31
+ v: 'v',
32
+ '1-u': '1.0 - u',
33
+ '1-v': '1.0 - v'
34
+ })[t]
35
+
36
+ /** GLSL if/else-if chain for per-face UV — must stay in sync with FACE_UV_TERMS. */
37
+ export function buildFaceUvGlsl(): string {
38
+ return FACE_UV_TERMS.map(([ut, vt], i) => ` ${i === 0 ? 'if' : 'else if'} (faceId == ${i}u) uv = vec2(${glslTerm(ut)}, ${glslTerm(vt)});`).join('\n')
39
+ }
40
+
41
+ function evalFaceUvTerm(t: FaceUvTerm, cu: number, cv: number): number {
42
+ switch (t) {
43
+ case 'u':
44
+ return cu
45
+ case '1-u':
46
+ return 1 - cu
47
+ case 'v':
48
+ return cv
49
+ case '1-v':
50
+ return 1 - cv
51
+ }
52
+ }
53
+
54
+ /** vi — quad corner in shader order (0..3): u = vi & 1, v = (vi >> 1) & 1. */
55
+ export function shaderCubeFaceUv(faceId: number, vi: number, flipU: boolean, flipV: boolean): [number, number] {
56
+ const [ut, vt] = FACE_UV_TERMS[faceId] ?? FACE_UV_TERMS[0]
57
+ const cu = vi & 1
58
+ const cv = (vi >> 1) & 1
59
+ let uLocal = evalFaceUvTerm(ut, cu, cv)
60
+ let vLocal = evalFaceUvTerm(vt, cu, cv)
61
+ if (flipU) uLocal = 1 - uLocal
62
+ if (flipV) vLocal = 1 - vLocal
63
+ return [uLocal, vLocal]
64
+ }
65
+
66
+ const faceUvGlsl = buildFaceUvGlsl()
67
+
10
68
  const vertexShader = /* glsl */ `
11
69
  precision highp float;
12
70
  precision highp int;
@@ -143,18 +201,16 @@ void main() {
143
201
  v_tintIndex = int(tint);
144
202
  v_faceId = int(faceId);
145
203
 
146
- // --- Per-face UV transform (legacy elemFaces + down +180°) ---
147
- if (faceId == 0u) {
148
- v_uv = vec2(u, 1.0 - v);
149
- } else if (faceId == 1u) {
150
- v_uv = vec2(1.0 - u, 1.0 - v);
151
- } else if (faceId == 2u || faceId == 3u) {
152
- v_uv = vec2(v, u);
153
- } else if (faceId == 4u) {
154
- v_uv = vec2(u, v);
155
- } else { // faceId == 5u
156
- v_uv = vec2(1.0 - u, v);
157
- }
204
+ // --- Per-face UV: canonical tile-local UV (su>0, sv>0), generated from
205
+ // FACE_UV_TERMS — same source as shaderCubeFaceUv() ---
206
+ vec2 uv;
207
+ ${faceUvGlsl}
208
+
209
+ // su/sv sign from resolved model: negative scale = mirror on axis.
210
+ // Bit offsets mirror WORD2.FLIP_U_SHIFT / WORD0.FLIP_V_SHIFT (GLSL cannot import TS).
211
+ if (((a_w2 >> 31u) & 0x1u) != 0u) uv.x = 1.0 - uv.x;
212
+ if (((a_w0 >> 31u) & 0x1u) != 0u) uv.y = 1.0 - uv.y;
213
+ v_uv = uv;
158
214
 
159
215
  // --- Position: section base (multiples of 16) + face quad + block-local 0..15 ---
160
216
  // Must mirror WORD2/WORD3 constants in TS (GLSL cannot import them).
@@ -321,6 +377,9 @@ void main() {
321
377
  }
322
378
  `
323
379
 
380
+ /** Vertex shader source (includes generated face UV table). Exported for parity tests. */
381
+ export const cubeBlockVertexShader = vertexShader
382
+
324
383
  export function createCubeBlockMaterial(): THREE.ShaderMaterial {
325
384
  return new THREE.ShaderMaterial({
326
385
  vertexShader,
@@ -417,7 +476,8 @@ export const WORD0 = {
417
476
  FACE_SHIFT: 12,
418
477
  TINT_SHIFT: 15,
419
478
  AO_SHIFT: 23,
420
- TRANSPARENT_SHIFT: 31
479
+ /** sv<0 mirror flag — no free bits remain in word0 after this. */
480
+ FLIP_V_SHIFT: 31
421
481
  } as const
422
482
 
423
483
  export const WORD1 = {
@@ -434,7 +494,8 @@ export const WORD2 = {
434
494
  SECTION_X_HI_SHIFT: 19,
435
495
  SECTION_Z_HI_SHIFT: 25,
436
496
  SECTION_HI_BITS: 6,
437
- SPARE_BITS: 1
497
+ /** su<0 mirror flag — no free bits remain in word2 after this. */
498
+ FLIP_U_SHIFT: 31
438
499
  } as const
439
500
 
440
501
  /** Section base X/Z: low 16 bits in a_w3, high 6 in a_w2 (22-bit biased section index). */
@@ -5,6 +5,7 @@ import type { ExportedSection, ExportedWorldGeometry } from '../mesher-shared/ex
5
5
  import { calculateSkyLightSimple } from '../lib/skyLight'
6
6
  import { bakeLegacyVertexColors } from '../lib/bakeLegacyLight'
7
7
  import { getShaderCubeResources } from '../wasm-mesher/bridge/shaderCubeBridge'
8
+ import { SHADER_CUBES_FORMAT_VERSION } from '../mesher-shared/shaderCubeFormat'
8
9
  import { createCubeBlockMaterial, setCubeSkyLevel } from './shaders/cubeBlockShader'
9
10
  import { createShaderCubeMesh } from './shaderCubeMesh'
10
11
 
@@ -181,6 +182,8 @@ export function createMeshesFromExport(
181
182
  ): THREE.Group[] {
182
183
  const groups: THREE.Group[] = []
183
184
  const resolvedShaderMat = shaderMaterial ?? shaderMaterialForExport(material, skyLevel)
185
+ let skippedShaderCubeSections = 0
186
+ let skippedShaderCubeFormatVersion: number | undefined
184
187
 
185
188
  for (const section of exportData.sections) {
186
189
  const group = new THREE.Group()
@@ -213,9 +216,14 @@ export function createMeshesFromExport(
213
216
 
214
217
  const shaderCubes = section.shaderCubes
215
218
  if (shaderCubes && shaderCubes.count > 0 && resolvedShaderMat) {
216
- const shaderMesh = createShaderCubeMesh(shaderCubes, resolvedShaderMat)
217
- shaderMesh.position.set(section.position.x, section.position.y, section.position.z)
218
- group.add(shaderMesh)
219
+ if (shaderCubes.formatVersion !== SHADER_CUBES_FORMAT_VERSION) {
220
+ skippedShaderCubeSections++
221
+ skippedShaderCubeFormatVersion ??= shaderCubes.formatVersion
222
+ } else {
223
+ const shaderMesh = createShaderCubeMesh(shaderCubes, resolvedShaderMat)
224
+ shaderMesh.position.set(section.position.x, section.position.y, section.position.z)
225
+ group.add(shaderMesh)
226
+ }
219
227
  }
220
228
 
221
229
  if (group.children.length > 0) {
@@ -223,6 +231,13 @@ export function createMeshesFromExport(
223
231
  }
224
232
  }
225
233
 
234
+ if (skippedShaderCubeSections > 0) {
235
+ console.warn(
236
+ `[worldGeometryExport] Skipped ${skippedShaderCubeSections} shader-cube section(s): ` +
237
+ `export format ${skippedShaderCubeFormatVersion} ≠ current ${SHADER_CUBES_FORMAT_VERSION}`
238
+ )
239
+ }
240
+
226
241
  return groups
227
242
  }
228
243