minerender 2.1.0-alpha.115 → 2.1.0-alpha.116

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.
@@ -279,7 +279,8 @@ var init_Caching = __esm({
279
279
  this.fontCache,
280
280
  this.bannerPatternCache,
281
281
  this.mineRenderDataCache,
282
- this.usernameUuidCache
282
+ this.usernameUuidCache,
283
+ this.biomeCache
283
284
  ];
284
285
  }
285
286
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
@@ -330,6 +331,7 @@ var init_Caching = __esm({
330
331
  _Caching.bannerPatternCache = _Caching.createAssetCache();
331
332
  _Caching.mineRenderDataCache = _Caching.createAssetCache();
332
333
  _Caching.usernameUuidCache = Caches.builder().expireAfterWrite(Time.minutes(5)).expirationInterval(Time.seconds(30)).buildAsync();
334
+ _Caching.biomeCache = _Caching.createAssetCache();
333
335
  Caching = _Caching;
334
336
  }
335
337
  });
@@ -1676,7 +1678,7 @@ ${key}` : key, async () => {
1676
1678
  case "blockStates":
1677
1679
  return Object.values(value).every((property) => object(property) && "default" in property && Array.isArray(property.values));
1678
1680
  case "blockTints":
1679
- return ["constant", "grass", "stem", "redstone"].includes(value.source) && (value.source !== "constant" || Number.isInteger(value.color));
1681
+ return ["constant", "grass", "stem", "redstone"].includes(value.source) && (value.source !== "constant" || Number.isInteger(value.color)) && (value.biome === void 0 || ["grass", "foliage", "dry_foliage", "water"].includes(value.biome));
1680
1682
  case "fluids":
1681
1683
  return ["water", "lava"].includes(value.kind) && typeof value.renderModel === "boolean" && (value.levelProperty === void 0 || typeof value.levelProperty === "string");
1682
1684
  case "dyes":
@@ -1820,15 +1822,19 @@ var init_Colormaps = __esm({
1820
1822
  Colormaps = class {
1821
1823
  /** Samples the selected resource pack's grass colormap using Minecraft's climate coordinates. */
1822
1824
  static async grassColor(origin, temperature = 0.5, downfall = 1) {
1823
- const key = AssetKey.parse("textures", "minecraft:colormap/grass", origin);
1825
+ return this.getColor("grass", origin, temperature, downfall);
1826
+ }
1827
+ /** Samples a resource-pack colormap at the supplied climate coordinates. */
1828
+ static async getColor(kind, origin, temperature = 0.5, downfall = 1) {
1829
+ const key = AssetKey.parse("textures", `minecraft:colormap/${kind}`, origin);
1824
1830
  const temp = Math.fround(temperature);
1825
1831
  const rain = Math.fround(downfall) * temp;
1826
1832
  const x = Math.floor((1 - temp) * 255), y = Math.floor((1 - rain) * 255);
1827
1833
  return await Caching.blockTintCache.get(`${key.serialize()}|${x},${y}`, async () => {
1828
1834
  const image = await ModelTextures.get(key);
1829
- if (!image) throw new MineRenderError(`Missing grass colormap: ${key.toNamespacedString()}`);
1835
+ if (!image) throw new MineRenderError(`Missing ${kind} colormap: ${key.toNamespacedString()}`);
1830
1836
  if (image.width !== 256 || image.height !== 256) {
1831
- throw new MineRenderError("Grass colormaps must be 256 \xD7 256 pixels");
1837
+ throw new MineRenderError(`${kind} colormaps must be 256 \xD7 256 pixels`);
1832
1838
  }
1833
1839
  const [red, green, blue] = image.data.getImageData(x, y, 1, 1).data;
1834
1840
  return red << 16 | green << 8 | blue;
@@ -7699,6 +7705,116 @@ init_AssetLoader();
7699
7705
  init_Assets();
7700
7706
  init_BannerPatterns();
7701
7707
 
7708
+ // src/assets/Biomes.ts
7709
+ init_AssetKey();
7710
+ init_AssetLoader();
7711
+ init_AssetParsers();
7712
+ init_Caching();
7713
+ init_MineRenderError();
7714
+ init_Colormaps();
7715
+
7716
+ // src/assets/_biomes/SwampGrassColor.ts
7717
+ var permutation;
7718
+ var gradients = [[1, 1], [-1, 1], [1, -1], [-1, -1], [1, 0], [-1, 0], [1, 0], [-1, 0], [0, 1], [0, -1], [0, 1], [0, -1]];
7719
+ var skew = (Math.sqrt(3) - 1) / 2;
7720
+ var unskew = (3 - Math.sqrt(3)) / 6;
7721
+ function createPermutation() {
7722
+ const multiplier = 0x5deece66dn, mask = (1n << 48n) - 1n;
7723
+ let seed = 2345n ^ multiplier;
7724
+ const next = (bits) => {
7725
+ seed = seed * multiplier + 11n & mask;
7726
+ return Number(seed >> BigInt(48 - bits));
7727
+ };
7728
+ for (let i = 0; i < 3; i++) {
7729
+ next(26);
7730
+ next(27);
7731
+ }
7732
+ const values = Uint8Array.from({ length: 256 }, (_, i) => i);
7733
+ for (let i = 0; i < 256; i++) {
7734
+ const bound = 256 - i;
7735
+ let offset;
7736
+ if ((bound & bound - 1) === 0) offset = Math.floor(bound * next(31) / 2147483648);
7737
+ else {
7738
+ let bits;
7739
+ do {
7740
+ bits = next(31);
7741
+ offset = bits % bound;
7742
+ } while (bits - offset + bound - 1 > 2147483647);
7743
+ }
7744
+ const other = i + offset;
7745
+ [values[i], values[other]] = [values[other], values[i]];
7746
+ }
7747
+ return values;
7748
+ }
7749
+ function swampGrassColor(x, z) {
7750
+ const p10 = permutation ?? (permutation = createPermutation());
7751
+ x *= 0.0225;
7752
+ z *= 0.0225;
7753
+ const s = (x + z) * skew;
7754
+ const i = Math.floor(x + s), j = Math.floor(z + s);
7755
+ const t = (i + j) * unskew;
7756
+ const dx = x - (i - t), dz = z - (j - t);
7757
+ const stepX = dx > dz ? 1 : 0, stepZ = 1 - stepX;
7758
+ const corner = (gx, gz, a, b) => {
7759
+ let falloff = 0.5 - a * a - b * b;
7760
+ if (falloff < 0) return 0;
7761
+ const gradient = gradients[p10[gx + p10[gz & 255] & 255] % 12];
7762
+ falloff *= falloff;
7763
+ return falloff * falloff * (gradient[0] * a + gradient[1] * b);
7764
+ };
7765
+ const noise = 70 * (corner(i, j, dx, dz) + corner(i + stepX, j + stepZ, dx - stepX + unskew, dz - stepZ + unskew) + corner(i + 1, j + 1, dx - 1 + 2 * unskew, dz - 1 + 2 * unskew));
7766
+ return noise < -0.1 ? 5011004 : 6975545;
7767
+ }
7768
+
7769
+ // src/assets/Biomes.ts
7770
+ var Biomes = class {
7771
+ /** Loads a biome registry entry, or returns `undefined` when no source provides it. */
7772
+ static async get(id, origin) {
7773
+ if (!/^(?:[a-z0-9_.-]+:)?[a-z0-9_./-]+$/.test(id)) throw new MineRenderError(`Invalid biome identifier: ${id}`);
7774
+ const [namespace, path] = id.includes(":") ? id.split(":") : ["minecraft", id];
7775
+ const key = new AssetKey(namespace, path, "worldgen", "biome", "data", ".json", origin?.root);
7776
+ return Caching.biomeCache.get(key.serialize(), async () => {
7777
+ const asset2 = await AssetLoader.get(key, "json" /* JSON */);
7778
+ if (asset2 === void 0) return void 0;
7779
+ if (!asset2 || typeof asset2 !== "object" || Array.isArray(asset2) || !asset2.effects || typeof asset2.effects !== "object" || Array.isArray(asset2.effects)) {
7780
+ throw new MineRenderError(`Invalid biome effects: ${id}`);
7781
+ }
7782
+ if (typeof asset2.temperature !== "number" || !Number.isFinite(asset2.temperature) || typeof asset2.downfall !== "number" || !Number.isFinite(asset2.downfall)) {
7783
+ throw new MineRenderError(`Invalid biome climate: ${id}`);
7784
+ }
7785
+ for (const kind of ["grass", "foliage", "dry_foliage", "water"]) {
7786
+ const color2 = asset2.effects[`${kind}_color`];
7787
+ if (color2 !== void 0) this.parseColor(color2, id, kind);
7788
+ }
7789
+ const modifier = asset2.effects.grass_color_modifier;
7790
+ if (modifier !== void 0 && modifier !== "none" && modifier !== "dark_forest" && modifier !== "swamp") {
7791
+ throw new MineRenderError(`Invalid biome grass color modifier: ${id}`);
7792
+ }
7793
+ return asset2;
7794
+ });
7795
+ }
7796
+ /** Returns packed sRGB, or `undefined` when the biome or requested water color is missing. */
7797
+ static async getColor(id, kind, origin, x = 0, z = 0) {
7798
+ const biome = await this.get(id, origin);
7799
+ if (!biome) return void 0;
7800
+ const rawColor = biome.effects[`${kind}_color`];
7801
+ let color2 = rawColor === void 0 ? void 0 : this.parseColor(rawColor, id, kind);
7802
+ if (kind === "water") return color2;
7803
+ const modifier = kind === "grass" ? biome.effects.grass_color_modifier : void 0;
7804
+ if (modifier === "swamp") return swampGrassColor(x, z);
7805
+ if (color2 === void 0) {
7806
+ const clamp = (value) => Math.max(0, Math.min(1, Math.fround(value)));
7807
+ color2 = await Colormaps.getColor(kind, origin, clamp(biome.temperature), clamp(biome.downfall));
7808
+ }
7809
+ return modifier === "dark_forest" ? (color2 & 16711422) + 2634762 >> 1 : color2;
7810
+ }
7811
+ static parseColor(value, id, kind) {
7812
+ if (typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value)) return parseInt(value.slice(1), 16);
7813
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 16777215) return value;
7814
+ throw new MineRenderError(`Invalid biome ${kind} color: ${id}`);
7815
+ }
7816
+ };
7817
+
7702
7818
  // src/assets/BlockEntities.ts
7703
7819
  init_AssetKey();
7704
7820
  init_AssetLoader();
@@ -12178,8 +12294,20 @@ var BlockStateResolver = class _BlockStateResolver {
12178
12294
  init_Colormaps();
12179
12295
  init_MineRenderData();
12180
12296
  var BlockTints = class {
12181
- /** Resolves block preview colors without biome context; explicit tints take precedence. */
12182
- static async get(key, state, model, tints) {
12297
+ /** Returns a block's versioned biome color source, leaving fixed-color blocks unchanged. */
12298
+ static async biomeSource(key) {
12299
+ const rule = key && (await MineRenderData.get("blockTints", key.root))[key.toNamespacedString()];
12300
+ return rule ? rule.biome ?? (rule.source === "grass" ? "grass" : void 0) : void 0;
12301
+ }
12302
+ /** Resolves one block's biome tint at world block coordinates, without biome blending. */
12303
+ static async getBiomeColor(key, biome, position) {
12304
+ if (!biome) return void 0;
12305
+ const x = position?.[0], z = position?.[2];
12306
+ const source = await this.biomeSource(key);
12307
+ return source ? Biomes.getColor(biome, source, key, x, z) : void 0;
12308
+ }
12309
+ /** Resolves used tint indices from biome or preview colors; explicit tints take precedence. */
12310
+ static async get(key, state, model, tints, biomeColor) {
12183
12311
  if (!key) return tints;
12184
12312
  const candidates = (model.elements ?? []).flatMap((element) => Object.values(element.faces)).some((face) => face?.tintindex !== void 0 && face.tintindex >= 0 && tints?.[face.tintindex] === void 0);
12185
12313
  if (!candidates) return tints;
@@ -12195,8 +12323,12 @@ var BlockTints = class {
12195
12323
  }
12196
12324
  }
12197
12325
  if (!indices.size) return tints;
12326
+ const source = rule.biome ?? (rule.source === "grass" ? "grass" : void 0);
12327
+ const biome = source ? typeof biomeColor === "function" ? await biomeColor() : biomeColor : void 0;
12198
12328
  let color2;
12199
- switch (rule.source) {
12329
+ if (biome !== void 0) {
12330
+ color2 = biome;
12331
+ } else switch (rule.source) {
12200
12332
  case "constant":
12201
12333
  color2 = rule.color;
12202
12334
  break;
@@ -12255,6 +12387,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
12255
12387
  this._entityGeneration = 0;
12256
12388
  this.options = (0, import_ts_deepmerge6.default)({}, _BlockObject.DEFAULT_OPTIONS, options ?? {});
12257
12389
  this.variantPosition = options?.variantPosition ? [...options.variantPosition] : void 0;
12390
+ this.biomePosition = options?.biomePosition ? [...options.biomePosition] : void 0;
12258
12391
  }
12259
12392
  async init() {
12260
12393
  const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
@@ -12447,11 +12580,16 @@ var _BlockObject = class _BlockObject extends SceneObject {
12447
12580
  const { FluidModelObject: FluidModelObject2, sampleFluid: sampleFluid2 } = await Promise.resolve().then(() => (init_FluidModelObject(), FluidModelObject_exports));
12448
12581
  this._fluidSampler = sample;
12449
12582
  const surface = sampleFluid2(kind, (x, y, z) => x === 0 && y === 0 && z === 0 ? { fluid: kind, level: this.fluidLevel } : sample?.(x, y, z) ?? {});
12450
- if (this._fluidKey === surface.key) return;
12583
+ const tint = this.options.tints?.[0] ?? this.waterTint;
12584
+ const fluidKey = `${surface.key}|${tint ?? "default"}`;
12585
+ if (this._fluidKey === fluidKey) return;
12451
12586
  const key = new AssetKey("minecraft", `${kind}/${surface.key}`, "models", "fluid", "assets", ".json", this.blockState.key?.root);
12452
12587
  const replacement = await this.scene.addSceneObject(
12453
12588
  { key },
12454
- () => new FluidModelObject2(kind, surface.sample, this.blockState.key, this.options)
12589
+ () => new FluidModelObject2(kind, surface.sample, this.blockState.key, {
12590
+ ...this.options,
12591
+ tints: tint === void 0 ? this.options.tints : { ...this.options.tints, 0: tint }
12592
+ })
12455
12593
  );
12456
12594
  const previous = this._fluidModel;
12457
12595
  const matrix = previous ? this.getModelMatrix(previous) : new Matrix411().makeTranslation(...this.position.toArray());
@@ -12460,7 +12598,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
12460
12598
  this._models = this._models.filter((model) => model !== previous);
12461
12599
  this._models.push(replacement);
12462
12600
  this._fluidModel = replacement;
12463
- this._fluidKey = surface.key;
12601
+ this._fluidKey = fluidKey;
12464
12602
  this._isInstanced || (this._isInstanced = isInstanceReference(replacement));
12465
12603
  this._instanceCounter = this._isInstanced ? 1 : 0;
12466
12604
  if (previous) this.removeModels([previous]);
@@ -12483,6 +12621,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
12483
12621
  }
12484
12622
  async recreateModels() {
12485
12623
  this.clearModels();
12624
+ this.waterTint = this.fluidKind === "water" && this.options.biome && this.options.tints?.[0] === void 0 ? await Biomes.getColor(this.options.biome, "water", this.blockState.key) : void 0;
12486
12625
  if (resolveBlockFluidState(this.blockState.key, this.state, this._fluidRules)?.renderModel === false) {
12487
12626
  await this.updateFluid(this._fluidSampler);
12488
12627
  return;
@@ -12517,7 +12656,13 @@ var _BlockObject = class _BlockObject extends SceneObject {
12517
12656
  const model = await Models.getMerged(modelKey);
12518
12657
  const options = {
12519
12658
  ...this.options,
12520
- tints: model ? await BlockTints.get(this.blockState.key, this.state, model, this.options.tints) : this.options.tints,
12659
+ tints: model ? await BlockTints.get(
12660
+ this.blockState.key,
12661
+ this.state,
12662
+ model,
12663
+ this.options.tints,
12664
+ () => BlockTints.getBiomeColor(this.blockState.key, this.options.biome, this.biomePosition)
12665
+ ) : this.options.tints,
12521
12666
  uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0,
12522
12667
  cullMask: this.options.displayPosition ? 0 : ModelCulling.toLocalMask(this._cullMask, rotation)
12523
12668
  };
@@ -16936,7 +17081,7 @@ var AnvilWorldSource = class {
16936
17081
  };
16937
17082
 
16938
17083
  // src/world/Chunk.ts
16939
- import { Vector3 as Vector318 } from "three";
17084
+ import { Color as Color13, Vector3 as Vector318 } from "three";
16940
17085
  init_BlockStates();
16941
17086
  init_AssetKey();
16942
17087
 
@@ -17006,24 +17151,48 @@ var SectionModels = class {
17006
17151
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("maxAtlasSize must be a positive integer");
17007
17152
  }
17008
17153
  /** Returns templates for the matched parts, or `undefined` when the block needs an individual render object. */
17009
- async get(blockState3, properties = {}, position) {
17010
- const variantPosition = position ? [...position] : void 0;
17011
- return (await this.prepareState(blockState3, properties))?.pick(variantPosition);
17154
+ async get(blockState3, properties = {}, position, biome) {
17155
+ const biomePosition = position ? [...position] : void 0;
17156
+ const prepared = await this.state(blockState3, properties);
17157
+ if (!prepared) return void 0;
17158
+ return Promise.all(prepared.groups.map(async (group) => {
17159
+ const variant = BlockStateResolver.choose(group, biomePosition);
17160
+ const choice = prepared.choices.get(variant);
17161
+ const tints = await BlockTints.get(
17162
+ blockState3.key,
17163
+ prepared.state,
17164
+ choice.model,
17165
+ void 0,
17166
+ () => BlockTints.getBiomeColor(blockState3.key, biome, biomePosition)
17167
+ );
17168
+ return this.template(variant, choice, tints);
17169
+ }));
17012
17170
  }
17013
- /** Resolves section templates once, with a synchronous weighted choice for each cell. */
17014
- async prepareState(blockState3, properties = {}) {
17171
+ /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
17172
+ async prepareState(blockState3, properties = {}, biomeColor) {
17173
+ const prepared = await this.state(blockState3, properties);
17174
+ if (!prepared) return void 0;
17175
+ let color2;
17176
+ const resolveColor = typeof biomeColor === "function" ? () => color2 ?? (color2 = biomeColor()) : biomeColor;
17177
+ const templates = /* @__PURE__ */ new Map();
17178
+ await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
17179
+ const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
17180
+ templates.set(variant, await this.template(variant, choice, tints));
17181
+ }));
17182
+ return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
17183
+ }
17184
+ async state(blockState3, properties) {
17015
17185
  let states = this.states.get(blockState3);
17016
17186
  if (!states) this.states.set(blockState3, states = /* @__PURE__ */ new Map());
17017
17187
  const key = JSON.stringify(Object.entries(properties).sort(([a], [b]) => a.localeCompare(b)));
17018
- const prepared = await cached(states, key, () => this.prepare(blockState3, properties));
17019
- if (!prepared) return void 0;
17020
- return { pick: (position) => prepared.groups.map((group) => prepared.templates.get(BlockStateResolver.choose(group, position))) };
17188
+ return cached(states, key, () => this.prepare(blockState3, properties));
17021
17189
  }
17022
17190
  /** Returns the shared still/flow atlas used by per-block fluid objects. */
17023
17191
  fluidAtlas(kind, root) {
17024
- return cached(this.fluidAtlases, kind, async () => {
17192
+ const key = new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root);
17193
+ return cached(this.fluidAtlases, key.serialize(), async () => {
17025
17194
  const atlas = await UVMapper.getAtlas({
17026
- key: new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root),
17195
+ key,
17027
17196
  textures: { still: `minecraft:block/${kind}_still`, flow: `minecraft:block/${kind}_flow` },
17028
17197
  elements: []
17029
17198
  });
@@ -17035,64 +17204,67 @@ var SectionModels = class {
17035
17204
  const state = { ...await BlockStateResolver.defaults(blockState3), ...properties };
17036
17205
  const groups = BlockStateResolver.matching(blockState3, state);
17037
17206
  if (!groups.length) return void 0;
17038
- const templates = /* @__PURE__ */ new Map();
17207
+ const prepared = /* @__PURE__ */ new Map();
17039
17208
  for (const group of groups) {
17040
17209
  const choices = Array.isArray(group) ? group : [group];
17041
17210
  if (!choices.length) return void 0;
17042
17211
  for (const variant of choices) {
17043
17212
  if (!variant.model) return void 0;
17044
- const model = await Models.getMerged(AssetKey.parse("models", variant.model));
17213
+ const modelKey = AssetKey.parse("models", variant.model);
17214
+ modelKey.root = blockState3.key?.root;
17215
+ const model = await Models.getMerged(modelKey);
17045
17216
  if (!model?.elements?.length) return void 0;
17046
17217
  const atlas = await UVMapper.getAtlas(model);
17047
17218
  const rotation = BlockStateResolver.rotation(variant);
17048
17219
  if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
17049
- const tints = await BlockTints.get(blockState3.key, state, model);
17050
- let models = this.models.get(model);
17051
- if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17052
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17053
- const template = await cached(models, key, async () => {
17054
- const object4 = new SectionModel(model, {
17055
- instanceMeshes: false,
17056
- mergeMeshes: true,
17057
- tints,
17058
- uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17059
- });
17060
- await object4.init();
17061
- let geometry = object4.children[0].geometry;
17062
- geometry.applyMatrix4(new Matrix412().makeRotationFromEuler(rotation));
17063
- object4.clear();
17064
- const keep = [];
17065
- const cullFaces = [];
17066
- for (const element of atlas.model.elements) {
17067
- for (const face of CUBE_FACES) {
17068
- const data = element.faces[face];
17069
- keep.push(data !== void 0);
17070
- if (!data) continue;
17071
- const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17072
- if (local < 0) {
17073
- cullFaces.push(0);
17074
- continue;
17075
- }
17076
- const normal = new Vector316(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17077
- const world = CUBE_FACE_OFFSETS.findIndex(([x, y, z]) => Math.abs(normal.x - x) < 1e-6 && Math.abs(normal.y - y) < 1e-6 && Math.abs(normal.z - z) < 1e-6);
17078
- cullFaces.push(world < 0 ? 0 : 1 << world);
17079
- }
17080
- }
17081
- geometry = compactQuads(geometry, keep);
17082
- const template2 = {
17083
- geometry,
17084
- atlas,
17085
- cullFaces: new Uint8Array(cullFaces),
17086
- occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17087
- layer: atlas.hasTranslucency ? 1 : 0
17088
- };
17089
- this.templates.add(template2);
17090
- return template2;
17091
- });
17092
- templates.set(variant, template);
17220
+ prepared.set(variant, { model, atlas, rotation });
17093
17221
  }
17094
17222
  }
17095
- return { groups, templates };
17223
+ return { state, groups, choices: prepared };
17224
+ }
17225
+ async template(variant, { model, atlas, rotation }, tints) {
17226
+ let models = this.models.get(model);
17227
+ if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17228
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17229
+ return cached(models, key, async () => {
17230
+ const object4 = new SectionModel(model, {
17231
+ instanceMeshes: false,
17232
+ mergeMeshes: true,
17233
+ tints,
17234
+ uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17235
+ });
17236
+ await object4.init();
17237
+ let geometry = object4.children[0].geometry;
17238
+ geometry.applyMatrix4(new Matrix412().makeRotationFromEuler(rotation));
17239
+ object4.clear();
17240
+ const keep = [];
17241
+ const cullFaces = [];
17242
+ for (const element of atlas.model.elements) {
17243
+ for (const face of CUBE_FACES) {
17244
+ const data = element.faces[face];
17245
+ keep.push(data !== void 0);
17246
+ if (!data) continue;
17247
+ const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17248
+ if (local < 0) {
17249
+ cullFaces.push(0);
17250
+ continue;
17251
+ }
17252
+ const normal = new Vector316(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17253
+ const world = CUBE_FACE_OFFSETS.findIndex(([x, y, z]) => Math.abs(normal.x - x) < 1e-6 && Math.abs(normal.y - y) < 1e-6 && Math.abs(normal.z - z) < 1e-6);
17254
+ cullFaces.push(world < 0 ? 0 : 1 << world);
17255
+ }
17256
+ }
17257
+ geometry = compactQuads(geometry, keep);
17258
+ const template = {
17259
+ geometry,
17260
+ atlas,
17261
+ cullFaces: new Uint8Array(cullFaces),
17262
+ occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17263
+ layer: atlas.hasTranslucency ? 1 : 0
17264
+ };
17265
+ this.templates.add(template);
17266
+ return template;
17267
+ });
17096
17268
  }
17097
17269
  /** Disposes prepared geometry and clears this world's template caches. */
17098
17270
  clear() {
@@ -17422,7 +17594,6 @@ var MineRenderWorld = class {
17422
17594
  }
17423
17595
  try {
17424
17596
  await this.clearChunkColumn(chunk.x, chunk.z);
17425
- let sliceStart = performance.now();
17426
17597
  for (const section of chunk.sections) {
17427
17598
  if (section.biomes) {
17428
17599
  const position = new Vector317(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17436,6 +17607,9 @@ var MineRenderWorld = class {
17436
17607
  if (!column) this.biomes.set(key, column = /* @__PURE__ */ new Map());
17437
17608
  column.set(section.y, copy);
17438
17609
  }
17610
+ }
17611
+ let sliceStart = performance.now();
17612
+ for (const section of chunk.sections) {
17439
17613
  if (!executor) {
17440
17614
  if (!occupiedSections.has(section.y)) continue;
17441
17615
  const position = new Vector317(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17600,18 +17774,34 @@ var MineRenderWorld = class {
17600
17774
  const key = this.worldPosToChunkKey(pos);
17601
17775
  let chunk = this._chunks.get(key);
17602
17776
  if (typeof chunk === "undefined") {
17777
+ const x = Math.floor(pos.x / 16), y = Math.floor(pos.y / 16), z = Math.floor(pos.z / 16);
17603
17778
  chunk = new Chunk(
17604
17779
  this.scene,
17605
- Math.floor(pos.x / 16),
17606
- Math.floor(pos.y / 16),
17607
- Math.floor(pos.z / 16),
17780
+ x,
17781
+ y,
17782
+ z,
17608
17783
  (positions) => this.updateCulling(positions),
17609
17784
  this.sectionModels,
17610
- (x, y, z) => {
17611
- const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
17785
+ (x2, y2, z2) => {
17786
+ const cx = Math.floor(x2 / 16), cy = Math.floor(y2 / 16), cz = Math.floor(z2 / 16);
17612
17787
  const section = this._chunks.get(`${cx}_${cy}_${cz}`);
17613
- const index = (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16;
17788
+ const index = (y2 - cy * 16) * 256 + (z2 - cz * 16) * 16 + x2 - cx * 16;
17614
17789
  return (section?.fluidByteIndex(index) ?? 0) | (section?.isOccludingIndex(index) ? 64 : 0);
17790
+ },
17791
+ (x2, y2, z2) => this.getBiomeAt(x2, y2, z2),
17792
+ async (biomes) => {
17793
+ if (this._chunks.get(key) !== chunk) return;
17794
+ const columnKey = `${x}_${z}`;
17795
+ let column = this.biomes.get(columnKey);
17796
+ if (biomes) {
17797
+ if (!column) this.biomes.set(columnKey, column = /* @__PURE__ */ new Map());
17798
+ column.set(y, biomes);
17799
+ } else {
17800
+ column?.delete(y);
17801
+ if (!column?.size) this.biomes.delete(columnKey);
17802
+ }
17803
+ await chunk.refreshBiomes();
17804
+ await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
17615
17805
  }
17616
17806
  );
17617
17807
  this._chunks.set(key, chunk);
@@ -17666,6 +17856,7 @@ function buildSectionGeometry(input) {
17666
17856
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
17667
17857
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
17668
17858
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
17859
+ if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
17669
17860
  for (const template of templateData) {
17670
17861
  const { quads } = template;
17671
17862
  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) {
@@ -17743,7 +17934,9 @@ function buildSectionGeometry(input) {
17743
17934
  height
17744
17935
  });
17745
17936
  }
17746
- fluidPages.get(fluid).fluids.push({ quads, x, y, z, data });
17937
+ const tintIndex = (x + z * 16 + y * 256) * 3;
17938
+ const tint = fluid === "water" && fluids.waterTints ? fluids.waterTints.subarray(tintIndex, tintIndex + 3) : data.tint;
17939
+ fluidPages.get(fluid).fluids.push({ quads, x, y, z, data, tint });
17747
17940
  }
17748
17941
  pages.push(...[...fluidPages.values()].sort((a, b) => Number(atlases[a.placements[0].atlas].animated) - Number(atlases[b.placements[0].atlas].animated) || a.placements[0].atlas - b.placements[0].atlas));
17749
17942
  }
@@ -17792,7 +17985,7 @@ function buildSectionGeometry(input) {
17792
17985
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
17793
17986
  }
17794
17987
  }
17795
- for (const { quads, x, y, z, data } of page.fluids) {
17988
+ for (const { quads, x, y, z, data, tint } of page.fluids) {
17796
17989
  const atlas = atlases[data.atlas];
17797
17990
  const placement = page.placements[0];
17798
17991
  const mapU = (u) => (placement.x + u * atlas.width) / page.width;
@@ -17816,7 +18009,7 @@ function buildSectionGeometry(input) {
17816
18009
  mapU((rect[0] + rect[2]) / atlas.width),
17817
18010
  mapV(1 - rect[1] / atlas.height)
17818
18011
  ], vertex * 4);
17819
- colors.set(data.tint, vertex * 3);
18012
+ colors.set(tint, vertex * 3);
17820
18013
  }
17821
18014
  outputIndices.set([start, start + 1, start + 2, start, start + 2, start + 3], indexOffset);
17822
18015
  indexOffset += 6;
@@ -17957,7 +18150,7 @@ function toInput(entries, maxAtlasSize, fluids) {
17957
18150
  input.cullMasks[i] = cullMask;
17958
18151
  }
17959
18152
  if (fluids) {
17960
- input.fluids = { cells: fluids.cells };
18153
+ input.fluids = { cells: fluids.cells, waterTints: fluids.waterTints };
17961
18154
  for (const kind of ["water", "lava"]) {
17962
18155
  const atlas = fluids[kind];
17963
18156
  if (!atlas) continue;
@@ -18058,11 +18251,14 @@ var SectionMesh = class _SectionMesh extends Group3 {
18058
18251
  init_FluidGeometry();
18059
18252
  init_FluidQuads();
18060
18253
  init_CubeFace();
18254
+ var WATER_TINT2 = new Color13(4159204).toArray();
18061
18255
  var Chunk = class _Chunk {
18062
- constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell) {
18256
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged) {
18063
18257
  this.onBlocksChanged = onBlocksChanged;
18064
18258
  this.sectionModels = sectionModels;
18065
18259
  this.neighborCell = neighborCell;
18260
+ this.biomeAt = biomeAt;
18261
+ this.onBiomesChanged = onBiomesChanged;
18066
18262
  this.data = new ChunkData();
18067
18263
  this.renderedBlocks = /* @__PURE__ */ new Map();
18068
18264
  this.sectionBlocks = /* @__PURE__ */ new Map();
@@ -18087,6 +18283,56 @@ var Chunk = class _Chunk {
18087
18283
  const index = _Chunk.chunkPosToBlockIndex(pos);
18088
18284
  return this.renderedBlocks.get(index);
18089
18285
  }
18286
+ getBiomeAt(posOrX, y, z) {
18287
+ if (typeof posOrX === "number") return this.getBiomeAt(new Vector318(posOrX, y, z));
18288
+ if (isTripleArray(posOrX)) return this.getBiomeAt(new Vector318(...posOrX));
18289
+ const pos = this.worldPosToChunkPos(posOrX);
18290
+ _Chunk.chunkPosToBlockIndex(pos);
18291
+ return this.biomeAt ? this.biomeAt(posOrX.x, posOrX.y, posOrX.z) : this.biomes?.[Math.floor(pos.x / 4) + Math.floor(pos.z / 4) * 4 + Math.floor(pos.y / 4) * 16];
18292
+ }
18293
+ /** Copies 64 biome IDs in x + z * 4 + y * 16 order and refreshes placed blocks. Omit the array to clear biome data. */
18294
+ setBiomes(biomes) {
18295
+ const copy = biomes ? [...biomes] : void 0;
18296
+ if (copy && (copy.length !== 64 || !copy.every((id) => typeof id === "string" && id.length > 0))) {
18297
+ throw new RangeError("Section biomes must contain 64 nonempty biome IDs");
18298
+ }
18299
+ if (this.onBiomesChanged) return this.onBiomesChanged(copy);
18300
+ this.biomes = copy;
18301
+ return this.refreshBiomes();
18302
+ }
18303
+ /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
18304
+ async refreshBiomes() {
18305
+ const affected = /* @__PURE__ */ new Map();
18306
+ const blocks = [];
18307
+ for (const index of this.renderedBlocks.keys()) {
18308
+ const block = this.data.get(index);
18309
+ const stateKey = ChunkData.paletteKey(block);
18310
+ let matches = affected.get(stateKey);
18311
+ if (!matches) {
18312
+ const key = AssetKey.parse("blockstates", block.type);
18313
+ matches = BlockStates.get(key).then(async (state) => {
18314
+ const origin = state?.key ?? key;
18315
+ const [source, fluid] = await Promise.all([BlockTints.biomeSource(origin), getBlockFluidState(origin, block.properties)]);
18316
+ return !!source || fluid?.kind === "water";
18317
+ });
18318
+ affected.set(stateKey, matches);
18319
+ }
18320
+ if (await matches) blocks.push({ index, block });
18321
+ }
18322
+ if (!blocks.length) return;
18323
+ const hidden = new Set(this.hiddenBlocks);
18324
+ try {
18325
+ await this.placeBlocks(blocks);
18326
+ } finally {
18327
+ for (const index of hidden) {
18328
+ this.hiddenBlocks.add(index);
18329
+ this.renderedBlocks.get(index)?.object?.setVisible(false);
18330
+ }
18331
+ const positions = blocks.map(({ index }) => new Vector318(...this.positionAt(index)));
18332
+ if (this.onBlocksChanged) await this.onBlocksChanged(positions);
18333
+ else await this.rebuildSectionMesh();
18334
+ }
18335
+ }
18090
18336
  async setBlockAt(posOrX, yOrBlock, z, block) {
18091
18337
  if (typeof posOrX == "number") {
18092
18338
  return this.setBlockAt(new Vector318(posOrX, yOrBlock, z), block);
@@ -18125,14 +18371,15 @@ var Chunk = class _Chunk {
18125
18371
  nbt: block.nbt === void 0 ? void 0 : structuredClone(block.nbt)
18126
18372
  } : void 0
18127
18373
  }));
18128
- const states = new Map(placements.filter(({ key }) => key !== void 0).map(({ key, block }) => [key, block]));
18129
- const settled = await Promise.allSettled([...states.values()].map(async (stored) => {
18130
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18131
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18132
- const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18133
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties);
18134
- return { blockState: blockState3, perBlock, fluid, prepared, stored };
18135
- }));
18374
+ const states = /* @__PURE__ */ new Map();
18375
+ for (const { index, key, block } of placements) {
18376
+ if (key === void 0) continue;
18377
+ let state = states.get(key);
18378
+ if (!state) states.set(key, state = { stored: block, indices: [] });
18379
+ state.indices.push(index);
18380
+ }
18381
+ const biomes = /* @__PURE__ */ new Map();
18382
+ const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
18136
18383
  const resolutions = new Map([...states.keys()].map((key, i) => [key, settled[i]]));
18137
18384
  const positions = [];
18138
18385
  let failed = false, failure;
@@ -18156,7 +18403,8 @@ var Chunk = class _Chunk {
18156
18403
  if (!readBlock) continue;
18157
18404
  const resolved = resolutions.get(key);
18158
18405
  if (resolved.status === "rejected") throw resolved.reason;
18159
- const { blockState: blockState3, perBlock, fluid, prepared, stored } = resolved.value;
18406
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts, stored } = resolved.value;
18407
+ const context = contexts.get(index);
18160
18408
  if (!blockState3) {
18161
18409
  this.data.assign(index, 0);
18162
18410
  continue;
@@ -18164,9 +18412,10 @@ var Chunk = class _Chunk {
18164
18412
  const variantPosition = worldPos.toArray();
18165
18413
  if (fluid) {
18166
18414
  this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18415
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18167
18416
  this.meshDirty = true;
18168
18417
  }
18169
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18418
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18170
18419
  if (templates) {
18171
18420
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18172
18421
  this.meshDirty = true;
@@ -18176,7 +18425,9 @@ var Chunk = class _Chunk {
18176
18425
  instanceMeshes: true,
18177
18426
  maxInstanceCount: 2e3,
18178
18427
  initialState: stored.properties,
18179
- variantPosition
18428
+ variantPosition,
18429
+ biome: context?.biome,
18430
+ biomePosition: this.biomePosition(index, blockState3, stored)
18180
18431
  });
18181
18432
  object4.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
18182
18433
  }
@@ -18199,24 +18450,21 @@ var Chunk = class _Chunk {
18199
18450
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
18200
18451
  this.sectionBlocks.clear();
18201
18452
  this.fluidCells.fill(0);
18453
+ this.waterTints = void 0;
18202
18454
  this.renderedBlocks.clear();
18203
18455
  this.hiddenBlocks.clear();
18204
18456
  this.meshDirty = true;
18205
18457
  this.data.copyFrom(data);
18206
- const ids = /* @__PURE__ */ new Set();
18458
+ const ids = /* @__PURE__ */ new Map();
18207
18459
  for (let index = 0; index < 4096; index++) {
18208
18460
  const id = this.data.idAt(index);
18209
- if (id) ids.add(id);
18210
- }
18211
- const palette = [...ids];
18212
- const settled = await Promise.allSettled(palette.map(async (id) => {
18213
- const state = this.data.stateAt(id);
18214
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", state.type));
18215
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18216
- const fluid = await getBlockFluidState(blockState3?.key, state.properties);
18217
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, state.properties);
18218
- return { blockState: blockState3, perBlock, fluid, prepared };
18219
- }));
18461
+ if (!id) continue;
18462
+ let indices = ids.get(id);
18463
+ if (!indices) ids.set(id, indices = []);
18464
+ indices.push(index);
18465
+ }
18466
+ const palette = [...ids.keys()], biomes = /* @__PURE__ */ new Map();
18467
+ const settled = await Promise.allSettled(palette.map((id) => this.resolveState(this.data.stateAt(id), ids.get(id), biomes)));
18220
18468
  const resolutions = new Map(palette.map((id, i) => [id, settled[i]]));
18221
18469
  const perBlocks = [];
18222
18470
  let failed = false, failure;
@@ -18226,24 +18474,28 @@ var Chunk = class _Chunk {
18226
18474
  try {
18227
18475
  const resolved = resolutions.get(id);
18228
18476
  if (resolved.status === "rejected") throw resolved.reason;
18229
- const { blockState: blockState3, perBlock, fluid, prepared } = resolved.value;
18477
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts } = resolved.value;
18478
+ const context = contexts.get(index);
18230
18479
  if (!blockState3) {
18231
18480
  this.data.assign(index, 0);
18232
18481
  this.fluidCells[index] = 0;
18233
18482
  continue;
18234
18483
  }
18235
- if (fluid) this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18484
+ if (fluid) {
18485
+ this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18486
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18487
+ }
18236
18488
  const variantPosition = [
18237
18489
  this.x * 16 + index % 16,
18238
18490
  this.y * 16 + Math.floor(index / 256),
18239
18491
  this.z * 16 + Math.floor(index / 16) % 16
18240
18492
  ];
18241
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18493
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18242
18494
  if (templates) {
18243
18495
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18244
18496
  this.meshDirty = true;
18245
18497
  } else {
18246
- perBlocks.push({ index, blockState: blockState3 });
18498
+ perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
18247
18499
  }
18248
18500
  const readBlock = this.data.snapshot(index);
18249
18501
  this.renderedBlocks.set(index, { get block() {
@@ -18256,7 +18508,7 @@ var Chunk = class _Chunk {
18256
18508
  failed = true;
18257
18509
  }
18258
18510
  }
18259
- for (const { index, blockState: blockState3 } of perBlocks) {
18511
+ for (const { index, blockState: blockState3, biome } of perBlocks) {
18260
18512
  let object4;
18261
18513
  try {
18262
18514
  object4 = await this.scene.addBlock(blockState3, {
@@ -18264,11 +18516,9 @@ var Chunk = class _Chunk {
18264
18516
  instanceMeshes: true,
18265
18517
  maxInstanceCount: 2e3,
18266
18518
  initialState: this.data.stateAt(this.data.idAt(index)).properties,
18267
- variantPosition: [
18268
- this.x * 16 + index % 16,
18269
- this.y * 16 + Math.floor(index / 256),
18270
- this.z * 16 + Math.floor(index / 16) % 16
18271
- ]
18519
+ variantPosition: this.positionAt(index),
18520
+ biome,
18521
+ biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
18272
18522
  });
18273
18523
  object4.setPosition(new Vector318(
18274
18524
  this.x * 256 + index % 16 * 16,
@@ -18287,6 +18537,68 @@ var Chunk = class _Chunk {
18287
18537
  }
18288
18538
  if (failed) throw failure;
18289
18539
  }
18540
+ positionAt(index) {
18541
+ return [this.x * 16 + index % 16, this.y * 16 + Math.floor(index / 256), this.z * 16 + Math.floor(index / 16) % 16];
18542
+ }
18543
+ biomePosition(index, blockState3, stored) {
18544
+ const position = this.positionAt(index), name = blockState3.key?.toNamespacedString();
18545
+ if (stored.properties?.half === "upper" && (name === "minecraft:tall_grass" || name === "minecraft:large_fern")) position[1]--;
18546
+ return position;
18547
+ }
18548
+ async resolveState(stored, indices, biomes) {
18549
+ const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18550
+ const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18551
+ const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18552
+ 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;
18553
+ const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
18554
+ const prepare = (color2) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color2);
18555
+ const contexts = /* @__PURE__ */ new Map();
18556
+ const groups = /* @__PURE__ */ new Map();
18557
+ if (blockState3 && (source || fluid?.kind === "water")) {
18558
+ for (const index of indices) {
18559
+ const position = this.biomePosition(index, blockState3, stored);
18560
+ const biome = this.biomeAt ? this.biomeAt(...position) : position[1] >= this.y * 16 ? this.getBiomeAt(position) : void 0;
18561
+ if (!biome) continue;
18562
+ const key = JSON.stringify(source === "grass" ? [biome, position[0], position[2]] : [biome]);
18563
+ let group = groups.get(key);
18564
+ if (!group) groups.set(key, group = { biome, position, indices: [] });
18565
+ group.indices.push(index);
18566
+ }
18567
+ }
18568
+ const groupedCount = [...groups.values()].reduce((sum, group) => sum + group.indices.length, 0);
18569
+ const prepared = groupedCount < indices.length ? await prepare() : void 0;
18570
+ await Promise.all([...groups.values()].map(async ({ biome, position, indices: indices2 }) => {
18571
+ const available = () => {
18572
+ const key = JSON.stringify([biome, blockState3?.key?.root]);
18573
+ let found = biomes.get(key);
18574
+ if (!found) {
18575
+ found = Biomes.get(biome, blockState3?.key).then((value) => value !== void 0);
18576
+ biomes.set(key, found);
18577
+ }
18578
+ return found;
18579
+ };
18580
+ const context = { biome };
18581
+ if (perBlock) {
18582
+ if (!await available()) context.biome = void 0;
18583
+ } else {
18584
+ context.prepared = await prepare(async () => await available() ? BlockTints.getBiomeColor(blockState3?.key, biome, position) : void 0);
18585
+ if (fluid?.kind === "water" && await available()) {
18586
+ const color2 = await Biomes.getColor(biome, "water", blockState3?.key);
18587
+ if (color2 !== void 0 && color2 !== 4159204) context.waterTint = new Color13(color2).toArray();
18588
+ }
18589
+ }
18590
+ for (const index of indices2) contexts.set(index, context);
18591
+ }));
18592
+ return { blockState: blockState3, perBlock, fluid, prepared, contexts, stored };
18593
+ }
18594
+ setWaterTint(index, tint) {
18595
+ if (!this.sectionModels || !this.waterTints && tint === void 0) return;
18596
+ if (!this.waterTints) {
18597
+ this.waterTints = new Float32Array(4096 * 3);
18598
+ for (let cell = 0; cell < 4096; cell++) this.waterTints.set(WATER_TINT2, cell * 3);
18599
+ }
18600
+ this.waterTints.set(tint ?? WATER_TINT2, index * 3);
18601
+ }
18290
18602
  /** Changes visibility at world block coordinates while preserving the block's data. */
18291
18603
  async setBlockVisibleAt(pos, visible) {
18292
18604
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
@@ -18390,7 +18702,7 @@ var Chunk = class _Chunk {
18390
18702
  if (block && !block.object) origins.set(kind, block.block.type);
18391
18703
  }
18392
18704
  if (origins.size) {
18393
- fluids = { cells: new Uint8Array(18 * 18 * 18) };
18705
+ fluids = { cells: new Uint8Array(18 * 18 * 18), waterTints: this.waterTints?.slice() };
18394
18706
  for (let y = -1; y <= 16; y++) {
18395
18707
  for (let z = -1; z <= 16; z++) {
18396
18708
  for (let x = -1; x <= 16; x++) {
@@ -18432,8 +18744,13 @@ var Chunk = class _Chunk {
18432
18744
  this.renderedBlocks.clear();
18433
18745
  this.hiddenBlocks.clear();
18434
18746
  this.fluidCells.fill(0);
18747
+ this.waterTints = void 0;
18435
18748
  this.data.clear();
18436
- if (notify) await this.onBlocksChanged?.(positions);
18749
+ this.biomes = void 0;
18750
+ if (notify) {
18751
+ await this.onBiomesChanged?.(void 0);
18752
+ await this.onBlocksChanged?.(positions);
18753
+ }
18437
18754
  }
18438
18755
  async dispose() {
18439
18756
  await this.clear();
@@ -18654,6 +18971,7 @@ export {
18654
18971
  BannerPatterns,
18655
18972
  BasicAssetKey,
18656
18973
  BatchedExecutor,
18974
+ Biomes,
18657
18975
  BlockEntities,
18658
18976
  BlockObject,
18659
18977
  BlockStateResolver,