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.
@@ -324,6 +324,9 @@ var init_Caching = __esm({
324
324
  static {
325
325
  this.usernameUuidCache = import_loading_cache.Caches.builder().expireAfterWrite(import_time.Time.minutes(5)).expirationInterval(import_time.Time.seconds(30)).buildAsync();
326
326
  }
327
+ static {
328
+ this.biomeCache = _Caching.createAssetCache();
329
+ }
327
330
  static createAssetCache() {
328
331
  return import_loading_cache.Caches.builder().expireAfterWrite(import_time.Time.minutes(10)).expireAfterAccess(import_time.Time.minutes(5)).expirationInterval(import_time.Time.seconds(30)).buildAsync();
329
332
  }
@@ -353,7 +356,8 @@ var init_Caching = __esm({
353
356
  this.fontCache,
354
357
  this.bannerPatternCache,
355
358
  this.mineRenderDataCache,
356
- this.usernameUuidCache
359
+ this.usernameUuidCache,
360
+ this.biomeCache
357
361
  ];
358
362
  }
359
363
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
@@ -1731,7 +1735,7 @@ ${key}` : key, async () => {
1731
1735
  case "blockStates":
1732
1736
  return Object.values(value).every((property) => object(property) && "default" in property && Array.isArray(property.values));
1733
1737
  case "blockTints":
1734
- return ["constant", "grass", "stem", "redstone"].includes(value.source) && (value.source !== "constant" || Number.isInteger(value.color));
1738
+ 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));
1735
1739
  case "fluids":
1736
1740
  return ["water", "lava"].includes(value.kind) && typeof value.renderModel === "boolean" && (value.levelProperty === void 0 || typeof value.levelProperty === "string");
1737
1741
  case "dyes":
@@ -1875,15 +1879,19 @@ var init_Colormaps = __esm({
1875
1879
  Colormaps = class {
1876
1880
  /** Samples the selected resource pack's grass colormap using Minecraft's climate coordinates. */
1877
1881
  static async grassColor(origin, temperature = 0.5, downfall = 1) {
1878
- const key = AssetKey.parse("textures", "minecraft:colormap/grass", origin);
1882
+ return this.getColor("grass", origin, temperature, downfall);
1883
+ }
1884
+ /** Samples a resource-pack colormap at the supplied climate coordinates. */
1885
+ static async getColor(kind, origin, temperature = 0.5, downfall = 1) {
1886
+ const key = AssetKey.parse("textures", `minecraft:colormap/${kind}`, origin);
1879
1887
  const temp = Math.fround(temperature);
1880
1888
  const rain = Math.fround(downfall) * temp;
1881
1889
  const x = Math.floor((1 - temp) * 255), y = Math.floor((1 - rain) * 255);
1882
1890
  return await Caching.blockTintCache.get(`${key.serialize()}|${x},${y}`, async () => {
1883
1891
  const image = await ModelTextures.get(key);
1884
- if (!image) throw new MineRenderError(`Missing grass colormap: ${key.toNamespacedString()}`);
1892
+ if (!image) throw new MineRenderError(`Missing ${kind} colormap: ${key.toNamespacedString()}`);
1885
1893
  if (image.width !== 256 || image.height !== 256) {
1886
- throw new MineRenderError("Grass colormaps must be 256 \xD7 256 pixels");
1894
+ throw new MineRenderError(`${kind} colormaps must be 256 \xD7 256 pixels`);
1887
1895
  }
1888
1896
  const [red, green, blue] = image.data.getImageData(x, y, 1, 1).data;
1889
1897
  return red << 16 | green << 8 | blue;
@@ -7614,6 +7622,7 @@ __export(index_node_exports, {
7614
7622
  BannerPatterns: () => BannerPatterns,
7615
7623
  BasicAssetKey: () => BasicAssetKey,
7616
7624
  BatchedExecutor: () => BatchedExecutor,
7625
+ Biomes: () => Biomes,
7617
7626
  BlockEntities: () => BlockEntities,
7618
7627
  BlockObject: () => BlockObject,
7619
7628
  BlockStateResolver: () => BlockStateResolver,
@@ -7956,6 +7965,116 @@ init_AssetLoader();
7956
7965
  init_Assets();
7957
7966
  init_BannerPatterns();
7958
7967
 
7968
+ // src/assets/Biomes.ts
7969
+ init_AssetKey();
7970
+ init_AssetLoader();
7971
+ init_AssetParsers();
7972
+ init_Caching();
7973
+ init_MineRenderError();
7974
+ init_Colormaps();
7975
+
7976
+ // src/assets/_biomes/SwampGrassColor.ts
7977
+ var permutation;
7978
+ 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]];
7979
+ var skew = (Math.sqrt(3) - 1) / 2;
7980
+ var unskew = (3 - Math.sqrt(3)) / 6;
7981
+ function createPermutation() {
7982
+ const multiplier = 0x5deece66dn, mask = (1n << 48n) - 1n;
7983
+ let seed = 2345n ^ multiplier;
7984
+ const next = (bits) => {
7985
+ seed = seed * multiplier + 11n & mask;
7986
+ return Number(seed >> BigInt(48 - bits));
7987
+ };
7988
+ for (let i = 0; i < 3; i++) {
7989
+ next(26);
7990
+ next(27);
7991
+ }
7992
+ const values = Uint8Array.from({ length: 256 }, (_, i) => i);
7993
+ for (let i = 0; i < 256; i++) {
7994
+ const bound = 256 - i;
7995
+ let offset;
7996
+ if ((bound & bound - 1) === 0) offset = Math.floor(bound * next(31) / 2147483648);
7997
+ else {
7998
+ let bits;
7999
+ do {
8000
+ bits = next(31);
8001
+ offset = bits % bound;
8002
+ } while (bits - offset + bound - 1 > 2147483647);
8003
+ }
8004
+ const other = i + offset;
8005
+ [values[i], values[other]] = [values[other], values[i]];
8006
+ }
8007
+ return values;
8008
+ }
8009
+ function swampGrassColor(x, z) {
8010
+ const p10 = permutation ??= createPermutation();
8011
+ x *= 0.0225;
8012
+ z *= 0.0225;
8013
+ const s = (x + z) * skew;
8014
+ const i = Math.floor(x + s), j = Math.floor(z + s);
8015
+ const t = (i + j) * unskew;
8016
+ const dx = x - (i - t), dz = z - (j - t);
8017
+ const stepX = dx > dz ? 1 : 0, stepZ = 1 - stepX;
8018
+ const corner = (gx, gz, a, b) => {
8019
+ let falloff = 0.5 - a * a - b * b;
8020
+ if (falloff < 0) return 0;
8021
+ const gradient = gradients[p10[gx + p10[gz & 255] & 255] % 12];
8022
+ falloff *= falloff;
8023
+ return falloff * falloff * (gradient[0] * a + gradient[1] * b);
8024
+ };
8025
+ 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));
8026
+ return noise < -0.1 ? 5011004 : 6975545;
8027
+ }
8028
+
8029
+ // src/assets/Biomes.ts
8030
+ var Biomes = class {
8031
+ /** Loads a biome registry entry, or returns `undefined` when no source provides it. */
8032
+ static async get(id, origin) {
8033
+ if (!/^(?:[a-z0-9_.-]+:)?[a-z0-9_./-]+$/.test(id)) throw new MineRenderError(`Invalid biome identifier: ${id}`);
8034
+ const [namespace, path] = id.includes(":") ? id.split(":") : ["minecraft", id];
8035
+ const key = new AssetKey(namespace, path, "worldgen", "biome", "data", ".json", origin?.root);
8036
+ return Caching.biomeCache.get(key.serialize(), async () => {
8037
+ const asset2 = await AssetLoader.get(key, "json" /* JSON */);
8038
+ if (asset2 === void 0) return void 0;
8039
+ if (!asset2 || typeof asset2 !== "object" || Array.isArray(asset2) || !asset2.effects || typeof asset2.effects !== "object" || Array.isArray(asset2.effects)) {
8040
+ throw new MineRenderError(`Invalid biome effects: ${id}`);
8041
+ }
8042
+ if (typeof asset2.temperature !== "number" || !Number.isFinite(asset2.temperature) || typeof asset2.downfall !== "number" || !Number.isFinite(asset2.downfall)) {
8043
+ throw new MineRenderError(`Invalid biome climate: ${id}`);
8044
+ }
8045
+ for (const kind of ["grass", "foliage", "dry_foliage", "water"]) {
8046
+ const color2 = asset2.effects[`${kind}_color`];
8047
+ if (color2 !== void 0) this.parseColor(color2, id, kind);
8048
+ }
8049
+ const modifier = asset2.effects.grass_color_modifier;
8050
+ if (modifier !== void 0 && modifier !== "none" && modifier !== "dark_forest" && modifier !== "swamp") {
8051
+ throw new MineRenderError(`Invalid biome grass color modifier: ${id}`);
8052
+ }
8053
+ return asset2;
8054
+ });
8055
+ }
8056
+ /** Returns packed sRGB, or `undefined` when the biome or requested water color is missing. */
8057
+ static async getColor(id, kind, origin, x = 0, z = 0) {
8058
+ const biome = await this.get(id, origin);
8059
+ if (!biome) return void 0;
8060
+ const rawColor = biome.effects[`${kind}_color`];
8061
+ let color2 = rawColor === void 0 ? void 0 : this.parseColor(rawColor, id, kind);
8062
+ if (kind === "water") return color2;
8063
+ const modifier = kind === "grass" ? biome.effects.grass_color_modifier : void 0;
8064
+ if (modifier === "swamp") return swampGrassColor(x, z);
8065
+ if (color2 === void 0) {
8066
+ const clamp = (value) => Math.max(0, Math.min(1, Math.fround(value)));
8067
+ color2 = await Colormaps.getColor(kind, origin, clamp(biome.temperature), clamp(biome.downfall));
8068
+ }
8069
+ return modifier === "dark_forest" ? (color2 & 16711422) + 2634762 >> 1 : color2;
8070
+ }
8071
+ static parseColor(value, id, kind) {
8072
+ if (typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value)) return parseInt(value.slice(1), 16);
8073
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 16777215) return value;
8074
+ throw new MineRenderError(`Invalid biome ${kind} color: ${id}`);
8075
+ }
8076
+ };
8077
+
7959
8078
  // src/assets/BlockEntities.ts
7960
8079
  var import_three10 = require("three");
7961
8080
  init_AssetKey();
@@ -12394,8 +12513,20 @@ var BlockStateResolver = class _BlockStateResolver {
12394
12513
  init_Colormaps();
12395
12514
  init_MineRenderData();
12396
12515
  var BlockTints = class {
12397
- /** Resolves block preview colors without biome context; explicit tints take precedence. */
12398
- static async get(key, state, model, tints) {
12516
+ /** Returns a block's versioned biome color source, leaving fixed-color blocks unchanged. */
12517
+ static async biomeSource(key) {
12518
+ const rule = key && (await MineRenderData.get("blockTints", key.root))[key.toNamespacedString()];
12519
+ return rule ? rule.biome ?? (rule.source === "grass" ? "grass" : void 0) : void 0;
12520
+ }
12521
+ /** Resolves one block's biome tint at world block coordinates, without biome blending. */
12522
+ static async getBiomeColor(key, biome, position) {
12523
+ if (!biome) return void 0;
12524
+ const x = position?.[0], z = position?.[2];
12525
+ const source = await this.biomeSource(key);
12526
+ return source ? Biomes.getColor(biome, source, key, x, z) : void 0;
12527
+ }
12528
+ /** Resolves used tint indices from biome or preview colors; explicit tints take precedence. */
12529
+ static async get(key, state, model, tints, biomeColor) {
12399
12530
  if (!key) return tints;
12400
12531
  const candidates = (model.elements ?? []).flatMap((element) => Object.values(element.faces)).some((face) => face?.tintindex !== void 0 && face.tintindex >= 0 && tints?.[face.tintindex] === void 0);
12401
12532
  if (!candidates) return tints;
@@ -12411,8 +12542,12 @@ var BlockTints = class {
12411
12542
  }
12412
12543
  }
12413
12544
  if (!indices.size) return tints;
12545
+ const source = rule.biome ?? (rule.source === "grass" ? "grass" : void 0);
12546
+ const biome = source ? typeof biomeColor === "function" ? await biomeColor() : biomeColor : void 0;
12414
12547
  let color2;
12415
- switch (rule.source) {
12548
+ if (biome !== void 0) {
12549
+ color2 = biome;
12550
+ } else switch (rule.source) {
12416
12551
  case "constant":
12417
12552
  color2 = rule.color;
12418
12553
  break;
@@ -12471,6 +12606,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12471
12606
  this._entityGeneration = 0;
12472
12607
  this.options = (0, import_ts_deepmerge6.default)({}, _BlockObject.DEFAULT_OPTIONS, options ?? {});
12473
12608
  this.variantPosition = options?.variantPosition ? [...options.variantPosition] : void 0;
12609
+ this.biomePosition = options?.biomePosition ? [...options.biomePosition] : void 0;
12474
12610
  }
12475
12611
  static {
12476
12612
  this.DEFAULT_OPTIONS = (0, import_ts_deepmerge6.default)({}, ModelObject.DEFAULT_OPTIONS, {
@@ -12668,11 +12804,16 @@ var BlockObject = class _BlockObject extends SceneObject {
12668
12804
  const { FluidModelObject: FluidModelObject2, sampleFluid: sampleFluid2 } = await Promise.resolve().then(() => (init_FluidModelObject(), FluidModelObject_exports));
12669
12805
  this._fluidSampler = sample;
12670
12806
  const surface = sampleFluid2(kind, (x, y, z) => x === 0 && y === 0 && z === 0 ? { fluid: kind, level: this.fluidLevel } : sample?.(x, y, z) ?? {});
12671
- if (this._fluidKey === surface.key) return;
12807
+ const tint = this.options.tints?.[0] ?? this.waterTint;
12808
+ const fluidKey = `${surface.key}|${tint ?? "default"}`;
12809
+ if (this._fluidKey === fluidKey) return;
12672
12810
  const key = new AssetKey("minecraft", `${kind}/${surface.key}`, "models", "fluid", "assets", ".json", this.blockState.key?.root);
12673
12811
  const replacement = await this.scene.addSceneObject(
12674
12812
  { key },
12675
- () => new FluidModelObject2(kind, surface.sample, this.blockState.key, this.options)
12813
+ () => new FluidModelObject2(kind, surface.sample, this.blockState.key, {
12814
+ ...this.options,
12815
+ tints: tint === void 0 ? this.options.tints : { ...this.options.tints, 0: tint }
12816
+ })
12676
12817
  );
12677
12818
  const previous = this._fluidModel;
12678
12819
  const matrix = previous ? this.getModelMatrix(previous) : new import_three34.Matrix4().makeTranslation(...this.position.toArray());
@@ -12681,7 +12822,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12681
12822
  this._models = this._models.filter((model) => model !== previous);
12682
12823
  this._models.push(replacement);
12683
12824
  this._fluidModel = replacement;
12684
- this._fluidKey = surface.key;
12825
+ this._fluidKey = fluidKey;
12685
12826
  this._isInstanced ||= isInstanceReference(replacement);
12686
12827
  this._instanceCounter = this._isInstanced ? 1 : 0;
12687
12828
  if (previous) this.removeModels([previous]);
@@ -12704,6 +12845,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12704
12845
  }
12705
12846
  async recreateModels() {
12706
12847
  this.clearModels();
12848
+ 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;
12707
12849
  if (resolveBlockFluidState(this.blockState.key, this.state, this._fluidRules)?.renderModel === false) {
12708
12850
  await this.updateFluid(this._fluidSampler);
12709
12851
  return;
@@ -12738,7 +12880,13 @@ var BlockObject = class _BlockObject extends SceneObject {
12738
12880
  const model = await Models.getMerged(modelKey);
12739
12881
  const options = {
12740
12882
  ...this.options,
12741
- tints: model ? await BlockTints.get(this.blockState.key, this.state, model, this.options.tints) : this.options.tints,
12883
+ tints: model ? await BlockTints.get(
12884
+ this.blockState.key,
12885
+ this.state,
12886
+ model,
12887
+ this.options.tints,
12888
+ () => BlockTints.getBiomeColor(this.blockState.key, this.options.biome, this.biomePosition)
12889
+ ) : this.options.tints,
12742
12890
  uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0,
12743
12891
  cullMask: this.options.displayPosition ? 0 : ModelCulling.toLocalMask(this._cullMask, rotation)
12744
12892
  };
@@ -17217,24 +17365,48 @@ var SectionModels = class {
17217
17365
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("maxAtlasSize must be a positive integer");
17218
17366
  }
17219
17367
  /** Returns templates for the matched parts, or `undefined` when the block needs an individual render object. */
17220
- async get(blockState3, properties = {}, position) {
17221
- const variantPosition = position ? [...position] : void 0;
17222
- return (await this.prepareState(blockState3, properties))?.pick(variantPosition);
17368
+ async get(blockState3, properties = {}, position, biome) {
17369
+ const biomePosition = position ? [...position] : void 0;
17370
+ const prepared = await this.state(blockState3, properties);
17371
+ if (!prepared) return void 0;
17372
+ return Promise.all(prepared.groups.map(async (group) => {
17373
+ const variant = BlockStateResolver.choose(group, biomePosition);
17374
+ const choice = prepared.choices.get(variant);
17375
+ const tints = await BlockTints.get(
17376
+ blockState3.key,
17377
+ prepared.state,
17378
+ choice.model,
17379
+ void 0,
17380
+ () => BlockTints.getBiomeColor(blockState3.key, biome, biomePosition)
17381
+ );
17382
+ return this.template(variant, choice, tints);
17383
+ }));
17223
17384
  }
17224
- /** Resolves section templates once, with a synchronous weighted choice for each cell. */
17225
- async prepareState(blockState3, properties = {}) {
17385
+ /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
17386
+ async prepareState(blockState3, properties = {}, biomeColor) {
17387
+ const prepared = await this.state(blockState3, properties);
17388
+ if (!prepared) return void 0;
17389
+ let color2;
17390
+ const resolveColor = typeof biomeColor === "function" ? () => color2 ??= biomeColor() : biomeColor;
17391
+ const templates = /* @__PURE__ */ new Map();
17392
+ await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
17393
+ const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
17394
+ templates.set(variant, await this.template(variant, choice, tints));
17395
+ }));
17396
+ return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
17397
+ }
17398
+ async state(blockState3, properties) {
17226
17399
  let states = this.states.get(blockState3);
17227
17400
  if (!states) this.states.set(blockState3, states = /* @__PURE__ */ new Map());
17228
17401
  const key = JSON.stringify(Object.entries(properties).sort(([a], [b]) => a.localeCompare(b)));
17229
- const prepared = await cached(states, key, () => this.prepare(blockState3, properties));
17230
- if (!prepared) return void 0;
17231
- return { pick: (position) => prepared.groups.map((group) => prepared.templates.get(BlockStateResolver.choose(group, position))) };
17402
+ return cached(states, key, () => this.prepare(blockState3, properties));
17232
17403
  }
17233
17404
  /** Returns the shared still/flow atlas used by per-block fluid objects. */
17234
17405
  fluidAtlas(kind, root) {
17235
- return cached(this.fluidAtlases, kind, async () => {
17406
+ const key = new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root);
17407
+ return cached(this.fluidAtlases, key.serialize(), async () => {
17236
17408
  const atlas = await UVMapper.getAtlas({
17237
- key: new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root),
17409
+ key,
17238
17410
  textures: { still: `minecraft:block/${kind}_still`, flow: `minecraft:block/${kind}_flow` },
17239
17411
  elements: []
17240
17412
  });
@@ -17246,64 +17418,67 @@ var SectionModels = class {
17246
17418
  const state = { ...await BlockStateResolver.defaults(blockState3), ...properties };
17247
17419
  const groups = BlockStateResolver.matching(blockState3, state);
17248
17420
  if (!groups.length) return void 0;
17249
- const templates = /* @__PURE__ */ new Map();
17421
+ const prepared = /* @__PURE__ */ new Map();
17250
17422
  for (const group of groups) {
17251
17423
  const choices = Array.isArray(group) ? group : [group];
17252
17424
  if (!choices.length) return void 0;
17253
17425
  for (const variant of choices) {
17254
17426
  if (!variant.model) return void 0;
17255
- const model = await Models.getMerged(AssetKey.parse("models", variant.model));
17427
+ const modelKey = AssetKey.parse("models", variant.model);
17428
+ modelKey.root = blockState3.key?.root;
17429
+ const model = await Models.getMerged(modelKey);
17256
17430
  if (!model?.elements?.length) return void 0;
17257
17431
  const atlas = await UVMapper.getAtlas(model);
17258
17432
  const rotation = BlockStateResolver.rotation(variant);
17259
17433
  if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
17260
- const tints = await BlockTints.get(blockState3.key, state, model);
17261
- let models = this.models.get(model);
17262
- if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17263
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17264
- const template = await cached(models, key, async () => {
17265
- const object4 = new SectionModel(model, {
17266
- instanceMeshes: false,
17267
- mergeMeshes: true,
17268
- tints,
17269
- uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17270
- });
17271
- await object4.init();
17272
- let geometry = object4.children[0].geometry;
17273
- geometry.applyMatrix4(new import_three41.Matrix4().makeRotationFromEuler(rotation));
17274
- object4.clear();
17275
- const keep = [];
17276
- const cullFaces = [];
17277
- for (const element of atlas.model.elements) {
17278
- for (const face of CUBE_FACES) {
17279
- const data = element.faces[face];
17280
- keep.push(data !== void 0);
17281
- if (!data) continue;
17282
- const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17283
- if (local < 0) {
17284
- cullFaces.push(0);
17285
- continue;
17286
- }
17287
- const normal = new import_three41.Vector3(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17288
- 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);
17289
- cullFaces.push(world < 0 ? 0 : 1 << world);
17290
- }
17291
- }
17292
- geometry = compactQuads(geometry, keep);
17293
- const template2 = {
17294
- geometry,
17295
- atlas,
17296
- cullFaces: new Uint8Array(cullFaces),
17297
- occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17298
- layer: atlas.hasTranslucency ? 1 : 0
17299
- };
17300
- this.templates.add(template2);
17301
- return template2;
17302
- });
17303
- templates.set(variant, template);
17434
+ prepared.set(variant, { model, atlas, rotation });
17304
17435
  }
17305
17436
  }
17306
- return { groups, templates };
17437
+ return { state, groups, choices: prepared };
17438
+ }
17439
+ async template(variant, { model, atlas, rotation }, tints) {
17440
+ let models = this.models.get(model);
17441
+ if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17442
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17443
+ return cached(models, key, async () => {
17444
+ const object4 = new SectionModel(model, {
17445
+ instanceMeshes: false,
17446
+ mergeMeshes: true,
17447
+ tints,
17448
+ uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17449
+ });
17450
+ await object4.init();
17451
+ let geometry = object4.children[0].geometry;
17452
+ geometry.applyMatrix4(new import_three41.Matrix4().makeRotationFromEuler(rotation));
17453
+ object4.clear();
17454
+ const keep = [];
17455
+ const cullFaces = [];
17456
+ for (const element of atlas.model.elements) {
17457
+ for (const face of CUBE_FACES) {
17458
+ const data = element.faces[face];
17459
+ keep.push(data !== void 0);
17460
+ if (!data) continue;
17461
+ const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17462
+ if (local < 0) {
17463
+ cullFaces.push(0);
17464
+ continue;
17465
+ }
17466
+ const normal = new import_three41.Vector3(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17467
+ 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);
17468
+ cullFaces.push(world < 0 ? 0 : 1 << world);
17469
+ }
17470
+ }
17471
+ geometry = compactQuads(geometry, keep);
17472
+ const template = {
17473
+ geometry,
17474
+ atlas,
17475
+ cullFaces: new Uint8Array(cullFaces),
17476
+ occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17477
+ layer: atlas.hasTranslucency ? 1 : 0
17478
+ };
17479
+ this.templates.add(template);
17480
+ return template;
17481
+ });
17307
17482
  }
17308
17483
  /** Disposes prepared geometry and clears this world's template caches. */
17309
17484
  clear() {
@@ -17633,7 +17808,6 @@ var MineRenderWorld = class {
17633
17808
  }
17634
17809
  try {
17635
17810
  await this.clearChunkColumn(chunk.x, chunk.z);
17636
- let sliceStart = performance.now();
17637
17811
  for (const section of chunk.sections) {
17638
17812
  if (section.biomes) {
17639
17813
  const position = new import_three42.Vector3(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17647,6 +17821,9 @@ var MineRenderWorld = class {
17647
17821
  if (!column) this.biomes.set(key, column = /* @__PURE__ */ new Map());
17648
17822
  column.set(section.y, copy);
17649
17823
  }
17824
+ }
17825
+ let sliceStart = performance.now();
17826
+ for (const section of chunk.sections) {
17650
17827
  if (!executor) {
17651
17828
  if (!occupiedSections.has(section.y)) continue;
17652
17829
  const position = new import_three42.Vector3(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17811,18 +17988,34 @@ var MineRenderWorld = class {
17811
17988
  const key = this.worldPosToChunkKey(pos);
17812
17989
  let chunk = this._chunks.get(key);
17813
17990
  if (typeof chunk === "undefined") {
17991
+ const x = Math.floor(pos.x / 16), y = Math.floor(pos.y / 16), z = Math.floor(pos.z / 16);
17814
17992
  chunk = new Chunk(
17815
17993
  this.scene,
17816
- Math.floor(pos.x / 16),
17817
- Math.floor(pos.y / 16),
17818
- Math.floor(pos.z / 16),
17994
+ x,
17995
+ y,
17996
+ z,
17819
17997
  (positions) => this.updateCulling(positions),
17820
17998
  this.sectionModels,
17821
- (x, y, z) => {
17822
- const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
17999
+ (x2, y2, z2) => {
18000
+ const cx = Math.floor(x2 / 16), cy = Math.floor(y2 / 16), cz = Math.floor(z2 / 16);
17823
18001
  const section = this._chunks.get(`${cx}_${cy}_${cz}`);
17824
- const index = (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16;
18002
+ const index = (y2 - cy * 16) * 256 + (z2 - cz * 16) * 16 + x2 - cx * 16;
17825
18003
  return (section?.fluidByteIndex(index) ?? 0) | (section?.isOccludingIndex(index) ? 64 : 0);
18004
+ },
18005
+ (x2, y2, z2) => this.getBiomeAt(x2, y2, z2),
18006
+ async (biomes) => {
18007
+ if (this._chunks.get(key) !== chunk) return;
18008
+ const columnKey = `${x}_${z}`;
18009
+ let column = this.biomes.get(columnKey);
18010
+ if (biomes) {
18011
+ if (!column) this.biomes.set(columnKey, column = /* @__PURE__ */ new Map());
18012
+ column.set(y, biomes);
18013
+ } else {
18014
+ column?.delete(y);
18015
+ if (!column?.size) this.biomes.delete(columnKey);
18016
+ }
18017
+ await chunk.refreshBiomes();
18018
+ await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
17826
18019
  }
17827
18020
  );
17828
18021
  this._chunks.set(key, chunk);
@@ -17877,6 +18070,7 @@ function buildSectionGeometry(input) {
17877
18070
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
17878
18071
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
17879
18072
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
18073
+ if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
17880
18074
  for (const template of templateData) {
17881
18075
  const { quads } = template;
17882
18076
  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) {
@@ -17954,7 +18148,9 @@ function buildSectionGeometry(input) {
17954
18148
  height
17955
18149
  });
17956
18150
  }
17957
- fluidPages.get(fluid).fluids.push({ quads, x, y, z, data });
18151
+ const tintIndex = (x + z * 16 + y * 256) * 3;
18152
+ const tint = fluid === "water" && fluids.waterTints ? fluids.waterTints.subarray(tintIndex, tintIndex + 3) : data.tint;
18153
+ fluidPages.get(fluid).fluids.push({ quads, x, y, z, data, tint });
17958
18154
  }
17959
18155
  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));
17960
18156
  }
@@ -18003,7 +18199,7 @@ function buildSectionGeometry(input) {
18003
18199
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
18004
18200
  }
18005
18201
  }
18006
- for (const { quads, x, y, z, data } of page.fluids) {
18202
+ for (const { quads, x, y, z, data, tint } of page.fluids) {
18007
18203
  const atlas = atlases[data.atlas];
18008
18204
  const placement = page.placements[0];
18009
18205
  const mapU = (u) => (placement.x + u * atlas.width) / page.width;
@@ -18027,7 +18223,7 @@ function buildSectionGeometry(input) {
18027
18223
  mapU((rect[0] + rect[2]) / atlas.width),
18028
18224
  mapV(1 - rect[1] / atlas.height)
18029
18225
  ], vertex * 4);
18030
- colors.set(data.tint, vertex * 3);
18226
+ colors.set(tint, vertex * 3);
18031
18227
  }
18032
18228
  outputIndices.set([start, start + 1, start + 2, start, start + 2, start + 3], indexOffset);
18033
18229
  indexOffset += 6;
@@ -18169,7 +18365,7 @@ function toInput(entries, maxAtlasSize, fluids) {
18169
18365
  input.cullMasks[i] = cullMask;
18170
18366
  }
18171
18367
  if (fluids) {
18172
- input.fluids = { cells: fluids.cells };
18368
+ input.fluids = { cells: fluids.cells, waterTints: fluids.waterTints };
18173
18369
  for (const kind of ["water", "lava"]) {
18174
18370
  const atlas = fluids[kind];
18175
18371
  if (!atlas) continue;
@@ -18270,11 +18466,14 @@ var SectionMesh = class _SectionMesh extends import_three43.Group {
18270
18466
  init_FluidGeometry();
18271
18467
  init_FluidQuads();
18272
18468
  init_CubeFace();
18469
+ var WATER_TINT2 = new import_three44.Color(4159204).toArray();
18273
18470
  var Chunk = class _Chunk {
18274
- constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell) {
18471
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged) {
18275
18472
  this.onBlocksChanged = onBlocksChanged;
18276
18473
  this.sectionModels = sectionModels;
18277
18474
  this.neighborCell = neighborCell;
18475
+ this.biomeAt = biomeAt;
18476
+ this.onBiomesChanged = onBiomesChanged;
18278
18477
  this.data = new ChunkData();
18279
18478
  this.renderedBlocks = /* @__PURE__ */ new Map();
18280
18479
  this.sectionBlocks = /* @__PURE__ */ new Map();
@@ -18299,6 +18498,56 @@ var Chunk = class _Chunk {
18299
18498
  const index = _Chunk.chunkPosToBlockIndex(pos);
18300
18499
  return this.renderedBlocks.get(index);
18301
18500
  }
18501
+ getBiomeAt(posOrX, y, z) {
18502
+ if (typeof posOrX === "number") return this.getBiomeAt(new import_three44.Vector3(posOrX, y, z));
18503
+ if (isTripleArray(posOrX)) return this.getBiomeAt(new import_three44.Vector3(...posOrX));
18504
+ const pos = this.worldPosToChunkPos(posOrX);
18505
+ _Chunk.chunkPosToBlockIndex(pos);
18506
+ 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];
18507
+ }
18508
+ /** Copies 64 biome IDs in x + z * 4 + y * 16 order and refreshes placed blocks. Omit the array to clear biome data. */
18509
+ setBiomes(biomes) {
18510
+ const copy = biomes ? [...biomes] : void 0;
18511
+ if (copy && (copy.length !== 64 || !copy.every((id) => typeof id === "string" && id.length > 0))) {
18512
+ throw new RangeError("Section biomes must contain 64 nonempty biome IDs");
18513
+ }
18514
+ if (this.onBiomesChanged) return this.onBiomesChanged(copy);
18515
+ this.biomes = copy;
18516
+ return this.refreshBiomes();
18517
+ }
18518
+ /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
18519
+ async refreshBiomes() {
18520
+ const affected = /* @__PURE__ */ new Map();
18521
+ const blocks = [];
18522
+ for (const index of this.renderedBlocks.keys()) {
18523
+ const block = this.data.get(index);
18524
+ const stateKey = ChunkData.paletteKey(block);
18525
+ let matches = affected.get(stateKey);
18526
+ if (!matches) {
18527
+ const key = AssetKey.parse("blockstates", block.type);
18528
+ matches = BlockStates.get(key).then(async (state) => {
18529
+ const origin = state?.key ?? key;
18530
+ const [source, fluid] = await Promise.all([BlockTints.biomeSource(origin), getBlockFluidState(origin, block.properties)]);
18531
+ return !!source || fluid?.kind === "water";
18532
+ });
18533
+ affected.set(stateKey, matches);
18534
+ }
18535
+ if (await matches) blocks.push({ index, block });
18536
+ }
18537
+ if (!blocks.length) return;
18538
+ const hidden = new Set(this.hiddenBlocks);
18539
+ try {
18540
+ await this.placeBlocks(blocks);
18541
+ } finally {
18542
+ for (const index of hidden) {
18543
+ this.hiddenBlocks.add(index);
18544
+ this.renderedBlocks.get(index)?.object?.setVisible(false);
18545
+ }
18546
+ const positions = blocks.map(({ index }) => new import_three44.Vector3(...this.positionAt(index)));
18547
+ if (this.onBlocksChanged) await this.onBlocksChanged(positions);
18548
+ else await this.rebuildSectionMesh();
18549
+ }
18550
+ }
18302
18551
  async setBlockAt(posOrX, yOrBlock, z, block) {
18303
18552
  if (typeof posOrX == "number") {
18304
18553
  return this.setBlockAt(new import_three44.Vector3(posOrX, yOrBlock, z), block);
@@ -18337,14 +18586,15 @@ var Chunk = class _Chunk {
18337
18586
  nbt: block.nbt === void 0 ? void 0 : structuredClone(block.nbt)
18338
18587
  } : void 0
18339
18588
  }));
18340
- const states = new Map(placements.filter(({ key }) => key !== void 0).map(({ key, block }) => [key, block]));
18341
- const settled = await Promise.allSettled([...states.values()].map(async (stored) => {
18342
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18343
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18344
- const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18345
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties);
18346
- return { blockState: blockState3, perBlock, fluid, prepared, stored };
18347
- }));
18589
+ const states = /* @__PURE__ */ new Map();
18590
+ for (const { index, key, block } of placements) {
18591
+ if (key === void 0) continue;
18592
+ let state = states.get(key);
18593
+ if (!state) states.set(key, state = { stored: block, indices: [] });
18594
+ state.indices.push(index);
18595
+ }
18596
+ const biomes = /* @__PURE__ */ new Map();
18597
+ const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
18348
18598
  const resolutions = new Map([...states.keys()].map((key, i) => [key, settled[i]]));
18349
18599
  const positions = [];
18350
18600
  let failed = false, failure;
@@ -18368,7 +18618,8 @@ var Chunk = class _Chunk {
18368
18618
  if (!readBlock) continue;
18369
18619
  const resolved = resolutions.get(key);
18370
18620
  if (resolved.status === "rejected") throw resolved.reason;
18371
- const { blockState: blockState3, perBlock, fluid, prepared, stored } = resolved.value;
18621
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts, stored } = resolved.value;
18622
+ const context = contexts.get(index);
18372
18623
  if (!blockState3) {
18373
18624
  this.data.assign(index, 0);
18374
18625
  continue;
@@ -18376,9 +18627,10 @@ var Chunk = class _Chunk {
18376
18627
  const variantPosition = worldPos.toArray();
18377
18628
  if (fluid) {
18378
18629
  this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18630
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18379
18631
  this.meshDirty = true;
18380
18632
  }
18381
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18633
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18382
18634
  if (templates) {
18383
18635
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18384
18636
  this.meshDirty = true;
@@ -18388,7 +18640,9 @@ var Chunk = class _Chunk {
18388
18640
  instanceMeshes: true,
18389
18641
  maxInstanceCount: 2e3,
18390
18642
  initialState: stored.properties,
18391
- variantPosition
18643
+ variantPosition,
18644
+ biome: context?.biome,
18645
+ biomePosition: this.biomePosition(index, blockState3, stored)
18392
18646
  });
18393
18647
  object4.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
18394
18648
  }
@@ -18411,24 +18665,21 @@ var Chunk = class _Chunk {
18411
18665
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
18412
18666
  this.sectionBlocks.clear();
18413
18667
  this.fluidCells.fill(0);
18668
+ this.waterTints = void 0;
18414
18669
  this.renderedBlocks.clear();
18415
18670
  this.hiddenBlocks.clear();
18416
18671
  this.meshDirty = true;
18417
18672
  this.data.copyFrom(data);
18418
- const ids = /* @__PURE__ */ new Set();
18673
+ const ids = /* @__PURE__ */ new Map();
18419
18674
  for (let index = 0; index < 4096; index++) {
18420
18675
  const id = this.data.idAt(index);
18421
- if (id) ids.add(id);
18422
- }
18423
- const palette = [...ids];
18424
- const settled = await Promise.allSettled(palette.map(async (id) => {
18425
- const state = this.data.stateAt(id);
18426
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", state.type));
18427
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18428
- const fluid = await getBlockFluidState(blockState3?.key, state.properties);
18429
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, state.properties);
18430
- return { blockState: blockState3, perBlock, fluid, prepared };
18431
- }));
18676
+ if (!id) continue;
18677
+ let indices = ids.get(id);
18678
+ if (!indices) ids.set(id, indices = []);
18679
+ indices.push(index);
18680
+ }
18681
+ const palette = [...ids.keys()], biomes = /* @__PURE__ */ new Map();
18682
+ const settled = await Promise.allSettled(palette.map((id) => this.resolveState(this.data.stateAt(id), ids.get(id), biomes)));
18432
18683
  const resolutions = new Map(palette.map((id, i) => [id, settled[i]]));
18433
18684
  const perBlocks = [];
18434
18685
  let failed = false, failure;
@@ -18438,24 +18689,28 @@ var Chunk = class _Chunk {
18438
18689
  try {
18439
18690
  const resolved = resolutions.get(id);
18440
18691
  if (resolved.status === "rejected") throw resolved.reason;
18441
- const { blockState: blockState3, perBlock, fluid, prepared } = resolved.value;
18692
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts } = resolved.value;
18693
+ const context = contexts.get(index);
18442
18694
  if (!blockState3) {
18443
18695
  this.data.assign(index, 0);
18444
18696
  this.fluidCells[index] = 0;
18445
18697
  continue;
18446
18698
  }
18447
- if (fluid) this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18699
+ if (fluid) {
18700
+ this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18701
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18702
+ }
18448
18703
  const variantPosition = [
18449
18704
  this.x * 16 + index % 16,
18450
18705
  this.y * 16 + Math.floor(index / 256),
18451
18706
  this.z * 16 + Math.floor(index / 16) % 16
18452
18707
  ];
18453
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18708
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18454
18709
  if (templates) {
18455
18710
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18456
18711
  this.meshDirty = true;
18457
18712
  } else {
18458
- perBlocks.push({ index, blockState: blockState3 });
18713
+ perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
18459
18714
  }
18460
18715
  const readBlock = this.data.snapshot(index);
18461
18716
  this.renderedBlocks.set(index, { get block() {
@@ -18468,7 +18723,7 @@ var Chunk = class _Chunk {
18468
18723
  failed = true;
18469
18724
  }
18470
18725
  }
18471
- for (const { index, blockState: blockState3 } of perBlocks) {
18726
+ for (const { index, blockState: blockState3, biome } of perBlocks) {
18472
18727
  let object4;
18473
18728
  try {
18474
18729
  object4 = await this.scene.addBlock(blockState3, {
@@ -18476,11 +18731,9 @@ var Chunk = class _Chunk {
18476
18731
  instanceMeshes: true,
18477
18732
  maxInstanceCount: 2e3,
18478
18733
  initialState: this.data.stateAt(this.data.idAt(index)).properties,
18479
- variantPosition: [
18480
- this.x * 16 + index % 16,
18481
- this.y * 16 + Math.floor(index / 256),
18482
- this.z * 16 + Math.floor(index / 16) % 16
18483
- ]
18734
+ variantPosition: this.positionAt(index),
18735
+ biome,
18736
+ biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
18484
18737
  });
18485
18738
  object4.setPosition(new import_three44.Vector3(
18486
18739
  this.x * 256 + index % 16 * 16,
@@ -18499,6 +18752,68 @@ var Chunk = class _Chunk {
18499
18752
  }
18500
18753
  if (failed) throw failure;
18501
18754
  }
18755
+ positionAt(index) {
18756
+ return [this.x * 16 + index % 16, this.y * 16 + Math.floor(index / 256), this.z * 16 + Math.floor(index / 16) % 16];
18757
+ }
18758
+ biomePosition(index, blockState3, stored) {
18759
+ const position = this.positionAt(index), name = blockState3.key?.toNamespacedString();
18760
+ if (stored.properties?.half === "upper" && (name === "minecraft:tall_grass" || name === "minecraft:large_fern")) position[1]--;
18761
+ return position;
18762
+ }
18763
+ async resolveState(stored, indices, biomes) {
18764
+ const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18765
+ const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18766
+ const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18767
+ 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;
18768
+ const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
18769
+ const prepare = (color2) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color2);
18770
+ const contexts = /* @__PURE__ */ new Map();
18771
+ const groups = /* @__PURE__ */ new Map();
18772
+ if (blockState3 && (source || fluid?.kind === "water")) {
18773
+ for (const index of indices) {
18774
+ const position = this.biomePosition(index, blockState3, stored);
18775
+ const biome = this.biomeAt ? this.biomeAt(...position) : position[1] >= this.y * 16 ? this.getBiomeAt(position) : void 0;
18776
+ if (!biome) continue;
18777
+ const key = JSON.stringify(source === "grass" ? [biome, position[0], position[2]] : [biome]);
18778
+ let group = groups.get(key);
18779
+ if (!group) groups.set(key, group = { biome, position, indices: [] });
18780
+ group.indices.push(index);
18781
+ }
18782
+ }
18783
+ const groupedCount = [...groups.values()].reduce((sum, group) => sum + group.indices.length, 0);
18784
+ const prepared = groupedCount < indices.length ? await prepare() : void 0;
18785
+ await Promise.all([...groups.values()].map(async ({ biome, position, indices: indices2 }) => {
18786
+ const available = () => {
18787
+ const key = JSON.stringify([biome, blockState3?.key?.root]);
18788
+ let found = biomes.get(key);
18789
+ if (!found) {
18790
+ found = Biomes.get(biome, blockState3?.key).then((value) => value !== void 0);
18791
+ biomes.set(key, found);
18792
+ }
18793
+ return found;
18794
+ };
18795
+ const context = { biome };
18796
+ if (perBlock) {
18797
+ if (!await available()) context.biome = void 0;
18798
+ } else {
18799
+ context.prepared = await prepare(async () => await available() ? BlockTints.getBiomeColor(blockState3?.key, biome, position) : void 0);
18800
+ if (fluid?.kind === "water" && await available()) {
18801
+ const color2 = await Biomes.getColor(biome, "water", blockState3?.key);
18802
+ if (color2 !== void 0 && color2 !== 4159204) context.waterTint = new import_three44.Color(color2).toArray();
18803
+ }
18804
+ }
18805
+ for (const index of indices2) contexts.set(index, context);
18806
+ }));
18807
+ return { blockState: blockState3, perBlock, fluid, prepared, contexts, stored };
18808
+ }
18809
+ setWaterTint(index, tint) {
18810
+ if (!this.sectionModels || !this.waterTints && tint === void 0) return;
18811
+ if (!this.waterTints) {
18812
+ this.waterTints = new Float32Array(4096 * 3);
18813
+ for (let cell = 0; cell < 4096; cell++) this.waterTints.set(WATER_TINT2, cell * 3);
18814
+ }
18815
+ this.waterTints.set(tint ?? WATER_TINT2, index * 3);
18816
+ }
18502
18817
  /** Changes visibility at world block coordinates while preserving the block's data. */
18503
18818
  async setBlockVisibleAt(pos, visible) {
18504
18819
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
@@ -18602,7 +18917,7 @@ var Chunk = class _Chunk {
18602
18917
  if (block && !block.object) origins.set(kind, block.block.type);
18603
18918
  }
18604
18919
  if (origins.size) {
18605
- fluids = { cells: new Uint8Array(18 * 18 * 18) };
18920
+ fluids = { cells: new Uint8Array(18 * 18 * 18), waterTints: this.waterTints?.slice() };
18606
18921
  for (let y = -1; y <= 16; y++) {
18607
18922
  for (let z = -1; z <= 16; z++) {
18608
18923
  for (let x = -1; x <= 16; x++) {
@@ -18644,8 +18959,13 @@ var Chunk = class _Chunk {
18644
18959
  this.renderedBlocks.clear();
18645
18960
  this.hiddenBlocks.clear();
18646
18961
  this.fluidCells.fill(0);
18962
+ this.waterTints = void 0;
18647
18963
  this.data.clear();
18648
- if (notify) await this.onBlocksChanged?.(positions);
18964
+ this.biomes = void 0;
18965
+ if (notify) {
18966
+ await this.onBiomesChanged?.(void 0);
18967
+ await this.onBlocksChanged?.(positions);
18968
+ }
18649
18969
  }
18650
18970
  async dispose() {
18651
18971
  await this.clear();
@@ -19031,6 +19351,7 @@ var NodeRenderer = class _NodeRenderer extends Renderer {
19031
19351
  BannerPatterns,
19032
19352
  BasicAssetKey,
19033
19353
  BatchedExecutor,
19354
+ Biomes,
19034
19355
  BlockEntities,
19035
19356
  BlockObject,
19036
19357
  BlockStateResolver,