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.
@@ -264,6 +264,10 @@ var init_Caching = __esm({
264
264
  import_loading_cache = require("@inventivetalent/loading-cache");
265
265
  import_time = require("@inventivetalent/time");
266
266
  _Caching = class _Caching {
267
+ /** Changes when shared assets are cleared, so derived caches can discard their old values. */
268
+ static get generation() {
269
+ return this._generation;
270
+ }
267
271
  static createAssetCache() {
268
272
  return import_loading_cache.Caches.builder().expireAfterWrite(import_time.Time.minutes(10)).expireAfterAccess(import_time.Time.minutes(5)).expirationInterval(import_time.Time.seconds(30)).buildAsync();
269
273
  }
@@ -299,6 +303,7 @@ var init_Caching = __esm({
299
303
  }
300
304
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
301
305
  static clear() {
306
+ this._generation++;
302
307
  for (const cache of this.all) {
303
308
  cache.invalidateAll();
304
309
  }
@@ -310,6 +315,7 @@ var init_Caching = __esm({
310
315
  * Use {@link shutdown} to also stop requests and the shared Ticker.
311
316
  */
312
317
  static end() {
318
+ this._generation++;
313
319
  for (const cache of this.all) {
314
320
  cache.invalidateAll();
315
321
  cache.end();
@@ -321,6 +327,7 @@ var init_Caching = __esm({
321
327
  };
322
328
  }
323
329
  };
330
+ _Caching._generation = 0;
324
331
  _Caching.rawImageCache = import_loading_cache.Caches.builder().expireAfterWrite(import_time.Time.minutes(10)).expireAfterAccess(import_time.Time.minutes(5)).expirationInterval(import_time.Time.seconds(5)).buildAsync();
325
332
  _Caching.imageDataCache = import_loading_cache.Caches.builder().expireAfterWrite(import_time.Time.minutes(10)).expireAfterAccess(import_time.Time.minutes(5)).expirationInterval(import_time.Time.seconds(10)).buildAsync();
326
333
  _Caching.canvasImageDataCache = import_loading_cache.Caches.builder().expireAfterWrite(import_time.Time.minutes(10)).expireAfterAccess(import_time.Time.minutes(5)).expirationInterval(import_time.Time.seconds(10)).buildAsync();
@@ -17509,18 +17516,23 @@ var SectionModels = class {
17509
17516
  return this.template(variant, choice, tints);
17510
17517
  }));
17511
17518
  }
17512
- /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
17513
- async prepareState(blockState3, properties = {}, biomeColor) {
17519
+ /** Resolves shared templates; dynamic biome tints are supplied on section placements instead of cached geometry. */
17520
+ async prepareState(blockState3, properties = {}, biomeColor, dynamicBiomeTint = false) {
17514
17521
  const prepared = await this.state(blockState3, properties);
17515
17522
  if (!prepared) return void 0;
17516
17523
  let color2;
17517
17524
  const resolveColor = typeof biomeColor === "function" ? () => color2 ?? (color2 = biomeColor()) : biomeColor;
17518
17525
  const templates = /* @__PURE__ */ new Map();
17526
+ const source = dynamicBiomeTint && await BlockTints.biomeSource(blockState3.key);
17519
17527
  await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
17520
17528
  const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
17521
- templates.set(variant, await this.template(variant, choice, tints));
17529
+ const biomeTints = source ? await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, 16777215) : void 0;
17530
+ templates.set(variant, await this.template(variant, choice, tints, biomeTints));
17522
17531
  }));
17523
- return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
17532
+ return {
17533
+ pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))),
17534
+ biomeTint: source ? [...templates.values()].some((template) => template.biomeTint?.some((value) => value === 1)) || void 0 : void 0
17535
+ };
17524
17536
  }
17525
17537
  async state(blockState3, properties) {
17526
17538
  let states = this.states.get(blockState3);
@@ -17563,10 +17575,10 @@ var SectionModels = class {
17563
17575
  }
17564
17576
  return { state, groups, choices: prepared };
17565
17577
  }
17566
- async template(variant, { model, atlas, rotation }, tints) {
17578
+ async template(variant, { model, atlas, rotation }, tints, biomeTints) {
17567
17579
  let models = this.models.get(model);
17568
17580
  if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17569
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17581
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints, biomeTints]);
17570
17582
  return cached(models, key, async () => {
17571
17583
  const object4 = new SectionModel(model, {
17572
17584
  instanceMeshes: false,
@@ -17580,11 +17592,13 @@ var SectionModels = class {
17580
17592
  object4.clear();
17581
17593
  const keep = [];
17582
17594
  const cullFaces = [];
17595
+ const biomeTint = biomeTints ? [] : void 0;
17583
17596
  for (const element of atlas.model.elements) {
17584
17597
  for (const face of CUBE_FACES) {
17585
17598
  const data = element.faces[face];
17586
17599
  keep.push(data !== void 0);
17587
17600
  if (!data) continue;
17601
+ biomeTint?.push(Number(data.tintindex !== void 0 && biomeTints?.[data.tintindex] !== void 0));
17588
17602
  const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17589
17603
  if (local < 0) {
17590
17604
  cullFaces.push(0);
@@ -17600,6 +17614,7 @@ var SectionModels = class {
17600
17614
  geometry,
17601
17615
  atlas,
17602
17616
  cullFaces: new Uint8Array(cullFaces),
17617
+ biomeTint: biomeTint?.some(Boolean) ? new Uint8Array(biomeTint) : void 0,
17603
17618
  occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17604
17619
  layer: atlas.hasTranslucency ? 1 : 0,
17605
17620
  ambientOcclusion: model.ambientocclusion !== false
@@ -17829,6 +17844,132 @@ var WorldEntities = class {
17829
17844
  // src/world/MineRenderWorld.ts
17830
17845
  init_FluidQuads();
17831
17846
 
17847
+ // src/world/_biomes/BiomeBlend.ts
17848
+ init_AssetKey();
17849
+ init_AssetLoader();
17850
+ init_Caching();
17851
+ var MAX_TILES = 256;
17852
+ function validateBiomeBlendRadius(radius) {
17853
+ if (!Number.isInteger(radius) || radius < 0 || radius > 7) {
17854
+ throw new RangeError("biomeBlendRadius must be an integer from 0 to 7");
17855
+ }
17856
+ }
17857
+ var BiomeBlend = class {
17858
+ constructor(biomeAt, radius) {
17859
+ this.biomeAt = biomeAt;
17860
+ this.tiles = /* @__PURE__ */ new Map();
17861
+ this.generation = Caching.generation;
17862
+ validateBiomeBlendRadius(radius);
17863
+ this._radius = radius;
17864
+ }
17865
+ get radius() {
17866
+ return this._radius;
17867
+ }
17868
+ clear() {
17869
+ this.tiles.clear();
17870
+ }
17871
+ invalidateColumn(x, z) {
17872
+ for (const [key, tile] of this.tiles) {
17873
+ if (Math.abs(tile.x - x) <= 1 && Math.abs(tile.z - z) <= 1) this.tiles.delete(key);
17874
+ }
17875
+ }
17876
+ async getColor(kind, origin, position) {
17877
+ const [x, y, z] = position;
17878
+ const biome = this.biomeAt(x, y, z);
17879
+ if (biome === void 0) return void 0;
17880
+ if (this.generation !== Caching.generation) {
17881
+ this.clear();
17882
+ this.generation = Caching.generation;
17883
+ }
17884
+ const tileX = Math.floor(x / 16), tileZ = Math.floor(z / 16), quartY = Math.floor(y / 4);
17885
+ const root = origin?.root ?? AssetLoader.ROOT;
17886
+ const key = JSON.stringify([root, AssetLoader.persistentScope, kind, this._radius, tileX, quartY, tileZ]);
17887
+ let tile = this.tiles.get(key);
17888
+ if (tile) {
17889
+ this.tiles.delete(key);
17890
+ this.tiles.set(key, tile);
17891
+ } else {
17892
+ const snapshot = new AssetKey("minecraft", "", void 0, void 0, "assets", ".json", root);
17893
+ const colors = this.buildTile(kind, snapshot, tileX, quartY, tileZ, this._radius).then((result2) => {
17894
+ if (!result2.complete && this.tiles.get(key)?.colors === colors) this.tiles.delete(key);
17895
+ return result2;
17896
+ }, (error) => {
17897
+ if (this.tiles.get(key)?.colors === colors) this.tiles.delete(key);
17898
+ throw error;
17899
+ });
17900
+ tile = { x: tileX, z: tileZ, colors };
17901
+ this.tiles.set(key, tile);
17902
+ if (this.tiles.size > MAX_TILES) this.tiles.delete(this.tiles.keys().next().value);
17903
+ }
17904
+ const result = await tile.colors;
17905
+ if (result.failures.has(biome)) throw result.failures.get(biome);
17906
+ const color2 = result.colors[(z - tileZ * 16) * 16 + x - tileX * 16];
17907
+ return color2 === -1 ? void 0 : color2;
17908
+ }
17909
+ async buildTile(kind, origin, tileX, quartY, tileZ, radius) {
17910
+ const size = 16 + radius * 2, stride = size + 1;
17911
+ const startX = tileX * 16 - radius, startZ = tileZ * 16 - radius;
17912
+ const quartX = Math.floor(startX / 4), quartZ = Math.floor(startZ / 4);
17913
+ const quartSize = Math.floor((startX + size - 1) / 4) - quartX + 1;
17914
+ const ids = new Array(quartSize * quartSize);
17915
+ const biomes = /* @__PURE__ */ new Map();
17916
+ for (let z = 0; z < quartSize; z++) {
17917
+ for (let x = 0; x < quartSize; x++) {
17918
+ const id = this.biomeAt((quartX + x) * 4, quartY * 4, (quartZ + z) * 4);
17919
+ ids[z * quartSize + x] = id;
17920
+ if (id !== void 0 && !biomes.has(id)) {
17921
+ biomes.set(id, (async () => {
17922
+ const swamp = kind === "grass" && (await Biomes.get(id, origin))?.effects.grass_color_modifier === "swamp";
17923
+ return { color: swamp ? void 0 : await Biomes.getColor(id, kind, origin), swamp };
17924
+ })());
17925
+ }
17926
+ }
17927
+ }
17928
+ const failures = /* @__PURE__ */ new Map();
17929
+ const loaded = new Map(await Promise.all([...biomes].map(async ([id, color2]) => {
17930
+ try {
17931
+ return [id, await color2];
17932
+ } catch (error) {
17933
+ failures.set(id, error);
17934
+ return [id, { color: void 0, swamp: false }];
17935
+ }
17936
+ })));
17937
+ const complete = [...loaded.values()].every((value) => value.swamp || value.color !== void 0);
17938
+ const samples = new Int32Array(size * size).fill(-1);
17939
+ const sums = Array.from({ length: 4 }, () => new Uint32Array(stride * stride));
17940
+ for (let z = 0; z < size; z++) {
17941
+ const row = [0, 0, 0, 0];
17942
+ for (let x = 0; x < size; x++) {
17943
+ const id = ids[(Math.floor((startZ + z) / 4) - quartZ) * quartSize + Math.floor((startX + x) / 4) - quartX];
17944
+ const value = id === void 0 ? void 0 : loaded.get(id);
17945
+ const color2 = value?.swamp ? swampGrassColor(startX + x, startZ + z) : value?.color;
17946
+ if (color2 !== void 0) {
17947
+ samples[z * size + x] = color2;
17948
+ row[0] += color2 >> 16 & 255;
17949
+ row[1] += color2 >> 8 & 255;
17950
+ row[2] += color2 & 255;
17951
+ row[3]++;
17952
+ }
17953
+ for (let channel = 0; channel < 4; channel++) {
17954
+ sums[channel][(z + 1) * stride + x + 1] = sums[channel][z * stride + x + 1] + row[channel];
17955
+ }
17956
+ }
17957
+ }
17958
+ const colors = new Int32Array(256).fill(-1), width = radius * 2 + 1;
17959
+ for (let z = 0; z < 16; z++) {
17960
+ for (let x = 0; x < 16; x++) {
17961
+ if (samples[(z + radius) * size + x + radius] === -1) continue;
17962
+ const topLeft = z * stride + x, topRight = topLeft + width;
17963
+ const bottomLeft = (z + width) * stride + x, bottomRight = bottomLeft + width;
17964
+ const sum = (values) => values[bottomRight] - values[bottomLeft] - values[topRight] + values[topLeft];
17965
+ const count = sum(sums[3]);
17966
+ 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);
17967
+ }
17968
+ }
17969
+ return { colors, complete, failures };
17970
+ }
17971
+ };
17972
+
17832
17973
  // src/world/WorldLight.ts
17833
17974
  function copySectionLight(light) {
17834
17975
  const copy = {};
@@ -17892,12 +18033,17 @@ var MineRenderWorld = class {
17892
18033
  this.lightValidity = /* @__PURE__ */ new Map();
17893
18034
  this.highestSkySection = /* @__PURE__ */ new Map();
17894
18035
  this.ambientOcclusionPending = false;
18036
+ this.pendingBiomes = /* @__PURE__ */ new Map();
17895
18037
  this.pendingCulling = /* @__PURE__ */ new Map();
17896
18038
  if (options.savedLighting && !options.sectionMeshing) throw new TypeError("savedLighting requires sectionMeshing");
17897
18039
  if (options.ambientOcclusion && !options.sectionMeshing) throw new TypeError("ambientOcclusion requires sectionMeshing");
17898
18040
  if (options.ambientLight !== void 0 && (!Number.isFinite(options.ambientLight) || options.ambientLight < 0 || options.ambientLight > 1)) {
17899
18041
  throw new RangeError("ambientLight must be a number from 0 to 1");
17900
18042
  }
18043
+ this._biomeBlendRadius = options.biomeBlendRadius ?? 0;
18044
+ validateBiomeBlendRadius(this._biomeBlendRadius);
18045
+ if (this._biomeBlendRadius > 0 && !options.sectionMeshing) throw new TypeError("biomeBlendRadius requires sectionMeshing");
18046
+ if (this._biomeBlendRadius) this.biomeBlend = new BiomeBlend((x, y, z) => this.getBiomeAt(x, y, z), this._biomeBlendRadius);
17901
18047
  this.scene = scene;
17902
18048
  this.savedLighting = options.savedLighting ?? false;
17903
18049
  this.ambientLight = options.ambientLight ?? 0;
@@ -17938,6 +18084,65 @@ var MineRenderWorld = class {
17938
18084
  await this.updateCulling([]);
17939
18085
  this.ambientOcclusionPending = false;
17940
18086
  }
18087
+ /** Horizontal biome-color sampling radius, from 0 to 7 blocks. Zero disables blending. */
18088
+ get biomeBlendRadius() {
18089
+ return this._biomeBlendRadius;
18090
+ }
18091
+ /** Refreshes loaded grass, foliage, and water colors without reloading their source chunks. */
18092
+ async setBiomeBlendRadius(radius) {
18093
+ validateBiomeBlendRadius(radius);
18094
+ if (radius > 0 && !this.sectionModels) throw new TypeError("biomeBlendRadius requires sectionMeshing");
18095
+ if (radius !== this._biomeBlendRadius) {
18096
+ this._biomeBlendRadius = radius;
18097
+ this.biomeBlend?.clear();
18098
+ this.biomeBlend = radius ? new BiomeBlend((x, y, z) => this.getBiomeAt(x, y, z), radius) : void 0;
18099
+ for (const chunk of this._chunks.values()) this.pendingBiomes.set(chunk, void 0);
18100
+ }
18101
+ const failures = await this.refreshBiomes();
18102
+ if (failures.size) throw failures.values().next().value;
18103
+ }
18104
+ markBiomesChanged(x, z, y, includeColumn = true) {
18105
+ const queued = [];
18106
+ if (!this.biomeBlend) return queued;
18107
+ this.biomeBlend.invalidateColumn(x, z);
18108
+ for (const chunk of this._chunks.values()) {
18109
+ 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;
18110
+ queued.push(chunk);
18111
+ if (!this.pendingBiomes.has(chunk)) this.pendingBiomes.set(chunk, []);
18112
+ const columns = this.pendingBiomes.get(chunk);
18113
+ if (columns && !columns.some((column) => column.x === x && column.z === z)) columns.push({ x, z });
18114
+ }
18115
+ return queued;
18116
+ }
18117
+ async refreshBiomes() {
18118
+ if (this.biomeRefreshing) await this.biomeRefreshing;
18119
+ if (!this.pendingBiomes.size) return /* @__PURE__ */ new Map();
18120
+ return this.biomeRefreshing ?? (this.biomeRefreshing = Promise.resolve().then(async () => {
18121
+ const failures = /* @__PURE__ */ new Map();
18122
+ const batch = [...this.pendingBiomes.keys()];
18123
+ let sliceStart = performance.now();
18124
+ try {
18125
+ for (const chunk of batch) {
18126
+ const columns = this.pendingBiomes.get(chunk);
18127
+ this.pendingBiomes.delete(chunk);
18128
+ if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) !== chunk) continue;
18129
+ try {
18130
+ await chunk.refreshBiomes(columns);
18131
+ } catch (error) {
18132
+ failures.set(chunk, error);
18133
+ if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
18134
+ }
18135
+ if (performance.now() - sliceStart > 8) {
18136
+ await yieldToEventLoop();
18137
+ sliceStart = performance.now();
18138
+ }
18139
+ }
18140
+ return failures;
18141
+ } finally {
18142
+ this.biomeRefreshing = void 0;
18143
+ }
18144
+ }));
18145
+ }
17941
18146
  getBlockAt(posOrX, y, z) {
17942
18147
  if (typeof posOrX == "number") {
17943
18148
  return this.getBlockAt(new import_three45.Vector3(posOrX, y, z));
@@ -18173,6 +18378,9 @@ var MineRenderWorld = class {
18173
18378
  }
18174
18379
  await placeEntities?.();
18175
18380
  } finally {
18381
+ this.markBiomesChanged(chunk.x, chunk.z, void 0, false);
18382
+ const failures = await this.refreshBiomes();
18383
+ if (failures.size) console.warn("Failed to refresh biome colors", failures.values().next().value);
18176
18384
  await this.updateCulling([...changes.values()]);
18177
18385
  }
18178
18386
  }
@@ -18189,11 +18397,14 @@ var MineRenderWorld = class {
18189
18397
  try {
18190
18398
  await this.clearChunkColumn(x, z);
18191
18399
  } finally {
18400
+ const failures = await this.refreshBiomes();
18401
+ if (failures.size) console.warn("Failed to refresh biome colors", failures.values().next().value);
18192
18402
  await this.updateCulling([]);
18193
18403
  }
18194
18404
  }
18195
18405
  async clearChunkColumn(x, z) {
18196
18406
  this.biomes.delete(`${x}_${z}`);
18407
+ this.markBiomesChanged(x, z, void 0, false);
18197
18408
  if (this.savedLighting) {
18198
18409
  for (const y of this.light.get(`${x}_${z}`)?.keys() ?? []) this.markSectionChanged({ x, y, z });
18199
18410
  }
@@ -18204,6 +18415,7 @@ var MineRenderWorld = class {
18204
18415
  for (const [key, section] of previous) {
18205
18416
  this._chunks.delete(key);
18206
18417
  this.pendingCulling.delete(section);
18418
+ this.pendingBiomes.delete(section);
18207
18419
  await section.clear(false);
18208
18420
  this.markSectionChanged(section);
18209
18421
  }
@@ -18238,6 +18450,8 @@ var MineRenderWorld = class {
18238
18450
  const chunks = [...this._chunks.values()];
18239
18451
  this._chunks.clear();
18240
18452
  this.biomes.clear();
18453
+ this.biomeBlend?.clear();
18454
+ this.pendingBiomes.clear();
18241
18455
  this.light.clear();
18242
18456
  this.lightValidity.clear();
18243
18457
  this.highestSkySection.clear();
@@ -18356,8 +18570,14 @@ var MineRenderWorld = class {
18356
18570
  column?.delete(y);
18357
18571
  if (!column?.size) this.biomes.delete(columnKey);
18358
18572
  }
18359
- await chunk.refreshBiomes();
18360
- await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
18573
+ if (this.biomeBlend) {
18574
+ const queued = this.markBiomesChanged(x, z, y);
18575
+ const failures = await this.refreshBiomes();
18576
+ for (const chunk2 of queued) if (failures.has(chunk2)) throw failures.get(chunk2);
18577
+ } else {
18578
+ await chunk.refreshBiomes();
18579
+ await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
18580
+ }
18361
18581
  },
18362
18582
  this.savedLighting ? (x2, y2, z2) => this.lightLevelAt(x2, y2, z2) : void 0,
18363
18583
  async () => {
@@ -18374,7 +18594,8 @@ var MineRenderWorld = class {
18374
18594
  await this.updateCulling([]);
18375
18595
  }
18376
18596
  },
18377
- this.ambientLight
18597
+ this.ambientLight,
18598
+ () => this.biomeBlend
18378
18599
  );
18379
18600
  this._chunks.set(key, chunk);
18380
18601
  }
@@ -18493,9 +18714,10 @@ var LIGHT_SAMPLE_OFFSETS = [0, 1, -1, 324, -324, 18, -18];
18493
18714
 
18494
18715
  // src/world/SectionGeometry.ts
18495
18716
  function buildSectionGeometry(input) {
18496
- const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion } = input;
18717
+ const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion, biomeTints } = input;
18497
18718
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
18498
18719
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
18720
+ if (biomeTints && biomeTints.length !== count * 3) throw new RangeError("Section biome tints must contain RGB for each placement");
18499
18721
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
18500
18722
  if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
18501
18723
  if (light && (light.length !== 5832 || light.some((level) => level > 15 && level !== 255))) {
@@ -18510,7 +18732,7 @@ function buildSectionGeometry(input) {
18510
18732
  const cornerBrightness2 = ambientOcclusion ? new Float32Array(4) : void 0;
18511
18733
  for (const template of templateData) {
18512
18734
  const { quads } = template;
18513
- 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) {
18735
+ 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) {
18514
18736
  throw new RangeError("Section template arrays do not match its quad count");
18515
18737
  }
18516
18738
  }
@@ -18620,6 +18842,7 @@ function buildSectionGeometry(input) {
18620
18842
  blockIndices[indexOffset / 6] = indices[entry];
18621
18843
  if (ambientMask) quadAmbientOcclusion(template.positions, template.normals, quad, ambientMask, cornerBrightness2);
18622
18844
  const brightness = light ? lightBrightness(light[lightIndex + LIGHT_SAMPLE_OFFSETS[quadLightIndex(template.positions, template.normals, quad)]], ambientLight) : 1;
18845
+ const tint = biomeTints && template.biomeTint?.[quad] && !Number.isNaN(biomeTints[entry * 3]) ? entry * 3 : void 0;
18623
18846
  const start = vertex;
18624
18847
  for (let corner = 0; corner < 4; corner++, vertex++) {
18625
18848
  const source = quad * 4 + corner;
@@ -18636,9 +18859,9 @@ function buildSectionGeometry(input) {
18636
18859
  uvBounds[vertex * 4 + 2] = mapU(template.uvBounds[source * 4 + 2]);
18637
18860
  uvBounds[vertex * 4 + 3] = mapV(template.uvBounds[source * 4 + 3]);
18638
18861
  const factor = brightness * (ambientMask ? cornerBrightness2[corner] : 1);
18639
- colors[vertex * 3] = template.colors[source * 3] * factor;
18640
- colors[vertex * 3 + 1] = template.colors[source * 3 + 1] * factor;
18641
- colors[vertex * 3 + 2] = template.colors[source * 3 + 2] * factor;
18862
+ colors[vertex * 3] = (tint === void 0 ? template.colors[source * 3] : biomeTints[tint]) * factor;
18863
+ colors[vertex * 3 + 1] = (tint === void 0 ? template.colors[source * 3 + 1] : biomeTints[tint + 1]) * factor;
18864
+ colors[vertex * 3 + 2] = (tint === void 0 ? template.colors[source * 3 + 2] : biomeTints[tint + 2]) * factor;
18642
18865
  }
18643
18866
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
18644
18867
  }
@@ -18759,7 +18982,11 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
18759
18982
  ...lighting
18760
18983
  };
18761
18984
  for (let i = 0; i < entries.length; i++) {
18762
- const { index, template, cullMask } = entries[i];
18985
+ const { index, template, cullMask, biomeTint } = entries[i];
18986
+ if (biomeTint) {
18987
+ input.biomeTints ?? (input.biomeTints = new Float32Array(entries.length * 3).fill(NaN));
18988
+ input.biomeTints.set(biomeTint, i * 3);
18989
+ }
18763
18990
  let id = templateIds.get(template);
18764
18991
  if (id === void 0) {
18765
18992
  let atlas = atlasIds.get(template.atlas);
@@ -18786,6 +19013,7 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
18786
19013
  colors: new Float32Array(quads * 12),
18787
19014
  indices: new Uint16Array(quads * 6),
18788
19015
  cullFaces: new Uint8Array(template.cullFaces),
19016
+ biomeTint: template.biomeTint ? new Uint8Array(template.biomeTint) : void 0,
18789
19017
  atlas: 0,
18790
19018
  layer: template.layer,
18791
19019
  ambientOcclusion: template.ambientOcclusion
@@ -18930,7 +19158,7 @@ init_CubeFace();
18930
19158
  init_InstanceReference();
18931
19159
  var WATER_TINT2 = new import_three47.Color(4159204).toArray();
18932
19160
  var Chunk = class _Chunk {
18933
- constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged, lightAt, onLightCleared, ambientLight = 0) {
19161
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged, lightAt, onLightCleared, ambientLight = 0, biomeBlend) {
18934
19162
  this.onBlocksChanged = onBlocksChanged;
18935
19163
  this.sectionModels = sectionModels;
18936
19164
  this.neighborCell = neighborCell;
@@ -18939,6 +19167,7 @@ var Chunk = class _Chunk {
18939
19167
  this.lightAt = lightAt;
18940
19168
  this.onLightCleared = onLightCleared;
18941
19169
  this.ambientLight = ambientLight;
19170
+ this.biomeBlend = biomeBlend;
18942
19171
  this.data = new ChunkData();
18943
19172
  this.renderedBlocks = /* @__PURE__ */ new Map();
18944
19173
  this.objectBlocks = /* @__PURE__ */ new Set();
@@ -18948,6 +19177,7 @@ var Chunk = class _Chunk {
18948
19177
  this.blockPosition = new import_three47.Vector3();
18949
19178
  this.meshDirty = false;
18950
19179
  this.meshGeneration = 0;
19180
+ this.blockChanges = /* @__PURE__ */ new Set();
18951
19181
  this.scene = scene;
18952
19182
  this.x = x;
18953
19183
  this.y = y;
@@ -19046,10 +19276,20 @@ var Chunk = class _Chunk {
19046
19276
  return this.refreshBiomes();
19047
19277
  }
19048
19278
  /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
19049
- async refreshBiomes() {
19279
+ refreshBiomes(columns) {
19280
+ return this.changeBlocks(() => this.refreshBiomeBlocks(columns), true);
19281
+ }
19282
+ async refreshBiomeBlocks(columns) {
19283
+ const blend = columns && this.biomeBlend?.();
19284
+ const ownColumn = columns?.some((column) => column.x === this.x && column.z === this.z);
19050
19285
  const affected = /* @__PURE__ */ new Map();
19051
19286
  const blocks = [];
19052
19287
  for (const index of this.renderedBlocks.keys()) {
19288
+ if (blend) {
19289
+ if (this.objectBlocks.has(index) && !ownColumn) continue;
19290
+ const [x, , z] = this.positionAt(index), radius = blend.radius;
19291
+ 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;
19292
+ }
19053
19293
  const block = this.data.get(index);
19054
19294
  const stateKey = ChunkData.paletteKey(block);
19055
19295
  let matches = affected.get(stateKey);
@@ -19064,10 +19304,32 @@ var Chunk = class _Chunk {
19064
19304
  }
19065
19305
  if (await matches) blocks.push({ index, block });
19066
19306
  }
19307
+ if (blend) {
19308
+ const groups = /* @__PURE__ */ new Map();
19309
+ for (const entry of blocks) {
19310
+ const key = ChunkData.paletteKey(entry.block);
19311
+ let group = groups.get(key);
19312
+ if (!group) groups.set(key, group = []);
19313
+ group.push(entry);
19314
+ }
19315
+ blocks.length = 0;
19316
+ const biomes = /* @__PURE__ */ new Map();
19317
+ for (const group of groups.values()) {
19318
+ const { contexts, fluid } = await this.resolveState(group[0].block, group.map((entry) => entry.index), biomes);
19319
+ for (const entry of group) {
19320
+ const context = contexts.get(entry.index);
19321
+ const current = this.sectionBlocks.get(entry.index)?.[0]?.biomeTint;
19322
+ const tint = context?.tint;
19323
+ const tintChanged = !!current !== !!tint || tint?.some((value, channel) => value !== current?.[channel]);
19324
+ 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])));
19325
+ if (this.objectBlocks.has(entry.index) || tintChanged || waterChanged) blocks.push(entry);
19326
+ }
19327
+ }
19328
+ }
19067
19329
  if (!blocks.length) return;
19068
19330
  const hidden = new Set(this.hiddenBlocks);
19069
19331
  try {
19070
- await this.placeBlocks(blocks);
19332
+ await this.replaceBlocks(blocks, true);
19071
19333
  } finally {
19072
19334
  for (const index of hidden) {
19073
19335
  this.hiddenBlocks.add(index);
@@ -19107,6 +19369,20 @@ var Chunk = class _Chunk {
19107
19369
  }
19108
19370
  /** Places cells in input order and returns changed world positions without refreshing neighbors. */
19109
19371
  async placeBlocks(blocks) {
19372
+ return this.changeBlocks(() => this.replaceBlocks(blocks));
19373
+ }
19374
+ changeBlocks(change, refreshBiomes = false) {
19375
+ const before = refreshBiomes ? Promise.allSettled(this.blockChanges) : this.biomeChange;
19376
+ const result = before ? before.catch(() => void 0).then(change) : change();
19377
+ const pending = result.finally(() => {
19378
+ this.blockChanges.delete(pending);
19379
+ if (this.biomeChange === pending) this.biomeChange = void 0;
19380
+ });
19381
+ this.blockChanges.add(pending);
19382
+ if (refreshBiomes) this.biomeChange = pending;
19383
+ return pending;
19384
+ }
19385
+ async replaceBlocks(blocks, preserveDataOnFailure = false) {
19110
19386
  const placements = blocks.map(({ index, block }) => ({
19111
19387
  index,
19112
19388
  key: block && !ChunkData.isAir(block) ? ChunkData.paletteKey(block) : void 0,
@@ -19126,6 +19402,10 @@ var Chunk = class _Chunk {
19126
19402
  const biomes = /* @__PURE__ */ new Map();
19127
19403
  const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
19128
19404
  const resolutions = new Map([...states.keys()].map((key, i) => [key, settled[i]]));
19405
+ if (preserveDataOnFailure) {
19406
+ const rejected = settled.find((result) => result.status === "rejected");
19407
+ if (rejected?.status === "rejected") throw rejected.reason;
19408
+ }
19129
19409
  const positions = [];
19130
19410
  let failed = false, failure;
19131
19411
  for (const { index, key, block } of placements) {
@@ -19163,7 +19443,7 @@ var Chunk = class _Chunk {
19163
19443
  }
19164
19444
  const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
19165
19445
  if (templates) {
19166
- this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
19446
+ this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0, biomeTint: context?.tint })));
19167
19447
  this.meshDirty = true;
19168
19448
  } else {
19169
19449
  object4 = await this.scene.addBlock(blockState3, {
@@ -19182,7 +19462,11 @@ var Chunk = class _Chunk {
19182
19462
  }, object: object4 });
19183
19463
  if (object4) this.objectBlocks.add(index);
19184
19464
  } catch (error) {
19185
- this.data.assign(index, 0);
19465
+ if (preserveDataOnFailure && readBlock) {
19466
+ this.renderedBlocks.set(index, { get block() {
19467
+ return readBlock();
19468
+ }, object: void 0 });
19469
+ } else this.data.assign(index, 0);
19186
19470
  this.fluidCells[index] = 0;
19187
19471
  object4?.removeFromScene();
19188
19472
  if (!failed) failure = error;
@@ -19193,7 +19477,10 @@ var Chunk = class _Chunk {
19193
19477
  return positions;
19194
19478
  }
19195
19479
  /** Replaces every cell from parsed section data. The caller refreshes neighbor culling. */
19196
- async placeSection(data) {
19480
+ placeSection(data) {
19481
+ return this.changeBlocks(() => this.replaceSection(data));
19482
+ }
19483
+ async replaceSection(data) {
19197
19484
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
19198
19485
  this.sectionBlocks.clear();
19199
19486
  this.fluidCells.fill(0);
@@ -19240,7 +19527,7 @@ var Chunk = class _Chunk {
19240
19527
  ];
19241
19528
  const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
19242
19529
  if (templates) {
19243
- this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
19530
+ this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0, biomeTint: context?.tint })));
19244
19531
  this.meshDirty = true;
19245
19532
  } else {
19246
19533
  perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
@@ -19324,12 +19611,35 @@ var Chunk = class _Chunk {
19324
19611
  }
19325
19612
  async resolveState(stored, indices, biomes) {
19326
19613
  const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
19327
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
19614
+ let perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
19328
19615
  const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
19329
19616
  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;
19330
19617
  const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
19331
19618
  const prepare = (color2) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color2);
19332
19619
  const contexts = /* @__PURE__ */ new Map();
19620
+ const blend = this.biomeBlend?.();
19621
+ if (blend && !perBlock && blockState3 && (source || fluid?.kind === "water")) {
19622
+ const prepared2 = fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties, void 0, true);
19623
+ if (!prepared2 && fluid?.renderModel !== false) {
19624
+ perBlock = true;
19625
+ } else {
19626
+ await Promise.all(indices.map(async (index) => {
19627
+ const position = this.biomePosition(index, blockState3, stored);
19628
+ const biomeColor = (kind) => blend.getColor(kind, blockState3.key, position);
19629
+ const context = {};
19630
+ if (source && prepared2?.biomeTint) {
19631
+ const color2 = await biomeColor(source);
19632
+ if (color2 !== void 0) context.tint = new import_three47.Color(color2).toArray();
19633
+ }
19634
+ if (fluid?.kind === "water") {
19635
+ const color2 = await biomeColor("water");
19636
+ if (color2 !== void 0 && color2 !== 4159204) context.waterTint = new import_three47.Color(color2).toArray();
19637
+ }
19638
+ contexts.set(index, context);
19639
+ }));
19640
+ return { blockState: blockState3, perBlock, fluid, prepared: prepared2, contexts, stored };
19641
+ }
19642
+ }
19333
19643
  const groups = /* @__PURE__ */ new Map();
19334
19644
  if (blockState3 && (source || fluid?.kind === "water")) {
19335
19645
  for (const index of indices) {
@@ -19377,7 +19687,11 @@ var Chunk = class _Chunk {
19377
19687
  this.waterTints.set(tint ?? WATER_TINT2, index * 3);
19378
19688
  }
19379
19689
  /** Changes visibility at world block coordinates while preserving the block's data. */
19380
- async setBlockVisibleAt(pos, visible) {
19690
+ setBlockVisibleAt(pos, visible) {
19691
+ const position = pos.clone();
19692
+ return this.changeBlocks(() => this.setBlockVisibility(position, visible));
19693
+ }
19694
+ async setBlockVisibility(pos, visible) {
19381
19695
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
19382
19696
  const block = this.renderedBlocks.get(index);
19383
19697
  if (!block || visible === !this.hiddenBlocks.has(index)) {
@@ -19547,6 +19861,7 @@ var Chunk = class _Chunk {
19547
19861
  }
19548
19862
  /** Removes stored blocks and render objects. Set `notify` to false to skip the owning world's update. */
19549
19863
  async clear(notify = true) {
19864
+ if (this.blockChanges.size) await Promise.allSettled(this.blockChanges);
19550
19865
  const positions = notify && this.onBlocksChanged ? [...this.renderedBlocks.keys()].map((index) => this.chunkPosToWorldPos(
19551
19866
  new import_three47.Vector3(index % 16, Math.floor(index / 256), Math.floor(index / 16) % 16)
19552
19867
  )) : [];