minecraft-renderer 0.1.82 → 0.1.84

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.82",
3
+ "version": "0.1.84",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,6 +9,12 @@ export const disposeObject = (obj: THREE.Object3D, cleanTextures = false) => {
9
9
  obj.geometry?.dispose?.()
10
10
  obj.material?.dispose?.()
11
11
  }
12
+ if (obj instanceof THREE.SkinnedMesh) {
13
+ // SkinnedMesh skeletons are not scene-graph children, so the recursion below never reaches them.
14
+ // skeleton.dispose() frees the lazily-created bone DataTexture (Skeleton.computeBoneTexture),
15
+ // which otherwise leaks +1 GPU texture per entity on every F3+A reload (issue #56).
16
+ obj.skeleton?.dispose?.()
17
+ }
12
18
  if (obj.children) {
13
19
  // eslint-disable-next-line unicorn/no-array-for-each
14
20
  obj.children.forEach(child => disposeObject(child, cleanTextures))
@@ -746,8 +746,8 @@ export function renderWasmOutputToGeometry(
746
746
  }
747
747
 
748
748
  // Mirror JS mesher: doAO = model.ao ?? block.boundingBox !== 'empty'.
749
- // When false, faces are emitted full-bright without AO/light sampling and without
750
- // triangle-flip reordering (matches JS `light = 1` and standard winding).
749
+ // When false, faces skip AO but still use flat block/sky light from the
750
+ // neighbor cell (see models.ts baseChannels), not full-bright defaults.
751
751
  const doAO = (model as any).ao ?? cachedModel.boundingBox !== 'empty'
752
752
 
753
753
  for (const element of model.elements ?? []) {
@@ -828,6 +828,11 @@ export function renderWasmOutputToGeometry(
828
828
 
829
829
  const tint = getTint(matchingEFace, cachedModel.blockName, cachedModel.blockProps, biome, world)
830
830
 
831
+ const useModelLighting = (!cachedModel.isCube || globalMatrix != null) && world
832
+ const baseChannels = useModelLighting
833
+ ? world.getChannelLightNorm(new Vec3(bx, by, bz).offset(transformedDirI[0], transformedDirI[1], transformedDirI[2]))
834
+ : null
835
+
831
836
  const baseIndex = tgtPos.length / 3
832
837
  const computedAoValues = [3, 3, 3, 3]
833
838
  for (let cornerIdx = 0; cornerIdx < 4; cornerIdx++) {
@@ -845,11 +850,9 @@ export function renderWasmOutputToGeometry(
845
850
 
846
851
  tgtNorm.push(transformedDir[0], transformedDir[1], transformedDir[2])
847
852
 
848
- const useModelLighting = (!cachedModel.isCube || globalMatrix != null) && world
849
-
850
853
  let ao = 3
851
- let skyLightNorm = 1
852
- let blockLightNorm = 0
854
+ let skyLightNorm = baseChannels?.sky ?? 1
855
+ let blockLightNorm = baseChannels?.block ?? 0
853
856
  const faceDir = transformedDirI as [number, number, number]
854
857
 
855
858
  if (!doAO) {
@@ -44,6 +44,9 @@ function processUpdateLightV17(rawPacket: Uint8Array, numSections: number): void
44
44
  })
45
45
  invalidateConversion(x, z)
46
46
  dirtyColumnSectionsForLightUpdate(x, z)
47
+ const hadColumn = !!world?.getColumn(x, z)
48
+ syncV17LightToColumn(x, z)
49
+ if (!hadColumn) pendingLightDirtyColumns.add(rawCacheKey(x, z))
47
50
  } catch (err) {
48
51
  console.warn('[WASM Mesher] parseUpdateLightV17 failed:', err)
49
52
  }
@@ -248,6 +251,44 @@ interface UpdateLightV17Entry {
248
251
  blockLight: Uint8Array
249
252
  }
250
253
  const updateLightV17Cache = new Map<string, UpdateLightV17Entry>()
254
+ /** Columns that received `update_light` before the worker column existed. */
255
+ const pendingLightDirtyColumns = new Set<string>()
256
+ const _syncLightPos = new Vec3(0, 0, 0)
257
+
258
+ function resolveUpdateLightV17Entry(x: number, z: number, chunk: any | undefined, worldMinY: number, worldMaxY: number): UpdateLightV17Entry | undefined {
259
+ const cached = updateLightV17Cache.get(rawCacheKey(x, z))
260
+ if (cached?.blockLight?.length) return cached
261
+ if (!chunk) return cached
262
+ const { result } = getOrConvertColumn(x, z, chunk, version, worldMinY, worldMaxY, () => convertChunkToWasm(chunk, version, x, z, worldMinY, worldMaxY), chunk)
263
+ return { blockLight: result.blockLight, skyLight: result.skyLight }
264
+ }
265
+
266
+ /** Write cached 1.17 `update_light` arrays into the worker prismarine column. */
267
+ function syncV17LightToColumn(x: number, z: number): boolean {
268
+ if (!world) return false
269
+ const col = world.getColumn(x, z)
270
+ const entry = updateLightV17Cache.get(rawCacheKey(x, z))
271
+ if (!col || !entry) return false
272
+
273
+ const CHUNK_SIZE = 16
274
+ const minY = config?.worldMinY ?? 0
275
+ const maxY = config?.worldMaxY ?? 256
276
+ const { blockLight, skyLight } = entry
277
+
278
+ for (let y = minY; y < maxY; y++) {
279
+ for (let lz = 0; lz < CHUNK_SIZE; lz++) {
280
+ for (let lx = 0; lx < CHUNK_SIZE; lx++) {
281
+ const idx = lx + lz * CHUNK_SIZE + (y - minY) * CHUNK_SIZE * CHUNK_SIZE
282
+ if (idx >= blockLight.length) continue
283
+ _syncLightPos.set(lx, y, lz)
284
+ col.setBlockLight(_syncLightPos, blockLight[idx]!)
285
+ col.setSkyLight(_syncLightPos, skyLight[idx]!)
286
+ }
287
+ }
288
+ }
289
+
290
+ return true
291
+ }
251
292
 
252
293
  // 1.16 path: pre-extracted section bytes + bit mask. Bit mask in 1.16
253
294
  // is a varint that fits in i32 (only 16 sections), so we accept it as a
@@ -625,7 +666,7 @@ const meshMultiColumnsFromParsedV16V17 = (
625
666
  bitMapLoHi[i * 2] = entry.bitMapLoHi[0]
626
667
  bitMapLoHi[i * 2 + 1] = entry.bitMapLoHi[1]
627
668
  if (i === 0) maxBitsPerBlock = entry.maxBitsPerBlock
628
- const light = updateLightV17Cache.get(key)
669
+ const light = resolveUpdateLightV17Entry(chunksToUse[i].x, chunksToUse[i].z, chunksToUse[i].chunk, worldMinY, worldMaxY)
629
670
  skyLightList.push(light?.skyLight ?? new Uint8Array(0))
630
671
  blockLightList.push(light?.blockLight ?? new Uint8Array(0))
631
672
  } else {
@@ -720,6 +761,11 @@ const handleMessage = async (data: any) => {
720
761
  invalidateConversion(data.x, data.z)
721
762
  if (!world) break
722
763
  world.addColumn(data.x, data.z, data.chunk)
764
+ syncV17LightToColumn(data.x, data.z)
765
+ const lightKey = rawCacheKey(data.x, data.z)
766
+ if (pendingLightDirtyColumns.delete(lightKey) || updateLightV17Cache.has(lightKey)) {
767
+ dirtyColumnSectionsForLightUpdate(data.x, data.z)
768
+ }
723
769
  if (data.customBlockModels) {
724
770
  const chunkKey = `${data.x},${data.z}`
725
771
  world.customBlockModels.set(chunkKey, data.customBlockModels)
@@ -760,6 +806,7 @@ const handleMessage = async (data: any) => {
760
806
  updateLightV17Cache.delete(rawCacheKey(data.x, data.z))
761
807
  parsedV16Cache.delete(rawCacheKey(data.x, data.z))
762
808
  updateLightV16Cache.delete(rawCacheKey(data.x, data.z))
809
+ pendingLightDirtyColumns.delete(rawCacheKey(data.x, data.z))
763
810
  if (!world) break
764
811
  world.removeColumn(data.x, data.z)
765
812
  world.customBlockModels.delete(`${data.x},${data.z}`)
@@ -863,6 +910,7 @@ const handleMessage = async (data: any) => {
863
910
  rawMapChunkCache.clear()
864
911
  parsedV17Cache.clear()
865
912
  updateLightV17Cache.clear()
913
+ pendingLightDirtyColumns.clear()
866
914
  parsedV16Cache.clear()
867
915
  updateLightV16Cache.clear()
868
916
  globalVar.mcData = null
@@ -1068,7 +1116,7 @@ function processColumnTick() {
1068
1116
  wasmResult = meshColumnFromRawV18Plus(rawEntry, x, z, worldMinY, worldMaxY, meta)
1069
1117
  if (wasmResult) columnMeshPath = 'v18_fused'
1070
1118
  } else if (v17Entry) {
1071
- const v17Light = updateLightV17Cache.get(rawCacheKey(x, z))
1119
+ const v17Light = resolveUpdateLightV17Entry(x, z, targetChunk, worldMinY, worldMaxY)
1072
1120
  wasmResult = meshColumnFromParsedV16V17(
1073
1121
  v17Entry.chunkData,
1074
1122
  v17Entry.bitMapLoHi,
@@ -1139,7 +1187,7 @@ function processColumnTick() {
1139
1187
  const cs = performance.now()
1140
1188
  const rawEntry = rawMapChunkCache.get(rawCacheKey(cx, cz))
1141
1189
  const v17Entry = parsedV17Cache.get(rawCacheKey(cx, cz))
1142
- const v17Light = updateLightV17Cache.get(rawCacheKey(cx, cz))
1190
+ const v17Light = resolveUpdateLightV17Entry(cx, cz, chunk, worldMinY, worldMaxY)
1143
1191
  const v16Entry = parsedV16Cache.get(rawCacheKey(cx, cz))
1144
1192
  const v16Light = updateLightV16Cache.get(rawCacheKey(cx, cz))
1145
1193