minerender 2.1.0-alpha.128 → 2.1.0-alpha.129

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.
@@ -263,6 +263,10 @@ var _Caching, Caching;
263
263
  var init_Caching = __esm({
264
264
  "src/cache/Caching.ts"() {
265
265
  _Caching = class _Caching {
266
+ /** Changes when shared assets are cleared, so derived caches can discard their old values. */
267
+ static get generation() {
268
+ return this._generation;
269
+ }
266
270
  static createAssetCache() {
267
271
  return Caches.builder().expireAfterWrite(Time.minutes(10)).expireAfterAccess(Time.minutes(5)).expirationInterval(Time.seconds(30)).buildAsync();
268
272
  }
@@ -298,6 +302,7 @@ var init_Caching = __esm({
298
302
  }
299
303
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
300
304
  static clear() {
305
+ this._generation++;
301
306
  for (const cache of this.all) {
302
307
  cache.invalidateAll();
303
308
  }
@@ -309,6 +314,7 @@ var init_Caching = __esm({
309
314
  * Use {@link shutdown} to also stop requests and the shared Ticker.
310
315
  */
311
316
  static end() {
317
+ this._generation++;
312
318
  for (const cache of this.all) {
313
319
  cache.invalidateAll();
314
320
  cache.end();
@@ -320,6 +326,7 @@ var init_Caching = __esm({
320
326
  };
321
327
  }
322
328
  };
329
+ _Caching._generation = 0;
323
330
  _Caching.rawImageCache = Caches.builder().expireAfterWrite(Time.minutes(10)).expireAfterAccess(Time.minutes(5)).expirationInterval(Time.seconds(5)).buildAsync();
324
331
  _Caching.imageDataCache = Caches.builder().expireAfterWrite(Time.minutes(10)).expireAfterAccess(Time.minutes(5)).expirationInterval(Time.seconds(10)).buildAsync();
325
332
  _Caching.canvasImageDataCache = Caches.builder().expireAfterWrite(Time.minutes(10)).expireAfterAccess(Time.minutes(5)).expirationInterval(Time.seconds(10)).buildAsync();
@@ -17345,18 +17352,23 @@ var SectionModels = class {
17345
17352
  return this.template(variant, choice, tints);
17346
17353
  }));
17347
17354
  }
17348
- /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
17349
- async prepareState(blockState3, properties = {}, biomeColor) {
17355
+ /** Resolves shared templates; dynamic biome tints are supplied on section placements instead of cached geometry. */
17356
+ async prepareState(blockState3, properties = {}, biomeColor, dynamicBiomeTint = false) {
17350
17357
  const prepared = await this.state(blockState3, properties);
17351
17358
  if (!prepared) return void 0;
17352
17359
  let color2;
17353
17360
  const resolveColor = typeof biomeColor === "function" ? () => color2 ?? (color2 = biomeColor()) : biomeColor;
17354
17361
  const templates = /* @__PURE__ */ new Map();
17362
+ const source = dynamicBiomeTint && await BlockTints.biomeSource(blockState3.key);
17355
17363
  await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
17356
17364
  const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
17357
- templates.set(variant, await this.template(variant, choice, tints));
17365
+ const biomeTints = source ? await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, 16777215) : void 0;
17366
+ templates.set(variant, await this.template(variant, choice, tints, biomeTints));
17358
17367
  }));
17359
- return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
17368
+ return {
17369
+ pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))),
17370
+ biomeTint: source ? [...templates.values()].some((template) => template.biomeTint?.some((value) => value === 1)) || void 0 : void 0
17371
+ };
17360
17372
  }
17361
17373
  async state(blockState3, properties) {
17362
17374
  let states = this.states.get(blockState3);
@@ -17399,10 +17411,10 @@ var SectionModels = class {
17399
17411
  }
17400
17412
  return { state, groups, choices: prepared };
17401
17413
  }
17402
- async template(variant, { model, atlas, rotation }, tints) {
17414
+ async template(variant, { model, atlas, rotation }, tints, biomeTints) {
17403
17415
  let models = this.models.get(model);
17404
17416
  if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17405
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17417
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints, biomeTints]);
17406
17418
  return cached(models, key, async () => {
17407
17419
  const object4 = new SectionModel(model, {
17408
17420
  instanceMeshes: false,
@@ -17416,11 +17428,13 @@ var SectionModels = class {
17416
17428
  object4.clear();
17417
17429
  const keep = [];
17418
17430
  const cullFaces = [];
17431
+ const biomeTint = biomeTints ? [] : void 0;
17419
17432
  for (const element of atlas.model.elements) {
17420
17433
  for (const face of CUBE_FACES) {
17421
17434
  const data = element.faces[face];
17422
17435
  keep.push(data !== void 0);
17423
17436
  if (!data) continue;
17437
+ biomeTint?.push(Number(data.tintindex !== void 0 && biomeTints?.[data.tintindex] !== void 0));
17424
17438
  const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17425
17439
  if (local < 0) {
17426
17440
  cullFaces.push(0);
@@ -17436,6 +17450,7 @@ var SectionModels = class {
17436
17450
  geometry,
17437
17451
  atlas,
17438
17452
  cullFaces: new Uint8Array(cullFaces),
17453
+ biomeTint: biomeTint?.some(Boolean) ? new Uint8Array(biomeTint) : void 0,
17439
17454
  occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17440
17455
  layer: atlas.hasTranslucency ? 1 : 0,
17441
17456
  ambientOcclusion: model.ambientocclusion !== false
@@ -17665,6 +17680,132 @@ var WorldEntities = class {
17665
17680
  // src/world/MineRenderWorld.ts
17666
17681
  init_FluidQuads();
17667
17682
 
17683
+ // src/world/_biomes/BiomeBlend.ts
17684
+ init_AssetKey();
17685
+ init_AssetLoader();
17686
+ init_Caching();
17687
+ var MAX_TILES = 256;
17688
+ function validateBiomeBlendRadius(radius) {
17689
+ if (!Number.isInteger(radius) || radius < 0 || radius > 7) {
17690
+ throw new RangeError("biomeBlendRadius must be an integer from 0 to 7");
17691
+ }
17692
+ }
17693
+ var BiomeBlend = class {
17694
+ constructor(biomeAt, radius) {
17695
+ this.biomeAt = biomeAt;
17696
+ this.tiles = /* @__PURE__ */ new Map();
17697
+ this.generation = Caching.generation;
17698
+ validateBiomeBlendRadius(radius);
17699
+ this._radius = radius;
17700
+ }
17701
+ get radius() {
17702
+ return this._radius;
17703
+ }
17704
+ clear() {
17705
+ this.tiles.clear();
17706
+ }
17707
+ invalidateColumn(x, z) {
17708
+ for (const [key, tile] of this.tiles) {
17709
+ if (Math.abs(tile.x - x) <= 1 && Math.abs(tile.z - z) <= 1) this.tiles.delete(key);
17710
+ }
17711
+ }
17712
+ async getColor(kind, origin, position) {
17713
+ const [x, y, z] = position;
17714
+ const biome = this.biomeAt(x, y, z);
17715
+ if (biome === void 0) return void 0;
17716
+ if (this.generation !== Caching.generation) {
17717
+ this.clear();
17718
+ this.generation = Caching.generation;
17719
+ }
17720
+ const tileX = Math.floor(x / 16), tileZ = Math.floor(z / 16), quartY = Math.floor(y / 4);
17721
+ const root = origin?.root ?? AssetLoader.ROOT;
17722
+ const key = JSON.stringify([root, AssetLoader.persistentScope, kind, this._radius, tileX, quartY, tileZ]);
17723
+ let tile = this.tiles.get(key);
17724
+ if (tile) {
17725
+ this.tiles.delete(key);
17726
+ this.tiles.set(key, tile);
17727
+ } else {
17728
+ const snapshot = new AssetKey("minecraft", "", void 0, void 0, "assets", ".json", root);
17729
+ const colors = this.buildTile(kind, snapshot, tileX, quartY, tileZ, this._radius).then((result2) => {
17730
+ if (!result2.complete && this.tiles.get(key)?.colors === colors) this.tiles.delete(key);
17731
+ return result2;
17732
+ }, (error) => {
17733
+ if (this.tiles.get(key)?.colors === colors) this.tiles.delete(key);
17734
+ throw error;
17735
+ });
17736
+ tile = { x: tileX, z: tileZ, colors };
17737
+ this.tiles.set(key, tile);
17738
+ if (this.tiles.size > MAX_TILES) this.tiles.delete(this.tiles.keys().next().value);
17739
+ }
17740
+ const result = await tile.colors;
17741
+ if (result.failures.has(biome)) throw result.failures.get(biome);
17742
+ const color2 = result.colors[(z - tileZ * 16) * 16 + x - tileX * 16];
17743
+ return color2 === -1 ? void 0 : color2;
17744
+ }
17745
+ async buildTile(kind, origin, tileX, quartY, tileZ, radius) {
17746
+ const size = 16 + radius * 2, stride = size + 1;
17747
+ const startX = tileX * 16 - radius, startZ = tileZ * 16 - radius;
17748
+ const quartX = Math.floor(startX / 4), quartZ = Math.floor(startZ / 4);
17749
+ const quartSize = Math.floor((startX + size - 1) / 4) - quartX + 1;
17750
+ const ids = new Array(quartSize * quartSize);
17751
+ const biomes = /* @__PURE__ */ new Map();
17752
+ for (let z = 0; z < quartSize; z++) {
17753
+ for (let x = 0; x < quartSize; x++) {
17754
+ const id = this.biomeAt((quartX + x) * 4, quartY * 4, (quartZ + z) * 4);
17755
+ ids[z * quartSize + x] = id;
17756
+ if (id !== void 0 && !biomes.has(id)) {
17757
+ biomes.set(id, (async () => {
17758
+ const swamp = kind === "grass" && (await Biomes.get(id, origin))?.effects.grass_color_modifier === "swamp";
17759
+ return { color: swamp ? void 0 : await Biomes.getColor(id, kind, origin), swamp };
17760
+ })());
17761
+ }
17762
+ }
17763
+ }
17764
+ const failures = /* @__PURE__ */ new Map();
17765
+ const loaded = new Map(await Promise.all([...biomes].map(async ([id, color2]) => {
17766
+ try {
17767
+ return [id, await color2];
17768
+ } catch (error) {
17769
+ failures.set(id, error);
17770
+ return [id, { color: void 0, swamp: false }];
17771
+ }
17772
+ })));
17773
+ const complete = [...loaded.values()].every((value) => value.swamp || value.color !== void 0);
17774
+ const samples = new Int32Array(size * size).fill(-1);
17775
+ const sums = Array.from({ length: 4 }, () => new Uint32Array(stride * stride));
17776
+ for (let z = 0; z < size; z++) {
17777
+ const row = [0, 0, 0, 0];
17778
+ for (let x = 0; x < size; x++) {
17779
+ const id = ids[(Math.floor((startZ + z) / 4) - quartZ) * quartSize + Math.floor((startX + x) / 4) - quartX];
17780
+ const value = id === void 0 ? void 0 : loaded.get(id);
17781
+ const color2 = value?.swamp ? swampGrassColor(startX + x, startZ + z) : value?.color;
17782
+ if (color2 !== void 0) {
17783
+ samples[z * size + x] = color2;
17784
+ row[0] += color2 >> 16 & 255;
17785
+ row[1] += color2 >> 8 & 255;
17786
+ row[2] += color2 & 255;
17787
+ row[3]++;
17788
+ }
17789
+ for (let channel = 0; channel < 4; channel++) {
17790
+ sums[channel][(z + 1) * stride + x + 1] = sums[channel][z * stride + x + 1] + row[channel];
17791
+ }
17792
+ }
17793
+ }
17794
+ const colors = new Int32Array(256).fill(-1), width = radius * 2 + 1;
17795
+ for (let z = 0; z < 16; z++) {
17796
+ for (let x = 0; x < 16; x++) {
17797
+ if (samples[(z + radius) * size + x + radius] === -1) continue;
17798
+ const topLeft = z * stride + x, topRight = topLeft + width;
17799
+ const bottomLeft = (z + width) * stride + x, bottomRight = bottomLeft + width;
17800
+ const sum = (values) => values[bottomRight] - values[bottomLeft] - values[topRight] + values[topLeft];
17801
+ const count = sum(sums[3]);
17802
+ if (count) colors[z * 16 + x] = Math.floor(sum(sums[0]) / count) << 16 | Math.floor(sum(sums[1]) / count) << 8 | Math.floor(sum(sums[2]) / count);
17803
+ }
17804
+ }
17805
+ return { colors, complete, failures };
17806
+ }
17807
+ };
17808
+
17668
17809
  // src/world/WorldLight.ts
17669
17810
  function copySectionLight(light) {
17670
17811
  const copy = {};
@@ -17728,12 +17869,17 @@ var MineRenderWorld = class {
17728
17869
  this.lightValidity = /* @__PURE__ */ new Map();
17729
17870
  this.highestSkySection = /* @__PURE__ */ new Map();
17730
17871
  this.ambientOcclusionPending = false;
17872
+ this.pendingBiomes = /* @__PURE__ */ new Map();
17731
17873
  this.pendingCulling = /* @__PURE__ */ new Map();
17732
17874
  if (options.savedLighting && !options.sectionMeshing) throw new TypeError("savedLighting requires sectionMeshing");
17733
17875
  if (options.ambientOcclusion && !options.sectionMeshing) throw new TypeError("ambientOcclusion requires sectionMeshing");
17734
17876
  if (options.ambientLight !== void 0 && (!Number.isFinite(options.ambientLight) || options.ambientLight < 0 || options.ambientLight > 1)) {
17735
17877
  throw new RangeError("ambientLight must be a number from 0 to 1");
17736
17878
  }
17879
+ this._biomeBlendRadius = options.biomeBlendRadius ?? 0;
17880
+ validateBiomeBlendRadius(this._biomeBlendRadius);
17881
+ if (this._biomeBlendRadius > 0 && !options.sectionMeshing) throw new TypeError("biomeBlendRadius requires sectionMeshing");
17882
+ if (this._biomeBlendRadius) this.biomeBlend = new BiomeBlend((x, y, z) => this.getBiomeAt(x, y, z), this._biomeBlendRadius);
17737
17883
  this.scene = scene;
17738
17884
  this.savedLighting = options.savedLighting ?? false;
17739
17885
  this.ambientLight = options.ambientLight ?? 0;
@@ -17774,6 +17920,65 @@ var MineRenderWorld = class {
17774
17920
  await this.updateCulling([]);
17775
17921
  this.ambientOcclusionPending = false;
17776
17922
  }
17923
+ /** Horizontal biome-color sampling radius, from 0 to 7 blocks. Zero disables blending. */
17924
+ get biomeBlendRadius() {
17925
+ return this._biomeBlendRadius;
17926
+ }
17927
+ /** Refreshes loaded grass, foliage, and water colors without reloading their source chunks. */
17928
+ async setBiomeBlendRadius(radius) {
17929
+ validateBiomeBlendRadius(radius);
17930
+ if (radius > 0 && !this.sectionModels) throw new TypeError("biomeBlendRadius requires sectionMeshing");
17931
+ if (radius !== this._biomeBlendRadius) {
17932
+ this._biomeBlendRadius = radius;
17933
+ this.biomeBlend?.clear();
17934
+ this.biomeBlend = radius ? new BiomeBlend((x, y, z) => this.getBiomeAt(x, y, z), radius) : void 0;
17935
+ for (const chunk of this._chunks.values()) this.pendingBiomes.set(chunk, void 0);
17936
+ }
17937
+ const failures = await this.refreshBiomes();
17938
+ if (failures.size) throw failures.values().next().value;
17939
+ }
17940
+ markBiomesChanged(x, z, y, includeColumn = true) {
17941
+ const queued = [];
17942
+ if (!this.biomeBlend) return queued;
17943
+ this.biomeBlend.invalidateColumn(x, z);
17944
+ for (const chunk of this._chunks.values()) {
17945
+ if (Math.abs(chunk.x - x) > 1 || Math.abs(chunk.z - z) > 1 || !includeColumn && chunk.x === x && chunk.z === z || y !== void 0 && chunk.y !== y && chunk.y !== y + 1) continue;
17946
+ queued.push(chunk);
17947
+ if (!this.pendingBiomes.has(chunk)) this.pendingBiomes.set(chunk, []);
17948
+ const columns = this.pendingBiomes.get(chunk);
17949
+ if (columns && !columns.some((column) => column.x === x && column.z === z)) columns.push({ x, z });
17950
+ }
17951
+ return queued;
17952
+ }
17953
+ async refreshBiomes() {
17954
+ if (this.biomeRefreshing) await this.biomeRefreshing;
17955
+ if (!this.pendingBiomes.size) return /* @__PURE__ */ new Map();
17956
+ return this.biomeRefreshing ?? (this.biomeRefreshing = Promise.resolve().then(async () => {
17957
+ const failures = /* @__PURE__ */ new Map();
17958
+ const batch = [...this.pendingBiomes.keys()];
17959
+ let sliceStart = performance.now();
17960
+ try {
17961
+ for (const chunk of batch) {
17962
+ const columns = this.pendingBiomes.get(chunk);
17963
+ this.pendingBiomes.delete(chunk);
17964
+ if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) !== chunk) continue;
17965
+ try {
17966
+ await chunk.refreshBiomes(columns);
17967
+ } catch (error) {
17968
+ failures.set(chunk, error);
17969
+ if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
17970
+ }
17971
+ if (performance.now() - sliceStart > 8) {
17972
+ await yieldToEventLoop();
17973
+ sliceStart = performance.now();
17974
+ }
17975
+ }
17976
+ return failures;
17977
+ } finally {
17978
+ this.biomeRefreshing = void 0;
17979
+ }
17980
+ }));
17981
+ }
17777
17982
  getBlockAt(posOrX, y, z) {
17778
17983
  if (typeof posOrX == "number") {
17779
17984
  return this.getBlockAt(new Vector319(posOrX, y, z));
@@ -18009,6 +18214,9 @@ var MineRenderWorld = class {
18009
18214
  }
18010
18215
  await placeEntities?.();
18011
18216
  } finally {
18217
+ this.markBiomesChanged(chunk.x, chunk.z, void 0, false);
18218
+ const failures = await this.refreshBiomes();
18219
+ if (failures.size) console.warn("Failed to refresh biome colors", failures.values().next().value);
18012
18220
  await this.updateCulling([...changes.values()]);
18013
18221
  }
18014
18222
  }
@@ -18025,11 +18233,14 @@ var MineRenderWorld = class {
18025
18233
  try {
18026
18234
  await this.clearChunkColumn(x, z);
18027
18235
  } finally {
18236
+ const failures = await this.refreshBiomes();
18237
+ if (failures.size) console.warn("Failed to refresh biome colors", failures.values().next().value);
18028
18238
  await this.updateCulling([]);
18029
18239
  }
18030
18240
  }
18031
18241
  async clearChunkColumn(x, z) {
18032
18242
  this.biomes.delete(`${x}_${z}`);
18243
+ this.markBiomesChanged(x, z, void 0, false);
18033
18244
  if (this.savedLighting) {
18034
18245
  for (const y of this.light.get(`${x}_${z}`)?.keys() ?? []) this.markSectionChanged({ x, y, z });
18035
18246
  }
@@ -18040,6 +18251,7 @@ var MineRenderWorld = class {
18040
18251
  for (const [key, section] of previous) {
18041
18252
  this._chunks.delete(key);
18042
18253
  this.pendingCulling.delete(section);
18254
+ this.pendingBiomes.delete(section);
18043
18255
  await section.clear(false);
18044
18256
  this.markSectionChanged(section);
18045
18257
  }
@@ -18074,6 +18286,8 @@ var MineRenderWorld = class {
18074
18286
  const chunks = [...this._chunks.values()];
18075
18287
  this._chunks.clear();
18076
18288
  this.biomes.clear();
18289
+ this.biomeBlend?.clear();
18290
+ this.pendingBiomes.clear();
18077
18291
  this.light.clear();
18078
18292
  this.lightValidity.clear();
18079
18293
  this.highestSkySection.clear();
@@ -18192,8 +18406,14 @@ var MineRenderWorld = class {
18192
18406
  column?.delete(y);
18193
18407
  if (!column?.size) this.biomes.delete(columnKey);
18194
18408
  }
18195
- await chunk.refreshBiomes();
18196
- await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
18409
+ if (this.biomeBlend) {
18410
+ const queued = this.markBiomesChanged(x, z, y);
18411
+ const failures = await this.refreshBiomes();
18412
+ for (const chunk2 of queued) if (failures.has(chunk2)) throw failures.get(chunk2);
18413
+ } else {
18414
+ await chunk.refreshBiomes();
18415
+ await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
18416
+ }
18197
18417
  },
18198
18418
  this.savedLighting ? (x2, y2, z2) => this.lightLevelAt(x2, y2, z2) : void 0,
18199
18419
  async () => {
@@ -18210,7 +18430,8 @@ var MineRenderWorld = class {
18210
18430
  await this.updateCulling([]);
18211
18431
  }
18212
18432
  },
18213
- this.ambientLight
18433
+ this.ambientLight,
18434
+ () => this.biomeBlend
18214
18435
  );
18215
18436
  this._chunks.set(key, chunk);
18216
18437
  }
@@ -18329,9 +18550,10 @@ var LIGHT_SAMPLE_OFFSETS = [0, 1, -1, 324, -324, 18, -18];
18329
18550
 
18330
18551
  // src/world/SectionGeometry.ts
18331
18552
  function buildSectionGeometry(input) {
18332
- const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion } = input;
18553
+ const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion, biomeTints } = input;
18333
18554
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
18334
18555
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
18556
+ if (biomeTints && biomeTints.length !== count * 3) throw new RangeError("Section biome tints must contain RGB for each placement");
18335
18557
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
18336
18558
  if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
18337
18559
  if (light && (light.length !== 5832 || light.some((level) => level > 15 && level !== 255))) {
@@ -18346,7 +18568,7 @@ function buildSectionGeometry(input) {
18346
18568
  const cornerBrightness2 = ambientOcclusion ? new Float32Array(4) : void 0;
18347
18569
  for (const template of templateData) {
18348
18570
  const { quads } = template;
18349
- if (template.positions.length !== quads * 12 || template.normals.length !== quads * 12 || template.uvs.length !== quads * 8 || template.uvBounds.length !== quads * 16 || template.colors.length !== quads * 12 || template.indices.length !== quads * 6 || template.cullFaces.length !== quads) {
18571
+ if (template.positions.length !== quads * 12 || template.normals.length !== quads * 12 || template.uvs.length !== quads * 8 || template.uvBounds.length !== quads * 16 || template.colors.length !== quads * 12 || template.indices.length !== quads * 6 || template.cullFaces.length !== quads || template.biomeTint && template.biomeTint.length !== quads) {
18350
18572
  throw new RangeError("Section template arrays do not match its quad count");
18351
18573
  }
18352
18574
  }
@@ -18456,6 +18678,7 @@ function buildSectionGeometry(input) {
18456
18678
  blockIndices[indexOffset / 6] = indices[entry];
18457
18679
  if (ambientMask) quadAmbientOcclusion(template.positions, template.normals, quad, ambientMask, cornerBrightness2);
18458
18680
  const brightness = light ? lightBrightness(light[lightIndex + LIGHT_SAMPLE_OFFSETS[quadLightIndex(template.positions, template.normals, quad)]], ambientLight) : 1;
18681
+ const tint = biomeTints && template.biomeTint?.[quad] && !Number.isNaN(biomeTints[entry * 3]) ? entry * 3 : void 0;
18459
18682
  const start = vertex;
18460
18683
  for (let corner = 0; corner < 4; corner++, vertex++) {
18461
18684
  const source = quad * 4 + corner;
@@ -18472,9 +18695,9 @@ function buildSectionGeometry(input) {
18472
18695
  uvBounds[vertex * 4 + 2] = mapU(template.uvBounds[source * 4 + 2]);
18473
18696
  uvBounds[vertex * 4 + 3] = mapV(template.uvBounds[source * 4 + 3]);
18474
18697
  const factor = brightness * (ambientMask ? cornerBrightness2[corner] : 1);
18475
- colors[vertex * 3] = template.colors[source * 3] * factor;
18476
- colors[vertex * 3 + 1] = template.colors[source * 3 + 1] * factor;
18477
- colors[vertex * 3 + 2] = template.colors[source * 3 + 2] * factor;
18698
+ colors[vertex * 3] = (tint === void 0 ? template.colors[source * 3] : biomeTints[tint]) * factor;
18699
+ colors[vertex * 3 + 1] = (tint === void 0 ? template.colors[source * 3 + 1] : biomeTints[tint + 1]) * factor;
18700
+ colors[vertex * 3 + 2] = (tint === void 0 ? template.colors[source * 3 + 2] : biomeTints[tint + 2]) * factor;
18478
18701
  }
18479
18702
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
18480
18703
  }
@@ -18595,7 +18818,11 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
18595
18818
  ...lighting
18596
18819
  };
18597
18820
  for (let i = 0; i < entries.length; i++) {
18598
- const { index, template, cullMask } = entries[i];
18821
+ const { index, template, cullMask, biomeTint } = entries[i];
18822
+ if (biomeTint) {
18823
+ input.biomeTints ?? (input.biomeTints = new Float32Array(entries.length * 3).fill(NaN));
18824
+ input.biomeTints.set(biomeTint, i * 3);
18825
+ }
18599
18826
  let id = templateIds.get(template);
18600
18827
  if (id === void 0) {
18601
18828
  let atlas = atlasIds.get(template.atlas);
@@ -18622,6 +18849,7 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
18622
18849
  colors: new Float32Array(quads * 12),
18623
18850
  indices: new Uint16Array(quads * 6),
18624
18851
  cullFaces: new Uint8Array(template.cullFaces),
18852
+ biomeTint: template.biomeTint ? new Uint8Array(template.biomeTint) : void 0,
18625
18853
  atlas: 0,
18626
18854
  layer: template.layer,
18627
18855
  ambientOcclusion: template.ambientOcclusion
@@ -18766,7 +18994,7 @@ init_CubeFace();
18766
18994
  init_InstanceReference();
18767
18995
  var WATER_TINT2 = new Color13(4159204).toArray();
18768
18996
  var Chunk = class _Chunk {
18769
- constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged, lightAt, onLightCleared, ambientLight = 0) {
18997
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged, lightAt, onLightCleared, ambientLight = 0, biomeBlend) {
18770
18998
  this.onBlocksChanged = onBlocksChanged;
18771
18999
  this.sectionModels = sectionModels;
18772
19000
  this.neighborCell = neighborCell;
@@ -18775,6 +19003,7 @@ var Chunk = class _Chunk {
18775
19003
  this.lightAt = lightAt;
18776
19004
  this.onLightCleared = onLightCleared;
18777
19005
  this.ambientLight = ambientLight;
19006
+ this.biomeBlend = biomeBlend;
18778
19007
  this.data = new ChunkData();
18779
19008
  this.renderedBlocks = /* @__PURE__ */ new Map();
18780
19009
  this.objectBlocks = /* @__PURE__ */ new Set();
@@ -18784,6 +19013,7 @@ var Chunk = class _Chunk {
18784
19013
  this.blockPosition = new Vector320();
18785
19014
  this.meshDirty = false;
18786
19015
  this.meshGeneration = 0;
19016
+ this.blockChanges = /* @__PURE__ */ new Set();
18787
19017
  this.scene = scene;
18788
19018
  this.x = x;
18789
19019
  this.y = y;
@@ -18882,10 +19112,20 @@ var Chunk = class _Chunk {
18882
19112
  return this.refreshBiomes();
18883
19113
  }
18884
19114
  /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
18885
- async refreshBiomes() {
19115
+ refreshBiomes(columns) {
19116
+ return this.changeBlocks(() => this.refreshBiomeBlocks(columns), true);
19117
+ }
19118
+ async refreshBiomeBlocks(columns) {
19119
+ const blend = columns && this.biomeBlend?.();
19120
+ const ownColumn = columns?.some((column) => column.x === this.x && column.z === this.z);
18886
19121
  const affected = /* @__PURE__ */ new Map();
18887
19122
  const blocks = [];
18888
19123
  for (const index of this.renderedBlocks.keys()) {
19124
+ if (blend) {
19125
+ if (this.objectBlocks.has(index) && !ownColumn) continue;
19126
+ const [x, , z] = this.positionAt(index), radius = blend.radius;
19127
+ if (!columns.some((column) => x >= column.x * 16 - radius && x <= column.x * 16 + 15 + radius && z >= column.z * 16 - radius && z <= column.z * 16 + 15 + radius)) continue;
19128
+ }
18889
19129
  const block = this.data.get(index);
18890
19130
  const stateKey = ChunkData.paletteKey(block);
18891
19131
  let matches = affected.get(stateKey);
@@ -18900,10 +19140,32 @@ var Chunk = class _Chunk {
18900
19140
  }
18901
19141
  if (await matches) blocks.push({ index, block });
18902
19142
  }
19143
+ if (blend) {
19144
+ const groups = /* @__PURE__ */ new Map();
19145
+ for (const entry of blocks) {
19146
+ const key = ChunkData.paletteKey(entry.block);
19147
+ let group = groups.get(key);
19148
+ if (!group) groups.set(key, group = []);
19149
+ group.push(entry);
19150
+ }
19151
+ blocks.length = 0;
19152
+ const biomes = /* @__PURE__ */ new Map();
19153
+ for (const group of groups.values()) {
19154
+ const { contexts, fluid } = await this.resolveState(group[0].block, group.map((entry) => entry.index), biomes);
19155
+ for (const entry of group) {
19156
+ const context = contexts.get(entry.index);
19157
+ const current = this.sectionBlocks.get(entry.index)?.[0]?.biomeTint;
19158
+ const tint = context?.tint;
19159
+ const tintChanged = !!current !== !!tint || tint?.some((value, channel) => value !== current?.[channel]);
19160
+ const waterChanged = fluid?.kind === "water" && (context?.waterTint ?? WATER_TINT2).some((value, channel) => Math.fround(value) !== (this.waterTints?.[entry.index * 3 + channel] ?? Math.fround(WATER_TINT2[channel])));
19161
+ if (this.objectBlocks.has(entry.index) || tintChanged || waterChanged) blocks.push(entry);
19162
+ }
19163
+ }
19164
+ }
18903
19165
  if (!blocks.length) return;
18904
19166
  const hidden = new Set(this.hiddenBlocks);
18905
19167
  try {
18906
- await this.placeBlocks(blocks);
19168
+ await this.replaceBlocks(blocks, true);
18907
19169
  } finally {
18908
19170
  for (const index of hidden) {
18909
19171
  this.hiddenBlocks.add(index);
@@ -18943,6 +19205,20 @@ var Chunk = class _Chunk {
18943
19205
  }
18944
19206
  /** Places cells in input order and returns changed world positions without refreshing neighbors. */
18945
19207
  async placeBlocks(blocks) {
19208
+ return this.changeBlocks(() => this.replaceBlocks(blocks));
19209
+ }
19210
+ changeBlocks(change, refreshBiomes = false) {
19211
+ const before = refreshBiomes ? Promise.allSettled(this.blockChanges) : this.biomeChange;
19212
+ const result = before ? before.catch(() => void 0).then(change) : change();
19213
+ const pending = result.finally(() => {
19214
+ this.blockChanges.delete(pending);
19215
+ if (this.biomeChange === pending) this.biomeChange = void 0;
19216
+ });
19217
+ this.blockChanges.add(pending);
19218
+ if (refreshBiomes) this.biomeChange = pending;
19219
+ return pending;
19220
+ }
19221
+ async replaceBlocks(blocks, preserveDataOnFailure = false) {
18946
19222
  const placements = blocks.map(({ index, block }) => ({
18947
19223
  index,
18948
19224
  key: block && !ChunkData.isAir(block) ? ChunkData.paletteKey(block) : void 0,
@@ -18962,6 +19238,10 @@ var Chunk = class _Chunk {
18962
19238
  const biomes = /* @__PURE__ */ new Map();
18963
19239
  const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
18964
19240
  const resolutions = new Map([...states.keys()].map((key, i) => [key, settled[i]]));
19241
+ if (preserveDataOnFailure) {
19242
+ const rejected = settled.find((result) => result.status === "rejected");
19243
+ if (rejected?.status === "rejected") throw rejected.reason;
19244
+ }
18965
19245
  const positions = [];
18966
19246
  let failed = false, failure;
18967
19247
  for (const { index, key, block } of placements) {
@@ -18999,7 +19279,7 @@ var Chunk = class _Chunk {
18999
19279
  }
19000
19280
  const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
19001
19281
  if (templates) {
19002
- this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
19282
+ this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0, biomeTint: context?.tint })));
19003
19283
  this.meshDirty = true;
19004
19284
  } else {
19005
19285
  object4 = await this.scene.addBlock(blockState3, {
@@ -19018,7 +19298,11 @@ var Chunk = class _Chunk {
19018
19298
  }, object: object4 });
19019
19299
  if (object4) this.objectBlocks.add(index);
19020
19300
  } catch (error) {
19021
- this.data.assign(index, 0);
19301
+ if (preserveDataOnFailure && readBlock) {
19302
+ this.renderedBlocks.set(index, { get block() {
19303
+ return readBlock();
19304
+ }, object: void 0 });
19305
+ } else this.data.assign(index, 0);
19022
19306
  this.fluidCells[index] = 0;
19023
19307
  object4?.removeFromScene();
19024
19308
  if (!failed) failure = error;
@@ -19029,7 +19313,10 @@ var Chunk = class _Chunk {
19029
19313
  return positions;
19030
19314
  }
19031
19315
  /** Replaces every cell from parsed section data. The caller refreshes neighbor culling. */
19032
- async placeSection(data) {
19316
+ placeSection(data) {
19317
+ return this.changeBlocks(() => this.replaceSection(data));
19318
+ }
19319
+ async replaceSection(data) {
19033
19320
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
19034
19321
  this.sectionBlocks.clear();
19035
19322
  this.fluidCells.fill(0);
@@ -19076,7 +19363,7 @@ var Chunk = class _Chunk {
19076
19363
  ];
19077
19364
  const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
19078
19365
  if (templates) {
19079
- this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
19366
+ this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0, biomeTint: context?.tint })));
19080
19367
  this.meshDirty = true;
19081
19368
  } else {
19082
19369
  perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
@@ -19160,12 +19447,35 @@ var Chunk = class _Chunk {
19160
19447
  }
19161
19448
  async resolveState(stored, indices, biomes) {
19162
19449
  const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
19163
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
19450
+ let perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
19164
19451
  const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
19165
19452
  const hasBiomes = this.getBiomeAt(this.x * 16, this.y * 16, this.z * 16) !== void 0 || this.biomeAt?.(this.x * 16, this.y * 16 - 1, this.z * 16) !== void 0;
19166
19453
  const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
19167
19454
  const prepare = (color2) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color2);
19168
19455
  const contexts = /* @__PURE__ */ new Map();
19456
+ const blend = this.biomeBlend?.();
19457
+ if (blend && !perBlock && blockState3 && (source || fluid?.kind === "water")) {
19458
+ const prepared2 = fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties, void 0, true);
19459
+ if (!prepared2 && fluid?.renderModel !== false) {
19460
+ perBlock = true;
19461
+ } else {
19462
+ await Promise.all(indices.map(async (index) => {
19463
+ const position = this.biomePosition(index, blockState3, stored);
19464
+ const biomeColor = (kind) => blend.getColor(kind, blockState3.key, position);
19465
+ const context = {};
19466
+ if (source && prepared2?.biomeTint) {
19467
+ const color2 = await biomeColor(source);
19468
+ if (color2 !== void 0) context.tint = new Color13(color2).toArray();
19469
+ }
19470
+ if (fluid?.kind === "water") {
19471
+ const color2 = await biomeColor("water");
19472
+ if (color2 !== void 0 && color2 !== 4159204) context.waterTint = new Color13(color2).toArray();
19473
+ }
19474
+ contexts.set(index, context);
19475
+ }));
19476
+ return { blockState: blockState3, perBlock, fluid, prepared: prepared2, contexts, stored };
19477
+ }
19478
+ }
19169
19479
  const groups = /* @__PURE__ */ new Map();
19170
19480
  if (blockState3 && (source || fluid?.kind === "water")) {
19171
19481
  for (const index of indices) {
@@ -19213,7 +19523,11 @@ var Chunk = class _Chunk {
19213
19523
  this.waterTints.set(tint ?? WATER_TINT2, index * 3);
19214
19524
  }
19215
19525
  /** Changes visibility at world block coordinates while preserving the block's data. */
19216
- async setBlockVisibleAt(pos, visible) {
19526
+ setBlockVisibleAt(pos, visible) {
19527
+ const position = pos.clone();
19528
+ return this.changeBlocks(() => this.setBlockVisibility(position, visible));
19529
+ }
19530
+ async setBlockVisibility(pos, visible) {
19217
19531
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
19218
19532
  const block = this.renderedBlocks.get(index);
19219
19533
  if (!block || visible === !this.hiddenBlocks.has(index)) {
@@ -19383,6 +19697,7 @@ var Chunk = class _Chunk {
19383
19697
  }
19384
19698
  /** Removes stored blocks and render objects. Set `notify` to false to skip the owning world's update. */
19385
19699
  async clear(notify = true) {
19700
+ if (this.blockChanges.size) await Promise.allSettled(this.blockChanges);
19386
19701
  const positions = notify && this.onBlocksChanged ? [...this.renderedBlocks.keys()].map((index) => this.chunkPosToWorldPos(
19387
19702
  new Vector320(index % 16, Math.floor(index / 256), Math.floor(index / 16) % 16)
19388
19703
  )) : [];