Enhance foliage rendering with cached chunk metrics, hysteresis LOD selection, and updated culling logic

This commit is contained in:
2026-05-03 13:34:29 +02:00
parent 8d6d53d5a6
commit 1a99be759d

View File

@@ -48,7 +48,10 @@ export interface FoliageRenderChunk {
layerId: string;
prototypeId: string;
chunkBounds: DerivedFoliageScatterChunk["bounds"];
center: Vec3;
radius: number;
lods: FoliageRenderLod[];
batchKeysByLodLevel: Partial<Record<FoliagePrototypeLodLevel, string>>;
lodBias: number;
maxCullDistance: number;
}
@@ -60,6 +63,8 @@ export interface FoliageRenderResourcePlan {
const IDENTITY_SOURCE_MATRIX = new Matrix4();
const UP_VECTOR = new Vector3(0, 1, 0);
const DEFAULT_FOLIAGE_LOD_HYSTERESIS_RATIO = 0.08;
const MIN_FOLIAGE_LOD_HYSTERESIS_DISTANCE = 0.5;
function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
@@ -99,6 +104,34 @@ function cloneChunkBounds(
};
}
function createChunkMetrics(
chunk: Pick<DerivedFoliageScatterChunk, "bounds">
): { center: Vec3; radius: number } {
const center = getChunkCenter(chunk);
return {
center,
radius: Math.max(
distanceBetween(center, chunk.bounds.min),
distanceBetween(center, chunk.bounds.max)
)
};
}
function distanceFromPointToCachedChunkCenter(
chunk: Pick<FoliageRenderChunk, "center">,
point: Vec3
): number {
return distanceBetween(chunk.center, point);
}
function getHysteresisDistance(distance: number, ratio: number): number {
return Math.max(
MIN_FOLIAGE_LOD_HYSTERESIS_DISTANCE,
Math.max(0, distance) * Math.max(0, ratio)
);
}
export function getFoliagePrototypeRenderLods(
prototype: FoliagePrototype
): FoliageRenderLod[] {
@@ -125,13 +158,46 @@ export function resolveFoliageRenderLod(options: {
cameraDistance: number;
lodBias: number;
maxDistanceMultiplier: number;
previousLodLevel?: FoliagePrototypeLodLevel | null;
hysteresisRatio?: number;
}): FoliageRenderLod | null {
const { lods, cameraDistance, lodBias, maxDistanceMultiplier } = options;
const {
lods,
cameraDistance,
lodBias,
maxDistanceMultiplier,
previousLodLevel = null,
hysteresisRatio = 0
} = options;
const distanceMultiplier = Math.max(0, maxDistanceMultiplier);
const biasedDistance = Math.max(
0,
cameraDistance * (1 + clamp(lodBias, -1, 1) * 0.12)
);
const previousLodIndex =
previousLodLevel === null
? -1
: lods.findIndex((lod) => lod.level === previousLodLevel);
if (previousLodIndex >= 0) {
const previousLod = lods[previousLodIndex]!;
const lowerDistance =
previousLodIndex === 0
? 0
: lods[previousLodIndex - 1]!.maxDistance * distanceMultiplier;
const upperDistance = previousLod.maxDistance * distanceMultiplier;
const hysteresisDistance = getHysteresisDistance(
upperDistance,
hysteresisRatio
);
if (
biasedDistance >= Math.max(0, lowerDistance - hysteresisDistance) &&
biasedDistance <= upperDistance + hysteresisDistance
) {
return previousLod;
}
}
for (const lod of lods) {
if (biasedDistance <= lod.maxDistance * distanceMultiplier) {
@@ -167,6 +233,38 @@ export function shouldCullFoliageChunkByFrustum(options: {
return !options.frustum.intersectsSphere(sphere);
}
function shouldCullCachedFoliageRenderChunkByDistance(options: {
chunk: Pick<FoliageRenderChunk, "center" | "radius">;
cameraPosition: Vec3;
maxDistance: number;
hysteresisDistance?: number;
}): boolean {
return (
distanceFromPointToCachedChunkCenter(options.chunk, options.cameraPosition) -
options.chunk.radius >
options.maxDistance + (options.hysteresisDistance ?? 0)
);
}
function shouldCullCachedFoliageRenderChunkByFrustum(options: {
chunk: Pick<FoliageRenderChunk, "center" | "radius">;
frustum: Frustum | null | undefined;
sphere: Sphere;
}): boolean {
if (options.frustum === null || options.frustum === undefined) {
return false;
}
options.sphere.center.set(
options.chunk.center.x,
options.chunk.center.y,
options.chunk.center.z
);
options.sphere.radius = options.chunk.radius;
return !options.frustum.intersectsSphere(options.sphere);
}
export function createFoliageRenderBatchKey(options: {
chunkId: string;
terrainId: string;
@@ -395,6 +493,10 @@ export function createFoliageRenderResourcePlan(
...group.instances.map((instance) => instance.cullDistance),
...renderLods.map((lod) => lod.maxDistance)
);
const chunkMetrics = createChunkMetrics(group.chunk);
const batchKeysByLodLevel: Partial<
Record<FoliagePrototypeLodLevel, string>
> = {};
chunks.push({
key,
@@ -403,21 +505,26 @@ export function createFoliageRenderResourcePlan(
layerId: group.layerId,
prototypeId: group.prototypeId,
chunkBounds: cloneChunkBounds(group.chunk.bounds),
center: chunkMetrics.center,
radius: chunkMetrics.radius,
lods: renderLods,
batchKeysByLodLevel,
lodBias,
maxCullDistance
});
for (const renderLod of renderLods) {
const batchKey = createFoliageRenderBatchKey({
chunkId: group.chunk.id,
terrainId: group.terrainId,
layerId: group.layerId,
prototypeId: group.prototypeId,
lodLevel: renderLod.level,
bundledPath: renderLod.bundledPath
});
batchKeysByLodLevel[renderLod.level] = batchKey;
batches.push({
key: createFoliageRenderBatchKey({
chunkId: group.chunk.id,
terrainId: group.terrainId,
layerId: group.layerId,
prototypeId: group.prototypeId,
lodLevel: renderLod.level,
bundledPath: renderLod.bundledPath
}),
key: batchKey,
chunkId: group.chunk.id,
terrainId: group.terrainId,
layerId: group.layerId,
@@ -441,8 +548,13 @@ export function resolveFoliageRenderChunkLod(options: {
chunk: FoliageRenderChunk;
view?: FoliageRenderView | null;
quality?: FoliageQualitySettings | null;
previousLodLevel?: FoliagePrototypeLodLevel | null;
hysteresisRatio?: number;
frustumSphere?: Sphere;
}): FoliageRenderLod | null {
const quality = resolveFoliageQualitySettings(options.quality);
const hysteresisRatio =
options.hysteresisRatio ?? DEFAULT_FOLIAGE_LOD_HYSTERESIS_RATIO;
if (
!quality.enabled ||
@@ -456,14 +568,11 @@ export function resolveFoliageRenderChunkLod(options: {
return options.chunk.lods[0]!;
}
const chunk = {
bounds: options.chunk.chunkBounds
};
if (
shouldCullFoliageChunkByFrustum({
chunk,
frustum: options.view.frustum
shouldCullCachedFoliageRenderChunkByFrustum({
chunk: options.chunk,
frustum: options.view.frustum,
sphere: options.frustumSphere ?? new Sphere()
})
) {
return null;
@@ -473,10 +582,15 @@ export function resolveFoliageRenderChunkLod(options: {
options.chunk.maxCullDistance * quality.maxDistanceMultiplier;
if (
shouldCullFoliageChunkByDistance({
chunk,
shouldCullCachedFoliageRenderChunkByDistance({
chunk: options.chunk,
cameraPosition: options.view.cameraPosition,
maxDistance: maxRenderDistance
maxDistance: maxRenderDistance,
hysteresisDistance:
options.previousLodLevel === null ||
options.previousLodLevel === undefined
? 0
: getHysteresisDistance(maxRenderDistance, hysteresisRatio)
})
) {
return null;
@@ -484,12 +598,14 @@ export function resolveFoliageRenderChunkLod(options: {
return resolveFoliageRenderLod({
lods: options.chunk.lods,
cameraDistance: distanceBetween(
getChunkCenter(chunk),
cameraDistance: distanceFromPointToCachedChunkCenter(
options.chunk,
options.view.cameraPosition
),
lodBias: options.chunk.lodBias,
maxDistanceMultiplier: quality.maxDistanceMultiplier
maxDistanceMultiplier: quality.maxDistanceMultiplier,
previousLodLevel: options.previousLodLevel,
hysteresisRatio
});
}