minecraft-renderer 0.1.29 → 0.1.31

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.29",
3
+ "version": "0.1.31",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,6 +25,10 @@ export const getInitialPlayerState = (): PlayerStateReactive => proxy({
25
25
  sneaking: false,
26
26
  flying: false,
27
27
  sprinting: false,
28
+ walkDist: 0,
29
+ prevWalkDist: 0,
30
+ bob: 0,
31
+ prevBob: 0,
28
32
  itemUsageTicks: 0,
29
33
  username: '',
30
34
  onlineMode: false,
@@ -1,95 +1,39 @@
1
1
  //@ts-nocheck
2
- export class CameraBobbing {
3
- private walkDistance = 0
4
- private prevWalkDistance = 0
5
- private bobAmount = 0
6
- private prevBobAmount = 0
7
- private readonly gameTimer = new GameTimer()
8
-
9
- // eslint-disable-next-line max-params
10
- constructor (
11
- private readonly BOB_FREQUENCY: number = Math.PI, // How fast the bob cycles
12
- private readonly BOB_BASE_AMPLITUDE: number = 0.5, // Base amplitude of the bob
13
- private readonly VERTICAL_MULTIPLIER: number = 1, // Vertical movement multiplier
14
- private readonly ROTATION_MULTIPLIER_Z: number = 3, // Roll rotation multiplier
15
- private readonly ROTATION_MULTIPLIER_X: number = 5 // Pitch rotation multiplier
16
- ) {}
17
-
18
- // Call this when player is moving
19
- public updateWalkDistance (distance: number): void {
20
- this.prevWalkDistance = this.walkDistance
21
- this.walkDistance = distance
22
- }
23
-
24
- // Call this when player is moving to update bob amount
25
- public updateBobAmount (isMoving: boolean): void {
26
- const targetBob = isMoving ? 1 : 0
27
- this.prevBobAmount = this.bobAmount
28
-
29
- // Update timing
30
- const ticks = this.gameTimer.update()
31
- const deltaTime = ticks / 20 // Convert ticks to seconds assuming 20 TPS
32
-
33
- // Smooth transition for bob amount
34
- const bobDelta = (targetBob - this.bobAmount) * Math.min(1, deltaTime * 10)
35
- this.bobAmount += bobDelta
36
- }
37
-
38
- // Call this in your render/animation loop
39
- public getBobbing (): { position: { x: number, y: number }, rotation: { x: number, z: number } } {
40
- // Interpolate walk distance
41
- const walkDist = this.prevWalkDistance +
42
- (this.walkDistance - this.prevWalkDistance) * this.gameTimer.partialTick
43
-
44
- // Interpolate bob amount
45
- const bob = this.prevBobAmount +
46
- (this.bobAmount - this.prevBobAmount) * this.gameTimer.partialTick
47
-
48
- // Calculate total distance for bob cycle
49
- const totalDist = -(walkDist * this.BOB_FREQUENCY)
50
-
51
- // Calculate offsets
52
- const xOffset = Math.sin(totalDist) * bob * this.BOB_BASE_AMPLITUDE
53
- const yOffset = -Math.abs(Math.cos(totalDist) * bob) * this.VERTICAL_MULTIPLIER
54
-
55
- // Calculate rotations (in radians)
56
- const zRot = (Math.sin(totalDist) * bob * this.ROTATION_MULTIPLIER_Z) * (Math.PI / 180)
57
- const xRot = (Math.abs(Math.cos(totalDist - 0.2) * bob) * this.ROTATION_MULTIPLIER_X) * (Math.PI / 180)
58
-
59
- return {
60
- position: { x: xOffset, y: yOffset },
61
- rotation: { x: xRot, z: zRot }
62
- }
63
- }
2
+ export interface CameraBobResult {
3
+ position: { x: number; y: number }
4
+ rotation: { x: number; z: number }
64
5
  }
65
6
 
66
- class GameTimer {
67
- private readonly msPerTick: number
68
- private lastMs: number
69
- public partialTick = 0
70
-
71
- constructor (tickRate = 20) {
72
- this.msPerTick = 1000 / tickRate
73
- this.lastMs = performance.now()
74
- }
75
-
76
- update (): number {
77
- const currentMs = performance.now()
78
- const deltaSinceLastTick = currentMs - this.lastMs
7
+ export interface CameraBobInput {
8
+ walkDist: number
9
+ prevWalkDist: number
10
+ bob: number
11
+ prevBob: number
12
+ partialTick: number
13
+ }
79
14
 
80
- // Calculate how much of a tick has passed
81
- const tickDelta = deltaSinceLastTick / this.msPerTick
82
- this.lastMs = currentMs
15
+ const DEG_TO_RAD = Math.PI / 180
83
16
 
84
- // Add to accumulated partial ticks
85
- this.partialTick += tickDelta
17
+ export function computeCameraBob (input: CameraBobInput): CameraBobResult {
18
+ const { walkDist, prevWalkDist, bob, prevBob, partialTick } = input
86
19
 
87
- // Get whole number of ticks that should occur
88
- const wholeTicks = Math.floor(this.partialTick)
20
+ // Vanilla uses "backwards interpolation": -(walkDist + delta * partialTick)
21
+ // See ClientAvatarState.getBackwardsInterpolatedWalkDistance()
22
+ const walkDelta = walkDist - prevWalkDist
23
+ const interpolatedWalkDist = -(walkDist + walkDelta * partialTick)
24
+ const interpolatedBob = prevBob + (bob - prevBob) * partialTick
89
25
 
90
- // Keep the remainder as the new partial tick
91
- this.partialTick -= wholeTicks
26
+ const sinWalk = Math.sin(interpolatedWalkDist * Math.PI)
27
+ const cosWalk = Math.cos(interpolatedWalkDist * Math.PI)
92
28
 
93
- return wholeTicks
29
+ return {
30
+ position: {
31
+ x: sinWalk * interpolatedBob * 0.5,
32
+ y: -Math.abs(cosWalk * interpolatedBob)
33
+ },
34
+ rotation: {
35
+ x: Math.abs(Math.cos(interpolatedWalkDist * Math.PI - 0.2) * interpolatedBob) * 5 * DEG_TO_RAD,
36
+ z: sinWalk * interpolatedBob * 3 * DEG_TO_RAD
37
+ }
94
38
  }
95
39
  }
@@ -32,6 +32,10 @@ export const getInitialPlayerState = () => proxy({
32
32
  sneaking: false,
33
33
  flying: false,
34
34
  sprinting: false,
35
+ walkDist: 0,
36
+ prevWalkDist: 0,
37
+ bob: 0,
38
+ prevBob: 0,
35
39
  itemUsageTicks: 0,
36
40
  username: '',
37
41
  onlineMode: false,
@@ -1,5 +1,6 @@
1
1
  //@ts-nocheck
2
2
  import * as THREE from 'three'
3
+ import { computeCameraBob, type CameraBobInput } from '../lib/cameraBobbing'
3
4
  import { WorldRendererThree } from './worldRendererThree'
4
5
 
5
6
  export class CameraShake {
@@ -9,6 +10,11 @@ export class CameraShake {
9
10
  private rollAnimation?: { startTime: number, startRoll: number, targetRoll: number, duration: number, returnToZero?: boolean }
10
11
  private basePitch = 0
11
12
  private baseYaw = 0
13
+ private cameraBobInput: CameraBobInput | null = null
14
+
15
+ setCameraBobInput(input: CameraBobInput | null) {
16
+ this.cameraBobInput = input
17
+ }
12
18
 
13
19
  constructor(public worldRenderer: WorldRendererThree, public onRenderCallbacks: Array<(deltaTime: number) => void>) {
14
20
  onRenderCallbacks.push(() => {
@@ -88,8 +94,26 @@ export class CameraShake {
88
94
  const pitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), pitchOffset)
89
95
  const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yawOffset)
90
96
  const rollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle))
91
- // Combine rotations in the correct order: pitch -> yaw -> roll
92
- const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat)
97
+
98
+ // Camera bobbing rotation
99
+ let bobRollQuat = new THREE.Quaternion()
100
+ let bobPitchQuat = new THREE.Quaternion()
101
+ const perspective = this.worldRenderer.playerStateReactive.perspective
102
+ if (this.cameraBobInput) {
103
+ const bob = computeCameraBob(this.cameraBobInput)
104
+ bobRollQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), bob.rotation.z)
105
+ bobPitchQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), bob.rotation.x)
106
+
107
+ // Apply position bobbing to the camera child (not cameraContainer) in first-person only
108
+ if (perspective === 'first_person') {
109
+ this.worldRenderer.camera.position.set(bob.position.x, bob.position.y, 0)
110
+ }
111
+ } else if (perspective === 'first_person') {
112
+ this.worldRenderer.camera.position.set(0, 0, 0)
113
+ }
114
+
115
+ // Combine: yaw * pitch * damageRoll * bobRoll(Z) * bobPitch(X) — vanilla applies Z then X
116
+ const finalQuat = yawQuat.multiply(pitchQuat).multiply(rollQuat).multiply(bobRollQuat).multiply(bobPitchQuat)
93
117
  camera.setRotationFromQuaternion(finalQuat)
94
118
  }
95
119
  }
@@ -0,0 +1,59 @@
1
+ //@ts-nocheck
2
+ import type { WorldRendererThree } from '../worldRendererThree'
3
+ import type { RendererModuleController, RendererModuleManifest } from '../rendererModuleSystem'
4
+
5
+ export class CameraBobbingModule implements RendererModuleController {
6
+ private enabled = false
7
+ private lastBobWalkDist = 0
8
+ private lastBobTickTime = 0
9
+
10
+ constructor(private readonly worldRenderer: WorldRendererThree) { }
11
+
12
+ enable(): void {
13
+ this.enabled = true
14
+ }
15
+
16
+ disable(): void {
17
+ this.enabled = false
18
+ this.worldRenderer.cameraShake.setCameraBobInput(null)
19
+ const { perspective } = this.worldRenderer.playerStateReactive
20
+ if (perspective === 'first_person') {
21
+ this.worldRenderer.camera.position.set(0, 0, 0)
22
+ }
23
+ }
24
+
25
+ render?: (deltaTime: number) => void = () => {
26
+ if (!this.enabled) return
27
+ const config = this.worldRenderer.displayOptions.inWorldRenderingConfig
28
+ const { perspective } = this.worldRenderer.playerStateReactive
29
+
30
+ if (config.viewBobbing && perspective === 'first_person') {
31
+ if (this.worldRenderer.playerStateReactive.walkDist !== this.lastBobWalkDist) {
32
+ this.lastBobTickTime = performance.now()
33
+ this.lastBobWalkDist = this.worldRenderer.playerStateReactive.walkDist
34
+ }
35
+ const partialTick = Math.min((performance.now() - this.lastBobTickTime) / 50, 1)
36
+
37
+ this.worldRenderer.cameraShake.setCameraBobInput({
38
+ walkDist: this.worldRenderer.playerStateReactive.walkDist,
39
+ prevWalkDist: this.worldRenderer.playerStateReactive.prevWalkDist,
40
+ bob: this.worldRenderer.playerStateReactive.bob,
41
+ prevBob: this.worldRenderer.playerStateReactive.prevBob,
42
+ partialTick
43
+ })
44
+ } else {
45
+ this.worldRenderer.cameraShake.setCameraBobInput(null)
46
+ }
47
+ }
48
+
49
+ dispose(): void {
50
+ this.disable()
51
+ }
52
+ }
53
+
54
+ export const cameraBobbingManifest: RendererModuleManifest = {
55
+ id: 'cameraBobbing',
56
+ controller: CameraBobbingModule,
57
+ enabledDefault: true,
58
+ cannotBeDisabled: true,
59
+ }
@@ -1,4 +1,5 @@
1
1
  //@ts-nocheck
2
+ import { cameraBobbingManifest } from './cameraBobbing'
2
3
  import { rainManifest } from './rain'
3
4
  import { sciFiWorldRevealManifest } from './sciFiWorldReveal'
4
5
  import { starfieldManifest } from './starfield'
@@ -7,4 +8,5 @@ export const BUILTIN_MODULES = {
7
8
  starfield: starfieldManifest,
8
9
  futuristicReveal: sciFiWorldRevealManifest,
9
10
  rain: rainManifest,
11
+ cameraBobbing: cameraBobbingManifest,
10
12
  }
@@ -4,7 +4,6 @@ import { Vec3 } from 'vec3'
4
4
  import nbt from 'prismarine-nbt'
5
5
  import { MesherGeometryOutput, IS_FULL_WORLD_SECTION } from '../mesher/shared'
6
6
  import { getBannerTexture, createBannerMesh, releaseBannerTexture } from './bannerRenderer'
7
- import { disposeObject } from './threeJsUtils'
8
7
  import type { WorldRendererThree } from './worldRendererThree'
9
8
 
10
9
  export interface SectionObject extends THREE.Object3D {
@@ -17,6 +16,9 @@ export class WorldBlockGeometry {
17
16
  sectionObjects: Record<string, SectionObject> = {}
18
17
  waitingChunksToDisplay: { [chunkKey: string]: string[] } = {}
19
18
  estimatedMemoryUsage = 0
19
+ private pendingUpdates: Map<string, { geometry: MesherGeometryOutput; key: string; type: string }> = new Map()
20
+ private pendingBufferStartTime: number | null = null
21
+ private static readonly MAX_BUFFER_MS = 500
20
22
 
21
23
  constructor(
22
24
  private readonly worldRenderer: WorldRendererThree,
@@ -26,21 +28,84 @@ export class WorldBlockGeometry {
26
28
  ) { }
27
29
 
28
30
  handleWorkerGeometryMessage(data: { geometry: MesherGeometryOutput; key: string; type: string }): void {
29
- let object: THREE.Object3D = this.sectionObjects[data.key]
30
- if (object) {
31
- // Track memory usage removal for existing section
32
- this.removeSectionMemoryUsage(object)
33
- // Cleanup banner textures before disposing
34
- object.traverse((child) => {
35
- if ((child as any).bannerTexture) {
36
- releaseBannerTexture((child as any).bannerTexture)
31
+ const isUpdate = !!this.sectionObjects[data.key]
32
+
33
+ if (isUpdate) {
34
+ // Buffer updates for existing sections — keep old mesh visible
35
+ this.pendingUpdates.set(data.key, data)
36
+ if (this.pendingBufferStartTime === null) {
37
+ this.pendingBufferStartTime = performance.now()
38
+ }
39
+ return
40
+ }
41
+
42
+ // Initial load — apply immediately
43
+ this._applySectionGeometry(data)
44
+ }
45
+
46
+ applyPendingUpdates(): void {
47
+ if (this.pendingUpdates.size === 0) return
48
+
49
+ const now = performance.now()
50
+ const sinceFirst = now - (this.pendingBufferStartTime ?? now)
51
+
52
+ // Wait for neighboring sections still being meshed (unless max timeout reached)
53
+ if (sinceFirst < WorldBlockGeometry.MAX_BUFFER_MS) {
54
+ const sectionHeight = this.worldRenderer.getSectionHeight()
55
+ for (const key of this.pendingUpdates.keys()) {
56
+ const [sx, sy, sz] = key.split(',').map(Number)
57
+ const neighborKeys = [
58
+ `${sx - 16},${sy},${sz}`, `${sx + 16},${sy},${sz}`,
59
+ `${sx},${sy - sectionHeight},${sz}`, `${sx},${sy + sectionHeight},${sz}`,
60
+ `${sx},${sy},${sz - 16}`, `${sx},${sy},${sz + 16}`,
61
+ ]
62
+ for (const neighborKey of neighborKeys) {
63
+ // Wait if neighbor is being meshed, hasn't arrived yet, and has a loaded mesh
64
+ if (this.worldRenderer.sectionsWaiting.has(neighborKey) &&
65
+ !this.pendingUpdates.has(neighborKey) &&
66
+ this.sectionObjects[neighborKey]) {
67
+ return
68
+ }
37
69
  }
38
- })
39
- this.worldRenderer.sceneOrigin.removeAndUntrackAll(object)
40
- disposeObject(object)
41
- delete this.sectionObjects[data.key]
70
+ }
42
71
  }
43
72
 
73
+ // Flush all pending updates atomically
74
+ for (const [key, data] of this.pendingUpdates) {
75
+ this._removeSectionSafely(key)
76
+ this._applySectionGeometry(data)
77
+ }
78
+
79
+ this.pendingUpdates.clear()
80
+ this.pendingBufferStartTime = null
81
+ }
82
+
83
+ private disposeSectionObject(obj: THREE.Object3D): void {
84
+ if (obj instanceof THREE.Mesh) {
85
+ obj.geometry?.dispose?.()
86
+ // Don't dispose material - it's shared across all sections
87
+ }
88
+ if (obj.children) {
89
+ obj.children.forEach(child => this.disposeSectionObject(child))
90
+ }
91
+ }
92
+
93
+ private _removeSectionSafely(key: string): void {
94
+ const object = this.sectionObjects[key]
95
+ if (!object) return
96
+
97
+ this.removeSectionMemoryUsage(object)
98
+ object.traverse((child) => {
99
+ if ((child as any).bannerTexture) {
100
+ releaseBannerTexture((child as any).bannerTexture)
101
+ }
102
+ })
103
+ this.worldRenderer.sceneOrigin.removeAndUntrackAll(object)
104
+ this.disposeSectionObject(object)
105
+ delete this.sectionObjects[key]
106
+ }
107
+
108
+ private _applySectionGeometry(data: { geometry: MesherGeometryOutput; key: string; type: string }): void {
44
109
  const chunkCoords = data.key.split(',')
45
110
  if (
46
111
  !this.worldRenderer.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] ||
@@ -69,7 +134,7 @@ export class WorldBlockGeometry {
69
134
  this.worldRenderer.sceneOrigin.track(mesh, { updateMatrix: true })
70
135
  mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
71
136
  mesh.name = 'mesh'
72
- object = new THREE.Group()
137
+ const object: THREE.Object3D = new THREE.Group()
73
138
  object.add(mesh)
74
139
  // mesh with static dimensions: 16x16xsectionHeight
75
140
  const sectionHeight = data.geometry.sectionEndY - data.geometry.sectionStartY
@@ -238,6 +303,8 @@ export class WorldBlockGeometry {
238
303
 
239
304
  // Reset memory tracking since all sections are cleared
240
305
  this.estimatedMemoryUsage = 0
306
+ this.pendingUpdates.clear()
307
+ this.pendingBufferStartTime = null
241
308
  }
242
309
 
243
310
  removeColumn(x: number, z: number) {
@@ -259,9 +326,10 @@ export class WorldBlockGeometry {
259
326
  }
260
327
  })
261
328
  this.worldRenderer.sceneOrigin.removeAndUntrackAll(mesh)
262
- disposeObject(mesh)
329
+ this.disposeSectionObject(mesh)
263
330
  }
264
331
  delete this.sectionObjects[key]
332
+ this.pendingUpdates.delete(key)
265
333
  } else {
266
334
  for (let y = worldMinY; y < this.worldRenderer.worldSizeParams.worldHeight; y += sectionHeight) {
267
335
  this.worldRenderer.setSectionDirty(new Vec3(x, y, z), false)
@@ -277,9 +345,10 @@ export class WorldBlockGeometry {
277
345
  }
278
346
  })
279
347
  this.worldRenderer.sceneOrigin.removeAndUntrackAll(mesh)
280
- disposeObject(mesh)
348
+ this.disposeSectionObject(mesh)
281
349
  }
282
350
  delete this.sectionObjects[key]
351
+ this.pendingUpdates.delete(key)
283
352
  }
284
353
  }
285
354
  }
@@ -985,7 +985,8 @@ export class WorldRendererThree extends WorldRendererCommon {
985
985
  this.camera.rotation.set(0, 0, 0)
986
986
  }
987
987
  } else {
988
- this.camera.position.set(0, 0, 0)
988
+ // Only reset z (clears third-person offset); x/y are managed by CameraShake for bobbing
989
+ this.camera.position.z = 0
989
990
  this.camera.rotation.set(0, 0, 0)
990
991
 
991
992
  // remove any debug raycasting
@@ -1065,6 +1066,8 @@ export class WorldRendererThree extends WorldRendererCommon {
1065
1066
 
1066
1067
  // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
1067
1068
  const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
1069
+ // Flush buffered geometry updates atomically before rendering
1070
+ this.worldBlockGeometry.applyPendingUpdates()
1068
1071
  this.renderer.render(this.scene, cam)
1069
1072
 
1070
1073
  if (