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.
@@ -252,6 +252,13 @@ var init_Caching = __esm({
252
252
  import_loading_cache = require("@inventivetalent/loading-cache");
253
253
  import_time = require("@inventivetalent/time");
254
254
  Caching = class _Caching {
255
+ static {
256
+ this._generation = 0;
257
+ }
258
+ /** Changes when shared assets are cleared, so derived caches can discard their old values. */
259
+ static get generation() {
260
+ return this._generation;
261
+ }
255
262
  static {
256
263
  this.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();
257
264
  }
@@ -362,6 +369,7 @@ var init_Caching = __esm({
362
369
  }
363
370
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
364
371
  static clear() {
372
+ this._generation++;
365
373
  for (const cache of this.all) {
366
374
  cache.invalidateAll();
367
375
  }
@@ -373,6 +381,7 @@ var init_Caching = __esm({
373
381
  * Use {@link shutdown} to also stop requests and the shared Ticker.
374
382
  */
375
383
  static end() {
384
+ this._generation++;
376
385
  for (const cache of this.all) {
377
386
  cache.invalidateAll();
378
387
  cache.end();
@@ -17544,18 +17553,23 @@ var SectionModels = class {
17544
17553
  return this.template(variant, choice, tints);
17545
17554
  }));
17546
17555
  }
17547
- /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
17548
- async prepareState(blockState3, properties = {}, biomeColor) {
17556
+ /** Resolves shared templates; dynamic biome tints are supplied on section placements instead of cached geometry. */
17557
+ async prepareState(blockState3, properties = {}, biomeColor, dynamicBiomeTint = false) {
17549
17558
  const prepared = await this.state(blockState3, properties);
17550
17559
  if (!prepared) return void 0;
17551
17560
  let color2;
17552
17561
  const resolveColor = typeof biomeColor === "function" ? () => color2 ??= biomeColor() : biomeColor;
17553
17562
  const templates = /* @__PURE__ */ new Map();
17563
+ const source = dynamicBiomeTint && await BlockTints.biomeSource(blockState3.key);
17554
17564
  await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
17555
17565
  const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
17556
- templates.set(variant, await this.template(variant, choice, tints));
17566
+ const biomeTints = source ? await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, 16777215) : void 0;
17567
+ templates.set(variant, await this.template(variant, choice, tints, biomeTints));
17557
17568
  }));
17558
- return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
17569
+ return {
17570
+ pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))),
17571
+ biomeTint: source ? [...templates.values()].some((template) => template.biomeTint?.some((value) => value === 1)) || void 0 : void 0
17572
+ };
17559
17573
  }
17560
17574
  async state(blockState3, properties) {
17561
17575
  let states = this.states.get(blockState3);
@@ -17598,10 +17612,10 @@ var SectionModels = class {
17598
17612
  }
17599
17613
  return { state, groups, choices: prepared };
17600
17614
  }
17601
- async template(variant, { model, atlas, rotation }, tints) {
17615
+ async template(variant, { model, atlas, rotation }, tints, biomeTints) {
17602
17616
  let models = this.models.get(model);
17603
17617
  if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17604
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17618
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints, biomeTints]);
17605
17619
  return cached(models, key, async () => {
17606
17620
  const object4 = new SectionModel(model, {
17607
17621
  instanceMeshes: false,
@@ -17615,11 +17629,13 @@ var SectionModels = class {
17615
17629
  object4.clear();
17616
17630
  const keep = [];
17617
17631
  const cullFaces = [];
17632
+ const biomeTint = biomeTints ? [] : void 0;
17618
17633
  for (const element of atlas.model.elements) {
17619
17634
  for (const face of CUBE_FACES) {
17620
17635
  const data = element.faces[face];
17621
17636
  keep.push(data !== void 0);
17622
17637
  if (!data) continue;
17638
+ biomeTint?.push(Number(data.tintindex !== void 0 && biomeTints?.[data.tintindex] !== void 0));
17623
17639
  const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17624
17640
  if (local < 0) {
17625
17641
  cullFaces.push(0);
@@ -17635,6 +17651,7 @@ var SectionModels = class {
17635
17651
  geometry,
17636
17652
  atlas,
17637
17653
  cullFaces: new Uint8Array(cullFaces),
17654
+ biomeTint: biomeTint?.some(Boolean) ? new Uint8Array(biomeTint) : void 0,
17638
17655
  occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17639
17656
  layer: atlas.hasTranslucency ? 1 : 0,
17640
17657
  ambientOcclusion: model.ambientocclusion !== false
@@ -17864,6 +17881,132 @@ var WorldEntities = class {
17864
17881
  // src/world/MineRenderWorld.ts
17865
17882
  init_FluidQuads();
17866
17883
 
17884
+ // src/world/_biomes/BiomeBlend.ts
17885
+ init_AssetKey();
17886
+ init_AssetLoader();
17887
+ init_Caching();
17888
+ var MAX_TILES = 256;
17889
+ function validateBiomeBlendRadius(radius) {
17890
+ if (!Number.isInteger(radius) || radius < 0 || radius > 7) {
17891
+ throw new RangeError("biomeBlendRadius must be an integer from 0 to 7");
17892
+ }
17893
+ }
17894
+ var BiomeBlend = class {
17895
+ constructor(biomeAt, radius) {
17896
+ this.biomeAt = biomeAt;
17897
+ this.tiles = /* @__PURE__ */ new Map();
17898
+ this.generation = Caching.generation;
17899
+ validateBiomeBlendRadius(radius);
17900
+ this._radius = radius;
17901
+ }
17902
+ get radius() {
17903
+ return this._radius;
17904
+ }
17905
+ clear() {
17906
+ this.tiles.clear();
17907
+ }
17908
+ invalidateColumn(x, z) {
17909
+ for (const [key, tile] of this.tiles) {
17910
+ if (Math.abs(tile.x - x) <= 1 && Math.abs(tile.z - z) <= 1) this.tiles.delete(key);
17911
+ }
17912
+ }
17913
+ async getColor(kind, origin, position) {
17914
+ const [x, y, z] = position;
17915
+ const biome = this.biomeAt(x, y, z);
17916
+ if (biome === void 0) return void 0;
17917
+ if (this.generation !== Caching.generation) {
17918
+ this.clear();
17919
+ this.generation = Caching.generation;
17920
+ }
17921
+ const tileX = Math.floor(x / 16), tileZ = Math.floor(z / 16), quartY = Math.floor(y / 4);
17922
+ const root = origin?.root ?? AssetLoader.ROOT;
17923
+ const key = JSON.stringify([root, AssetLoader.persistentScope, kind, this._radius, tileX, quartY, tileZ]);
17924
+ let tile = this.tiles.get(key);
17925
+ if (tile) {
17926
+ this.tiles.delete(key);
17927
+ this.tiles.set(key, tile);
17928
+ } else {
17929
+ const snapshot = new AssetKey("minecraft", "", void 0, void 0, "assets", ".json", root);
17930
+ const colors = this.buildTile(kind, snapshot, tileX, quartY, tileZ, this._radius).then((result2) => {
17931
+ if (!result2.complete && this.tiles.get(key)?.colors === colors) this.tiles.delete(key);
17932
+ return result2;
17933
+ }, (error) => {
17934
+ if (this.tiles.get(key)?.colors === colors) this.tiles.delete(key);
17935
+ throw error;
17936
+ });
17937
+ tile = { x: tileX, z: tileZ, colors };
17938
+ this.tiles.set(key, tile);
17939
+ if (this.tiles.size > MAX_TILES) this.tiles.delete(this.tiles.keys().next().value);
17940
+ }
17941
+ const result = await tile.colors;
17942
+ if (result.failures.has(biome)) throw result.failures.get(biome);
17943
+ const color2 = result.colors[(z - tileZ * 16) * 16 + x - tileX * 16];
17944
+ return color2 === -1 ? void 0 : color2;
17945
+ }
17946
+ async buildTile(kind, origin, tileX, quartY, tileZ, radius) {
17947
+ const size = 16 + radius * 2, stride = size + 1;
17948
+ const startX = tileX * 16 - radius, startZ = tileZ * 16 - radius;
17949
+ const quartX = Math.floor(startX / 4), quartZ = Math.floor(startZ / 4);
17950
+ const quartSize = Math.floor((startX + size - 1) / 4) - quartX + 1;
17951
+ const ids = new Array(quartSize * quartSize);
17952
+ const biomes = /* @__PURE__ */ new Map();
17953
+ for (let z = 0; z < quartSize; z++) {
17954
+ for (let x = 0; x < quartSize; x++) {
17955
+ const id = this.biomeAt((quartX + x) * 4, quartY * 4, (quartZ + z) * 4);
17956
+ ids[z * quartSize + x] = id;
17957
+ if (id !== void 0 && !biomes.has(id)) {
17958
+ biomes.set(id, (async () => {
17959
+ const swamp = kind === "grass" && (await Biomes.get(id, origin))?.effects.grass_color_modifier === "swamp";
17960
+ return { color: swamp ? void 0 : await Biomes.getColor(id, kind, origin), swamp };
17961
+ })());
17962
+ }
17963
+ }
17964
+ }
17965
+ const failures = /* @__PURE__ */ new Map();
17966
+ const loaded = new Map(await Promise.all([...biomes].map(async ([id, color2]) => {
17967
+ try {
17968
+ return [id, await color2];
17969
+ } catch (error) {
17970
+ failures.set(id, error);
17971
+ return [id, { color: void 0, swamp: false }];
17972
+ }
17973
+ })));
17974
+ const complete = [...loaded.values()].every((value) => value.swamp || value.color !== void 0);
17975
+ const samples = new Int32Array(size * size).fill(-1);
17976
+ const sums = Array.from({ length: 4 }, () => new Uint32Array(stride * stride));
17977
+ for (let z = 0; z < size; z++) {
17978
+ const row = [0, 0, 0, 0];
17979
+ for (let x = 0; x < size; x++) {
17980
+ const id = ids[(Math.floor((startZ + z) / 4) - quartZ) * quartSize + Math.floor((startX + x) / 4) - quartX];
17981
+ const value = id === void 0 ? void 0 : loaded.get(id);
17982
+ const color2 = value?.swamp ? swampGrassColor(startX + x, startZ + z) : value?.color;
17983
+ if (color2 !== void 0) {
17984
+ samples[z * size + x] = color2;
17985
+ row[0] += color2 >> 16 & 255;
17986
+ row[1] += color2 >> 8 & 255;
17987
+ row[2] += color2 & 255;
17988
+ row[3]++;
17989
+ }
17990
+ for (let channel = 0; channel < 4; channel++) {
17991
+ sums[channel][(z + 1) * stride + x + 1] = sums[channel][z * stride + x + 1] + row[channel];
17992
+ }
17993
+ }
17994
+ }
17995
+ const colors = new Int32Array(256).fill(-1), width = radius * 2 + 1;
17996
+ for (let z = 0; z < 16; z++) {
17997
+ for (let x = 0; x < 16; x++) {
17998
+ if (samples[(z + radius) * size + x + radius] === -1) continue;
17999
+ const topLeft = z * stride + x, topRight = topLeft + width;
18000
+ const bottomLeft = (z + width) * stride + x, bottomRight = bottomLeft + width;
18001
+ const sum = (values) => values[bottomRight] - values[bottomLeft] - values[topRight] + values[topLeft];
18002
+ const count = sum(sums[3]);
18003
+ 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);
18004
+ }
18005
+ }
18006
+ return { colors, complete, failures };
18007
+ }
18008
+ };
18009
+
17867
18010
  // src/world/WorldLight.ts
17868
18011
  function copySectionLight(light) {
17869
18012
  const copy = {};
@@ -17927,12 +18070,17 @@ var MineRenderWorld = class {
17927
18070
  this.lightValidity = /* @__PURE__ */ new Map();
17928
18071
  this.highestSkySection = /* @__PURE__ */ new Map();
17929
18072
  this.ambientOcclusionPending = false;
18073
+ this.pendingBiomes = /* @__PURE__ */ new Map();
17930
18074
  this.pendingCulling = /* @__PURE__ */ new Map();
17931
18075
  if (options.savedLighting && !options.sectionMeshing) throw new TypeError("savedLighting requires sectionMeshing");
17932
18076
  if (options.ambientOcclusion && !options.sectionMeshing) throw new TypeError("ambientOcclusion requires sectionMeshing");
17933
18077
  if (options.ambientLight !== void 0 && (!Number.isFinite(options.ambientLight) || options.ambientLight < 0 || options.ambientLight > 1)) {
17934
18078
  throw new RangeError("ambientLight must be a number from 0 to 1");
17935
18079
  }
18080
+ this._biomeBlendRadius = options.biomeBlendRadius ?? 0;
18081
+ validateBiomeBlendRadius(this._biomeBlendRadius);
18082
+ if (this._biomeBlendRadius > 0 && !options.sectionMeshing) throw new TypeError("biomeBlendRadius requires sectionMeshing");
18083
+ if (this._biomeBlendRadius) this.biomeBlend = new BiomeBlend((x, y, z) => this.getBiomeAt(x, y, z), this._biomeBlendRadius);
17936
18084
  this.scene = scene;
17937
18085
  this.savedLighting = options.savedLighting ?? false;
17938
18086
  this.ambientLight = options.ambientLight ?? 0;
@@ -17973,6 +18121,65 @@ var MineRenderWorld = class {
17973
18121
  await this.updateCulling([]);
17974
18122
  this.ambientOcclusionPending = false;
17975
18123
  }
18124
+ /** Horizontal biome-color sampling radius, from 0 to 7 blocks. Zero disables blending. */
18125
+ get biomeBlendRadius() {
18126
+ return this._biomeBlendRadius;
18127
+ }
18128
+ /** Refreshes loaded grass, foliage, and water colors without reloading their source chunks. */
18129
+ async setBiomeBlendRadius(radius) {
18130
+ validateBiomeBlendRadius(radius);
18131
+ if (radius > 0 && !this.sectionModels) throw new TypeError("biomeBlendRadius requires sectionMeshing");
18132
+ if (radius !== this._biomeBlendRadius) {
18133
+ this._biomeBlendRadius = radius;
18134
+ this.biomeBlend?.clear();
18135
+ this.biomeBlend = radius ? new BiomeBlend((x, y, z) => this.getBiomeAt(x, y, z), radius) : void 0;
18136
+ for (const chunk of this._chunks.values()) this.pendingBiomes.set(chunk, void 0);
18137
+ }
18138
+ const failures = await this.refreshBiomes();
18139
+ if (failures.size) throw failures.values().next().value;
18140
+ }
18141
+ markBiomesChanged(x, z, y, includeColumn = true) {
18142
+ const queued = [];
18143
+ if (!this.biomeBlend) return queued;
18144
+ this.biomeBlend.invalidateColumn(x, z);
18145
+ for (const chunk of this._chunks.values()) {
18146
+ 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;
18147
+ queued.push(chunk);
18148
+ if (!this.pendingBiomes.has(chunk)) this.pendingBiomes.set(chunk, []);
18149
+ const columns = this.pendingBiomes.get(chunk);
18150
+ if (columns && !columns.some((column) => column.x === x && column.z === z)) columns.push({ x, z });
18151
+ }
18152
+ return queued;
18153
+ }
18154
+ async refreshBiomes() {
18155
+ if (this.biomeRefreshing) await this.biomeRefreshing;
18156
+ if (!this.pendingBiomes.size) return /* @__PURE__ */ new Map();
18157
+ return this.biomeRefreshing ??= Promise.resolve().then(async () => {
18158
+ const failures = /* @__PURE__ */ new Map();
18159
+ const batch = [...this.pendingBiomes.keys()];
18160
+ let sliceStart = performance.now();
18161
+ try {
18162
+ for (const chunk of batch) {
18163
+ const columns = this.pendingBiomes.get(chunk);
18164
+ this.pendingBiomes.delete(chunk);
18165
+ if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) !== chunk) continue;
18166
+ try {
18167
+ await chunk.refreshBiomes(columns);
18168
+ } catch (error) {
18169
+ failures.set(chunk, error);
18170
+ if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
18171
+ }
18172
+ if (performance.now() - sliceStart > 8) {
18173
+ await yieldToEventLoop();
18174
+ sliceStart = performance.now();
18175
+ }
18176
+ }
18177
+ return failures;
18178
+ } finally {
18179
+ this.biomeRefreshing = void 0;
18180
+ }
18181
+ });
18182
+ }
17976
18183
  getBlockAt(posOrX, y, z) {
17977
18184
  if (typeof posOrX == "number") {
17978
18185
  return this.getBlockAt(new import_three45.Vector3(posOrX, y, z));
@@ -18208,6 +18415,9 @@ var MineRenderWorld = class {
18208
18415
  }
18209
18416
  await placeEntities?.();
18210
18417
  } finally {
18418
+ this.markBiomesChanged(chunk.x, chunk.z, void 0, false);
18419
+ const failures = await this.refreshBiomes();
18420
+ if (failures.size) console.warn("Failed to refresh biome colors", failures.values().next().value);
18211
18421
  await this.updateCulling([...changes.values()]);
18212
18422
  }
18213
18423
  }
@@ -18224,11 +18434,14 @@ var MineRenderWorld = class {
18224
18434
  try {
18225
18435
  await this.clearChunkColumn(x, z);
18226
18436
  } finally {
18437
+ const failures = await this.refreshBiomes();
18438
+ if (failures.size) console.warn("Failed to refresh biome colors", failures.values().next().value);
18227
18439
  await this.updateCulling([]);
18228
18440
  }
18229
18441
  }
18230
18442
  async clearChunkColumn(x, z) {
18231
18443
  this.biomes.delete(`${x}_${z}`);
18444
+ this.markBiomesChanged(x, z, void 0, false);
18232
18445
  if (this.savedLighting) {
18233
18446
  for (const y of this.light.get(`${x}_${z}`)?.keys() ?? []) this.markSectionChanged({ x, y, z });
18234
18447
  }
@@ -18239,6 +18452,7 @@ var MineRenderWorld = class {
18239
18452
  for (const [key, section] of previous) {
18240
18453
  this._chunks.delete(key);
18241
18454
  this.pendingCulling.delete(section);
18455
+ this.pendingBiomes.delete(section);
18242
18456
  await section.clear(false);
18243
18457
  this.markSectionChanged(section);
18244
18458
  }
@@ -18273,6 +18487,8 @@ var MineRenderWorld = class {
18273
18487
  const chunks = [...this._chunks.values()];
18274
18488
  this._chunks.clear();
18275
18489
  this.biomes.clear();
18490
+ this.biomeBlend?.clear();
18491
+ this.pendingBiomes.clear();
18276
18492
  this.light.clear();
18277
18493
  this.lightValidity.clear();
18278
18494
  this.highestSkySection.clear();
@@ -18391,8 +18607,14 @@ var MineRenderWorld = class {
18391
18607
  column?.delete(y);
18392
18608
  if (!column?.size) this.biomes.delete(columnKey);
18393
18609
  }
18394
- await chunk.refreshBiomes();
18395
- await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
18610
+ if (this.biomeBlend) {
18611
+ const queued = this.markBiomesChanged(x, z, y);
18612
+ const failures = await this.refreshBiomes();
18613
+ for (const chunk2 of queued) if (failures.has(chunk2)) throw failures.get(chunk2);
18614
+ } else {
18615
+ await chunk.refreshBiomes();
18616
+ await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
18617
+ }
18396
18618
  },
18397
18619
  this.savedLighting ? (x2, y2, z2) => this.lightLevelAt(x2, y2, z2) : void 0,
18398
18620
  async () => {
@@ -18409,7 +18631,8 @@ var MineRenderWorld = class {
18409
18631
  await this.updateCulling([]);
18410
18632
  }
18411
18633
  },
18412
- this.ambientLight
18634
+ this.ambientLight,
18635
+ () => this.biomeBlend
18413
18636
  );
18414
18637
  this._chunks.set(key, chunk);
18415
18638
  }
@@ -18528,9 +18751,10 @@ var LIGHT_SAMPLE_OFFSETS = [0, 1, -1, 324, -324, 18, -18];
18528
18751
 
18529
18752
  // src/world/SectionGeometry.ts
18530
18753
  function buildSectionGeometry(input) {
18531
- const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion } = input;
18754
+ const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion, biomeTints } = input;
18532
18755
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
18533
18756
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
18757
+ if (biomeTints && biomeTints.length !== count * 3) throw new RangeError("Section biome tints must contain RGB for each placement");
18534
18758
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
18535
18759
  if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
18536
18760
  if (light && (light.length !== 5832 || light.some((level) => level > 15 && level !== 255))) {
@@ -18545,7 +18769,7 @@ function buildSectionGeometry(input) {
18545
18769
  const cornerBrightness2 = ambientOcclusion ? new Float32Array(4) : void 0;
18546
18770
  for (const template of templateData) {
18547
18771
  const { quads } = template;
18548
- 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) {
18772
+ 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) {
18549
18773
  throw new RangeError("Section template arrays do not match its quad count");
18550
18774
  }
18551
18775
  }
@@ -18655,6 +18879,7 @@ function buildSectionGeometry(input) {
18655
18879
  blockIndices[indexOffset / 6] = indices[entry];
18656
18880
  if (ambientMask) quadAmbientOcclusion(template.positions, template.normals, quad, ambientMask, cornerBrightness2);
18657
18881
  const brightness = light ? lightBrightness(light[lightIndex + LIGHT_SAMPLE_OFFSETS[quadLightIndex(template.positions, template.normals, quad)]], ambientLight) : 1;
18882
+ const tint = biomeTints && template.biomeTint?.[quad] && !Number.isNaN(biomeTints[entry * 3]) ? entry * 3 : void 0;
18658
18883
  const start = vertex;
18659
18884
  for (let corner = 0; corner < 4; corner++, vertex++) {
18660
18885
  const source = quad * 4 + corner;
@@ -18671,9 +18896,9 @@ function buildSectionGeometry(input) {
18671
18896
  uvBounds[vertex * 4 + 2] = mapU(template.uvBounds[source * 4 + 2]);
18672
18897
  uvBounds[vertex * 4 + 3] = mapV(template.uvBounds[source * 4 + 3]);
18673
18898
  const factor = brightness * (ambientMask ? cornerBrightness2[corner] : 1);
18674
- colors[vertex * 3] = template.colors[source * 3] * factor;
18675
- colors[vertex * 3 + 1] = template.colors[source * 3 + 1] * factor;
18676
- colors[vertex * 3 + 2] = template.colors[source * 3 + 2] * factor;
18899
+ colors[vertex * 3] = (tint === void 0 ? template.colors[source * 3] : biomeTints[tint]) * factor;
18900
+ colors[vertex * 3 + 1] = (tint === void 0 ? template.colors[source * 3 + 1] : biomeTints[tint + 1]) * factor;
18901
+ colors[vertex * 3 + 2] = (tint === void 0 ? template.colors[source * 3 + 2] : biomeTints[tint + 2]) * factor;
18677
18902
  }
18678
18903
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
18679
18904
  }
@@ -18795,7 +19020,11 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
18795
19020
  ...lighting
18796
19021
  };
18797
19022
  for (let i = 0; i < entries.length; i++) {
18798
- const { index, template, cullMask } = entries[i];
19023
+ const { index, template, cullMask, biomeTint } = entries[i];
19024
+ if (biomeTint) {
19025
+ input.biomeTints ??= new Float32Array(entries.length * 3).fill(NaN);
19026
+ input.biomeTints.set(biomeTint, i * 3);
19027
+ }
18799
19028
  let id = templateIds.get(template);
18800
19029
  if (id === void 0) {
18801
19030
  let atlas = atlasIds.get(template.atlas);
@@ -18822,6 +19051,7 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
18822
19051
  colors: new Float32Array(quads * 12),
18823
19052
  indices: new Uint16Array(quads * 6),
18824
19053
  cullFaces: new Uint8Array(template.cullFaces),
19054
+ biomeTint: template.biomeTint ? new Uint8Array(template.biomeTint) : void 0,
18825
19055
  atlas: 0,
18826
19056
  layer: template.layer,
18827
19057
  ambientOcclusion: template.ambientOcclusion
@@ -18966,7 +19196,7 @@ init_CubeFace();
18966
19196
  init_InstanceReference();
18967
19197
  var WATER_TINT2 = new import_three47.Color(4159204).toArray();
18968
19198
  var Chunk = class _Chunk {
18969
- constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged, lightAt, onLightCleared, ambientLight = 0) {
19199
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged, lightAt, onLightCleared, ambientLight = 0, biomeBlend) {
18970
19200
  this.onBlocksChanged = onBlocksChanged;
18971
19201
  this.sectionModels = sectionModels;
18972
19202
  this.neighborCell = neighborCell;
@@ -18975,6 +19205,7 @@ var Chunk = class _Chunk {
18975
19205
  this.lightAt = lightAt;
18976
19206
  this.onLightCleared = onLightCleared;
18977
19207
  this.ambientLight = ambientLight;
19208
+ this.biomeBlend = biomeBlend;
18978
19209
  this.data = new ChunkData();
18979
19210
  this.renderedBlocks = /* @__PURE__ */ new Map();
18980
19211
  this.objectBlocks = /* @__PURE__ */ new Set();
@@ -18984,6 +19215,7 @@ var Chunk = class _Chunk {
18984
19215
  this.blockPosition = new import_three47.Vector3();
18985
19216
  this.meshDirty = false;
18986
19217
  this.meshGeneration = 0;
19218
+ this.blockChanges = /* @__PURE__ */ new Set();
18987
19219
  this.scene = scene;
18988
19220
  this.x = x;
18989
19221
  this.y = y;
@@ -19082,10 +19314,20 @@ var Chunk = class _Chunk {
19082
19314
  return this.refreshBiomes();
19083
19315
  }
19084
19316
  /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
19085
- async refreshBiomes() {
19317
+ refreshBiomes(columns) {
19318
+ return this.changeBlocks(() => this.refreshBiomeBlocks(columns), true);
19319
+ }
19320
+ async refreshBiomeBlocks(columns) {
19321
+ const blend = columns && this.biomeBlend?.();
19322
+ const ownColumn = columns?.some((column) => column.x === this.x && column.z === this.z);
19086
19323
  const affected = /* @__PURE__ */ new Map();
19087
19324
  const blocks = [];
19088
19325
  for (const index of this.renderedBlocks.keys()) {
19326
+ if (blend) {
19327
+ if (this.objectBlocks.has(index) && !ownColumn) continue;
19328
+ const [x, , z] = this.positionAt(index), radius = blend.radius;
19329
+ 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;
19330
+ }
19089
19331
  const block = this.data.get(index);
19090
19332
  const stateKey = ChunkData.paletteKey(block);
19091
19333
  let matches = affected.get(stateKey);
@@ -19100,10 +19342,32 @@ var Chunk = class _Chunk {
19100
19342
  }
19101
19343
  if (await matches) blocks.push({ index, block });
19102
19344
  }
19345
+ if (blend) {
19346
+ const groups = /* @__PURE__ */ new Map();
19347
+ for (const entry of blocks) {
19348
+ const key = ChunkData.paletteKey(entry.block);
19349
+ let group = groups.get(key);
19350
+ if (!group) groups.set(key, group = []);
19351
+ group.push(entry);
19352
+ }
19353
+ blocks.length = 0;
19354
+ const biomes = /* @__PURE__ */ new Map();
19355
+ for (const group of groups.values()) {
19356
+ const { contexts, fluid } = await this.resolveState(group[0].block, group.map((entry) => entry.index), biomes);
19357
+ for (const entry of group) {
19358
+ const context = contexts.get(entry.index);
19359
+ const current = this.sectionBlocks.get(entry.index)?.[0]?.biomeTint;
19360
+ const tint = context?.tint;
19361
+ const tintChanged = !!current !== !!tint || tint?.some((value, channel) => value !== current?.[channel]);
19362
+ 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])));
19363
+ if (this.objectBlocks.has(entry.index) || tintChanged || waterChanged) blocks.push(entry);
19364
+ }
19365
+ }
19366
+ }
19103
19367
  if (!blocks.length) return;
19104
19368
  const hidden = new Set(this.hiddenBlocks);
19105
19369
  try {
19106
- await this.placeBlocks(blocks);
19370
+ await this.replaceBlocks(blocks, true);
19107
19371
  } finally {
19108
19372
  for (const index of hidden) {
19109
19373
  this.hiddenBlocks.add(index);
@@ -19143,6 +19407,20 @@ var Chunk = class _Chunk {
19143
19407
  }
19144
19408
  /** Places cells in input order and returns changed world positions without refreshing neighbors. */
19145
19409
  async placeBlocks(blocks) {
19410
+ return this.changeBlocks(() => this.replaceBlocks(blocks));
19411
+ }
19412
+ changeBlocks(change, refreshBiomes = false) {
19413
+ const before = refreshBiomes ? Promise.allSettled(this.blockChanges) : this.biomeChange;
19414
+ const result = before ? before.catch(() => void 0).then(change) : change();
19415
+ const pending = result.finally(() => {
19416
+ this.blockChanges.delete(pending);
19417
+ if (this.biomeChange === pending) this.biomeChange = void 0;
19418
+ });
19419
+ this.blockChanges.add(pending);
19420
+ if (refreshBiomes) this.biomeChange = pending;
19421
+ return pending;
19422
+ }
19423
+ async replaceBlocks(blocks, preserveDataOnFailure = false) {
19146
19424
  const placements = blocks.map(({ index, block }) => ({
19147
19425
  index,
19148
19426
  key: block && !ChunkData.isAir(block) ? ChunkData.paletteKey(block) : void 0,
@@ -19162,6 +19440,10 @@ var Chunk = class _Chunk {
19162
19440
  const biomes = /* @__PURE__ */ new Map();
19163
19441
  const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
19164
19442
  const resolutions = new Map([...states.keys()].map((key, i) => [key, settled[i]]));
19443
+ if (preserveDataOnFailure) {
19444
+ const rejected = settled.find((result) => result.status === "rejected");
19445
+ if (rejected?.status === "rejected") throw rejected.reason;
19446
+ }
19165
19447
  const positions = [];
19166
19448
  let failed = false, failure;
19167
19449
  for (const { index, key, block } of placements) {
@@ -19199,7 +19481,7 @@ var Chunk = class _Chunk {
19199
19481
  }
19200
19482
  const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
19201
19483
  if (templates) {
19202
- this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
19484
+ this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0, biomeTint: context?.tint })));
19203
19485
  this.meshDirty = true;
19204
19486
  } else {
19205
19487
  object4 = await this.scene.addBlock(blockState3, {
@@ -19218,7 +19500,11 @@ var Chunk = class _Chunk {
19218
19500
  }, object: object4 });
19219
19501
  if (object4) this.objectBlocks.add(index);
19220
19502
  } catch (error) {
19221
- this.data.assign(index, 0);
19503
+ if (preserveDataOnFailure && readBlock) {
19504
+ this.renderedBlocks.set(index, { get block() {
19505
+ return readBlock();
19506
+ }, object: void 0 });
19507
+ } else this.data.assign(index, 0);
19222
19508
  this.fluidCells[index] = 0;
19223
19509
  object4?.removeFromScene();
19224
19510
  if (!failed) failure = error;
@@ -19229,7 +19515,10 @@ var Chunk = class _Chunk {
19229
19515
  return positions;
19230
19516
  }
19231
19517
  /** Replaces every cell from parsed section data. The caller refreshes neighbor culling. */
19232
- async placeSection(data) {
19518
+ placeSection(data) {
19519
+ return this.changeBlocks(() => this.replaceSection(data));
19520
+ }
19521
+ async replaceSection(data) {
19233
19522
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
19234
19523
  this.sectionBlocks.clear();
19235
19524
  this.fluidCells.fill(0);
@@ -19276,7 +19565,7 @@ var Chunk = class _Chunk {
19276
19565
  ];
19277
19566
  const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
19278
19567
  if (templates) {
19279
- this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
19568
+ this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0, biomeTint: context?.tint })));
19280
19569
  this.meshDirty = true;
19281
19570
  } else {
19282
19571
  perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
@@ -19360,12 +19649,35 @@ var Chunk = class _Chunk {
19360
19649
  }
19361
19650
  async resolveState(stored, indices, biomes) {
19362
19651
  const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
19363
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
19652
+ let perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
19364
19653
  const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
19365
19654
  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;
19366
19655
  const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
19367
19656
  const prepare = (color2) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color2);
19368
19657
  const contexts = /* @__PURE__ */ new Map();
19658
+ const blend = this.biomeBlend?.();
19659
+ if (blend && !perBlock && blockState3 && (source || fluid?.kind === "water")) {
19660
+ const prepared2 = fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties, void 0, true);
19661
+ if (!prepared2 && fluid?.renderModel !== false) {
19662
+ perBlock = true;
19663
+ } else {
19664
+ await Promise.all(indices.map(async (index) => {
19665
+ const position = this.biomePosition(index, blockState3, stored);
19666
+ const biomeColor = (kind) => blend.getColor(kind, blockState3.key, position);
19667
+ const context = {};
19668
+ if (source && prepared2?.biomeTint) {
19669
+ const color2 = await biomeColor(source);
19670
+ if (color2 !== void 0) context.tint = new import_three47.Color(color2).toArray();
19671
+ }
19672
+ if (fluid?.kind === "water") {
19673
+ const color2 = await biomeColor("water");
19674
+ if (color2 !== void 0 && color2 !== 4159204) context.waterTint = new import_three47.Color(color2).toArray();
19675
+ }
19676
+ contexts.set(index, context);
19677
+ }));
19678
+ return { blockState: blockState3, perBlock, fluid, prepared: prepared2, contexts, stored };
19679
+ }
19680
+ }
19369
19681
  const groups = /* @__PURE__ */ new Map();
19370
19682
  if (blockState3 && (source || fluid?.kind === "water")) {
19371
19683
  for (const index of indices) {
@@ -19413,7 +19725,11 @@ var Chunk = class _Chunk {
19413
19725
  this.waterTints.set(tint ?? WATER_TINT2, index * 3);
19414
19726
  }
19415
19727
  /** Changes visibility at world block coordinates while preserving the block's data. */
19416
- async setBlockVisibleAt(pos, visible) {
19728
+ setBlockVisibleAt(pos, visible) {
19729
+ const position = pos.clone();
19730
+ return this.changeBlocks(() => this.setBlockVisibility(position, visible));
19731
+ }
19732
+ async setBlockVisibility(pos, visible) {
19417
19733
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
19418
19734
  const block = this.renderedBlocks.get(index);
19419
19735
  if (!block || visible === !this.hiddenBlocks.has(index)) {
@@ -19583,6 +19899,7 @@ var Chunk = class _Chunk {
19583
19899
  }
19584
19900
  /** Removes stored blocks and render objects. Set `notify` to false to skip the owning world's update. */
19585
19901
  async clear(notify = true) {
19902
+ if (this.blockChanges.size) await Promise.allSettled(this.blockChanges);
19586
19903
  const positions = notify && this.onBlocksChanged ? [...this.renderedBlocks.keys()].map((index) => this.chunkPosToWorldPos(
19587
19904
  new import_three47.Vector3(index % 16, Math.floor(index / 256), Math.floor(index / 16) % 16)
19588
19905
  )) : [];