From 6008bff329bba1de7bb0c28fd4759369c53fa30c Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Sat, 2 May 2026 11:25:17 +0200 Subject: [PATCH] Implement and validate foliage blocker masks across document systems --- src/document/scene-document-validation.ts | 58 +++++++++++++++++++++++ src/document/terrains.ts | 31 ++++-------- src/foliage/foliage-scatter.ts | 14 ++++++ src/viewport-three/viewport-host.ts | 3 ++ 4 files changed, 85 insertions(+), 21 deletions(-) diff --git a/src/document/scene-document-validation.ts b/src/document/scene-document-validation.ts index 867e8b9f..b38601ec 100644 --- a/src/document/scene-document-validation.ts +++ b/src/document/scene-document-validation.ts @@ -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( diff --git a/src/document/terrains.ts b/src/document/terrains.ts index eeb6ada1..bba39e56 100644 --- a/src/document/terrains.ts +++ b/src/document/terrains.ts @@ -956,7 +956,10 @@ function getStoredTerrainPaintWeightAtSample( } export function getTerrainFoliageMaskSampleIndex( - mask: Pick, + 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), diff --git a/src/foliage/foliage-scatter.ts b/src/foliage/foliage-scatter.ts index 5e0f36ce..0a03739f 100644 --- a/src/foliage/foliage-scatter.ts +++ b/src/foliage/foliage-scatter.ts @@ -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, diff --git a/src/viewport-three/viewport-host.ts b/src/viewport-three/viewport-host.ts index 50e047c8..4f25dc26 100644 --- a/src/viewport-three/viewport-host.ts +++ b/src/viewport-three/viewport-host.ts @@ -391,6 +391,7 @@ interface ActiveTerrainBrushStroke { heightSampleIndices: Set; paintWeightIndices: Set; foliageMaskValueKeys: Set; + foliageBlockerMaskValueIndices: Set; 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;