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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -268,7 +268,6 @@ const generateAtlas = async (appViewer: AppViewer, images: Record<string, HTMLIm
268
268
  }
269
269
 
270
270
  export const generateGuiAtlas = async (appViewer: AppViewer) => {
271
- console.trace('generateGuiAtlas')
272
271
  const { blockModelsResolved, itemsModelsResolved } = getNonFullBlocksModels(appViewer)
273
272
 
274
273
  // Generate blocks atlas
@@ -718,5 +718,13 @@
718
718
  "barrier": true,
719
719
  "light": true,
720
720
  "moving_piston": true
721
+ },
722
+ "hasSemiTransparentTextuersRegex": {
723
+ "_stained_glass$": true,
724
+ "_stained_glass_pane$": true,
725
+ "^ice$": true,
726
+ "^tinted_glass$": true,
727
+ "^slime_block$": true,
728
+ "^honey_block$": true
721
729
  }
722
730
  }
@@ -0,0 +1,125 @@
1
+ //@ts-nocheck
2
+ /**
3
+ * Calculates sky light level based on Minecraft time of day.
4
+ *
5
+ * Minecraft time reference:
6
+ * - 0 ticks = 6:00 AM (sunrise complete)
7
+ * - 6000 ticks = 12:00 PM (noon) - brightest
8
+ * - 12000 ticks = 6:00 PM (sunset begins)
9
+ * - 13000 ticks = 7:00 PM (dusk/night begins)
10
+ * - 18000 ticks = 12:00 AM (midnight) - darkest
11
+ * - 23000 ticks = 5:00 AM (dawn begins)
12
+ * - 24000 ticks = 6:00 AM (same as 0)
13
+ *
14
+ * Sky light ranges from 4 (night) to 15 (day).
15
+ */
16
+
17
+ /**
18
+ * Calculate celestial angle from time of day (0-1 range representing sun position)
19
+ */
20
+ export const getCelestialAngle = (timeOfDay: number): number => {
21
+ // Normalize time to 0-1 range
22
+ let angle = ((timeOfDay % 24_000) / 24_000) - 0.25
23
+
24
+ if (angle < 0) angle += 1
25
+ if (angle > 1) angle -= 1
26
+
27
+ // Vanilla Minecraft applies a smoothing curve
28
+ const smoothedAngle = angle + (1 - Math.cos(angle * Math.PI)) / 2
29
+ return smoothedAngle
30
+ }
31
+
32
+ /**
33
+ * Calculate sky light level (0-15) based on time of day in ticks.
34
+ * Matches Minecraft vanilla behavior.
35
+ *
36
+ * @param timeOfDay - Time in ticks (0-24000)
37
+ * @returns Sky light level (4-15, where 15 is brightest day, 4 is darkest night)
38
+ */
39
+ export const calculateSkyLight = (timeOfDay: number): number => {
40
+ // Normalize time to 0-24000 range
41
+ const normalizedTime = ((timeOfDay % 24_000) + 24_000) % 24_000
42
+
43
+ // Calculate celestial angle (0-1, where 0.25 is noon, 0.75 is midnight)
44
+ const celestialAngle = getCelestialAngle(normalizedTime)
45
+
46
+ // Calculate brightness factor based on celestial angle
47
+ // cos gives us smooth day/night transition
48
+ const cos = Math.cos(celestialAngle * Math.PI * 2)
49
+
50
+ // Map cos (-1 to 1) to brightness (0 to 1)
51
+ // At noon (celestialAngle ~0.25): cos(0.5π) = 0, but we want max brightness
52
+ // At midnight (celestialAngle ~0.75): cos(1.5π) = 0, but we want min brightness
53
+
54
+ // Vanilla-like calculation:
55
+ // brightness goes from 0 (dark) to 1 (bright)
56
+ const brightness = cos * 0.5 + 0.5
57
+
58
+ // Apply threshold - night should be darker
59
+ // Vanilla has minimum sky light of 4 during night
60
+ const skyLight = Math.round(4 + brightness * 11)
61
+
62
+ return Math.max(4, Math.min(15, skyLight))
63
+ }
64
+
65
+ /**
66
+ * Simplified sky light calculation that more closely matches vanilla behavior.
67
+ * Uses piecewise linear interpolation based on known Minecraft light levels.
68
+ *
69
+ * @param timeOfDay - Time in ticks (0-24000)
70
+ * @returns Sky light level (4-15)
71
+ */
72
+ export const calculateSkyLightSimple = (timeOfDay: number): number => {
73
+ // Normalize to 0-24000
74
+ const time = ((timeOfDay % 24_000) + 24_000) % 24_000
75
+
76
+ // Vanilla Minecraft approximate sky light levels:
77
+ // 0-12000 (6AM-6PM): Day, sky light = 15
78
+ // 12000-13000 (6PM-7PM): Sunset transition, 15 -> 4
79
+ // 13000-23000 (7PM-5AM): Night, sky light = 4
80
+ // 23000-24000 (5AM-6AM): Sunrise transition, 4 -> 15
81
+
82
+ if (time >= 0 && time < 12_000) {
83
+ // Day time - full brightness
84
+ return 15
85
+ } else if (time >= 12_000 && time < 13_000) {
86
+ // Sunset transition (6PM to 7PM)
87
+ const progress = (time - 12_000) / 1000
88
+ return Math.round(15 - progress * 11)
89
+ } else if (time >= 13_000 && time < 23_000) {
90
+ // Night time - minimum brightness
91
+ return 4
92
+ } else {
93
+ // Sunrise transition (5AM to 6AM)
94
+ const progress = (time - 23_000) / 1000
95
+ return Math.round(4 + progress * 11)
96
+ }
97
+ }
98
+
99
+ // Test/debug helper - run this to see values at different times
100
+ export const debugSkyLight = () => {
101
+ const testTimes = [
102
+ { ticks: 0, label: '6:00 AM (sunrise)' },
103
+ { ticks: 6000, label: '12:00 PM (noon)' },
104
+ { ticks: 12_000, label: '6:00 PM (sunset starts)' },
105
+ { ticks: 12_500, label: '6:30 PM (sunset mid)' },
106
+ { ticks: 13_000, label: '7:00 PM (night begins)' },
107
+ { ticks: 18_000, label: '12:00 AM (midnight)' },
108
+ { ticks: 19_000, label: '1:00 AM' },
109
+ { ticks: 23_000, label: '5:00 AM (dawn begins)' },
110
+ { ticks: 23_500, label: '5:30 AM (dawn mid)' },
111
+ ]
112
+
113
+ console.log('Sky Light Debug:')
114
+ console.log('================')
115
+ for (const { ticks, label } of testTimes) {
116
+ const smooth = calculateSkyLight(ticks)
117
+ const simple = calculateSkyLightSimple(ticks)
118
+ console.log(`${ticks.toString().padStart(5)} ticks (${label}): smooth=${smooth}, simple=${simple}`)
119
+ }
120
+ }
121
+
122
+ // Export for global access in console
123
+ if (typeof window !== 'undefined') {
124
+ (window as any).debugSkyLight = debugSkyLight
125
+ }
@@ -17,6 +17,7 @@ import { getPlayerStateUtils } from '../graphicsBackend/playerState'
17
17
  type PlayerStateUtils = ReturnType<typeof getPlayerStateUtils>
18
18
  import { MesherLogReader } from './mesherlogReader'
19
19
  import { setSkinsConfig } from './utils/skins'
20
+ import { calculateSkyLightSimple } from './skyLight'
20
21
  import { WorldViewWorker } from '../worldView'
21
22
  import { generateSpiralMatrix } from './spiral'
22
23
  import { PlayerStateReactive } from '../playerState/playerState'
@@ -540,19 +541,8 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
540
541
  }
541
542
 
542
543
  getMesherConfig(): MesherConfig {
543
- let skyLight = 15
544
544
  const timeOfDay = this.timeOfTheDay
545
- if (timeOfDay < 0 || timeOfDay > 24_000) {
546
- //
547
- } else if (timeOfDay <= 6000 || timeOfDay >= 18_000) {
548
- skyLight = 15
549
- } else if (timeOfDay > 6000 && timeOfDay < 12_000) {
550
- skyLight = 15 - ((timeOfDay - 6000) / 6000) * 15
551
- } else if (timeOfDay >= 12_000 && timeOfDay < 18_000) {
552
- skyLight = ((timeOfDay - 12_000) / 6000) * 15
553
- }
554
-
555
- skyLight = Math.floor(skyLight)
545
+ const skyLight = (timeOfDay < 0 || timeOfDay > 24_000) ? 15 : calculateSkyLightSimple(timeOfDay)
556
546
  return {
557
547
  version: this.version,
558
548
  enableLighting: this.worldRendererConfig.enableLighting,
@@ -1,6 +1,7 @@
1
1
  //@ts-nocheck
2
2
  import { Vec3 } from 'vec3'
3
3
  import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
4
+ import moreBlockDataGeneratedJson from '../lib/moreBlockDataGenerated.json'
4
5
  import legacyJson from '../lib/preflatMap.json'
5
6
  import { BlockType } from '../playground/shared'
6
7
  import { World, BlockModelPartsResolved, WorldBlock as Block, WorldBlock, worldColumnKey } from './world'
@@ -16,6 +17,7 @@ let blockProvider: WorldBlockProvider
16
17
 
17
18
  const tints: any = {}
18
19
  let needTiles = false
20
+ let semiTransparentBlocks: string[] = []
19
21
 
20
22
  let tintsData
21
23
  try {
@@ -700,7 +702,7 @@ export function getSectionGeometry(sx: number, sy: number, sz: number, world: Wo
700
702
 
701
703
  for (const element of model.elements ?? []) {
702
704
  const ao = model.ao ?? block.boundingBox !== 'empty'
703
- if (block.transparent) {
705
+ if (block.transparent && semiTransparentBlocks.includes(block.name)) {
704
706
  const pos = cursor.clone()
705
707
  delayedRender.push(() => {
706
708
  renderElement(world, pos, element, ao, attr, globalMatrix, globalShift, block, biome)
@@ -794,7 +796,7 @@ function arrayNeedsUint32(array) {
794
796
 
795
797
  }
796
798
 
797
- export const setBlockStatesData = (blockstatesModels, blocksAtlas: any, _needTiles = false, useUnknownBlockModel = true, version = 'latest') => {
799
+ export const setBlockStatesData = (blockstatesModels, blocksAtlas: any, _needTiles = false, useUnknownBlockModel = true, version = 'latest', mcData = (globalThis as any).mcData) => {
798
800
  blockProvider = worldBlockProvider(blockstatesModels, blocksAtlas, version)
799
801
  globalThis.blockProvider = blockProvider
800
802
  if (useUnknownBlockModel) {
@@ -802,4 +804,28 @@ export const setBlockStatesData = (blockstatesModels, blocksAtlas: any, _needTil
802
804
  }
803
805
 
804
806
  needTiles = _needTiles
807
+
808
+ // Cache semi-transparent blocks based on regex patterns from moreBlockDataGenerated.json
809
+ const regexPatterns = Object.keys(moreBlockDataGeneratedJson.hasSemiTransparentTextuersRegex || {})
810
+ semiTransparentBlocks = []
811
+
812
+ // Get all block names from blockstatesModels
813
+ if (!Array.isArray(mcData.blocks)) throw new Error('mcData.blocks is not an array')
814
+ const allBlockNames = mcData.blocks.map(block => block.name)
815
+
816
+ // Filter blocks that match any of the regex patterns
817
+ for (const blockName of allBlockNames) {
818
+ for (const pattern of regexPatterns) {
819
+ try {
820
+ const regex = new RegExp(pattern)
821
+ if (regex.test(blockName)) {
822
+ semiTransparentBlocks.push(blockName)
823
+ break // Only add once per block
824
+ }
825
+ } catch (err) {
826
+ // Invalid regex pattern, skip
827
+ console.warn('Invalid regex pattern in hasSemiTransparentTextuersRegex:', pattern)
828
+ }
829
+ }
830
+ }
805
831
  }
@@ -39,7 +39,7 @@ export const setup = (version, initialBlocks: Array<[number[], string]>, options
39
39
  }
40
40
  }
41
41
 
42
- setBlockStatesData(blockStatesModels, blocksAtlasesJson, !options?.noDebugTiles, false, version)
42
+ setBlockStatesData(blockStatesModels, blocksAtlasesJson, !options?.noDebugTiles, false, version, { blocks: mcData.blocksArray })
43
43
  const reload = () => {
44
44
  mesherWorld.removeColumn(0, 0)
45
45
  mesherWorld.addColumn(0, 0, chunk1.toJson())
@@ -137,7 +137,10 @@ export const renderComponent = (
137
137
  const textWidths: number[] = []
138
138
 
139
139
  const renderText = (component: Message, parentFormatting?: Formatting | undefined) => {
140
- const { text } = component
140
+ if (component.text !== null && component.text !== undefined && typeof component.text !== 'string') {
141
+ console.warn('renderText received non-string text value:', typeof component.text, component.text)
142
+ }
143
+ const text = component.text === null || component.text === undefined ? undefined : String(component.text)
141
144
  const formatting = {
142
145
  color: component.color ?? parentFormatting?.color,
143
146
  underlined: component.underlined ?? parentFormatting?.underlined,
@@ -144,10 +144,11 @@ export const renderBanner = (
144
144
 
145
145
  ctx.imageSmoothingEnabled = false
146
146
 
147
- // Always render base color first (even if no patterns)
148
- const baseColorHex = BANNER_COLORS[baseColor] || BANNER_COLORS[15]
149
- ctx.fillStyle = baseColorHex
150
- ctx.fillRect(0, 0, BANNER_WIDTH * scale, BANNER_HEIGHT * scale)
147
+ // Base color rendering disabled
148
+ // // Always render base color first (even if no patterns)
149
+ // const baseColorHex = BANNER_COLORS[baseColor] || BANNER_COLORS[15]
150
+ // ctx.fillStyle = baseColorHex
151
+ // ctx.fillRect(0, 0, BANNER_WIDTH * scale, BANNER_HEIGHT * scale)
151
152
 
152
153
  // Render patterns on top of base color (if any)
153
154
  if (blockEntity?.Patterns && blockEntity.Patterns.length > 0) {
@@ -97,8 +97,8 @@ export class CinimaticScriptRunner {
97
97
  }
98
98
 
99
99
  runExampleScripts(index: number) {
100
- const { cameraObject } = this.worldRenderer
101
- const playerPos = new Vec3(cameraObject.position.x, cameraObject.position.y, cameraObject.position.z)
100
+ const cameraWorldPos = this.worldRenderer.getCameraPosition()
101
+ const playerPos = new Vec3(cameraWorldPos.x, cameraWorldPos.y, cameraWorldPos.z)
102
102
 
103
103
  // Circular flyby around current position
104
104
  const circular = CinimaticScriptRunner.createCircularFlyby(playerPos, 30, 20, 15_000)
@@ -423,6 +423,9 @@ export class Entities {
423
423
 
424
424
  // Update position and rotation
425
425
  if (playerData.position) {
426
+ if (!this.worldRenderer.sceneOrigin.getWorldPosition(this.playerEntity)) {
427
+ this.worldRenderer.sceneOrigin.track(this.playerEntity)
428
+ }
426
429
  this.playerEntity.position.set(playerData.position.x, playerData.position.y, playerData.position.z)
427
430
  }
428
431
  if (playerData.yaw !== undefined) {
@@ -434,7 +437,7 @@ export class Entities {
434
437
 
435
438
  clear() {
436
439
  for (const mesh of Object.values(this.entities)) {
437
- this.worldRenderer.scene.remove(mesh)
440
+ this.worldRenderer.sceneOrigin.removeAndUntrack(mesh)
438
441
  disposeObject(mesh)
439
442
  }
440
443
  this.entities = {}
@@ -446,7 +449,7 @@ export class Entities {
446
449
 
447
450
  // Clean up player entity
448
451
  if (this.playerEntity) {
449
- this.worldRenderer.scene.remove(this.playerEntity)
452
+ this.worldRenderer.sceneOrigin.removeAndUntrack(this.playerEntity)
450
453
  disposeObject(this.playerEntity)
451
454
  this.playerEntity = null
452
455
  }
@@ -534,22 +537,29 @@ export class Entities {
534
537
 
535
538
  if (thirdPersonNow) {
536
539
  const yOffset = this.worldRenderer.playerStateReactive.eyeHeight
537
- const pos = this.worldRenderer.cameraObject.position.clone().add(new THREE.Vector3(0, -yOffset, 0))
538
- entity.position.set(pos.x, pos.y, pos.z)
540
+ // Set world position proxy auto-converts to scene coords
541
+ entity.position.set(
542
+ this.worldRenderer.cameraWorldPos.x,
543
+ this.worldRenderer.cameraWorldPos.y - yOffset,
544
+ this.worldRenderer.cameraWorldPos.z
545
+ )
539
546
 
540
547
  const p: any = (this.worldRenderer.playerStateReactive as any).position
541
548
  if (p && typeof p.x === 'number') {
542
549
  this.updateAutoWalkFlags(entityKey, entity, dtRaw, new THREE.Vector3(p.x, p.y, p.z))
543
550
  } else {
544
- this.updateAutoWalkFlags(entityKey, entity, dtRaw, entity.position)
551
+ const wp = this.worldRenderer.sceneOrigin.getWorldPosition(entity)
552
+ this.updateAutoWalkFlags(entityKey, entity, dtRaw, wp ? new THREE.Vector3(wp.x, wp.y, wp.z) : entity.position)
545
553
  }
546
554
 
547
555
  this.updateThirdPersonHeadAndBody(entity, dt)
548
556
  } else {
549
- this.updateAutoWalkFlags(entityKey, entity, dtRaw, entity.position)
557
+ const wp = this.worldRenderer.sceneOrigin.getWorldPosition(entity)
558
+ this.updateAutoWalkFlags(entityKey, entity, dtRaw, wp ? new THREE.Vector3(wp.x, wp.y, wp.z) : entity.position)
550
559
  }
551
560
  } else {
552
- this.updateAutoWalkFlags(entityKey, entity, dtRaw, entity.position)
561
+ const wp = this.worldRenderer.sceneOrigin.getWorldPosition(entity)
562
+ this.updateAutoWalkFlags(entityKey, entity, dtRaw, wp ? new THREE.Vector3(wp.x, wp.y, wp.z) : entity.position)
553
563
  }
554
564
 
555
565
  const { playerObject } = entity
@@ -790,6 +800,7 @@ export class Entities {
790
800
  skinTexture.needsUpdate = true
791
801
  playerObject.skin.map = skinTexture as any
792
802
  playerObject.skin.modelType = inferModelType(skinCanvas)
803
+ playerObject.skin['isCustom'] = skinUrl !== stevePngUrl
793
804
 
794
805
  let earsCanvas: HTMLCanvasElement | undefined
795
806
  if (!playerCustomSkinImage) {
@@ -1031,12 +1042,13 @@ export class Entities {
1031
1042
 
1032
1043
  if (entity.delete) {
1033
1044
  if (!e) return
1045
+ e.userData._posTween?.stop()
1034
1046
  if (e.additionalCleanup) e.additionalCleanup()
1035
1047
  e.traverse(c => {
1036
1048
  if (c['additionalCleanup']) c['additionalCleanup']()
1037
1049
  })
1038
1050
  this.onRemoveEntity(entity)
1039
- this.worldRenderer.scene.remove(e)
1051
+ this.worldRenderer.sceneOrigin.removeAndUntrack(e)
1040
1052
  disposeObject(e)
1041
1053
  // todo dispose textures as well ?
1042
1054
  delete this.entities[entity.id]
@@ -1109,6 +1121,7 @@ export class Entities {
1109
1121
  mesh.name = 'mesh'
1110
1122
  // set initial position so there are no weird jumps update after
1111
1123
  const pos = entity.pos ?? entity.position
1124
+ this.worldRenderer.sceneOrigin.track(group)
1112
1125
  group.position.set(pos.x, pos.y, pos.z)
1113
1126
 
1114
1127
  // todo use width and height instead
@@ -1326,7 +1339,20 @@ export class Entities {
1326
1339
  if (!e) return
1327
1340
  const ANIMATION_DURATION = justAdded ? 0 : TWEEN_DURATION
1328
1341
  if (entity.position) {
1329
- new TWEEN.Tween(e.position).to({ x: entity.position.x, y: entity.position.y, z: entity.position.z }, ANIMATION_DURATION).start()
1342
+ // Initialize tween target from current world position
1343
+ const currentWorld = this.worldRenderer.sceneOrigin.getWorldPosition(e) ?? { x: entity.position.x, y: entity.position.y, z: entity.position.z }
1344
+ if (!e.userData._tweenTarget) {
1345
+ e.userData._tweenTarget = { x: currentWorld.x, y: currentWorld.y, z: currentWorld.z }
1346
+ }
1347
+ // Stop previous position tween to prevent accumulation
1348
+ e.userData._posTween?.stop()
1349
+ // Tween a separate target object, apply via proxy on each update
1350
+ e.userData._posTween = new TWEEN.Tween(e.userData._tweenTarget)
1351
+ .to({ x: entity.position.x, y: entity.position.y, z: entity.position.z }, ANIMATION_DURATION)
1352
+ .onUpdate(() => {
1353
+ e.position.set(e.userData._tweenTarget.x, e.userData._tweenTarget.y, e.userData._tweenTarget.z)
1354
+ })
1355
+ .start()
1330
1356
  }
1331
1357
  if (entity.yaw) {
1332
1358
  const da = (entity.yaw - e.rotation.y) % (Math.PI * 2)
@@ -1366,8 +1392,13 @@ export class Entities {
1366
1392
  if (!mesh.visible) return
1367
1393
 
1368
1394
  const MAX_DISTANCE_SKIN_LOAD = 128
1369
- const cameraPos = this.worldRenderer.cameraObject.position
1370
- const distance = mesh.position.distanceTo(cameraPos)
1395
+ const cameraPos = this.worldRenderer.getCameraPosition()
1396
+ // Use world positions for accurate distance calculation
1397
+ const wp = this.worldRenderer.sceneOrigin.getWorldPosition(mesh)
1398
+ const entityWorldPos = wp
1399
+ ? new THREE.Vector3(wp.x, wp.y, wp.z)
1400
+ : mesh.position.clone().add(new THREE.Vector3(this.worldRenderer.sceneOrigin.x, this.worldRenderer.sceneOrigin.y, this.worldRenderer.sceneOrigin.z))
1401
+ const distance = entityWorldPos.distanceTo(cameraPos)
1371
1402
  if (distance < MAX_DISTANCE_SKIN_LOAD && distance < (this.worldRenderer.viewDistance * 16)) {
1372
1403
  if (this.loadedSkinEntityIds.has(String(entityId))) return
1373
1404
  void this.updatePlayerSkin(entityId, mesh.playerObject.realUsername, mesh.playerObject.realPlayerUuid, true, true)
@@ -331,7 +331,7 @@ export function getMesh(
331
331
  geometry.setAttribute('skinWeight', new THREE.Float32BufferAttribute(geoData.skinWeights, 4))
332
332
  geometry.setIndex(geoData.indices)
333
333
 
334
- const material = new THREE.MeshLambertMaterial({ transparent: true, alphaTest: 0.1 })
334
+ const material = new THREE.MeshLambertMaterial({ transparent: true, alphaTest: 0.1, side: THREE.DoubleSide })
335
335
  const mesh = new THREE.SkinnedMesh(geometry, material)
336
336
  mesh.add(...rootBones)
337
337
  mesh.bind(skeleton)
@@ -125,11 +125,14 @@
125
125
  "render_controllers": ["controller.render.arrow"]
126
126
  },
127
127
  "bat": {
128
+ "_comment": "Wing tip mirror flags are swapped vs vanilla Bedrock model: our addCube() mirror implementation flips UVs in the opposite direction, so rightWingTip needs mirror=true and leftWingTip needs mirror=false to render correctly.",
128
129
  "identifier": "minecraft:bat",
129
130
  "materials": {"default": "bat"},
130
- "textures": {"default": "textures/entity/bat"},
131
+ "textures": {"default": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAALHSURBVHja7VrBSiQxFJybHvci6G1gF2TBwyoKelnWy55kWYS9ePDmD+in+KcevLb7GgpqipeeTkzPpGdewSNJmx6nKtXJS3oWiwTOv33pbs6Ou9vr731YHddQ5/ZiAHa/9cXnjLln62DyLALIL48P+4AIQ+RZAP2cnO909/PH4uvRQcdh1zSqwCMPIiDPIqwT8uHufMVJuDfnOxnhq9OT7vHPZR9WZxEgSjUBUi7IcQBG++XpphfB6rgvVwCQ//f7og8WAeL8F6Kr6oD7X8s+PPuOmQNYAAsIUOIAkDeSLIK1LeCMagIweZQ8iRkBlJuYl5gwSlxjZ2R9KM/2XBphsyxEQGhfOCEVNQXQ0UZdo2i21+edR57DmxjZFRpTCaD2/7QAXsAFcEKq3zYEGHJBtgCeC97f3vtnG0uY1e1aavQ3/QgMuaBoDtCsDyWv3yqSlxhpTClAygXZArAILIaS8ezOo52TIJVA1/sqAswRnhCfEoAnuqGkZwpbZxJfyflTQuysAN7GxxNiZ62fEoCFMBGqpcK8h8ckqIkRoiU3VPkHIM/2R4rMaTLK0pGr9oVrw9v7Iy/QDLFEAH5umxTAW9utreTHOuD1+e/KaQ4mrGrb1ykE0L07HODFmKMsJs1b2GYfAc0MdYPEThiTvIA8DjWayNxSs3pJePPDOvJNCKDL27r22L95AlRJXaewuh57cVuPx7Acen1VlNkIoKR0++udCer84PXn42wj660EW38BooS99rqTn1R/FkCTnibyAH3l5bW9GOqjx+Wa8TWVCXpn/NzW63oIote1bH6HlSKUK0BKkFkIgDSX01+taz9+vTV0X/MC6Hu+1OFmSb9tHp6MRopIrQgBAjMDtr2bfOvb3KTIP2vZy9HnvH6vXJD6ZdheOYAJl/yOZ/YCMOFYygKBQCAQCAQCgUAgEAgEAoFAIBDYKXwAXSs8dJ1Ggb4AAAAASUVORK5CYII="},
131
132
  "geometry": {
132
133
  "default": {
134
+ "texturewidth": 64,
135
+ "textureheight": 64,
133
136
  "visible_bounds_width": 1,
134
137
  "visible_bounds_height": 1,
135
138
  "visible_bounds_offset": [0, 0.5, 0],
@@ -174,6 +177,7 @@
174
177
  },
175
178
  {
176
179
  "name": "rightWingTip",
180
+ "mirror": true,
177
181
  "pivot": [-12, 23, 1.5],
178
182
  "cubes": [
179
183
  {"origin": [-20, 10, 1.5], "size": [8, 12, 1], "uv": [24, 16]}
@@ -191,7 +195,6 @@
191
195
  },
192
196
  {
193
197
  "name": "leftWingTip",
194
- "mirror": true,
195
198
  "pivot": [12, 23, 1.5],
196
199
  "cubes": [
197
200
  {"origin": [12, 10, 1.5], "size": [8, 12, 1], "uv": [24, 16]}
@@ -3541,6 +3544,109 @@
3541
3544
  "render_controllers": ["controller.render.salmon"],
3542
3545
  "spawn_egg": {"texture": "spawn_egg", "texture_index": 47}
3543
3546
  },
3547
+ "sheep": {
3548
+ "identifier": "minecraft:sheep",
3549
+ "materials": {"default": "sheep"},
3550
+ "textures": {"default": "textures/entity/sheep/sheep", "wool": "textures/entity/sheep/sheep_fur"},
3551
+ "geometry": {
3552
+ "default": {
3553
+ "visible_bounds_width": 2,
3554
+ "visible_bounds_height": 1.75,
3555
+ "visible_bounds_offset": [0, 0.5, 0],
3556
+ "texturewidth": 64,
3557
+ "textureheight": 32,
3558
+ "bones": [
3559
+ {
3560
+ "name": "body",
3561
+ "pivot": [0, 19, 2],
3562
+ "bind_pose_rotation": [90, 0, 0],
3563
+ "cubes": [
3564
+ {"origin": [-4, 13, -5], "size": [8, 16, 6], "uv": [28, 8]}
3565
+ ]
3566
+ },
3567
+ {
3568
+ "name": "head",
3569
+ "pivot": [0, 18, -8],
3570
+ "cubes": [
3571
+ {"origin": [-3, 16, -14], "size": [6, 6, 8], "uv": [0, 0]}
3572
+ ]
3573
+ },
3574
+ {
3575
+ "name": "leg0",
3576
+ "parent": "body",
3577
+ "pivot": [-3, 12, 7],
3578
+ "cubes": [{"origin": [-5, 0, 5], "size": [4, 12, 4], "uv": [0, 16]}]
3579
+ },
3580
+ {
3581
+ "name": "leg1",
3582
+ "parent": "body",
3583
+ "pivot": [3, 12, 7],
3584
+ "cubes": [{"origin": [1, 0, 5], "size": [4, 12, 4], "uv": [0, 16]}]
3585
+ },
3586
+ {
3587
+ "name": "leg2",
3588
+ "parent": "body",
3589
+ "pivot": [-3, 12, -5],
3590
+ "cubes": [{"origin": [-5, 0, -7], "size": [4, 12, 4], "uv": [0, 16]}]
3591
+ },
3592
+ {
3593
+ "name": "leg3",
3594
+ "parent": "body",
3595
+ "pivot": [3, 12, -5],
3596
+ "cubes": [{"origin": [1, 0, -7], "size": [4, 12, 4], "uv": [0, 16]}]
3597
+ }
3598
+ ]
3599
+ },
3600
+ "wool": {
3601
+ "visible_bounds_width": 2,
3602
+ "visible_bounds_height": 1.75,
3603
+ "visible_bounds_offset": [0, 0.5, 0],
3604
+ "texturewidth": 64,
3605
+ "textureheight": 64,
3606
+ "bones": [
3607
+ {
3608
+ "name": "head",
3609
+ "pivot": [0, 18, -8],
3610
+ "cubes": [
3611
+ {"origin": [-3, 16, -12], "size": [6, 6, 6], "uv": [0, 32], "inflate": 0.6}
3612
+ ]
3613
+ },
3614
+ {
3615
+ "name": "body",
3616
+ "pivot": [0, 19, 2],
3617
+ "bind_pose_rotation": [90, 0, 0],
3618
+ "cubes": [
3619
+ {"origin": [-4, 13, -5], "size": [8, 16, 6], "uv": [28, 40], "inflate": 1.75}
3620
+ ]
3621
+ },
3622
+ {
3623
+ "name": "leg0",
3624
+ "parent": "body",
3625
+ "pivot": [-3, 12, 7],
3626
+ "cubes": [{"origin": [-5, 6, 5], "size": [4, 6, 4], "uv": [0, 48], "inflate": 0.5}]
3627
+ },
3628
+ {
3629
+ "name": "leg1",
3630
+ "parent": "body",
3631
+ "pivot": [3, 12, 7],
3632
+ "cubes": [{"origin": [1, 6, 5], "size": [4, 6, 4], "uv": [0, 48], "inflate": 0.5}]
3633
+ },
3634
+ {
3635
+ "name": "leg2",
3636
+ "parent": "body",
3637
+ "pivot": [-3, 12, -5],
3638
+ "cubes": [{"origin": [-5, 6, -7], "size": [4, 6, 4], "uv": [0, 48], "inflate": 0.5}]
3639
+ },
3640
+ {
3641
+ "name": "leg3",
3642
+ "parent": "body",
3643
+ "pivot": [3, 12, -5],
3644
+ "cubes": [{"origin": [1, 6, -7], "size": [4, 6, 4], "uv": [0, 48], "inflate": 0.5}]
3645
+ }
3646
+ ]
3647
+ }
3648
+ }
3649
+ },
3544
3650
  "shulker_bullet": {
3545
3651
  "identifier": "minecraft:shulker_bullet",
3546
3652
  "materials": {"default": "shulker_bullet"},
@@ -22,7 +22,6 @@ export { default as parrot } from './models/parrot.obj'
22
22
  export { default as piglin } from './models/piglin.obj'
23
23
  export { default as pillager } from './models/pillager.obj'
24
24
  export { default as rabbit } from './models/rabbit.obj'
25
- export { default as sheep } from './models/sheep.obj'
26
25
  export { default as arrow } from './models/arrow.obj'
27
26
  export { default as shulker } from './models/shulker.obj'
28
27
  export { default as sniffer } from './models/sniffer.obj'