minecraft-renderer 0.1.30 → 0.1.32

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.30",
3
+ "version": "0.1.32",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,6 +45,7 @@ export const defaultWorldRendererConfig = {
45
45
  // Camera visual related settings
46
46
  showHand: false,
47
47
  viewBobbing: false,
48
+ handRenderer: 'vanilla' as 'vanilla' | 'legacy',
48
49
  renderEars: true,
49
50
  highlightBlockColor: 'blue' as 'blue' | 'classic' | 'auto' | undefined,
50
51
 
@@ -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
  }
@@ -288,7 +288,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
288
288
  if (initial) {
289
289
  callback(this.playerStateReactive[key])
290
290
  }
291
- subscribeKey(this.playerStateReactive, key, callback)
291
+ return subscribeKey(this.playerStateReactive, key, callback)
292
292
  }
293
293
 
294
294
  onReactiveConfigUpdated<T extends keyof typeof this.worldRendererConfig>(key: T, callback: (value: typeof this.worldRendererConfig[T]) => void) {
@@ -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
  }
package/src/three/hand.ts CHANGED
@@ -21,70 +21,121 @@ export const getMyHand = async (image?: string, userName?: string) => {
21
21
 
22
22
  newMap.magFilter = THREE.NearestFilter
23
23
  newMap.minFilter = THREE.NearestFilter
24
- // right arm
25
- const box = new THREE.BoxGeometry()
26
- const material = new THREE.MeshStandardMaterial()
24
+
27
25
  const slim = false
28
- const mesh = new THREE.Mesh(box, material)
29
- mesh.scale.x = slim ? 3 : 4
30
- mesh.scale.y = 12
31
- mesh.scale.z = 4
32
- setSkinUVs(box, 40, 16, slim ? 3 : 4, 12, 4)
26
+ const pixelWidth = slim ? 3 : 4
27
+
28
+ // Exact replica of vanilla's Cube: addBox(-3, -2, -2, 4, 12, 4) at texOffs(40, 16)
29
+ const box = createVanillaCubeGeometry(
30
+ 40, 16,
31
+ slim ? -2 : -3, -2, -2,
32
+ pixelWidth, 12, 4,
33
+ 64, 64
34
+ )
35
+
36
+ const material = new THREE.MeshStandardMaterial()
33
37
  material.map = newMap
34
38
  material.needsUpdate = true
39
+
40
+ const mesh = new THREE.Mesh(box, material)
41
+
35
42
  const group = new THREE.Group()
36
43
  group.add(mesh)
37
- group.scale.set(0.1, 0.1, 0.1)
38
- mesh.rotation.z = Math.PI
39
44
  return group
40
45
  }
41
46
 
42
- function setUVs (
43
- box: THREE.BoxGeometry,
44
- u: number,
45
- v: number,
46
- width: number,
47
- height: number,
48
- depth: number,
49
- textureWidth: number,
50
- textureHeight: number
51
- ): void {
52
- const toFaceVertices = (x1: number, y1: number, x2: number, y2: number) => [
53
- new THREE.Vector2(x1 / textureWidth, 1 - y2 / textureHeight),
54
- new THREE.Vector2(x2 / textureWidth, 1 - y2 / textureHeight),
55
- new THREE.Vector2(x2 / textureWidth, 1 - y1 / textureHeight),
56
- new THREE.Vector2(x1 / textureWidth, 1 - y1 / textureHeight),
47
+ /**
48
+ * Creates a BufferGeometry replicating vanilla Minecraft's ModelPart.Cube exactly.
49
+ * Vertices, face winding, normals, and UV mapping match the decompiled Java source.
50
+ * Position coordinates are in pixels, divided by 16 for block units.
51
+ */
52
+ function createVanillaCubeGeometry (
53
+ texU: number, texV: number,
54
+ originX: number, originY: number, originZ: number,
55
+ sizeX: number, sizeY: number, sizeZ: number,
56
+ texWidth: number, texHeight: number,
57
+ mirror = false
58
+ ): THREE.BufferGeometry {
59
+ let minX = originX / 16
60
+ let minY = originY / 16
61
+ let minZ = originZ / 16
62
+ let maxX = (originX + sizeX) / 16
63
+ let maxY = (originY + sizeY) / 16
64
+ let maxZ = (originZ + sizeZ) / 16
65
+
66
+ if (mirror) {
67
+ [minX, maxX] = [maxX, minX]
68
+ }
69
+
70
+ // 8 corner vertices matching vanilla's Cube constructor
71
+ const V = [
72
+ [minX, minY, minZ], // 0
73
+ [maxX, minY, minZ], // 1
74
+ [maxX, maxY, minZ], // 2
75
+ [minX, maxY, minZ], // 3
76
+ [minX, minY, maxZ], // 4
77
+ [maxX, minY, maxZ], // 5
78
+ [maxX, maxY, maxZ], // 6
79
+ [minX, maxY, maxZ], // 7
57
80
  ]
58
81
 
59
- const top = toFaceVertices(u + depth, v, u + width + depth, v + depth)
60
- const bottom = toFaceVertices(u + width + depth, v, u + width * 2 + depth, v + depth)
61
- const left = toFaceVertices(u, v + depth, u + depth, v + depth + height)
62
- const front = toFaceVertices(u + depth, v + depth, u + width + depth, v + depth + height)
63
- const right = toFaceVertices(u + width + depth, v + depth, u + width + depth * 2, v + height + depth)
64
- const back = toFaceVertices(u + width + depth * 2, v + depth, u + width * 2 + depth * 2, v + height + depth)
65
-
66
- const uvAttr = box.attributes.uv as THREE.BufferAttribute
67
- const uvRight = [right[3], right[2], right[0], right[1]]
68
- const uvLeft = [left[3], left[2], left[0], left[1]]
69
- const uvTop = [top[3], top[2], top[0], top[1]]
70
- const uvBottom = [bottom[0], bottom[1], bottom[3], bottom[2]]
71
- const uvFront = [front[3], front[2], front[0], front[1]]
72
- const uvBack = [back[3], back[2], back[0], back[1]]
73
-
74
- // Create a new array to hold the modified UV data
75
- const newUVData = [] as number[]
76
-
77
- // Iterate over the arrays and copy the data to uvData
78
- for (const uvArray of [uvRight, uvLeft, uvTop, uvBottom, uvFront, uvBack]) {
79
- for (const uv of uvArray) {
80
- newUVData.push(uv.x, uv.y)
82
+ // UV grid (pixel coords)
83
+ const u0 = texU
84
+ const u1 = texU + sizeZ
85
+ const u2 = texU + sizeZ + sizeX
86
+ const u3 = texU + sizeZ + sizeX + sizeX
87
+ const u4 = texU + sizeZ + sizeX + sizeZ
88
+ const u5 = texU + sizeZ + sizeX + sizeZ + sizeX
89
+ const v0 = texV
90
+ const v1 = texV + sizeZ
91
+ const v2 = texV + sizeZ + sizeY
92
+
93
+ // 6 faces: vanilla vertex order + UV rect + normal
94
+ const faces: { vi: number[]; uv: number[]; n: number[] }[] = [
95
+ { vi: [5, 4, 0, 1], uv: [u1, v0, u2, v1], n: [0, -1, 0] }, // DOWN
96
+ { vi: [2, 3, 7, 6], uv: [u2, v1, u3, v0], n: [0, 1, 0] }, // UP
97
+ { vi: [0, 4, 7, 3], uv: [u0, v1, u1, v2], n: [-1, 0, 0] }, // WEST
98
+ { vi: [1, 0, 3, 2], uv: [u1, v1, u2, v2], n: [0, 0, -1] }, // NORTH
99
+ { vi: [5, 1, 2, 6], uv: [u2, v1, u4, v2], n: [1, 0, 0] }, // EAST
100
+ { vi: [4, 5, 6, 7], uv: [u4, v1, u5, v2], n: [0, 0, 1] }, // SOUTH
101
+ ]
102
+
103
+ const positions: number[] = []
104
+ const uvs: number[] = []
105
+ const normals: number[] = []
106
+ const indices: number[] = []
107
+
108
+ for (let fi = 0; fi < faces.length; fi++) {
109
+ const face = faces[fi]
110
+ const base = fi * 4
111
+
112
+ const [uL, vT, uR, vB] = face.uv
113
+ // Vanilla vertex UV order: top-right, top-left, bottom-left, bottom-right
114
+ const fUV = [
115
+ [uR / texWidth, 1 - vT / texHeight],
116
+ [uL / texWidth, 1 - vT / texHeight],
117
+ [uL / texWidth, 1 - vB / texHeight],
118
+ [uR / texWidth, 1 - vB / texHeight],
119
+ ]
120
+
121
+ const order = mirror ? [3, 2, 1, 0] : [0, 1, 2, 3]
122
+ const nx = mirror ? -face.n[0] : face.n[0]
123
+
124
+ for (let i = 0; i < 4; i++) {
125
+ const vert = V[face.vi[order[i]]]
126
+ positions.push(vert[0], vert[1], vert[2])
127
+ uvs.push(fUV[i][0], fUV[i][1])
128
+ normals.push(nx, face.n[1], face.n[2])
81
129
  }
130
+
131
+ indices.push(base, base + 1, base + 2, base, base + 2, base + 3)
82
132
  }
83
133
 
84
- uvAttr.set(new Float32Array(newUVData))
85
- uvAttr.needsUpdate = true
86
- }
134
+ const geometry = new THREE.BufferGeometry()
135
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
136
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
137
+ geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))
138
+ geometry.setIndex(indices)
87
139
 
88
- function setSkinUVs (box: THREE.BoxGeometry, u: number, v: number, width: number, height: number, depth: number): void {
89
- setUVs(box, u, v, width, height, depth, 64, 64)
140
+ return geometry
90
141
  }
@@ -0,0 +1,90 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+ import { loadSkinFromUsername, loadSkinImage } from '../lib/utils/skins'
4
+ import { steveTexture } from './entities'
5
+
6
+
7
+ export const getMyHand = async (image?: string, userName?: string) => {
8
+ let newMap: THREE.Texture
9
+ if (!image && !userName) {
10
+ newMap = await steveTexture
11
+ } else {
12
+ if (!image) {
13
+ image = await loadSkinFromUsername(userName!, 'skin')
14
+ }
15
+ if (!image) {
16
+ return
17
+ }
18
+ const { canvas } = await loadSkinImage(image)
19
+ newMap = new THREE.CanvasTexture(canvas)
20
+ }
21
+
22
+ newMap.magFilter = THREE.NearestFilter
23
+ newMap.minFilter = THREE.NearestFilter
24
+ // right arm
25
+ const box = new THREE.BoxGeometry()
26
+ const material = new THREE.MeshStandardMaterial()
27
+ const slim = false
28
+ const mesh = new THREE.Mesh(box, material)
29
+ mesh.scale.x = slim ? 3 : 4
30
+ mesh.scale.y = 12
31
+ mesh.scale.z = 4
32
+ setSkinUVs(box, 40, 16, slim ? 3 : 4, 12, 4)
33
+ material.map = newMap
34
+ material.needsUpdate = true
35
+ const group = new THREE.Group()
36
+ group.add(mesh)
37
+ group.scale.set(0.1, 0.1, 0.1)
38
+ mesh.rotation.z = Math.PI
39
+ return group
40
+ }
41
+
42
+ function setUVs (
43
+ box: THREE.BoxGeometry,
44
+ u: number,
45
+ v: number,
46
+ width: number,
47
+ height: number,
48
+ depth: number,
49
+ textureWidth: number,
50
+ textureHeight: number
51
+ ): void {
52
+ const toFaceVertices = (x1: number, y1: number, x2: number, y2: number) => [
53
+ new THREE.Vector2(x1 / textureWidth, 1 - y2 / textureHeight),
54
+ new THREE.Vector2(x2 / textureWidth, 1 - y2 / textureHeight),
55
+ new THREE.Vector2(x2 / textureWidth, 1 - y1 / textureHeight),
56
+ new THREE.Vector2(x1 / textureWidth, 1 - y1 / textureHeight),
57
+ ]
58
+
59
+ const top = toFaceVertices(u + depth, v, u + width + depth, v + depth)
60
+ const bottom = toFaceVertices(u + width + depth, v, u + width * 2 + depth, v + depth)
61
+ const left = toFaceVertices(u, v + depth, u + depth, v + depth + height)
62
+ const front = toFaceVertices(u + depth, v + depth, u + width + depth, v + depth + height)
63
+ const right = toFaceVertices(u + width + depth, v + depth, u + width + depth * 2, v + height + depth)
64
+ const back = toFaceVertices(u + width + depth * 2, v + depth, u + width * 2 + depth * 2, v + height + depth)
65
+
66
+ const uvAttr = box.attributes.uv as THREE.BufferAttribute
67
+ const uvRight = [right[3], right[2], right[0], right[1]]
68
+ const uvLeft = [left[3], left[2], left[0], left[1]]
69
+ const uvTop = [top[3], top[2], top[0], top[1]]
70
+ const uvBottom = [bottom[0], bottom[1], bottom[3], bottom[2]]
71
+ const uvFront = [front[3], front[2], front[0], front[1]]
72
+ const uvBack = [back[3], back[2], back[0], back[1]]
73
+
74
+ // Create a new array to hold the modified UV data
75
+ const newUVData = [] as number[]
76
+
77
+ // Iterate over the arrays and copy the data to uvData
78
+ for (const uvArray of [uvRight, uvLeft, uvTop, uvBottom, uvFront, uvBack]) {
79
+ for (const uv of uvArray) {
80
+ newUVData.push(uv.x, uv.y)
81
+ }
82
+ }
83
+
84
+ uvAttr.set(new Float32Array(newUVData))
85
+ uvAttr.needsUpdate = true
86
+ }
87
+
88
+ function setSkinUVs (box: THREE.BoxGeometry, u: number, v: number, width: number, height: number, depth: number): void {
89
+ setUVs(box, u, v, width, height, depth, 64, 64)
90
+ }