Feature: Add foliage mask support to terrain brush stamping and patching

This commit is contained in:
2026-05-02 04:12:22 +02:00
parent e9657dc22d
commit da753fc560

View File

@@ -7,7 +7,10 @@ import type {
} from "../core/terrain-brush"; } from "../core/terrain-brush";
import { import {
createTerrain, createTerrain,
getOrCreateTerrainFoliageMask,
getTerrainHeightAtSample, getTerrainHeightAtSample,
getTerrainFoliageMask,
getTerrainFoliageMaskSampleIndex,
getTerrainPaintWeightSampleOffset, getTerrainPaintWeightSampleOffset,
getTerrainSampleIndex, getTerrainSampleIndex,
getTerrainSampleLayerWeights, getTerrainSampleLayerWeights,
@@ -32,6 +35,12 @@ export interface TerrainBrushStampMutationResult {
dirtyBounds: TerrainBrushDirtySampleBounds | null; dirtyBounds: TerrainBrushDirtySampleBounds | null;
heightSampleIndices: number[]; heightSampleIndices: number[];
paintWeightIndices: number[]; paintWeightIndices: number[];
foliageMaskValueIndices: TerrainFoliageMaskValueIndex[];
}
export interface TerrainFoliageMaskValueIndex {
layerId: string;
index: number;
} }
interface TerrainSmoothHeightSource { interface TerrainSmoothHeightSource {
@@ -324,7 +333,8 @@ export function applyTerrainBrushStampInPlace(options: {
changed: false, changed: false,
dirtyBounds: null, dirtyBounds: null,
heightSampleIndices: [], heightSampleIndices: [],
paintWeightIndices: [] paintWeightIndices: [],
foliageMaskValueIndices: []
}; };
} }
@@ -342,6 +352,7 @@ export function applyTerrainBrushStampInPlace(options: {
let dirtyBounds: TerrainBrushDirtySampleBounds | null = null; let dirtyBounds: TerrainBrushDirtySampleBounds | null = null;
const heightSampleIndices: number[] = []; const heightSampleIndices: number[] = [];
const paintWeightIndices: number[] = []; const paintWeightIndices: number[] = [];
const foliageMaskValueIndices: TerrainFoliageMaskValueIndex[] = [];
const markDirty = (sampleX: number, sampleZ: number) => { const markDirty = (sampleX: number, sampleZ: number) => {
if (dirtyBounds === null) { if (dirtyBounds === null) {
@@ -448,6 +459,41 @@ export function applyTerrainBrushStampInPlace(options: {
} }
continue; continue;
} }
case "foliagePaint":
case "foliageErase": {
const foliageLayerId =
"foliageLayerId" in settings ? settings.foliageLayerId : null;
if (foliageLayerId === null) {
throw new Error(
"Foliage terrain brush stamps require a foliage layer id."
);
}
const mask = getOrCreateTerrainFoliageMask(terrain, foliageLayerId);
const maskIndex = getTerrainFoliageMaskSampleIndex(
mask,
sampleX,
sampleZ
);
const currentMaskValue = mask.values[maskIndex] ?? 0;
const targetMaskValue = tool === "foliagePaint" ? 1 : 0;
const nextMaskValue = lerp(
currentMaskValue,
targetMaskValue,
clamp01(smoothingStrength * weight)
);
if (nextMaskValue !== currentMaskValue) {
mask.values[maskIndex] = nextMaskValue;
foliageMaskValueIndices.push({
layerId: foliageLayerId,
index: maskIndex
});
markDirty(sampleX, sampleZ);
}
continue;
}
} }
if (nextHeight !== currentHeight) { if (nextHeight !== currentHeight) {
@@ -462,7 +508,8 @@ export function applyTerrainBrushStampInPlace(options: {
changed: dirtyBounds !== null, changed: dirtyBounds !== null,
dirtyBounds, dirtyBounds,
heightSampleIndices, heightSampleIndices,
paintWeightIndices paintWeightIndices,
foliageMaskValueIndices
}; };
} }
@@ -471,6 +518,7 @@ export function createTerrainBrushPatchFromTerrains(options: {
after: Terrain; after: Terrain;
heightSampleIndices: Iterable<number>; heightSampleIndices: Iterable<number>;
paintWeightIndices: Iterable<number>; paintWeightIndices: Iterable<number>;
foliageMaskValueIndices?: Iterable<TerrainFoliageMaskValueIndex>;
}): TerrainBrushPatch { }): TerrainBrushPatch {
const { before, after } = options; const { before, after } = options;
@@ -491,6 +539,7 @@ export function createTerrainBrushPatchFromTerrains(options: {
const heightSamples: TerrainBrushPatch["heightSamples"] = []; const heightSamples: TerrainBrushPatch["heightSamples"] = [];
const paintWeights: TerrainBrushPatch["paintWeights"] = []; const paintWeights: TerrainBrushPatch["paintWeights"] = [];
const foliageMaskValues: TerrainBrushPatch["foliageMaskValues"] = [];
const normalizeIndices = ( const normalizeIndices = (
indices: Iterable<number>, indices: Iterable<number>,
length: number, length: number,
@@ -547,10 +596,51 @@ export function createTerrainBrushPatchFromTerrains(options: {
}); });
} }
const seenFoliageMaskValueKeys = new Set<string>();
for (const entry of options.foliageMaskValueIndices ?? []) {
const beforeMask = getTerrainFoliageMask(before, entry.layerId);
const afterMask = getTerrainFoliageMask(after, entry.layerId);
const maskLength =
beforeMask?.values.length ??
afterMask?.values.length ??
before.sampleCountX * before.sampleCountZ;
if (
!Number.isInteger(entry.index) ||
entry.index < 0 ||
entry.index >= maskLength
) {
throw new Error(`Terrain foliage mask patch index ${entry.index} is out of range.`);
}
const key = `${entry.layerId}\u0000${entry.index}`;
if (seenFoliageMaskValueKeys.has(key)) {
continue;
}
seenFoliageMaskValueKeys.add(key);
const beforeValue = beforeMask?.values[entry.index] ?? 0;
const afterValue = afterMask?.values[entry.index] ?? 0;
if (beforeValue === afterValue) {
continue;
}
foliageMaskValues.push({
layerId: entry.layerId,
index: entry.index,
before: beforeValue,
after: afterValue
});
}
return { return {
terrainId: before.id, terrainId: before.id,
heightSamples, heightSamples,
paintWeights paintWeights,
foliageMaskValues
}; };
} }