Enhance foliage rendering with cached chunk metrics, hysteresis LOD selection, and updated culling logic
This commit is contained in:
@@ -48,7 +48,10 @@ export interface FoliageRenderChunk {
|
|||||||
layerId: string;
|
layerId: string;
|
||||||
prototypeId: string;
|
prototypeId: string;
|
||||||
chunkBounds: DerivedFoliageScatterChunk["bounds"];
|
chunkBounds: DerivedFoliageScatterChunk["bounds"];
|
||||||
|
center: Vec3;
|
||||||
|
radius: number;
|
||||||
lods: FoliageRenderLod[];
|
lods: FoliageRenderLod[];
|
||||||
|
batchKeysByLodLevel: Partial<Record<FoliagePrototypeLodLevel, string>>;
|
||||||
lodBias: number;
|
lodBias: number;
|
||||||
maxCullDistance: number;
|
maxCullDistance: number;
|
||||||
}
|
}
|
||||||
@@ -60,6 +63,8 @@ export interface FoliageRenderResourcePlan {
|
|||||||
|
|
||||||
const IDENTITY_SOURCE_MATRIX = new Matrix4();
|
const IDENTITY_SOURCE_MATRIX = new Matrix4();
|
||||||
const UP_VECTOR = new Vector3(0, 1, 0);
|
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 {
|
function clamp(value: number, min: number, max: number): number {
|
||||||
return Math.min(max, Math.max(min, value));
|
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(
|
export function getFoliagePrototypeRenderLods(
|
||||||
prototype: FoliagePrototype
|
prototype: FoliagePrototype
|
||||||
): FoliageRenderLod[] {
|
): FoliageRenderLod[] {
|
||||||
@@ -125,13 +158,46 @@ export function resolveFoliageRenderLod(options: {
|
|||||||
cameraDistance: number;
|
cameraDistance: number;
|
||||||
lodBias: number;
|
lodBias: number;
|
||||||
maxDistanceMultiplier: number;
|
maxDistanceMultiplier: number;
|
||||||
|
previousLodLevel?: FoliagePrototypeLodLevel | null;
|
||||||
|
hysteresisRatio?: number;
|
||||||
}): FoliageRenderLod | null {
|
}): 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 distanceMultiplier = Math.max(0, maxDistanceMultiplier);
|
||||||
const biasedDistance = Math.max(
|
const biasedDistance = Math.max(
|
||||||
0,
|
0,
|
||||||
cameraDistance * (1 + clamp(lodBias, -1, 1) * 0.12)
|
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) {
|
for (const lod of lods) {
|
||||||
if (biasedDistance <= lod.maxDistance * distanceMultiplier) {
|
if (biasedDistance <= lod.maxDistance * distanceMultiplier) {
|
||||||
@@ -167,6 +233,38 @@ export function shouldCullFoliageChunkByFrustum(options: {
|
|||||||
return !options.frustum.intersectsSphere(sphere);
|
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: {
|
export function createFoliageRenderBatchKey(options: {
|
||||||
chunkId: string;
|
chunkId: string;
|
||||||
terrainId: string;
|
terrainId: string;
|
||||||
@@ -395,6 +493,10 @@ export function createFoliageRenderResourcePlan(
|
|||||||
...group.instances.map((instance) => instance.cullDistance),
|
...group.instances.map((instance) => instance.cullDistance),
|
||||||
...renderLods.map((lod) => lod.maxDistance)
|
...renderLods.map((lod) => lod.maxDistance)
|
||||||
);
|
);
|
||||||
|
const chunkMetrics = createChunkMetrics(group.chunk);
|
||||||
|
const batchKeysByLodLevel: Partial<
|
||||||
|
Record<FoliagePrototypeLodLevel, string>
|
||||||
|
> = {};
|
||||||
|
|
||||||
chunks.push({
|
chunks.push({
|
||||||
key,
|
key,
|
||||||
@@ -403,21 +505,26 @@ export function createFoliageRenderResourcePlan(
|
|||||||
layerId: group.layerId,
|
layerId: group.layerId,
|
||||||
prototypeId: group.prototypeId,
|
prototypeId: group.prototypeId,
|
||||||
chunkBounds: cloneChunkBounds(group.chunk.bounds),
|
chunkBounds: cloneChunkBounds(group.chunk.bounds),
|
||||||
|
center: chunkMetrics.center,
|
||||||
|
radius: chunkMetrics.radius,
|
||||||
lods: renderLods,
|
lods: renderLods,
|
||||||
|
batchKeysByLodLevel,
|
||||||
lodBias,
|
lodBias,
|
||||||
maxCullDistance
|
maxCullDistance
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const renderLod of renderLods) {
|
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({
|
batches.push({
|
||||||
key: createFoliageRenderBatchKey({
|
key: batchKey,
|
||||||
chunkId: group.chunk.id,
|
|
||||||
terrainId: group.terrainId,
|
|
||||||
layerId: group.layerId,
|
|
||||||
prototypeId: group.prototypeId,
|
|
||||||
lodLevel: renderLod.level,
|
|
||||||
bundledPath: renderLod.bundledPath
|
|
||||||
}),
|
|
||||||
chunkId: group.chunk.id,
|
chunkId: group.chunk.id,
|
||||||
terrainId: group.terrainId,
|
terrainId: group.terrainId,
|
||||||
layerId: group.layerId,
|
layerId: group.layerId,
|
||||||
@@ -441,8 +548,13 @@ export function resolveFoliageRenderChunkLod(options: {
|
|||||||
chunk: FoliageRenderChunk;
|
chunk: FoliageRenderChunk;
|
||||||
view?: FoliageRenderView | null;
|
view?: FoliageRenderView | null;
|
||||||
quality?: FoliageQualitySettings | null;
|
quality?: FoliageQualitySettings | null;
|
||||||
|
previousLodLevel?: FoliagePrototypeLodLevel | null;
|
||||||
|
hysteresisRatio?: number;
|
||||||
|
frustumSphere?: Sphere;
|
||||||
}): FoliageRenderLod | null {
|
}): FoliageRenderLod | null {
|
||||||
const quality = resolveFoliageQualitySettings(options.quality);
|
const quality = resolveFoliageQualitySettings(options.quality);
|
||||||
|
const hysteresisRatio =
|
||||||
|
options.hysteresisRatio ?? DEFAULT_FOLIAGE_LOD_HYSTERESIS_RATIO;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!quality.enabled ||
|
!quality.enabled ||
|
||||||
@@ -456,14 +568,11 @@ export function resolveFoliageRenderChunkLod(options: {
|
|||||||
return options.chunk.lods[0]!;
|
return options.chunk.lods[0]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunk = {
|
|
||||||
bounds: options.chunk.chunkBounds
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
shouldCullFoliageChunkByFrustum({
|
shouldCullCachedFoliageRenderChunkByFrustum({
|
||||||
chunk,
|
chunk: options.chunk,
|
||||||
frustum: options.view.frustum
|
frustum: options.view.frustum,
|
||||||
|
sphere: options.frustumSphere ?? new Sphere()
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
@@ -473,10 +582,15 @@ export function resolveFoliageRenderChunkLod(options: {
|
|||||||
options.chunk.maxCullDistance * quality.maxDistanceMultiplier;
|
options.chunk.maxCullDistance * quality.maxDistanceMultiplier;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
shouldCullFoliageChunkByDistance({
|
shouldCullCachedFoliageRenderChunkByDistance({
|
||||||
chunk,
|
chunk: options.chunk,
|
||||||
cameraPosition: options.view.cameraPosition,
|
cameraPosition: options.view.cameraPosition,
|
||||||
maxDistance: maxRenderDistance
|
maxDistance: maxRenderDistance,
|
||||||
|
hysteresisDistance:
|
||||||
|
options.previousLodLevel === null ||
|
||||||
|
options.previousLodLevel === undefined
|
||||||
|
? 0
|
||||||
|
: getHysteresisDistance(maxRenderDistance, hysteresisRatio)
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
@@ -484,12 +598,14 @@ export function resolveFoliageRenderChunkLod(options: {
|
|||||||
|
|
||||||
return resolveFoliageRenderLod({
|
return resolveFoliageRenderLod({
|
||||||
lods: options.chunk.lods,
|
lods: options.chunk.lods,
|
||||||
cameraDistance: distanceBetween(
|
cameraDistance: distanceFromPointToCachedChunkCenter(
|
||||||
getChunkCenter(chunk),
|
options.chunk,
|
||||||
options.view.cameraPosition
|
options.view.cameraPosition
|
||||||
),
|
),
|
||||||
lodBias: options.chunk.lodBias,
|
lodBias: options.chunk.lodBias,
|
||||||
maxDistanceMultiplier: quality.maxDistanceMultiplier
|
maxDistanceMultiplier: quality.maxDistanceMultiplier,
|
||||||
|
previousLodLevel: options.previousLodLevel,
|
||||||
|
hysteresisRatio
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user