Refactor foliage rendering pipeline to use resource signatures and view visibility
This commit is contained in:
@@ -20,8 +20,12 @@ import { applyRendererRenderCategoryFromMaterial } from "../rendering/render-lay
|
|||||||
import { loadBundledFoliageModelTemplate } from "./bundled-foliage-model-loader";
|
import { loadBundledFoliageModelTemplate } from "./bundled-foliage-model-loader";
|
||||||
import {
|
import {
|
||||||
createFoliageInstanceMatrix,
|
createFoliageInstanceMatrix,
|
||||||
createFoliageRenderBatches,
|
createFoliageRenderBatchKey,
|
||||||
|
createFoliageRenderResourcePlan,
|
||||||
|
resolveFoliageRenderChunkLod,
|
||||||
type FoliageRenderBatch,
|
type FoliageRenderBatch,
|
||||||
|
type FoliageRenderChunk,
|
||||||
|
type FoliageRenderResourcePlan,
|
||||||
type FoliageRenderView
|
type FoliageRenderView
|
||||||
} from "./foliage-render-batches";
|
} from "./foliage-render-batches";
|
||||||
import type {
|
import type {
|
||||||
@@ -57,6 +61,32 @@ interface FoliageTemplateSourceMesh {
|
|||||||
|
|
||||||
const VIEW_SIGNATURE_PRECISION = 100;
|
const VIEW_SIGNATURE_PRECISION = 100;
|
||||||
|
|
||||||
|
function stableStringify(value: unknown): string {
|
||||||
|
if (
|
||||||
|
value === null ||
|
||||||
|
typeof value === "number" ||
|
||||||
|
typeof value === "boolean" ||
|
||||||
|
typeof value === "string"
|
||||||
|
) {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "object") {
|
||||||
|
const record = value as Record<string, unknown>;
|
||||||
|
|
||||||
|
return `{${Object.keys(record)
|
||||||
|
.sort()
|
||||||
|
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
||||||
|
.join(",")}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(String(value));
|
||||||
|
}
|
||||||
|
|
||||||
function cloneMaterial(material: Material): Material {
|
function cloneMaterial(material: Material): Material {
|
||||||
return material.clone();
|
return material.clone();
|
||||||
}
|
}
|
||||||
@@ -194,6 +224,24 @@ function collectTemplateSourceMeshes(template: Group): FoliageTemplateSourceMesh
|
|||||||
return sourceMeshes;
|
return sourceMeshes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createFoliageRenderResourceSignature(options: {
|
||||||
|
terrains: Record<string, Terrain>;
|
||||||
|
foliageLayers: FoliageLayerRegistry;
|
||||||
|
prototypeRegistry: FoliagePrototypeRegistry;
|
||||||
|
quality: FoliageQualitySettings;
|
||||||
|
}): string {
|
||||||
|
return stableStringify({
|
||||||
|
terrains: options.terrains,
|
||||||
|
foliageLayers: options.foliageLayers,
|
||||||
|
prototypeRegistry: options.prototypeRegistry,
|
||||||
|
quality: {
|
||||||
|
enabled: options.quality.enabled,
|
||||||
|
densityMultiplier: options.quality.densityMultiplier,
|
||||||
|
shadows: options.quality.shadows
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function createInstancedMeshForSource(
|
function createInstancedMeshForSource(
|
||||||
batch: FoliageRenderBatch,
|
batch: FoliageRenderBatch,
|
||||||
sourceMesh: FoliageTemplateSourceMesh
|
sourceMesh: FoliageTemplateSourceMesh
|
||||||
@@ -246,11 +294,18 @@ export class FoliageInstancedRenderer {
|
|||||||
|
|
||||||
private requestId = 0;
|
private requestId = 0;
|
||||||
private activeBatchGroup: Group | null = null;
|
private activeBatchGroup: Group | null = null;
|
||||||
|
private batchGroupsByKey = new Map<string, Group>();
|
||||||
|
private renderChunks: FoliageRenderChunk[] = [];
|
||||||
private scatter: FoliageScatterResult | null = null;
|
private scatter: FoliageScatterResult | null = null;
|
||||||
private prototypeRegistry: FoliagePrototypeRegistry = {};
|
private prototypeRegistry: FoliagePrototypeRegistry = {};
|
||||||
private quality: FoliageQualitySettings = resolveFoliageQualitySettings(null);
|
private quality: FoliageQualitySettings = resolveFoliageQualitySettings(null);
|
||||||
private currentView: FoliageRenderView | null = null;
|
private currentView: FoliageRenderView | null = null;
|
||||||
private viewSignature: string | null = null;
|
private viewSignature: string | null = null;
|
||||||
|
private renderResourceSignature: string | null = null;
|
||||||
|
private readonly sourceMeshPromisesByBundledPath = new Map<
|
||||||
|
string,
|
||||||
|
Promise<FoliageTemplateSourceMesh[]>
|
||||||
|
>();
|
||||||
private readonly onRebuilt?: () => void;
|
private readonly onRebuilt?: () => void;
|
||||||
private readonly onDiagnostic?: (message: string) => void;
|
private readonly onDiagnostic?: (message: string) => void;
|
||||||
|
|
||||||
@@ -272,24 +327,40 @@ export class FoliageInstancedRenderer {
|
|||||||
input.foliageLayers,
|
input.foliageLayers,
|
||||||
quality.densityMultiplier
|
quality.densityMultiplier
|
||||||
);
|
);
|
||||||
|
const renderResourceSignature = createFoliageRenderResourceSignature({
|
||||||
|
terrains,
|
||||||
|
foliageLayers,
|
||||||
|
prototypeRegistry,
|
||||||
|
quality
|
||||||
|
});
|
||||||
|
|
||||||
this.quality = quality;
|
this.quality = quality;
|
||||||
this.prototypeRegistry = prototypeRegistry;
|
this.prototypeRegistry = prototypeRegistry;
|
||||||
|
|
||||||
if (!quality.enabled || quality.densityMultiplier <= 0) {
|
if (!quality.enabled || quality.densityMultiplier <= 0) {
|
||||||
this.scatter = null;
|
this.scatter = null;
|
||||||
|
this.renderResourceSignature = null;
|
||||||
this.clearActiveBatches();
|
this.clearActiveBatches();
|
||||||
this.onRebuilt?.();
|
this.onRebuilt?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
renderResourceSignature === this.renderResourceSignature &&
|
||||||
|
this.scatter !== null
|
||||||
|
) {
|
||||||
|
this.applyCurrentViewToRenderResources();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderResourceSignature = renderResourceSignature;
|
||||||
this.scatter = generateFoliageScatterForScene({
|
this.scatter = generateFoliageScatterForScene({
|
||||||
terrains,
|
terrains,
|
||||||
foliageLayers,
|
foliageLayers,
|
||||||
foliagePrototypes: input.foliagePrototypes,
|
foliagePrototypes: input.foliagePrototypes,
|
||||||
bundledFoliagePrototypes: input.bundledFoliagePrototypes
|
bundledFoliagePrototypes: input.bundledFoliagePrototypes
|
||||||
});
|
});
|
||||||
this.rebuildCurrentBatches();
|
this.rebuildRenderResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateView(camera: Camera) {
|
updateView(camera: Camera) {
|
||||||
@@ -306,10 +377,10 @@ export class FoliageInstancedRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.viewSignature = nextViewSignature;
|
this.viewSignature = nextViewSignature;
|
||||||
this.rebuildCurrentBatches();
|
this.applyCurrentViewToRenderResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
private rebuildCurrentBatches() {
|
private rebuildRenderResources() {
|
||||||
const requestId = ++this.requestId;
|
const requestId = ++this.requestId;
|
||||||
const scatter = this.scatter;
|
const scatter = this.scatter;
|
||||||
|
|
||||||
@@ -319,29 +390,73 @@ export class FoliageInstancedRenderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const batches = createFoliageRenderBatches(scatter, this.prototypeRegistry, {
|
const renderResourcePlan = createFoliageRenderResourcePlan(
|
||||||
view: this.currentView,
|
scatter,
|
||||||
quality: this.quality
|
this.prototypeRegistry,
|
||||||
});
|
{
|
||||||
|
quality: this.quality
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (batches.length === 0) {
|
if (renderResourcePlan.batches.length === 0) {
|
||||||
this.clearActiveBatches();
|
this.clearActiveBatches();
|
||||||
this.onRebuilt?.();
|
this.onRebuilt?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void this.rebuildBatchesAsync(requestId, batches);
|
void this.rebuildBatchesAsync(requestId, renderResourcePlan);
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyCurrentViewToRenderResources() {
|
||||||
|
if (this.activeBatchGroup === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleBatchKeys = new Set<string>();
|
||||||
|
|
||||||
|
for (const chunk of this.renderChunks) {
|
||||||
|
const renderLod = resolveFoliageRenderChunkLod({
|
||||||
|
chunk,
|
||||||
|
view: this.currentView,
|
||||||
|
quality: this.quality
|
||||||
|
});
|
||||||
|
|
||||||
|
if (renderLod === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleBatchKeys.add(
|
||||||
|
createFoliageRenderBatchKey({
|
||||||
|
chunkId: chunk.chunkId,
|
||||||
|
terrainId: chunk.terrainId,
|
||||||
|
layerId: chunk.layerId,
|
||||||
|
prototypeId: chunk.prototypeId,
|
||||||
|
lodLevel: renderLod.level,
|
||||||
|
bundledPath: renderLod.bundledPath
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [batchKey, batchGroup] of this.batchGroupsByKey) {
|
||||||
|
batchGroup.visible = visibleBatchKeys.has(batchKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this.requestId += 1;
|
this.requestId += 1;
|
||||||
this.scatter = null;
|
this.scatter = null;
|
||||||
this.prototypeRegistry = {};
|
this.prototypeRegistry = {};
|
||||||
|
this.currentView = null;
|
||||||
this.viewSignature = null;
|
this.viewSignature = null;
|
||||||
|
this.renderResourceSignature = null;
|
||||||
|
this.sourceMeshPromisesByBundledPath.clear();
|
||||||
this.clearActiveBatches();
|
this.clearActiveBatches();
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearActiveBatches() {
|
private clearActiveBatches() {
|
||||||
|
this.batchGroupsByKey.clear();
|
||||||
|
this.renderChunks = [];
|
||||||
|
|
||||||
if (this.activeBatchGroup === null) {
|
if (this.activeBatchGroup === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -360,19 +475,41 @@ export class FoliageInstancedRenderer {
|
|||||||
console.warn(message);
|
console.warn(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private loadTemplateSourceMeshes(
|
||||||
|
bundledPath: string
|
||||||
|
): Promise<FoliageTemplateSourceMesh[]> {
|
||||||
|
const cachedSourceMeshPromise =
|
||||||
|
this.sourceMeshPromisesByBundledPath.get(bundledPath);
|
||||||
|
|
||||||
|
if (cachedSourceMeshPromise !== undefined) {
|
||||||
|
return cachedSourceMeshPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceMeshPromise = loadBundledFoliageModelTemplate(bundledPath)
|
||||||
|
.then((template) => collectTemplateSourceMeshes(template))
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
this.sourceMeshPromisesByBundledPath.delete(bundledPath);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sourceMeshPromisesByBundledPath.set(bundledPath, sourceMeshPromise);
|
||||||
|
return sourceMeshPromise;
|
||||||
|
}
|
||||||
|
|
||||||
private async rebuildBatchesAsync(
|
private async rebuildBatchesAsync(
|
||||||
requestId: number,
|
requestId: number,
|
||||||
batches: readonly FoliageRenderBatch[]
|
renderResourcePlan: FoliageRenderResourcePlan
|
||||||
) {
|
) {
|
||||||
const nextBatchGroup = new Group();
|
const nextBatchGroup = new Group();
|
||||||
|
const nextBatchGroupsByKey = new Map<string, Group>();
|
||||||
nextBatchGroup.name = "foliageInstancedBatches";
|
nextBatchGroup.name = "foliageInstancedBatches";
|
||||||
nextBatchGroup.userData.nonPickable = true;
|
nextBatchGroup.userData.nonPickable = true;
|
||||||
|
|
||||||
for (const batch of batches) {
|
for (const batch of renderResourcePlan.batches) {
|
||||||
let template: Group;
|
let sourceMeshes: FoliageTemplateSourceMesh[];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
template = await loadBundledFoliageModelTemplate(batch.bundledPath);
|
sourceMeshes = await this.loadTemplateSourceMeshes(batch.bundledPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
@@ -387,8 +524,6 @@ export class FoliageInstancedRenderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceMeshes = collectTemplateSourceMeshes(template);
|
|
||||||
|
|
||||||
if (sourceMeshes.length === 0) {
|
if (sourceMeshes.length === 0) {
|
||||||
this.emitDiagnostic(
|
this.emitDiagnostic(
|
||||||
`Bundled foliage model ${batch.bundledPath} contains no renderable meshes.`
|
`Bundled foliage model ${batch.bundledPath} contains no renderable meshes.`
|
||||||
@@ -398,6 +533,7 @@ export class FoliageInstancedRenderer {
|
|||||||
|
|
||||||
const batchGroup = new Group();
|
const batchGroup = new Group();
|
||||||
batchGroup.name = `FoliageBatch:${batch.prototypeId}`;
|
batchGroup.name = `FoliageBatch:${batch.prototypeId}`;
|
||||||
|
batchGroup.visible = false;
|
||||||
batchGroup.userData.nonPickable = true;
|
batchGroup.userData.nonPickable = true;
|
||||||
batchGroup.userData.foliageBatchKey = batch.key;
|
batchGroup.userData.foliageBatchKey = batch.key;
|
||||||
batchGroup.userData.foliagePrototypeId = batch.prototypeId;
|
batchGroup.userData.foliagePrototypeId = batch.prototypeId;
|
||||||
@@ -409,6 +545,7 @@ export class FoliageInstancedRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyRendererRenderCategoryFromMaterial(batchGroup);
|
applyRendererRenderCategoryFromMaterial(batchGroup);
|
||||||
|
nextBatchGroupsByKey.set(batch.key, batchGroup);
|
||||||
nextBatchGroup.add(batchGroup);
|
nextBatchGroup.add(batchGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +563,10 @@ export class FoliageInstancedRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.activeBatchGroup = nextBatchGroup;
|
this.activeBatchGroup = nextBatchGroup;
|
||||||
|
this.batchGroupsByKey = nextBatchGroupsByKey;
|
||||||
|
this.renderChunks = [...renderResourcePlan.chunks];
|
||||||
this.group.add(nextBatchGroup);
|
this.group.add(nextBatchGroup);
|
||||||
|
this.applyCurrentViewToRenderResources();
|
||||||
this.onRebuilt?.();
|
this.onRebuilt?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user