Refactor terrain brush stamping logic and introduce smooth height source management

This commit is contained in:
2026-04-30 02:53:27 +02:00
parent 24d4592c34
commit a7e62f0256

View File

@@ -19,6 +19,25 @@ export interface TerrainBrushPoint {
z: number; z: number;
} }
export interface TerrainBrushDirtySampleBounds {
minSampleX: number;
maxSampleX: number;
minSampleZ: number;
maxSampleZ: number;
}
export interface TerrainBrushStampMutationResult {
changed: boolean;
dirtyBounds: TerrainBrushDirtySampleBounds | null;
}
interface TerrainSmoothHeightSource {
minSampleX: number;
minSampleZ: number;
width: number;
heights: number[];
}
function clamp(value: number, min: number, max: number): number { function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value)); return Math.min(max, Math.max(min, value));
} }
@@ -128,9 +147,50 @@ export function createTerrainBrushPreviewPoints(
return points; return points;
} }
function readSmoothHeightSource(
source: TerrainSmoothHeightSource,
sampleX: number,
sampleZ: number
): number {
return (
source.heights[
(sampleZ - source.minSampleZ) * source.width +
(sampleX - source.minSampleX)
] ?? 0
);
}
function createTerrainSmoothHeightSource(
terrain: Terrain,
bounds: TerrainBrushDirtySampleBounds
): TerrainSmoothHeightSource {
const minSampleX = Math.max(0, bounds.minSampleX - 1);
const maxSampleX = Math.min(terrain.sampleCountX - 1, bounds.maxSampleX + 1);
const minSampleZ = Math.max(0, bounds.minSampleZ - 1);
const maxSampleZ = Math.min(terrain.sampleCountZ - 1, bounds.maxSampleZ + 1);
const width = maxSampleX - minSampleX + 1;
const heights = new Array<number>(
width * (maxSampleZ - minSampleZ + 1)
);
for (let sampleZ = minSampleZ; sampleZ <= maxSampleZ; sampleZ += 1) {
for (let sampleX = minSampleX; sampleX <= maxSampleX; sampleX += 1) {
heights[(sampleZ - minSampleZ) * width + (sampleX - minSampleX)] =
getTerrainHeightAtSample(terrain, sampleX, sampleZ);
}
}
return {
minSampleX,
minSampleZ,
width,
heights
};
}
function getTerrainSmoothTargetHeight( function getTerrainSmoothTargetHeight(
terrain: Terrain, terrain: Terrain,
sourceHeights: readonly number[], source: TerrainSmoothHeightSource,
sampleX: number, sampleX: number,
sampleZ: number sampleZ: number
): number { ): number {
@@ -147,14 +207,13 @@ function getTerrainSmoothTargetHeight(
neighborX <= Math.min(terrain.sampleCountX - 1, sampleX + 1); neighborX <= Math.min(terrain.sampleCountX - 1, sampleX + 1);
neighborX += 1 neighborX += 1
) { ) {
total += total += readSmoothHeightSource(source, neighborX, neighborZ);
sourceHeights[getTerrainSampleIndex(terrain, neighborX, neighborZ)] ?? 0;
count += 1; count += 1;
} }
} }
return count === 0 return count === 0
? sourceHeights[getTerrainSampleIndex(terrain, sampleX, sampleZ)] ?? 0 ? readSmoothHeightSource(source, sampleX, sampleZ)
: total / count; : total / count;
} }
@@ -198,6 +257,23 @@ export function applyTerrainBrushStamp(options: {
referenceHeight?: number | null; referenceHeight?: number | null;
layerIndex?: number | null; layerIndex?: number | null;
}): Terrain { }): Terrain {
const nextTerrain = createTerrain(options.terrain);
const result = applyTerrainBrushStampInPlace({
...options,
terrain: nextTerrain
});
return result.changed ? nextTerrain : options.terrain;
}
export function applyTerrainBrushStampInPlace(options: {
terrain: Terrain;
center: TerrainBrushPoint;
settings: TerrainBrushSettings;
tool: TerrainBrushTool;
referenceHeight?: number | null;
layerIndex?: number | null;
}): TerrainBrushStampMutationResult {
const { const {
terrain, terrain,
center, center,
@@ -223,12 +299,35 @@ export function applyTerrainBrushStamp(options: {
terrain.sampleCountZ - 1, terrain.sampleCountZ - 1,
Math.ceil((center.z - terrain.position.z + radius) / terrain.cellSize) Math.ceil((center.z - terrain.position.z + radius) / terrain.cellSize)
); );
const sourceHeights = terrain.heights; const stampBounds = {
const sourcePaintWeights = terrain.paintWeights; minSampleX,
const nextHeights = [...sourceHeights]; maxSampleX,
const nextPaintWeights = [...sourcePaintWeights]; minSampleZ,
maxSampleZ
};
const smoothHeightSource =
tool === "smooth"
? createTerrainSmoothHeightSource(terrain, stampBounds)
: null;
const smoothingStrength = clamp01(strength); const smoothingStrength = clamp01(strength);
let changed = false; let dirtyBounds: TerrainBrushDirtySampleBounds | null = null;
const markDirty = (sampleX: number, sampleZ: number) => {
if (dirtyBounds === null) {
dirtyBounds = {
minSampleX: sampleX,
maxSampleX: sampleX,
minSampleZ: sampleZ,
maxSampleZ: sampleZ
};
return;
}
dirtyBounds.minSampleX = Math.min(dirtyBounds.minSampleX, sampleX);
dirtyBounds.maxSampleX = Math.max(dirtyBounds.maxSampleX, sampleX);
dirtyBounds.minSampleZ = Math.min(dirtyBounds.minSampleZ, sampleZ);
dirtyBounds.maxSampleZ = Math.max(dirtyBounds.maxSampleZ, sampleZ);
};
for (let sampleZ = minSampleZ; sampleZ <= maxSampleZ; sampleZ += 1) { for (let sampleZ = minSampleZ; sampleZ <= maxSampleZ; sampleZ += 1) {
for (let sampleX = minSampleX; sampleX <= maxSampleX; sampleX += 1) { for (let sampleX = minSampleX; sampleX <= maxSampleX; sampleX += 1) {
@@ -242,7 +341,7 @@ export function applyTerrainBrushStamp(options: {
} }
const sampleIndex = getTerrainSampleIndex(terrain, sampleX, sampleZ); const sampleIndex = getTerrainSampleIndex(terrain, sampleX, sampleZ);
const currentHeight = sourceHeights[sampleIndex] ?? 0; const currentHeight = terrain.heights[sampleIndex] ?? 0;
let nextHeight = currentHeight; let nextHeight = currentHeight;
switch (tool) { switch (tool) {
@@ -255,7 +354,7 @@ export function applyTerrainBrushStamp(options: {
case "smooth": { case "smooth": {
const smoothTargetHeight = getTerrainSmoothTargetHeight( const smoothTargetHeight = getTerrainSmoothTargetHeight(
terrain, terrain,
sourceHeights, smoothHeightSource!,
sampleX, sampleX,
sampleZ sampleZ
); );
@@ -304,32 +403,29 @@ export function applyTerrainBrushStamp(options: {
nextWeights[3] !== currentWeights[3] nextWeights[3] !== currentWeights[3]
) { ) {
setTerrainSamplePaintWeights( setTerrainSamplePaintWeights(
nextPaintWeights, terrain.paintWeights,
terrain, terrain,
sampleX, sampleX,
sampleZ, sampleZ,
nextWeights nextWeights
); );
changed = true; markDirty(sampleX, sampleZ);
} }
continue; continue;
} }
} }
if (nextHeight !== currentHeight) { if (nextHeight !== currentHeight) {
nextHeights[sampleIndex] = nextHeight; terrain.heights[sampleIndex] = nextHeight;
changed = true; markDirty(sampleX, sampleZ);
} }
} }
} }
return changed return {
? createTerrain({ changed: dirtyBounds !== null,
...terrain, dirtyBounds
heights: nextHeights, };
paintWeights: nextPaintWeights
})
: terrain;
} }
export function getTerrainBrushStrokeSpacing( export function getTerrainBrushStrokeSpacing(