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.
@@ -3746,6 +3746,7 @@ interface SectionTemplateData {
3746
3746
  cullFaces: Uint8Array;
3747
3747
  atlas: number;
3748
3748
  layer: number;
3749
+ ambientOcclusion?: boolean;
3749
3750
  }
3750
3751
  /** The pixel dimensions and animation state of a source atlas. */
3751
3752
  interface SectionAtlasSize {
@@ -3783,6 +3784,8 @@ interface SectionGeometryInput {
3783
3784
  light?: Uint8Array;
3784
3785
  /** Minimum brightness for known light levels, from 0 to 1. */
3785
3786
  ambientLight?: number;
3787
+ /** Opaque full cubes in an 18×18×18 neighborhood, stored as zero or one. */
3788
+ ambientOcclusion?: Uint8Array;
3786
3789
  }
3787
3790
  /** Merged quads and source-atlas placements for one atlas page. */
3788
3791
  interface SectionGeometryPage {
@@ -3812,6 +3815,8 @@ interface SectionMeshTemplate {
3812
3815
  atlas: TextureAtlas;
3813
3816
  cullFaces: Uint8Array;
3814
3817
  occludes: boolean;
3818
+ /** Whether the model permits corner shading from neighboring full cubes. */
3819
+ ambientOcclusion?: boolean;
3815
3820
  layer: 0 | 1;
3816
3821
  }
3817
3822
  /** One block placement in a section, with its storage index and hidden-face mask. */
@@ -3832,9 +3837,9 @@ interface SectionFluids {
3832
3837
  declare class SectionMesh extends Group {
3833
3838
  private readonly ownedMeshes;
3834
3839
  /** Builds section-local meshes from visible quads. `maxAtlasSize` limits each atlas dimension in pixels. */
3835
- static build(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight">): SectionMesh;
3840
+ static build(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight" | "ambientOcclusion">): SectionMesh;
3836
3841
  /** Builds section-local meshes in a browser worker when available, with synchronous fallback. */
3837
- static buildAsync(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight">): Promise<SectionMesh>;
3842
+ static buildAsync(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight" | "ambientOcclusion">): Promise<SectionMesh>;
3838
3843
  private static fromPages;
3839
3844
  dispose(): void;
3840
3845
  }
@@ -3891,6 +3896,7 @@ declare class Chunk<SectionMeshing extends boolean = false> {
3891
3896
  private sectionMesh?;
3892
3897
  private meshDirty;
3893
3898
  private meshGeneration;
3899
+ private ambientOcclusionCells?;
3894
3900
  constructor(scene: MineRenderScene, x: number, y: number, z: number, onBlocksChanged?: ((positions: Vector3[]) => Promise<void>) | undefined, sectionModels?: SectionModels | undefined, neighborCell?: ((x: number, y: number, z: number) => number) | undefined, biomeAt?: ((x: number, y: number, z: number) => Maybe<string>) | undefined, onBiomesChanged?: ((biomes?: readonly string[]) => Promise<void>) | undefined, lightAt?: ((x: number, y: number, z: number) => number) | undefined, onLightCleared?: (() => Promise<void>) | undefined, ambientLight?: number);
3895
3901
  getBlockAt(x: number, y: number, z: number): Maybe<BlockInfo<SectionMeshing>>;
3896
3902
  getBlockAt(pos: Vector3): Maybe<BlockInfo<SectionMeshing>>;
@@ -3923,6 +3929,9 @@ declare class Chunk<SectionMeshing extends boolean = false> {
3923
3929
  private positionAt;
3924
3930
  /** Refreshes saved lighting after adjacent columns load or unload, including columns with only light data. */
3925
3931
  refreshLight(indices?: Iterable<number>): Promise<void>;
3932
+ /** Refreshes baked corner shading after edits or a world setting change. Disabled chunks keep no AO neighborhood. */
3933
+ refreshAmbientOcclusion(enabled: boolean): void;
3934
+ private buildAmbientOcclusionCells;
3926
3935
  private biomePosition;
3927
3936
  private resolveState;
3928
3937
  private setWaterTint;
@@ -3969,11 +3978,17 @@ declare class MineRenderWorld<SectionMeshing extends boolean = false> {
3969
3978
  private readonly highestSkySection;
3970
3979
  private readonly savedLighting;
3971
3980
  private readonly ambientLight;
3981
+ private _ambientOcclusion;
3982
+ private ambientOcclusionPending;
3972
3983
  private readonly sectionModels?;
3973
3984
  private readonly entities?;
3974
3985
  private readonly pendingCulling;
3975
3986
  private culling?;
3976
3987
  constructor(scene: MineRenderScene, options?: MineRenderWorldOptions<SectionMeshing>);
3988
+ /** Whether neighboring opaque cubes shade block-face corners. Defaults to false. */
3989
+ get ambientOcclusion(): boolean;
3990
+ /** Rebuilds loaded blocks with or without corner shading. Block data and saved lighting are preserved. */
3991
+ setAmbientOcclusion(enabled: boolean): Promise<void>;
3977
3992
  /** Returns a placed block at world block coordinates, or `undefined` for an empty position. */
3978
3993
  getBlockAt(x: number, y: number, z: number): Maybe<BlockInfo<SectionMeshing>>;
3979
3994
  getBlockAt(pos: Vector3): Maybe<BlockInfo<SectionMeshing>>;
@@ -4047,6 +4062,8 @@ interface MineRenderWorldOptions<SectionMeshing extends boolean = boolean> {
4047
4062
  savedLighting?: boolean;
4048
4063
  /** Minimum brightness for saved lighting, from 0 to 1, like a dimension type's `ambient_light`. Defaults to 0. */
4049
4064
  ambientLight?: number;
4065
+ /** Bake corner shading from neighboring opaque full cubes. Defaults to false; no per-frame shading pass is added. Requires `sectionMeshing`. */
4066
+ ambientOcclusion?: boolean;
4050
4067
  /** Maximum width and height of each section atlas page, in pixels. */
4051
4068
  maxAtlasSize?: number;
4052
4069
  }
@@ -3746,6 +3746,7 @@ interface SectionTemplateData {
3746
3746
  cullFaces: Uint8Array;
3747
3747
  atlas: number;
3748
3748
  layer: number;
3749
+ ambientOcclusion?: boolean;
3749
3750
  }
3750
3751
  /** The pixel dimensions and animation state of a source atlas. */
3751
3752
  interface SectionAtlasSize {
@@ -3783,6 +3784,8 @@ interface SectionGeometryInput {
3783
3784
  light?: Uint8Array;
3784
3785
  /** Minimum brightness for known light levels, from 0 to 1. */
3785
3786
  ambientLight?: number;
3787
+ /** Opaque full cubes in an 18×18×18 neighborhood, stored as zero or one. */
3788
+ ambientOcclusion?: Uint8Array;
3786
3789
  }
3787
3790
  /** Merged quads and source-atlas placements for one atlas page. */
3788
3791
  interface SectionGeometryPage {
@@ -3812,6 +3815,8 @@ interface SectionMeshTemplate {
3812
3815
  atlas: TextureAtlas;
3813
3816
  cullFaces: Uint8Array;
3814
3817
  occludes: boolean;
3818
+ /** Whether the model permits corner shading from neighboring full cubes. */
3819
+ ambientOcclusion?: boolean;
3815
3820
  layer: 0 | 1;
3816
3821
  }
3817
3822
  /** One block placement in a section, with its storage index and hidden-face mask. */
@@ -3832,9 +3837,9 @@ interface SectionFluids {
3832
3837
  declare class SectionMesh extends Group {
3833
3838
  private readonly ownedMeshes;
3834
3839
  /** Builds section-local meshes from visible quads. `maxAtlasSize` limits each atlas dimension in pixels. */
3835
- static build(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight">): SectionMesh;
3840
+ static build(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight" | "ambientOcclusion">): SectionMesh;
3836
3841
  /** Builds section-local meshes in a browser worker when available, with synchronous fallback. */
3837
- static buildAsync(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight">): Promise<SectionMesh>;
3842
+ static buildAsync(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight" | "ambientOcclusion">): Promise<SectionMesh>;
3838
3843
  private static fromPages;
3839
3844
  dispose(): void;
3840
3845
  }
@@ -3891,6 +3896,7 @@ declare class Chunk<SectionMeshing extends boolean = false> {
3891
3896
  private sectionMesh?;
3892
3897
  private meshDirty;
3893
3898
  private meshGeneration;
3899
+ private ambientOcclusionCells?;
3894
3900
  constructor(scene: MineRenderScene, x: number, y: number, z: number, onBlocksChanged?: ((positions: Vector3[]) => Promise<void>) | undefined, sectionModels?: SectionModels | undefined, neighborCell?: ((x: number, y: number, z: number) => number) | undefined, biomeAt?: ((x: number, y: number, z: number) => Maybe<string>) | undefined, onBiomesChanged?: ((biomes?: readonly string[]) => Promise<void>) | undefined, lightAt?: ((x: number, y: number, z: number) => number) | undefined, onLightCleared?: (() => Promise<void>) | undefined, ambientLight?: number);
3895
3901
  getBlockAt(x: number, y: number, z: number): Maybe<BlockInfo<SectionMeshing>>;
3896
3902
  getBlockAt(pos: Vector3): Maybe<BlockInfo<SectionMeshing>>;
@@ -3923,6 +3929,9 @@ declare class Chunk<SectionMeshing extends boolean = false> {
3923
3929
  private positionAt;
3924
3930
  /** Refreshes saved lighting after adjacent columns load or unload, including columns with only light data. */
3925
3931
  refreshLight(indices?: Iterable<number>): Promise<void>;
3932
+ /** Refreshes baked corner shading after edits or a world setting change. Disabled chunks keep no AO neighborhood. */
3933
+ refreshAmbientOcclusion(enabled: boolean): void;
3934
+ private buildAmbientOcclusionCells;
3926
3935
  private biomePosition;
3927
3936
  private resolveState;
3928
3937
  private setWaterTint;
@@ -3969,11 +3978,17 @@ declare class MineRenderWorld<SectionMeshing extends boolean = false> {
3969
3978
  private readonly highestSkySection;
3970
3979
  private readonly savedLighting;
3971
3980
  private readonly ambientLight;
3981
+ private _ambientOcclusion;
3982
+ private ambientOcclusionPending;
3972
3983
  private readonly sectionModels?;
3973
3984
  private readonly entities?;
3974
3985
  private readonly pendingCulling;
3975
3986
  private culling?;
3976
3987
  constructor(scene: MineRenderScene, options?: MineRenderWorldOptions<SectionMeshing>);
3988
+ /** Whether neighboring opaque cubes shade block-face corners. Defaults to false. */
3989
+ get ambientOcclusion(): boolean;
3990
+ /** Rebuilds loaded blocks with or without corner shading. Block data and saved lighting are preserved. */
3991
+ setAmbientOcclusion(enabled: boolean): Promise<void>;
3977
3992
  /** Returns a placed block at world block coordinates, or `undefined` for an empty position. */
3978
3993
  getBlockAt(x: number, y: number, z: number): Maybe<BlockInfo<SectionMeshing>>;
3979
3994
  getBlockAt(pos: Vector3): Maybe<BlockInfo<SectionMeshing>>;
@@ -4047,6 +4062,8 @@ interface MineRenderWorldOptions<SectionMeshing extends boolean = boolean> {
4047
4062
  savedLighting?: boolean;
4048
4063
  /** Minimum brightness for saved lighting, from 0 to 1, like a dimension type's `ambient_light`. Defaults to 0. */
4049
4064
  ambientLight?: number;
4065
+ /** Bake corner shading from neighboring opaque full cubes. Defaults to false; no per-frame shading pass is added. Requires `sectionMeshing`. */
4066
+ ambientOcclusion?: boolean;
4050
4067
  /** Maximum width and height of each section atlas page, in pixels. */
4051
4068
  maxAtlasSize?: number;
4052
4069
  }
@@ -17530,7 +17530,8 @@ var SectionModels = class {
17530
17530
  atlas,
17531
17531
  cullFaces: new Uint8Array(cullFaces),
17532
17532
  occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17533
- layer: atlas.hasTranslucency ? 1 : 0
17533
+ layer: atlas.hasTranslucency ? 1 : 0,
17534
+ ambientOcclusion: model.ambientocclusion !== false
17534
17535
  };
17535
17536
  this.templates.add(template);
17536
17537
  return template;
@@ -17721,17 +17722,38 @@ var MineRenderWorld = class {
17721
17722
  this.light = /* @__PURE__ */ new Map();
17722
17723
  this.lightValidity = /* @__PURE__ */ new Map();
17723
17724
  this.highestSkySection = /* @__PURE__ */ new Map();
17725
+ this.ambientOcclusionPending = false;
17724
17726
  this.pendingCulling = /* @__PURE__ */ new Map();
17725
17727
  if (options.savedLighting && !options.sectionMeshing) throw new TypeError("savedLighting requires sectionMeshing");
17728
+ if (options.ambientOcclusion && !options.sectionMeshing) throw new TypeError("ambientOcclusion requires sectionMeshing");
17726
17729
  if (options.ambientLight !== void 0 && (!Number.isFinite(options.ambientLight) || options.ambientLight < 0 || options.ambientLight > 1)) {
17727
17730
  throw new RangeError("ambientLight must be a number from 0 to 1");
17728
17731
  }
17729
17732
  this.scene = scene;
17730
17733
  this.savedLighting = options.savedLighting ?? false;
17731
17734
  this.ambientLight = options.ambientLight ?? 0;
17735
+ this._ambientOcclusion = options.ambientOcclusion ?? false;
17732
17736
  if (options.sectionMeshing) this.sectionModels = new SectionModels(options.maxAtlasSize);
17733
17737
  if (options.renderEntities) this.entities = new WorldEntities(scene);
17734
17738
  }
17739
+ /** Whether neighboring opaque cubes shade block-face corners. Defaults to false. */
17740
+ get ambientOcclusion() {
17741
+ return this._ambientOcclusion;
17742
+ }
17743
+ /** Rebuilds loaded blocks with or without corner shading. Block data and saved lighting are preserved. */
17744
+ async setAmbientOcclusion(enabled) {
17745
+ if (typeof enabled !== "boolean") throw new TypeError("Ambient occlusion must be a boolean");
17746
+ if (enabled && !this.sectionModels) throw new TypeError("ambientOcclusion requires sectionMeshing");
17747
+ if (enabled === this._ambientOcclusion && !this.ambientOcclusionPending) {
17748
+ await this.culling;
17749
+ return;
17750
+ }
17751
+ this._ambientOcclusion = enabled;
17752
+ this.ambientOcclusionPending = true;
17753
+ for (const chunk of this._chunks.values()) this.pendingCulling.set(chunk, "all");
17754
+ await this.updateCulling([]);
17755
+ this.ambientOcclusionPending = false;
17756
+ }
17735
17757
  getBlockAt(posOrX, y, z) {
17736
17758
  if (typeof posOrX == "number") {
17737
17759
  return this.getBlockAt(new import_three42.Vector3(posOrX, y, z));
@@ -18055,7 +18077,7 @@ var MineRenderWorld = class {
18055
18077
  const index = (ny & 15) * 256 + (nz & 15) * 16 + (nx & 15);
18056
18078
  let pending = this.pendingCulling.get(section);
18057
18079
  if (pending === "all" || pending?.has(index)) continue;
18058
- if (Math.abs(x) + Math.abs(y) + Math.abs(z) > 1 && !section.isFluidIndex(index)) continue;
18080
+ if (Math.abs(x) + Math.abs(y) + Math.abs(z) > 1 && !this._ambientOcclusion && !section.isFluidIndex(index)) continue;
18059
18081
  if (!pending) this.pendingCulling.set(section, pending = /* @__PURE__ */ new Set());
18060
18082
  pending.add(index);
18061
18083
  }
@@ -18063,6 +18085,7 @@ var MineRenderWorld = class {
18063
18085
  }
18064
18086
  }
18065
18087
  return this.culling ?? (this.culling = Promise.resolve().then(async () => {
18088
+ let sliceStart = performance.now();
18066
18089
  try {
18067
18090
  while (this.pendingCulling.size) {
18068
18091
  const batch = [...this.pendingCulling];
@@ -18098,6 +18121,11 @@ var MineRenderWorld = class {
18098
18121
  }
18099
18122
  for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
18100
18123
  await chunk.refreshLight(indices === "all" ? void 0 : indices);
18124
+ chunk.refreshAmbientOcclusion(this._ambientOcclusion);
18125
+ if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
18126
+ await yieldToEventLoop();
18127
+ sliceStart = performance.now();
18128
+ }
18101
18129
  }
18102
18130
  await Promise.all(batch.map(([chunk]) => chunk.rebuildSectionMesh()));
18103
18131
  }
@@ -18204,6 +18232,50 @@ init_Materials();
18204
18232
  // src/world/SectionGeometry.ts
18205
18233
  init_FluidQuads();
18206
18234
 
18235
+ // src/world/_light/AmbientOcclusion.ts
18236
+ var AXIS_STRIDES = [1, 9, 3];
18237
+ function sectionAmbientOcclusionMask(cells, center) {
18238
+ let mask = 0;
18239
+ for (let y = -1; y <= 1; y++) for (let z = -1; z <= 1; z++) for (let x = -1; x <= 1; x++) {
18240
+ if (Math.abs(x) + Math.abs(y) + Math.abs(z) < 2) continue;
18241
+ if (cells[center + y * 324 + z * 18 + x]) mask |= 1 << (y + 1) * 9 + (z + 1) * 3 + x + 1;
18242
+ }
18243
+ return mask;
18244
+ }
18245
+ function cornerBrightness(mask, center, u, v) {
18246
+ const sideU = mask >>> center + u & 1;
18247
+ const sideV = mask >>> center + v & 1;
18248
+ const corner = sideU && sideV ? 1 : mask >>> center + u + v & 1;
18249
+ return 1 - (sideU + sideV + corner) * 0.2;
18250
+ }
18251
+ function quadAmbientOcclusion(positions, normals, quad, mask, output) {
18252
+ output.fill(1);
18253
+ if (!mask) return;
18254
+ const start = quad * 12;
18255
+ let axis = 0;
18256
+ if (Math.abs(normals[start + 1]) > Math.abs(normals[start + axis])) axis = 1;
18257
+ if (Math.abs(normals[start + 2]) > Math.abs(normals[start + axis])) axis = 2;
18258
+ const uAxis = (axis + 1) % 3, vAxis = (axis + 2) % 3;
18259
+ 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;
18260
+ const sign = normals[start + axis] > 0 ? 1 : -1;
18261
+ for (let vertex = 0; vertex < 4; vertex++) {
18262
+ const offset = start + vertex * 3;
18263
+ 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;
18264
+ }
18265
+ const center = 13 + sign * AXIS_STRIDES[axis];
18266
+ const u = AXIS_STRIDES[uAxis], v = AXIS_STRIDES[vAxis];
18267
+ const lowLow = cornerBrightness(mask, center, -u, -v);
18268
+ const highLow = cornerBrightness(mask, center, u, -v);
18269
+ const lowHigh = cornerBrightness(mask, center, -u, v);
18270
+ const highHigh = cornerBrightness(mask, center, u, v);
18271
+ for (let vertex = 0; vertex < 4; vertex++) {
18272
+ const offset = start + vertex * 3;
18273
+ const a = Math.max(0, Math.min(1, (positions[offset + uAxis] + 8) / 16));
18274
+ const b = Math.max(0, Math.min(1, (positions[offset + vAxis] + 8) / 16));
18275
+ output[vertex] = (lowLow * (1 - a) + highLow * a) * (1 - b) + (lowHigh * (1 - a) + highHigh * a) * b;
18276
+ }
18277
+ }
18278
+
18207
18279
  // src/world/_light/FaceLight.ts
18208
18280
  function lightBrightness(level, ambient = 0) {
18209
18281
  if (level === 255) return 1;
@@ -18229,7 +18301,7 @@ var LIGHT_SAMPLE_OFFSETS = [0, 1, -1, 324, -324, 18, -18];
18229
18301
 
18230
18302
  // src/world/SectionGeometry.ts
18231
18303
  function buildSectionGeometry(input) {
18232
- const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight } = input;
18304
+ const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion } = input;
18233
18305
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
18234
18306
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
18235
18307
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
@@ -18240,6 +18312,10 @@ function buildSectionGeometry(input) {
18240
18312
  if (ambientLight !== void 0 && (!Number.isFinite(ambientLight) || ambientLight < 0 || ambientLight > 1)) {
18241
18313
  throw new RangeError("Section ambient light must be a number from 0 to 1");
18242
18314
  }
18315
+ if (ambientOcclusion && (ambientOcclusion.length !== 5832 || ambientOcclusion.some((value) => value > 1))) {
18316
+ throw new RangeError("Section ambient occlusion must cover an 18\xD718\xD718 neighborhood with zero or one per cell");
18317
+ }
18318
+ const cornerBrightness2 = ambientOcclusion ? new Float32Array(4) : void 0;
18243
18319
  for (const template of templateData) {
18244
18320
  const { quads } = template;
18245
18321
  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) {
@@ -18345,8 +18421,10 @@ function buildSectionGeometry(input) {
18345
18421
  const offsetY = Math.floor(indices[entry] / 256) * 16;
18346
18422
  const offsetZ = Math.floor(indices[entry] / 16) % 16 * 16;
18347
18423
  const lightIndex = (offsetY / 16 + 1) * 324 + (offsetZ / 16 + 1) * 18 + offsetX / 16 + 1;
18424
+ const ambientMask = ambientOcclusion && template.ambientOcclusion !== false ? sectionAmbientOcclusionMask(ambientOcclusion, lightIndex) : 0;
18348
18425
  for (let quad = 0; quad < template.quads; quad++) {
18349
18426
  if (cullMasks[entry] & template.cullFaces[quad]) continue;
18427
+ if (ambientMask) quadAmbientOcclusion(template.positions, template.normals, quad, ambientMask, cornerBrightness2);
18350
18428
  const brightness = light ? lightBrightness(light[lightIndex + LIGHT_SAMPLE_OFFSETS[quadLightIndex(template.positions, template.normals, quad)]], ambientLight) : 1;
18351
18429
  const start = vertex;
18352
18430
  for (let corner = 0; corner < 4; corner++, vertex++) {
@@ -18363,9 +18441,10 @@ function buildSectionGeometry(input) {
18363
18441
  uvBounds[vertex * 4 + 1] = mapV(template.uvBounds[source * 4 + 1]);
18364
18442
  uvBounds[vertex * 4 + 2] = mapU(template.uvBounds[source * 4 + 2]);
18365
18443
  uvBounds[vertex * 4 + 3] = mapV(template.uvBounds[source * 4 + 3]);
18366
- colors[vertex * 3] = template.colors[source * 3] * brightness;
18367
- colors[vertex * 3 + 1] = template.colors[source * 3 + 1] * brightness;
18368
- colors[vertex * 3 + 2] = template.colors[source * 3 + 2] * brightness;
18444
+ const factor = brightness * (ambientMask ? cornerBrightness2[corner] : 1);
18445
+ colors[vertex * 3] = template.colors[source * 3] * factor;
18446
+ colors[vertex * 3 + 1] = template.colors[source * 3 + 1] * factor;
18447
+ colors[vertex * 3 + 2] = template.colors[source * 3 + 2] * factor;
18369
18448
  }
18370
18449
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
18371
18450
  }
@@ -18513,7 +18592,8 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
18513
18592
  indices: new Uint16Array(quads * 6),
18514
18593
  cullFaces: new Uint8Array(template.cullFaces),
18515
18594
  atlas: 0,
18516
- layer: template.layer
18595
+ layer: template.layer,
18596
+ ambientOcclusion: template.ambientOcclusion
18517
18597
  };
18518
18598
  const normal = template.geometry.getAttribute("normal");
18519
18599
  const uv = template.geometry.getAttribute("uv");
@@ -18943,6 +19023,25 @@ var Chunk = class _Chunk {
18943
19023
  if (this.renderedBlocks.has(index)) this.meshDirty = true;
18944
19024
  }
18945
19025
  }
19026
+ /** Refreshes baked corner shading after edits or a world setting change. Disabled chunks keep no AO neighborhood. */
19027
+ refreshAmbientOcclusion(enabled) {
19028
+ if (!enabled && !this.ambientOcclusionCells) return;
19029
+ const cells = enabled ? this.buildAmbientOcclusionCells() : void 0;
19030
+ if (cells?.some((value, index) => value !== this.ambientOcclusionCells?.[index]) || !!cells !== !!this.ambientOcclusionCells) this.meshDirty = true;
19031
+ this.ambientOcclusionCells = cells;
19032
+ }
19033
+ buildAmbientOcclusionCells() {
19034
+ const cells = new Uint8Array(18 * 18 * 18);
19035
+ for (let y = -1; y <= 16; y++) {
19036
+ for (let z = -1; z <= 16; z++) {
19037
+ for (let x = -1; x <= 16; x++) {
19038
+ const inner = x >= 0 && x < 16 && y >= 0 && y < 16 && z >= 0 && z < 16;
19039
+ 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));
19040
+ }
19041
+ }
19042
+ }
19043
+ return cells;
19044
+ }
18946
19045
  biomePosition(index, blockState3, stored) {
18947
19046
  const position = this.positionAt(index), name = blockState3.key?.toNamespacedString();
18948
19047
  if (stored.properties?.half === "upper" && (name === "minecraft:tall_grass" || name === "minecraft:large_fern")) position[1]--;
@@ -19131,12 +19230,18 @@ var Chunk = class _Chunk {
19131
19230
  }
19132
19231
  }
19133
19232
  }
19134
- const next = entries.length || fluids ? await SectionMesh.buildAsync(
19135
- entries,
19136
- this.sectionModels.maxAtlasSize,
19137
- fluids,
19138
- light ? { light, ambientLight: this.ambientLight } : void 0
19139
- ) : void 0;
19233
+ let next;
19234
+ try {
19235
+ next = entries.length || fluids ? await SectionMesh.buildAsync(
19236
+ entries,
19237
+ this.sectionModels.maxAtlasSize,
19238
+ fluids,
19239
+ { light, ambientLight: this.ambientLight, ambientOcclusion: this.ambientOcclusionCells }
19240
+ ) : void 0;
19241
+ } catch (error) {
19242
+ if (generation === this.meshGeneration) this.meshDirty = true;
19243
+ throw error;
19244
+ }
19140
19245
  if (generation !== this.meshGeneration) {
19141
19246
  next?.dispose();
19142
19247
  return;
@@ -19164,6 +19269,7 @@ var Chunk = class _Chunk {
19164
19269
  this.hiddenBlocks.clear();
19165
19270
  this.fluidCells.fill(0);
19166
19271
  this.waterTints = void 0;
19272
+ this.ambientOcclusionCells = void 0;
19167
19273
  this.data.clear();
19168
19274
  this.biomes = void 0;
19169
19275
  if (notify) {