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.
@@ -3748,6 +3748,7 @@ interface SectionTemplateData {
3748
3748
  cullFaces: Uint8Array;
3749
3749
  atlas: number;
3750
3750
  layer: number;
3751
+ ambientOcclusion?: boolean;
3751
3752
  }
3752
3753
  /** The pixel dimensions and animation state of a source atlas. */
3753
3754
  interface SectionAtlasSize {
@@ -3785,6 +3786,8 @@ interface SectionGeometryInput {
3785
3786
  light?: Uint8Array;
3786
3787
  /** Minimum brightness for known light levels, from 0 to 1. */
3787
3788
  ambientLight?: number;
3789
+ /** Opaque full cubes in an 18×18×18 neighborhood, stored as zero or one. */
3790
+ ambientOcclusion?: Uint8Array;
3788
3791
  }
3789
3792
  /** Merged quads and source-atlas placements for one atlas page. */
3790
3793
  interface SectionGeometryPage {
@@ -3814,6 +3817,8 @@ interface SectionMeshTemplate {
3814
3817
  atlas: TextureAtlas;
3815
3818
  cullFaces: Uint8Array;
3816
3819
  occludes: boolean;
3820
+ /** Whether the model permits corner shading from neighboring full cubes. */
3821
+ ambientOcclusion?: boolean;
3817
3822
  layer: 0 | 1;
3818
3823
  }
3819
3824
  /** One block placement in a section, with its storage index and hidden-face mask. */
@@ -3834,9 +3839,9 @@ interface SectionFluids {
3834
3839
  declare class SectionMesh extends Group {
3835
3840
  private readonly ownedMeshes;
3836
3841
  /** Builds section-local meshes from visible quads. `maxAtlasSize` limits each atlas dimension in pixels. */
3837
- static build(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight">): SectionMesh;
3842
+ static build(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight" | "ambientOcclusion">): SectionMesh;
3838
3843
  /** Builds section-local meshes in a browser worker when available, with synchronous fallback. */
3839
- static buildAsync(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight">): Promise<SectionMesh>;
3844
+ static buildAsync(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight" | "ambientOcclusion">): Promise<SectionMesh>;
3840
3845
  private static fromPages;
3841
3846
  dispose(): void;
3842
3847
  }
@@ -3893,6 +3898,7 @@ declare class Chunk<SectionMeshing extends boolean = false> {
3893
3898
  private sectionMesh?;
3894
3899
  private meshDirty;
3895
3900
  private meshGeneration;
3901
+ private ambientOcclusionCells?;
3896
3902
  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);
3897
3903
  getBlockAt(x: number, y: number, z: number): Maybe<BlockInfo<SectionMeshing>>;
3898
3904
  getBlockAt(pos: Vector3): Maybe<BlockInfo<SectionMeshing>>;
@@ -3925,6 +3931,9 @@ declare class Chunk<SectionMeshing extends boolean = false> {
3925
3931
  private positionAt;
3926
3932
  /** Refreshes saved lighting after adjacent columns load or unload, including columns with only light data. */
3927
3933
  refreshLight(indices?: Iterable<number>): Promise<void>;
3934
+ /** Refreshes baked corner shading after edits or a world setting change. Disabled chunks keep no AO neighborhood. */
3935
+ refreshAmbientOcclusion(enabled: boolean): void;
3936
+ private buildAmbientOcclusionCells;
3928
3937
  private biomePosition;
3929
3938
  private resolveState;
3930
3939
  private setWaterTint;
@@ -3971,11 +3980,17 @@ declare class MineRenderWorld<SectionMeshing extends boolean = false> {
3971
3980
  private readonly highestSkySection;
3972
3981
  private readonly savedLighting;
3973
3982
  private readonly ambientLight;
3983
+ private _ambientOcclusion;
3984
+ private ambientOcclusionPending;
3974
3985
  private readonly sectionModels?;
3975
3986
  private readonly entities?;
3976
3987
  private readonly pendingCulling;
3977
3988
  private culling?;
3978
3989
  constructor(scene: MineRenderScene, options?: MineRenderWorldOptions<SectionMeshing>);
3990
+ /** Whether neighboring opaque cubes shade block-face corners. Defaults to false. */
3991
+ get ambientOcclusion(): boolean;
3992
+ /** Rebuilds loaded blocks with or without corner shading. Block data and saved lighting are preserved. */
3993
+ setAmbientOcclusion(enabled: boolean): Promise<void>;
3979
3994
  /** Returns a placed block at world block coordinates, or `undefined` for an empty position. */
3980
3995
  getBlockAt(x: number, y: number, z: number): Maybe<BlockInfo<SectionMeshing>>;
3981
3996
  getBlockAt(pos: Vector3): Maybe<BlockInfo<SectionMeshing>>;
@@ -4049,6 +4064,8 @@ interface MineRenderWorldOptions<SectionMeshing extends boolean = boolean> {
4049
4064
  savedLighting?: boolean;
4050
4065
  /** Minimum brightness for saved lighting, from 0 to 1, like a dimension type's `ambient_light`. Defaults to 0. */
4051
4066
  ambientLight?: number;
4067
+ /** Bake corner shading from neighboring opaque full cubes. Defaults to false; no per-frame shading pass is added. Requires `sectionMeshing`. */
4068
+ ambientOcclusion?: boolean;
4052
4069
  /** Maximum width and height of each section atlas page, in pixels. */
4053
4070
  maxAtlasSize?: number;
4054
4071
  }
@@ -3748,6 +3748,7 @@ interface SectionTemplateData {
3748
3748
  cullFaces: Uint8Array;
3749
3749
  atlas: number;
3750
3750
  layer: number;
3751
+ ambientOcclusion?: boolean;
3751
3752
  }
3752
3753
  /** The pixel dimensions and animation state of a source atlas. */
3753
3754
  interface SectionAtlasSize {
@@ -3785,6 +3786,8 @@ interface SectionGeometryInput {
3785
3786
  light?: Uint8Array;
3786
3787
  /** Minimum brightness for known light levels, from 0 to 1. */
3787
3788
  ambientLight?: number;
3789
+ /** Opaque full cubes in an 18×18×18 neighborhood, stored as zero or one. */
3790
+ ambientOcclusion?: Uint8Array;
3788
3791
  }
3789
3792
  /** Merged quads and source-atlas placements for one atlas page. */
3790
3793
  interface SectionGeometryPage {
@@ -3814,6 +3817,8 @@ interface SectionMeshTemplate {
3814
3817
  atlas: TextureAtlas;
3815
3818
  cullFaces: Uint8Array;
3816
3819
  occludes: boolean;
3820
+ /** Whether the model permits corner shading from neighboring full cubes. */
3821
+ ambientOcclusion?: boolean;
3817
3822
  layer: 0 | 1;
3818
3823
  }
3819
3824
  /** One block placement in a section, with its storage index and hidden-face mask. */
@@ -3834,9 +3839,9 @@ interface SectionFluids {
3834
3839
  declare class SectionMesh extends Group {
3835
3840
  private readonly ownedMeshes;
3836
3841
  /** Builds section-local meshes from visible quads. `maxAtlasSize` limits each atlas dimension in pixels. */
3837
- static build(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight">): SectionMesh;
3842
+ static build(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight" | "ambientOcclusion">): SectionMesh;
3838
3843
  /** Builds section-local meshes in a browser worker when available, with synchronous fallback. */
3839
- static buildAsync(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight">): Promise<SectionMesh>;
3844
+ static buildAsync(entries: readonly SectionMeshEntry[], maxAtlasSize?: number, fluids?: SectionFluids, lighting?: Pick<SectionGeometryInput, "light" | "ambientLight" | "ambientOcclusion">): Promise<SectionMesh>;
3840
3845
  private static fromPages;
3841
3846
  dispose(): void;
3842
3847
  }
@@ -3893,6 +3898,7 @@ declare class Chunk<SectionMeshing extends boolean = false> {
3893
3898
  private sectionMesh?;
3894
3899
  private meshDirty;
3895
3900
  private meshGeneration;
3901
+ private ambientOcclusionCells?;
3896
3902
  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);
3897
3903
  getBlockAt(x: number, y: number, z: number): Maybe<BlockInfo<SectionMeshing>>;
3898
3904
  getBlockAt(pos: Vector3): Maybe<BlockInfo<SectionMeshing>>;
@@ -3925,6 +3931,9 @@ declare class Chunk<SectionMeshing extends boolean = false> {
3925
3931
  private positionAt;
3926
3932
  /** Refreshes saved lighting after adjacent columns load or unload, including columns with only light data. */
3927
3933
  refreshLight(indices?: Iterable<number>): Promise<void>;
3934
+ /** Refreshes baked corner shading after edits or a world setting change. Disabled chunks keep no AO neighborhood. */
3935
+ refreshAmbientOcclusion(enabled: boolean): void;
3936
+ private buildAmbientOcclusionCells;
3928
3937
  private biomePosition;
3929
3938
  private resolveState;
3930
3939
  private setWaterTint;
@@ -3971,11 +3980,17 @@ declare class MineRenderWorld<SectionMeshing extends boolean = false> {
3971
3980
  private readonly highestSkySection;
3972
3981
  private readonly savedLighting;
3973
3982
  private readonly ambientLight;
3983
+ private _ambientOcclusion;
3984
+ private ambientOcclusionPending;
3974
3985
  private readonly sectionModels?;
3975
3986
  private readonly entities?;
3976
3987
  private readonly pendingCulling;
3977
3988
  private culling?;
3978
3989
  constructor(scene: MineRenderScene, options?: MineRenderWorldOptions<SectionMeshing>);
3990
+ /** Whether neighboring opaque cubes shade block-face corners. Defaults to false. */
3991
+ get ambientOcclusion(): boolean;
3992
+ /** Rebuilds loaded blocks with or without corner shading. Block data and saved lighting are preserved. */
3993
+ setAmbientOcclusion(enabled: boolean): Promise<void>;
3979
3994
  /** Returns a placed block at world block coordinates, or `undefined` for an empty position. */
3980
3995
  getBlockAt(x: number, y: number, z: number): Maybe<BlockInfo<SectionMeshing>>;
3981
3996
  getBlockAt(pos: Vector3): Maybe<BlockInfo<SectionMeshing>>;
@@ -4049,6 +4064,8 @@ interface MineRenderWorldOptions<SectionMeshing extends boolean = boolean> {
4049
4064
  savedLighting?: boolean;
4050
4065
  /** Minimum brightness for saved lighting, from 0 to 1, like a dimension type's `ambient_light`. Defaults to 0. */
4051
4066
  ambientLight?: number;
4067
+ /** Bake corner shading from neighboring opaque full cubes. Defaults to false; no per-frame shading pass is added. Requires `sectionMeshing`. */
4068
+ ambientOcclusion?: boolean;
4052
4069
  /** Maximum width and height of each section atlas page, in pixels. */
4053
4070
  maxAtlasSize?: number;
4054
4071
  }
@@ -17565,7 +17565,8 @@ var SectionModels = class {
17565
17565
  atlas,
17566
17566
  cullFaces: new Uint8Array(cullFaces),
17567
17567
  occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17568
- layer: atlas.hasTranslucency ? 1 : 0
17568
+ layer: atlas.hasTranslucency ? 1 : 0,
17569
+ ambientOcclusion: model.ambientocclusion !== false
17569
17570
  };
17570
17571
  this.templates.add(template);
17571
17572
  return template;
@@ -17756,17 +17757,38 @@ var MineRenderWorld = class {
17756
17757
  this.light = /* @__PURE__ */ new Map();
17757
17758
  this.lightValidity = /* @__PURE__ */ new Map();
17758
17759
  this.highestSkySection = /* @__PURE__ */ new Map();
17760
+ this.ambientOcclusionPending = false;
17759
17761
  this.pendingCulling = /* @__PURE__ */ new Map();
17760
17762
  if (options.savedLighting && !options.sectionMeshing) throw new TypeError("savedLighting requires sectionMeshing");
17763
+ if (options.ambientOcclusion && !options.sectionMeshing) throw new TypeError("ambientOcclusion requires sectionMeshing");
17761
17764
  if (options.ambientLight !== void 0 && (!Number.isFinite(options.ambientLight) || options.ambientLight < 0 || options.ambientLight > 1)) {
17762
17765
  throw new RangeError("ambientLight must be a number from 0 to 1");
17763
17766
  }
17764
17767
  this.scene = scene;
17765
17768
  this.savedLighting = options.savedLighting ?? false;
17766
17769
  this.ambientLight = options.ambientLight ?? 0;
17770
+ this._ambientOcclusion = options.ambientOcclusion ?? false;
17767
17771
  if (options.sectionMeshing) this.sectionModels = new SectionModels(options.maxAtlasSize);
17768
17772
  if (options.renderEntities) this.entities = new WorldEntities(scene);
17769
17773
  }
17774
+ /** Whether neighboring opaque cubes shade block-face corners. Defaults to false. */
17775
+ get ambientOcclusion() {
17776
+ return this._ambientOcclusion;
17777
+ }
17778
+ /** Rebuilds loaded blocks with or without corner shading. Block data and saved lighting are preserved. */
17779
+ async setAmbientOcclusion(enabled) {
17780
+ if (typeof enabled !== "boolean") throw new TypeError("Ambient occlusion must be a boolean");
17781
+ if (enabled && !this.sectionModels) throw new TypeError("ambientOcclusion requires sectionMeshing");
17782
+ if (enabled === this._ambientOcclusion && !this.ambientOcclusionPending) {
17783
+ await this.culling;
17784
+ return;
17785
+ }
17786
+ this._ambientOcclusion = enabled;
17787
+ this.ambientOcclusionPending = true;
17788
+ for (const chunk of this._chunks.values()) this.pendingCulling.set(chunk, "all");
17789
+ await this.updateCulling([]);
17790
+ this.ambientOcclusionPending = false;
17791
+ }
17770
17792
  getBlockAt(posOrX, y, z) {
17771
17793
  if (typeof posOrX == "number") {
17772
17794
  return this.getBlockAt(new import_three42.Vector3(posOrX, y, z));
@@ -18090,7 +18112,7 @@ var MineRenderWorld = class {
18090
18112
  const index = (ny & 15) * 256 + (nz & 15) * 16 + (nx & 15);
18091
18113
  let pending = this.pendingCulling.get(section);
18092
18114
  if (pending === "all" || pending?.has(index)) continue;
18093
- if (Math.abs(x) + Math.abs(y) + Math.abs(z) > 1 && !section.isFluidIndex(index)) continue;
18115
+ if (Math.abs(x) + Math.abs(y) + Math.abs(z) > 1 && !this._ambientOcclusion && !section.isFluidIndex(index)) continue;
18094
18116
  if (!pending) this.pendingCulling.set(section, pending = /* @__PURE__ */ new Set());
18095
18117
  pending.add(index);
18096
18118
  }
@@ -18098,6 +18120,7 @@ var MineRenderWorld = class {
18098
18120
  }
18099
18121
  }
18100
18122
  return this.culling ??= Promise.resolve().then(async () => {
18123
+ let sliceStart = performance.now();
18101
18124
  try {
18102
18125
  while (this.pendingCulling.size) {
18103
18126
  const batch = [...this.pendingCulling];
@@ -18133,6 +18156,11 @@ var MineRenderWorld = class {
18133
18156
  }
18134
18157
  for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
18135
18158
  await chunk.refreshLight(indices === "all" ? void 0 : indices);
18159
+ chunk.refreshAmbientOcclusion(this._ambientOcclusion);
18160
+ if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
18161
+ await yieldToEventLoop();
18162
+ sliceStart = performance.now();
18163
+ }
18136
18164
  }
18137
18165
  await Promise.all(batch.map(([chunk]) => chunk.rebuildSectionMesh()));
18138
18166
  }
@@ -18239,6 +18267,50 @@ init_Materials();
18239
18267
  // src/world/SectionGeometry.ts
18240
18268
  init_FluidQuads();
18241
18269
 
18270
+ // src/world/_light/AmbientOcclusion.ts
18271
+ var AXIS_STRIDES = [1, 9, 3];
18272
+ function sectionAmbientOcclusionMask(cells, center) {
18273
+ let mask = 0;
18274
+ for (let y = -1; y <= 1; y++) for (let z = -1; z <= 1; z++) for (let x = -1; x <= 1; x++) {
18275
+ if (Math.abs(x) + Math.abs(y) + Math.abs(z) < 2) continue;
18276
+ if (cells[center + y * 324 + z * 18 + x]) mask |= 1 << (y + 1) * 9 + (z + 1) * 3 + x + 1;
18277
+ }
18278
+ return mask;
18279
+ }
18280
+ function cornerBrightness(mask, center, u, v) {
18281
+ const sideU = mask >>> center + u & 1;
18282
+ const sideV = mask >>> center + v & 1;
18283
+ const corner = sideU && sideV ? 1 : mask >>> center + u + v & 1;
18284
+ return 1 - (sideU + sideV + corner) * 0.2;
18285
+ }
18286
+ function quadAmbientOcclusion(positions, normals, quad, mask, output) {
18287
+ output.fill(1);
18288
+ if (!mask) return;
18289
+ const start = quad * 12;
18290
+ let axis = 0;
18291
+ if (Math.abs(normals[start + 1]) > Math.abs(normals[start + axis])) axis = 1;
18292
+ if (Math.abs(normals[start + 2]) > Math.abs(normals[start + axis])) axis = 2;
18293
+ const uAxis = (axis + 1) % 3, vAxis = (axis + 2) % 3;
18294
+ 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;
18295
+ const sign = normals[start + axis] > 0 ? 1 : -1;
18296
+ for (let vertex = 0; vertex < 4; vertex++) {
18297
+ const offset = start + vertex * 3;
18298
+ 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;
18299
+ }
18300
+ const center = 13 + sign * AXIS_STRIDES[axis];
18301
+ const u = AXIS_STRIDES[uAxis], v = AXIS_STRIDES[vAxis];
18302
+ const lowLow = cornerBrightness(mask, center, -u, -v);
18303
+ const highLow = cornerBrightness(mask, center, u, -v);
18304
+ const lowHigh = cornerBrightness(mask, center, -u, v);
18305
+ const highHigh = cornerBrightness(mask, center, u, v);
18306
+ for (let vertex = 0; vertex < 4; vertex++) {
18307
+ const offset = start + vertex * 3;
18308
+ const a = Math.max(0, Math.min(1, (positions[offset + uAxis] + 8) / 16));
18309
+ const b = Math.max(0, Math.min(1, (positions[offset + vAxis] + 8) / 16));
18310
+ output[vertex] = (lowLow * (1 - a) + highLow * a) * (1 - b) + (lowHigh * (1 - a) + highHigh * a) * b;
18311
+ }
18312
+ }
18313
+
18242
18314
  // src/world/_light/FaceLight.ts
18243
18315
  function lightBrightness(level, ambient = 0) {
18244
18316
  if (level === 255) return 1;
@@ -18264,7 +18336,7 @@ var LIGHT_SAMPLE_OFFSETS = [0, 1, -1, 324, -324, 18, -18];
18264
18336
 
18265
18337
  // src/world/SectionGeometry.ts
18266
18338
  function buildSectionGeometry(input) {
18267
- const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight } = input;
18339
+ const { count, indices, templates, cullMasks, templateData, atlases, maxAtlasSize, fluids, light, ambientLight, ambientOcclusion } = input;
18268
18340
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
18269
18341
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
18270
18342
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
@@ -18275,6 +18347,10 @@ function buildSectionGeometry(input) {
18275
18347
  if (ambientLight !== void 0 && (!Number.isFinite(ambientLight) || ambientLight < 0 || ambientLight > 1)) {
18276
18348
  throw new RangeError("Section ambient light must be a number from 0 to 1");
18277
18349
  }
18350
+ if (ambientOcclusion && (ambientOcclusion.length !== 5832 || ambientOcclusion.some((value) => value > 1))) {
18351
+ throw new RangeError("Section ambient occlusion must cover an 18\xD718\xD718 neighborhood with zero or one per cell");
18352
+ }
18353
+ const cornerBrightness2 = ambientOcclusion ? new Float32Array(4) : void 0;
18278
18354
  for (const template of templateData) {
18279
18355
  const { quads } = template;
18280
18356
  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) {
@@ -18380,8 +18456,10 @@ function buildSectionGeometry(input) {
18380
18456
  const offsetY = Math.floor(indices[entry] / 256) * 16;
18381
18457
  const offsetZ = Math.floor(indices[entry] / 16) % 16 * 16;
18382
18458
  const lightIndex = (offsetY / 16 + 1) * 324 + (offsetZ / 16 + 1) * 18 + offsetX / 16 + 1;
18459
+ const ambientMask = ambientOcclusion && template.ambientOcclusion !== false ? sectionAmbientOcclusionMask(ambientOcclusion, lightIndex) : 0;
18383
18460
  for (let quad = 0; quad < template.quads; quad++) {
18384
18461
  if (cullMasks[entry] & template.cullFaces[quad]) continue;
18462
+ if (ambientMask) quadAmbientOcclusion(template.positions, template.normals, quad, ambientMask, cornerBrightness2);
18385
18463
  const brightness = light ? lightBrightness(light[lightIndex + LIGHT_SAMPLE_OFFSETS[quadLightIndex(template.positions, template.normals, quad)]], ambientLight) : 1;
18386
18464
  const start = vertex;
18387
18465
  for (let corner = 0; corner < 4; corner++, vertex++) {
@@ -18398,9 +18476,10 @@ function buildSectionGeometry(input) {
18398
18476
  uvBounds[vertex * 4 + 1] = mapV(template.uvBounds[source * 4 + 1]);
18399
18477
  uvBounds[vertex * 4 + 2] = mapU(template.uvBounds[source * 4 + 2]);
18400
18478
  uvBounds[vertex * 4 + 3] = mapV(template.uvBounds[source * 4 + 3]);
18401
- colors[vertex * 3] = template.colors[source * 3] * brightness;
18402
- colors[vertex * 3 + 1] = template.colors[source * 3 + 1] * brightness;
18403
- colors[vertex * 3 + 2] = template.colors[source * 3 + 2] * brightness;
18479
+ const factor = brightness * (ambientMask ? cornerBrightness2[corner] : 1);
18480
+ colors[vertex * 3] = template.colors[source * 3] * factor;
18481
+ colors[vertex * 3 + 1] = template.colors[source * 3 + 1] * factor;
18482
+ colors[vertex * 3 + 2] = template.colors[source * 3 + 2] * factor;
18404
18483
  }
18405
18484
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
18406
18485
  }
@@ -18549,7 +18628,8 @@ function toInput(entries, maxAtlasSize, fluids, lighting) {
18549
18628
  indices: new Uint16Array(quads * 6),
18550
18629
  cullFaces: new Uint8Array(template.cullFaces),
18551
18630
  atlas: 0,
18552
- layer: template.layer
18631
+ layer: template.layer,
18632
+ ambientOcclusion: template.ambientOcclusion
18553
18633
  };
18554
18634
  const normal = template.geometry.getAttribute("normal");
18555
18635
  const uv = template.geometry.getAttribute("uv");
@@ -18979,6 +19059,25 @@ var Chunk = class _Chunk {
18979
19059
  if (this.renderedBlocks.has(index)) this.meshDirty = true;
18980
19060
  }
18981
19061
  }
19062
+ /** Refreshes baked corner shading after edits or a world setting change. Disabled chunks keep no AO neighborhood. */
19063
+ refreshAmbientOcclusion(enabled) {
19064
+ if (!enabled && !this.ambientOcclusionCells) return;
19065
+ const cells = enabled ? this.buildAmbientOcclusionCells() : void 0;
19066
+ if (cells?.some((value, index) => value !== this.ambientOcclusionCells?.[index]) || !!cells !== !!this.ambientOcclusionCells) this.meshDirty = true;
19067
+ this.ambientOcclusionCells = cells;
19068
+ }
19069
+ buildAmbientOcclusionCells() {
19070
+ const cells = new Uint8Array(18 * 18 * 18);
19071
+ for (let y = -1; y <= 16; y++) {
19072
+ for (let z = -1; z <= 16; z++) {
19073
+ for (let x = -1; x <= 16; x++) {
19074
+ const inner = x >= 0 && x < 16 && y >= 0 && y < 16 && z >= 0 && z < 16;
19075
+ 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));
19076
+ }
19077
+ }
19078
+ }
19079
+ return cells;
19080
+ }
18982
19081
  biomePosition(index, blockState3, stored) {
18983
19082
  const position = this.positionAt(index), name = blockState3.key?.toNamespacedString();
18984
19083
  if (stored.properties?.half === "upper" && (name === "minecraft:tall_grass" || name === "minecraft:large_fern")) position[1]--;
@@ -19167,12 +19266,18 @@ var Chunk = class _Chunk {
19167
19266
  }
19168
19267
  }
19169
19268
  }
19170
- const next = entries.length || fluids ? await SectionMesh.buildAsync(
19171
- entries,
19172
- this.sectionModels.maxAtlasSize,
19173
- fluids,
19174
- light ? { light, ambientLight: this.ambientLight } : void 0
19175
- ) : void 0;
19269
+ let next;
19270
+ try {
19271
+ next = entries.length || fluids ? await SectionMesh.buildAsync(
19272
+ entries,
19273
+ this.sectionModels.maxAtlasSize,
19274
+ fluids,
19275
+ { light, ambientLight: this.ambientLight, ambientOcclusion: this.ambientOcclusionCells }
19276
+ ) : void 0;
19277
+ } catch (error) {
19278
+ if (generation === this.meshGeneration) this.meshDirty = true;
19279
+ throw error;
19280
+ }
19176
19281
  if (generation !== this.meshGeneration) {
19177
19282
  next?.dispose();
19178
19283
  return;
@@ -19200,6 +19305,7 @@ var Chunk = class _Chunk {
19200
19305
  this.hiddenBlocks.clear();
19201
19306
  this.fluidCells.fill(0);
19202
19307
  this.waterTints = void 0;
19308
+ this.ambientOcclusionCells = void 0;
19203
19309
  this.data.clear();
19204
19310
  this.biomes = void 0;
19205
19311
  if (notify) {