Refactor terrain brush stamping logic and introduce smooth height source management
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user