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

View File

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