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.
package/dist/bundle.js CHANGED
@@ -41476,6 +41476,10 @@ void main() {
41476
41476
  init_esm2();
41477
41477
  init_esm();
41478
41478
  _Caching = class _Caching {
41479
+ /** Changes when shared assets are cleared, so derived caches can discard their old values. */
41480
+ static get generation() {
41481
+ return this._generation;
41482
+ }
41479
41483
  static createAssetCache() {
41480
41484
  return Caches.builder().expireAfterWrite(Time.minutes(10)).expireAfterAccess(Time.minutes(5)).expirationInterval(Time.seconds(30)).buildAsync();
41481
41485
  }
@@ -41511,6 +41515,7 @@ void main() {
41511
41515
  }
41512
41516
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
41513
41517
  static clear() {
41518
+ this._generation++;
41514
41519
  for (const cache of this.all) {
41515
41520
  cache.invalidateAll();
41516
41521
  }
@@ -41522,6 +41527,7 @@ void main() {
41522
41527
  * Use {@link shutdown} to also stop requests and the shared Ticker.
41523
41528
  */
41524
41529
  static end() {
41530
+ this._generation++;
41525
41531
  for (const cache of this.all) {
41526
41532
  cache.invalidateAll();
41527
41533
  cache.end();
@@ -41533,6 +41539,7 @@ void main() {
41533
41539
  };
41534
41540
  }
41535
41541
  };
41542
+ _Caching._generation = 0;
41536
41543
  _Caching.rawImageCache = Caches.builder().expireAfterWrite(Time.minutes(10)).expireAfterAccess(Time.minutes(5)).expirationInterval(Time.seconds(5)).buildAsync();
41537
41544
  _Caching.imageDataCache = Caches.builder().expireAfterWrite(Time.minutes(10)).expireAfterAccess(Time.minutes(5)).expirationInterval(Time.seconds(10)).buildAsync();
41538
41545
  _Caching.canvasImageDataCache = Caches.builder().expireAfterWrite(Time.minutes(10)).expireAfterAccess(Time.minutes(5)).expirationInterval(Time.seconds(10)).buildAsync();
@@ -107734,18 +107741,23 @@ gl_FragDepth=readDepth(vUv);
107734
107741
  return this.template(variant, choice, tints);
107735
107742
  }));
107736
107743
  }
107737
- /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
107738
- async prepareState(blockState3, properties = {}, biomeColor) {
107744
+ /** Resolves shared templates; dynamic biome tints are supplied on section placements instead of cached geometry. */
107745
+ async prepareState(blockState3, properties = {}, biomeColor, dynamicBiomeTint = false) {
107739
107746
  const prepared = await this.state(blockState3, properties);
107740
107747
  if (!prepared) return void 0;
107741
107748
  let color3;
107742
107749
  const resolveColor = typeof biomeColor === "function" ? () => color3 ?? (color3 = biomeColor()) : biomeColor;
107743
107750
  const templates = /* @__PURE__ */ new Map();
107751
+ const source = dynamicBiomeTint && await BlockTints.biomeSource(blockState3.key);
107744
107752
  await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
107745
107753
  const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
107746
- templates.set(variant, await this.template(variant, choice, tints));
107754
+ const biomeTints = source ? await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, 16777215) : void 0;
107755
+ templates.set(variant, await this.template(variant, choice, tints, biomeTints));
107747
107756
  }));
107748
- return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
107757
+ return {
107758
+ pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))),
107759
+ biomeTint: source ? [...templates.values()].some((template) => template.biomeTint?.some((value) => value === 1)) || void 0 : void 0
107760
+ };
107749
107761
  }
107750
107762
  async state(blockState3, properties) {
107751
107763
  let states = this.states.get(blockState3);
@@ -107788,10 +107800,10 @@ gl_FragDepth=readDepth(vUv);
107788
107800
  }
107789
107801
  return { state, groups, choices: prepared };
107790
107802
  }
107791
- async template(variant, { model, atlas, rotation }, tints) {
107803
+ async template(variant, { model, atlas, rotation }, tints, biomeTints) {
107792
107804
  let models = this.models.get(model);
107793
107805
  if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
107794
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
107806
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints, biomeTints]);
107795
107807
  return cached(models, key, async () => {
107796
107808
  const object4 = new SectionModel(model, {
107797
107809
  instanceMeshes: false,
@@ -107805,11 +107817,13 @@ gl_FragDepth=readDepth(vUv);
107805
107817
  object4.clear();
107806
107818
  const keep = [];
107807
107819
  const cullFaces = [];
107820
+ const biomeTint = biomeTints ? [] : void 0;
107808
107821
  for (const element of atlas.model.elements) {
107809
107822
  for (const face of CUBE_FACES) {
107810
107823
  const data = element.faces[face];
107811
107824
  keep.push(data !== void 0);
107812
107825
  if (!data) continue;
107826
+ biomeTint?.push(Number(data.tintindex !== void 0 && biomeTints?.[data.tintindex] !== void 0));
107813
107827
  const local = CUBE_FACES.findIndex((name2) => name2 === data.cullface);
107814
107828
  if (local < 0) {
107815
107829
  cullFaces.push(0);
@@ -107825,6 +107839,7 @@ gl_FragDepth=readDepth(vUv);
107825
107839
  geometry,
107826
107840
  atlas,
107827
107841
  cullFaces: new Uint8Array(cullFaces),
107842
+ biomeTint: biomeTint?.some(Boolean) ? new Uint8Array(biomeTint) : void 0,
107828
107843
  occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
107829
107844
  layer: atlas.hasTranslucency ? 1 : 0,
107830
107845
  ambientOcclusion: model.ambientocclusion !== false
@@ -108063,6 +108078,135 @@ gl_FragDepth=readDepth(vUv);
108063
108078
  // src/world/MineRenderWorld.ts
108064
108079
  init_FluidQuads();
108065
108080
 
108081
+ // src/world/_biomes/BiomeBlend.ts
108082
+ init_dirname();
108083
+ init_buffer2();
108084
+ init_process2();
108085
+ init_AssetKey();
108086
+ init_AssetLoader();
108087
+ init_Caching();
108088
+ var MAX_TILES = 256;
108089
+ function validateBiomeBlendRadius(radius) {
108090
+ if (!Number.isInteger(radius) || radius < 0 || radius > 7) {
108091
+ throw new RangeError("biomeBlendRadius must be an integer from 0 to 7");
108092
+ }
108093
+ }
108094
+ var BiomeBlend = class {
108095
+ constructor(biomeAt, radius) {
108096
+ this.biomeAt = biomeAt;
108097
+ this.tiles = /* @__PURE__ */ new Map();
108098
+ this.generation = Caching.generation;
108099
+ validateBiomeBlendRadius(radius);
108100
+ this._radius = radius;
108101
+ }
108102
+ get radius() {
108103
+ return this._radius;
108104
+ }
108105
+ clear() {
108106
+ this.tiles.clear();
108107
+ }
108108
+ invalidateColumn(x4, z4) {
108109
+ for (const [key, tile] of this.tiles) {
108110
+ if (Math.abs(tile.x - x4) <= 1 && Math.abs(tile.z - z4) <= 1) this.tiles.delete(key);
108111
+ }
108112
+ }
108113
+ async getColor(kind, origin, position) {
108114
+ const [x4, y5, z4] = position;
108115
+ const biome = this.biomeAt(x4, y5, z4);
108116
+ if (biome === void 0) return void 0;
108117
+ if (this.generation !== Caching.generation) {
108118
+ this.clear();
108119
+ this.generation = Caching.generation;
108120
+ }
108121
+ const tileX = Math.floor(x4 / 16), tileZ = Math.floor(z4 / 16), quartY = Math.floor(y5 / 4);
108122
+ const root = origin?.root ?? AssetLoader.ROOT;
108123
+ const key = JSON.stringify([root, AssetLoader.persistentScope, kind, this._radius, tileX, quartY, tileZ]);
108124
+ let tile = this.tiles.get(key);
108125
+ if (tile) {
108126
+ this.tiles.delete(key);
108127
+ this.tiles.set(key, tile);
108128
+ } else {
108129
+ const snapshot = new AssetKey("minecraft", "", void 0, void 0, "assets", ".json", root);
108130
+ const colors = this.buildTile(kind, snapshot, tileX, quartY, tileZ, this._radius).then((result2) => {
108131
+ if (!result2.complete && this.tiles.get(key)?.colors === colors) this.tiles.delete(key);
108132
+ return result2;
108133
+ }, (error2) => {
108134
+ if (this.tiles.get(key)?.colors === colors) this.tiles.delete(key);
108135
+ throw error2;
108136
+ });
108137
+ tile = { x: tileX, z: tileZ, colors };
108138
+ this.tiles.set(key, tile);
108139
+ if (this.tiles.size > MAX_TILES) this.tiles.delete(this.tiles.keys().next().value);
108140
+ }
108141
+ const result = await tile.colors;
108142
+ if (result.failures.has(biome)) throw result.failures.get(biome);
108143
+ const color3 = result.colors[(z4 - tileZ * 16) * 16 + x4 - tileX * 16];
108144
+ return color3 === -1 ? void 0 : color3;
108145
+ }
108146
+ async buildTile(kind, origin, tileX, quartY, tileZ, radius) {
108147
+ const size = 16 + radius * 2, stride = size + 1;
108148
+ const startX = tileX * 16 - radius, startZ = tileZ * 16 - radius;
108149
+ const quartX = Math.floor(startX / 4), quartZ = Math.floor(startZ / 4);
108150
+ const quartSize = Math.floor((startX + size - 1) / 4) - quartX + 1;
108151
+ const ids = new Array(quartSize * quartSize);
108152
+ const biomes = /* @__PURE__ */ new Map();
108153
+ for (let z4 = 0; z4 < quartSize; z4++) {
108154
+ for (let x4 = 0; x4 < quartSize; x4++) {
108155
+ const id = this.biomeAt((quartX + x4) * 4, quartY * 4, (quartZ + z4) * 4);
108156
+ ids[z4 * quartSize + x4] = id;
108157
+ if (id !== void 0 && !biomes.has(id)) {
108158
+ biomes.set(id, (async () => {
108159
+ const swamp = kind === "grass" && (await Biomes.get(id, origin))?.effects.grass_color_modifier === "swamp";
108160
+ return { color: swamp ? void 0 : await Biomes.getColor(id, kind, origin), swamp };
108161
+ })());
108162
+ }
108163
+ }
108164
+ }
108165
+ const failures = /* @__PURE__ */ new Map();
108166
+ const loaded = new Map(await Promise.all([...biomes].map(async ([id, color3]) => {
108167
+ try {
108168
+ return [id, await color3];
108169
+ } catch (error2) {
108170
+ failures.set(id, error2);
108171
+ return [id, { color: void 0, swamp: false }];
108172
+ }
108173
+ })));
108174
+ const complete = [...loaded.values()].every((value) => value.swamp || value.color !== void 0);
108175
+ const samples = new Int32Array(size * size).fill(-1);
108176
+ const sums = Array.from({ length: 4 }, () => new Uint32Array(stride * stride));
108177
+ for (let z4 = 0; z4 < size; z4++) {
108178
+ const row = [0, 0, 0, 0];
108179
+ for (let x4 = 0; x4 < size; x4++) {
108180
+ const id = ids[(Math.floor((startZ + z4) / 4) - quartZ) * quartSize + Math.floor((startX + x4) / 4) - quartX];
108181
+ const value = id === void 0 ? void 0 : loaded.get(id);
108182
+ const color3 = value?.swamp ? swampGrassColor(startX + x4, startZ + z4) : value?.color;
108183
+ if (color3 !== void 0) {
108184
+ samples[z4 * size + x4] = color3;
108185
+ row[0] += color3 >> 16 & 255;
108186
+ row[1] += color3 >> 8 & 255;
108187
+ row[2] += color3 & 255;
108188
+ row[3]++;
108189
+ }
108190
+ for (let channel = 0; channel < 4; channel++) {
108191
+ sums[channel][(z4 + 1) * stride + x4 + 1] = sums[channel][z4 * stride + x4 + 1] + row[channel];
108192
+ }
108193
+ }
108194
+ }
108195
+ const colors = new Int32Array(256).fill(-1), width = radius * 2 + 1;
108196
+ for (let z4 = 0; z4 < 16; z4++) {
108197
+ for (let x4 = 0; x4 < 16; x4++) {
108198
+ if (samples[(z4 + radius) * size + x4 + radius] === -1) continue;
108199
+ const topLeft = z4 * stride + x4, topRight = topLeft + width;
108200
+ const bottomLeft = (z4 + width) * stride + x4, bottomRight = bottomLeft + width;
108201
+ const sum = (values) => values[bottomRight] - values[bottomLeft] - values[topRight] + values[topLeft];
108202
+ const count = sum(sums[3]);
108203
+ if (count) colors[z4 * 16 + x4] = Math.floor(sum(sums[0]) / count) << 16 | Math.floor(sum(sums[1]) / count) << 8 | Math.floor(sum(sums[2]) / count);
108204
+ }
108205
+ }
108206
+ return { colors, complete, failures };
108207
+ }
108208
+ };
108209
+
108066
108210
  // src/world/WorldLight.ts
108067
108211
  init_dirname();
108068
108212
  init_buffer2();
@@ -108132,12 +108276,17 @@ gl_FragDepth=readDepth(vUv);
108132
108276
  this.lightValidity = /* @__PURE__ */ new Map();
108133
108277
  this.highestSkySection = /* @__PURE__ */ new Map();
108134
108278
  this.ambientOcclusionPending = false;
108279
+ this.pendingBiomes = /* @__PURE__ */ new Map();
108135
108280
  this.pendingCulling = /* @__PURE__ */ new Map();
108136
108281
  if (options.savedLighting && !options.sectionMeshing) throw new TypeError("savedLighting requires sectionMeshing");
108137
108282
  if (options.ambientOcclusion && !options.sectionMeshing) throw new TypeError("ambientOcclusion requires sectionMeshing");
108138
108283
  if (options.ambientLight !== void 0 && (!Number.isFinite(options.ambientLight) || options.ambientLight < 0 || options.ambientLight > 1)) {
108139
108284
  throw new RangeError("ambientLight must be a number from 0 to 1");
108140
108285
  }
108286
+ this._biomeBlendRadius = options.biomeBlendRadius ?? 0;
108287
+ validateBiomeBlendRadius(this._biomeBlendRadius);
108288
+ if (this._biomeBlendRadius > 0 && !options.sectionMeshing) throw new TypeError("biomeBlendRadius requires sectionMeshing");
108289
+ if (this._biomeBlendRadius) this.biomeBlend = new BiomeBlend((x4, y5, z4) => this.getBiomeAt(x4, y5, z4), this._biomeBlendRadius);
108141
108290
  this.scene = scene;
108142
108291
  this.savedLighting = options.savedLighting ?? false;
108143
108292
  this.ambientLight = options.ambientLight ?? 0;
@@ -108178,6 +108327,65 @@ gl_FragDepth=readDepth(vUv);
108178
108327
  await this.updateCulling([]);
108179
108328
  this.ambientOcclusionPending = false;
108180
108329
  }
108330
+ /** Horizontal biome-color sampling radius, from 0 to 7 blocks. Zero disables blending. */
108331
+ get biomeBlendRadius() {
108332
+ return this._biomeBlendRadius;
108333
+ }
108334
+ /** Refreshes loaded grass, foliage, and water colors without reloading their source chunks. */
108335
+ async setBiomeBlendRadius(radius) {
108336
+ validateBiomeBlendRadius(radius);
108337
+ if (radius > 0 && !this.sectionModels) throw new TypeError("biomeBlendRadius requires sectionMeshing");
108338
+ if (radius !== this._biomeBlendRadius) {
108339
+ this._biomeBlendRadius = radius;
108340
+ this.biomeBlend?.clear();
108341
+ this.biomeBlend = radius ? new BiomeBlend((x4, y5, z4) => this.getBiomeAt(x4, y5, z4), radius) : void 0;
108342
+ for (const chunk of this._chunks.values()) this.pendingBiomes.set(chunk, void 0);
108343
+ }
108344
+ const failures = await this.refreshBiomes();
108345
+ if (failures.size) throw failures.values().next().value;
108346
+ }
108347
+ markBiomesChanged(x4, z4, y5, includeColumn = true) {
108348
+ const queued = [];
108349
+ if (!this.biomeBlend) return queued;
108350
+ this.biomeBlend.invalidateColumn(x4, z4);
108351
+ for (const chunk of this._chunks.values()) {
108352
+ if (Math.abs(chunk.x - x4) > 1 || Math.abs(chunk.z - z4) > 1 || !includeColumn && chunk.x === x4 && chunk.z === z4 || y5 !== void 0 && chunk.y !== y5 && chunk.y !== y5 + 1) continue;
108353
+ queued.push(chunk);
108354
+ if (!this.pendingBiomes.has(chunk)) this.pendingBiomes.set(chunk, []);
108355
+ const columns = this.pendingBiomes.get(chunk);
108356
+ if (columns && !columns.some((column) => column.x === x4 && column.z === z4)) columns.push({ x: x4, z: z4 });
108357
+ }
108358
+ return queued;
108359
+ }
108360
+ async refreshBiomes() {
108361
+ if (this.biomeRefreshing) await this.biomeRefreshing;
108362
+ if (!this.pendingBiomes.size) return /* @__PURE__ */ new Map();
108363
+ return this.biomeRefreshing ?? (this.biomeRefreshing = Promise.resolve().then(async () => {
108364
+ const failures = /* @__PURE__ */ new Map();
108365
+ const batch = [...this.pendingBiomes.keys()];
108366
+ let sliceStart = performance.now();
108367
+ try {
108368
+ for (const chunk of batch) {
108369
+ const columns = this.pendingBiomes.get(chunk);
108370
+ this.pendingBiomes.delete(chunk);
108371
+ if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) !== chunk) continue;
108372
+ try {
108373
+ await chunk.refreshBiomes(columns);
108374
+ } catch (error2) {
108375
+ failures.set(chunk, error2);
108376
+ if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
108377
+ }
108378
+ if (performance.now() - sliceStart > 8) {
108379
+ await yieldToEventLoop();
108380
+ sliceStart = performance.now();
108381
+ }
108382
+ }
108383
+ return failures;
108384
+ } finally {
108385
+ this.biomeRefreshing = void 0;
108386
+ }
108387
+ }));
108388
+ }
108181
108389
  getBlockAt(posOrX, y5, z4) {
108182
108390
  if (typeof posOrX == "number") {
108183
108391
  return this.getBlockAt(new Vector3(posOrX, y5, z4));
@@ -108413,6 +108621,9 @@ gl_FragDepth=readDepth(vUv);
108413
108621
  }
108414
108622
  await placeEntities?.();
108415
108623
  } finally {
108624
+ this.markBiomesChanged(chunk.x, chunk.z, void 0, false);
108625
+ const failures = await this.refreshBiomes();
108626
+ if (failures.size) console.warn("Failed to refresh biome colors", failures.values().next().value);
108416
108627
  await this.updateCulling([...changes.values()]);
108417
108628
  }
108418
108629
  }
@@ -108429,11 +108640,14 @@ gl_FragDepth=readDepth(vUv);
108429
108640
  try {
108430
108641
  await this.clearChunkColumn(x4, z4);
108431
108642
  } finally {
108643
+ const failures = await this.refreshBiomes();
108644
+ if (failures.size) console.warn("Failed to refresh biome colors", failures.values().next().value);
108432
108645
  await this.updateCulling([]);
108433
108646
  }
108434
108647
  }
108435
108648
  async clearChunkColumn(x4, z4) {
108436
108649
  this.biomes.delete(`${x4}_${z4}`);
108650
+ this.markBiomesChanged(x4, z4, void 0, false);
108437
108651
  if (this.savedLighting) {
108438
108652
  for (const y5 of this.light.get(`${x4}_${z4}`)?.keys() ?? []) this.markSectionChanged({ x: x4, y: y5, z: z4 });
108439
108653
  }
@@ -108444,6 +108658,7 @@ gl_FragDepth=readDepth(vUv);
108444
108658
  for (const [key, section] of previous) {
108445
108659
  this._chunks.delete(key);
108446
108660
  this.pendingCulling.delete(section);
108661
+ this.pendingBiomes.delete(section);
108447
108662
  await section.clear(false);
108448
108663
  this.markSectionChanged(section);
108449
108664
  }
@@ -108478,6 +108693,8 @@ gl_FragDepth=readDepth(vUv);
108478
108693
  const chunks = [...this._chunks.values()];
108479
108694
  this._chunks.clear();
108480
108695
  this.biomes.clear();
108696
+ this.biomeBlend?.clear();
108697
+ this.pendingBiomes.clear();
108481
108698
  this.light.clear();
108482
108699
  this.lightValidity.clear();
108483
108700
  this.highestSkySection.clear();
@@ -108596,8 +108813,14 @@ gl_FragDepth=readDepth(vUv);
108596
108813
  column?.delete(y5);
108597
108814
  if (!column?.size) this.biomes.delete(columnKey);
108598
108815
  }
108599
- await chunk.refreshBiomes();
108600
- await this._chunks.get(`${x4}_${y5 + 1}_${z4}`)?.refreshBiomes();
108816
+ if (this.biomeBlend) {
108817
+ const queued = this.markBiomesChanged(x4, z4, y5);
108818
+ const failures = await this.refreshBiomes();
108819
+ for (const chunk2 of queued) if (failures.has(chunk2)) throw failures.get(chunk2);
108820
+ } else {
108821
+ await chunk.refreshBiomes();
108822
+ await this._chunks.get(`${x4}_${y5 + 1}_${z4}`)?.refreshBiomes();
108823
+ }
108601
108824
  },
108602
108825
  this.savedLighting ? (x5, y6, z5) => this.lightLevelAt(x5, y6, z5) : void 0,
108603
108826
  async () => {
@@ -108614,7 +108837,8 @@ gl_FragDepth=readDepth(vUv);
108614
108837
  await this.updateCulling([]);
108615
108838
  }
108616
108839
  },
108617
- this.ambientLight
108840
+ this.ambientLight,
108841
+ () => this.biomeBlend
108618
108842
  );
108619
108843
  this._chunks.set(key, chunk);
108620
108844
  }
@@ -108745,9 +108969,10 @@ gl_FragDepth=readDepth(vUv);
108745
108969
 
108746
108970
  // src/world/SectionGeometry.ts
108747
108971
  function buildSectionGeometry(input) {
108748
- const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion } = input;
108972
+ const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion, biomeTints } = input;
108749
108973
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
108750
108974
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
108975
+ if (biomeTints && biomeTints.length !== count * 3) throw new RangeError("Section biome tints must contain RGB for each placement");
108751
108976
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
108752
108977
  if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
108753
108978
  if (light && (light.length !== 5832 || light.some((level) => level > 15 && level !== 255))) {
@@ -108762,7 +108987,7 @@ gl_FragDepth=readDepth(vUv);
108762
108987
  const cornerBrightness2 = ambientOcclusion ? new Float32Array(4) : void 0;
108763
108988
  for (const template of templateData) {
108764
108989
  const { quads } = template;
108765
- 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) {
108990
+ 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) {
108766
108991
  throw new RangeError("Section template arrays do not match its quad count");
108767
108992
  }
108768
108993
  }
@@ -108872,6 +109097,7 @@ gl_FragDepth=readDepth(vUv);
108872
109097
  blockIndices[indexOffset / 6] = indices[entry];
108873
109098
  if (ambientMask) quadAmbientOcclusion(template.positions, template.normals, quad, ambientMask, cornerBrightness2);
108874
109099
  const brightness = light ? lightBrightness(light[lightIndex + LIGHT_SAMPLE_OFFSETS[quadLightIndex(template.positions, template.normals, quad)]], ambientLight) : 1;
109100
+ const tint = biomeTints && template.biomeTint?.[quad] && !Number.isNaN(biomeTints[entry * 3]) ? entry * 3 : void 0;
108875
109101
  const start = vertex2;
108876
109102
  for (let corner = 0; corner < 4; corner++, vertex2++) {
108877
109103
  const source = quad * 4 + corner;
@@ -108888,9 +109114,9 @@ gl_FragDepth=readDepth(vUv);
108888
109114
  uvBounds[vertex2 * 4 + 2] = mapU(template.uvBounds[source * 4 + 2]);
108889
109115
  uvBounds[vertex2 * 4 + 3] = mapV(template.uvBounds[source * 4 + 3]);
108890
109116
  const factor = brightness * (ambientMask ? cornerBrightness2[corner] : 1);
108891
- colors[vertex2 * 3] = template.colors[source * 3] * factor;
108892
- colors[vertex2 * 3 + 1] = template.colors[source * 3 + 1] * factor;
108893
- colors[vertex2 * 3 + 2] = template.colors[source * 3 + 2] * factor;
109117
+ colors[vertex2 * 3] = (tint === void 0 ? template.colors[source * 3] : biomeTints[tint]) * factor;
109118
+ colors[vertex2 * 3 + 1] = (tint === void 0 ? template.colors[source * 3 + 1] : biomeTints[tint + 1]) * factor;
109119
+ colors[vertex2 * 3 + 2] = (tint === void 0 ? template.colors[source * 3 + 2] : biomeTints[tint + 2]) * factor;
108894
109120
  }
108895
109121
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
108896
109122
  }
@@ -109014,7 +109240,11 @@ gl_FragDepth=readDepth(vUv);
109014
109240
  ...lighting
109015
109241
  };
109016
109242
  for (let i4 = 0; i4 < entries.length; i4++) {
109017
- const { index, template, cullMask } = entries[i4];
109243
+ const { index, template, cullMask, biomeTint } = entries[i4];
109244
+ if (biomeTint) {
109245
+ input.biomeTints ?? (input.biomeTints = new Float32Array(entries.length * 3).fill(NaN));
109246
+ input.biomeTints.set(biomeTint, i4 * 3);
109247
+ }
109018
109248
  let id = templateIds.get(template);
109019
109249
  if (id === void 0) {
109020
109250
  let atlas = atlasIds.get(template.atlas);
@@ -109041,6 +109271,7 @@ gl_FragDepth=readDepth(vUv);
109041
109271
  colors: new Float32Array(quads * 12),
109042
109272
  indices: new Uint16Array(quads * 6),
109043
109273
  cullFaces: new Uint8Array(template.cullFaces),
109274
+ biomeTint: template.biomeTint ? new Uint8Array(template.biomeTint) : void 0,
109044
109275
  atlas: 0,
109045
109276
  layer: template.layer,
109046
109277
  ambientOcclusion: template.ambientOcclusion
@@ -109185,7 +109416,7 @@ gl_FragDepth=readDepth(vUv);
109185
109416
  init_InstanceReference();
109186
109417
  var WATER_TINT2 = new Color(4159204).toArray();
109187
109418
  var Chunk = class _Chunk {
109188
- constructor(scene, x4, y5, z4, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged, lightAt, onLightCleared, ambientLight = 0) {
109419
+ constructor(scene, x4, y5, z4, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged, lightAt, onLightCleared, ambientLight = 0, biomeBlend) {
109189
109420
  this.onBlocksChanged = onBlocksChanged;
109190
109421
  this.sectionModels = sectionModels;
109191
109422
  this.neighborCell = neighborCell;
@@ -109194,6 +109425,7 @@ gl_FragDepth=readDepth(vUv);
109194
109425
  this.lightAt = lightAt;
109195
109426
  this.onLightCleared = onLightCleared;
109196
109427
  this.ambientLight = ambientLight;
109428
+ this.biomeBlend = biomeBlend;
109197
109429
  this.data = new ChunkData();
109198
109430
  this.renderedBlocks = /* @__PURE__ */ new Map();
109199
109431
  this.objectBlocks = /* @__PURE__ */ new Set();
@@ -109203,6 +109435,7 @@ gl_FragDepth=readDepth(vUv);
109203
109435
  this.blockPosition = new Vector3();
109204
109436
  this.meshDirty = false;
109205
109437
  this.meshGeneration = 0;
109438
+ this.blockChanges = /* @__PURE__ */ new Set();
109206
109439
  this.scene = scene;
109207
109440
  this.x = x4;
109208
109441
  this.y = y5;
@@ -109301,10 +109534,20 @@ gl_FragDepth=readDepth(vUv);
109301
109534
  return this.refreshBiomes();
109302
109535
  }
109303
109536
  /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
109304
- async refreshBiomes() {
109537
+ refreshBiomes(columns) {
109538
+ return this.changeBlocks(() => this.refreshBiomeBlocks(columns), true);
109539
+ }
109540
+ async refreshBiomeBlocks(columns) {
109541
+ const blend = columns && this.biomeBlend?.();
109542
+ const ownColumn = columns?.some((column) => column.x === this.x && column.z === this.z);
109305
109543
  const affected = /* @__PURE__ */ new Map();
109306
109544
  const blocks = [];
109307
109545
  for (const index of this.renderedBlocks.keys()) {
109546
+ if (blend) {
109547
+ if (this.objectBlocks.has(index) && !ownColumn) continue;
109548
+ const [x4, , z4] = this.positionAt(index), radius = blend.radius;
109549
+ if (!columns.some((column) => x4 >= column.x * 16 - radius && x4 <= column.x * 16 + 15 + radius && z4 >= column.z * 16 - radius && z4 <= column.z * 16 + 15 + radius)) continue;
109550
+ }
109308
109551
  const block = this.data.get(index);
109309
109552
  const stateKey = ChunkData.paletteKey(block);
109310
109553
  let matches = affected.get(stateKey);
@@ -109319,10 +109562,32 @@ gl_FragDepth=readDepth(vUv);
109319
109562
  }
109320
109563
  if (await matches) blocks.push({ index, block });
109321
109564
  }
109565
+ if (blend) {
109566
+ const groups = /* @__PURE__ */ new Map();
109567
+ for (const entry of blocks) {
109568
+ const key = ChunkData.paletteKey(entry.block);
109569
+ let group = groups.get(key);
109570
+ if (!group) groups.set(key, group = []);
109571
+ group.push(entry);
109572
+ }
109573
+ blocks.length = 0;
109574
+ const biomes = /* @__PURE__ */ new Map();
109575
+ for (const group of groups.values()) {
109576
+ const { contexts, fluid } = await this.resolveState(group[0].block, group.map((entry) => entry.index), biomes);
109577
+ for (const entry of group) {
109578
+ const context = contexts.get(entry.index);
109579
+ const current = this.sectionBlocks.get(entry.index)?.[0]?.biomeTint;
109580
+ const tint = context?.tint;
109581
+ const tintChanged = !!current !== !!tint || tint?.some((value, channel) => value !== current?.[channel]);
109582
+ 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])));
109583
+ if (this.objectBlocks.has(entry.index) || tintChanged || waterChanged) blocks.push(entry);
109584
+ }
109585
+ }
109586
+ }
109322
109587
  if (!blocks.length) return;
109323
109588
  const hidden = new Set(this.hiddenBlocks);
109324
109589
  try {
109325
- await this.placeBlocks(blocks);
109590
+ await this.replaceBlocks(blocks, true);
109326
109591
  } finally {
109327
109592
  for (const index of hidden) {
109328
109593
  this.hiddenBlocks.add(index);
@@ -109362,6 +109627,20 @@ gl_FragDepth=readDepth(vUv);
109362
109627
  }
109363
109628
  /** Places cells in input order and returns changed world positions without refreshing neighbors. */
109364
109629
  async placeBlocks(blocks) {
109630
+ return this.changeBlocks(() => this.replaceBlocks(blocks));
109631
+ }
109632
+ changeBlocks(change, refreshBiomes = false) {
109633
+ const before = refreshBiomes ? Promise.allSettled(this.blockChanges) : this.biomeChange;
109634
+ const result = before ? before.catch(() => void 0).then(change) : change();
109635
+ const pending = result.finally(() => {
109636
+ this.blockChanges.delete(pending);
109637
+ if (this.biomeChange === pending) this.biomeChange = void 0;
109638
+ });
109639
+ this.blockChanges.add(pending);
109640
+ if (refreshBiomes) this.biomeChange = pending;
109641
+ return pending;
109642
+ }
109643
+ async replaceBlocks(blocks, preserveDataOnFailure = false) {
109365
109644
  const placements = blocks.map(({ index, block }) => ({
109366
109645
  index,
109367
109646
  key: block && !ChunkData.isAir(block) ? ChunkData.paletteKey(block) : void 0,
@@ -109381,6 +109660,10 @@ gl_FragDepth=readDepth(vUv);
109381
109660
  const biomes = /* @__PURE__ */ new Map();
109382
109661
  const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
109383
109662
  const resolutions = new Map([...states.keys()].map((key, i4) => [key, settled[i4]]));
109663
+ if (preserveDataOnFailure) {
109664
+ const rejected = settled.find((result) => result.status === "rejected");
109665
+ if (rejected?.status === "rejected") throw rejected.reason;
109666
+ }
109384
109667
  const positions = [];
109385
109668
  let failed = false, failure;
109386
109669
  for (const { index, key, block } of placements) {
@@ -109418,7 +109701,7 @@ gl_FragDepth=readDepth(vUv);
109418
109701
  }
109419
109702
  const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
109420
109703
  if (templates) {
109421
- this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
109704
+ this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0, biomeTint: context?.tint })));
109422
109705
  this.meshDirty = true;
109423
109706
  } else {
109424
109707
  object4 = await this.scene.addBlock(blockState3, {
@@ -109437,7 +109720,11 @@ gl_FragDepth=readDepth(vUv);
109437
109720
  }, object: object4 });
109438
109721
  if (object4) this.objectBlocks.add(index);
109439
109722
  } catch (error2) {
109440
- this.data.assign(index, 0);
109723
+ if (preserveDataOnFailure && readBlock) {
109724
+ this.renderedBlocks.set(index, { get block() {
109725
+ return readBlock();
109726
+ }, object: void 0 });
109727
+ } else this.data.assign(index, 0);
109441
109728
  this.fluidCells[index] = 0;
109442
109729
  object4?.removeFromScene();
109443
109730
  if (!failed) failure = error2;
@@ -109448,7 +109735,10 @@ gl_FragDepth=readDepth(vUv);
109448
109735
  return positions;
109449
109736
  }
109450
109737
  /** Replaces every cell from parsed section data. The caller refreshes neighbor culling. */
109451
- async placeSection(data) {
109738
+ placeSection(data) {
109739
+ return this.changeBlocks(() => this.replaceSection(data));
109740
+ }
109741
+ async replaceSection(data) {
109452
109742
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
109453
109743
  this.sectionBlocks.clear();
109454
109744
  this.fluidCells.fill(0);
@@ -109495,7 +109785,7 @@ gl_FragDepth=readDepth(vUv);
109495
109785
  ];
109496
109786
  const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
109497
109787
  if (templates) {
109498
- this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
109788
+ this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0, biomeTint: context?.tint })));
109499
109789
  this.meshDirty = true;
109500
109790
  } else {
109501
109791
  perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
@@ -109579,12 +109869,35 @@ gl_FragDepth=readDepth(vUv);
109579
109869
  }
109580
109870
  async resolveState(stored, indices, biomes) {
109581
109871
  const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
109582
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
109872
+ let perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
109583
109873
  const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
109584
109874
  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;
109585
109875
  const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
109586
109876
  const prepare = (color3) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color3);
109587
109877
  const contexts = /* @__PURE__ */ new Map();
109878
+ const blend = this.biomeBlend?.();
109879
+ if (blend && !perBlock && blockState3 && (source || fluid?.kind === "water")) {
109880
+ const prepared2 = fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties, void 0, true);
109881
+ if (!prepared2 && fluid?.renderModel !== false) {
109882
+ perBlock = true;
109883
+ } else {
109884
+ await Promise.all(indices.map(async (index) => {
109885
+ const position = this.biomePosition(index, blockState3, stored);
109886
+ const biomeColor = (kind) => blend.getColor(kind, blockState3.key, position);
109887
+ const context = {};
109888
+ if (source && prepared2?.biomeTint) {
109889
+ const color3 = await biomeColor(source);
109890
+ if (color3 !== void 0) context.tint = new Color(color3).toArray();
109891
+ }
109892
+ if (fluid?.kind === "water") {
109893
+ const color3 = await biomeColor("water");
109894
+ if (color3 !== void 0 && color3 !== 4159204) context.waterTint = new Color(color3).toArray();
109895
+ }
109896
+ contexts.set(index, context);
109897
+ }));
109898
+ return { blockState: blockState3, perBlock, fluid, prepared: prepared2, contexts, stored };
109899
+ }
109900
+ }
109588
109901
  const groups = /* @__PURE__ */ new Map();
109589
109902
  if (blockState3 && (source || fluid?.kind === "water")) {
109590
109903
  for (const index of indices) {
@@ -109632,7 +109945,11 @@ gl_FragDepth=readDepth(vUv);
109632
109945
  this.waterTints.set(tint ?? WATER_TINT2, index * 3);
109633
109946
  }
109634
109947
  /** Changes visibility at world block coordinates while preserving the block's data. */
109635
- async setBlockVisibleAt(pos, visible) {
109948
+ setBlockVisibleAt(pos, visible) {
109949
+ const position = pos.clone();
109950
+ return this.changeBlocks(() => this.setBlockVisibility(position, visible));
109951
+ }
109952
+ async setBlockVisibility(pos, visible) {
109636
109953
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
109637
109954
  const block = this.renderedBlocks.get(index);
109638
109955
  if (!block || visible === !this.hiddenBlocks.has(index)) {
@@ -109802,6 +110119,7 @@ gl_FragDepth=readDepth(vUv);
109802
110119
  }
109803
110120
  /** Removes stored blocks and render objects. Set `notify` to false to skip the owning world's update. */
109804
110121
  async clear(notify = true) {
110122
+ if (this.blockChanges.size) await Promise.allSettled(this.blockChanges);
109805
110123
  const positions = notify && this.onBlocksChanged ? [...this.renderedBlocks.keys()].map((index) => this.chunkPosToWorldPos(
109806
110124
  new Vector3(index % 16, Math.floor(index / 256), Math.floor(index / 16) % 16)
109807
110125
  )) : [];