minerender 2.1.0-alpha.40 → 2.1.0-alpha.42

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.
@@ -32,8 +32,8 @@ var require_dist = __commonJS({
32
32
  __assign = Object.assign || function(t) {
33
33
  for (var s, i = 1, n = arguments.length; i < n; i++) {
34
34
  s = arguments[i];
35
- for (var p11 in s) if (Object.prototype.hasOwnProperty.call(s, p11))
36
- t[p11] = s[p11];
35
+ for (var p10 in s) if (Object.prototype.hasOwnProperty.call(s, p10))
36
+ t[p10] = s[p10];
37
37
  }
38
38
  return t;
39
39
  };
@@ -766,11 +766,11 @@ var ImageLoader = class _ImageLoader {
766
766
  static async loadCanvasData(src) {
767
767
  const keyStr = serializeImageKey({ src });
768
768
  const info = this.getInfo(src);
769
- const cached = Caching.rawImageCache.getIfPresent(keyStr);
769
+ const cached2 = Caching.rawImageCache.getIfPresent(keyStr);
770
770
  try {
771
771
  return await this.infoToCanvasData(await info);
772
772
  } catch (err) {
773
- if (cached && Caching.rawImageCache.getIfPresent(keyStr) === cached) {
773
+ if (cached2 && Caching.rawImageCache.getIfPresent(keyStr) === cached2) {
774
774
  Caching.rawImageCache.invalidate(keyStr);
775
775
  }
776
776
  throw err;
@@ -905,8 +905,8 @@ var Textures = class _Textures {
905
905
  }
906
906
  static getImage(key) {
907
907
  const keyStr = serializeTextureKey(key);
908
- const cached = Caching.textureCache.peek(keyStr);
909
- if (cached && TextureLoader.hasFailed(cached)) {
908
+ const cached2 = Caching.textureCache.peek(keyStr);
909
+ if (cached2 && TextureLoader.hasFailed(cached2)) {
910
910
  Caching.textureCache.invalidate(keyStr);
911
911
  }
912
912
  return Caching.textureCache.get(keyStr, (k) => {
@@ -1173,12 +1173,12 @@ var AssetKey = class _AssetKey extends BasicAssetKey {
1173
1173
  return a.join("/");
1174
1174
  }
1175
1175
  getFullPath() {
1176
- let p11 = [];
1176
+ let p10 = [];
1177
1177
  if (this.type) {
1178
- p11.push(this.type);
1178
+ p10.push(this.type);
1179
1179
  }
1180
- p11.push(this.path);
1181
- return p11.join("/");
1180
+ p10.push(this.path);
1181
+ return p10.join("/");
1182
1182
  }
1183
1183
  toNamespacedString() {
1184
1184
  return this.namespace + ":" + this.getFullPath();
@@ -1265,7 +1265,7 @@ var ModelMerger = class {
1265
1265
  merged.display = { ...inheritedDisplay, ...DisplayTransforms.withHandFallbacks(source.display) };
1266
1266
  }
1267
1267
  }
1268
- merged.hierarchy = models.map((m) => m.parent).filter((p11) => `${p11}`);
1268
+ merged.hierarchy = models.map((m) => m.parent).filter((p10) => `${p10}`);
1269
1269
  merged.hierarchy.push(`${merged.parent}`);
1270
1270
  return merged;
1271
1271
  }
@@ -1972,7 +1972,7 @@ var ModelTextures = class {
1972
1972
  static async get(key) {
1973
1973
  const keyStr = key.serialize();
1974
1974
  const pending = this.preload(key);
1975
- const cached = Caching.textureAssetCache.getIfPresent(keyStr);
1975
+ const cached2 = Caching.textureAssetCache.getIfPresent(keyStr);
1976
1976
  try {
1977
1977
  const asset = await pending;
1978
1978
  if (asset) {
@@ -1980,7 +1980,7 @@ var ModelTextures = class {
1980
1980
  }
1981
1981
  return void 0;
1982
1982
  } catch (err) {
1983
- if (cached && Caching.textureAssetCache.getIfPresent(keyStr) === cached) {
1983
+ if (cached2 && Caching.textureAssetCache.getIfPresent(keyStr) === cached2) {
1984
1984
  Caching.textureAssetCache.invalidate(keyStr);
1985
1985
  }
1986
1986
  throw err;
@@ -3856,6 +3856,93 @@ var ModelCulling = class {
3856
3856
  }
3857
3857
  };
3858
3858
 
3859
+ // src/model/block/BlockStateResolver.ts
3860
+ import { Euler as Euler5 } from "three";
3861
+ function matchesCondition(condition, state) {
3862
+ return Object.entries(condition).every(([key, value]) => {
3863
+ if (Array.isArray(value)) {
3864
+ if (key === "OR") return value.some((child) => matchesCondition(child, state));
3865
+ if (key === "AND") return value.every((child) => matchesCondition(child, state));
3866
+ return false;
3867
+ }
3868
+ return typeof value === "string" && state[key] !== void 0 && value.split("|").includes(`${state[key]}`);
3869
+ });
3870
+ }
3871
+ var BlockStateResolver = class {
3872
+ static async defaults(blockState) {
3873
+ const defaults = blockState.key ? await BlockStates.getDefaultState(blockState.key) : void 0;
3874
+ const state = {};
3875
+ if (defaults && Object.keys(defaults).length > 0) {
3876
+ for (const key in defaults) state[key] = defaults[key].default;
3877
+ } else if (blockState.variants) {
3878
+ const variant = Object.keys(blockState.variants)[0];
3879
+ if (variant) {
3880
+ for (const property of variant.split(",")) {
3881
+ const [key, value] = property.split("=");
3882
+ state[key] = value;
3883
+ }
3884
+ }
3885
+ } else if (blockState.multipart) {
3886
+ const condition = blockState.multipart.map((part) => part.when).find((when) => when !== void 0 && Object.values(when).every((value) => typeof value === "string"));
3887
+ for (const [key, value] of Object.entries(condition ?? {})) state[key] = value.split("|")[0];
3888
+ }
3889
+ return state;
3890
+ }
3891
+ static select(blockState, state, choose = this.choose) {
3892
+ return this.matching(blockState, state).map(choose);
3893
+ }
3894
+ static matching(blockState, state) {
3895
+ const out = [];
3896
+ if (blockState.variants) {
3897
+ if (Object.keys(blockState.variants).length === 1 && "" in blockState.variants) {
3898
+ out.push(blockState.variants[""]);
3899
+ } else {
3900
+ for (const variantKey in blockState.variants) {
3901
+ const matches = variantKey.split(",").every((property) => {
3902
+ const [key, value] = property.split("=");
3903
+ return `${state[key]}` === `${value}`;
3904
+ });
3905
+ if (matches) out.push(blockState.variants[variantKey]);
3906
+ }
3907
+ }
3908
+ } else if (blockState.multipart) {
3909
+ for (const part of blockState.multipart) {
3910
+ if (part.apply && (!part.when || matchesCondition(part.when, state))) {
3911
+ out.push(part.apply);
3912
+ }
3913
+ }
3914
+ }
3915
+ return out;
3916
+ }
3917
+ static choose(variants) {
3918
+ if (!Array.isArray(variants)) return variants;
3919
+ if (!variants.length) throw new MineRenderError("Blockstate variant arrays must not be empty");
3920
+ const total = variants.reduce((sum, variant) => {
3921
+ const weight = variant.weight ?? 1;
3922
+ if (!Number.isInteger(weight) || weight < 1) {
3923
+ throw new MineRenderError(`Invalid blockstate variant weight: ${weight}`);
3924
+ }
3925
+ return sum + weight;
3926
+ }, 0);
3927
+ let choice = Math.random() * total;
3928
+ for (const variant of variants) {
3929
+ choice -= variant.weight ?? 1;
3930
+ if (choice < 0) return variant;
3931
+ }
3932
+ return variants[variants.length - 1];
3933
+ }
3934
+ static rotation(variant) {
3935
+ const rotation = new Euler5();
3936
+ if (typeof variant.x !== "undefined") {
3937
+ rotation.x = toRadians(clampRotationDegrees(variant.x));
3938
+ }
3939
+ if (typeof variant.y !== "undefined") {
3940
+ rotation.y = toRadians(clampRotationDegrees(typeof variant.x !== "undefined" ? variant.y : 360 - variant.y));
3941
+ }
3942
+ return rotation;
3943
+ }
3944
+ };
3945
+
3859
3946
  // src/model/block/blockTints.json
3860
3947
  var blockTints_default = {
3861
3948
  "minecraft:large_fern": { source: "grass" },
@@ -3942,7 +4029,7 @@ var BlockTints = class {
3942
4029
  };
3943
4030
 
3944
4031
  // src/model/scene/ModelObject.ts
3945
- import { BoxGeometry as BoxGeometry5, BufferAttribute as BufferAttribute4, Color as Color3, Euler as Euler5, Matrix4 as Matrix44 } from "three";
4032
+ import { BoxGeometry as BoxGeometry5, BufferAttribute as BufferAttribute4, Color as Color3, Euler as Euler6, Matrix4 as Matrix44 } from "three";
3946
4033
 
3947
4034
  // src/three/BufferGeometryUtils.js
3948
4035
  import {
@@ -4533,7 +4620,7 @@ var _ModelObject = class _ModelObject extends SceneObject {
4533
4620
  elGeo.clearGroups();
4534
4621
  }
4535
4622
  if (this.options.uvLockRotation) {
4536
- UVMapper.lockUvs(elGeo, el.faces, this.atlas, new Euler5(...this.options.uvLockRotation));
4623
+ UVMapper.lockUvs(elGeo, el.faces, this.atlas, new Euler6(...this.options.uvLockRotation));
4537
4624
  }
4538
4625
  if (this.options.tints) {
4539
4626
  const colors = new Float32Array(elGeo.getAttribute("position").count * 3).fill(1);
@@ -4631,18 +4718,7 @@ function isModelObject(obj) {
4631
4718
 
4632
4719
  // src/model/block/scene/BlockObject.ts
4633
4720
  var import_ts_deepmerge6 = __toESM(require_dist());
4634
- import { Euler as Euler6, Matrix4 as Matrix45, Quaternion as Quaternion4, Vector3 as Vector38 } from "three";
4635
- var p10 = prefix("BlockObject");
4636
- function matchesCondition(condition, state) {
4637
- return Object.entries(condition).every(([key, value]) => {
4638
- if (Array.isArray(value)) {
4639
- if (key === "OR") return value.some((child) => matchesCondition(child, state));
4640
- if (key === "AND") return value.every((child) => matchesCondition(child, state));
4641
- return false;
4642
- }
4643
- return typeof value === "string" && state[key] !== void 0 && value.split("|").includes(`${state[key]}`);
4644
- });
4645
- }
4721
+ import { Euler as Euler7, Matrix4 as Matrix45, Quaternion as Quaternion4, Vector3 as Vector38 } from "three";
4646
4722
  var _BlockObject = class _BlockObject extends SceneObject {
4647
4723
  constructor(blockState, options) {
4648
4724
  super(options);
@@ -4661,22 +4737,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
4661
4737
  }
4662
4738
  async init() {
4663
4739
  if (this.options.applyDefaultState) {
4664
- const defaultState = this.blockState.key ? await BlockStates.getDefaultState(this.blockState.key) : void 0;
4665
- if (defaultState && Object.keys(defaultState).length > 0) {
4666
- const state = {};
4667
- for (let k in defaultState) {
4668
- state[k] = defaultState[k].default;
4669
- }
4670
- this._setState(state);
4671
- } else {
4672
- if (this.blockState.variants) {
4673
- this._setState(Object.keys(this.blockState.variants)[0]);
4674
- } else if (this.blockState.multipart) {
4675
- const condition = this.blockState.multipart.map((part) => part.when).find((when) => when !== void 0 && Object.values(when).every((value) => typeof value === "string"));
4676
- const state = Object.fromEntries(Object.entries(condition ?? {}).map(([key, value]) => [key, value.split("|")[0]]));
4677
- this._setState(state);
4678
- }
4679
- }
4740
+ this._setState(await BlockStateResolver.defaults(this.blockState));
4680
4741
  }
4681
4742
  if (this.options.initialState !== void 0) this._setState(this.options.initialState);
4682
4743
  await this.recreateModels();
@@ -4719,7 +4780,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
4719
4780
  const scale = new Vector38();
4720
4781
  this.getModelMatrix(model).decompose(position, rotation, scale);
4721
4782
  if (position.distanceToSquared(this.position) > 1e-10 || scale.distanceToSquared(new Vector38(1, 1, 1)) > 1e-10) return 0;
4722
- return ModelCulling.toLocalMask(worldMask, new Euler6().setFromQuaternion(rotation));
4783
+ return ModelCulling.toLocalMask(worldMask, new Euler7().setFromQuaternion(rotation));
4723
4784
  }
4724
4785
  async setCullMask(worldMask) {
4725
4786
  worldMask &= 63;
@@ -4767,39 +4828,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
4767
4828
  return ref;
4768
4829
  }
4769
4830
  async mapStateToVariant(state) {
4770
- const out = [];
4771
- if (this.blockState.variants) {
4772
- if (Object.keys(this.blockState.variants).length === 1 && "" in this.blockState.variants) {
4773
- out.push(this.getSingleVariant(this.blockState.variants[""]));
4774
- } else {
4775
- for (let variantKey in this.blockState.variants) {
4776
- const split = variantKey.split(",");
4777
- let matches = true;
4778
- for (let s of split) {
4779
- const [k, v] = s.split("=");
4780
- if (`${state[k]}` !== `${v}`) {
4781
- matches = false;
4782
- break;
4783
- }
4784
- }
4785
- if (matches) {
4786
- const variants = this.blockState.variants[variantKey];
4787
- out.push(this.getSingleVariant(variants));
4788
- }
4789
- }
4790
- }
4791
- } else if (this.blockState.multipart) {
4792
- for (let part of this.blockState.multipart) {
4793
- if (!part.apply) {
4794
- console.debug(p10, "Missing apply for blockState part", part);
4795
- continue;
4796
- }
4797
- if (!part.when || matchesCondition(part.when, state)) {
4798
- out.push(this.getSingleVariant(part.apply));
4799
- }
4800
- }
4801
- }
4802
- return out;
4831
+ return BlockStateResolver.select(this.blockState, state, (variants) => this.getSingleVariant(variants));
4803
4832
  }
4804
4833
  async recreateModels() {
4805
4834
  this.clearModels();
@@ -4810,21 +4839,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
4810
4839
  }
4811
4840
  }
4812
4841
  getSingleVariant(variants) {
4813
- if (!Array.isArray(variants)) return variants;
4814
- if (!variants.length) throw new MineRenderError("Blockstate variant arrays must not be empty");
4815
- const total = variants.reduce((sum, variant) => {
4816
- const weight = variant.weight ?? 1;
4817
- if (!Number.isInteger(weight) || weight < 1) {
4818
- throw new MineRenderError(`Invalid blockstate variant weight: ${weight}`);
4819
- }
4820
- return sum + weight;
4821
- }, 0);
4822
- let choice = Math.random() * total;
4823
- for (const variant of variants) {
4824
- choice -= variant.weight ?? 1;
4825
- if (choice < 0) return variant;
4826
- }
4827
- return variants[variants.length - 1];
4842
+ return BlockStateResolver.choose(variants);
4828
4843
  }
4829
4844
  // @deprecated
4830
4845
  async createAVariant(variants, instanceInfo) {
@@ -4832,13 +4847,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
4832
4847
  await this.createVariant(variant);
4833
4848
  }
4834
4849
  async createVariant(variant) {
4835
- const rotation = new Euler6();
4836
- if (typeof variant.x !== "undefined") {
4837
- rotation.x = toRadians(clampRotationDegrees(variant.x));
4838
- }
4839
- if (typeof variant.y !== "undefined") {
4840
- rotation.y = toRadians(clampRotationDegrees(typeof variant.x !== "undefined" ? variant.y : 360 - variant.y));
4841
- }
4850
+ const rotation = BlockStateResolver.rotation(variant);
4842
4851
  const modelKey = AssetKey.parse("models", variant.model);
4843
4852
  modelKey.root = this.blockState.key?.root;
4844
4853
  const model = await Models.getMerged(modelKey);
@@ -9343,32 +9352,121 @@ var AnvilParser = class {
9343
9352
  };
9344
9353
 
9345
9354
  // src/world/Chunk.ts
9355
+ import { Vector3 as Vector313 } from "three";
9356
+
9357
+ // src/world/MineRenderWorld.ts
9346
9358
  import { Vector3 as Vector312 } from "three";
9347
9359
 
9360
+ // src/world/SectionModels.ts
9361
+ import { Matrix4 as Matrix46, Vector3 as Vector311 } from "three";
9362
+ var SectionModel = class extends ModelObject {
9363
+ applyTextures() {
9364
+ }
9365
+ };
9366
+ function cached(map, key, load) {
9367
+ let value = map.get(key);
9368
+ if (!value) {
9369
+ value = load().catch((error) => {
9370
+ map.delete(key);
9371
+ throw error;
9372
+ });
9373
+ map.set(key, value);
9374
+ }
9375
+ return value;
9376
+ }
9377
+ var SectionModels = class {
9378
+ constructor(maxAtlasSize = 2048) {
9379
+ this.maxAtlasSize = maxAtlasSize;
9380
+ this.states = /* @__PURE__ */ new WeakMap();
9381
+ this.models = /* @__PURE__ */ new WeakMap();
9382
+ this.templates = /* @__PURE__ */ new Set();
9383
+ if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("maxAtlasSize must be a positive integer");
9384
+ }
9385
+ async get(blockState, properties = {}) {
9386
+ if (!blockState.variants || blockState.multipart) return void 0;
9387
+ let states = this.states.get(blockState);
9388
+ if (!states) this.states.set(blockState, states = /* @__PURE__ */ new Map());
9389
+ const key = JSON.stringify(Object.entries(properties).sort(([a], [b]) => a.localeCompare(b)));
9390
+ const prepared = await cached(states, key, () => this.prepare(blockState, properties));
9391
+ if (!prepared) return void 0;
9392
+ return prepared.templates.get(BlockStateResolver.choose(prepared.variants));
9393
+ }
9394
+ async prepare(blockState, properties) {
9395
+ const state = { ...await BlockStateResolver.defaults(blockState), ...properties };
9396
+ const groups = BlockStateResolver.matching(blockState, state);
9397
+ if (groups.length !== 1) return void 0;
9398
+ const variants = groups[0];
9399
+ const choices = Array.isArray(variants) ? variants : [variants];
9400
+ if (!choices.length) return void 0;
9401
+ const templates = /* @__PURE__ */ new Map();
9402
+ for (const variant of choices) {
9403
+ const model = await Models.getMerged(AssetKey.parse("models", variant.model));
9404
+ if (!model) return void 0;
9405
+ const atlas = await UVMapper.getAtlas(model);
9406
+ const rotation = BlockStateResolver.rotation(variant);
9407
+ if (!atlas || atlas.hasAnimation || !ModelCulling.isOpaqueFullCube(atlas) || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize || ModelCulling.toLocalMask(63, rotation) !== 63) return void 0;
9408
+ const tints = await BlockTints.get(blockState.key, state, model);
9409
+ let models = this.models.get(model);
9410
+ if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
9411
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
9412
+ const template = await cached(models, key, async () => {
9413
+ const object = new SectionModel(model, {
9414
+ instanceMeshes: false,
9415
+ mergeMeshes: false,
9416
+ tints,
9417
+ uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
9418
+ });
9419
+ await object.init();
9420
+ const geometry = object.children[0].geometry;
9421
+ geometry.applyMatrix4(new Matrix46().makeRotationFromEuler(rotation));
9422
+ object.clear();
9423
+ const cullFaces = CUBE_FACES.map((face) => {
9424
+ const local = CUBE_FACES.findIndex((name) => name === atlas.model.elements[0].faces[face]?.cullface);
9425
+ if (local < 0) return 0;
9426
+ const normal = new Vector311(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
9427
+ 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);
9428
+ return 1 << world;
9429
+ });
9430
+ const template2 = { geometry, atlas, cullFaces };
9431
+ this.templates.add(template2);
9432
+ return template2;
9433
+ });
9434
+ templates.set(variant, template);
9435
+ }
9436
+ return { variants, templates };
9437
+ }
9438
+ clear() {
9439
+ for (const template of this.templates) template.geometry.dispose();
9440
+ this.templates.clear();
9441
+ this.states = /* @__PURE__ */ new WeakMap();
9442
+ this.models = /* @__PURE__ */ new WeakMap();
9443
+ }
9444
+ };
9445
+
9348
9446
  // src/world/MineRenderWorld.ts
9349
- import { Vector3 as Vector311 } from "three";
9350
9447
  var MineRenderWorld = class {
9351
- constructor(scene) {
9448
+ constructor(scene, options = {}) {
9352
9449
  this._chunks = /* @__PURE__ */ new Map();
9353
9450
  this.pendingCulling = /* @__PURE__ */ new Map();
9354
9451
  this.scene = scene;
9452
+ if (options.sectionMeshing) this.sectionModels = new SectionModels(options.maxAtlasSize);
9355
9453
  }
9356
9454
  getBlockAt(posOrX, y, z) {
9357
9455
  if (typeof posOrX == "number") {
9358
- return this.getBlockAt(new Vector311(posOrX, y, z));
9456
+ return this.getBlockAt(new Vector312(posOrX, y, z));
9359
9457
  }
9360
9458
  if (isTripleArray(posOrX)) {
9361
- return this.getBlockAt(new Vector311(posOrX[0], posOrX[1], posOrX[2]));
9459
+ return this.getBlockAt(new Vector312(posOrX[0], posOrX[1], posOrX[2]));
9362
9460
  }
9363
9461
  this.validatePosBounds(posOrX);
9364
9462
  return this.getChunkAt(posOrX)?.getBlockAt(posOrX);
9365
9463
  }
9366
9464
  async setBlockAt(posOrX, yOrBlock, z, block) {
9367
9465
  if (typeof posOrX == "number") {
9368
- return this.setBlockAt(new Vector311(posOrX, yOrBlock, z), block);
9466
+ return this.setBlockAt(new Vector312(posOrX, yOrBlock, z), block);
9369
9467
  }
9370
9468
  if (isTripleArray(posOrX)) {
9371
- return this.setBlockAt(new Vector311(posOrX[0], posOrX[1], posOrX[2]), yOrBlock);
9469
+ return this.setBlockAt(new Vector312(posOrX[0], posOrX[1], posOrX[2]), yOrBlock);
9372
9470
  }
9373
9471
  return this.placeBlock(posOrX, yOrBlock);
9374
9472
  }
@@ -9389,7 +9487,7 @@ var MineRenderWorld = class {
9389
9487
  const collect = async (positions2) => {
9390
9488
  for (const pos of positions2) changes.set(pos.toArray().join(","), pos);
9391
9489
  };
9392
- const place = (block) => this.placeBlock(new Vector311(...block.position), block, collect);
9490
+ const place = (block) => this.placeBlock(new Vector312(...block.position), block, collect);
9393
9491
  const keys = /* @__PURE__ */ new Map();
9394
9492
  for (const block of multiblock.blocks) {
9395
9493
  if (Chunk.isAir(block)) continue;
@@ -9461,12 +9559,13 @@ var MineRenderWorld = class {
9461
9559
  for (const chunk of chunks) {
9462
9560
  await chunk.dispose();
9463
9561
  }
9562
+ this.sectionModels?.clear();
9464
9563
  }
9465
9564
  updateCulling(positions) {
9466
9565
  for (const pos of positions) {
9467
9566
  this.pendingCulling.set(pos.toArray().join(","), pos.clone());
9468
9567
  for (const offset of CUBE_FACE_OFFSETS) {
9469
- const neighbor = pos.clone().add(new Vector311(...offset));
9568
+ const neighbor = pos.clone().add(new Vector312(...offset));
9470
9569
  this.pendingCulling.set(neighbor.toArray().join(","), neighbor);
9471
9570
  }
9472
9571
  }
@@ -9475,17 +9574,22 @@ var MineRenderWorld = class {
9475
9574
  while (this.pendingCulling.size) {
9476
9575
  const batch = [...this.pendingCulling.values()];
9477
9576
  this.pendingCulling.clear();
9577
+ const changed = /* @__PURE__ */ new Set();
9478
9578
  for (const pos of batch) {
9479
- const block = this.getBlockAt(pos)?.object;
9480
- if (!block) continue;
9579
+ const chunk = this.getChunkAt(pos);
9580
+ if (!chunk) continue;
9581
+ changed.add(chunk);
9582
+ if (!chunk.getBlockAt(pos)) continue;
9481
9583
  let mask = 0;
9482
9584
  for (const [face, offset] of CUBE_FACE_OFFSETS.entries()) {
9483
- if (this.getBlockAt(pos.clone().add(new Vector311(...offset)))?.object.isOccluding) {
9585
+ const neighbor = pos.clone().add(new Vector312(...offset));
9586
+ if (this.getChunkAt(neighbor)?.isOccludingAt(neighbor)) {
9484
9587
  mask |= 1 << face;
9485
9588
  }
9486
9589
  }
9487
- await block.setCullMask(mask);
9590
+ await chunk.setCullMaskAt(pos, mask);
9488
9591
  }
9592
+ for (const chunk of changed) chunk.rebuildSectionMesh();
9489
9593
  }
9490
9594
  } finally {
9491
9595
  this.culling = void 0;
@@ -9501,7 +9605,8 @@ var MineRenderWorld = class {
9501
9605
  Math.floor(pos.x / 16),
9502
9606
  Math.floor(pos.y / 16),
9503
9607
  Math.floor(pos.z / 16),
9504
- (positions) => this.updateCulling(positions)
9608
+ (positions) => this.updateCulling(positions),
9609
+ this.sectionModels
9505
9610
  );
9506
9611
  this._chunks.set(key, chunk);
9507
9612
  }
@@ -9511,14 +9616,14 @@ var MineRenderWorld = class {
9511
9616
  return this._chunks.get(this.worldPosToChunkKey(pos));
9512
9617
  }
9513
9618
  static worldToScenePosition(pos) {
9514
- return new Vector311(
9619
+ return new Vector312(
9515
9620
  pos.x * 16,
9516
9621
  pos.y * 16,
9517
9622
  pos.z * 16
9518
9623
  );
9519
9624
  }
9520
9625
  static sceneToWorldPosition(pos) {
9521
- return new Vector311(
9626
+ return new Vector312(
9522
9627
  pos.x / 16,
9523
9628
  pos.y / 16,
9524
9629
  pos.z / 16
@@ -9537,12 +9642,118 @@ var MineRenderWorld = class {
9537
9642
  }
9538
9643
  };
9539
9644
 
9645
+ // src/world/SectionMesh.ts
9646
+ import { BufferGeometry as BufferGeometry2, Float32BufferAttribute as Float32BufferAttribute4, Group, Mesh as Mesh7 } from "three";
9647
+ var SectionMesh = class _SectionMesh extends Group {
9648
+ constructor() {
9649
+ super(...arguments);
9650
+ this.ownedMeshes = [];
9651
+ }
9652
+ static build(entries, maxAtlasSize = 2048) {
9653
+ if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
9654
+ const visible = entries.filter((entry) => entry.template.cullFaces.some((direction) => !(entry.cullMask & direction)));
9655
+ const atlases = [...new Set(visible.map((entry) => entry.template.atlas))].sort((a, b) => b.image.height - a.image.height);
9656
+ const pages = [];
9657
+ const locations = /* @__PURE__ */ new Map();
9658
+ for (const atlas of atlases) {
9659
+ const { width, height } = atlas.image;
9660
+ if (width > maxAtlasSize || height > maxAtlasSize) throw new RangeError("Model atlas exceeds the section atlas size limit");
9661
+ let selected;
9662
+ for (const page of pages) {
9663
+ const nextRow = page.x + width > maxAtlasSize;
9664
+ if ((nextRow ? page.y + page.rowHeight : page.y) + height > maxAtlasSize) continue;
9665
+ if (nextRow) {
9666
+ page.y += page.rowHeight;
9667
+ page.x = 0;
9668
+ page.rowHeight = 0;
9669
+ }
9670
+ selected = page;
9671
+ break;
9672
+ }
9673
+ if (!selected) {
9674
+ selected = { placements: [], entries: [], x: 0, y: 0, rowHeight: 0, width: 0, height: 0 };
9675
+ pages.push(selected);
9676
+ }
9677
+ const placement = { atlas, x: selected.x, y: selected.y };
9678
+ selected.placements.push(placement);
9679
+ selected.x += width;
9680
+ selected.rowHeight = Math.max(selected.rowHeight, height);
9681
+ selected.width = Math.max(selected.width, selected.x);
9682
+ selected.height = Math.max(selected.height, selected.y + height);
9683
+ locations.set(atlas, { page: selected, placement });
9684
+ }
9685
+ for (const entry of visible) locations.get(entry.template.atlas).page.entries.push(entry);
9686
+ const section = new _SectionMesh();
9687
+ for (const page of pages) {
9688
+ const canvas = createCanvas(page.width, page.height);
9689
+ const context = canvas.getContext("2d");
9690
+ for (const { atlas, x, y } of page.placements) {
9691
+ context.drawImage(atlas.image.canvas, x, y);
9692
+ }
9693
+ const positions = [], normals = [], uvs = [], colors = [], indices = [];
9694
+ for (const { index, template, cullMask } of page.entries) {
9695
+ const position = template.geometry.getAttribute("position");
9696
+ const normal = template.geometry.getAttribute("normal");
9697
+ const uv = template.geometry.getAttribute("uv");
9698
+ const color = template.geometry.getAttribute("color");
9699
+ const sourceIndices = template.geometry.getIndex();
9700
+ const placement = locations.get(template.atlas).placement;
9701
+ const offsetX = index % 16 * 16;
9702
+ const offsetY = Math.floor(index / 256) * 16;
9703
+ const offsetZ = Math.floor(index / 16) % 16 * 16;
9704
+ for (let face = 0; face < 6; face++) {
9705
+ if (cullMask & template.cullFaces[face]) continue;
9706
+ const start = positions.length / 3;
9707
+ for (let corner = 0; corner < 4; corner++) {
9708
+ const vertex = face * 4 + corner;
9709
+ positions.push(position.getX(vertex) + offsetX, position.getY(vertex) + offsetY, position.getZ(vertex) + offsetZ);
9710
+ normals.push(normal.getX(vertex), normal.getY(vertex), normal.getZ(vertex));
9711
+ uvs.push(
9712
+ (placement.x + uv.getX(vertex) * template.atlas.image.width) / page.width,
9713
+ 1 - (placement.y + (1 - uv.getY(vertex)) * template.atlas.image.height) / page.height
9714
+ );
9715
+ colors.push(color?.getX(vertex) ?? 1, color?.getY(vertex) ?? 1, color?.getZ(vertex) ?? 1);
9716
+ }
9717
+ for (let corner = 0; corner < 6; corner++) indices.push(start + sourceIndices.getX(face * 6 + corner) - face * 4);
9718
+ }
9719
+ }
9720
+ const geometry = new BufferGeometry2();
9721
+ geometry.setAttribute("position", new Float32BufferAttribute4(positions, 3));
9722
+ geometry.setAttribute("normal", new Float32BufferAttribute4(normals, 3));
9723
+ geometry.setAttribute("uv", new Float32BufferAttribute4(uvs, 2));
9724
+ geometry.setAttribute("color", new Float32BufferAttribute4(colors, 3));
9725
+ geometry.setIndex(indices);
9726
+ geometry.computeBoundingBox();
9727
+ geometry.computeBoundingSphere();
9728
+ const material = Materials.createShadedCanvasMaterial(canvas, false);
9729
+ material.vertexColors = true;
9730
+ const texture = material.uniforms?.map?.value ?? material.map;
9731
+ const mesh = new Mesh7(geometry, material);
9732
+ section.add(mesh);
9733
+ section.ownedMeshes.push({ mesh, texture });
9734
+ }
9735
+ return section;
9736
+ }
9737
+ dispose() {
9738
+ for (const { mesh, texture } of this.ownedMeshes.splice(0)) {
9739
+ mesh.removeFromParent();
9740
+ mesh.geometry.dispose();
9741
+ mesh.material.dispose();
9742
+ texture?.dispose();
9743
+ }
9744
+ this.removeFromParent();
9745
+ }
9746
+ };
9747
+
9540
9748
  // src/world/Chunk.ts
9541
9749
  var Chunk = class _Chunk {
9542
- constructor(scene, x, y, z, onBlocksChanged) {
9750
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels) {
9543
9751
  this.onBlocksChanged = onBlocksChanged;
9752
+ this.sectionModels = sectionModels;
9544
9753
  this.data = new ChunkData();
9545
9754
  this.renderedBlocks = /* @__PURE__ */ new Map();
9755
+ this.sectionBlocks = /* @__PURE__ */ new Map();
9756
+ this.meshDirty = false;
9546
9757
  this.scene = scene;
9547
9758
  this.x = x;
9548
9759
  this.y = y;
@@ -9550,10 +9761,10 @@ var Chunk = class _Chunk {
9550
9761
  }
9551
9762
  getBlockAt(posOrX, y, z) {
9552
9763
  if (typeof posOrX == "number") {
9553
- return this.getBlockAt(new Vector312(posOrX, y, z));
9764
+ return this.getBlockAt(new Vector313(posOrX, y, z));
9554
9765
  }
9555
9766
  if (isTripleArray(posOrX)) {
9556
- return this.getBlockAt(new Vector312(posOrX[0], posOrX[1], posOrX[2]));
9767
+ return this.getBlockAt(new Vector313(posOrX[0], posOrX[1], posOrX[2]));
9557
9768
  }
9558
9769
  const pos = this.worldPosToChunkPos(posOrX);
9559
9770
  const index = _Chunk.chunkPosToBlockIndex(pos);
@@ -9561,10 +9772,10 @@ var Chunk = class _Chunk {
9561
9772
  }
9562
9773
  async setBlockAt(posOrX, yOrBlock, z, block) {
9563
9774
  if (typeof posOrX == "number") {
9564
- return this.setBlockAt(new Vector312(posOrX, yOrBlock, z), block);
9775
+ return this.setBlockAt(new Vector313(posOrX, yOrBlock, z), block);
9565
9776
  }
9566
9777
  if (isTripleArray(posOrX)) {
9567
- return this.setBlockAt(new Vector312(posOrX[0], posOrX[1], posOrX[2]), yOrBlock);
9778
+ return this.setBlockAt(new Vector313(posOrX[0], posOrX[1], posOrX[2]), yOrBlock);
9568
9779
  }
9569
9780
  const worldPos = posOrX;
9570
9781
  const pos = this.worldPosToChunkPos(worldPos);
@@ -9581,7 +9792,8 @@ var Chunk = class _Chunk {
9581
9792
  const index = _Chunk.chunkPosToBlockIndex(pos);
9582
9793
  this.data.set(index, block);
9583
9794
  const readBlock = this.data.snapshot(index);
9584
- this.renderedBlocks.get(index)?.object.removeFromScene();
9795
+ this.renderedBlocks.get(index)?.object?.removeFromScene();
9796
+ if (this.sectionBlocks.delete(index)) this.meshDirty = true;
9585
9797
  this.renderedBlocks.delete(index);
9586
9798
  let object;
9587
9799
  try {
@@ -9592,13 +9804,19 @@ var Chunk = class _Chunk {
9592
9804
  this.data.set(index, void 0);
9593
9805
  return void 0;
9594
9806
  }
9595
- object = await this.scene.addBlock(blockState, {
9596
- mergeMeshes: true,
9597
- instanceMeshes: true,
9598
- maxInstanceCount: 2e3,
9599
- initialState: stored.properties
9600
- });
9601
- object.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
9807
+ const template = await this.sectionModels?.get(blockState, stored.properties);
9808
+ if (template) {
9809
+ this.sectionBlocks.set(index, { index, template, cullMask: 0 });
9810
+ this.meshDirty = true;
9811
+ } else {
9812
+ object = await this.scene.addBlock(blockState, {
9813
+ mergeMeshes: true,
9814
+ instanceMeshes: true,
9815
+ maxInstanceCount: 2e3,
9816
+ initialState: stored.properties
9817
+ });
9818
+ object.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
9819
+ }
9602
9820
  const info = { get block() {
9603
9821
  return readBlock();
9604
9822
  }, object };
@@ -9612,11 +9830,43 @@ var Chunk = class _Chunk {
9612
9830
  await onBlocksChanged?.([worldPos]);
9613
9831
  }
9614
9832
  }
9833
+ isOccludingAt(pos) {
9834
+ const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
9835
+ return this.sectionBlocks.has(index) || (this.renderedBlocks.get(index)?.object?.isOccluding ?? false);
9836
+ }
9837
+ async setCullMaskAt(pos, mask) {
9838
+ const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
9839
+ const entry = this.sectionBlocks.get(index);
9840
+ if (entry) {
9841
+ if (entry.cullMask !== mask) {
9842
+ entry.cullMask = mask;
9843
+ this.meshDirty = true;
9844
+ }
9845
+ } else {
9846
+ await this.renderedBlocks.get(index)?.object?.setCullMask(mask);
9847
+ }
9848
+ }
9849
+ rebuildSectionMesh() {
9850
+ if (!this.meshDirty) return;
9851
+ const next = this.sectionBlocks.size ? SectionMesh.build([...this.sectionBlocks.values()], this.sectionModels.maxAtlasSize) : void 0;
9852
+ this.sectionMesh?.dispose();
9853
+ this.sectionMesh = next;
9854
+ if (next) {
9855
+ next.position.set(this.x * 256, this.y * 256, this.z * 256);
9856
+ this.scene.add(next);
9857
+ }
9858
+ this.scene.dirty = true;
9859
+ this.meshDirty = false;
9860
+ }
9615
9861
  async clear(onBlocksChanged = this.onBlocksChanged) {
9616
9862
  const positions = [...this.renderedBlocks.keys()].map((index) => this.chunkPosToWorldPos(
9617
- new Vector312(index % 16, Math.floor(index / 256), Math.floor(index / 16) % 16)
9863
+ new Vector313(index % 16, Math.floor(index / 256), Math.floor(index / 16) % 16)
9618
9864
  ));
9619
- for (const info of this.renderedBlocks.values()) info.object.removeFromScene();
9865
+ for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
9866
+ this.sectionBlocks.clear();
9867
+ this.sectionMesh?.dispose();
9868
+ this.sectionMesh = void 0;
9869
+ this.meshDirty = false;
9620
9870
  this.renderedBlocks.clear();
9621
9871
  this.data.clear();
9622
9872
  await onBlocksChanged?.(positions);
@@ -9634,14 +9884,14 @@ var Chunk = class _Chunk {
9634
9884
  return pos.y * 16 * 16 + pos.z * 16 + pos.x;
9635
9885
  }
9636
9886
  worldPosToChunkPos(pos) {
9637
- return new Vector312(
9887
+ return new Vector313(
9638
9888
  pos.x - this.x * 16,
9639
9889
  pos.y - this.y * 16,
9640
9890
  pos.z - this.z * 16
9641
9891
  );
9642
9892
  }
9643
9893
  chunkPosToWorldPos(pos) {
9644
- return new Vector312(
9894
+ return new Vector313(
9645
9895
  this.x * 16 + pos.x,
9646
9896
  this.y * 16 + pos.y,
9647
9897
  this.z * 16 + pos.z
@@ -9664,6 +9914,7 @@ export {
9664
9914
  BasicAssetKey,
9665
9915
  BatchedExecutor,
9666
9916
  BlockObject,
9917
+ BlockStateResolver,
9667
9918
  BlockStates,
9668
9919
  BlockTints,
9669
9920
  BrowserArchiveProxy,
@@ -9722,6 +9973,8 @@ export {
9722
9973
  SceneStats,
9723
9974
  SceneStatsDisplay,
9724
9975
  SchematicParser,
9976
+ SectionMesh,
9977
+ SectionModels,
9725
9978
  SkinImage,
9726
9979
  SkinObject,
9727
9980
  SkinPart,