minecraft-renderer 0.1.27 → 0.1.29

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 (37) hide show
  1. package/dist/mesher.js +22 -22
  2. package/dist/mesher.js.map +3 -3
  3. package/dist/mesherWasm.js +17 -17
  4. package/dist/minecraft-renderer.js +65 -619
  5. package/dist/minecraft-renderer.js.meta.json +1 -0
  6. package/dist/threeWorker.js +453 -1007
  7. package/package.json +1 -1
  8. package/src/lib/guiRenderer.ts +0 -1
  9. package/src/lib/moreBlockDataGenerated.json +8 -0
  10. package/src/lib/skyLight.ts +125 -0
  11. package/src/lib/worldrendererCommon.ts +2 -12
  12. package/src/mesher/models.ts +28 -2
  13. package/src/mesher/test/mesherTester.ts +1 -1
  14. package/src/sign-renderer/index.ts +4 -1
  15. package/src/three/bannerRenderer.ts +5 -4
  16. package/src/three/cinimaticScript.ts +2 -2
  17. package/src/three/entities.ts +42 -11
  18. package/src/three/entity/EntityMesh.ts +1 -1
  19. package/src/three/entity/entities.json +108 -2
  20. package/src/three/entity/exportedModels.js +0 -1
  21. package/src/three/entity/externalTextures.json +1 -1
  22. package/src/three/fireworks.ts +18 -5
  23. package/src/three/fireworksRenderer.ts +15 -13
  24. package/src/three/modules/rain.ts +6 -1
  25. package/src/three/modules/sciFiWorldReveal.ts +14 -10
  26. package/src/three/modules/starfield.ts +1 -1
  27. package/src/three/sceneOrigin.ts +215 -0
  28. package/src/three/skyboxRenderer.ts +3 -3
  29. package/src/three/threeJsMedia.ts +12 -6
  30. package/src/three/threeJsParticles.ts +42 -14
  31. package/src/three/threeJsSound.ts +3 -3
  32. package/src/three/waypointSprite.ts +45 -23
  33. package/src/three/waypoints.ts +12 -4
  34. package/src/three/world/cursorBlock.ts +5 -5
  35. package/src/three/worldBlockGeometry.ts +14 -5
  36. package/src/three/worldGeometryExport.ts +4 -3
  37. package/src/three/worldRendererThree.ts +155 -30
@@ -58,7 +58,7 @@ export class CursorBlock {
58
58
  this.blockBreakMesh.visible = false
59
59
  this.blockBreakMesh.renderOrder = 999
60
60
  this.blockBreakMesh.name = 'blockBreakMesh'
61
- this.worldRenderer.scene.add(this.blockBreakMesh)
61
+ this.worldRenderer.sceneOrigin.addAndTrack(this.blockBreakMesh)
62
62
 
63
63
  this.worldRenderer.onReactivePlayerStateUpdated('gameMode', () => {
64
64
  this.updateLineMaterial()
@@ -132,7 +132,7 @@ export class CursorBlock {
132
132
  return
133
133
  }
134
134
  if (this.interactionLines !== null) {
135
- this.worldRenderer.scene.remove(this.interactionLines.mesh)
135
+ this.worldRenderer.sceneOrigin.removeAndUntrack(this.interactionLines.mesh)
136
136
  this.interactionLines = null
137
137
  }
138
138
  if (blockPos === null) {
@@ -146,12 +146,12 @@ export class CursorBlock {
146
146
  const geometry = new THREE.BoxGeometry(...scale)
147
147
  const lines = new LineSegmentsGeometry().fromEdgesGeometry(new THREE.EdgesGeometry(geometry))
148
148
  const wireframe = new Wireframe(lines, this.cursorLineMaterial)
149
- const pos = blockPos.plus(position)
150
- wireframe.position.set(pos.x, pos.y, pos.z)
149
+ wireframe.position.set(position.x, position.y, position.z)
151
150
  wireframe.computeLineDistances()
152
151
  group.add(wireframe)
153
152
  }
154
- this.worldRenderer.scene.add(group)
153
+ this.worldRenderer.sceneOrigin.addAndTrack(group)
154
+ group.position.set(blockPos.x, blockPos.y, blockPos.z)
155
155
  group.visible = !this.cursorLinesHidden
156
156
  this.interactionLines = { blockPos, mesh: group, shapePositions }
157
157
  }
@@ -36,7 +36,7 @@ export class WorldBlockGeometry {
36
36
  releaseBannerTexture((child as any).bannerTexture)
37
37
  }
38
38
  })
39
- this.scene.remove(object)
39
+ this.worldRenderer.sceneOrigin.removeAndUntrackAll(object)
40
40
  disposeObject(object)
41
41
  delete this.sectionObjects[data.key]
42
42
  }
@@ -66,6 +66,7 @@ export class WorldBlockGeometry {
66
66
  this.addSectionMemoryUsage(geometry)
67
67
 
68
68
  const mesh = new THREE.Mesh(geometry, this.material)
69
+ this.worldRenderer.sceneOrigin.track(mesh, { updateMatrix: true })
69
70
  mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
70
71
  mesh.name = 'mesh'
71
72
  object = new THREE.Group()
@@ -77,9 +78,10 @@ export class WorldBlockGeometry {
77
78
  new THREE.BoxGeometry(CHUNK_SIZE, sectionHeight, CHUNK_SIZE),
78
79
  new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 })
79
80
  )
80
- staticChunkMesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
81
81
  const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
82
82
  boxHelper.name = 'helper'
83
+ this.worldRenderer.sceneOrigin.track(boxHelper, { updateMatrix: true })
84
+ boxHelper.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
83
85
  object.add(boxHelper)
84
86
  object.name = 'chunk'
85
87
  ; (object as any).tilesCount = data.geometry.positions.length / 3 / 4
@@ -127,10 +129,15 @@ export class WorldBlockGeometry {
127
129
  const bannerTexture = getBannerTexture(this.worldRenderer, blockName, nbt.simplify(bannerBlockEntity))
128
130
  if (!bannerTexture) continue
129
131
  const banner = createBannerMesh(new Vec3(+x, +y, +z), rotation, isWall, bannerTexture)
132
+ const { x: bwx, y: bwy, z: bwz } = banner.position
133
+ this.worldRenderer.sceneOrigin.track(banner)
134
+ banner.position.set(bwx, bwy, bwz)
130
135
  object.add(banner)
131
136
  }
132
137
  }
133
138
  this.sectionObjects[data.key] = object
139
+ // Store section key on object for easier lookup
140
+ ;(object as any).sectionKey = data.key
134
141
  if (this.displayOptions.inWorldRenderingConfig._renderByChunks) {
135
142
  object.visible = false
136
143
  const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
@@ -144,6 +151,8 @@ export class WorldBlockGeometry {
144
151
 
145
152
  this.worldRenderer.updatePosDataChunk(data.key)
146
153
  object.matrixAutoUpdate = false
154
+ // Force matrix update after setting camera-relative position (matrixAutoUpdate is false)
155
+ object.updateMatrix()
147
156
  mesh.onAfterRender = (renderer, scene, camera, geometry, material, group) => {
148
157
  // mesh.matrixAutoUpdate = false
149
158
  }
@@ -222,7 +231,7 @@ export class WorldBlockGeometry {
222
231
  for (const mesh of Object.values(this.sectionObjects)) {
223
232
  // Track memory usage removal for all sections
224
233
  this.removeSectionMemoryUsage(mesh)
225
- this.scene.remove(mesh)
234
+ this.worldRenderer.sceneOrigin.removeAndUntrackAll(mesh)
226
235
  }
227
236
  this.sectionObjects = {}
228
237
  this.waitingChunksToDisplay = {}
@@ -249,7 +258,7 @@ export class WorldBlockGeometry {
249
258
  releaseBannerTexture((child as any).bannerTexture)
250
259
  }
251
260
  })
252
- this.scene.remove(mesh)
261
+ this.worldRenderer.sceneOrigin.removeAndUntrackAll(mesh)
253
262
  disposeObject(mesh)
254
263
  }
255
264
  delete this.sectionObjects[key]
@@ -267,7 +276,7 @@ export class WorldBlockGeometry {
267
276
  releaseBannerTexture((child as any).bannerTexture)
268
277
  }
269
278
  })
270
- this.scene.remove(mesh)
279
+ this.worldRenderer.sceneOrigin.removeAndUntrackAll(mesh)
271
280
  disposeObject(mesh)
272
281
  }
273
282
  delete this.sectionObjects[key]
@@ -52,12 +52,13 @@ export function exportWorldGeometry(
52
52
 
53
53
  if (!positionAttr || !indexAttr) continue
54
54
 
55
+ const wp = worldRenderer.sceneOrigin.getWorldPosition(mesh)
55
56
  sections.push({
56
57
  key,
57
58
  position: {
58
- x: mesh.position.x,
59
- y: mesh.position.y,
60
- z: mesh.position.z
59
+ x: wp?.x ?? worldRenderer.sceneOrigin.toWorldX(mesh.position.x),
60
+ y: wp?.y ?? worldRenderer.sceneOrigin.toWorldY(mesh.position.y),
61
+ z: wp?.z ?? worldRenderer.sceneOrigin.toWorldZ(mesh.position.z)
61
62
  },
62
63
  geometry: {
63
64
  positions: [...positionAttr.array],
@@ -31,6 +31,7 @@ import { FireworksRenderer } from './fireworksRenderer'
31
31
  import { CinimaticScriptRunner, CinimaticScript } from './cinimaticScript'
32
32
  import { DEFAULT_TEMPERATURE, SkyboxRenderer } from './skyboxRenderer'
33
33
  import { FireworksManager } from './fireworks'
34
+ import { SceneOrigin } from './sceneOrigin'
34
35
  import { downloadWorldGeometry } from './worldGeometryExport'
35
36
  import { WorldBlockGeometry } from './worldBlockGeometry'
36
37
  import type { RendererModuleManifest, RegisteredModule, RendererModuleController } from './rendererModuleSystem'
@@ -67,6 +68,14 @@ export class WorldRendererThree extends WorldRendererCommon {
67
68
  }
68
69
  waypoints: WaypointsRenderer
69
70
  cinimaticScript: CinimaticScriptRunner
71
+ /**
72
+ * Three.js camera used for rendering.
73
+ *
74
+ * **WARNING:** `camera.position` is scene-local (near origin due to sceneOrigin rebasing),
75
+ * NOT world-space. In first-person mode it's `(0,0,0)`; in third-person it's `(0,0,zOffset)`.
76
+ *
77
+ * Use `getCameraPosition()` or `cameraWorldPos` for actual world-space coordinates.
78
+ */
70
79
  camera!: THREE.PerspectiveCamera
71
80
  renderTimeAvg = 0
72
81
  // Memory usage tracking (in bytes)
@@ -97,10 +106,29 @@ export class WorldRendererThree extends WorldRendererCommon {
97
106
  DEBUG_RAYCAST = false
98
107
  skyboxRenderer: SkyboxRenderer
99
108
  fireworks: FireworksManager
109
+ sceneOrigin = new SceneOrigin(this.scene)
110
+ /** Camera world position stored in float64 (JS number) for precision */
111
+ cameraWorldPos = { x: 0, y: 0, z: 0 }
112
+
113
+ /** Whether we've warned about camera.position access (one-time dev warning) */
114
+ private _cameraPositionAccessWarned = false
115
+
116
+ private readonly _tmpCameraPos = new THREE.Vector3()
100
117
 
101
- private currentPosTween?: tweenJs.Tween<THREE.Vector3>
118
+ private currentPosTween?: tweenJs.Tween<{ x: number, y: number, z: number }>
102
119
  private currentRotTween?: tweenJs.Tween<{ pitch: number, yaw: number }>
103
120
 
121
+ // Pre-allocated objects for getThirdPersonCamera (avoid per-frame allocs)
122
+ private readonly _tpDirection = new THREE.Vector3()
123
+ private readonly _tpPitchQuat = new THREE.Quaternion()
124
+ private readonly _tpYawQuat = new THREE.Quaternion()
125
+ private readonly _tpFinalQuat = new THREE.Quaternion()
126
+ private readonly _tpScenePos = new THREE.Vector3()
127
+ private readonly _tpAxisX = new THREE.Vector3(1, 0, 0)
128
+ private readonly _tpAxisY = new THREE.Vector3(0, 1, 0)
129
+ private readonly _tpRaycaster = new THREE.Raycaster()
130
+ private readonly _tpChunkWorldPos = new THREE.Vector3()
131
+
104
132
  get tilesRendered() {
105
133
  return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
106
134
  }
@@ -146,13 +174,13 @@ export class WorldRendererThree extends WorldRendererCommon {
146
174
  (pos, yaw, pitch) => this.setCinimaticCamera(pos, yaw, pitch),
147
175
  (fov) => this.setCinimaticFov(fov),
148
176
  () => ({
149
- position: new Vec3(this.cameraObject.position.x, this.cameraObject.position.y, this.cameraObject.position.z),
177
+ position: new Vec3(this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z),
150
178
  yaw: this.cameraShake.getBaseRotation().yaw,
151
179
  pitch: this.cameraShake.getBaseRotation().pitch,
152
180
  fov: this.camera.fov
153
181
  })
154
182
  )
155
- this.fireworks = new FireworksManager(this.scene)
183
+ this.fireworks = new FireworksManager(this.scene, this.sceneOrigin)
156
184
 
157
185
  // this.fountain = new Fountain(this.scene, this.scene, {
158
186
  // position: new THREE.Vector3(0, 10, 0),
@@ -313,10 +341,65 @@ export class WorldRendererThree extends WorldRendererCommon {
313
341
  return Object.values(this.modules).some(m => m.enabled && m.manifest.requiresHeightmap)
314
342
  }
315
343
 
344
+ /** Returns the active camera container (may differ in VR mode). Used for position resets and rotation. */
316
345
  get cameraObject() {
317
346
  return this.cameraGroupVr ?? this.cameraContainer
318
347
  }
319
348
 
349
+ /**
350
+ * Wraps camera.position in a Proxy that logs a one-time warning when .set/.setX/.setY/.setZ
351
+ * or .x/.y/.z assignment is used with values that look like world coords (|v| > 20).
352
+ * camera.position is scene-local (0,0,0 or 0,0,zOffset). Use cameraWorldPos + sceneOrigin.update().
353
+ */
354
+ private _wrapCameraPositionWithWarning() {
355
+ const realPos = this.camera.position
356
+ const self = this
357
+ const WORLD_COORD_THRESHOLD = 20 // our zOffset is ~4, so 20 catches mistaken world coords
358
+ const looksLikeWorldCoords = (x: number, y: number, z: number) =>
359
+ Math.abs(x) > WORLD_COORD_THRESHOLD || Math.abs(y) > WORLD_COORD_THRESHOLD || Math.abs(z) > WORLD_COORD_THRESHOLD
360
+ const warnOnce = () => {
361
+ if (!self._cameraPositionAccessWarned && typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
362
+ self._cameraPositionAccessWarned = true
363
+ console.warn(
364
+ '[WorldRendererThree] Do not set camera.position to world coordinates — it is scene-local. ' +
365
+ 'Use cameraWorldPos and sceneOrigin.update() to move the camera.'
366
+ )
367
+ }
368
+ }
369
+ const proxy = new Proxy(realPos, {
370
+ set(target, prop, value) {
371
+ if ((prop === 'x' || prop === 'y' || prop === 'z') && typeof value === 'number' && Math.abs(value) > WORLD_COORD_THRESHOLD) {
372
+ warnOnce()
373
+ }
374
+ ;(target as any)[prop] = value
375
+ return true
376
+ },
377
+ get(target, prop, receiver) {
378
+ const value = (target as any)[prop]
379
+ if (prop === 'set') {
380
+ return function (x: number, y: number, z: number) {
381
+ if (looksLikeWorldCoords(x, y, z)) warnOnce()
382
+ return (target as THREE.Vector3).set(x, y, z)
383
+ }
384
+ }
385
+ if (prop === 'setX' || prop === 'setY' || prop === 'setZ') {
386
+ return function (v: number) {
387
+ if (Math.abs(v) > WORLD_COORD_THRESHOLD) warnOnce()
388
+ return (target as any)[prop](v)
389
+ }
390
+ }
391
+ if (prop === 'copy') {
392
+ return function (v: THREE.Vector3) {
393
+ if (looksLikeWorldCoords(v.x, v.y, v.z)) warnOnce()
394
+ return (target as THREE.Vector3).copy(v)
395
+ }
396
+ }
397
+ return typeof value === 'function' ? value.bind(target) : value
398
+ }
399
+ })
400
+ Object.defineProperty(this.camera, 'position', { value: proxy, configurable: true, enumerable: true })
401
+ }
402
+
320
403
  worldSwitchActions() {
321
404
  this.onWorldSwitched.push(() => {
322
405
  // clear custom blocks
@@ -333,7 +416,7 @@ export class WorldRendererThree extends WorldRendererCommon {
333
416
  }
334
417
 
335
418
  downloadWorldGeometry() {
336
- downloadWorldGeometry(this, this.cameraObject.position, this.cameraShake.getBaseRotation(), 'world-geometry.json')
419
+ downloadWorldGeometry(this, new THREE.Vector3(this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z), this.cameraShake.getBaseRotation(), 'world-geometry.json')
337
420
  }
338
421
 
339
422
  updateEntity(e, isPosUpdate = false) {
@@ -358,6 +441,11 @@ export class WorldRendererThree extends WorldRendererCommon {
358
441
  }
359
442
 
360
443
  resetScene() {
444
+ this.sceneOrigin.update(0, 0, 0)
445
+ this.cameraWorldPos.x = 0
446
+ this.cameraWorldPos.y = 0
447
+ this.cameraWorldPos.z = 0
448
+
361
449
  this.scene.matrixAutoUpdate = false // for perf
362
450
  this.scene.background = new THREE.Color(this.initOptions.config.sceneBackground)
363
451
  this.scene.add(this.ambientLight)
@@ -367,6 +455,7 @@ export class WorldRendererThree extends WorldRendererCommon {
367
455
 
368
456
  const size = this.renderer.getSize(new THREE.Vector2())
369
457
  this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
458
+ this._wrapCameraPositionWithWarning()
370
459
  this.cameraContainer = new THREE.Object3D()
371
460
  this.cameraContainer.add(this.camera)
372
461
  this.scene.add(this.cameraContainer)
@@ -396,7 +485,7 @@ export class WorldRendererThree extends WorldRendererCommon {
396
485
  })
397
486
  this.onReactivePlayerStateUpdated('perspective', (value) => {
398
487
  // Update camera perspective when it changes
399
- const vecPos = new Vec3(this.cameraObject.position.x, this.cameraObject.position.y, this.cameraObject.position.z)
488
+ const vecPos = new Vec3(this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z)
400
489
  this.updateCamera(vecPos, this.cameraShake.getBaseRotation().yaw, this.cameraShake.getBaseRotation().pitch)
401
490
  // todo also update camera when block within camera was changed
402
491
  })
@@ -654,10 +743,8 @@ export class WorldRendererThree extends WorldRendererCommon {
654
743
  return tex
655
744
  }
656
745
 
657
- getCameraPosition() {
658
- const worldPos = new THREE.Vector3()
659
- this.camera.getWorldPosition(worldPos)
660
- return worldPos
746
+ getCameraPosition(target?: THREE.Vector3): THREE.Vector3 {
747
+ return (target ?? this._tmpCameraPos).set(this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z)
661
748
  }
662
749
 
663
750
  getSectionCameraPosition() {
@@ -686,7 +773,7 @@ export class WorldRendererThree extends WorldRendererCommon {
686
773
  }
687
774
 
688
775
  getThirdPersonCamera(pos: THREE.Vector3 | null, yaw: number, pitch: number) {
689
- pos ??= this.cameraObject.position
776
+ pos ??= new THREE.Vector3(this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z)
690
777
 
691
778
  // Calculate camera offset based on perspective
692
779
  const isBack = this.playerStateReactive.perspective === 'third_person_back'
@@ -697,12 +784,12 @@ export class WorldRendererThree extends WorldRendererCommon {
697
784
 
698
785
  // Create a direction vector that represents where the camera is looking
699
786
  // This matches the Three.js camera coordinate system
700
- const direction = new THREE.Vector3(0, 0, -1) // Forward direction in camera space
787
+ const direction = this._tpDirection.set(0, 0, -1) // Forward direction in camera space
701
788
 
702
789
  // Apply the same rotation that's applied to the camera container
703
- const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitch)
704
- const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yaw)
705
- const finalQuat = new THREE.Quaternion().multiplyQuaternions(yawQuat, pitchQuat)
790
+ const pitchQuat = this._tpPitchQuat.setFromAxisAngle(this._tpAxisX, pitch)
791
+ const yawQuat = this._tpYawQuat.setFromAxisAngle(this._tpAxisY, yaw)
792
+ const finalQuat = this._tpFinalQuat.multiplyQuaternions(yawQuat, pitchQuat)
706
793
 
707
794
  // Transform the direction vector by the camera's rotation
708
795
  direction.applyQuaternion(finalQuat)
@@ -718,9 +805,16 @@ export class WorldRendererThree extends WorldRendererCommon {
718
805
  this.debugRaycast(pos, direction, distance)
719
806
  }
720
807
 
808
+ // Convert world position to scene-relative coordinates for raycasting
809
+ const scenePos = this._tpScenePos.set(
810
+ this.sceneOrigin.toSceneX(pos.x),
811
+ this.sceneOrigin.toSceneY(pos.y),
812
+ this.sceneOrigin.toSceneZ(pos.z)
813
+ )
814
+
721
815
  // Perform raycast to avoid camera going through blocks
722
- const raycaster = new THREE.Raycaster()
723
- raycaster.set(pos, direction)
816
+ const raycaster = this._tpRaycaster
817
+ raycaster.set(scenePos, direction)
724
818
  raycaster.far = distance // Limit raycast distance
725
819
 
726
820
  // Filter to only nearby chunks for performance
@@ -732,9 +826,9 @@ export class WorldRendererThree extends WorldRendererCommon {
732
826
  if (!mesh) return false
733
827
 
734
828
  // Check distance from player position to chunk
735
- const chunkWorldPos = new THREE.Vector3()
829
+ const chunkWorldPos = this._tpChunkWorldPos
736
830
  mesh.getWorldPosition(chunkWorldPos)
737
- const distance = pos.distanceTo(chunkWorldPos)
831
+ const distance = scenePos.distanceTo(chunkWorldPos)
738
832
  return distance < 80 // Only check chunks within 80 blocks
739
833
  })
740
834
 
@@ -776,10 +870,17 @@ export class WorldRendererThree extends WorldRendererCommon {
776
870
  this.debugHitPoint = undefined
777
871
  }
778
872
 
873
+ // Convert world position to scene-relative coordinates
874
+ const scenePos = new THREE.Vector3(
875
+ this.sceneOrigin.toSceneX(pos.x),
876
+ this.sceneOrigin.toSceneY(pos.y),
877
+ this.sceneOrigin.toSceneZ(pos.z)
878
+ )
879
+
779
880
  // Create raycast arrow
780
881
  this.debugRaycastHelper = new THREE.ArrowHelper(
781
882
  direction.clone().normalize(),
782
- pos,
883
+ scenePos,
783
884
  distance,
784
885
  0xff_00_00, // Red color
785
886
  distance * 0.1,
@@ -791,7 +892,7 @@ export class WorldRendererThree extends WorldRendererCommon {
791
892
  const hitGeometry = new THREE.SphereGeometry(0.2, 8, 8)
792
893
  const hitMaterial = new THREE.MeshBasicMaterial({ color: 0x00_ff_00 })
793
894
  this.debugHitPoint = new THREE.Mesh(hitGeometry, hitMaterial)
794
- this.debugHitPoint.position.copy(pos).add(direction.clone().multiplyScalar(distance))
895
+ this.debugHitPoint.position.copy(scenePos).add(direction.clone().multiplyScalar(distance))
795
896
  this.scene.add(this.debugHitPoint)
796
897
  }
797
898
 
@@ -799,7 +900,11 @@ export class WorldRendererThree extends WorldRendererCommon {
799
900
 
800
901
  setCinimaticCamera(pos: Vec3, yaw: number, pitch: number): void {
801
902
  // Directly set camera position and rotation for cinematic mode
802
- this.cameraObject.position.set(pos.x, pos.y, pos.z)
903
+ this.cameraWorldPos.x = pos.x
904
+ this.cameraWorldPos.y = pos.y
905
+ this.cameraWorldPos.z = pos.z
906
+ this.sceneOrigin.update(pos.x, pos.y, pos.z)
907
+ this.cameraObject.position.set(0, 0, 0)
803
908
  this.cameraShake.setBaseRotation(pitch, yaw)
804
909
  this.updateCameraSectionPos()
805
910
  }
@@ -831,7 +936,13 @@ export class WorldRendererThree extends WorldRendererCommon {
831
936
  const tweenDelay = this.displayOptions.inWorldRenderingConfig.instantCameraUpdate
832
937
  ? 0
833
938
  : (this.playerStateUtils.isSpectatingEntity() ? 150 : 50)
834
- this.currentPosTween = new tweenJs.Tween(this.cameraObject.position).to({ x: pos.x, y: pos.y, z: pos.z }, tweenDelay).start()
939
+ this.currentPosTween = new tweenJs.Tween(this.cameraWorldPos)
940
+ .to({ x: pos.x, y: pos.y, z: pos.z }, tweenDelay)
941
+ .onUpdate(() => {
942
+ this.sceneOrigin.update(this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z)
943
+ this.cameraObject.position.set(0, 0, 0)
944
+ })
945
+ .start()
835
946
  // this.freeFlyState.position = pos
836
947
  }
837
948
 
@@ -855,14 +966,14 @@ export class WorldRendererThree extends WorldRendererCommon {
855
966
  const { perspective } = this.playerStateReactive
856
967
  if (perspective === 'third_person_back' || perspective === 'third_person_front') {
857
968
  // Use getThirdPersonCamera for proper raycasting with max distance of 4
858
- const currentCameraPos = this.cameraObject.position
969
+ const currentWorldPos = new THREE.Vector3(this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z)
859
970
  const thirdPersonPos = this.getThirdPersonCamera(
860
- new THREE.Vector3(currentCameraPos.x, currentCameraPos.y, currentCameraPos.z),
971
+ currentWorldPos,
861
972
  yaw,
862
973
  pitch
863
974
  )
864
975
 
865
- const distance = currentCameraPos.distanceTo(new THREE.Vector3(thirdPersonPos.x, thirdPersonPos.y, thirdPersonPos.z))
976
+ const distance = currentWorldPos.distanceTo(new THREE.Vector3(thirdPersonPos.x, thirdPersonPos.y, thirdPersonPos.z))
866
977
  // Apply Z offset based on perspective and calculated distance
867
978
  const zOffset = perspective === 'third_person_back' ? distance : -distance
868
979
  this.camera.position.set(0, 0, zOffset)
@@ -1014,6 +1125,7 @@ export class WorldRendererThree extends WorldRendererCommon {
1014
1125
  // move head model down as armor have a different offset than blocks
1015
1126
  mesh.position.y -= 23 / 16
1016
1127
  group.add(mesh)
1128
+ this.sceneOrigin.track(group)
1017
1129
  group.position.set(position.x + 0.5, position.y + 0.045, position.z + 0.5)
1018
1130
  group.rotation.set(
1019
1131
  0,
@@ -1065,6 +1177,7 @@ export class WorldRendererThree extends WorldRendererCommon {
1065
1177
  const height = (isHanging ? 10 : 8) / 16
1066
1178
  const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
1067
1179
  const textPosition = height / 2 + heightOffset
1180
+ this.sceneOrigin.track(group)
1068
1181
  group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
1069
1182
  return group
1070
1183
  }
@@ -1180,12 +1293,24 @@ export class WorldRendererThree extends WorldRendererCommon {
1180
1293
  }
1181
1294
 
1182
1295
  shouldObjectVisible(object: THREE.Object3D) {
1183
- // Get chunk coordinates
1296
+ // Get chunk coordinates - use world coords from userData if available, otherwise convert from scene coords
1184
1297
  const CHUNK_SIZE = 16
1185
1298
  const sectionHeight = this.getSectionHeight()
1186
- const chunkX = Math.floor(object.position.x / CHUNK_SIZE) * CHUNK_SIZE
1187
- const chunkZ = Math.floor(object.position.z / CHUNK_SIZE) * CHUNK_SIZE
1188
- const sectionY = Math.floor(object.position.y / sectionHeight) * sectionHeight
1299
+ let worldX: number, worldY: number, worldZ: number
1300
+ const wp = this.sceneOrigin.getWorldPosition(object)
1301
+ if (wp) {
1302
+ worldX = wp.x
1303
+ worldY = wp.y
1304
+ worldZ = wp.z
1305
+ } else {
1306
+ // Fallback for untracked objects: convert scene coords back to world
1307
+ worldX = this.sceneOrigin.toWorldX(object.position.x)
1308
+ worldY = this.sceneOrigin.toWorldY(object.position.y)
1309
+ worldZ = this.sceneOrigin.toWorldZ(object.position.z)
1310
+ }
1311
+ const chunkX = Math.floor(worldX / CHUNK_SIZE) * CHUNK_SIZE
1312
+ const chunkZ = Math.floor(worldZ / CHUNK_SIZE) * CHUNK_SIZE
1313
+ const sectionY = Math.floor(worldY / sectionHeight) * sectionHeight
1189
1314
 
1190
1315
  const chunkKey = `${chunkX},${chunkZ}`
1191
1316
  const sectionKey = `${chunkX},${sectionY},${chunkZ}`
@@ -1231,7 +1356,7 @@ export class WorldRendererThree extends WorldRendererCommon {
1231
1356
  }
1232
1357
  }
1233
1358
 
1234
- // Apply the offset to the section object
1359
+ // Apply the offset to the section object (compose with camera-relative base position)
1235
1360
  const section = this.sectionObjects[key]
1236
1361
  if (section) {
1237
1362
  section.position.set(