auto-git:

[change] src/geometry/terrain-mesh.ts
 [change] tests/geometry/terrain-mesh.test.ts
This commit is contained in:
2026-05-21 07:59:27 +02:00
parent ee65a479e3
commit 77f1a19310
2 changed files with 55 additions and 78 deletions

View File

@@ -37,11 +37,7 @@ export interface DerivedTerrainMeshData {
export const TERRAIN_LOD_CHUNK_SIZE_CELLS = 64;
export const TERRAIN_LOD_STRIDES = [1, 2, 4, 8, 16] as const;
export const TERRAIN_LOD_DEBUG_COLORS = [
0xff4d4d,
0xffa53d,
0xffe66d,
0x4ee06f,
0x4ba3ff
0xff4d4d, 0xffa53d, 0xffe66d, 0x4ee06f, 0x4ba3ff
] as const;
const TERRAIN_LOD_DISTANCE_MULTIPLIERS = [0.75, 1.5, 3, 6] as const;
const TERRAIN_LOD_HYSTERESIS_RATIO = 0.16;
@@ -232,7 +228,9 @@ export function buildTerrainDerivedMeshData(
const vertexCount = terrain.sampleCountX * terrain.sampleCountZ;
const positions = new Float32Array(vertexCount * 3);
const uvs = new Float32Array(vertexCount * 2);
const layerWeights = new Float32Array(vertexCount * TERRAIN_SHADER_LAYER_COUNT);
const layerWeights = new Float32Array(
vertexCount * TERRAIN_SHADER_LAYER_COUNT
);
const foliageMaskWeights = new Float32Array(vertexCount);
const foliageMask =
options.foliageMaskLayerId === undefined ||
@@ -289,11 +287,7 @@ export function buildTerrainDerivedMeshData(
)
: foliageMask === null
? 0
: getTerrainFoliageMaskValueAtSample(
foliageMask,
sampleX,
sampleZ
);
: getTerrainFoliageMaskValueAtSample(foliageMask, sampleX, sampleZ);
foliageMaskWeightOffset += 1;
}
}
@@ -494,7 +488,11 @@ function sampleTerrainLayerWeightsAtSamplePosition(
): number[] {
const layerWeights = new Array<number>(terrain.layers.length).fill(0);
for (let layerIndex = 0; layerIndex < terrain.layers.length; layerIndex += 1) {
for (
let layerIndex = 0;
layerIndex < terrain.layers.length;
layerIndex += 1
) {
layerWeights[layerIndex] = sampleTerrainScalarAtSamplePosition(
terrain,
sampleX,
@@ -550,13 +548,22 @@ function pushTerrainLodNormal(
sampleZ: number,
normals: number[]
) {
const leftSampleX = clampSampleCoordinate(sampleX - 1, terrain.sampleCountX - 1);
const rightSampleX = clampSampleCoordinate(sampleX + 1, terrain.sampleCountX - 1);
const leftSampleX = clampSampleCoordinate(
sampleX - 1,
terrain.sampleCountX - 1
);
const rightSampleX = clampSampleCoordinate(
sampleX + 1,
terrain.sampleCountX - 1
);
const bottomSampleZ = clampSampleCoordinate(
sampleZ - 1,
terrain.sampleCountZ - 1
);
const topSampleZ = clampSampleCoordinate(sampleZ + 1, terrain.sampleCountZ - 1);
const topSampleZ = clampSampleCoordinate(
sampleZ + 1,
terrain.sampleCountZ - 1
);
const dxDenominator = Math.max(
(rightSampleX - leftSampleX) * terrain.cellSize,
Number.EPSILON
@@ -752,7 +759,10 @@ function pushTerrainLodBoundaryPoint(
) {
const previous = points[points.length - 1];
if (previous !== undefined && areTerrainLodSamplePointsEqual(previous, point)) {
if (
previous !== undefined &&
areTerrainLodSamplePointsEqual(previous, point)
) {
return;
}
@@ -844,16 +854,8 @@ function buildTerrainLodLevelMeshData(
stride: number,
options: TerrainMeshBuildOptions
): TerrainLodLevelMeshData {
const sampleXs = createLodSampleCoordinates(
startSampleX,
endSampleX,
stride
);
const sampleZs = createLodSampleCoordinates(
startSampleZ,
endSampleZ,
stride
);
const sampleXs = createLodSampleCoordinates(startSampleX, endSampleX, stride);
const sampleZs = createLodSampleCoordinates(startSampleZ, endSampleZ, stride);
const positions: number[] = [];
const uvs: number[] = [];
const layerWeights: number[] = [];
@@ -1307,29 +1309,23 @@ export function resolveTerrainLodLevelIndexWithHysteresis(options: {
const lowerBoundary =
activeLevelIndex <= 0
? Number.NEGATIVE_INFINITY
: TERRAIN_LOD_DISTANCE_MULTIPLIERS[activeLevelIndex - 1] ??
: (TERRAIN_LOD_DISTANCE_MULTIPLIERS[activeLevelIndex - 1] ??
TERRAIN_LOD_DISTANCE_MULTIPLIERS[
TERRAIN_LOD_DISTANCE_MULTIPLIERS.length - 1
]!;
]!);
const upperBoundary =
activeLevelIndex >= options.levelCount - 1
? Number.POSITIVE_INFINITY
: TERRAIN_LOD_DISTANCE_MULTIPLIERS[activeLevelIndex] ??
: (TERRAIN_LOD_DISTANCE_MULTIPLIERS[activeLevelIndex] ??
TERRAIN_LOD_DISTANCE_MULTIPLIERS[
TERRAIN_LOD_DISTANCE_MULTIPLIERS.length - 1
]!;
]!);
if (
normalizedDistance >
upperBoundary * (1 + TERRAIN_LOD_HYSTERESIS_RATIO)
) {
if (normalizedDistance > upperBoundary * (1 + TERRAIN_LOD_HYSTERESIS_RATIO)) {
return Math.min(activeLevelIndex + 1, options.levelCount - 1);
}
if (
normalizedDistance <
lowerBoundary * (1 - TERRAIN_LOD_HYSTERESIS_RATIO)
) {
if (normalizedDistance < lowerBoundary * (1 - TERRAIN_LOD_HYSTERESIS_RATIO)) {
return Math.max(activeLevelIndex - 1, 0);
}

View File

@@ -14,7 +14,10 @@ import {
resolveTerrainLodLevelIndexWithHysteresis
} from "../../src/geometry/terrain-mesh";
function createJaggedTerrainHeights(sampleCountX: number, sampleCountZ: number) {
function createJaggedTerrainHeights(
sampleCountX: number,
sampleCountZ: number
) {
return Array.from({ length: sampleCountX * sampleCountZ }, (_, index) => {
const sampleX = index % sampleCountX;
const sampleZ = Math.floor(index / sampleCountX);
@@ -62,13 +65,9 @@ function collectTopEdgeKeys(options: {
}
const sampleX =
options.axis === "x"
? options.sampleCoordinate
: roundedVaryingSample;
options.axis === "x" ? options.sampleCoordinate : roundedVaryingSample;
const sampleZ =
options.axis === "x"
? roundedVaryingSample
: options.sampleCoordinate;
options.axis === "x" ? roundedVaryingSample : options.sampleCoordinate;
const expectedHeight = getTerrainHeightAtSample(
options.terrain,
sampleX,
@@ -83,7 +82,9 @@ function collectTopEdgeKeys(options: {
keys.add(`${roundedVaryingSample}:${height.toFixed(6)}`);
}
return [...keys].sort((left, right) => Number(left.split(":")[0]) - Number(right.split(":")[0]));
return [...keys].sort(
(left, right) => Number(left.split(":")[0]) - Number(right.split(":")[0])
);
}
function collectLoweredEdgeVertexCount(options: {
@@ -123,18 +124,17 @@ function collectLoweredEdgeVertexCount(options: {
continue;
}
if (roundedVaryingSample === 0 || roundedVaryingSample === maxVaryingSample) {
if (
roundedVaryingSample === 0 ||
roundedVaryingSample === maxVaryingSample
) {
continue;
}
const sampleX =
options.axis === "x"
? options.sampleCoordinate
: roundedVaryingSample;
options.axis === "x" ? options.sampleCoordinate : roundedVaryingSample;
const sampleZ =
options.axis === "x"
? roundedVaryingSample
: options.sampleCoordinate;
options.axis === "x" ? roundedVaryingSample : options.sampleCoordinate;
const expectedHeight = getTerrainHeightAtSample(
options.terrain,
sampleX,
@@ -201,27 +201,16 @@ describe("terrain mesh generation", () => {
});
const derivedMesh = buildTerrainDerivedMeshData(terrain);
expect(Array.from(derivedMesh.uvs)).toEqual([10, -6, 12, -6, 10, -4, 12, -4]);
expect(Array.from(derivedMesh.uvs)).toEqual([
10, -6, 12, -6, 10, -4, 12, -4
]);
});
it("derives full per-vertex layer weights from the compact terrain paint data", () => {
const terrain = createTerrain({
sampleCountX: 2,
sampleCountZ: 2,
paintWeights: [
0.2,
0.3,
0.1,
0,
0.5,
0,
0.1,
0.1,
0.1,
0.25,
0.25,
0.25
]
paintWeights: [0.2, 0.3, 0.1, 0, 0.5, 0, 0.1, 0.1, 0.1, 0.25, 0.25, 0.25]
});
const derivedMesh = buildTerrainDerivedMeshData(terrain);
@@ -318,11 +307,7 @@ describe("terrain mesh generation", () => {
expect(chunk).toBeDefined();
expect(chunk!.levels.map((level) => level.stride)).toEqual([
1,
2,
4,
8,
16
1, 2, 4, 8, 16
]);
const vertexCounts = chunk!.levels.map(
@@ -362,11 +347,7 @@ describe("terrain mesh generation", () => {
const [chunk] = buildTerrainLodMeshData(terrain).chunks;
const [level] = chunk!.levels;
for (
let offset = 0;
offset < level!.layerWeights.length;
offset += 8
) {
for (let offset = 0; offset < level!.layerWeights.length; offset += 8) {
const sum =
level!.layerWeights[offset]! +
level!.layerWeights[offset + 1]! +