minecraft-renderer 0.1.32 → 0.1.34

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.32",
3
+ "version": "0.1.34",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -68,6 +68,14 @@ export const getBackendMethods = (worldRenderer: WorldRendererThree): any => {
68
68
  setSkyboxImage: worldRenderer.skyboxRenderer.setSkyboxImage.bind(worldRenderer.skyboxRenderer),
69
69
  // Rain methods
70
70
  setRain: (newState: boolean) => worldRenderer.toggleModule('rain', newState),
71
+ spawnBlockBreakParticles(x: number, y: number, z: number, blockName: string, floorMap: number[], biomeName?: string) {
72
+ const module = worldRenderer.getModule<import('./modules/blockBreakParticles').BlockBreakParticlesModule>('blockBreakParticles')
73
+ module?.spawnBlockBreakParticles(x, y, z, blockName, floorMap, biomeName)
74
+ },
75
+ spawnBlockCrackParticle(x: number, y: number, z: number, face: number, blockName: string, floorMap: number[], biomeName?: string) {
76
+ const module = worldRenderer.getModule<import('./modules/blockBreakParticles').BlockBreakParticlesModule>('blockBreakParticles')
77
+ module?.spawnCrackParticle(x, y, z, face, blockName, floorMap, biomeName)
78
+ },
71
79
  async loadGeometryExport(exportData: any) {
72
80
  // Import dynamically to avoid circular dependencies
73
81
  const { applyWorldGeometryExport } = await import('./worldGeometryExport')
@@ -386,9 +386,10 @@ export default class HoldingBlock implements IHoldingBlock {
386
386
  }
387
387
  const partialTick = Math.min((now - this.lastBobTickTime) / 50, 1)
388
388
 
389
+ const handBobSpeedMultiplier = 1.8
389
390
  const bob = computeCameraBob({
390
- walkDist: ps.walkDist,
391
- prevWalkDist: ps.prevWalkDist,
391
+ walkDist: ps.walkDist * handBobSpeedMultiplier,
392
+ prevWalkDist: ps.prevWalkDist * handBobSpeedMultiplier,
392
393
  bob: ps.bob,
393
394
  prevBob: ps.prevBob,
394
395
  partialTick
@@ -0,0 +1,438 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+ import type { WorldRendererThree } from '../worldRendererThree'
4
+ import type { RendererModuleController, RendererModuleManifest } from '../rendererModuleSystem'
5
+
6
+ // --- Tint data (lazy-loaded from globalThis.loadedData.tints at runtime) ---
7
+ const tints: Record<string, Record<string, [number, number, number]>> = {}
8
+ let tintsInitialized = false
9
+
10
+ function ensureTintsLoaded(): void {
11
+ if (tintsInitialized) return
12
+ const tintsData = (globalThis as any).loadedData?.tints
13
+ if (!tintsData) return
14
+ for (const key of Object.keys(tintsData)) {
15
+ tints[key] = prepareTints(tintsData[key])
16
+ }
17
+ tintsInitialized = true
18
+ }
19
+
20
+ function prepareTints(data: any): Record<string, [number, number, number]> {
21
+ const result: Record<string, [number, number, number]> = {}
22
+ const defaultColor = tintToGl(data.default ?? 0xFFFFFF)
23
+ if (data.data) {
24
+ for (const entry of data.data) {
25
+ const color = tintToGl(entry.color)
26
+ for (const key of entry.keys) {
27
+ result[key] = color
28
+ }
29
+ }
30
+ }
31
+ return new Proxy(result, {
32
+ get(target, prop: string) {
33
+ return target[prop] ?? defaultColor
34
+ }
35
+ })
36
+ }
37
+
38
+ function tintToGl(tint: number): [number, number, number] {
39
+ return [
40
+ ((tint >> 16) & 0xFF) / 255,
41
+ ((tint >> 8) & 0xFF) / 255,
42
+ (tint & 0xFF) / 255,
43
+ ]
44
+ }
45
+
46
+ function resolveTintColor(blockName: string, biomeName: string): [number, number, number] {
47
+ ensureTintsLoaded()
48
+ if (blockName === 'grass_block') return [1, 1, 1]
49
+ if (blockName === 'redstone_wire') return [1, 1, 1]
50
+ if (blockName === 'birch_leaves' || blockName === 'spruce_leaves' || blockName === 'lily_pad') {
51
+ return tints.constant?.[blockName] ?? [1, 1, 1]
52
+ }
53
+ if (blockName.includes('leaves') || blockName === 'vine') {
54
+ return tints.foliage?.[biomeName] ?? [1, 1, 1]
55
+ }
56
+ const grassTintedBlocks = ['short_grass', 'tall_grass', 'fern', 'large_fern', 'sugar_cane', 'grass']
57
+ if (grassTintedBlocks.includes(blockName)) {
58
+ return tints.grass?.[biomeName] ?? [1, 1, 1]
59
+ }
60
+ return [1, 1, 1]
61
+ }
62
+
63
+ interface BreakParticle {
64
+ mesh: THREE.Mesh
65
+ active: boolean
66
+ x: number; y: number; z: number
67
+ prevX: number; prevY: number; prevZ: number
68
+ xd: number; yd: number; zd: number
69
+ age: number
70
+ maxAge: number
71
+ onGround: boolean
72
+ floorMap: number[]
73
+ blockX: number
74
+ blockZ: number
75
+ }
76
+
77
+ const MAX_PARTICLES = 512
78
+ const TICK_RATE = 1 / 20
79
+
80
+ export class BlockBreakParticlesModule implements RendererModuleController {
81
+ private particles: BreakParticle[] = []
82
+ private sharedMaterial?: THREE.MeshBasicMaterial
83
+ private enabled = false
84
+ private tickAccumulator = 0
85
+ private nextParticleIndex = 0
86
+
87
+ constructor(private readonly worldRenderer: WorldRendererThree) {}
88
+
89
+ enable(): void {
90
+ if (this.enabled) return
91
+ this.enabled = true
92
+ this.ensureMaterial()
93
+ }
94
+
95
+ disable(): void {
96
+ if (!this.enabled) return
97
+ this.enabled = false
98
+ }
99
+
100
+ dispose(): void {
101
+ for (const p of this.particles) {
102
+ if (p.active) {
103
+ this.worldRenderer.sceneOrigin.removeAndUntrack(p.mesh)
104
+ }
105
+ p.mesh.geometry.dispose()
106
+ }
107
+ this.particles = []
108
+ this.sharedMaterial?.dispose()
109
+ this.sharedMaterial = undefined
110
+ this.nextParticleIndex = 0
111
+ }
112
+
113
+ render = (deltaTime: number): void => {
114
+ if (!this.enabled) return
115
+
116
+ this.tickAccumulator += deltaTime
117
+ while (this.tickAccumulator >= TICK_RATE) {
118
+ this.tickAccumulator -= TICK_RATE
119
+ this.tickPhysics()
120
+ }
121
+
122
+ const alpha = this.tickAccumulator / TICK_RATE
123
+ this.updateVisuals(alpha)
124
+ }
125
+
126
+ spawnBlockBreakParticles(worldX: number, worldY: number, worldZ: number, blockName: string, floorMap: number[], biomeName = 'plains'): void {
127
+ if (!this.enabled) return
128
+
129
+ const texInfo = this.resolveBlockTexture(blockName)
130
+ if (!texInfo) return
131
+
132
+ const tintColor = resolveTintColor(blockName, biomeName)
133
+
134
+ for (let ox = 0; ox < 4; ox++) {
135
+ for (let oy = 0; oy < 4; oy++) {
136
+ for (let oz = 0; oz < 4; oz++) {
137
+ const px = worldX + (ox + 0.5) / 4
138
+ const py = worldY + (oy + 0.5) / 4
139
+ const pz = worldZ + (oz + 0.5) / 4
140
+
141
+ let motionX = px - worldX - 0.5
142
+ let motionY = py - worldY - 0.5
143
+ let motionZ = pz - worldZ - 0.5
144
+
145
+ motionX += (Math.random() * 2 - 1) * 0.4
146
+ motionY += (Math.random() * 2 - 1) * 0.4
147
+ motionZ += (Math.random() * 2 - 1) * 0.4
148
+
149
+ const strength = (Math.random() + Math.random() + 1) * 0.15
150
+ const len = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ)
151
+ const xd = (motionX / len) * strength * 0.4
152
+ const yd = (motionY / len) * strength * 0.4 + 0.1
153
+ const zd = (motionZ / len) * strength * 0.4
154
+
155
+ const maxAge = Math.floor(4 / (Math.random() * 0.9 + 0.1))
156
+
157
+ this.createParticle(px, py, pz, xd, yd, zd, maxAge, texInfo, floorMap, worldX, worldZ, 1.0, tintColor)
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ private tickPhysics(): void {
164
+ for (const p of this.particles) {
165
+ if (!p.active) continue
166
+
167
+ p.prevX = p.x
168
+ p.prevY = p.y
169
+ p.prevZ = p.z
170
+
171
+ p.age++
172
+ if (p.age >= p.maxAge) {
173
+ this.deactivateParticle(p)
174
+ continue
175
+ }
176
+
177
+ p.yd -= 0.04
178
+ p.x += p.xd
179
+ p.y += p.yd
180
+ p.z += p.zd
181
+
182
+ // Recalculate onGround each tick (particle may move to a different column)
183
+ const floorY = this.getFloorY(p)
184
+ if (p.y <= floorY) {
185
+ p.y = floorY
186
+ p.yd = 0
187
+ p.onGround = true
188
+ } else {
189
+ p.onGround = false
190
+ }
191
+
192
+ p.xd *= 0.98
193
+ p.yd *= 0.98
194
+ p.zd *= 0.98
195
+
196
+ if (p.onGround) {
197
+ p.xd *= 0.7
198
+ p.zd *= 0.7
199
+ }
200
+ }
201
+ }
202
+
203
+ private updateVisuals(alpha: number): void {
204
+ // Camera is at ~(0,0,0) in scene space (sceneOrigin tracks camera)
205
+ const cameraPosScene = this.worldRenderer.camera.position
206
+
207
+ for (const p of this.particles) {
208
+ if (!p.active) continue
209
+
210
+ const displayX = p.prevX + (p.x - p.prevX) * alpha
211
+ const displayY = p.prevY + (p.y - p.prevY) * alpha
212
+ const displayZ = p.prevZ + (p.z - p.prevZ) * alpha
213
+
214
+ p.mesh.position.set(displayX, displayY, displayZ)
215
+ // lookAt operates in parent (scene) coords — use scene-local camera pos
216
+ p.mesh.lookAt(cameraPosScene.x, cameraPosScene.y, cameraPosScene.z)
217
+ }
218
+ }
219
+
220
+ spawnCrackParticle(worldX: number, worldY: number, worldZ: number, face: number, blockName: string, floorMap: number[], biomeName = 'plains'): void {
221
+ if (!this.enabled) return
222
+
223
+ const texInfo = this.resolveBlockTexture(blockName)
224
+ if (!texInfo) return
225
+
226
+ const tintColor = resolveTintColor(blockName, biomeName)
227
+
228
+ // Random position within block, inset 0.1 on each axis
229
+ let px = worldX + Math.random() * 0.8 + 0.1
230
+ let py = worldY + Math.random() * 0.8 + 0.1
231
+ let pz = worldZ + Math.random() * 0.8 + 0.1
232
+
233
+ // Override position on the hit face axis to be at face + 0.1 offset outward
234
+ switch (face) {
235
+ case 0: py = worldY - 0.1; break
236
+ case 1: py = worldY + 1.0 + 0.1; break
237
+ case 2: pz = worldZ - 0.1; break
238
+ case 3: pz = worldZ + 1.0 + 0.1; break
239
+ case 4: px = worldX - 0.1; break
240
+ case 5: px = worldX + 1.0 + 0.1; break
241
+ }
242
+
243
+ // Small random velocity, heavily damped
244
+ const xd = (Math.random() * 2 - 1) * 0.4 * 0.2
245
+ const yd = (Math.random() * 2 - 1) * 0.4 * 0.2 + 0.1 * 0.2
246
+ const zd = (Math.random() * 2 - 1) * 0.4 * 0.2
247
+
248
+ const maxAge = Math.floor(4 / (Math.random() * 0.9 + 0.1))
249
+
250
+ this.createParticle(px, py, pz, xd, yd, zd, maxAge, texInfo, floorMap, worldX, worldZ, 0.6, tintColor)
251
+ }
252
+
253
+ private createParticle(
254
+ px: number, py: number, pz: number,
255
+ xd: number, yd: number, zd: number,
256
+ maxAge: number,
257
+ texInfo: { u: number; v: number; su: number; sv: number },
258
+ floorMap: number[],
259
+ blockX: number, blockZ: number,
260
+ scaleFactor = 1.0,
261
+ tintColor: [number, number, number] = [1, 1, 1]
262
+ ): void {
263
+ this.ensureMaterial()
264
+
265
+ let particle = this.findInactiveParticle()
266
+
267
+ if (!particle) {
268
+ if (this.particles.length < MAX_PARTICLES) {
269
+ particle = this.allocateParticle()
270
+ } else {
271
+ particle = this.recycleOldest()
272
+ }
273
+ }
274
+
275
+ const randomU = Math.floor(Math.random() * 4)
276
+ const randomV = Math.floor(Math.random() * 4)
277
+ const particleU = texInfo.u + (randomU / 4) * texInfo.su
278
+ const particleV = texInfo.v + (randomV / 4) * texInfo.sv
279
+ const particleSU = texInfo.su / 4
280
+ const particleSV = texInfo.sv / 4
281
+
282
+ this.setGeometryUVs(particle.mesh.geometry as THREE.PlaneGeometry, particleU, particleV, particleSU, particleSV)
283
+
284
+ particle.active = true
285
+ particle.x = px; particle.y = py; particle.z = pz
286
+ particle.prevX = px; particle.prevY = py; particle.prevZ = pz
287
+ particle.xd = xd; particle.yd = yd; particle.zd = zd
288
+ particle.age = 0
289
+ particle.maxAge = maxAge
290
+ particle.onGround = false
291
+ particle.floorMap = floorMap
292
+ particle.blockX = Math.floor(blockX)
293
+ particle.blockZ = Math.floor(blockZ)
294
+
295
+ const scale = 0.1 * (0.5 + Math.random() * 0.5) * 2 * scaleFactor
296
+ particle.mesh.scale.set(scale, scale, scale)
297
+ particle.mesh.position.set(px, py, pz)
298
+ particle.mesh.visible = true
299
+
300
+ // Apply tint: base darkening 0.6 × tint color
301
+ const r = 0.6 * tintColor[0]
302
+ const g = 0.6 * tintColor[1]
303
+ const b = 0.6 * tintColor[2]
304
+ const colorArray = new Float32Array([r, g, b, r, g, b, r, g, b, r, g, b])
305
+ const colorAttr = particle.mesh.geometry.getAttribute('color') as THREE.BufferAttribute
306
+ if (colorAttr) {
307
+ colorAttr.set(colorArray)
308
+ colorAttr.needsUpdate = true
309
+ } else {
310
+ particle.mesh.geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3))
311
+ }
312
+
313
+ this.worldRenderer.sceneOrigin.addAndTrack(particle.mesh)
314
+ }
315
+
316
+ private allocateParticle(): BreakParticle {
317
+ const geometry = new THREE.PlaneGeometry(1, 1)
318
+ const mesh = new THREE.Mesh(geometry, this.sharedMaterial!)
319
+ mesh.visible = false
320
+
321
+ const particle: BreakParticle = {
322
+ mesh,
323
+ active: false,
324
+ x: 0, y: 0, z: 0,
325
+ prevX: 0, prevY: 0, prevZ: 0,
326
+ xd: 0, yd: 0, zd: 0,
327
+ age: 0,
328
+ maxAge: 0,
329
+ onGround: false,
330
+ floorMap: [],
331
+ blockX: 0,
332
+ blockZ: 0,
333
+ }
334
+
335
+ this.particles.push(particle)
336
+ return particle
337
+ }
338
+
339
+ private findInactiveParticle(): BreakParticle | undefined {
340
+ for (let i = 0; i < this.particles.length; i++) {
341
+ const idx = (this.nextParticleIndex + i) % this.particles.length
342
+ if (!this.particles[idx].active) {
343
+ this.nextParticleIndex = (idx + 1) % this.particles.length
344
+ return this.particles[idx]
345
+ }
346
+ }
347
+ return undefined
348
+ }
349
+
350
+ private recycleOldest(): BreakParticle {
351
+ let oldest: BreakParticle = this.particles[0]
352
+ for (const p of this.particles) {
353
+ if (p.age > oldest.age) {
354
+ oldest = p
355
+ }
356
+ }
357
+ this.deactivateParticle(oldest)
358
+ return oldest
359
+ }
360
+
361
+ private deactivateParticle(p: BreakParticle): void {
362
+ if (!p.active) return
363
+ p.active = false
364
+ p.mesh.visible = false
365
+ this.worldRenderer.sceneOrigin.removeAndUntrack(p.mesh)
366
+ }
367
+
368
+ private getFloorY(particle: BreakParticle): number {
369
+ let dx = Math.floor(particle.x) - particle.blockX
370
+ let dz = Math.floor(particle.z) - particle.blockZ
371
+ dx = Math.max(-2, Math.min(2, dx))
372
+ dz = Math.max(-2, Math.min(2, dz))
373
+ return particle.floorMap[(dz + 2) * 5 + (dx + 2)]
374
+ }
375
+
376
+ private resolveBlockTexture(blockName: string): { u: number; v: number; su: number; sv: number } | null {
377
+ const resources = this.worldRenderer.resourcesManager.currentResources
378
+ if (!resources) return null
379
+
380
+ const atlasJson = resources.blocksAtlasJson
381
+ const textures = atlasJson.textures
382
+
383
+ if (textures[blockName]) return this.extractUV(textures[blockName], atlasJson)
384
+
385
+ for (const suffix of ['_side', '_top', '_front', '_0', '']) {
386
+ const key = blockName + suffix
387
+ if (textures[key]) return this.extractUV(textures[key], atlasJson)
388
+ }
389
+
390
+ for (const key of Object.keys(textures)) {
391
+ if (key.startsWith(blockName)) return this.extractUV(textures[key], atlasJson)
392
+ }
393
+
394
+ return null
395
+ }
396
+
397
+ private extractUV(
398
+ texInfo: { u: number; v: number; su?: number; sv?: number },
399
+ atlasJson: { suSv: number }
400
+ ): { u: number; v: number; su: number; sv: number } {
401
+ return {
402
+ u: texInfo.u,
403
+ v: texInfo.v,
404
+ su: texInfo.su ?? atlasJson.suSv,
405
+ sv: texInfo.sv ?? atlasJson.suSv,
406
+ }
407
+ }
408
+
409
+ private setGeometryUVs(geometry: THREE.PlaneGeometry, u: number, v: number, su: number, sv: number): void {
410
+ const uvAttr = geometry.getAttribute('uv') as THREE.BufferAttribute
411
+ // PlaneGeometry UV layout: (0,1) (1,1) (0,0) (1,0)
412
+ uvAttr.setXY(0, u, v)
413
+ uvAttr.setXY(1, u + su, v)
414
+ uvAttr.setXY(2, u, v + sv)
415
+ uvAttr.setXY(3, u + su, v + sv)
416
+ uvAttr.needsUpdate = true
417
+ }
418
+
419
+ private ensureMaterial(): void {
420
+ if (this.sharedMaterial) return
421
+ const atlasTexture = this.worldRenderer.material.map
422
+ if (!atlasTexture) return
423
+ this.sharedMaterial = new THREE.MeshBasicMaterial({
424
+ map: atlasTexture,
425
+ vertexColors: true,
426
+ transparent: true,
427
+ alphaTest: 0.1,
428
+ })
429
+ }
430
+ }
431
+
432
+ export const blockBreakParticlesManifest: RendererModuleManifest = {
433
+ id: 'blockBreakParticles',
434
+ controller: BlockBreakParticlesModule,
435
+ enabledDefault: true,
436
+ cannotBeDisabled: true,
437
+ requiresHeightmap: false,
438
+ }
@@ -1,4 +1,5 @@
1
1
  //@ts-nocheck
2
+ import { blockBreakParticlesManifest } from './blockBreakParticles'
2
3
  import { cameraBobbingManifest } from './cameraBobbing'
3
4
  import { rainManifest } from './rain'
4
5
  import { sciFiWorldRevealManifest } from './sciFiWorldReveal'
@@ -9,4 +10,5 @@ export const BUILTIN_MODULES = {
9
10
  futuristicReveal: sciFiWorldRevealManifest,
10
11
  rain: rainManifest,
11
12
  cameraBobbing: cameraBobbingManifest,
13
+ blockBreakParticles: blockBreakParticlesManifest,
12
14
  }