auto-git:

[change] src/document/terrains.ts
This commit is contained in:
2026-05-02 11:23:04 +02:00
parent 62c4717748
commit 47defd9e03

View File

@@ -12,6 +12,12 @@ export interface TerrainFoliageMask {
values: number[]; values: number[];
} }
export interface TerrainFoliageBlockerMask {
resolutionX: number;
resolutionZ: number;
values: number[];
}
export type TerrainFoliageMaskRegistry = Record<string, TerrainFoliageMask>; export type TerrainFoliageMaskRegistry = Record<string, TerrainFoliageMask>;
export interface Terrain { export interface Terrain {
@@ -29,6 +35,7 @@ export interface Terrain {
layers: TerrainLayer[]; layers: TerrainLayer[];
paintWeights: number[]; paintWeights: number[];
foliageMasks: TerrainFoliageMaskRegistry; foliageMasks: TerrainFoliageMaskRegistry;
foliageBlockerMask: TerrainFoliageBlockerMask;
} }
export interface TerrainHeightPatchEntry { export interface TerrainHeightPatchEntry {
@@ -298,6 +305,14 @@ export function createFlatTerrainFoliageMaskValues(
); );
} }
export function createFlatTerrainFoliageBlockerMaskValues(
resolutionX: number,
resolutionZ: number,
value = 0
): number[] {
return createFlatTerrainFoliageMaskValues(resolutionX, resolutionZ, value);
}
export function getTerrainSampleIndex( export function getTerrainSampleIndex(
terrain: Pick<Terrain, "sampleCountX" | "sampleCountZ">, terrain: Pick<Terrain, "sampleCountX" | "sampleCountZ">,
sampleX: number, sampleX: number,
@@ -463,6 +478,65 @@ export function cloneTerrainFoliageMask(
return createTerrainFoliageMask(mask); return createTerrainFoliageMask(mask);
} }
export function createTerrainFoliageBlockerMask(options: {
resolutionX: number;
resolutionZ: number;
values?: readonly number[];
}): TerrainFoliageBlockerMask {
const resolutionX = normalizeTerrainSampleCount(
options.resolutionX,
"Terrain foliage blocker mask resolutionX"
);
const resolutionZ = normalizeTerrainSampleCount(
options.resolutionZ,
"Terrain foliage blocker mask resolutionZ"
);
const expectedValueCount = resolutionX * resolutionZ;
const values =
options.values === undefined
? createFlatTerrainFoliageBlockerMaskValues(resolutionX, resolutionZ)
: [...options.values];
if (values.length !== expectedValueCount) {
throw new Error(
`Terrain foliage blocker mask values must contain exactly ${expectedValueCount} samples.`
);
}
for (let index = 0; index < values.length; index += 1) {
const value = values[index];
if (!Number.isFinite(value)) {
throw new Error(
"Terrain foliage blocker mask values must remain finite."
);
}
values[index] = clamp(value, 0, 1);
}
return {
resolutionX,
resolutionZ,
values
};
}
export function createEmptyTerrainFoliageBlockerMask(
terrain: Pick<Terrain, "sampleCountX" | "sampleCountZ">
): TerrainFoliageBlockerMask {
return createTerrainFoliageBlockerMask({
resolutionX: terrain.sampleCountX,
resolutionZ: terrain.sampleCountZ
});
}
export function cloneTerrainFoliageBlockerMask(
mask: TerrainFoliageBlockerMask
): TerrainFoliageBlockerMask {
return createTerrainFoliageBlockerMask(mask);
}
function normalizeTerrainFoliageMasks( function normalizeTerrainFoliageMasks(
sampleCountX: number, sampleCountX: number,
sampleCountZ: number, sampleCountZ: number,
@@ -498,6 +572,31 @@ function normalizeTerrainFoliageMasks(
return normalizedMasks; return normalizedMasks;
} }
function normalizeTerrainFoliageBlockerMask(
sampleCountX: number,
sampleCountZ: number,
foliageBlockerMask: TerrainFoliageBlockerMask | undefined
): TerrainFoliageBlockerMask {
const normalizedMask =
foliageBlockerMask === undefined
? createTerrainFoliageBlockerMask({
resolutionX: sampleCountX,
resolutionZ: sampleCountZ
})
: createTerrainFoliageBlockerMask(foliageBlockerMask);
if (
normalizedMask.resolutionX !== sampleCountX ||
normalizedMask.resolutionZ !== sampleCountZ
) {
throw new Error(
"Terrain foliage blocker mask resolution must match the terrain sample grid."
);
}
return normalizedMask;
}
export function cloneTerrainFoliageMasks( export function cloneTerrainFoliageMasks(
foliageMasks: TerrainFoliageMaskRegistry foliageMasks: TerrainFoliageMaskRegistry
): TerrainFoliageMaskRegistry { ): TerrainFoliageMaskRegistry {
@@ -857,7 +956,7 @@ function getStoredTerrainPaintWeightAtSample(
} }
export function getTerrainFoliageMaskSampleIndex( export function getTerrainFoliageMaskSampleIndex(
mask: Pick<TerrainFoliageMask, "resolutionX" | "resolutionZ">, mask: Pick<TerrainFoliageMask | TerrainFoliageBlockerMask, "resolutionX" | "resolutionZ">,
sampleX: number, sampleX: number,
sampleZ: number sampleZ: number
): number { ): number {
@@ -890,6 +989,16 @@ export function getTerrainFoliageMaskValueAtSample(
); );
} }
export function getTerrainFoliageBlockerMaskValueAtSample(
mask: TerrainFoliageBlockerMask,
sampleX: number,
sampleZ: number
): number {
return (
mask.values[getTerrainFoliageMaskSampleIndex(mask, sampleX, sampleZ)] ?? 0
);
}
export function getTerrainFoliageMask( export function getTerrainFoliageMask(
terrain: Pick<Terrain, "foliageMasks">, terrain: Pick<Terrain, "foliageMasks">,
layerId: string layerId: string
@@ -922,6 +1031,12 @@ export function isTerrainFoliageMaskEmpty(
return mask.values.every((value) => value === 0); return mask.values.every((value) => value === 0);
} }
export function isTerrainFoliageBlockerMaskEmpty(
mask: TerrainFoliageBlockerMask
): boolean {
return mask.values.every((value) => value === 0);
}
function sampleTerrainPaintWeightAtGridCoordinate( function sampleTerrainPaintWeightAtGridCoordinate(
terrain: Terrain, terrain: Terrain,
sampleX: number, sampleX: number,
@@ -969,7 +1084,7 @@ function sampleTerrainPaintWeightAtGridCoordinate(
} }
function sampleTerrainFoliageMaskAtGridCoordinate( function sampleTerrainFoliageMaskAtGridCoordinate(
mask: TerrainFoliageMask, mask: TerrainFoliageMask | TerrainFoliageBlockerMask,
sampleX: number, sampleX: number,
sampleZ: number sampleZ: number
): number { ): number {
@@ -982,22 +1097,22 @@ function sampleTerrainFoliageMaskAtGridCoordinate(
const blendX = clampedSampleX - minSampleX; const blendX = clampedSampleX - minSampleX;
const blendZ = clampedSampleZ - minSampleZ; const blendZ = clampedSampleZ - minSampleZ;
const value00 = getTerrainFoliageMaskValueAtSample( const value00 = getTerrainFoliageMaskValueAtSample(
mask, mask as TerrainFoliageMask,
minSampleX, minSampleX,
minSampleZ minSampleZ
); );
const value10 = getTerrainFoliageMaskValueAtSample( const value10 = getTerrainFoliageMaskValueAtSample(
mask, mask as TerrainFoliageMask,
maxSampleX, maxSampleX,
minSampleZ minSampleZ
); );
const value01 = getTerrainFoliageMaskValueAtSample( const value01 = getTerrainFoliageMaskValueAtSample(
mask, mask as TerrainFoliageMask,
minSampleX, minSampleX,
maxSampleZ maxSampleZ
); );
const value11 = getTerrainFoliageMaskValueAtSample( const value11 = getTerrainFoliageMaskValueAtSample(
mask, mask as TerrainFoliageMask,
maxSampleX, maxSampleX,
maxSampleZ maxSampleZ
); );
@@ -1009,6 +1124,14 @@ function sampleTerrainFoliageMaskAtGridCoordinate(
); );
} }
function sampleTerrainFoliageBlockerMaskAtGridCoordinate(
mask: TerrainFoliageBlockerMask,
sampleX: number,
sampleZ: number
): number {
return sampleTerrainFoliageMaskAtGridCoordinate(mask, sampleX, sampleZ);
}
export function sampleTerrainFoliageMaskAtLocalPosition( export function sampleTerrainFoliageMaskAtLocalPosition(
terrain: Terrain, terrain: Terrain,
layerId: string, layerId: string,
@@ -1061,6 +1184,49 @@ export function sampleTerrainFoliageMaskAtWorldPosition(
); );
} }
export function sampleTerrainFoliageBlockerMaskAtLocalPosition(
terrain: Terrain,
localX: number,
localZ: number,
clampToBounds = false
): number | null {
const sampleSpaceX = localX / terrain.cellSize;
const sampleSpaceZ = localZ / terrain.cellSize;
const maxSampleX = terrain.sampleCountX - 1;
const maxSampleZ = terrain.sampleCountZ - 1;
if (!clampToBounds) {
if (
sampleSpaceX < 0 ||
sampleSpaceX > maxSampleX ||
sampleSpaceZ < 0 ||
sampleSpaceZ > maxSampleZ
) {
return null;
}
}
return sampleTerrainFoliageBlockerMaskAtGridCoordinate(
terrain.foliageBlockerMask,
sampleSpaceX,
sampleSpaceZ
);
}
export function sampleTerrainFoliageBlockerMaskAtWorldPosition(
terrain: Terrain,
worldX: number,
worldZ: number,
clampToBounds = false
): number | null {
return sampleTerrainFoliageBlockerMaskAtLocalPosition(
terrain,
worldX - terrain.position.x,
worldZ - terrain.position.z,
clampToBounds
);
}
function createTerrainPositionFromCenter( function createTerrainPositionFromCenter(
center: Vec3, center: Vec3,
sampleCountX: number, sampleCountX: number,
@@ -1192,6 +1358,41 @@ function createResampledTerrainFoliageMasks(
); );
} }
function createResampledTerrainFoliageBlockerMask(
terrain: Terrain,
sampleCountX: number,
sampleCountZ: number
): TerrainFoliageBlockerMask {
const values = new Array<number>(sampleCountX * sampleCountZ);
for (let sampleZ = 0; sampleZ < sampleCountZ; sampleZ += 1) {
const normalizedSampleZ =
sampleCountZ === 1 ? 0 : sampleZ / (sampleCountZ - 1);
const sourceSampleZ =
normalizedSampleZ * (terrain.foliageBlockerMask.resolutionZ - 1);
for (let sampleX = 0; sampleX < sampleCountX; sampleX += 1) {
const normalizedSampleX =
sampleCountX === 1 ? 0 : sampleX / (sampleCountX - 1);
const sourceSampleX =
normalizedSampleX * (terrain.foliageBlockerMask.resolutionX - 1);
values[sampleZ * sampleCountX + sampleX] =
sampleTerrainFoliageBlockerMaskAtGridCoordinate(
terrain.foliageBlockerMask,
sourceSampleX,
sourceSampleZ
);
}
}
return createTerrainFoliageBlockerMask({
resolutionX: sampleCountX,
resolutionZ: sampleCountZ,
values
});
}
export function resizeTerrainGrid( export function resizeTerrainGrid(
terrain: Terrain, terrain: Terrain,
options: Pick<Terrain, "sampleCountX" | "sampleCountZ" | "cellSize"> & { options: Pick<Terrain, "sampleCountX" | "sampleCountZ" | "cellSize"> & {
@@ -1233,6 +1434,11 @@ export function resizeTerrainGrid(
terrain, terrain,
sampleCountX, sampleCountX,
sampleCountZ sampleCountZ
),
foliageBlockerMask: createResampledTerrainFoliageBlockerMask(
terrain,
sampleCountX,
sampleCountZ
) )
}); });
} }
@@ -1266,6 +1472,7 @@ export function createTerrain(
| "layers" | "layers"
| "paintWeights" | "paintWeights"
| "foliageMasks" | "foliageMasks"
| "foliageBlockerMask"
> >
> = {} > = {}
): Terrain { ): Terrain {
@@ -1299,6 +1506,11 @@ export function createTerrain(
sampleCountZ, sampleCountZ,
overrides.foliageMasks overrides.foliageMasks
); );
const foliageBlockerMask = normalizeTerrainFoliageBlockerMask(
sampleCountX,
sampleCountZ,
overrides.foliageBlockerMask
);
const visible = overrides.visible ?? DEFAULT_TERRAIN_VISIBLE; const visible = overrides.visible ?? DEFAULT_TERRAIN_VISIBLE;
const enabled = overrides.enabled ?? DEFAULT_TERRAIN_ENABLED; const enabled = overrides.enabled ?? DEFAULT_TERRAIN_ENABLED;
const collisionEnabled = normalizeTerrainCollisionEnabled( const collisionEnabled = normalizeTerrainCollisionEnabled(
@@ -1339,7 +1551,8 @@ export function createTerrain(
heights, heights,
layers, layers,
paintWeights, paintWeights,
foliageMasks foliageMasks,
foliageBlockerMask
}; };
} }
@@ -1382,7 +1595,16 @@ export function areTerrainsEqual(left: Terrain, right: Terrain): boolean {
leftMask.values.length === rightMask.values.length && leftMask.values.length === rightMask.values.length &&
leftMask.values.every((value, index) => value === rightMask.values[index]) leftMask.values.every((value, index) => value === rightMask.values[index])
); );
}) }) &&
left.foliageBlockerMask.resolutionX ===
right.foliageBlockerMask.resolutionX &&
left.foliageBlockerMask.resolutionZ ===
right.foliageBlockerMask.resolutionZ &&
left.foliageBlockerMask.values.length ===
right.foliageBlockerMask.values.length &&
left.foliageBlockerMask.values.every(
(value, index) => value === right.foliageBlockerMask.values[index]
)
); );
} }