Enhance terrain rendering and brush feedback by implementing dirty bounds tracking and chunk refresh logic

This commit is contained in:
2026-04-30 02:55:37 +02:00
parent ff2c10bc2a
commit 3136a47bb7

View File

@@ -9089,6 +9089,99 @@ export class ViewportHost {
); );
} }
private terrainChunkOverlapsDirtySampleBounds(
chunk: TerrainRenderChunkObjects,
bounds: TerrainBrushDirtySampleBounds
): boolean {
return (
chunk.startSampleX <= bounds.maxSampleX &&
chunk.endSampleX >= bounds.minSampleX &&
chunk.startSampleZ <= bounds.maxSampleZ &&
chunk.endSampleZ >= bounds.minSampleZ
);
}
private refreshDisplayedTerrainDirtyBounds(
terrainId: string,
bounds: TerrainBrushDirtySampleBounds | null
) {
if (bounds === null) {
return;
}
const terrain = this.getDisplayedTerrainState(terrainId);
const renderObjects = this.terrainRenderObjects.get(terrainId);
if (terrain === null || renderObjects === undefined) {
this.rebuildDisplayedTerrainState();
return;
}
let refreshedAnyChunk = false;
for (const chunk of renderObjects.chunks) {
if (!this.terrainChunkOverlapsDirtySampleBounds(chunk, bounds)) {
continue;
}
const nextChunk = buildTerrainLodChunkMeshData(
terrain,
chunk.startSampleX,
chunk.startSampleZ
);
if (nextChunk === null) {
this.rebuildDisplayedTerrainState();
return;
}
const previousGeometries = new Set(chunk.levelGeometries);
const nextLevelGeometries = nextChunk.levels.map(
(level) => level.geometry
);
const nextActiveLevelIndex = Math.min(
chunk.activeLevelIndex,
nextLevelGeometries.length - 1
);
chunk.levelGeometries = nextLevelGeometries;
chunk.activeLevelIndex = nextActiveLevelIndex;
chunk.startSampleX = nextChunk.startSampleX;
chunk.startSampleZ = nextChunk.startSampleZ;
chunk.endSampleX = nextChunk.endSampleX;
chunk.endSampleZ = nextChunk.endSampleZ;
chunk.worldCenter = {
x: terrain.position.x + nextChunk.localCenter.x,
y: terrain.position.y + nextChunk.localCenter.y,
z: terrain.position.z + nextChunk.localCenter.z
};
chunk.diagonal = nextChunk.diagonal;
chunk.mesh.geometry = nextLevelGeometries[nextActiveLevelIndex]!;
chunk.mesh.material =
nextActiveLevelIndex >= 2
? renderObjects.distantMaterial
: renderObjects.detailMaterial;
chunk.debugMesh.geometry = nextLevelGeometries[nextActiveLevelIndex]!;
chunk.debugMesh.material =
renderObjects.debugMaterials[nextActiveLevelIndex] ??
renderObjects.debugMaterials[renderObjects.debugMaterials.length - 1]!;
chunk.pickMesh.geometry = nextLevelGeometries[0]!;
for (const geometry of previousGeometries) {
geometry.dispose();
}
refreshedAnyChunk = true;
}
if (!refreshedAnyChunk) {
return;
}
this.updateTerrainLodVisibility();
this.syncTerrainBrushPreview();
}
private isTerrainBrushActive(): boolean { private isTerrainBrushActive(): boolean {
return ( return (
this.toolMode === "select" && this.toolMode === "select" &&
@@ -9309,8 +9402,8 @@ export class ViewportHost {
}, },
toolState: ArmedTerrainBrushState, toolState: ArmedTerrainBrushState,
referenceHeight: number | null referenceHeight: number | null
): Terrain { ): ReturnType<typeof applyTerrainBrushStampInPlace> {
return applyTerrainBrushStamp({ return applyTerrainBrushStampInPlace({
terrain, terrain,
center: point, center: point,
settings: toolState, settings: toolState,
@@ -9333,7 +9426,8 @@ export class ViewportHost {
toolState: ArmedTerrainBrushState, toolState: ArmedTerrainBrushState,
referenceHeight: number | null referenceHeight: number | null
): { ): {
terrain: Terrain; changed: boolean;
dirtyBounds: TerrainBrushDirtySampleBounds | null;
lastAppliedPoint: { lastAppliedPoint: {
x: number; x: number;
z: number; z: number;
@@ -9346,14 +9440,43 @@ export class ViewportHost {
if (distance < spacing) { if (distance < spacing) {
return { return {
terrain, changed: false,
dirtyBounds: null,
lastAppliedPoint: from lastAppliedPoint: from
}; };
} }
let nextTerrain = terrain; let changed = false;
let dirtyBounds: TerrainBrushDirtySampleBounds | null = null;
let lastAppliedPoint = from; let lastAppliedPoint = from;
const stepCount = Math.floor(distance / spacing); const stepCount = Math.floor(distance / spacing);
const mergeDirtyBounds = (nextBounds: TerrainBrushDirtySampleBounds | null) => {
if (nextBounds === null) {
return;
}
if (dirtyBounds === null) {
dirtyBounds = { ...nextBounds };
return;
}
dirtyBounds.minSampleX = Math.min(
dirtyBounds.minSampleX,
nextBounds.minSampleX
);
dirtyBounds.maxSampleX = Math.max(
dirtyBounds.maxSampleX,
nextBounds.maxSampleX
);
dirtyBounds.minSampleZ = Math.min(
dirtyBounds.minSampleZ,
nextBounds.minSampleZ
);
dirtyBounds.maxSampleZ = Math.max(
dirtyBounds.maxSampleZ,
nextBounds.maxSampleZ
);
};
for (let stepIndex = 1; stepIndex <= stepCount; stepIndex += 1) { for (let stepIndex = 1; stepIndex <= stepCount; stepIndex += 1) {
const t = Math.min(1, (stepIndex * spacing) / distance); const t = Math.min(1, (stepIndex * spacing) / distance);
@@ -9361,17 +9484,20 @@ export class ViewportHost {
x: from.x + deltaX * t, x: from.x + deltaX * t,
z: from.z + deltaZ * t z: from.z + deltaZ * t
}; };
nextTerrain = this.applyTerrainBrushPoint( const result = this.applyTerrainBrushPoint(
nextTerrain, terrain,
point, point,
toolState, toolState,
referenceHeight referenceHeight
); );
changed ||= result.changed;
mergeDirtyBounds(result.dirtyBounds);
lastAppliedPoint = point; lastAppliedPoint = point;
} }
return { return {
terrain: nextTerrain, changed,
dirtyBounds,
lastAppliedPoint lastAppliedPoint
}; };
} }