Enhance foliage rendering with view-based updates and quality settings support

This commit is contained in:
2026-05-02 10:51:12 +02:00
parent a53de8a33d
commit 590d255221

View File

@@ -1,29 +1,39 @@
import { import {
Camera,
Color, Color,
Frustum,
Group, Group,
InstancedMesh, InstancedMesh,
Matrix4, Matrix4,
Mesh, Mesh,
Vector3,
type BufferGeometry, type BufferGeometry,
type Material type Material
} from "three"; } from "three";
import type { Terrain } from "../document/terrains"; import type { Terrain } from "../document/terrains";
import {
resolveFoliageQualitySettings,
type FoliageQualitySettings
} from "../document/world-settings";
import { applyRendererRenderCategoryFromMaterial } from "../rendering/render-layers"; import { applyRendererRenderCategoryFromMaterial } from "../rendering/render-layers";
import { loadBundledFoliageModelTemplate } from "./bundled-foliage-model-loader"; import { loadBundledFoliageModelTemplate } from "./bundled-foliage-model-loader";
import { import {
createFoliageInstanceMatrix, createFoliageInstanceMatrix,
createFoliageRenderBatches, createFoliageRenderBatches,
type FoliageRenderBatch type FoliageRenderBatch,
type FoliageRenderView
} from "./foliage-render-batches"; } from "./foliage-render-batches";
import type { import type {
FoliageLayer,
FoliageLayerRegistry, FoliageLayerRegistry,
FoliagePrototypeRegistry FoliagePrototypeRegistry
} from "./foliage"; } from "./foliage";
import { import {
createFoliageScatterPrototypeRegistry, createFoliageScatterPrototypeRegistry,
generateFoliageScatterForScene, generateFoliageScatterForScene,
type FoliageScatterPrototypeSource type FoliageScatterPrototypeSource,
type FoliageScatterResult
} from "./foliage-scatter"; } from "./foliage-scatter";
export interface FoliageInstancedRendererSyncInput { export interface FoliageInstancedRendererSyncInput {
@@ -31,6 +41,7 @@ export interface FoliageInstancedRendererSyncInput {
foliageLayers: FoliageLayerRegistry; foliageLayers: FoliageLayerRegistry;
foliagePrototypes?: FoliagePrototypeRegistry; foliagePrototypes?: FoliagePrototypeRegistry;
bundledFoliagePrototypes?: FoliageScatterPrototypeSource; bundledFoliagePrototypes?: FoliageScatterPrototypeSource;
quality?: FoliageQualitySettings | null;
} }
export interface FoliageInstancedRendererOptions { export interface FoliageInstancedRendererOptions {
@@ -44,6 +55,8 @@ interface FoliageTemplateSourceMesh {
localMatrix: Matrix4; localMatrix: Matrix4;
} }
const VIEW_SIGNATURE_PRECISION = 100;
function cloneMaterial(material: Material): Material { function cloneMaterial(material: Material): Material {
return material.clone(); return material.clone();
} }
@@ -101,6 +114,62 @@ function normalizeTerrainRegistry(
return terrains as Record<string, Terrain>; return terrains as Record<string, Terrain>;
} }
function scaleFoliageLayerDensity(
layer: FoliageLayer,
densityMultiplier: number
): FoliageLayer {
return {
...layer,
density: layer.density * densityMultiplier
};
}
function scaleFoliageLayerRegistryDensities(
layers: FoliageLayerRegistry,
densityMultiplier: number
): FoliageLayerRegistry {
if (densityMultiplier === 1) {
return layers;
}
return Object.fromEntries(
Object.entries(layers).map(([layerId, layer]) => [
layerId,
scaleFoliageLayerDensity(layer, densityMultiplier)
])
);
}
function createRenderViewFromCamera(camera: Camera): FoliageRenderView {
camera.updateMatrixWorld();
const cameraPosition = new Vector3();
camera.getWorldPosition(cameraPosition);
const projectionViewMatrix = new Matrix4().multiplyMatrices(
camera.projectionMatrix,
camera.matrixWorldInverse
);
return {
cameraPosition: {
x: cameraPosition.x,
y: cameraPosition.y,
z: cameraPosition.z
},
frustum: new Frustum().setFromProjectionMatrix(projectionViewMatrix)
};
}
function createCameraViewSignature(camera: Camera): string {
const values = [
...camera.matrixWorld.elements,
...camera.projectionMatrix.elements
];
return values
.map((value) => Math.round(value * VIEW_SIGNATURE_PRECISION))
.join("|");
}
function collectTemplateSourceMeshes(template: Group): FoliageTemplateSourceMesh[] { function collectTemplateSourceMeshes(template: Group): FoliageTemplateSourceMesh[] {
const sourceMeshes: FoliageTemplateSourceMesh[] = []; const sourceMeshes: FoliageTemplateSourceMesh[] = [];
@@ -135,9 +204,11 @@ function createInstancedMeshForSource(
); );
const color = new Color(); const color = new Color();
mesh.name = `Foliage:${batch.prototypeId}:${batch.lodLevel}`; mesh.name = `Foliage:${batch.prototypeId}:${batch.chunkId}:${batch.lodLevel}`;
mesh.userData.nonPickable = true; mesh.userData.nonPickable = true;
mesh.userData.shadowIgnored = !batch.castShadow;
mesh.userData.foliageBatchKey = batch.key; mesh.userData.foliageBatchKey = batch.key;
mesh.userData.foliageChunkId = batch.chunkId;
mesh.userData.foliagePrototypeId = batch.prototypeId; mesh.userData.foliagePrototypeId = batch.prototypeId;
mesh.userData.foliageLayerId = batch.layerId; mesh.userData.foliageLayerId = batch.layerId;
mesh.userData.foliageTerrainId = batch.terrainId; mesh.userData.foliageTerrainId = batch.terrainId;
@@ -174,6 +245,11 @@ export class FoliageInstancedRenderer {
private requestId = 0; private requestId = 0;
private activeBatchGroup: Group | null = null; private activeBatchGroup: Group | null = null;
private scatter: FoliageScatterResult | null = null;
private prototypeRegistry: FoliagePrototypeRegistry = {};
private quality: FoliageQualitySettings = resolveFoliageQualitySettings(null);
private currentView: FoliageRenderView | null = null;
private viewSignature: string | null = null;
private readonly onRebuilt?: () => void; private readonly onRebuilt?: () => void;
private readonly onDiagnostic?: (message: string) => void; private readonly onDiagnostic?: (message: string) => void;
@@ -185,19 +261,67 @@ export class FoliageInstancedRenderer {
} }
sync(input: FoliageInstancedRendererSyncInput) { sync(input: FoliageInstancedRendererSyncInput) {
const requestId = ++this.requestId;
const terrains = normalizeTerrainRegistry(input.terrains); const terrains = normalizeTerrainRegistry(input.terrains);
const quality = resolveFoliageQualitySettings(input.quality);
const prototypeRegistry = createFoliageScatterPrototypeRegistry({ const prototypeRegistry = createFoliageScatterPrototypeRegistry({
foliagePrototypes: input.foliagePrototypes, foliagePrototypes: input.foliagePrototypes,
bundledFoliagePrototypes: input.bundledFoliagePrototypes bundledFoliagePrototypes: input.bundledFoliagePrototypes
}); });
const scatter = generateFoliageScatterForScene({ const foliageLayers = scaleFoliageLayerRegistryDensities(
input.foliageLayers,
quality.densityMultiplier
);
this.quality = quality;
this.prototypeRegistry = prototypeRegistry;
if (!quality.enabled || quality.densityMultiplier <= 0) {
this.scatter = null;
this.clearActiveBatches();
this.onRebuilt?.();
return;
}
this.scatter = generateFoliageScatterForScene({
terrains, terrains,
foliageLayers: input.foliageLayers, foliageLayers,
foliagePrototypes: input.foliagePrototypes, foliagePrototypes: input.foliagePrototypes,
bundledFoliagePrototypes: input.bundledFoliagePrototypes bundledFoliagePrototypes: input.bundledFoliagePrototypes
}); });
const batches = createFoliageRenderBatches(scatter, prototypeRegistry); this.rebuildCurrentBatches();
}
updateView(camera: Camera) {
this.currentView = createRenderViewFromCamera(camera);
if (this.scatter === null) {
return;
}
const nextViewSignature = createCameraViewSignature(camera);
if (nextViewSignature === this.viewSignature) {
return;
}
this.viewSignature = nextViewSignature;
this.rebuildCurrentBatches();
}
private rebuildCurrentBatches() {
const requestId = ++this.requestId;
const scatter = this.scatter;
if (scatter === null) {
this.clearActiveBatches();
this.onRebuilt?.();
return;
}
const batches = createFoliageRenderBatches(scatter, this.prototypeRegistry, {
view: this.currentView,
quality: this.quality
});
this.clearActiveBatches(); this.clearActiveBatches();
@@ -211,6 +335,9 @@ export class FoliageInstancedRenderer {
dispose() { dispose() {
this.requestId += 1; this.requestId += 1;
this.scatter = null;
this.prototypeRegistry = {};
this.viewSignature = null;
this.clearActiveBatches(); this.clearActiveBatches();
} }