minerender 2.1.0-alpha.120 → 2.1.0-alpha.121

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.
@@ -17367,7 +17367,8 @@ var SectionModels = class {
17367
17367
  atlas,
17368
17368
  cullFaces: new Uint8Array(cullFaces),
17369
17369
  occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17370
- layer: atlas.hasTranslucency ? 1 : 0
17370
+ layer: atlas.hasTranslucency ? 1 : 0,
17371
+ ambientOcclusion: model.ambientocclusion !== false
17371
17372
  };
17372
17373
  this.templates.add(template);
17373
17374
  return template;
@@ -17558,17 +17559,38 @@ var MineRenderWorld = class {
17558
17559
  this.light = /* @__PURE__ */ new Map();
17559
17560
  this.lightValidity = /* @__PURE__ */ new Map();
17560
17561
  this.highestSkySection = /* @__PURE__ */ new Map();
17562
+ this.ambientOcclusionPending = false;
17561
17563
  this.pendingCulling = /* @__PURE__ */ new Map();
17562
17564
  if (options.savedLighting && !options.sectionMeshing) throw new TypeError("savedLighting requires sectionMeshing");
17565
+ if (options.ambientOcclusion && !options.sectionMeshing) throw new TypeError("ambientOcclusion requires sectionMeshing");
17563
17566
  if (options.ambientLight !== void 0 && (!Number.isFinite(options.ambientLight) || options.ambientLight < 0 || options.ambientLight > 1)) {
17564
17567
  throw new RangeError("ambientLight must be a number from 0 to 1");
17565
17568
  }
17566
17569
  this.scene = scene;
17567
17570
  this.savedLighting = options.savedLighting ?? false;
17568
17571
  this.ambientLight = options.ambientLight ?? 0;
17572
+ this._ambientOcclusion = options.ambientOcclusion ?? false;
17569
17573
  if (options.sectionMeshing) this.sectionModels = new SectionModels(options.maxAtlasSize);
17570
17574
  if (options.renderEntities) this.entities = new WorldEntities(scene);
17571
17575
  }
17576
+ /** Whether neighboring opaque cubes shade block-face corners. Defaults to false. */
17577
+ get ambientOcclusion() {
17578
+ return this._ambientOcclusion;
17579
+ }
17580
+ /** Rebuilds loaded blocks with or without corner shading. Block data and saved lighting are preserved. */
17581
+ async setAmbientOcclusion(enabled) {
17582
+ if (typeof enabled !== "boolean") throw new TypeError("Ambient occlusion must be a boolean");
17583
+ if (enabled && !this.sectionModels) throw new TypeError("ambientOcclusion requires sectionMeshing");
17584
+ if (enabled === this._ambientOcclusion && !this.ambientOcclusionPending) {
17585
+ await this.culling;
17586
+ return;
17587
+ }
17588
+ this._ambientOcclusion = enabled;
17589
+ this.ambientOcclusionPending = true;
17590
+ for (const chunk of this._chunks.values()) this.pendingCulling.set(chunk, "all");
17591
+ await this.updateCulling([]);
17592
+ this.ambientOcclusionPending = false;
17593
+ }
17572
17594
  getBlockAt(posOrX, y, z) {
17573
17595
  if (typeof posOrX == "number") {
17574
17596
  return this.getBlockAt(new Vector317(posOrX, y, z));
@@ -17892,7 +17914,7 @@ var MineRenderWorld = class {
17892
17914
  const index = (ny & 15) * 256 + (nz & 15) * 16 + (nx & 15);
17893
17915
  let pending = this.pendingCulling.get(section);
17894
17916
  if (pending === "all" || pending?.has(index)) continue;
17895
- if (Math.abs(x) + Math.abs(y) + Math.abs(z) > 1 && !section.isFluidIndex(index)) continue;
17917
+ if (Math.abs(x) + Math.abs(y) + Math.abs(z) > 1 && !this._ambientOcclusion && !section.isFluidIndex(index)) continue;
17896
17918
  if (!pending) this.pendingCulling.set(section, pending = /* @__PURE__ */ new Set());
17897
17919
  pending.add(index);
17898
17920
  }
@@ -17900,6 +17922,7 @@ var MineRenderWorld = class {
17900
17922
  }
17901
17923
  }
17902
17924
  return this.culling ?? (this.culling = Promise.resolve().then(async () => {
17925
+ let sliceStart = performance.now();
17903
17926
  try {
17904
17927
  while (this.pendingCulling.size) {
17905
17928
  const batch = [...this.pendingCulling];
@@ -17935,6 +17958,11 @@ var MineRenderWorld = class {
17935
17958
  }
17936
17959
  for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
17937
17960
  await chunk.refreshLight(indices === "all" ? void 0 : indices);
17961
+ chunk.refreshAmbientOcclusion(this._ambientOcclusion);
17962
+ if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
17963
+ await yieldToEventLoop();
17964
+ sliceStart = performance.now();
17965
+ }
17938
17966
  }
17939
17967
  await Promise.all(batch.map(([chunk]) => chunk.rebuildSectionMesh()));
17940
17968
  }
@@ -18041,6 +18069,50 @@ import { BufferGeometry as BufferGeometry9, Color as Color12, DoubleSide as Doub
18041
18069
  // src/world/SectionGeometry.ts
18042
18070
  init_FluidQuads();
18043
18071
 
18072
+ // src/world/_light/AmbientOcclusion.ts
18073
+ var AXIS_STRIDES = [1, 9, 3];
18074
+ function sectionAmbientOcclusionMask(cells, center) {
18075
+ let mask = 0;
18076
+ for (let y = -1; y <= 1; y++) for (let z = -1; z <= 1; z++) for (let x = -1; x <= 1; x++) {
18077
+ if (Math.abs(x) + Math.abs(y) + Math.abs(z) < 2) continue;
18078
+ if (cells[center + y * 324 + z * 18 + x]) mask |= 1 << (y + 1) * 9 + (z + 1) * 3 + x + 1;
18079
+ }
18080
+ return mask;
18081
+ }
18082
+ function cornerBrightness(mask, center, u, v) {
18083
+ const sideU = mask >>> center + u & 1;
18084
+ const sideV = mask >>> center + v & 1;
18085
+ const corner = sideU && sideV ? 1 : mask >>> center + u + v & 1;
18086
+ return 1 - (sideU + sideV + corner) * 0.2;
18087
+ }
18088
+ function quadAmbientOcclusion(positions, normals, quad, mask, output) {
18089
+ output.fill(1);
18090
+ if (!mask) return;
18091
+ const start = quad * 12;
18092
+ let axis = 0;
18093
+ if (Math.abs(normals[start + 1]) > Math.abs(normals[start + axis])) axis = 1;
18094
+ if (Math.abs(normals[start + 2]) > Math.abs(normals[start + axis])) axis = 2;
18095
+ const uAxis = (axis + 1) % 3, vAxis = (axis + 2) % 3;
18096
+ if (Math.abs(Math.abs(normals[start + axis]) - 1) > 1e-6 || Math.abs(normals[start + uAxis]) > 1e-6 || Math.abs(normals[start + vAxis]) > 1e-6) return;
18097
+ const sign = normals[start + axis] > 0 ? 1 : -1;
18098
+ for (let vertex = 0; vertex < 4; vertex++) {
18099
+ const offset = start + vertex * 3;
18100
+ if (Math.abs(positions[offset + axis] - sign * 8) > 16e-4 || Math.abs(positions[offset + uAxis]) > 8.0016 || Math.abs(positions[offset + vAxis]) > 8.0016) return;
18101
+ }
18102
+ const center = 13 + sign * AXIS_STRIDES[axis];
18103
+ const u = AXIS_STRIDES[uAxis], v = AXIS_STRIDES[vAxis];
18104
+ const lowLow = cornerBrightness(mask, center, -u, -v);
18105
+ const highLow = cornerBrightness(mask, center, u, -v);
18106
+ const lowHigh = cornerBrightness(mask, center, -u, v);
18107
+ const highHigh = cornerBrightness(mask, center, u, v);
18108
+ for (let vertex = 0; vertex < 4; vertex++) {
18109
+ const offset = start + vertex * 3;
18110
+ const a = Math.max(0, Math.min(1, (positions[offset + uAxis] + 8) / 16));
18111
+ const b = Math.max(0, Math.min(1, (positions[offset + vAxis] + 8) / 16));
18112
+ output[vertex] = (lowLow * (1 - a) + highLow * a) * (1 - b) + (lowHigh * (1 - a) + highHigh * a) * b;
18113
+ }
18114
+ }
18115
+
18044
18116
  // src/world/_light/FaceLight.ts
18045
18117
  function lightBrightness(level, ambient = 0) {
18046
18118
  if (level === 255) return 1;
@@ -18066,7 +18138,7 @@ var LIGHT_SAMPLE_OFFSETS = [0, 1, -1, 324, -324, 18, -18];
18066
18138
 
18067
18139
  // src/world/SectionGeometry.ts
18068
18140
  function buildSectionGeometry(input) {
18069
- const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight } = input;
18141
+ const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion } = input;
18070
18142
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
18071
18143
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
18072
18144
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
@@ -18077,6 +18149,10 @@ function buildSectionGeometry(input) {
18077
18149
  if (ambientLight !== void 0 && (!Number.isFinite(ambientLight) || ambientLight < 0 || ambientLight > 1)) {
18078
18150
  throw new RangeError("Section ambient light must be a number from 0 to 1");
18079
18151
  }
18152
+ if (ambientOcclusion && (ambientOcclusion.length !== 5832 || ambientOcclusion.some((value) => value > 1))) {
18153
+ throw new RangeError("Section ambient occlusion must cover an 18\xD718\xD718 neighborhood with zero or one per cell");
18154
+ }
18155
+ const cornerBrightness2 = ambientOcclusion ? new Float32Array(4) : void 0;
18080
18156
  for (const template of templateData) {
18081
18157
  const { quads } = template;
18082
18158
  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) {
@@ -18182,8 +18258,10 @@ function buildSectionGeometry(input) {
18182
18258
  const offsetY = Math.floor(indices[entry] / 256) * 16;
18183
18259
  const offsetZ = Math.floor(indices[entry] / 16) % 16 * 16;
18184
18260
  const lightIndex = (offsetY / 16 + 1) * 324 + (offsetZ / 16 + 1) * 18 + offsetX / 16 + 1;
18261
+ const ambientMask = ambientOcclusion && template.ambientOcclusion !== false ? sectionAmbientOcclusionMask(ambientOcclusion, lightIndex) : 0;
18185
18262
  for (let quad = 0; quad < template.quads; quad++) {
18186
18263
  if (cullMasks[entry] & template.cullFaces[quad]) continue;
18264
+ if (ambientMask) quadAmbientOcclusion(template.positions, template.normals, quad, ambientMask, cornerBrightness2);
18187
18265
  const brightness = light ? lightBrightness(light[lightIndex + LIGHT_SAMPLE_OFFSETS[quadLightIndex(template.positions, template.normals, quad)]], ambientLight) : 1;
18188
18266
  const start = vertex;
18189
18267
  for (let corner = 0; corner < 4; corner++, vertex++) {
@@ -18200,9 +18278,10 @@ function buildSectionGeometry(input) {
18200
18278
  uvBounds[vertex * 4 + 1] = mapV(template.uvBounds[source * 4 + 1]);
18201
18279
  uvBounds[vertex * 4 + 2] = mapU(template.uvBounds[source * 4 + 2]);
18202
18280
  uvBounds[vertex * 4 + 3] = mapV(template.uvBounds[source * 4 + 3]);
18203
- colors[vertex * 3] = template.colors[source * 3] * brightness;
18204
- colors[vertex * 3 + 1] = template.colors[source * 3 + 1] * brightness;
18205
- colors[vertex * 3 + 2] = template.colors[source * 3 + 2] * brightness;
18281
+ const factor = brightness * (ambientMask ? cornerBrightness2[corner] : 1);
18282
+ colors[vertex * 3] = template.colors[source * 3] * factor;
18283
+ colors[vertex * 3 + 1] = template.colors[source * 3 + 1] * factor;
18284
+ colors[vertex * 3 + 2] = template.colors[source * 3 + 2] * factor;
18206
18285
  }
18207
18286
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
18208
18287
  }
@@ -18350,7 +18429,8 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
18350
18429
  indices: new Uint16Array(quads * 6),
18351
18430
  cullFaces: new Uint8Array(template.cullFaces),
18352
18431
  atlas: 0,
18353
- layer: template.layer
18432
+ layer: template.layer,
18433
+ ambientOcclusion: template.ambientOcclusion
18354
18434
  };
18355
18435
  const normal = template.geometry.getAttribute("normal");
18356
18436
  const uv = template.geometry.getAttribute("uv");
@@ -18780,6 +18860,25 @@ var Chunk = class _Chunk {
18780
18860
  if (this.renderedBlocks.has(index)) this.meshDirty = true;
18781
18861
  }
18782
18862
  }
18863
+ /** Refreshes baked corner shading after edits or a world setting change. Disabled chunks keep no AO neighborhood. */
18864
+ refreshAmbientOcclusion(enabled) {
18865
+ if (!enabled && !this.ambientOcclusionCells) return;
18866
+ const cells = enabled ? this.buildAmbientOcclusionCells() : void 0;
18867
+ if (cells?.some((value, index) => value !== this.ambientOcclusionCells?.[index]) || !!cells !== !!this.ambientOcclusionCells) this.meshDirty = true;
18868
+ this.ambientOcclusionCells = cells;
18869
+ }
18870
+ buildAmbientOcclusionCells() {
18871
+ const cells = new Uint8Array(18 * 18 * 18);
18872
+ for (let y = -1; y <= 16; y++) {
18873
+ for (let z = -1; z <= 16; z++) {
18874
+ for (let x = -1; x <= 16; x++) {
18875
+ const inner = x >= 0 && x < 16 && y >= 0 && y < 16 && z >= 0 && z < 16;
18876
+ cells[(y + 1) * 324 + (z + 1) * 18 + x + 1] = inner ? Number(this.isOccludingIndex(y * 256 + z * 16 + x)) : Number(!!((this.neighborCell?.(this.x * 16 + x, this.y * 16 + y, this.z * 16 + z) ?? 0) & 64));
18877
+ }
18878
+ }
18879
+ }
18880
+ return cells;
18881
+ }
18783
18882
  biomePosition(index, blockState3, stored) {
18784
18883
  const position = this.positionAt(index), name = blockState3.key?.toNamespacedString();
18785
18884
  if (stored.properties?.half === "upper" && (name === "minecraft:tall_grass" || name === "minecraft:large_fern")) position[1]--;
@@ -18968,12 +19067,18 @@ var Chunk = class _Chunk {
18968
19067
  }
18969
19068
  }
18970
19069
  }
18971
- const next = entries.length || fluids ? await SectionMesh.buildAsync(
18972
- entries,
18973
- this.sectionModels.maxAtlasSize,
18974
- fluids,
18975
- light ? { light, ambientLight: this.ambientLight } : void 0
18976
- ) : void 0;
19070
+ let next;
19071
+ try {
19072
+ next = entries.length || fluids ? await SectionMesh.buildAsync(
19073
+ entries,
19074
+ this.sectionModels.maxAtlasSize,
19075
+ fluids,
19076
+ { light, ambientLight: this.ambientLight, ambientOcclusion: this.ambientOcclusionCells }
19077
+ ) : void 0;
19078
+ } catch (error) {
19079
+ if (generation === this.meshGeneration) this.meshDirty = true;
19080
+ throw error;
19081
+ }
18977
19082
  if (generation !== this.meshGeneration) {
18978
19083
  next?.dispose();
18979
19084
  return;
@@ -19001,6 +19106,7 @@ var Chunk = class _Chunk {
19001
19106
  this.hiddenBlocks.clear();
19002
19107
  this.fluidCells.fill(0);
19003
19108
  this.waterTints = void 0;
19109
+ this.ambientOcclusionCells = void 0;
19004
19110
  this.data.clear();
19005
19111
  this.biomes = void 0;
19006
19112
  if (notify) {