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.
@@ -280,7 +280,8 @@ var init_Caching = __esm({
280
280
  this.fontCache,
281
281
  this.bannerPatternCache,
282
282
  this.mineRenderDataCache,
283
- this.usernameUuidCache
283
+ this.usernameUuidCache,
284
+ this.biomeCache
284
285
  ];
285
286
  }
286
287
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
@@ -331,6 +332,7 @@ var init_Caching = __esm({
331
332
  _Caching.bannerPatternCache = _Caching.createAssetCache();
332
333
  _Caching.mineRenderDataCache = _Caching.createAssetCache();
333
334
  _Caching.usernameUuidCache = import_loading_cache.Caches.builder().expireAfterWrite(import_time.Time.minutes(5)).expirationInterval(import_time.Time.seconds(30)).buildAsync();
335
+ _Caching.biomeCache = _Caching.createAssetCache();
334
336
  Caching = _Caching;
335
337
  }
336
338
  });
@@ -1677,7 +1679,7 @@ ${key}` : key, async () => {
1677
1679
  case "blockStates":
1678
1680
  return Object.values(value).every((property) => object(property) && "default" in property && Array.isArray(property.values));
1679
1681
  case "blockTints":
1680
- return ["constant", "grass", "stem", "redstone"].includes(value.source) && (value.source !== "constant" || Number.isInteger(value.color));
1682
+ 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));
1681
1683
  case "fluids":
1682
1684
  return ["water", "lava"].includes(value.kind) && typeof value.renderModel === "boolean" && (value.levelProperty === void 0 || typeof value.levelProperty === "string");
1683
1685
  case "dyes":
@@ -1821,15 +1823,19 @@ var init_Colormaps = __esm({
1821
1823
  Colormaps = class {
1822
1824
  /** Samples the selected resource pack's grass colormap using Minecraft's climate coordinates. */
1823
1825
  static async grassColor(origin, temperature = 0.5, downfall = 1) {
1824
- const key = AssetKey.parse("textures", "minecraft:colormap/grass", origin);
1826
+ return this.getColor("grass", origin, temperature, downfall);
1827
+ }
1828
+ /** Samples a resource-pack colormap at the supplied climate coordinates. */
1829
+ static async getColor(kind, origin, temperature = 0.5, downfall = 1) {
1830
+ const key = AssetKey.parse("textures", `minecraft:colormap/${kind}`, origin);
1825
1831
  const temp = Math.fround(temperature);
1826
1832
  const rain = Math.fround(downfall) * temp;
1827
1833
  const x = Math.floor((1 - temp) * 255), y = Math.floor((1 - rain) * 255);
1828
1834
  return await Caching.blockTintCache.get(`${key.serialize()}|${x},${y}`, async () => {
1829
1835
  const image = await ModelTextures.get(key);
1830
- if (!image) throw new MineRenderError(`Missing grass colormap: ${key.toNamespacedString()}`);
1836
+ if (!image) throw new MineRenderError(`Missing ${kind} colormap: ${key.toNamespacedString()}`);
1831
1837
  if (image.width !== 256 || image.height !== 256) {
1832
- throw new MineRenderError("Grass colormaps must be 256 \xD7 256 pixels");
1838
+ throw new MineRenderError(`${kind} colormaps must be 256 \xD7 256 pixels`);
1833
1839
  }
1834
1840
  const [red, green, blue] = image.data.getImageData(x, y, 1, 1).data;
1835
1841
  return red << 16 | green << 8 | blue;
@@ -7516,6 +7522,7 @@ __export(index_browser_exports, {
7516
7522
  BannerPatterns: () => BannerPatterns,
7517
7523
  BasicAssetKey: () => BasicAssetKey,
7518
7524
  BatchedExecutor: () => BatchedExecutor,
7525
+ Biomes: () => Biomes,
7519
7526
  BlockEntities: () => BlockEntities,
7520
7527
  BlockObject: () => BlockObject,
7521
7528
  BlockStateResolver: () => BlockStateResolver,
@@ -7905,6 +7912,116 @@ init_AssetLoader();
7905
7912
  init_Assets();
7906
7913
  init_BannerPatterns();
7907
7914
 
7915
+ // src/assets/Biomes.ts
7916
+ init_AssetKey();
7917
+ init_AssetLoader();
7918
+ init_AssetParsers();
7919
+ init_Caching();
7920
+ init_MineRenderError();
7921
+ init_Colormaps();
7922
+
7923
+ // src/assets/_biomes/SwampGrassColor.ts
7924
+ var permutation;
7925
+ 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]];
7926
+ var skew = (Math.sqrt(3) - 1) / 2;
7927
+ var unskew = (3 - Math.sqrt(3)) / 6;
7928
+ function createPermutation() {
7929
+ const multiplier = 0x5deece66dn, mask = (1n << 48n) - 1n;
7930
+ let seed = 2345n ^ multiplier;
7931
+ const next = (bits) => {
7932
+ seed = seed * multiplier + 11n & mask;
7933
+ return Number(seed >> BigInt(48 - bits));
7934
+ };
7935
+ for (let i = 0; i < 3; i++) {
7936
+ next(26);
7937
+ next(27);
7938
+ }
7939
+ const values = Uint8Array.from({ length: 256 }, (_, i) => i);
7940
+ for (let i = 0; i < 256; i++) {
7941
+ const bound = 256 - i;
7942
+ let offset;
7943
+ if ((bound & bound - 1) === 0) offset = Math.floor(bound * next(31) / 2147483648);
7944
+ else {
7945
+ let bits;
7946
+ do {
7947
+ bits = next(31);
7948
+ offset = bits % bound;
7949
+ } while (bits - offset + bound - 1 > 2147483647);
7950
+ }
7951
+ const other = i + offset;
7952
+ [values[i], values[other]] = [values[other], values[i]];
7953
+ }
7954
+ return values;
7955
+ }
7956
+ function swampGrassColor(x, z) {
7957
+ const p10 = permutation ?? (permutation = createPermutation());
7958
+ x *= 0.0225;
7959
+ z *= 0.0225;
7960
+ const s = (x + z) * skew;
7961
+ const i = Math.floor(x + s), j = Math.floor(z + s);
7962
+ const t = (i + j) * unskew;
7963
+ const dx = x - (i - t), dz = z - (j - t);
7964
+ const stepX = dx > dz ? 1 : 0, stepZ = 1 - stepX;
7965
+ const corner = (gx, gz, a, b) => {
7966
+ let falloff = 0.5 - a * a - b * b;
7967
+ if (falloff < 0) return 0;
7968
+ const gradient = gradients[p10[gx + p10[gz & 255] & 255] % 12];
7969
+ falloff *= falloff;
7970
+ return falloff * falloff * (gradient[0] * a + gradient[1] * b);
7971
+ };
7972
+ 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));
7973
+ return noise < -0.1 ? 5011004 : 6975545;
7974
+ }
7975
+
7976
+ // src/assets/Biomes.ts
7977
+ var Biomes = class {
7978
+ /** Loads a biome registry entry, or returns `undefined` when no source provides it. */
7979
+ static async get(id, origin) {
7980
+ if (!/^(?:[a-z0-9_.-]+:)?[a-z0-9_./-]+$/.test(id)) throw new MineRenderError(`Invalid biome identifier: ${id}`);
7981
+ const [namespace, path] = id.includes(":") ? id.split(":") : ["minecraft", id];
7982
+ const key = new AssetKey(namespace, path, "worldgen", "biome", "data", ".json", origin?.root);
7983
+ return Caching.biomeCache.get(key.serialize(), async () => {
7984
+ const asset2 = await AssetLoader.get(key, "json" /* JSON */);
7985
+ if (asset2 === void 0) return void 0;
7986
+ if (!asset2 || typeof asset2 !== "object" || Array.isArray(asset2) || !asset2.effects || typeof asset2.effects !== "object" || Array.isArray(asset2.effects)) {
7987
+ throw new MineRenderError(`Invalid biome effects: ${id}`);
7988
+ }
7989
+ if (typeof asset2.temperature !== "number" || !Number.isFinite(asset2.temperature) || typeof asset2.downfall !== "number" || !Number.isFinite(asset2.downfall)) {
7990
+ throw new MineRenderError(`Invalid biome climate: ${id}`);
7991
+ }
7992
+ for (const kind of ["grass", "foliage", "dry_foliage", "water"]) {
7993
+ const color2 = asset2.effects[`${kind}_color`];
7994
+ if (color2 !== void 0) this.parseColor(color2, id, kind);
7995
+ }
7996
+ const modifier = asset2.effects.grass_color_modifier;
7997
+ if (modifier !== void 0 && modifier !== "none" && modifier !== "dark_forest" && modifier !== "swamp") {
7998
+ throw new MineRenderError(`Invalid biome grass color modifier: ${id}`);
7999
+ }
8000
+ return asset2;
8001
+ });
8002
+ }
8003
+ /** Returns packed sRGB, or `undefined` when the biome or requested water color is missing. */
8004
+ static async getColor(id, kind, origin, x = 0, z = 0) {
8005
+ const biome = await this.get(id, origin);
8006
+ if (!biome) return void 0;
8007
+ const rawColor = biome.effects[`${kind}_color`];
8008
+ let color2 = rawColor === void 0 ? void 0 : this.parseColor(rawColor, id, kind);
8009
+ if (kind === "water") return color2;
8010
+ const modifier = kind === "grass" ? biome.effects.grass_color_modifier : void 0;
8011
+ if (modifier === "swamp") return swampGrassColor(x, z);
8012
+ if (color2 === void 0) {
8013
+ const clamp = (value) => Math.max(0, Math.min(1, Math.fround(value)));
8014
+ color2 = await Colormaps.getColor(kind, origin, clamp(biome.temperature), clamp(biome.downfall));
8015
+ }
8016
+ return modifier === "dark_forest" ? (color2 & 16711422) + 2634762 >> 1 : color2;
8017
+ }
8018
+ static parseColor(value, id, kind) {
8019
+ if (typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value)) return parseInt(value.slice(1), 16);
8020
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 16777215) return value;
8021
+ throw new MineRenderError(`Invalid biome ${kind} color: ${id}`);
8022
+ }
8023
+ };
8024
+
7908
8025
  // src/assets/BlockEntities.ts
7909
8026
  var import_three10 = require("three");
7910
8027
  init_AssetKey();
@@ -12343,8 +12460,20 @@ var BlockStateResolver = class _BlockStateResolver {
12343
12460
  init_Colormaps();
12344
12461
  init_MineRenderData();
12345
12462
  var BlockTints = class {
12346
- /** Resolves block preview colors without biome context; explicit tints take precedence. */
12347
- static async get(key, state, model, tints) {
12463
+ /** Returns a block's versioned biome color source, leaving fixed-color blocks unchanged. */
12464
+ static async biomeSource(key) {
12465
+ const rule = key && (await MineRenderData.get("blockTints", key.root))[key.toNamespacedString()];
12466
+ return rule ? rule.biome ?? (rule.source === "grass" ? "grass" : void 0) : void 0;
12467
+ }
12468
+ /** Resolves one block's biome tint at world block coordinates, without biome blending. */
12469
+ static async getBiomeColor(key, biome, position) {
12470
+ if (!biome) return void 0;
12471
+ const x = position?.[0], z = position?.[2];
12472
+ const source = await this.biomeSource(key);
12473
+ return source ? Biomes.getColor(biome, source, key, x, z) : void 0;
12474
+ }
12475
+ /** Resolves used tint indices from biome or preview colors; explicit tints take precedence. */
12476
+ static async get(key, state, model, tints, biomeColor) {
12348
12477
  if (!key) return tints;
12349
12478
  const candidates = (model.elements ?? []).flatMap((element) => Object.values(element.faces)).some((face) => face?.tintindex !== void 0 && face.tintindex >= 0 && tints?.[face.tintindex] === void 0);
12350
12479
  if (!candidates) return tints;
@@ -12360,8 +12489,12 @@ var BlockTints = class {
12360
12489
  }
12361
12490
  }
12362
12491
  if (!indices.size) return tints;
12492
+ const source = rule.biome ?? (rule.source === "grass" ? "grass" : void 0);
12493
+ const biome = source ? typeof biomeColor === "function" ? await biomeColor() : biomeColor : void 0;
12363
12494
  let color2;
12364
- switch (rule.source) {
12495
+ if (biome !== void 0) {
12496
+ color2 = biome;
12497
+ } else switch (rule.source) {
12365
12498
  case "constant":
12366
12499
  color2 = rule.color;
12367
12500
  break;
@@ -12420,6 +12553,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
12420
12553
  this._entityGeneration = 0;
12421
12554
  this.options = (0, import_ts_deepmerge6.default)({}, _BlockObject.DEFAULT_OPTIONS, options ?? {});
12422
12555
  this.variantPosition = options?.variantPosition ? [...options.variantPosition] : void 0;
12556
+ this.biomePosition = options?.biomePosition ? [...options.biomePosition] : void 0;
12423
12557
  }
12424
12558
  async init() {
12425
12559
  const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
@@ -12612,11 +12746,16 @@ var _BlockObject = class _BlockObject extends SceneObject {
12612
12746
  const { FluidModelObject: FluidModelObject2, sampleFluid: sampleFluid2 } = await Promise.resolve().then(() => (init_FluidModelObject(), FluidModelObject_exports));
12613
12747
  this._fluidSampler = sample;
12614
12748
  const surface = sampleFluid2(kind, (x, y, z) => x === 0 && y === 0 && z === 0 ? { fluid: kind, level: this.fluidLevel } : sample?.(x, y, z) ?? {});
12615
- if (this._fluidKey === surface.key) return;
12749
+ const tint = this.options.tints?.[0] ?? this.waterTint;
12750
+ const fluidKey = `${surface.key}|${tint ?? "default"}`;
12751
+ if (this._fluidKey === fluidKey) return;
12616
12752
  const key = new AssetKey("minecraft", `${kind}/${surface.key}`, "models", "fluid", "assets", ".json", this.blockState.key?.root);
12617
12753
  const replacement = await this.scene.addSceneObject(
12618
12754
  { key },
12619
- () => new FluidModelObject2(kind, surface.sample, this.blockState.key, this.options)
12755
+ () => new FluidModelObject2(kind, surface.sample, this.blockState.key, {
12756
+ ...this.options,
12757
+ tints: tint === void 0 ? this.options.tints : { ...this.options.tints, 0: tint }
12758
+ })
12620
12759
  );
12621
12760
  const previous = this._fluidModel;
12622
12761
  const matrix = previous ? this.getModelMatrix(previous) : new import_three34.Matrix4().makeTranslation(...this.position.toArray());
@@ -12625,7 +12764,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
12625
12764
  this._models = this._models.filter((model) => model !== previous);
12626
12765
  this._models.push(replacement);
12627
12766
  this._fluidModel = replacement;
12628
- this._fluidKey = surface.key;
12767
+ this._fluidKey = fluidKey;
12629
12768
  this._isInstanced || (this._isInstanced = isInstanceReference(replacement));
12630
12769
  this._instanceCounter = this._isInstanced ? 1 : 0;
12631
12770
  if (previous) this.removeModels([previous]);
@@ -12648,6 +12787,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
12648
12787
  }
12649
12788
  async recreateModels() {
12650
12789
  this.clearModels();
12790
+ 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;
12651
12791
  if (resolveBlockFluidState(this.blockState.key, this.state, this._fluidRules)?.renderModel === false) {
12652
12792
  await this.updateFluid(this._fluidSampler);
12653
12793
  return;
@@ -12682,7 +12822,13 @@ var _BlockObject = class _BlockObject extends SceneObject {
12682
12822
  const model = await Models.getMerged(modelKey);
12683
12823
  const options = {
12684
12824
  ...this.options,
12685
- tints: model ? await BlockTints.get(this.blockState.key, this.state, model, this.options.tints) : this.options.tints,
12825
+ tints: model ? await BlockTints.get(
12826
+ this.blockState.key,
12827
+ this.state,
12828
+ model,
12829
+ this.options.tints,
12830
+ () => BlockTints.getBiomeColor(this.blockState.key, this.options.biome, this.biomePosition)
12831
+ ) : this.options.tints,
12686
12832
  uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0,
12687
12833
  cullMask: this.options.displayPosition ? 0 : ModelCulling.toLocalMask(this._cullMask, rotation)
12688
12834
  };
@@ -17163,24 +17309,48 @@ var SectionModels = class {
17163
17309
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("maxAtlasSize must be a positive integer");
17164
17310
  }
17165
17311
  /** Returns templates for the matched parts, or `undefined` when the block needs an individual render object. */
17166
- async get(blockState3, properties = {}, position) {
17167
- const variantPosition = position ? [...position] : void 0;
17168
- return (await this.prepareState(blockState3, properties))?.pick(variantPosition);
17312
+ async get(blockState3, properties = {}, position, biome) {
17313
+ const biomePosition = position ? [...position] : void 0;
17314
+ const prepared = await this.state(blockState3, properties);
17315
+ if (!prepared) return void 0;
17316
+ return Promise.all(prepared.groups.map(async (group) => {
17317
+ const variant = BlockStateResolver.choose(group, biomePosition);
17318
+ const choice = prepared.choices.get(variant);
17319
+ const tints = await BlockTints.get(
17320
+ blockState3.key,
17321
+ prepared.state,
17322
+ choice.model,
17323
+ void 0,
17324
+ () => BlockTints.getBiomeColor(blockState3.key, biome, biomePosition)
17325
+ );
17326
+ return this.template(variant, choice, tints);
17327
+ }));
17169
17328
  }
17170
- /** Resolves section templates once, with a synchronous weighted choice for each cell. */
17171
- async prepareState(blockState3, properties = {}) {
17329
+ /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
17330
+ async prepareState(blockState3, properties = {}, biomeColor) {
17331
+ const prepared = await this.state(blockState3, properties);
17332
+ if (!prepared) return void 0;
17333
+ let color2;
17334
+ const resolveColor = typeof biomeColor === "function" ? () => color2 ?? (color2 = biomeColor()) : biomeColor;
17335
+ const templates = /* @__PURE__ */ new Map();
17336
+ await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
17337
+ const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
17338
+ templates.set(variant, await this.template(variant, choice, tints));
17339
+ }));
17340
+ return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
17341
+ }
17342
+ async state(blockState3, properties) {
17172
17343
  let states = this.states.get(blockState3);
17173
17344
  if (!states) this.states.set(blockState3, states = /* @__PURE__ */ new Map());
17174
17345
  const key = JSON.stringify(Object.entries(properties).sort(([a], [b]) => a.localeCompare(b)));
17175
- const prepared = await cached(states, key, () => this.prepare(blockState3, properties));
17176
- if (!prepared) return void 0;
17177
- return { pick: (position) => prepared.groups.map((group) => prepared.templates.get(BlockStateResolver.choose(group, position))) };
17346
+ return cached(states, key, () => this.prepare(blockState3, properties));
17178
17347
  }
17179
17348
  /** Returns the shared still/flow atlas used by per-block fluid objects. */
17180
17349
  fluidAtlas(kind, root) {
17181
- return cached(this.fluidAtlases, kind, async () => {
17350
+ const key = new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root);
17351
+ return cached(this.fluidAtlases, key.serialize(), async () => {
17182
17352
  const atlas = await UVMapper.getAtlas({
17183
- key: new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root),
17353
+ key,
17184
17354
  textures: { still: `minecraft:block/${kind}_still`, flow: `minecraft:block/${kind}_flow` },
17185
17355
  elements: []
17186
17356
  });
@@ -17192,64 +17362,67 @@ var SectionModels = class {
17192
17362
  const state = { ...await BlockStateResolver.defaults(blockState3), ...properties };
17193
17363
  const groups = BlockStateResolver.matching(blockState3, state);
17194
17364
  if (!groups.length) return void 0;
17195
- const templates = /* @__PURE__ */ new Map();
17365
+ const prepared = /* @__PURE__ */ new Map();
17196
17366
  for (const group of groups) {
17197
17367
  const choices = Array.isArray(group) ? group : [group];
17198
17368
  if (!choices.length) return void 0;
17199
17369
  for (const variant of choices) {
17200
17370
  if (!variant.model) return void 0;
17201
- const model = await Models.getMerged(AssetKey.parse("models", variant.model));
17371
+ const modelKey = AssetKey.parse("models", variant.model);
17372
+ modelKey.root = blockState3.key?.root;
17373
+ const model = await Models.getMerged(modelKey);
17202
17374
  if (!model?.elements?.length) return void 0;
17203
17375
  const atlas = await UVMapper.getAtlas(model);
17204
17376
  const rotation = BlockStateResolver.rotation(variant);
17205
17377
  if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
17206
- const tints = await BlockTints.get(blockState3.key, state, model);
17207
- let models = this.models.get(model);
17208
- if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17209
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17210
- const template = await cached(models, key, async () => {
17211
- const object4 = new SectionModel(model, {
17212
- instanceMeshes: false,
17213
- mergeMeshes: true,
17214
- tints,
17215
- uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17216
- });
17217
- await object4.init();
17218
- let geometry = object4.children[0].geometry;
17219
- geometry.applyMatrix4(new import_three41.Matrix4().makeRotationFromEuler(rotation));
17220
- object4.clear();
17221
- const keep = [];
17222
- const cullFaces = [];
17223
- for (const element of atlas.model.elements) {
17224
- for (const face of CUBE_FACES) {
17225
- const data = element.faces[face];
17226
- keep.push(data !== void 0);
17227
- if (!data) continue;
17228
- const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17229
- if (local < 0) {
17230
- cullFaces.push(0);
17231
- continue;
17232
- }
17233
- const normal = new import_three41.Vector3(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17234
- 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);
17235
- cullFaces.push(world < 0 ? 0 : 1 << world);
17236
- }
17237
- }
17238
- geometry = compactQuads(geometry, keep);
17239
- const template2 = {
17240
- geometry,
17241
- atlas,
17242
- cullFaces: new Uint8Array(cullFaces),
17243
- occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17244
- layer: atlas.hasTranslucency ? 1 : 0
17245
- };
17246
- this.templates.add(template2);
17247
- return template2;
17248
- });
17249
- templates.set(variant, template);
17378
+ prepared.set(variant, { model, atlas, rotation });
17250
17379
  }
17251
17380
  }
17252
- return { groups, templates };
17381
+ return { state, groups, choices: prepared };
17382
+ }
17383
+ async template(variant, { model, atlas, rotation }, tints) {
17384
+ let models = this.models.get(model);
17385
+ if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17386
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17387
+ return cached(models, key, async () => {
17388
+ const object4 = new SectionModel(model, {
17389
+ instanceMeshes: false,
17390
+ mergeMeshes: true,
17391
+ tints,
17392
+ uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17393
+ });
17394
+ await object4.init();
17395
+ let geometry = object4.children[0].geometry;
17396
+ geometry.applyMatrix4(new import_three41.Matrix4().makeRotationFromEuler(rotation));
17397
+ object4.clear();
17398
+ const keep = [];
17399
+ const cullFaces = [];
17400
+ for (const element of atlas.model.elements) {
17401
+ for (const face of CUBE_FACES) {
17402
+ const data = element.faces[face];
17403
+ keep.push(data !== void 0);
17404
+ if (!data) continue;
17405
+ const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17406
+ if (local < 0) {
17407
+ cullFaces.push(0);
17408
+ continue;
17409
+ }
17410
+ const normal = new import_three41.Vector3(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17411
+ 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);
17412
+ cullFaces.push(world < 0 ? 0 : 1 << world);
17413
+ }
17414
+ }
17415
+ geometry = compactQuads(geometry, keep);
17416
+ const template = {
17417
+ geometry,
17418
+ atlas,
17419
+ cullFaces: new Uint8Array(cullFaces),
17420
+ occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17421
+ layer: atlas.hasTranslucency ? 1 : 0
17422
+ };
17423
+ this.templates.add(template);
17424
+ return template;
17425
+ });
17253
17426
  }
17254
17427
  /** Disposes prepared geometry and clears this world's template caches. */
17255
17428
  clear() {
@@ -17579,7 +17752,6 @@ var MineRenderWorld = class {
17579
17752
  }
17580
17753
  try {
17581
17754
  await this.clearChunkColumn(chunk.x, chunk.z);
17582
- let sliceStart = performance.now();
17583
17755
  for (const section of chunk.sections) {
17584
17756
  if (section.biomes) {
17585
17757
  const position = new import_three42.Vector3(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17593,6 +17765,9 @@ var MineRenderWorld = class {
17593
17765
  if (!column) this.biomes.set(key, column = /* @__PURE__ */ new Map());
17594
17766
  column.set(section.y, copy);
17595
17767
  }
17768
+ }
17769
+ let sliceStart = performance.now();
17770
+ for (const section of chunk.sections) {
17596
17771
  if (!executor) {
17597
17772
  if (!occupiedSections.has(section.y)) continue;
17598
17773
  const position = new import_three42.Vector3(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17757,18 +17932,34 @@ var MineRenderWorld = class {
17757
17932
  const key = this.worldPosToChunkKey(pos);
17758
17933
  let chunk = this._chunks.get(key);
17759
17934
  if (typeof chunk === "undefined") {
17935
+ const x = Math.floor(pos.x / 16), y = Math.floor(pos.y / 16), z = Math.floor(pos.z / 16);
17760
17936
  chunk = new Chunk(
17761
17937
  this.scene,
17762
- Math.floor(pos.x / 16),
17763
- Math.floor(pos.y / 16),
17764
- Math.floor(pos.z / 16),
17938
+ x,
17939
+ y,
17940
+ z,
17765
17941
  (positions) => this.updateCulling(positions),
17766
17942
  this.sectionModels,
17767
- (x, y, z) => {
17768
- const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
17943
+ (x2, y2, z2) => {
17944
+ const cx = Math.floor(x2 / 16), cy = Math.floor(y2 / 16), cz = Math.floor(z2 / 16);
17769
17945
  const section = this._chunks.get(`${cx}_${cy}_${cz}`);
17770
- const index = (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16;
17946
+ const index = (y2 - cy * 16) * 256 + (z2 - cz * 16) * 16 + x2 - cx * 16;
17771
17947
  return (section?.fluidByteIndex(index) ?? 0) | (section?.isOccludingIndex(index) ? 64 : 0);
17948
+ },
17949
+ (x2, y2, z2) => this.getBiomeAt(x2, y2, z2),
17950
+ async (biomes) => {
17951
+ if (this._chunks.get(key) !== chunk) return;
17952
+ const columnKey = `${x}_${z}`;
17953
+ let column = this.biomes.get(columnKey);
17954
+ if (biomes) {
17955
+ if (!column) this.biomes.set(columnKey, column = /* @__PURE__ */ new Map());
17956
+ column.set(y, biomes);
17957
+ } else {
17958
+ column?.delete(y);
17959
+ if (!column?.size) this.biomes.delete(columnKey);
17960
+ }
17961
+ await chunk.refreshBiomes();
17962
+ await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
17772
17963
  }
17773
17964
  );
17774
17965
  this._chunks.set(key, chunk);
@@ -17823,6 +18014,7 @@ function buildSectionGeometry(input) {
17823
18014
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
17824
18015
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
17825
18016
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
18017
+ if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
17826
18018
  for (const template of templateData) {
17827
18019
  const { quads } = template;
17828
18020
  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) {
@@ -17900,7 +18092,9 @@ function buildSectionGeometry(input) {
17900
18092
  height
17901
18093
  });
17902
18094
  }
17903
- fluidPages.get(fluid).fluids.push({ quads, x, y, z, data });
18095
+ const tintIndex = (x + z * 16 + y * 256) * 3;
18096
+ const tint = fluid === "water" && fluids.waterTints ? fluids.waterTints.subarray(tintIndex, tintIndex + 3) : data.tint;
18097
+ fluidPages.get(fluid).fluids.push({ quads, x, y, z, data, tint });
17904
18098
  }
17905
18099
  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));
17906
18100
  }
@@ -17949,7 +18143,7 @@ function buildSectionGeometry(input) {
17949
18143
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
17950
18144
  }
17951
18145
  }
17952
- for (const { quads, x, y, z, data } of page.fluids) {
18146
+ for (const { quads, x, y, z, data, tint } of page.fluids) {
17953
18147
  const atlas = atlases[data.atlas];
17954
18148
  const placement = page.placements[0];
17955
18149
  const mapU = (u) => (placement.x + u * atlas.width) / page.width;
@@ -17973,7 +18167,7 @@ function buildSectionGeometry(input) {
17973
18167
  mapU((rect[0] + rect[2]) / atlas.width),
17974
18168
  mapV(1 - rect[1] / atlas.height)
17975
18169
  ], vertex * 4);
17976
- colors.set(data.tint, vertex * 3);
18170
+ colors.set(tint, vertex * 3);
17977
18171
  }
17978
18172
  outputIndices.set([start, start + 1, start + 2, start, start + 2, start + 3], indexOffset);
17979
18173
  indexOffset += 6;
@@ -18114,7 +18308,7 @@ function toInput(entries, maxAtlasSize, fluids) {
18114
18308
  input.cullMasks[i] = cullMask;
18115
18309
  }
18116
18310
  if (fluids) {
18117
- input.fluids = { cells: fluids.cells };
18311
+ input.fluids = { cells: fluids.cells, waterTints: fluids.waterTints };
18118
18312
  for (const kind of ["water", "lava"]) {
18119
18313
  const atlas = fluids[kind];
18120
18314
  if (!atlas) continue;
@@ -18215,11 +18409,14 @@ var SectionMesh = class _SectionMesh extends import_three43.Group {
18215
18409
  init_FluidGeometry();
18216
18410
  init_FluidQuads();
18217
18411
  init_CubeFace();
18412
+ var WATER_TINT2 = new import_three44.Color(4159204).toArray();
18218
18413
  var Chunk = class _Chunk {
18219
- constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell) {
18414
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged) {
18220
18415
  this.onBlocksChanged = onBlocksChanged;
18221
18416
  this.sectionModels = sectionModels;
18222
18417
  this.neighborCell = neighborCell;
18418
+ this.biomeAt = biomeAt;
18419
+ this.onBiomesChanged = onBiomesChanged;
18223
18420
  this.data = new ChunkData();
18224
18421
  this.renderedBlocks = /* @__PURE__ */ new Map();
18225
18422
  this.sectionBlocks = /* @__PURE__ */ new Map();
@@ -18244,6 +18441,56 @@ var Chunk = class _Chunk {
18244
18441
  const index = _Chunk.chunkPosToBlockIndex(pos);
18245
18442
  return this.renderedBlocks.get(index);
18246
18443
  }
18444
+ getBiomeAt(posOrX, y, z) {
18445
+ if (typeof posOrX === "number") return this.getBiomeAt(new import_three44.Vector3(posOrX, y, z));
18446
+ if (isTripleArray(posOrX)) return this.getBiomeAt(new import_three44.Vector3(...posOrX));
18447
+ const pos = this.worldPosToChunkPos(posOrX);
18448
+ _Chunk.chunkPosToBlockIndex(pos);
18449
+ 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];
18450
+ }
18451
+ /** Copies 64 biome IDs in x + z * 4 + y * 16 order and refreshes placed blocks. Omit the array to clear biome data. */
18452
+ setBiomes(biomes) {
18453
+ const copy = biomes ? [...biomes] : void 0;
18454
+ if (copy && (copy.length !== 64 || !copy.every((id) => typeof id === "string" && id.length > 0))) {
18455
+ throw new RangeError("Section biomes must contain 64 nonempty biome IDs");
18456
+ }
18457
+ if (this.onBiomesChanged) return this.onBiomesChanged(copy);
18458
+ this.biomes = copy;
18459
+ return this.refreshBiomes();
18460
+ }
18461
+ /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
18462
+ async refreshBiomes() {
18463
+ const affected = /* @__PURE__ */ new Map();
18464
+ const blocks = [];
18465
+ for (const index of this.renderedBlocks.keys()) {
18466
+ const block = this.data.get(index);
18467
+ const stateKey = ChunkData.paletteKey(block);
18468
+ let matches = affected.get(stateKey);
18469
+ if (!matches) {
18470
+ const key = AssetKey.parse("blockstates", block.type);
18471
+ matches = BlockStates.get(key).then(async (state) => {
18472
+ const origin = state?.key ?? key;
18473
+ const [source, fluid] = await Promise.all([BlockTints.biomeSource(origin), getBlockFluidState(origin, block.properties)]);
18474
+ return !!source || fluid?.kind === "water";
18475
+ });
18476
+ affected.set(stateKey, matches);
18477
+ }
18478
+ if (await matches) blocks.push({ index, block });
18479
+ }
18480
+ if (!blocks.length) return;
18481
+ const hidden = new Set(this.hiddenBlocks);
18482
+ try {
18483
+ await this.placeBlocks(blocks);
18484
+ } finally {
18485
+ for (const index of hidden) {
18486
+ this.hiddenBlocks.add(index);
18487
+ this.renderedBlocks.get(index)?.object?.setVisible(false);
18488
+ }
18489
+ const positions = blocks.map(({ index }) => new import_three44.Vector3(...this.positionAt(index)));
18490
+ if (this.onBlocksChanged) await this.onBlocksChanged(positions);
18491
+ else await this.rebuildSectionMesh();
18492
+ }
18493
+ }
18247
18494
  async setBlockAt(posOrX, yOrBlock, z, block) {
18248
18495
  if (typeof posOrX == "number") {
18249
18496
  return this.setBlockAt(new import_three44.Vector3(posOrX, yOrBlock, z), block);
@@ -18282,14 +18529,15 @@ var Chunk = class _Chunk {
18282
18529
  nbt: block.nbt === void 0 ? void 0 : structuredClone(block.nbt)
18283
18530
  } : void 0
18284
18531
  }));
18285
- const states = new Map(placements.filter(({ key }) => key !== void 0).map(({ key, block }) => [key, block]));
18286
- const settled = await Promise.allSettled([...states.values()].map(async (stored) => {
18287
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18288
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18289
- const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18290
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties);
18291
- return { blockState: blockState3, perBlock, fluid, prepared, stored };
18292
- }));
18532
+ const states = /* @__PURE__ */ new Map();
18533
+ for (const { index, key, block } of placements) {
18534
+ if (key === void 0) continue;
18535
+ let state = states.get(key);
18536
+ if (!state) states.set(key, state = { stored: block, indices: [] });
18537
+ state.indices.push(index);
18538
+ }
18539
+ const biomes = /* @__PURE__ */ new Map();
18540
+ const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
18293
18541
  const resolutions = new Map([...states.keys()].map((key, i) => [key, settled[i]]));
18294
18542
  const positions = [];
18295
18543
  let failed = false, failure;
@@ -18313,7 +18561,8 @@ var Chunk = class _Chunk {
18313
18561
  if (!readBlock) continue;
18314
18562
  const resolved = resolutions.get(key);
18315
18563
  if (resolved.status === "rejected") throw resolved.reason;
18316
- const { blockState: blockState3, perBlock, fluid, prepared, stored } = resolved.value;
18564
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts, stored } = resolved.value;
18565
+ const context = contexts.get(index);
18317
18566
  if (!blockState3) {
18318
18567
  this.data.assign(index, 0);
18319
18568
  continue;
@@ -18321,9 +18570,10 @@ var Chunk = class _Chunk {
18321
18570
  const variantPosition = worldPos.toArray();
18322
18571
  if (fluid) {
18323
18572
  this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18573
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18324
18574
  this.meshDirty = true;
18325
18575
  }
18326
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18576
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18327
18577
  if (templates) {
18328
18578
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18329
18579
  this.meshDirty = true;
@@ -18333,7 +18583,9 @@ var Chunk = class _Chunk {
18333
18583
  instanceMeshes: true,
18334
18584
  maxInstanceCount: 2e3,
18335
18585
  initialState: stored.properties,
18336
- variantPosition
18586
+ variantPosition,
18587
+ biome: context?.biome,
18588
+ biomePosition: this.biomePosition(index, blockState3, stored)
18337
18589
  });
18338
18590
  object4.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
18339
18591
  }
@@ -18356,24 +18608,21 @@ var Chunk = class _Chunk {
18356
18608
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
18357
18609
  this.sectionBlocks.clear();
18358
18610
  this.fluidCells.fill(0);
18611
+ this.waterTints = void 0;
18359
18612
  this.renderedBlocks.clear();
18360
18613
  this.hiddenBlocks.clear();
18361
18614
  this.meshDirty = true;
18362
18615
  this.data.copyFrom(data);
18363
- const ids = /* @__PURE__ */ new Set();
18616
+ const ids = /* @__PURE__ */ new Map();
18364
18617
  for (let index = 0; index < 4096; index++) {
18365
18618
  const id = this.data.idAt(index);
18366
- if (id) ids.add(id);
18367
- }
18368
- const palette = [...ids];
18369
- const settled = await Promise.allSettled(palette.map(async (id) => {
18370
- const state = this.data.stateAt(id);
18371
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", state.type));
18372
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18373
- const fluid = await getBlockFluidState(blockState3?.key, state.properties);
18374
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, state.properties);
18375
- return { blockState: blockState3, perBlock, fluid, prepared };
18376
- }));
18619
+ if (!id) continue;
18620
+ let indices = ids.get(id);
18621
+ if (!indices) ids.set(id, indices = []);
18622
+ indices.push(index);
18623
+ }
18624
+ const palette = [...ids.keys()], biomes = /* @__PURE__ */ new Map();
18625
+ const settled = await Promise.allSettled(palette.map((id) => this.resolveState(this.data.stateAt(id), ids.get(id), biomes)));
18377
18626
  const resolutions = new Map(palette.map((id, i) => [id, settled[i]]));
18378
18627
  const perBlocks = [];
18379
18628
  let failed = false, failure;
@@ -18383,24 +18632,28 @@ var Chunk = class _Chunk {
18383
18632
  try {
18384
18633
  const resolved = resolutions.get(id);
18385
18634
  if (resolved.status === "rejected") throw resolved.reason;
18386
- const { blockState: blockState3, perBlock, fluid, prepared } = resolved.value;
18635
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts } = resolved.value;
18636
+ const context = contexts.get(index);
18387
18637
  if (!blockState3) {
18388
18638
  this.data.assign(index, 0);
18389
18639
  this.fluidCells[index] = 0;
18390
18640
  continue;
18391
18641
  }
18392
- if (fluid) this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18642
+ if (fluid) {
18643
+ this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18644
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18645
+ }
18393
18646
  const variantPosition = [
18394
18647
  this.x * 16 + index % 16,
18395
18648
  this.y * 16 + Math.floor(index / 256),
18396
18649
  this.z * 16 + Math.floor(index / 16) % 16
18397
18650
  ];
18398
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18651
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18399
18652
  if (templates) {
18400
18653
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18401
18654
  this.meshDirty = true;
18402
18655
  } else {
18403
- perBlocks.push({ index, blockState: blockState3 });
18656
+ perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
18404
18657
  }
18405
18658
  const readBlock = this.data.snapshot(index);
18406
18659
  this.renderedBlocks.set(index, { get block() {
@@ -18413,7 +18666,7 @@ var Chunk = class _Chunk {
18413
18666
  failed = true;
18414
18667
  }
18415
18668
  }
18416
- for (const { index, blockState: blockState3 } of perBlocks) {
18669
+ for (const { index, blockState: blockState3, biome } of perBlocks) {
18417
18670
  let object4;
18418
18671
  try {
18419
18672
  object4 = await this.scene.addBlock(blockState3, {
@@ -18421,11 +18674,9 @@ var Chunk = class _Chunk {
18421
18674
  instanceMeshes: true,
18422
18675
  maxInstanceCount: 2e3,
18423
18676
  initialState: this.data.stateAt(this.data.idAt(index)).properties,
18424
- variantPosition: [
18425
- this.x * 16 + index % 16,
18426
- this.y * 16 + Math.floor(index / 256),
18427
- this.z * 16 + Math.floor(index / 16) % 16
18428
- ]
18677
+ variantPosition: this.positionAt(index),
18678
+ biome,
18679
+ biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
18429
18680
  });
18430
18681
  object4.setPosition(new import_three44.Vector3(
18431
18682
  this.x * 256 + index % 16 * 16,
@@ -18444,6 +18695,68 @@ var Chunk = class _Chunk {
18444
18695
  }
18445
18696
  if (failed) throw failure;
18446
18697
  }
18698
+ positionAt(index) {
18699
+ return [this.x * 16 + index % 16, this.y * 16 + Math.floor(index / 256), this.z * 16 + Math.floor(index / 16) % 16];
18700
+ }
18701
+ biomePosition(index, blockState3, stored) {
18702
+ const position = this.positionAt(index), name = blockState3.key?.toNamespacedString();
18703
+ if (stored.properties?.half === "upper" && (name === "minecraft:tall_grass" || name === "minecraft:large_fern")) position[1]--;
18704
+ return position;
18705
+ }
18706
+ async resolveState(stored, indices, biomes) {
18707
+ const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18708
+ const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18709
+ const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18710
+ 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;
18711
+ const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
18712
+ const prepare = (color2) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color2);
18713
+ const contexts = /* @__PURE__ */ new Map();
18714
+ const groups = /* @__PURE__ */ new Map();
18715
+ if (blockState3 && (source || fluid?.kind === "water")) {
18716
+ for (const index of indices) {
18717
+ const position = this.biomePosition(index, blockState3, stored);
18718
+ const biome = this.biomeAt ? this.biomeAt(...position) : position[1] >= this.y * 16 ? this.getBiomeAt(position) : void 0;
18719
+ if (!biome) continue;
18720
+ const key = JSON.stringify(source === "grass" ? [biome, position[0], position[2]] : [biome]);
18721
+ let group = groups.get(key);
18722
+ if (!group) groups.set(key, group = { biome, position, indices: [] });
18723
+ group.indices.push(index);
18724
+ }
18725
+ }
18726
+ const groupedCount = [...groups.values()].reduce((sum, group) => sum + group.indices.length, 0);
18727
+ const prepared = groupedCount < indices.length ? await prepare() : void 0;
18728
+ await Promise.all([...groups.values()].map(async ({ biome, position, indices: indices2 }) => {
18729
+ const available = () => {
18730
+ const key = JSON.stringify([biome, blockState3?.key?.root]);
18731
+ let found = biomes.get(key);
18732
+ if (!found) {
18733
+ found = Biomes.get(biome, blockState3?.key).then((value) => value !== void 0);
18734
+ biomes.set(key, found);
18735
+ }
18736
+ return found;
18737
+ };
18738
+ const context = { biome };
18739
+ if (perBlock) {
18740
+ if (!await available()) context.biome = void 0;
18741
+ } else {
18742
+ context.prepared = await prepare(async () => await available() ? BlockTints.getBiomeColor(blockState3?.key, biome, position) : void 0);
18743
+ if (fluid?.kind === "water" && await available()) {
18744
+ const color2 = await Biomes.getColor(biome, "water", blockState3?.key);
18745
+ if (color2 !== void 0 && color2 !== 4159204) context.waterTint = new import_three44.Color(color2).toArray();
18746
+ }
18747
+ }
18748
+ for (const index of indices2) contexts.set(index, context);
18749
+ }));
18750
+ return { blockState: blockState3, perBlock, fluid, prepared, contexts, stored };
18751
+ }
18752
+ setWaterTint(index, tint) {
18753
+ if (!this.sectionModels || !this.waterTints && tint === void 0) return;
18754
+ if (!this.waterTints) {
18755
+ this.waterTints = new Float32Array(4096 * 3);
18756
+ for (let cell = 0; cell < 4096; cell++) this.waterTints.set(WATER_TINT2, cell * 3);
18757
+ }
18758
+ this.waterTints.set(tint ?? WATER_TINT2, index * 3);
18759
+ }
18447
18760
  /** Changes visibility at world block coordinates while preserving the block's data. */
18448
18761
  async setBlockVisibleAt(pos, visible) {
18449
18762
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
@@ -18547,7 +18860,7 @@ var Chunk = class _Chunk {
18547
18860
  if (block && !block.object) origins.set(kind, block.block.type);
18548
18861
  }
18549
18862
  if (origins.size) {
18550
- fluids = { cells: new Uint8Array(18 * 18 * 18) };
18863
+ fluids = { cells: new Uint8Array(18 * 18 * 18), waterTints: this.waterTints?.slice() };
18551
18864
  for (let y = -1; y <= 16; y++) {
18552
18865
  for (let z = -1; z <= 16; z++) {
18553
18866
  for (let x = -1; x <= 16; x++) {
@@ -18589,8 +18902,13 @@ var Chunk = class _Chunk {
18589
18902
  this.renderedBlocks.clear();
18590
18903
  this.hiddenBlocks.clear();
18591
18904
  this.fluidCells.fill(0);
18905
+ this.waterTints = void 0;
18592
18906
  this.data.clear();
18593
- if (notify) await this.onBlocksChanged?.(positions);
18907
+ this.biomes = void 0;
18908
+ if (notify) {
18909
+ await this.onBiomesChanged?.(void 0);
18910
+ await this.onBlocksChanged?.(positions);
18911
+ }
18594
18912
  }
18595
18913
  async dispose() {
18596
18914
  await this.clear();