Feature: Implement wind simulation for foliage rendering across renderer, runtime, and viewport hosts

This commit is contained in:
2026-05-21 06:08:12 +02:00
parent d0a33aa97d
commit fe1bb3b9ce
4 changed files with 69 additions and 3 deletions

View File

@@ -473,6 +473,8 @@ export class FoliageInstancedRenderer {
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 windTime = 0;
private windUniforms: FoliageWindUniformSet[] = [];
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 renderResourceSignature: string | null = null;
@@ -498,6 +500,18 @@ export class FoliageInstancedRenderer {
this.group.userData.nonPickable = true; this.group.userData.nonPickable = true;
} }
updateWind(deltaSeconds: number) {
if (!this.shouldApplyWindShader()) {
return;
}
if (Number.isFinite(deltaSeconds) && deltaSeconds > 0) {
this.windTime += deltaSeconds;
}
this.writeWindUniforms();
}
sync(input: FoliageInstancedRendererSyncInput) { sync(input: FoliageInstancedRendererSyncInput) {
const terrains = normalizeTerrainRegistry(input.terrains); const terrains = normalizeTerrainRegistry(input.terrains);
const quality = resolveFoliageQualitySettings(input.quality); const quality = resolveFoliageQualitySettings(input.quality);
@@ -536,6 +550,7 @@ export class FoliageInstancedRenderer {
renderResourceSignature === this.renderResourceSignature && renderResourceSignature === this.renderResourceSignature &&
this.scatter !== null this.scatter !== null
) { ) {
this.writeWindUniforms();
this.applyCurrentViewToRenderResources(); this.applyCurrentViewToRenderResources();
return; return;
} }
@@ -668,6 +683,7 @@ export class FoliageInstancedRenderer {
this.currentView = null; this.currentView = null;
this.viewSignature = null; this.viewSignature = null;
this.renderResourceSignature = null; this.renderResourceSignature = null;
this.windUniforms = [];
this.sourceMeshPromisesByBundledPath.clear(); this.sourceMeshPromisesByBundledPath.clear();
this.clearActiveBatches(); this.clearActiveBatches();
} }
@@ -677,6 +693,7 @@ export class FoliageInstancedRenderer {
this.activeBatchKeyByChunkKey.clear(); this.activeBatchKeyByChunkKey.clear();
this.activeLodLevelByChunkKey.clear(); this.activeLodLevelByChunkKey.clear();
this.renderChunks = []; this.renderChunks = [];
this.windUniforms = [];
if (this.activeBatchGroup === null) { if (this.activeBatchGroup === null) {
return; return;
@@ -696,6 +713,41 @@ export class FoliageInstancedRenderer {
console.warn(message); console.warn(message);
} }
private shouldApplyWindShader(): boolean {
return (
this.quality.enabled &&
this.quality.densityMultiplier > 0 &&
this.quality.windEnabled
);
}
private registerWindUniforms = (uniforms: FoliageWindUniformSet) => {
writeFoliageWindUniformValues(uniforms, this.quality, this.windTime);
this.windUniforms.push(uniforms);
};
private writeWindUniforms() {
if (!this.shouldApplyWindShader()) {
return;
}
for (const uniforms of this.windUniforms) {
writeFoliageWindUniformValues(uniforms, this.quality, this.windTime);
}
}
private getWindMaterialOptions(): FoliageWindMaterialOptions | undefined {
if (!this.shouldApplyWindShader()) {
return undefined;
}
return {
getSettings: () => this.quality,
getTime: () => this.windTime,
registerUniforms: this.registerWindUniforms
};
}
private loadTemplateSourceMeshes( private loadTemplateSourceMeshes(
bundledPath: string bundledPath: string
): Promise<FoliageTemplateSourceMesh[]> { ): Promise<FoliageTemplateSourceMesh[]> {
@@ -761,8 +813,12 @@ export class FoliageInstancedRenderer {
batchGroup.userData.foliageLayerId = batch.layerId; batchGroup.userData.foliageLayerId = batch.layerId;
batchGroup.userData.foliageTerrainId = batch.terrainId; batchGroup.userData.foliageTerrainId = batch.terrainId;
const windOptions = this.getWindMaterialOptions();
for (const sourceMesh of sourceMeshes) { for (const sourceMesh of sourceMeshes) {
batchGroup.add(createInstancedMeshForSource(batch, sourceMesh)); batchGroup.add(
createInstancedMeshForSource(batch, sourceMesh, windOptions)
);
} }
applyRendererRenderCategoryFromMaterial(batchGroup); applyRendererRenderCategoryFromMaterial(batchGroup);

View File

@@ -5704,6 +5704,7 @@ export class RuntimeHost {
this.updateTerrainLodVisibility(); this.updateTerrainLodVisibility();
this.foliageRenderer.updateView(this.camera); this.foliageRenderer.updateView(this.camera);
this.foliageRenderer.updateWind(simulationDt);
this.updateUnderwaterSceneFog(); this.updateUnderwaterSceneFog();
this.syncCelestialShadowState(); this.syncCelestialShadowState();

View File

@@ -14188,6 +14188,7 @@ export class ViewportHost {
this.updateTransformGizmoPose(); this.updateTransformGizmoPose();
this.updateTerrainLodVisibility(); this.updateTerrainLodVisibility();
this.foliageRenderer.updateView(this.getActiveCamera()); this.foliageRenderer.updateView(this.getActiveCamera());
this.foliageRenderer.updateWind(dt);
this.volumeTime += dt; this.volumeTime += dt;
for (const uniform of this.volumeAnimatedUniforms) { for (const uniform of this.volumeAnimatedUniforms) {

View File

@@ -341,7 +341,11 @@ describe("foliage render batch helpers", () => {
enabled: false, enabled: false,
densityMultiplier: 1, densityMultiplier: 1,
maxDistanceMultiplier: 1, maxDistanceMultiplier: 1,
shadows: "near" shadows: "near",
windEnabled: true,
windStrength: 1,
windSpeed: 1,
windDirectionDegrees: 35
} }
} }
); );
@@ -373,7 +377,11 @@ describe("foliage render batch helpers", () => {
enabled: true, enabled: true,
densityMultiplier: 1, densityMultiplier: 1,
maxDistanceMultiplier: 1, maxDistanceMultiplier: 1,
shadows: "near" shadows: "near",
windEnabled: true,
windStrength: 1,
windSpeed: 1,
windDirectionDegrees: 35
} }
} }
); );