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.
@@ -251,6 +251,13 @@ var Caching;
251
251
  var init_Caching = __esm({
252
252
  "src/cache/Caching.ts"() {
253
253
  Caching = class _Caching {
254
+ static {
255
+ this._generation = 0;
256
+ }
257
+ /** Changes when shared assets are cleared, so derived caches can discard their old values. */
258
+ static get generation() {
259
+ return this._generation;
260
+ }
254
261
  static {
255
262
  this.rawImageCache = Caches.builder().expireAfterWrite(Time.minutes(10)).expireAfterAccess(Time.minutes(5)).expirationInterval(Time.seconds(5)).buildAsync();
256
263
  }
@@ -361,6 +368,7 @@ var init_Caching = __esm({
361
368
  }
362
369
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
363
370
  static clear() {
371
+ this._generation++;
364
372
  for (const cache of this.all) {
365
373
  cache.invalidateAll();
366
374
  }
@@ -372,6 +380,7 @@ var init_Caching = __esm({
372
380
  * Use {@link shutdown} to also stop requests and the shared Ticker.
373
381
  */
374
382
  static end() {
383
+ this._generation++;
375
384
  for (const cache of this.all) {
376
385
  cache.invalidateAll();
377
386
  cache.end();
@@ -17381,18 +17390,23 @@ var SectionModels = class {
17381
17390
  return this.template(variant, choice, tints);
17382
17391
  }));
17383
17392
  }
17384
- /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
17385
- async prepareState(blockState3, properties = {}, biomeColor) {
17393
+ /** Resolves shared templates; dynamic biome tints are supplied on section placements instead of cached geometry. */
17394
+ async prepareState(blockState3, properties = {}, biomeColor, dynamicBiomeTint = false) {
17386
17395
  const prepared = await this.state(blockState3, properties);
17387
17396
  if (!prepared) return void 0;
17388
17397
  let color2;
17389
17398
  const resolveColor = typeof biomeColor === "function" ? () => color2 ??= biomeColor() : biomeColor;
17390
17399
  const templates = /* @__PURE__ */ new Map();
17400
+ const source = dynamicBiomeTint && await BlockTints.biomeSource(blockState3.key);
17391
17401
  await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
17392
17402
  const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
17393
- templates.set(variant, await this.template(variant, choice, tints));
17403
+ const biomeTints = source ? await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, 16777215) : void 0;
17404
+ templates.set(variant, await this.template(variant, choice, tints, biomeTints));
17394
17405
  }));
17395
- return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
17406
+ return {
17407
+ pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))),
17408
+ biomeTint: source ? [...templates.values()].some((template) => template.biomeTint?.some((value) => value === 1)) || void 0 : void 0
17409
+ };
17396
17410
  }
17397
17411
  async state(blockState3, properties) {
17398
17412
  let states = this.states.get(blockState3);
@@ -17435,10 +17449,10 @@ var SectionModels = class {
17435
17449
  }
17436
17450
  return { state, groups, choices: prepared };
17437
17451
  }
17438
- async template(variant, { model, atlas, rotation }, tints) {
17452
+ async template(variant, { model, atlas, rotation }, tints, biomeTints) {
17439
17453
  let models = this.models.get(model);
17440
17454
  if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17441
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17455
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints, biomeTints]);
17442
17456
  return cached(models, key, async () => {
17443
17457
  const object4 = new SectionModel(model, {
17444
17458
  instanceMeshes: false,
@@ -17452,11 +17466,13 @@ var SectionModels = class {
17452
17466
  object4.clear();
17453
17467
  const keep = [];
17454
17468
  const cullFaces = [];
17469
+ const biomeTint = biomeTints ? [] : void 0;
17455
17470
  for (const element of atlas.model.elements) {
17456
17471
  for (const face of CUBE_FACES) {
17457
17472
  const data = element.faces[face];
17458
17473
  keep.push(data !== void 0);
17459
17474
  if (!data) continue;
17475
+ biomeTint?.push(Number(data.tintindex !== void 0 && biomeTints?.[data.tintindex] !== void 0));
17460
17476
  const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17461
17477
  if (local < 0) {
17462
17478
  cullFaces.push(0);
@@ -17472,6 +17488,7 @@ var SectionModels = class {
17472
17488
  geometry,
17473
17489
  atlas,
17474
17490
  cullFaces: new Uint8Array(cullFaces),
17491
+ biomeTint: biomeTint?.some(Boolean) ? new Uint8Array(biomeTint) : void 0,
17475
17492
  occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17476
17493
  layer: atlas.hasTranslucency ? 1 : 0,
17477
17494
  ambientOcclusion: model.ambientocclusion !== false
@@ -17701,6 +17718,132 @@ var WorldEntities = class {
17701
17718
  // src/world/MineRenderWorld.ts
17702
17719
  init_FluidQuads();
17703
17720
 
17721
+ // src/world/_biomes/BiomeBlend.ts
17722
+ init_AssetKey();
17723
+ init_AssetLoader();
17724
+ init_Caching();
17725
+ var MAX_TILES = 256;
17726
+ function validateBiomeBlendRadius(radius) {
17727
+ if (!Number.isInteger(radius) || radius < 0 || radius > 7) {
17728
+ throw new RangeError("biomeBlendRadius must be an integer from 0 to 7");
17729
+ }
17730
+ }
17731
+ var BiomeBlend = class {
17732
+ constructor(biomeAt, radius) {
17733
+ this.biomeAt = biomeAt;
17734
+ this.tiles = /* @__PURE__ */ new Map();
17735
+ this.generation = Caching.generation;
17736
+ validateBiomeBlendRadius(radius);
17737
+ this._radius = radius;
17738
+ }
17739
+ get radius() {
17740
+ return this._radius;
17741
+ }
17742
+ clear() {
17743
+ this.tiles.clear();
17744
+ }
17745
+ invalidateColumn(x, z) {
17746
+ for (const [key, tile] of this.tiles) {
17747
+ if (Math.abs(tile.x - x) <= 1 && Math.abs(tile.z - z) <= 1) this.tiles.delete(key);
17748
+ }
17749
+ }
17750
+ async getColor(kind, origin, position) {
17751
+ const [x, y, z] = position;
17752
+ const biome = this.biomeAt(x, y, z);
17753
+ if (biome === void 0) return void 0;
17754
+ if (this.generation !== Caching.generation) {
17755
+ this.clear();
17756
+ this.generation = Caching.generation;
17757
+ }
17758
+ const tileX = Math.floor(x / 16), tileZ = Math.floor(z / 16), quartY = Math.floor(y / 4);
17759
+ const root = origin?.root ?? AssetLoader.ROOT;
17760
+ const key = JSON.stringify([root, AssetLoader.persistentScope, kind, this._radius, tileX, quartY, tileZ]);
17761
+ let tile = this.tiles.get(key);
17762
+ if (tile) {
17763
+ this.tiles.delete(key);
17764
+ this.tiles.set(key, tile);
17765
+ } else {
17766
+ const snapshot = new AssetKey("minecraft", "", void 0, void 0, "assets", ".json", root);
17767
+ const colors = this.buildTile(kind, snapshot, tileX, quartY, tileZ, this._radius).then((result2) => {
17768
+ if (!result2.complete && this.tiles.get(key)?.colors === colors) this.tiles.delete(key);
17769
+ return result2;
17770
+ }, (error) => {
17771
+ if (this.tiles.get(key)?.colors === colors) this.tiles.delete(key);
17772
+ throw error;
17773
+ });
17774
+ tile = { x: tileX, z: tileZ, colors };
17775
+ this.tiles.set(key, tile);
17776
+ if (this.tiles.size > MAX_TILES) this.tiles.delete(this.tiles.keys().next().value);
17777
+ }
17778
+ const result = await tile.colors;
17779
+ if (result.failures.has(biome)) throw result.failures.get(biome);
17780
+ const color2 = result.colors[(z - tileZ * 16) * 16 + x - tileX * 16];
17781
+ return color2 === -1 ? void 0 : color2;
17782
+ }
17783
+ async buildTile(kind, origin, tileX, quartY, tileZ, radius) {
17784
+ const size = 16 + radius * 2, stride = size + 1;
17785
+ const startX = tileX * 16 - radius, startZ = tileZ * 16 - radius;
17786
+ const quartX = Math.floor(startX / 4), quartZ = Math.floor(startZ / 4);
17787
+ const quartSize = Math.floor((startX + size - 1) / 4) - quartX + 1;
17788
+ const ids = new Array(quartSize * quartSize);
17789
+ const biomes = /* @__PURE__ */ new Map();
17790
+ for (let z = 0; z < quartSize; z++) {
17791
+ for (let x = 0; x < quartSize; x++) {
17792
+ const id = this.biomeAt((quartX + x) * 4, quartY * 4, (quartZ + z) * 4);
17793
+ ids[z * quartSize + x] = id;
17794
+ if (id !== void 0 && !biomes.has(id)) {
17795
+ biomes.set(id, (async () => {
17796
+ const swamp = kind === "grass" && (await Biomes.get(id, origin))?.effects.grass_color_modifier === "swamp";
17797
+ return { color: swamp ? void 0 : await Biomes.getColor(id, kind, origin), swamp };
17798
+ })());
17799
+ }
17800
+ }
17801
+ }
17802
+ const failures = /* @__PURE__ */ new Map();
17803
+ const loaded = new Map(await Promise.all([...biomes].map(async ([id, color2]) => {
17804
+ try {
17805
+ return [id, await color2];
17806
+ } catch (error) {
17807
+ failures.set(id, error);
17808
+ return [id, { color: void 0, swamp: false }];
17809
+ }
17810
+ })));
17811
+ const complete = [...loaded.values()].every((value) => value.swamp || value.color !== void 0);
17812
+ const samples = new Int32Array(size * size).fill(-1);
17813
+ const sums = Array.from({ length: 4 }, () => new Uint32Array(stride * stride));
17814
+ for (let z = 0; z < size; z++) {
17815
+ const row = [0, 0, 0, 0];
17816
+ for (let x = 0; x < size; x++) {
17817
+ const id = ids[(Math.floor((startZ + z) / 4) - quartZ) * quartSize + Math.floor((startX + x) / 4) - quartX];
17818
+ const value = id === void 0 ? void 0 : loaded.get(id);
17819
+ const color2 = value?.swamp ? swampGrassColor(startX + x, startZ + z) : value?.color;
17820
+ if (color2 !== void 0) {
17821
+ samples[z * size + x] = color2;
17822
+ row[0] += color2 >> 16 & 255;
17823
+ row[1] += color2 >> 8 & 255;
17824
+ row[2] += color2 & 255;
17825
+ row[3]++;
17826
+ }
17827
+ for (let channel = 0; channel < 4; channel++) {
17828
+ sums[channel][(z + 1) * stride + x + 1] = sums[channel][z * stride + x + 1] + row[channel];
17829
+ }
17830
+ }
17831
+ }
17832
+ const colors = new Int32Array(256).fill(-1), width = radius * 2 + 1;
17833
+ for (let z = 0; z < 16; z++) {
17834
+ for (let x = 0; x < 16; x++) {
17835
+ if (samples[(z + radius) * size + x + radius] === -1) continue;
17836
+ const topLeft = z * stride + x, topRight = topLeft + width;
17837
+ const bottomLeft = (z + width) * stride + x, bottomRight = bottomLeft + width;
17838
+ const sum = (values) => values[bottomRight] - values[bottomLeft] - values[topRight] + values[topLeft];
17839
+ const count = sum(sums[3]);
17840
+ 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);
17841
+ }
17842
+ }
17843
+ return { colors, complete, failures };
17844
+ }
17845
+ };
17846
+
17704
17847
  // src/world/WorldLight.ts
17705
17848
  function copySectionLight(light) {
17706
17849
  const copy = {};
@@ -17764,12 +17907,17 @@ var MineRenderWorld = class {
17764
17907
  this.lightValidity = /* @__PURE__ */ new Map();
17765
17908
  this.highestSkySection = /* @__PURE__ */ new Map();
17766
17909
  this.ambientOcclusionPending = false;
17910
+ this.pendingBiomes = /* @__PURE__ */ new Map();
17767
17911
  this.pendingCulling = /* @__PURE__ */ new Map();
17768
17912
  if (options.savedLighting && !options.sectionMeshing) throw new TypeError("savedLighting requires sectionMeshing");
17769
17913
  if (options.ambientOcclusion && !options.sectionMeshing) throw new TypeError("ambientOcclusion requires sectionMeshing");
17770
17914
  if (options.ambientLight !== void 0 && (!Number.isFinite(options.ambientLight) || options.ambientLight < 0 || options.ambientLight > 1)) {
17771
17915
  throw new RangeError("ambientLight must be a number from 0 to 1");
17772
17916
  }
17917
+ this._biomeBlendRadius = options.biomeBlendRadius ?? 0;
17918
+ validateBiomeBlendRadius(this._biomeBlendRadius);
17919
+ if (this._biomeBlendRadius > 0 && !options.sectionMeshing) throw new TypeError("biomeBlendRadius requires sectionMeshing");
17920
+ if (this._biomeBlendRadius) this.biomeBlend = new BiomeBlend((x, y, z) => this.getBiomeAt(x, y, z), this._biomeBlendRadius);
17773
17921
  this.scene = scene;
17774
17922
  this.savedLighting = options.savedLighting ?? false;
17775
17923
  this.ambientLight = options.ambientLight ?? 0;
@@ -17810,6 +17958,65 @@ var MineRenderWorld = class {
17810
17958
  await this.updateCulling([]);
17811
17959
  this.ambientOcclusionPending = false;
17812
17960
  }
17961
+ /** Horizontal biome-color sampling radius, from 0 to 7 blocks. Zero disables blending. */
17962
+ get biomeBlendRadius() {
17963
+ return this._biomeBlendRadius;
17964
+ }
17965
+ /** Refreshes loaded grass, foliage, and water colors without reloading their source chunks. */
17966
+ async setBiomeBlendRadius(radius) {
17967
+ validateBiomeBlendRadius(radius);
17968
+ if (radius > 0 && !this.sectionModels) throw new TypeError("biomeBlendRadius requires sectionMeshing");
17969
+ if (radius !== this._biomeBlendRadius) {
17970
+ this._biomeBlendRadius = radius;
17971
+ this.biomeBlend?.clear();
17972
+ this.biomeBlend = radius ? new BiomeBlend((x, y, z) => this.getBiomeAt(x, y, z), radius) : void 0;
17973
+ for (const chunk of this._chunks.values()) this.pendingBiomes.set(chunk, void 0);
17974
+ }
17975
+ const failures = await this.refreshBiomes();
17976
+ if (failures.size) throw failures.values().next().value;
17977
+ }
17978
+ markBiomesChanged(x, z, y, includeColumn = true) {
17979
+ const queued = [];
17980
+ if (!this.biomeBlend) return queued;
17981
+ this.biomeBlend.invalidateColumn(x, z);
17982
+ for (const chunk of this._chunks.values()) {
17983
+ 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;
17984
+ queued.push(chunk);
17985
+ if (!this.pendingBiomes.has(chunk)) this.pendingBiomes.set(chunk, []);
17986
+ const columns = this.pendingBiomes.get(chunk);
17987
+ if (columns && !columns.some((column) => column.x === x && column.z === z)) columns.push({ x, z });
17988
+ }
17989
+ return queued;
17990
+ }
17991
+ async refreshBiomes() {
17992
+ if (this.biomeRefreshing) await this.biomeRefreshing;
17993
+ if (!this.pendingBiomes.size) return /* @__PURE__ */ new Map();
17994
+ return this.biomeRefreshing ??= Promise.resolve().then(async () => {
17995
+ const failures = /* @__PURE__ */ new Map();
17996
+ const batch = [...this.pendingBiomes.keys()];
17997
+ let sliceStart = performance.now();
17998
+ try {
17999
+ for (const chunk of batch) {
18000
+ const columns = this.pendingBiomes.get(chunk);
18001
+ this.pendingBiomes.delete(chunk);
18002
+ if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) !== chunk) continue;
18003
+ try {
18004
+ await chunk.refreshBiomes(columns);
18005
+ } catch (error) {
18006
+ failures.set(chunk, error);
18007
+ if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
18008
+ }
18009
+ if (performance.now() - sliceStart > 8) {
18010
+ await yieldToEventLoop();
18011
+ sliceStart = performance.now();
18012
+ }
18013
+ }
18014
+ return failures;
18015
+ } finally {
18016
+ this.biomeRefreshing = void 0;
18017
+ }
18018
+ });
18019
+ }
17813
18020
  getBlockAt(posOrX, y, z) {
17814
18021
  if (typeof posOrX == "number") {
17815
18022
  return this.getBlockAt(new Vector319(posOrX, y, z));
@@ -18045,6 +18252,9 @@ var MineRenderWorld = class {
18045
18252
  }
18046
18253
  await placeEntities?.();
18047
18254
  } finally {
18255
+ this.markBiomesChanged(chunk.x, chunk.z, void 0, false);
18256
+ const failures = await this.refreshBiomes();
18257
+ if (failures.size) console.warn("Failed to refresh biome colors", failures.values().next().value);
18048
18258
  await this.updateCulling([...changes.values()]);
18049
18259
  }
18050
18260
  }
@@ -18061,11 +18271,14 @@ var MineRenderWorld = class {
18061
18271
  try {
18062
18272
  await this.clearChunkColumn(x, z);
18063
18273
  } finally {
18274
+ const failures = await this.refreshBiomes();
18275
+ if (failures.size) console.warn("Failed to refresh biome colors", failures.values().next().value);
18064
18276
  await this.updateCulling([]);
18065
18277
  }
18066
18278
  }
18067
18279
  async clearChunkColumn(x, z) {
18068
18280
  this.biomes.delete(`${x}_${z}`);
18281
+ this.markBiomesChanged(x, z, void 0, false);
18069
18282
  if (this.savedLighting) {
18070
18283
  for (const y of this.light.get(`${x}_${z}`)?.keys() ?? []) this.markSectionChanged({ x, y, z });
18071
18284
  }
@@ -18076,6 +18289,7 @@ var MineRenderWorld = class {
18076
18289
  for (const [key, section] of previous) {
18077
18290
  this._chunks.delete(key);
18078
18291
  this.pendingCulling.delete(section);
18292
+ this.pendingBiomes.delete(section);
18079
18293
  await section.clear(false);
18080
18294
  this.markSectionChanged(section);
18081
18295
  }
@@ -18110,6 +18324,8 @@ var MineRenderWorld = class {
18110
18324
  const chunks = [...this._chunks.values()];
18111
18325
  this._chunks.clear();
18112
18326
  this.biomes.clear();
18327
+ this.biomeBlend?.clear();
18328
+ this.pendingBiomes.clear();
18113
18329
  this.light.clear();
18114
18330
  this.lightValidity.clear();
18115
18331
  this.highestSkySection.clear();
@@ -18228,8 +18444,14 @@ var MineRenderWorld = class {
18228
18444
  column?.delete(y);
18229
18445
  if (!column?.size) this.biomes.delete(columnKey);
18230
18446
  }
18231
- await chunk.refreshBiomes();
18232
- await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
18447
+ if (this.biomeBlend) {
18448
+ const queued = this.markBiomesChanged(x, z, y);
18449
+ const failures = await this.refreshBiomes();
18450
+ for (const chunk2 of queued) if (failures.has(chunk2)) throw failures.get(chunk2);
18451
+ } else {
18452
+ await chunk.refreshBiomes();
18453
+ await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
18454
+ }
18233
18455
  },
18234
18456
  this.savedLighting ? (x2, y2, z2) => this.lightLevelAt(x2, y2, z2) : void 0,
18235
18457
  async () => {
@@ -18246,7 +18468,8 @@ var MineRenderWorld = class {
18246
18468
  await this.updateCulling([]);
18247
18469
  }
18248
18470
  },
18249
- this.ambientLight
18471
+ this.ambientLight,
18472
+ () => this.biomeBlend
18250
18473
  );
18251
18474
  this._chunks.set(key, chunk);
18252
18475
  }
@@ -18365,9 +18588,10 @@ var LIGHT_SAMPLE_OFFSETS = [0, 1, -1, 324, -324, 18, -18];
18365
18588
 
18366
18589
  // src/world/SectionGeometry.ts
18367
18590
  function buildSectionGeometry(input) {
18368
- const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion } = input;
18591
+ const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion, biomeTints } = input;
18369
18592
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
18370
18593
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
18594
+ if (biomeTints && biomeTints.length !== count * 3) throw new RangeError("Section biome tints must contain RGB for each placement");
18371
18595
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
18372
18596
  if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
18373
18597
  if (light && (light.length !== 5832 || light.some((level) => level > 15 && level !== 255))) {
@@ -18382,7 +18606,7 @@ function buildSectionGeometry(input) {
18382
18606
  const cornerBrightness2 = ambientOcclusion ? new Float32Array(4) : void 0;
18383
18607
  for (const template of templateData) {
18384
18608
  const { quads } = template;
18385
- 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) {
18609
+ 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) {
18386
18610
  throw new RangeError("Section template arrays do not match its quad count");
18387
18611
  }
18388
18612
  }
@@ -18492,6 +18716,7 @@ function buildSectionGeometry(input) {
18492
18716
  blockIndices[indexOffset / 6] = indices[entry];
18493
18717
  if (ambientMask) quadAmbientOcclusion(template.positions, template.normals, quad, ambientMask, cornerBrightness2);
18494
18718
  const brightness = light ? lightBrightness(light[lightIndex + LIGHT_SAMPLE_OFFSETS[quadLightIndex(template.positions, template.normals, quad)]], ambientLight) : 1;
18719
+ const tint = biomeTints && template.biomeTint?.[quad] && !Number.isNaN(biomeTints[entry * 3]) ? entry * 3 : void 0;
18495
18720
  const start = vertex;
18496
18721
  for (let corner = 0; corner < 4; corner++, vertex++) {
18497
18722
  const source = quad * 4 + corner;
@@ -18508,9 +18733,9 @@ function buildSectionGeometry(input) {
18508
18733
  uvBounds[vertex * 4 + 2] = mapU(template.uvBounds[source * 4 + 2]);
18509
18734
  uvBounds[vertex * 4 + 3] = mapV(template.uvBounds[source * 4 + 3]);
18510
18735
  const factor = brightness * (ambientMask ? cornerBrightness2[corner] : 1);
18511
- colors[vertex * 3] = template.colors[source * 3] * factor;
18512
- colors[vertex * 3 + 1] = template.colors[source * 3 + 1] * factor;
18513
- colors[vertex * 3 + 2] = template.colors[source * 3 + 2] * factor;
18736
+ colors[vertex * 3] = (tint === void 0 ? template.colors[source * 3] : biomeTints[tint]) * factor;
18737
+ colors[vertex * 3 + 1] = (tint === void 0 ? template.colors[source * 3 + 1] : biomeTints[tint + 1]) * factor;
18738
+ colors[vertex * 3 + 2] = (tint === void 0 ? template.colors[source * 3 + 2] : biomeTints[tint + 2]) * factor;
18514
18739
  }
18515
18740
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
18516
18741
  }
@@ -18632,7 +18857,11 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
18632
18857
  ...lighting
18633
18858
  };
18634
18859
  for (let i = 0; i < entries.length; i++) {
18635
- const { index, template, cullMask } = entries[i];
18860
+ const { index, template, cullMask, biomeTint } = entries[i];
18861
+ if (biomeTint) {
18862
+ input.biomeTints ??= new Float32Array(entries.length * 3).fill(NaN);
18863
+ input.biomeTints.set(biomeTint, i * 3);
18864
+ }
18636
18865
  let id = templateIds.get(template);
18637
18866
  if (id === void 0) {
18638
18867
  let atlas = atlasIds.get(template.atlas);
@@ -18659,6 +18888,7 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
18659
18888
  colors: new Float32Array(quads * 12),
18660
18889
  indices: new Uint16Array(quads * 6),
18661
18890
  cullFaces: new Uint8Array(template.cullFaces),
18891
+ biomeTint: template.biomeTint ? new Uint8Array(template.biomeTint) : void 0,
18662
18892
  atlas: 0,
18663
18893
  layer: template.layer,
18664
18894
  ambientOcclusion: template.ambientOcclusion
@@ -18803,7 +19033,7 @@ init_CubeFace();
18803
19033
  init_InstanceReference();
18804
19034
  var WATER_TINT2 = new Color13(4159204).toArray();
18805
19035
  var Chunk = class _Chunk {
18806
- constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged, lightAt, onLightCleared, ambientLight = 0) {
19036
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged, lightAt, onLightCleared, ambientLight = 0, biomeBlend) {
18807
19037
  this.onBlocksChanged = onBlocksChanged;
18808
19038
  this.sectionModels = sectionModels;
18809
19039
  this.neighborCell = neighborCell;
@@ -18812,6 +19042,7 @@ var Chunk = class _Chunk {
18812
19042
  this.lightAt = lightAt;
18813
19043
  this.onLightCleared = onLightCleared;
18814
19044
  this.ambientLight = ambientLight;
19045
+ this.biomeBlend = biomeBlend;
18815
19046
  this.data = new ChunkData();
18816
19047
  this.renderedBlocks = /* @__PURE__ */ new Map();
18817
19048
  this.objectBlocks = /* @__PURE__ */ new Set();
@@ -18821,6 +19052,7 @@ var Chunk = class _Chunk {
18821
19052
  this.blockPosition = new Vector320();
18822
19053
  this.meshDirty = false;
18823
19054
  this.meshGeneration = 0;
19055
+ this.blockChanges = /* @__PURE__ */ new Set();
18824
19056
  this.scene = scene;
18825
19057
  this.x = x;
18826
19058
  this.y = y;
@@ -18919,10 +19151,20 @@ var Chunk = class _Chunk {
18919
19151
  return this.refreshBiomes();
18920
19152
  }
18921
19153
  /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
18922
- async refreshBiomes() {
19154
+ refreshBiomes(columns) {
19155
+ return this.changeBlocks(() => this.refreshBiomeBlocks(columns), true);
19156
+ }
19157
+ async refreshBiomeBlocks(columns) {
19158
+ const blend = columns && this.biomeBlend?.();
19159
+ const ownColumn = columns?.some((column) => column.x === this.x && column.z === this.z);
18923
19160
  const affected = /* @__PURE__ */ new Map();
18924
19161
  const blocks = [];
18925
19162
  for (const index of this.renderedBlocks.keys()) {
19163
+ if (blend) {
19164
+ if (this.objectBlocks.has(index) && !ownColumn) continue;
19165
+ const [x, , z] = this.positionAt(index), radius = blend.radius;
19166
+ 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;
19167
+ }
18926
19168
  const block = this.data.get(index);
18927
19169
  const stateKey = ChunkData.paletteKey(block);
18928
19170
  let matches = affected.get(stateKey);
@@ -18937,10 +19179,32 @@ var Chunk = class _Chunk {
18937
19179
  }
18938
19180
  if (await matches) blocks.push({ index, block });
18939
19181
  }
19182
+ if (blend) {
19183
+ const groups = /* @__PURE__ */ new Map();
19184
+ for (const entry of blocks) {
19185
+ const key = ChunkData.paletteKey(entry.block);
19186
+ let group = groups.get(key);
19187
+ if (!group) groups.set(key, group = []);
19188
+ group.push(entry);
19189
+ }
19190
+ blocks.length = 0;
19191
+ const biomes = /* @__PURE__ */ new Map();
19192
+ for (const group of groups.values()) {
19193
+ const { contexts, fluid } = await this.resolveState(group[0].block, group.map((entry) => entry.index), biomes);
19194
+ for (const entry of group) {
19195
+ const context = contexts.get(entry.index);
19196
+ const current = this.sectionBlocks.get(entry.index)?.[0]?.biomeTint;
19197
+ const tint = context?.tint;
19198
+ const tintChanged = !!current !== !!tint || tint?.some((value, channel) => value !== current?.[channel]);
19199
+ 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])));
19200
+ if (this.objectBlocks.has(entry.index) || tintChanged || waterChanged) blocks.push(entry);
19201
+ }
19202
+ }
19203
+ }
18940
19204
  if (!blocks.length) return;
18941
19205
  const hidden = new Set(this.hiddenBlocks);
18942
19206
  try {
18943
- await this.placeBlocks(blocks);
19207
+ await this.replaceBlocks(blocks, true);
18944
19208
  } finally {
18945
19209
  for (const index of hidden) {
18946
19210
  this.hiddenBlocks.add(index);
@@ -18980,6 +19244,20 @@ var Chunk = class _Chunk {
18980
19244
  }
18981
19245
  /** Places cells in input order and returns changed world positions without refreshing neighbors. */
18982
19246
  async placeBlocks(blocks) {
19247
+ return this.changeBlocks(() => this.replaceBlocks(blocks));
19248
+ }
19249
+ changeBlocks(change, refreshBiomes = false) {
19250
+ const before = refreshBiomes ? Promise.allSettled(this.blockChanges) : this.biomeChange;
19251
+ const result = before ? before.catch(() => void 0).then(change) : change();
19252
+ const pending = result.finally(() => {
19253
+ this.blockChanges.delete(pending);
19254
+ if (this.biomeChange === pending) this.biomeChange = void 0;
19255
+ });
19256
+ this.blockChanges.add(pending);
19257
+ if (refreshBiomes) this.biomeChange = pending;
19258
+ return pending;
19259
+ }
19260
+ async replaceBlocks(blocks, preserveDataOnFailure = false) {
18983
19261
  const placements = blocks.map(({ index, block }) => ({
18984
19262
  index,
18985
19263
  key: block && !ChunkData.isAir(block) ? ChunkData.paletteKey(block) : void 0,
@@ -18999,6 +19277,10 @@ var Chunk = class _Chunk {
18999
19277
  const biomes = /* @__PURE__ */ new Map();
19000
19278
  const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
19001
19279
  const resolutions = new Map([...states.keys()].map((key, i) => [key, settled[i]]));
19280
+ if (preserveDataOnFailure) {
19281
+ const rejected = settled.find((result) => result.status === "rejected");
19282
+ if (rejected?.status === "rejected") throw rejected.reason;
19283
+ }
19002
19284
  const positions = [];
19003
19285
  let failed = false, failure;
19004
19286
  for (const { index, key, block } of placements) {
@@ -19036,7 +19318,7 @@ var Chunk = class _Chunk {
19036
19318
  }
19037
19319
  const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
19038
19320
  if (templates) {
19039
- this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
19321
+ this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0, biomeTint: context?.tint })));
19040
19322
  this.meshDirty = true;
19041
19323
  } else {
19042
19324
  object4 = await this.scene.addBlock(blockState3, {
@@ -19055,7 +19337,11 @@ var Chunk = class _Chunk {
19055
19337
  }, object: object4 });
19056
19338
  if (object4) this.objectBlocks.add(index);
19057
19339
  } catch (error) {
19058
- this.data.assign(index, 0);
19340
+ if (preserveDataOnFailure && readBlock) {
19341
+ this.renderedBlocks.set(index, { get block() {
19342
+ return readBlock();
19343
+ }, object: void 0 });
19344
+ } else this.data.assign(index, 0);
19059
19345
  this.fluidCells[index] = 0;
19060
19346
  object4?.removeFromScene();
19061
19347
  if (!failed) failure = error;
@@ -19066,7 +19352,10 @@ var Chunk = class _Chunk {
19066
19352
  return positions;
19067
19353
  }
19068
19354
  /** Replaces every cell from parsed section data. The caller refreshes neighbor culling. */
19069
- async placeSection(data) {
19355
+ placeSection(data) {
19356
+ return this.changeBlocks(() => this.replaceSection(data));
19357
+ }
19358
+ async replaceSection(data) {
19070
19359
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
19071
19360
  this.sectionBlocks.clear();
19072
19361
  this.fluidCells.fill(0);
@@ -19113,7 +19402,7 @@ var Chunk = class _Chunk {
19113
19402
  ];
19114
19403
  const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
19115
19404
  if (templates) {
19116
- this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
19405
+ this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0, biomeTint: context?.tint })));
19117
19406
  this.meshDirty = true;
19118
19407
  } else {
19119
19408
  perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
@@ -19197,12 +19486,35 @@ var Chunk = class _Chunk {
19197
19486
  }
19198
19487
  async resolveState(stored, indices, biomes) {
19199
19488
  const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
19200
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
19489
+ let perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
19201
19490
  const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
19202
19491
  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;
19203
19492
  const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
19204
19493
  const prepare = (color2) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color2);
19205
19494
  const contexts = /* @__PURE__ */ new Map();
19495
+ const blend = this.biomeBlend?.();
19496
+ if (blend && !perBlock && blockState3 && (source || fluid?.kind === "water")) {
19497
+ const prepared2 = fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties, void 0, true);
19498
+ if (!prepared2 && fluid?.renderModel !== false) {
19499
+ perBlock = true;
19500
+ } else {
19501
+ await Promise.all(indices.map(async (index) => {
19502
+ const position = this.biomePosition(index, blockState3, stored);
19503
+ const biomeColor = (kind) => blend.getColor(kind, blockState3.key, position);
19504
+ const context = {};
19505
+ if (source && prepared2?.biomeTint) {
19506
+ const color2 = await biomeColor(source);
19507
+ if (color2 !== void 0) context.tint = new Color13(color2).toArray();
19508
+ }
19509
+ if (fluid?.kind === "water") {
19510
+ const color2 = await biomeColor("water");
19511
+ if (color2 !== void 0 && color2 !== 4159204) context.waterTint = new Color13(color2).toArray();
19512
+ }
19513
+ contexts.set(index, context);
19514
+ }));
19515
+ return { blockState: blockState3, perBlock, fluid, prepared: prepared2, contexts, stored };
19516
+ }
19517
+ }
19206
19518
  const groups = /* @__PURE__ */ new Map();
19207
19519
  if (blockState3 && (source || fluid?.kind === "water")) {
19208
19520
  for (const index of indices) {
@@ -19250,7 +19562,11 @@ var Chunk = class _Chunk {
19250
19562
  this.waterTints.set(tint ?? WATER_TINT2, index * 3);
19251
19563
  }
19252
19564
  /** Changes visibility at world block coordinates while preserving the block's data. */
19253
- async setBlockVisibleAt(pos, visible) {
19565
+ setBlockVisibleAt(pos, visible) {
19566
+ const position = pos.clone();
19567
+ return this.changeBlocks(() => this.setBlockVisibility(position, visible));
19568
+ }
19569
+ async setBlockVisibility(pos, visible) {
19254
19570
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
19255
19571
  const block = this.renderedBlocks.get(index);
19256
19572
  if (!block || visible === !this.hiddenBlocks.has(index)) {
@@ -19420,6 +19736,7 @@ var Chunk = class _Chunk {
19420
19736
  }
19421
19737
  /** Removes stored blocks and render objects. Set `notify` to false to skip the owning world's update. */
19422
19738
  async clear(notify = true) {
19739
+ if (this.blockChanges.size) await Promise.allSettled(this.blockChanges);
19423
19740
  const positions = notify && this.onBlocksChanged ? [...this.renderedBlocks.keys()].map((index) => this.chunkPosToWorldPos(
19424
19741
  new Vector320(index % 16, Math.floor(index / 256), Math.floor(index / 16) % 16)
19425
19742
  )) : [];