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.
@@ -323,6 +323,9 @@ var init_Caching = __esm({
323
323
  static {
324
324
  this.usernameUuidCache = Caches.builder().expireAfterWrite(Time.minutes(5)).expirationInterval(Time.seconds(30)).buildAsync();
325
325
  }
326
+ static {
327
+ this.biomeCache = _Caching.createAssetCache();
328
+ }
326
329
  static createAssetCache() {
327
330
  return Caches.builder().expireAfterWrite(Time.minutes(10)).expireAfterAccess(Time.minutes(5)).expirationInterval(Time.seconds(30)).buildAsync();
328
331
  }
@@ -352,7 +355,8 @@ var init_Caching = __esm({
352
355
  this.fontCache,
353
356
  this.bannerPatternCache,
354
357
  this.mineRenderDataCache,
355
- this.usernameUuidCache
358
+ this.usernameUuidCache,
359
+ this.biomeCache
356
360
  ];
357
361
  }
358
362
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
@@ -1730,7 +1734,7 @@ ${key}` : key, async () => {
1730
1734
  case "blockStates":
1731
1735
  return Object.values(value).every((property) => object(property) && "default" in property && Array.isArray(property.values));
1732
1736
  case "blockTints":
1733
- return ["constant", "grass", "stem", "redstone"].includes(value.source) && (value.source !== "constant" || Number.isInteger(value.color));
1737
+ 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));
1734
1738
  case "fluids":
1735
1739
  return ["water", "lava"].includes(value.kind) && typeof value.renderModel === "boolean" && (value.levelProperty === void 0 || typeof value.levelProperty === "string");
1736
1740
  case "dyes":
@@ -1874,15 +1878,19 @@ var init_Colormaps = __esm({
1874
1878
  Colormaps = class {
1875
1879
  /** Samples the selected resource pack's grass colormap using Minecraft's climate coordinates. */
1876
1880
  static async grassColor(origin, temperature = 0.5, downfall = 1) {
1877
- const key = AssetKey.parse("textures", "minecraft:colormap/grass", origin);
1881
+ return this.getColor("grass", origin, temperature, downfall);
1882
+ }
1883
+ /** Samples a resource-pack colormap at the supplied climate coordinates. */
1884
+ static async getColor(kind, origin, temperature = 0.5, downfall = 1) {
1885
+ const key = AssetKey.parse("textures", `minecraft:colormap/${kind}`, origin);
1878
1886
  const temp = Math.fround(temperature);
1879
1887
  const rain = Math.fround(downfall) * temp;
1880
1888
  const x = Math.floor((1 - temp) * 255), y = Math.floor((1 - rain) * 255);
1881
1889
  return await Caching.blockTintCache.get(`${key.serialize()}|${x},${y}`, async () => {
1882
1890
  const image = await ModelTextures.get(key);
1883
- if (!image) throw new MineRenderError(`Missing grass colormap: ${key.toNamespacedString()}`);
1891
+ if (!image) throw new MineRenderError(`Missing ${kind} colormap: ${key.toNamespacedString()}`);
1884
1892
  if (image.width !== 256 || image.height !== 256) {
1885
- throw new MineRenderError("Grass colormaps must be 256 \xD7 256 pixels");
1893
+ throw new MineRenderError(`${kind} colormaps must be 256 \xD7 256 pixels`);
1886
1894
  }
1887
1895
  const [red, green, blue] = image.data.getImageData(x, y, 1, 1).data;
1888
1896
  return red << 16 | green << 8 | blue;
@@ -7751,6 +7759,116 @@ init_AssetLoader();
7751
7759
  init_Assets();
7752
7760
  init_BannerPatterns();
7753
7761
 
7762
+ // src/assets/Biomes.ts
7763
+ init_AssetKey();
7764
+ init_AssetLoader();
7765
+ init_AssetParsers();
7766
+ init_Caching();
7767
+ init_MineRenderError();
7768
+ init_Colormaps();
7769
+
7770
+ // src/assets/_biomes/SwampGrassColor.ts
7771
+ var permutation;
7772
+ 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]];
7773
+ var skew = (Math.sqrt(3) - 1) / 2;
7774
+ var unskew = (3 - Math.sqrt(3)) / 6;
7775
+ function createPermutation() {
7776
+ const multiplier = 0x5deece66dn, mask = (1n << 48n) - 1n;
7777
+ let seed = 2345n ^ multiplier;
7778
+ const next = (bits) => {
7779
+ seed = seed * multiplier + 11n & mask;
7780
+ return Number(seed >> BigInt(48 - bits));
7781
+ };
7782
+ for (let i = 0; i < 3; i++) {
7783
+ next(26);
7784
+ next(27);
7785
+ }
7786
+ const values = Uint8Array.from({ length: 256 }, (_, i) => i);
7787
+ for (let i = 0; i < 256; i++) {
7788
+ const bound = 256 - i;
7789
+ let offset;
7790
+ if ((bound & bound - 1) === 0) offset = Math.floor(bound * next(31) / 2147483648);
7791
+ else {
7792
+ let bits;
7793
+ do {
7794
+ bits = next(31);
7795
+ offset = bits % bound;
7796
+ } while (bits - offset + bound - 1 > 2147483647);
7797
+ }
7798
+ const other = i + offset;
7799
+ [values[i], values[other]] = [values[other], values[i]];
7800
+ }
7801
+ return values;
7802
+ }
7803
+ function swampGrassColor(x, z) {
7804
+ const p10 = permutation ??= createPermutation();
7805
+ x *= 0.0225;
7806
+ z *= 0.0225;
7807
+ const s = (x + z) * skew;
7808
+ const i = Math.floor(x + s), j = Math.floor(z + s);
7809
+ const t = (i + j) * unskew;
7810
+ const dx = x - (i - t), dz = z - (j - t);
7811
+ const stepX = dx > dz ? 1 : 0, stepZ = 1 - stepX;
7812
+ const corner = (gx, gz, a, b) => {
7813
+ let falloff = 0.5 - a * a - b * b;
7814
+ if (falloff < 0) return 0;
7815
+ const gradient = gradients[p10[gx + p10[gz & 255] & 255] % 12];
7816
+ falloff *= falloff;
7817
+ return falloff * falloff * (gradient[0] * a + gradient[1] * b);
7818
+ };
7819
+ 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));
7820
+ return noise < -0.1 ? 5011004 : 6975545;
7821
+ }
7822
+
7823
+ // src/assets/Biomes.ts
7824
+ var Biomes = class {
7825
+ /** Loads a biome registry entry, or returns `undefined` when no source provides it. */
7826
+ static async get(id, origin) {
7827
+ if (!/^(?:[a-z0-9_.-]+:)?[a-z0-9_./-]+$/.test(id)) throw new MineRenderError(`Invalid biome identifier: ${id}`);
7828
+ const [namespace, path] = id.includes(":") ? id.split(":") : ["minecraft", id];
7829
+ const key = new AssetKey(namespace, path, "worldgen", "biome", "data", ".json", origin?.root);
7830
+ return Caching.biomeCache.get(key.serialize(), async () => {
7831
+ const asset2 = await AssetLoader.get(key, "json" /* JSON */);
7832
+ if (asset2 === void 0) return void 0;
7833
+ if (!asset2 || typeof asset2 !== "object" || Array.isArray(asset2) || !asset2.effects || typeof asset2.effects !== "object" || Array.isArray(asset2.effects)) {
7834
+ throw new MineRenderError(`Invalid biome effects: ${id}`);
7835
+ }
7836
+ if (typeof asset2.temperature !== "number" || !Number.isFinite(asset2.temperature) || typeof asset2.downfall !== "number" || !Number.isFinite(asset2.downfall)) {
7837
+ throw new MineRenderError(`Invalid biome climate: ${id}`);
7838
+ }
7839
+ for (const kind of ["grass", "foliage", "dry_foliage", "water"]) {
7840
+ const color2 = asset2.effects[`${kind}_color`];
7841
+ if (color2 !== void 0) this.parseColor(color2, id, kind);
7842
+ }
7843
+ const modifier = asset2.effects.grass_color_modifier;
7844
+ if (modifier !== void 0 && modifier !== "none" && modifier !== "dark_forest" && modifier !== "swamp") {
7845
+ throw new MineRenderError(`Invalid biome grass color modifier: ${id}`);
7846
+ }
7847
+ return asset2;
7848
+ });
7849
+ }
7850
+ /** Returns packed sRGB, or `undefined` when the biome or requested water color is missing. */
7851
+ static async getColor(id, kind, origin, x = 0, z = 0) {
7852
+ const biome = await this.get(id, origin);
7853
+ if (!biome) return void 0;
7854
+ const rawColor = biome.effects[`${kind}_color`];
7855
+ let color2 = rawColor === void 0 ? void 0 : this.parseColor(rawColor, id, kind);
7856
+ if (kind === "water") return color2;
7857
+ const modifier = kind === "grass" ? biome.effects.grass_color_modifier : void 0;
7858
+ if (modifier === "swamp") return swampGrassColor(x, z);
7859
+ if (color2 === void 0) {
7860
+ const clamp = (value) => Math.max(0, Math.min(1, Math.fround(value)));
7861
+ color2 = await Colormaps.getColor(kind, origin, clamp(biome.temperature), clamp(biome.downfall));
7862
+ }
7863
+ return modifier === "dark_forest" ? (color2 & 16711422) + 2634762 >> 1 : color2;
7864
+ }
7865
+ static parseColor(value, id, kind) {
7866
+ if (typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value)) return parseInt(value.slice(1), 16);
7867
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 16777215) return value;
7868
+ throw new MineRenderError(`Invalid biome ${kind} color: ${id}`);
7869
+ }
7870
+ };
7871
+
7754
7872
  // src/assets/BlockEntities.ts
7755
7873
  init_AssetKey();
7756
7874
  init_AssetLoader();
@@ -12230,8 +12348,20 @@ var BlockStateResolver = class _BlockStateResolver {
12230
12348
  init_Colormaps();
12231
12349
  init_MineRenderData();
12232
12350
  var BlockTints = class {
12233
- /** Resolves block preview colors without biome context; explicit tints take precedence. */
12234
- static async get(key, state, model, tints) {
12351
+ /** Returns a block's versioned biome color source, leaving fixed-color blocks unchanged. */
12352
+ static async biomeSource(key) {
12353
+ const rule = key && (await MineRenderData.get("blockTints", key.root))[key.toNamespacedString()];
12354
+ return rule ? rule.biome ?? (rule.source === "grass" ? "grass" : void 0) : void 0;
12355
+ }
12356
+ /** Resolves one block's biome tint at world block coordinates, without biome blending. */
12357
+ static async getBiomeColor(key, biome, position) {
12358
+ if (!biome) return void 0;
12359
+ const x = position?.[0], z = position?.[2];
12360
+ const source = await this.biomeSource(key);
12361
+ return source ? Biomes.getColor(biome, source, key, x, z) : void 0;
12362
+ }
12363
+ /** Resolves used tint indices from biome or preview colors; explicit tints take precedence. */
12364
+ static async get(key, state, model, tints, biomeColor) {
12235
12365
  if (!key) return tints;
12236
12366
  const candidates = (model.elements ?? []).flatMap((element) => Object.values(element.faces)).some((face) => face?.tintindex !== void 0 && face.tintindex >= 0 && tints?.[face.tintindex] === void 0);
12237
12367
  if (!candidates) return tints;
@@ -12247,8 +12377,12 @@ var BlockTints = class {
12247
12377
  }
12248
12378
  }
12249
12379
  if (!indices.size) return tints;
12380
+ const source = rule.biome ?? (rule.source === "grass" ? "grass" : void 0);
12381
+ const biome = source ? typeof biomeColor === "function" ? await biomeColor() : biomeColor : void 0;
12250
12382
  let color2;
12251
- switch (rule.source) {
12383
+ if (biome !== void 0) {
12384
+ color2 = biome;
12385
+ } else switch (rule.source) {
12252
12386
  case "constant":
12253
12387
  color2 = rule.color;
12254
12388
  break;
@@ -12307,6 +12441,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12307
12441
  this._entityGeneration = 0;
12308
12442
  this.options = (0, import_ts_deepmerge6.default)({}, _BlockObject.DEFAULT_OPTIONS, options ?? {});
12309
12443
  this.variantPosition = options?.variantPosition ? [...options.variantPosition] : void 0;
12444
+ this.biomePosition = options?.biomePosition ? [...options.biomePosition] : void 0;
12310
12445
  }
12311
12446
  static {
12312
12447
  this.DEFAULT_OPTIONS = (0, import_ts_deepmerge6.default)({}, ModelObject.DEFAULT_OPTIONS, {
@@ -12504,11 +12639,16 @@ var BlockObject = class _BlockObject extends SceneObject {
12504
12639
  const { FluidModelObject: FluidModelObject2, sampleFluid: sampleFluid2 } = await Promise.resolve().then(() => (init_FluidModelObject(), FluidModelObject_exports));
12505
12640
  this._fluidSampler = sample;
12506
12641
  const surface = sampleFluid2(kind, (x, y, z) => x === 0 && y === 0 && z === 0 ? { fluid: kind, level: this.fluidLevel } : sample?.(x, y, z) ?? {});
12507
- if (this._fluidKey === surface.key) return;
12642
+ const tint = this.options.tints?.[0] ?? this.waterTint;
12643
+ const fluidKey = `${surface.key}|${tint ?? "default"}`;
12644
+ if (this._fluidKey === fluidKey) return;
12508
12645
  const key = new AssetKey("minecraft", `${kind}/${surface.key}`, "models", "fluid", "assets", ".json", this.blockState.key?.root);
12509
12646
  const replacement = await this.scene.addSceneObject(
12510
12647
  { key },
12511
- () => new FluidModelObject2(kind, surface.sample, this.blockState.key, this.options)
12648
+ () => new FluidModelObject2(kind, surface.sample, this.blockState.key, {
12649
+ ...this.options,
12650
+ tints: tint === void 0 ? this.options.tints : { ...this.options.tints, 0: tint }
12651
+ })
12512
12652
  );
12513
12653
  const previous = this._fluidModel;
12514
12654
  const matrix = previous ? this.getModelMatrix(previous) : new Matrix411().makeTranslation(...this.position.toArray());
@@ -12517,7 +12657,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12517
12657
  this._models = this._models.filter((model) => model !== previous);
12518
12658
  this._models.push(replacement);
12519
12659
  this._fluidModel = replacement;
12520
- this._fluidKey = surface.key;
12660
+ this._fluidKey = fluidKey;
12521
12661
  this._isInstanced ||= isInstanceReference(replacement);
12522
12662
  this._instanceCounter = this._isInstanced ? 1 : 0;
12523
12663
  if (previous) this.removeModels([previous]);
@@ -12540,6 +12680,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12540
12680
  }
12541
12681
  async recreateModels() {
12542
12682
  this.clearModels();
12683
+ 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;
12543
12684
  if (resolveBlockFluidState(this.blockState.key, this.state, this._fluidRules)?.renderModel === false) {
12544
12685
  await this.updateFluid(this._fluidSampler);
12545
12686
  return;
@@ -12574,7 +12715,13 @@ var BlockObject = class _BlockObject extends SceneObject {
12574
12715
  const model = await Models.getMerged(modelKey);
12575
12716
  const options = {
12576
12717
  ...this.options,
12577
- tints: model ? await BlockTints.get(this.blockState.key, this.state, model, this.options.tints) : this.options.tints,
12718
+ tints: model ? await BlockTints.get(
12719
+ this.blockState.key,
12720
+ this.state,
12721
+ model,
12722
+ this.options.tints,
12723
+ () => BlockTints.getBiomeColor(this.blockState.key, this.options.biome, this.biomePosition)
12724
+ ) : this.options.tints,
12578
12725
  uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0,
12579
12726
  cullMask: this.options.displayPosition ? 0 : ModelCulling.toLocalMask(this._cullMask, rotation)
12580
12727
  };
@@ -16991,7 +17138,7 @@ var AnvilWorldSource = class {
16991
17138
  };
16992
17139
 
16993
17140
  // src/world/Chunk.ts
16994
- import { Vector3 as Vector318 } from "three";
17141
+ import { Color as Color13, Vector3 as Vector318 } from "three";
16995
17142
  init_BlockStates();
16996
17143
  init_AssetKey();
16997
17144
 
@@ -17061,24 +17208,48 @@ var SectionModels = class {
17061
17208
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("maxAtlasSize must be a positive integer");
17062
17209
  }
17063
17210
  /** Returns templates for the matched parts, or `undefined` when the block needs an individual render object. */
17064
- async get(blockState3, properties = {}, position) {
17065
- const variantPosition = position ? [...position] : void 0;
17066
- return (await this.prepareState(blockState3, properties))?.pick(variantPosition);
17211
+ async get(blockState3, properties = {}, position, biome) {
17212
+ const biomePosition = position ? [...position] : void 0;
17213
+ const prepared = await this.state(blockState3, properties);
17214
+ if (!prepared) return void 0;
17215
+ return Promise.all(prepared.groups.map(async (group) => {
17216
+ const variant = BlockStateResolver.choose(group, biomePosition);
17217
+ const choice = prepared.choices.get(variant);
17218
+ const tints = await BlockTints.get(
17219
+ blockState3.key,
17220
+ prepared.state,
17221
+ choice.model,
17222
+ void 0,
17223
+ () => BlockTints.getBiomeColor(blockState3.key, biome, biomePosition)
17224
+ );
17225
+ return this.template(variant, choice, tints);
17226
+ }));
17067
17227
  }
17068
- /** Resolves section templates once, with a synchronous weighted choice for each cell. */
17069
- async prepareState(blockState3, properties = {}) {
17228
+ /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
17229
+ async prepareState(blockState3, properties = {}, biomeColor) {
17230
+ const prepared = await this.state(blockState3, properties);
17231
+ if (!prepared) return void 0;
17232
+ let color2;
17233
+ const resolveColor = typeof biomeColor === "function" ? () => color2 ??= biomeColor() : biomeColor;
17234
+ const templates = /* @__PURE__ */ new Map();
17235
+ await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
17236
+ const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
17237
+ templates.set(variant, await this.template(variant, choice, tints));
17238
+ }));
17239
+ return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
17240
+ }
17241
+ async state(blockState3, properties) {
17070
17242
  let states = this.states.get(blockState3);
17071
17243
  if (!states) this.states.set(blockState3, states = /* @__PURE__ */ new Map());
17072
17244
  const key = JSON.stringify(Object.entries(properties).sort(([a], [b]) => a.localeCompare(b)));
17073
- const prepared = await cached(states, key, () => this.prepare(blockState3, properties));
17074
- if (!prepared) return void 0;
17075
- return { pick: (position) => prepared.groups.map((group) => prepared.templates.get(BlockStateResolver.choose(group, position))) };
17245
+ return cached(states, key, () => this.prepare(blockState3, properties));
17076
17246
  }
17077
17247
  /** Returns the shared still/flow atlas used by per-block fluid objects. */
17078
17248
  fluidAtlas(kind, root) {
17079
- return cached(this.fluidAtlases, kind, async () => {
17249
+ const key = new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root);
17250
+ return cached(this.fluidAtlases, key.serialize(), async () => {
17080
17251
  const atlas = await UVMapper.getAtlas({
17081
- key: new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root),
17252
+ key,
17082
17253
  textures: { still: `minecraft:block/${kind}_still`, flow: `minecraft:block/${kind}_flow` },
17083
17254
  elements: []
17084
17255
  });
@@ -17090,64 +17261,67 @@ var SectionModels = class {
17090
17261
  const state = { ...await BlockStateResolver.defaults(blockState3), ...properties };
17091
17262
  const groups = BlockStateResolver.matching(blockState3, state);
17092
17263
  if (!groups.length) return void 0;
17093
- const templates = /* @__PURE__ */ new Map();
17264
+ const prepared = /* @__PURE__ */ new Map();
17094
17265
  for (const group of groups) {
17095
17266
  const choices = Array.isArray(group) ? group : [group];
17096
17267
  if (!choices.length) return void 0;
17097
17268
  for (const variant of choices) {
17098
17269
  if (!variant.model) return void 0;
17099
- const model = await Models.getMerged(AssetKey.parse("models", variant.model));
17270
+ const modelKey = AssetKey.parse("models", variant.model);
17271
+ modelKey.root = blockState3.key?.root;
17272
+ const model = await Models.getMerged(modelKey);
17100
17273
  if (!model?.elements?.length) return void 0;
17101
17274
  const atlas = await UVMapper.getAtlas(model);
17102
17275
  const rotation = BlockStateResolver.rotation(variant);
17103
17276
  if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
17104
- const tints = await BlockTints.get(blockState3.key, state, model);
17105
- let models = this.models.get(model);
17106
- if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17107
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17108
- const template = await cached(models, key, async () => {
17109
- const object4 = new SectionModel(model, {
17110
- instanceMeshes: false,
17111
- mergeMeshes: true,
17112
- tints,
17113
- uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17114
- });
17115
- await object4.init();
17116
- let geometry = object4.children[0].geometry;
17117
- geometry.applyMatrix4(new Matrix412().makeRotationFromEuler(rotation));
17118
- object4.clear();
17119
- const keep = [];
17120
- const cullFaces = [];
17121
- for (const element of atlas.model.elements) {
17122
- for (const face of CUBE_FACES) {
17123
- const data = element.faces[face];
17124
- keep.push(data !== void 0);
17125
- if (!data) continue;
17126
- const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17127
- if (local < 0) {
17128
- cullFaces.push(0);
17129
- continue;
17130
- }
17131
- const normal = new Vector316(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17132
- 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);
17133
- cullFaces.push(world < 0 ? 0 : 1 << world);
17134
- }
17135
- }
17136
- geometry = compactQuads(geometry, keep);
17137
- const template2 = {
17138
- geometry,
17139
- atlas,
17140
- cullFaces: new Uint8Array(cullFaces),
17141
- occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17142
- layer: atlas.hasTranslucency ? 1 : 0
17143
- };
17144
- this.templates.add(template2);
17145
- return template2;
17146
- });
17147
- templates.set(variant, template);
17277
+ prepared.set(variant, { model, atlas, rotation });
17148
17278
  }
17149
17279
  }
17150
- return { groups, templates };
17280
+ return { state, groups, choices: prepared };
17281
+ }
17282
+ async template(variant, { model, atlas, rotation }, tints) {
17283
+ let models = this.models.get(model);
17284
+ if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17285
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17286
+ return cached(models, key, async () => {
17287
+ const object4 = new SectionModel(model, {
17288
+ instanceMeshes: false,
17289
+ mergeMeshes: true,
17290
+ tints,
17291
+ uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17292
+ });
17293
+ await object4.init();
17294
+ let geometry = object4.children[0].geometry;
17295
+ geometry.applyMatrix4(new Matrix412().makeRotationFromEuler(rotation));
17296
+ object4.clear();
17297
+ const keep = [];
17298
+ const cullFaces = [];
17299
+ for (const element of atlas.model.elements) {
17300
+ for (const face of CUBE_FACES) {
17301
+ const data = element.faces[face];
17302
+ keep.push(data !== void 0);
17303
+ if (!data) continue;
17304
+ const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17305
+ if (local < 0) {
17306
+ cullFaces.push(0);
17307
+ continue;
17308
+ }
17309
+ const normal = new Vector316(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17310
+ 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);
17311
+ cullFaces.push(world < 0 ? 0 : 1 << world);
17312
+ }
17313
+ }
17314
+ geometry = compactQuads(geometry, keep);
17315
+ const template = {
17316
+ geometry,
17317
+ atlas,
17318
+ cullFaces: new Uint8Array(cullFaces),
17319
+ occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17320
+ layer: atlas.hasTranslucency ? 1 : 0
17321
+ };
17322
+ this.templates.add(template);
17323
+ return template;
17324
+ });
17151
17325
  }
17152
17326
  /** Disposes prepared geometry and clears this world's template caches. */
17153
17327
  clear() {
@@ -17477,7 +17651,6 @@ var MineRenderWorld = class {
17477
17651
  }
17478
17652
  try {
17479
17653
  await this.clearChunkColumn(chunk.x, chunk.z);
17480
- let sliceStart = performance.now();
17481
17654
  for (const section of chunk.sections) {
17482
17655
  if (section.biomes) {
17483
17656
  const position = new Vector317(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17491,6 +17664,9 @@ var MineRenderWorld = class {
17491
17664
  if (!column) this.biomes.set(key, column = /* @__PURE__ */ new Map());
17492
17665
  column.set(section.y, copy);
17493
17666
  }
17667
+ }
17668
+ let sliceStart = performance.now();
17669
+ for (const section of chunk.sections) {
17494
17670
  if (!executor) {
17495
17671
  if (!occupiedSections.has(section.y)) continue;
17496
17672
  const position = new Vector317(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17655,18 +17831,34 @@ var MineRenderWorld = class {
17655
17831
  const key = this.worldPosToChunkKey(pos);
17656
17832
  let chunk = this._chunks.get(key);
17657
17833
  if (typeof chunk === "undefined") {
17834
+ const x = Math.floor(pos.x / 16), y = Math.floor(pos.y / 16), z = Math.floor(pos.z / 16);
17658
17835
  chunk = new Chunk(
17659
17836
  this.scene,
17660
- Math.floor(pos.x / 16),
17661
- Math.floor(pos.y / 16),
17662
- Math.floor(pos.z / 16),
17837
+ x,
17838
+ y,
17839
+ z,
17663
17840
  (positions) => this.updateCulling(positions),
17664
17841
  this.sectionModels,
17665
- (x, y, z) => {
17666
- const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
17842
+ (x2, y2, z2) => {
17843
+ const cx = Math.floor(x2 / 16), cy = Math.floor(y2 / 16), cz = Math.floor(z2 / 16);
17667
17844
  const section = this._chunks.get(`${cx}_${cy}_${cz}`);
17668
- const index = (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16;
17845
+ const index = (y2 - cy * 16) * 256 + (z2 - cz * 16) * 16 + x2 - cx * 16;
17669
17846
  return (section?.fluidByteIndex(index) ?? 0) | (section?.isOccludingIndex(index) ? 64 : 0);
17847
+ },
17848
+ (x2, y2, z2) => this.getBiomeAt(x2, y2, z2),
17849
+ async (biomes) => {
17850
+ if (this._chunks.get(key) !== chunk) return;
17851
+ const columnKey = `${x}_${z}`;
17852
+ let column = this.biomes.get(columnKey);
17853
+ if (biomes) {
17854
+ if (!column) this.biomes.set(columnKey, column = /* @__PURE__ */ new Map());
17855
+ column.set(y, biomes);
17856
+ } else {
17857
+ column?.delete(y);
17858
+ if (!column?.size) this.biomes.delete(columnKey);
17859
+ }
17860
+ await chunk.refreshBiomes();
17861
+ await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
17670
17862
  }
17671
17863
  );
17672
17864
  this._chunks.set(key, chunk);
@@ -17721,6 +17913,7 @@ function buildSectionGeometry(input) {
17721
17913
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
17722
17914
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
17723
17915
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
17916
+ if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
17724
17917
  for (const template of templateData) {
17725
17918
  const { quads } = template;
17726
17919
  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) {
@@ -17798,7 +17991,9 @@ function buildSectionGeometry(input) {
17798
17991
  height
17799
17992
  });
17800
17993
  }
17801
- fluidPages.get(fluid).fluids.push({ quads, x, y, z, data });
17994
+ const tintIndex = (x + z * 16 + y * 256) * 3;
17995
+ const tint = fluid === "water" && fluids.waterTints ? fluids.waterTints.subarray(tintIndex, tintIndex + 3) : data.tint;
17996
+ fluidPages.get(fluid).fluids.push({ quads, x, y, z, data, tint });
17802
17997
  }
17803
17998
  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));
17804
17999
  }
@@ -17847,7 +18042,7 @@ function buildSectionGeometry(input) {
17847
18042
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
17848
18043
  }
17849
18044
  }
17850
- for (const { quads, x, y, z, data } of page.fluids) {
18045
+ for (const { quads, x, y, z, data, tint } of page.fluids) {
17851
18046
  const atlas = atlases[data.atlas];
17852
18047
  const placement = page.placements[0];
17853
18048
  const mapU = (u) => (placement.x + u * atlas.width) / page.width;
@@ -17871,7 +18066,7 @@ function buildSectionGeometry(input) {
17871
18066
  mapU((rect[0] + rect[2]) / atlas.width),
17872
18067
  mapV(1 - rect[1] / atlas.height)
17873
18068
  ], vertex * 4);
17874
- colors.set(data.tint, vertex * 3);
18069
+ colors.set(tint, vertex * 3);
17875
18070
  }
17876
18071
  outputIndices.set([start, start + 1, start + 2, start, start + 2, start + 3], indexOffset);
17877
18072
  indexOffset += 6;
@@ -18013,7 +18208,7 @@ function toInput(entries, maxAtlasSize, fluids) {
18013
18208
  input.cullMasks[i] = cullMask;
18014
18209
  }
18015
18210
  if (fluids) {
18016
- input.fluids = { cells: fluids.cells };
18211
+ input.fluids = { cells: fluids.cells, waterTints: fluids.waterTints };
18017
18212
  for (const kind of ["water", "lava"]) {
18018
18213
  const atlas = fluids[kind];
18019
18214
  if (!atlas) continue;
@@ -18114,11 +18309,14 @@ var SectionMesh = class _SectionMesh extends Group3 {
18114
18309
  init_FluidGeometry();
18115
18310
  init_FluidQuads();
18116
18311
  init_CubeFace();
18312
+ var WATER_TINT2 = new Color13(4159204).toArray();
18117
18313
  var Chunk = class _Chunk {
18118
- constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell) {
18314
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged) {
18119
18315
  this.onBlocksChanged = onBlocksChanged;
18120
18316
  this.sectionModels = sectionModels;
18121
18317
  this.neighborCell = neighborCell;
18318
+ this.biomeAt = biomeAt;
18319
+ this.onBiomesChanged = onBiomesChanged;
18122
18320
  this.data = new ChunkData();
18123
18321
  this.renderedBlocks = /* @__PURE__ */ new Map();
18124
18322
  this.sectionBlocks = /* @__PURE__ */ new Map();
@@ -18143,6 +18341,56 @@ var Chunk = class _Chunk {
18143
18341
  const index = _Chunk.chunkPosToBlockIndex(pos);
18144
18342
  return this.renderedBlocks.get(index);
18145
18343
  }
18344
+ getBiomeAt(posOrX, y, z) {
18345
+ if (typeof posOrX === "number") return this.getBiomeAt(new Vector318(posOrX, y, z));
18346
+ if (isTripleArray(posOrX)) return this.getBiomeAt(new Vector318(...posOrX));
18347
+ const pos = this.worldPosToChunkPos(posOrX);
18348
+ _Chunk.chunkPosToBlockIndex(pos);
18349
+ 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];
18350
+ }
18351
+ /** Copies 64 biome IDs in x + z * 4 + y * 16 order and refreshes placed blocks. Omit the array to clear biome data. */
18352
+ setBiomes(biomes) {
18353
+ const copy = biomes ? [...biomes] : void 0;
18354
+ if (copy && (copy.length !== 64 || !copy.every((id) => typeof id === "string" && id.length > 0))) {
18355
+ throw new RangeError("Section biomes must contain 64 nonempty biome IDs");
18356
+ }
18357
+ if (this.onBiomesChanged) return this.onBiomesChanged(copy);
18358
+ this.biomes = copy;
18359
+ return this.refreshBiomes();
18360
+ }
18361
+ /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
18362
+ async refreshBiomes() {
18363
+ const affected = /* @__PURE__ */ new Map();
18364
+ const blocks = [];
18365
+ for (const index of this.renderedBlocks.keys()) {
18366
+ const block = this.data.get(index);
18367
+ const stateKey = ChunkData.paletteKey(block);
18368
+ let matches = affected.get(stateKey);
18369
+ if (!matches) {
18370
+ const key = AssetKey.parse("blockstates", block.type);
18371
+ matches = BlockStates.get(key).then(async (state) => {
18372
+ const origin = state?.key ?? key;
18373
+ const [source, fluid] = await Promise.all([BlockTints.biomeSource(origin), getBlockFluidState(origin, block.properties)]);
18374
+ return !!source || fluid?.kind === "water";
18375
+ });
18376
+ affected.set(stateKey, matches);
18377
+ }
18378
+ if (await matches) blocks.push({ index, block });
18379
+ }
18380
+ if (!blocks.length) return;
18381
+ const hidden = new Set(this.hiddenBlocks);
18382
+ try {
18383
+ await this.placeBlocks(blocks);
18384
+ } finally {
18385
+ for (const index of hidden) {
18386
+ this.hiddenBlocks.add(index);
18387
+ this.renderedBlocks.get(index)?.object?.setVisible(false);
18388
+ }
18389
+ const positions = blocks.map(({ index }) => new Vector318(...this.positionAt(index)));
18390
+ if (this.onBlocksChanged) await this.onBlocksChanged(positions);
18391
+ else await this.rebuildSectionMesh();
18392
+ }
18393
+ }
18146
18394
  async setBlockAt(posOrX, yOrBlock, z, block) {
18147
18395
  if (typeof posOrX == "number") {
18148
18396
  return this.setBlockAt(new Vector318(posOrX, yOrBlock, z), block);
@@ -18181,14 +18429,15 @@ var Chunk = class _Chunk {
18181
18429
  nbt: block.nbt === void 0 ? void 0 : structuredClone(block.nbt)
18182
18430
  } : void 0
18183
18431
  }));
18184
- const states = new Map(placements.filter(({ key }) => key !== void 0).map(({ key, block }) => [key, block]));
18185
- const settled = await Promise.allSettled([...states.values()].map(async (stored) => {
18186
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18187
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18188
- const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18189
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties);
18190
- return { blockState: blockState3, perBlock, fluid, prepared, stored };
18191
- }));
18432
+ const states = /* @__PURE__ */ new Map();
18433
+ for (const { index, key, block } of placements) {
18434
+ if (key === void 0) continue;
18435
+ let state = states.get(key);
18436
+ if (!state) states.set(key, state = { stored: block, indices: [] });
18437
+ state.indices.push(index);
18438
+ }
18439
+ const biomes = /* @__PURE__ */ new Map();
18440
+ const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
18192
18441
  const resolutions = new Map([...states.keys()].map((key, i) => [key, settled[i]]));
18193
18442
  const positions = [];
18194
18443
  let failed = false, failure;
@@ -18212,7 +18461,8 @@ var Chunk = class _Chunk {
18212
18461
  if (!readBlock) continue;
18213
18462
  const resolved = resolutions.get(key);
18214
18463
  if (resolved.status === "rejected") throw resolved.reason;
18215
- const { blockState: blockState3, perBlock, fluid, prepared, stored } = resolved.value;
18464
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts, stored } = resolved.value;
18465
+ const context = contexts.get(index);
18216
18466
  if (!blockState3) {
18217
18467
  this.data.assign(index, 0);
18218
18468
  continue;
@@ -18220,9 +18470,10 @@ var Chunk = class _Chunk {
18220
18470
  const variantPosition = worldPos.toArray();
18221
18471
  if (fluid) {
18222
18472
  this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18473
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18223
18474
  this.meshDirty = true;
18224
18475
  }
18225
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18476
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18226
18477
  if (templates) {
18227
18478
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18228
18479
  this.meshDirty = true;
@@ -18232,7 +18483,9 @@ var Chunk = class _Chunk {
18232
18483
  instanceMeshes: true,
18233
18484
  maxInstanceCount: 2e3,
18234
18485
  initialState: stored.properties,
18235
- variantPosition
18486
+ variantPosition,
18487
+ biome: context?.biome,
18488
+ biomePosition: this.biomePosition(index, blockState3, stored)
18236
18489
  });
18237
18490
  object4.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
18238
18491
  }
@@ -18255,24 +18508,21 @@ var Chunk = class _Chunk {
18255
18508
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
18256
18509
  this.sectionBlocks.clear();
18257
18510
  this.fluidCells.fill(0);
18511
+ this.waterTints = void 0;
18258
18512
  this.renderedBlocks.clear();
18259
18513
  this.hiddenBlocks.clear();
18260
18514
  this.meshDirty = true;
18261
18515
  this.data.copyFrom(data);
18262
- const ids = /* @__PURE__ */ new Set();
18516
+ const ids = /* @__PURE__ */ new Map();
18263
18517
  for (let index = 0; index < 4096; index++) {
18264
18518
  const id = this.data.idAt(index);
18265
- if (id) ids.add(id);
18266
- }
18267
- const palette = [...ids];
18268
- const settled = await Promise.allSettled(palette.map(async (id) => {
18269
- const state = this.data.stateAt(id);
18270
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", state.type));
18271
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18272
- const fluid = await getBlockFluidState(blockState3?.key, state.properties);
18273
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, state.properties);
18274
- return { blockState: blockState3, perBlock, fluid, prepared };
18275
- }));
18519
+ if (!id) continue;
18520
+ let indices = ids.get(id);
18521
+ if (!indices) ids.set(id, indices = []);
18522
+ indices.push(index);
18523
+ }
18524
+ const palette = [...ids.keys()], biomes = /* @__PURE__ */ new Map();
18525
+ const settled = await Promise.allSettled(palette.map((id) => this.resolveState(this.data.stateAt(id), ids.get(id), biomes)));
18276
18526
  const resolutions = new Map(palette.map((id, i) => [id, settled[i]]));
18277
18527
  const perBlocks = [];
18278
18528
  let failed = false, failure;
@@ -18282,24 +18532,28 @@ var Chunk = class _Chunk {
18282
18532
  try {
18283
18533
  const resolved = resolutions.get(id);
18284
18534
  if (resolved.status === "rejected") throw resolved.reason;
18285
- const { blockState: blockState3, perBlock, fluid, prepared } = resolved.value;
18535
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts } = resolved.value;
18536
+ const context = contexts.get(index);
18286
18537
  if (!blockState3) {
18287
18538
  this.data.assign(index, 0);
18288
18539
  this.fluidCells[index] = 0;
18289
18540
  continue;
18290
18541
  }
18291
- if (fluid) this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18542
+ if (fluid) {
18543
+ this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18544
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18545
+ }
18292
18546
  const variantPosition = [
18293
18547
  this.x * 16 + index % 16,
18294
18548
  this.y * 16 + Math.floor(index / 256),
18295
18549
  this.z * 16 + Math.floor(index / 16) % 16
18296
18550
  ];
18297
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18551
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18298
18552
  if (templates) {
18299
18553
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18300
18554
  this.meshDirty = true;
18301
18555
  } else {
18302
- perBlocks.push({ index, blockState: blockState3 });
18556
+ perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
18303
18557
  }
18304
18558
  const readBlock = this.data.snapshot(index);
18305
18559
  this.renderedBlocks.set(index, { get block() {
@@ -18312,7 +18566,7 @@ var Chunk = class _Chunk {
18312
18566
  failed = true;
18313
18567
  }
18314
18568
  }
18315
- for (const { index, blockState: blockState3 } of perBlocks) {
18569
+ for (const { index, blockState: blockState3, biome } of perBlocks) {
18316
18570
  let object4;
18317
18571
  try {
18318
18572
  object4 = await this.scene.addBlock(blockState3, {
@@ -18320,11 +18574,9 @@ var Chunk = class _Chunk {
18320
18574
  instanceMeshes: true,
18321
18575
  maxInstanceCount: 2e3,
18322
18576
  initialState: this.data.stateAt(this.data.idAt(index)).properties,
18323
- variantPosition: [
18324
- this.x * 16 + index % 16,
18325
- this.y * 16 + Math.floor(index / 256),
18326
- this.z * 16 + Math.floor(index / 16) % 16
18327
- ]
18577
+ variantPosition: this.positionAt(index),
18578
+ biome,
18579
+ biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
18328
18580
  });
18329
18581
  object4.setPosition(new Vector318(
18330
18582
  this.x * 256 + index % 16 * 16,
@@ -18343,6 +18595,68 @@ var Chunk = class _Chunk {
18343
18595
  }
18344
18596
  if (failed) throw failure;
18345
18597
  }
18598
+ positionAt(index) {
18599
+ return [this.x * 16 + index % 16, this.y * 16 + Math.floor(index / 256), this.z * 16 + Math.floor(index / 16) % 16];
18600
+ }
18601
+ biomePosition(index, blockState3, stored) {
18602
+ const position = this.positionAt(index), name = blockState3.key?.toNamespacedString();
18603
+ if (stored.properties?.half === "upper" && (name === "minecraft:tall_grass" || name === "minecraft:large_fern")) position[1]--;
18604
+ return position;
18605
+ }
18606
+ async resolveState(stored, indices, biomes) {
18607
+ const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18608
+ const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18609
+ const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18610
+ 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;
18611
+ const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
18612
+ const prepare = (color2) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color2);
18613
+ const contexts = /* @__PURE__ */ new Map();
18614
+ const groups = /* @__PURE__ */ new Map();
18615
+ if (blockState3 && (source || fluid?.kind === "water")) {
18616
+ for (const index of indices) {
18617
+ const position = this.biomePosition(index, blockState3, stored);
18618
+ const biome = this.biomeAt ? this.biomeAt(...position) : position[1] >= this.y * 16 ? this.getBiomeAt(position) : void 0;
18619
+ if (!biome) continue;
18620
+ const key = JSON.stringify(source === "grass" ? [biome, position[0], position[2]] : [biome]);
18621
+ let group = groups.get(key);
18622
+ if (!group) groups.set(key, group = { biome, position, indices: [] });
18623
+ group.indices.push(index);
18624
+ }
18625
+ }
18626
+ const groupedCount = [...groups.values()].reduce((sum, group) => sum + group.indices.length, 0);
18627
+ const prepared = groupedCount < indices.length ? await prepare() : void 0;
18628
+ await Promise.all([...groups.values()].map(async ({ biome, position, indices: indices2 }) => {
18629
+ const available = () => {
18630
+ const key = JSON.stringify([biome, blockState3?.key?.root]);
18631
+ let found = biomes.get(key);
18632
+ if (!found) {
18633
+ found = Biomes.get(biome, blockState3?.key).then((value) => value !== void 0);
18634
+ biomes.set(key, found);
18635
+ }
18636
+ return found;
18637
+ };
18638
+ const context = { biome };
18639
+ if (perBlock) {
18640
+ if (!await available()) context.biome = void 0;
18641
+ } else {
18642
+ context.prepared = await prepare(async () => await available() ? BlockTints.getBiomeColor(blockState3?.key, biome, position) : void 0);
18643
+ if (fluid?.kind === "water" && await available()) {
18644
+ const color2 = await Biomes.getColor(biome, "water", blockState3?.key);
18645
+ if (color2 !== void 0 && color2 !== 4159204) context.waterTint = new Color13(color2).toArray();
18646
+ }
18647
+ }
18648
+ for (const index of indices2) contexts.set(index, context);
18649
+ }));
18650
+ return { blockState: blockState3, perBlock, fluid, prepared, contexts, stored };
18651
+ }
18652
+ setWaterTint(index, tint) {
18653
+ if (!this.sectionModels || !this.waterTints && tint === void 0) return;
18654
+ if (!this.waterTints) {
18655
+ this.waterTints = new Float32Array(4096 * 3);
18656
+ for (let cell = 0; cell < 4096; cell++) this.waterTints.set(WATER_TINT2, cell * 3);
18657
+ }
18658
+ this.waterTints.set(tint ?? WATER_TINT2, index * 3);
18659
+ }
18346
18660
  /** Changes visibility at world block coordinates while preserving the block's data. */
18347
18661
  async setBlockVisibleAt(pos, visible) {
18348
18662
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
@@ -18446,7 +18760,7 @@ var Chunk = class _Chunk {
18446
18760
  if (block && !block.object) origins.set(kind, block.block.type);
18447
18761
  }
18448
18762
  if (origins.size) {
18449
- fluids = { cells: new Uint8Array(18 * 18 * 18) };
18763
+ fluids = { cells: new Uint8Array(18 * 18 * 18), waterTints: this.waterTints?.slice() };
18450
18764
  for (let y = -1; y <= 16; y++) {
18451
18765
  for (let z = -1; z <= 16; z++) {
18452
18766
  for (let x = -1; x <= 16; x++) {
@@ -18488,8 +18802,13 @@ var Chunk = class _Chunk {
18488
18802
  this.renderedBlocks.clear();
18489
18803
  this.hiddenBlocks.clear();
18490
18804
  this.fluidCells.fill(0);
18805
+ this.waterTints = void 0;
18491
18806
  this.data.clear();
18492
- if (notify) await this.onBlocksChanged?.(positions);
18807
+ this.biomes = void 0;
18808
+ if (notify) {
18809
+ await this.onBiomesChanged?.(void 0);
18810
+ await this.onBlocksChanged?.(positions);
18811
+ }
18493
18812
  }
18494
18813
  async dispose() {
18495
18814
  await this.clear();
@@ -18874,6 +19193,7 @@ export {
18874
19193
  BannerPatterns,
18875
19194
  BasicAssetKey,
18876
19195
  BatchedExecutor,
19196
+ Biomes,
18877
19197
  BlockEntities,
18878
19198
  BlockObject,
18879
19199
  BlockStateResolver,