Refactor foliage batch generation with advanced culling and LOD resolution

This commit is contained in:
2026-05-02 10:50:01 +02:00
parent b6b4232499
commit 2c09c4c3e1

View File

@@ -1,22 +1,43 @@
import { Matrix4, Quaternion, Vector3 } from "three"; import { Frustum, Matrix4, Quaternion, Sphere, Vector3 } from "three";
import type { Vec3 } from "../core/vector"; import type { Vec3 } from "../core/vector";
import type { FoliagePrototype, FoliagePrototypeRegistry } from "./foliage"; import {
resolveFoliageQualitySettings,
type FoliageQualitySettings
} from "../document/world-settings";
import type {
FoliagePrototype,
FoliagePrototypeLodLevel,
FoliagePrototypeRegistry
} from "./foliage";
import type { import type {
DerivedFoliageInstance, DerivedFoliageInstance,
DerivedFoliageScatterChunk,
FoliageScatterResult FoliageScatterResult
} from "./foliage-scatter"; } from "./foliage-scatter";
export const FOLIAGE_RENDER_LOD_LEVEL = 0 as const; export interface FoliageRenderLod {
level: FoliagePrototypeLodLevel;
bundledPath: string;
maxDistance: number;
castShadow: boolean;
}
export interface FoliageRenderView {
cameraPosition: Vec3;
frustum?: Frustum | null;
}
export interface FoliageRenderBatch { export interface FoliageRenderBatch {
key: string; key: string;
chunkId: string;
terrainId: string; terrainId: string;
layerId: string; layerId: string;
prototypeId: string; prototypeId: string;
lodLevel: typeof FOLIAGE_RENDER_LOD_LEVEL; lodLevel: FoliagePrototypeLodLevel;
bundledPath: string; bundledPath: string;
castShadow: boolean; castShadow: boolean;
chunkBounds: DerivedFoliageScatterChunk["bounds"];
instances: DerivedFoliageInstance[]; instances: DerivedFoliageInstance[];
} }
@@ -31,48 +52,141 @@ function createVector3(vector: Vec3): Vector3 {
return new Vector3(vector.x, vector.y, vector.z); return new Vector3(vector.x, vector.y, vector.z);
} }
export function getFoliagePrototypeRenderLod( function distanceBetween(left: Vec3, right: Vec3): number {
prototype: FoliagePrototype return Math.hypot(left.x - right.x, left.y - right.y, left.z - right.z);
): {
bundledPath: string;
castShadow: boolean;
} | null {
const lod = prototype.lods.find(
(candidate) => candidate.level === FOLIAGE_RENDER_LOD_LEVEL
);
if (lod === undefined || lod.source !== "bundled") {
return null;
} }
function getChunkCenter(chunk: Pick<DerivedFoliageScatterChunk, "bounds">): Vec3 {
return { return {
bundledPath: lod.bundledPath, x: (chunk.bounds.min.x + chunk.bounds.max.x) * 0.5,
castShadow: lod.castShadow y: (chunk.bounds.min.y + chunk.bounds.max.y) * 0.5,
z: (chunk.bounds.min.z + chunk.bounds.max.z) * 0.5
}; };
} }
function getChunkRadius(chunk: Pick<DerivedFoliageScatterChunk, "bounds">): number {
const center = getChunkCenter(chunk);
return Math.max(
distanceBetween(center, chunk.bounds.min),
distanceBetween(center, chunk.bounds.max)
);
}
function cloneChunkBounds(
bounds: DerivedFoliageScatterChunk["bounds"]
): DerivedFoliageScatterChunk["bounds"] {
return {
min: { ...bounds.min },
max: { ...bounds.max }
};
}
export function getFoliagePrototypeRenderLods(
prototype: FoliagePrototype
): FoliageRenderLod[] {
return prototype.lods
.filter((lod) => lod.source === "bundled")
.map((lod) => ({
level: lod.level,
bundledPath: lod.bundledPath,
maxDistance: lod.maxDistance,
castShadow: lod.castShadow
}))
.sort((left, right) => left.level - right.level);
}
export function resolveFoliageRenderLod(options: {
lods: readonly FoliageRenderLod[];
cameraDistance: number;
lodBias: number;
maxDistanceMultiplier: number;
}): FoliageRenderLod | null {
const { lods, cameraDistance, lodBias, maxDistanceMultiplier } = options;
const distanceMultiplier = Math.max(0, maxDistanceMultiplier);
const biasedDistance = Math.max(
0,
cameraDistance * (1 + clamp(lodBias, -1, 1) * 0.12)
);
for (const lod of lods) {
if (biasedDistance <= lod.maxDistance * distanceMultiplier) {
return lod;
}
}
return null;
}
export function shouldCullFoliageChunkByDistance(options: {
chunk: Pick<DerivedFoliageScatterChunk, "bounds">;
cameraPosition: Vec3;
maxDistance: number;
}): boolean {
const center = getChunkCenter(options.chunk);
const radius = getChunkRadius(options.chunk);
return distanceBetween(center, options.cameraPosition) - radius > options.maxDistance;
}
export function shouldCullFoliageChunkByFrustum(options: {
chunk: Pick<DerivedFoliageScatterChunk, "bounds">;
frustum: Frustum | null | undefined;
}): boolean {
if (options.frustum === null || options.frustum === undefined) {
return false;
}
const center = getChunkCenter(options.chunk);
const sphere = new Sphere(createVector3(center), getChunkRadius(options.chunk));
return !options.frustum.intersectsSphere(sphere);
}
export function createFoliageRenderBatchKey(options: { export function createFoliageRenderBatchKey(options: {
chunkId: string;
terrainId: string; terrainId: string;
layerId: string; layerId: string;
prototypeId: string; prototypeId: string;
lodLevel: FoliagePrototypeLodLevel;
bundledPath: string; bundledPath: string;
}): string { }): string {
return [ return [
options.chunkId,
options.terrainId, options.terrainId,
options.layerId, options.layerId,
options.prototypeId, options.prototypeId,
FOLIAGE_RENDER_LOD_LEVEL, options.lodLevel,
options.bundledPath options.bundledPath
].join("|"); ].join("|");
} }
export function createFoliageRenderBatches( export function createFoliageRenderBatches(
scatter: FoliageScatterResult, scatter: FoliageScatterResult,
prototypeRegistry: FoliagePrototypeRegistry prototypeRegistry: FoliagePrototypeRegistry,
options: {
view?: FoliageRenderView | null;
quality?: FoliageQualitySettings | null;
} = {}
): FoliageRenderBatch[] { ): FoliageRenderBatch[] {
const quality = resolveFoliageQualitySettings(options.quality);
if (!quality.enabled || quality.densityMultiplier <= 0) {
return [];
}
const batches = new Map<string, FoliageRenderBatch>(); const batches = new Map<string, FoliageRenderBatch>();
for (const chunk of scatter.chunks) { for (const chunk of scatter.chunks) {
if (
shouldCullFoliageChunkByFrustum({
chunk,
frustum: options.view?.frustum
})
) {
continue;
}
for (const instance of chunk.instances) { for (const instance of chunk.instances) {
const prototype = prototypeRegistry[instance.prototypeId]; const prototype = prototypeRegistry[instance.prototypeId];
@@ -80,16 +194,59 @@ export function createFoliageRenderBatches(
continue; continue;
} }
const renderLod = getFoliagePrototypeRenderLod(prototype); const renderLods = getFoliagePrototypeRenderLods(prototype);
if (renderLods.length === 0) {
continue;
}
const maxRenderDistance =
Math.max(
instance.cullDistance,
...renderLods.map((lod) => lod.maxDistance)
) * quality.maxDistanceMultiplier;
if (
options.view !== null &&
options.view !== undefined &&
shouldCullFoliageChunkByDistance({
chunk,
cameraPosition: options.view.cameraPosition,
maxDistance: maxRenderDistance
})
) {
continue;
}
const renderLod =
options.view === null || options.view === undefined
? renderLods[0]!
: resolveFoliageRenderLod({
lods: renderLods,
cameraDistance: distanceBetween(
instance.position,
options.view.cameraPosition
),
lodBias: instance.lodBias,
maxDistanceMultiplier: quality.maxDistanceMultiplier
});
if (renderLod === null) { if (renderLod === null) {
continue; continue;
} }
const castShadow =
quality.shadows === "off"
? false
: quality.shadows === "full"
? renderLod.castShadow
: renderLod.castShadow && renderLod.level <= 1;
const key = createFoliageRenderBatchKey({ const key = createFoliageRenderBatchKey({
chunkId: chunk.id,
terrainId: instance.terrainId, terrainId: instance.terrainId,
layerId: instance.layerId, layerId: instance.layerId,
prototypeId: instance.prototypeId, prototypeId: instance.prototypeId,
lodLevel: renderLod.level,
bundledPath: renderLod.bundledPath bundledPath: renderLod.bundledPath
}); });
let batch = batches.get(key); let batch = batches.get(key);
@@ -97,12 +254,14 @@ export function createFoliageRenderBatches(
if (batch === undefined) { if (batch === undefined) {
batch = { batch = {
key, key,
chunkId: chunk.id,
terrainId: instance.terrainId, terrainId: instance.terrainId,
layerId: instance.layerId, layerId: instance.layerId,
prototypeId: instance.prototypeId, prototypeId: instance.prototypeId,
lodLevel: FOLIAGE_RENDER_LOD_LEVEL, lodLevel: renderLod.level,
bundledPath: renderLod.bundledPath, bundledPath: renderLod.bundledPath,
castShadow: renderLod.castShadow, castShadow,
chunkBounds: cloneChunkBounds(chunk.bounds),
instances: [] instances: []
}; };
batches.set(key, batch); batches.set(key, batch);