From c7abf109302407c18c517414a918ae9dd4daa074 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Thu, 21 May 2026 06:06:32 +0200 Subject: [PATCH] Feat: Add foliage wind settings for advanced rendering Implements controls and logic for managing foliage wind parameters (enablement, strength, speed, and direction) in the advanced rendering settings. Updates serialization tests to handle these new fields and migration from older document versions. --- src/app/App.tsx | 241 ++++++++++++++++++ .../serialization/scene-document-json.test.ts | 29 ++- 2 files changed, 269 insertions(+), 1 deletion(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index 5f2f436c..aebaf999 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -208,8 +208,12 @@ import { MAX_ADVANCED_RENDERING_LENS_FLARE_GHOST_COUNT, MAX_FOLIAGE_QUALITY_DENSITY_MULTIPLIER, MAX_FOLIAGE_QUALITY_MAX_DISTANCE_MULTIPLIER, + MAX_FOLIAGE_QUALITY_WIND_SPEED, + MAX_FOLIAGE_QUALITY_WIND_STRENGTH, MIN_FOLIAGE_QUALITY_DENSITY_MULTIPLIER, MIN_FOLIAGE_QUALITY_MAX_DISTANCE_MULTIPLIER, + MIN_FOLIAGE_QUALITY_WIND_SPEED, + MIN_FOLIAGE_QUALITY_WIND_STRENGTH, areWorldSettingsEqual, changeWorldBackgroundMode, cloneWorldSettings, @@ -1267,6 +1271,12 @@ function clampNumber(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value)); } +function normalizeDegrees(value: number): number { + const normalized = value % 360; + + return normalized < 0 ? normalized + 360 : normalized; +} + function assertAdvancedRenderingDistanceFogRange( distanceFog: AdvancedRenderingDistanceFogSettings ) { @@ -3820,6 +3830,26 @@ export function App({ editorState.document.world.advancedRendering.foliage.maxDistanceMultiplier ) ); + const [ + advancedRenderingFoliageWindStrengthDraft, + setAdvancedRenderingFoliageWindStrengthDraft + ] = useState( + String(editorState.document.world.advancedRendering.foliage.windStrength) + ); + const [ + advancedRenderingFoliageWindSpeedDraft, + setAdvancedRenderingFoliageWindSpeedDraft + ] = useState( + String(editorState.document.world.advancedRendering.foliage.windSpeed) + ); + const [ + advancedRenderingFoliageWindDirectionDraft, + setAdvancedRenderingFoliageWindDirectionDraft + ] = useState( + String( + editorState.document.world.advancedRendering.foliage.windDirectionDegrees + ) + ); const [ advancedRenderingAmbientOcclusionIntensityDraft, setAdvancedRenderingAmbientOcclusionIntensityDraft @@ -5223,6 +5253,15 @@ export function App({ setAdvancedRenderingFoliageMaxDistanceMultiplierDraft( String(advancedRendering.foliage.maxDistanceMultiplier) ); + setAdvancedRenderingFoliageWindStrengthDraft( + String(advancedRendering.foliage.windStrength) + ); + setAdvancedRenderingFoliageWindSpeedDraft( + String(advancedRendering.foliage.windSpeed) + ); + setAdvancedRenderingFoliageWindDirectionDraft( + String(advancedRendering.foliage.windDirectionDegrees) + ); setAdvancedRenderingAmbientOcclusionIntensityDraft( String(advancedRendering.ambientOcclusion.intensity) ); @@ -13890,6 +13929,89 @@ export function App({ ); }; + const applyAdvancedRenderingFoliageWindEnabled = (enabled: boolean) => { + applyAdvancedRenderingSettings( + "Set foliage wind", + enabled ? "Foliage wind enabled." : "Foliage wind disabled.", + (advancedRendering) => { + advancedRendering.foliage.windEnabled = enabled; + } + ); + }; + + const applyAdvancedRenderingFoliageWindStrength = () => { + try { + const windStrength = clampNumber( + readFiniteNumberDraft( + advancedRenderingFoliageWindStrengthDraft, + "Foliage wind strength" + ), + MIN_FOLIAGE_QUALITY_WIND_STRENGTH, + MAX_FOLIAGE_QUALITY_WIND_STRENGTH + ); + + setAdvancedRenderingFoliageWindStrengthDraft(String(windStrength)); + applyAdvancedRenderingSettings( + "Set foliage wind strength", + "Updated the foliage wind strength.", + (advancedRendering) => { + advancedRendering.foliage.windStrength = windStrength; + } + ); + } catch (error) { + setStatusMessage(getErrorMessage(error)); + } + }; + + const applyAdvancedRenderingFoliageWindSpeed = () => { + try { + const windSpeed = clampNumber( + readFiniteNumberDraft( + advancedRenderingFoliageWindSpeedDraft, + "Foliage wind speed" + ), + MIN_FOLIAGE_QUALITY_WIND_SPEED, + MAX_FOLIAGE_QUALITY_WIND_SPEED + ); + + setAdvancedRenderingFoliageWindSpeedDraft(String(windSpeed)); + applyAdvancedRenderingSettings( + "Set foliage wind speed", + "Updated the foliage wind speed.", + (advancedRendering) => { + advancedRendering.foliage.windSpeed = windSpeed; + } + ); + } catch (error) { + setStatusMessage(getErrorMessage(error)); + } + }; + + const applyAdvancedRenderingFoliageWindDirection = () => { + try { + const windDirectionDegrees = normalizeDegrees( + readFiniteNumberDraft( + advancedRenderingFoliageWindDirectionDraft, + "Foliage wind direction" + ) + ); + + setAdvancedRenderingFoliageWindDirectionDraft( + String(windDirectionDegrees) + ); + applyAdvancedRenderingSettings( + "Set foliage wind direction", + "Updated the foliage wind direction.", + (advancedRendering) => { + advancedRendering.foliage.windDirectionDegrees = + windDirectionDegrees; + } + ); + } catch (error) { + setStatusMessage(getErrorMessage(error)); + } + }; + const applyAdvancedRenderingShadowsEnabled = (enabled: boolean) => { applyAdvancedRenderingSettings( "Set advanced rendering shadows", @@ -20273,6 +20395,125 @@ export function App({ ))} + +
+ + + +
{!advancedRendering.enabled ? null : ( diff --git a/tests/serialization/scene-document-json.test.ts b/tests/serialization/scene-document-json.test.ts index 7757ba57..2711b8e6 100644 --- a/tests/serialization/scene-document-json.test.ts +++ b/tests/serialization/scene-document-json.test.ts @@ -1172,7 +1172,7 @@ describe("scene document JSON", () => { expect( migratedDocument.splineCorridorJunctions[junction.id]?.shapeMode ).toBe("straight"); - expect(LENS_FLARE_SCENE_DOCUMENT_VERSION).toBe(SCENE_DOCUMENT_VERSION); + expect(FOLIAGE_WIND_SCENE_DOCUMENT_VERSION).toBe(SCENE_DOCUMENT_VERSION); }); it("round-trips spline corridor junctions", () => { @@ -1865,6 +1865,33 @@ describe("scene document JSON", () => { ); }); + it("migrates v111 scene documents without foliage wind settings to defaults", () => { + const emptyScene = createEmptySceneDocument({ + name: "Legacy Foliage Wind Scene" + }); + const legacyDocument = JSON.parse( + serializeSceneDocument(emptyScene) + ) as Record; + const legacyFoliage = + legacyDocument.world.advancedRendering.foliage as Record< + string, + unknown + >; + + legacyDocument.version = LENS_FLARE_SCENE_DOCUMENT_VERSION; + delete legacyFoliage.windEnabled; + delete legacyFoliage.windStrength; + delete legacyFoliage.windSpeed; + delete legacyFoliage.windDirectionDegrees; + + const migratedDocument = migrateSceneDocument(legacyDocument); + + expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION); + expect(migratedDocument.world.advancedRendering.foliage).toEqual( + emptyScene.world.advancedRendering.foliage + ); + }); + it("defaults missing water reflection mode and clamps legacy foam limits during migration", () => { const migratedDocument = migrateSceneDocument({ version: SCENE_DOCUMENT_VERSION,