Implement and validate foliage blocker masks across document systems

This commit is contained in:
2026-05-02 11:25:17 +02:00
parent e9adf9f37e
commit 6008bff329
4 changed files with 85 additions and 21 deletions

View File

@@ -3010,6 +3010,64 @@ function validateTerrain(
}
}
}
const blockerMask = terrain.foliageBlockerMask;
const blockerMaskPath = `${path}.foliageBlockerMask`;
if (blockerMask.resolutionX !== terrain.sampleCountX) {
diagnostics.push(
createDiagnostic(
"error",
"invalid-terrain-foliage-blocker-mask-resolution-x",
"Terrain foliage blocker mask resolutionX must match terrain sampleCountX.",
`${blockerMaskPath}.resolutionX`
)
);
}
if (blockerMask.resolutionZ !== terrain.sampleCountZ) {
diagnostics.push(
createDiagnostic(
"error",
"invalid-terrain-foliage-blocker-mask-resolution-z",
"Terrain foliage blocker mask resolutionZ must match terrain sampleCountZ.",
`${blockerMaskPath}.resolutionZ`
)
);
}
const expectedBlockerMaskValueCount =
blockerMask.resolutionX * blockerMask.resolutionZ;
if (blockerMask.values.length !== expectedBlockerMaskValueCount) {
diagnostics.push(
createDiagnostic(
"error",
"invalid-terrain-foliage-blocker-mask-value-count",
`Terrain foliage blocker mask values must contain exactly ${expectedBlockerMaskValueCount} samples.`,
`${blockerMaskPath}.values`
)
);
}
if (options.terrainSampleValues !== "skip") {
for (let index = 0; index < blockerMask.values.length; index += 1) {
const maskValue = blockerMask.values[index];
if (isFiniteNumber(maskValue) && maskValue >= 0 && maskValue <= 1) {
continue;
}
diagnostics.push(
createDiagnostic(
"error",
"invalid-terrain-foliage-blocker-mask-value",
"Terrain foliage blocker mask values must remain finite values between 0 and 1.",
`${blockerMaskPath}.values.${index}`
)
);
}
}
}
function validateFoliagePrototype(

View File

@@ -956,7 +956,10 @@ function getStoredTerrainPaintWeightAtSample(
}
export function getTerrainFoliageMaskSampleIndex(
mask: Pick<TerrainFoliageMask | TerrainFoliageBlockerMask, "resolutionX" | "resolutionZ">,
mask: Pick<
TerrainFoliageMask | TerrainFoliageBlockerMask,
"resolutionX" | "resolutionZ"
>,
sampleX: number,
sampleZ: number
): number {
@@ -1096,26 +1099,12 @@ function sampleTerrainFoliageMaskAtGridCoordinate(
const maxSampleZ = Math.min(mask.resolutionZ - 1, minSampleZ + 1);
const blendX = clampedSampleX - minSampleX;
const blendZ = clampedSampleZ - minSampleZ;
const value00 = getTerrainFoliageMaskValueAtSample(
mask as TerrainFoliageMask,
minSampleX,
minSampleZ
);
const value10 = getTerrainFoliageMaskValueAtSample(
mask as TerrainFoliageMask,
maxSampleX,
minSampleZ
);
const value01 = getTerrainFoliageMaskValueAtSample(
mask as TerrainFoliageMask,
minSampleX,
maxSampleZ
);
const value11 = getTerrainFoliageMaskValueAtSample(
mask as TerrainFoliageMask,
maxSampleX,
maxSampleZ
);
const readMaskValue = (x: number, z: number) =>
mask.values[getTerrainFoliageMaskSampleIndex(mask, x, z)] ?? 0;
const value00 = readMaskValue(minSampleX, minSampleZ);
const value10 = readMaskValue(maxSampleX, minSampleZ);
const value01 = readMaskValue(minSampleX, maxSampleZ);
const value11 = readMaskValue(maxSampleX, maxSampleZ);
return lerp(
lerp(value00, value10, blendX),

View File

@@ -6,6 +6,7 @@ import {
getTerrainFootprintWidth,
getTerrainHeightAtSample,
isTerrainFoliageMaskEmpty,
sampleTerrainFoliageBlockerMaskAtWorldPosition,
sampleTerrainFoliageMaskAtWorldPosition,
type Terrain
} from "../document/terrains";
@@ -91,6 +92,7 @@ interface WeightedFoliagePrototypeSet {
export const DEFAULT_FOLIAGE_SCATTER_CHUNK_SIZE_METERS = 16;
export const DEFAULT_MAX_FOLIAGE_SCATTER_INSTANCES_PER_CHUNK = 512;
export const FOLIAGE_BLOCKER_MASK_THRESHOLD = 0.1;
const HASH_OFFSET_BASIS = 2166136261;
const HASH_PRIME = 16777619;
@@ -512,6 +514,18 @@ function generateChunkInstances(options: {
continue;
}
const blockerValue =
sampleTerrainFoliageBlockerMaskAtWorldPosition(
terrain,
worldX,
worldZ,
false
) ?? 0;
if (blockerValue > FOLIAGE_BLOCKER_MASK_THRESHOLD) {
continue;
}
const noiseValue = sampleValueNoise(
worldX,
worldZ,

View File

@@ -391,6 +391,7 @@ interface ActiveTerrainBrushStroke {
heightSampleIndices: Set<number>;
paintWeightIndices: Set<number>;
foliageMaskValueKeys: Set<string>;
foliageBlockerMaskValueIndices: Set<number>;
referenceHeight: number | null;
lastAppliedPoint: {
x: number;
@@ -498,6 +499,8 @@ const TERRAIN_BRUSH_PREVIEW_FLATTEN_COLOR = 0xf1d37d;
const TERRAIN_BRUSH_PREVIEW_PAINT_COLOR = 0x8eb9ff;
const TERRAIN_BRUSH_PREVIEW_FOLIAGE_PAINT_COLOR = 0x65d36e;
const TERRAIN_BRUSH_PREVIEW_FOLIAGE_ERASE_COLOR = 0xf0a853;
const TERRAIN_BRUSH_PREVIEW_FOLIAGE_BLOCKER_PAINT_COLOR = 0xdf5e77;
const TERRAIN_BRUSH_PREVIEW_FOLIAGE_BLOCKER_ERASE_COLOR = 0x5ec6df;
const TERRAIN_BRUSH_PREVIEW_OFFSET = 0.05;
const BOX_CREATE_PREVIEW_FILL = 0x89b6ff;
const BOX_CREATE_PREVIEW_EDGE = 0xf3be8f;