fix(render): foliage wind keys on the DAT-classified WeatherKind, not the day-group index (Campaign VM VM6)
FoliageWindByDayGroup / FoliageWindDayGroupPoint(int ActiveDayGroup, ...) becomes FoliageWindByWeather / FoliageWindWeatherPoint(string WeatherKind, ...) in AtmospherePolicyDeclaration (Plugin.Abstractions is BCL-only, so the key is the exact member name of AcDream.Core.World.WeatherKind rather than the enum itself). The raw activeDayGroup index carries no weather meaning by itself; WeatherState.cs already classifies each day group's authored DAT name into one of five real weather kinds, and that fact was already threaded through AtmosphericFrameInputs.Weather / uAtmosphereWeather.x — this reuses it instead of guessing an index-to-category mapping. Built-in table (BuiltInAtmosphericRenderPack.AtmospherePolicy()): Clear 0.25/0.15, Overcast 0.60/0.35, Rain 0.85/0.60, Snow 0.35/0.20, Storm 1.00/0.75 — all five WeatherKind members declared, the invented "Cloudy" row dropped. RenderPackAtmospherePolicyEvaluation.FoliageWind now takes a WeatherKind and matches by weather.ToString() (ordinal) against each declared point's name; a kind absent from the table falls back to the declared Clear row, then to (0,0) if Clear itself is undeclared. The delta-seconds EMA interpolation (EaseTowardTarget) is unchanged. AtmosphericPostProcessGraph.ResolveFoliageWind and its two callers (RenderPostProcess via inputs.Weather; RenderDirectionalShadows via foundation.Atmosphere.Kind) now pass WeatherKind instead of the day-group int. RenderPackValidation.ValidateAtmosphere (runs for every pack declaring an AtmospherePolicy, not gated to Tier2/shadow packs) now rejects an unknown or non-exact-case weather-kind name and a repeated kind, mirroring the existing ActiveDayGroupMultiplier duplicate-key check. Tests: RenderPackAtmospherePolicyEvaluationTests rewritten for the kind-keyed API (all five kinds resolve to their declared row, an unlisted kind falls back to Clear, ordinal exact-case matching, null-table handling); RenderPackSpirvValidatorTests gains four descriptor-validation cases (unknown name, wrong case, duplicate kind, the five-kind table accepted); AtmosphericPostProcessGraphTests' three foliage-wind cases now select WeatherKind.Storm via `with` instead of an assumed day-group index. Spot-check (per the coordinator's ask, not changed here): yes — ActiveDayGroupMultiplier / EvaluateDayGroupPolicy (pre-existing, Campaign AR/VM3-era — BuiltInAtmosphericRenderPack.AtmospherePolicy()'s three rows `new ActiveDayGroupMultiplier(0, 1.0), (1, 0.35), (2, 0.20)`) key the sun-ray/shadow/volumetric day-group strength multiplier by the same raw activeDayGroup index with an undocumented assumed meaning (0=brightest ... 2=dimmest), the identical class of issue this commit fixes for foliage wind. Left unchanged per instruction; flagging for the coordinator to file. Full solution Debug and Release builds green. App hermetic filter 6024/6026 — the same 2 pre-existing failures as VM6a/VM6b. Both were re-run in isolation per the verification ask: both still fail alone (not a load-flake in this environment) — confirmed via git stash earlier this session that both already fail on the unmodified pre-VM6 baseline, so they are pre-existing and unrelated to this change. Core.Tests hermetic 4697/4697. RenderPackValidator.Tests 30/30. No shader/spv changes in this commit (pure C#/docs fix). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
39e8408c7d
commit
6cc5e183b9
10 changed files with 325 additions and 106 deletions
|
|
@ -37,7 +37,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Intentional architecture (IA) — 22 active rows (IA-25 filed 2026-08-22 for Campaign VM VM6's opt-in weather-driven foliage wind — a render-only vertex displacement with no authored retail wind direction to read; IA-24 filed 2026-08-22 for Campaign AR's opt-in real-time sun/moon directional shadows; IA-23 filed 2026-08-17 at the night-round review fix round (F8) — the House tab's not-yet-expired purchase-restriction line renders .NET's culture-default `DateTime.ToString()` where retail renders the C runtime's `strftime("%c", localtime(...))`, a different formatting engine producing a different-shaped (but equivalent-intent) date string; IA-22 filed 2026-08-13 — the #391 user-directed modern-only curated resolution list + desktop-mode default, replacing retail's full adapter enumeration + authored 800x600 default)
|
## 1. Intentional architecture (IA) — 22 active rows (IA-25 filed 2026-08-22 for Campaign VM VM6's opt-in weather-driven foliage wind — a render-only vertex displacement keyed by the DAT-classified `WeatherKind`, not the raw day-group index, with no authored retail wind direction to read; IA-24 filed 2026-08-22 for Campaign AR's opt-in real-time sun/moon directional shadows; IA-23 filed 2026-08-17 at the night-round review fix round (F8) — the House tab's not-yet-expired purchase-restriction line renders .NET's culture-default `DateTime.ToString()` where retail renders the C runtime's `strftime("%c", localtime(...))`, a different formatting engine producing a different-shaped (but equivalent-intent) date string; IA-22 filed 2026-08-13 — the #391 user-directed modern-only curated resolution list + desktop-mode default, replacing retail's full adapter enumeration + authored 800x600 default)
|
||||||
|
|
||||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
|
|
@ -62,7 +62,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
| IA-22 | **Filed 2026-08-13 (#391, user-directed: "we should only support modern resolutions. Not any old format").** The Config Resolution dropdown offers a CURATED list — the monitor's real mode enumeration filtered to modern widescreen families (16:9/16:10/21:9/32:9, ≥1280 wide, fitting the desktop; `DisplayModeCatalog.Curate`) — and its Defaults value is the desktop's own mode. Retail offered the adapter's complete enumeration including 4:3 legacy modes and authored `800x600` as the row default (`gmConfigUI::InitOptions SetDefaultValue(0x03200258)`; `gmClient::Init @0x004047af` `Device::ForceDisplayResolution(1, 0x320, 0x258)`). | `src/AcDream.App/Rendering/DisplayModeCatalog.cs`; `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (Resolution row); fixture fallback `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs` (`AvailableResolutions`, 800x600 removed) | Explicit product direction. **Amended 2026-08-16 (#407, Campaign CC gate round 1):** the dropdown now offers `DisplayModeCatalog.WindowedResolutions` — the curated hardware modes UNIONed with the static modern-ladder sizes that fit the desktop — because a WINDOWED pick is a plain Size write needing no video mode, and remote/RDP virtual displays advertise almost no modes (the live RDP display exposed only 1920x1080 + the 2056x1290 desktop, starving the dropdown). The original "an offered mode is supported by construction" invariant now holds for the FULLSCREEN half only: the fullscreen apply still validates against the hardware `Resolutions` list plus `GlfwDisplayModeSwitcher`'s monitor-mode-list hard guard, so a fullscreen pick of a windowed-only entry refuses safely (log-and-stay, #388; the #392 apply-result seam is that family's open follow-up) — "Graphics mode not supported" crashes remain unreachable from the dropdown. | A user wanting a genuine legacy 4:3 mode cannot pick it; retail-parity comparisons of the Config tab's list/default will show the deviation. | decomp sites in the Divergence column; ISSUES #391 |
|
| IA-22 | **Filed 2026-08-13 (#391, user-directed: "we should only support modern resolutions. Not any old format").** The Config Resolution dropdown offers a CURATED list — the monitor's real mode enumeration filtered to modern widescreen families (16:9/16:10/21:9/32:9, ≥1280 wide, fitting the desktop; `DisplayModeCatalog.Curate`) — and its Defaults value is the desktop's own mode. Retail offered the adapter's complete enumeration including 4:3 legacy modes and authored `800x600` as the row default (`gmConfigUI::InitOptions SetDefaultValue(0x03200258)`; `gmClient::Init @0x004047af` `Device::ForceDisplayResolution(1, 0x320, 0x258)`). | `src/AcDream.App/Rendering/DisplayModeCatalog.cs`; `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (Resolution row); fixture fallback `src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs` (`AvailableResolutions`, 800x600 removed) | Explicit product direction. **Amended 2026-08-16 (#407, Campaign CC gate round 1):** the dropdown now offers `DisplayModeCatalog.WindowedResolutions` — the curated hardware modes UNIONed with the static modern-ladder sizes that fit the desktop — because a WINDOWED pick is a plain Size write needing no video mode, and remote/RDP virtual displays advertise almost no modes (the live RDP display exposed only 1920x1080 + the 2056x1290 desktop, starving the dropdown). The original "an offered mode is supported by construction" invariant now holds for the FULLSCREEN half only: the fullscreen apply still validates against the hardware `Resolutions` list plus `GlfwDisplayModeSwitcher`'s monitor-mode-list hard guard, so a fullscreen pick of a windowed-only entry refuses safely (log-and-stay, #388; the #392 apply-result seam is that family's open follow-up) — "Graphics mode not supported" crashes remain unreachable from the dropdown. | A user wanting a genuine legacy 4:3 mode cannot pick it; retail-parity comparisons of the Config tab's list/default will show the deviation. | decomp sites in the Divergence column; ISSUES #391 |
|
||||||
| IA-23 | **Filed 2026-08-17 at the night-round review fix round (F8).** `gmHouseUI::DisplayPurchaseTimeText @0x004a3110`'s not-yet-expired branch renders `"You may buy another landscape house at " + strftime("%c", localtime(timestamp + 0x278d00)) + ". This restriction does not apply to apartments."` — byte-decoded from raw pushed literals at `@0x004a3265`/`@0x004a321d`/`@0x004a3235` (all three text pieces confirmed; a prior filing had wrongly called this "unrecoverable"). This port renders the SAME three pieces, in the same order, with the same expiry-timestamp math, but formats the middle date/time piece with .NET's culture-default `DateTime.ToString()` (no explicit format string) rather than the C runtime's `strftime("%c", ...)` — the two engines do not share a format table, so the RENDERED SHAPE of the date/time differs (e.g. .NET's short numeric date+time vs the CRT's `Ddd Mon DD HH:MM:SS YYYY`-style locale string) even though both express "the process's own locale's full date+time" and use the SAME underlying instant (local time, matching retail's `localtime()`). | `src/AcDream.Runtime/Gameplay/RuntimeHouseState.cs` (`Recompute`'s not-expired branch) | Both are "whatever the process locale says" full date+time strings; no game-logic reads or parses this text back, it is pure chat-scroll presentation, so a differently-shaped (but equally legible) date string carries no functional risk | A retail-side-by-side visual comparison will show a differently formatted date/time (not a byte-identical `strftime("%c")` reproduction) — cosmetic only | `gmHouseUI::DisplayPurchaseTimeText @0x004a3110`; `strftime`/`localtime` CRT calls at `@0x004a322c`/`@0x004a3216` |
|
| IA-23 | **Filed 2026-08-17 at the night-round review fix round (F8).** `gmHouseUI::DisplayPurchaseTimeText @0x004a3110`'s not-yet-expired branch renders `"You may buy another landscape house at " + strftime("%c", localtime(timestamp + 0x278d00)) + ". This restriction does not apply to apartments."` — byte-decoded from raw pushed literals at `@0x004a3265`/`@0x004a321d`/`@0x004a3235` (all three text pieces confirmed; a prior filing had wrongly called this "unrecoverable"). This port renders the SAME three pieces, in the same order, with the same expiry-timestamp math, but formats the middle date/time piece with .NET's culture-default `DateTime.ToString()` (no explicit format string) rather than the C runtime's `strftime("%c", ...)` — the two engines do not share a format table, so the RENDERED SHAPE of the date/time differs (e.g. .NET's short numeric date+time vs the CRT's `Ddd Mon DD HH:MM:SS YYYY`-style locale string) even though both express "the process's own locale's full date+time" and use the SAME underlying instant (local time, matching retail's `localtime()`). | `src/AcDream.Runtime/Gameplay/RuntimeHouseState.cs` (`Recompute`'s not-expired branch) | Both are "whatever the process locale says" full date+time strings; no game-logic reads or parses this text back, it is pure chat-scroll presentation, so a differently-shaped (but equally legible) date string carries no functional risk | A retail-side-by-side visual comparison will show a differently formatted date/time (not a byte-identical `strftime("%c")` reproduction) — cosmetic only | `gmHouseUI::DisplayPurchaseTimeText @0x004a3110`; `strftime`/`localtime` CRT calls at `@0x004a322c`/`@0x004a3216` |
|
||||||
| IA-24 | **Filed 2026-08-22, Campaign AR.** An explicitly selected atmospheric render pack adds cascaded real-time directional shadows from terrain, trees, buildings, players, monsters, and other retained outdoor casters. The one shadow direction follows the visible authored sun, then the dominant haloed moon (`0x01001F6A`), then the secondary moon (`0x01001F67`); a moon supplies direction only while colour/energy remains retail's single interpolated `SkyTimeOfDay.DirColor × DirBright` channel. Retail renders none of these real-time object-shadow maps and does not expose a second moon light. | `src/AcDream.App/Rendering/Packs/AuthoredCelestialShadowSource.cs`; `src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs`; pack-only receiver shaders; evidence `docs/research/2026-08-22-dereth-celestial-shadow-sources.md` | This is the user-requested headline graphics enhancement and is strictly opt-in. The retail path remains the default and authoritative fallback; pack-off does not build/select shadow work or change `SceneLighting`. One selected source reuses one cascade array, so moon support does not multiply shadow resources. | Pack-on output intentionally differs from retail. A wrong celestial identity/transform or stale source transition would visibly misalign shadows from the sky; pack-off output changing would violate the campaign's primary safety contract. | `SkyDesc::GetLighting @0x00500A80`; `GameSky::UseTime @0x005075B0`; installed Region `0x13000000`; cited research note |
|
| IA-24 | **Filed 2026-08-22, Campaign AR.** An explicitly selected atmospheric render pack adds cascaded real-time directional shadows from terrain, trees, buildings, players, monsters, and other retained outdoor casters. The one shadow direction follows the visible authored sun, then the dominant haloed moon (`0x01001F6A`), then the secondary moon (`0x01001F67`); a moon supplies direction only while colour/energy remains retail's single interpolated `SkyTimeOfDay.DirColor × DirBright` channel. Retail renders none of these real-time object-shadow maps and does not expose a second moon light. | `src/AcDream.App/Rendering/Packs/AuthoredCelestialShadowSource.cs`; `src/AcDream.App/Rendering/DirectionalSunShadowRenderer.cs`; pack-only receiver shaders; evidence `docs/research/2026-08-22-dereth-celestial-shadow-sources.md` | This is the user-requested headline graphics enhancement and is strictly opt-in. The retail path remains the default and authoritative fallback; pack-off does not build/select shadow work or change `SceneLighting`. One selected source reuses one cascade array, so moon support does not multiply shadow resources. | Pack-on output intentionally differs from retail. A wrong celestial identity/transform or stale source transition would visibly misalign shadows from the sky; pack-off output changing would violate the campaign's primary safety contract. | `SkyDesc::GetLighting @0x00500A80`; `GameSky::UseTime @0x005075B0`; installed Region `0x13000000`; cited research note |
|
||||||
| IA-25 | **Filed 2026-08-22, Campaign VM VM6.** An explicitly selected atmospheric render pack sways procedural-scenery foliage (trees/bushes — entity ids in the `0x8XXYYIII` `ProceduralSceneryIdAllocator` namespace) in `mesh_atmospheric.vert` and the four `directional_shadow_world_*` caster vertex shaders, driven by a weather-table lean/branch/flutter vertex displacement (`foliage_wind.glsl`, `FoliageWindModel` CPU mirror) whose mean/gust strength is looked up per active day group and eases toward its target over `WeatherSystem.TransitionSeconds` (10 s) so a weather change never snaps. Retail's fixed-function renderer applies no per-vertex wind displacement to any scenery mesh — Dereth's trees are static geometry. Wind direction (`wind-direction-degrees`, default 225°) is a plain pack default: there is no authored retail wind direction to read (no wind data exists in retail at all). Render-only: `WorldPicker` picks the undisplaced mesh, so a swaying leaf can be up to `lean + branch` metres from its pick volume at the moment of a click; foliage subsets are cosmetic scenery, not interactable in retail either. | `src/AcDream.App/Rendering/Shaders/foliage_wind.glsl`; `src/AcDream.App/Rendering/Wb/FoliageWindClassification.cs`; `src/AcDream.App/Rendering/Packs/FoliageWindModel.cs`; `src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs` (`ResolveFoliageWind`); `src/AcDream.App/Rendering/Packs/BuiltInAtmosphericRenderPack.cs` (wind settings + `FoliageWindByDayGroup`) | Explicitly opt-in graphics enhancement — the retail path (`mesh_modern`, `terrain_modern`, `mesh_detail`) never reads `BatchData.flags` bits 1/2 and is pixel-identical with the pack off. The classification never touches Runtime/Core physics — the collision BSP is the trunk, and picking against the undisplaced mesh has no gameplay consequence since foliage is not interactable. | Pack-on output intentionally differs from retail (moving foliage where retail has none). A wrong classification bit would sway a non-foliage object or leave a real tree still; a caster/receiver clock or amplitude mismatch would visibly misalign a leaf's shadow from the leaf itself. Pack-off output changing would violate the campaign's primary safety contract. | None — retail applies no vertex wind displacement to any geometry; `ProceduralSceneryIdAllocator` (bit 31 entity-id namespace, existing acdream mechanism, not retail) |
|
| IA-25 | **Filed 2026-08-22, Campaign VM VM6.** An explicitly selected atmospheric render pack sways procedural-scenery foliage (trees/bushes — entity ids in the `0x8XXYYIII` `ProceduralSceneryIdAllocator` namespace) in `mesh_atmospheric.vert` and the four `directional_shadow_world_*` caster vertex shaders, driven by a weather-table lean/branch/flutter vertex displacement (`foliage_wind.glsl`, `FoliageWindModel` CPU mirror) whose mean/gust strength is looked up per DAT-classified `AcDream.Core.World.WeatherKind` (Clear/Overcast/Rain/Snow/Storm — the same classification `WeatherState.cs` already derives from the active day group's authored name, not the day group's raw index, which carries no weather meaning by itself) and eases toward its target over `WeatherSystem.TransitionSeconds` (10 s) so a weather change never snaps. Retail's fixed-function renderer applies no per-vertex wind displacement to any scenery mesh — Dereth's trees are static geometry. Wind direction (`wind-direction-degrees`, default 225°) is a plain pack default: there is no authored retail wind direction to read (no wind data exists in retail at all). Render-only: `WorldPicker` picks the undisplaced mesh, so a swaying leaf can be up to `lean + branch` metres from its pick volume at the moment of a click; foliage subsets are cosmetic scenery, not interactable in retail either. | `src/AcDream.App/Rendering/Shaders/foliage_wind.glsl`; `src/AcDream.App/Rendering/Wb/FoliageWindClassification.cs`; `src/AcDream.App/Rendering/Packs/FoliageWindModel.cs`; `src/AcDream.App/Rendering/Packs/AtmosphericPostProcessGraph.cs` (`ResolveFoliageWind`); `src/AcDream.App/Rendering/Packs/BuiltInAtmosphericRenderPack.cs` (wind settings + `FoliageWindByWeather`) | Explicitly opt-in graphics enhancement — the retail path (`mesh_modern`, `terrain_modern`, `mesh_detail`) never reads `BatchData.flags` bits 1/2 and is pixel-identical with the pack off. The classification never touches Runtime/Core physics — the collision BSP is the trunk, and picking against the undisplaced mesh has no gameplay consequence since foliage is not interactable. | Pack-on output intentionally differs from retail (moving foliage where retail has none). A wrong classification bit would sway a non-foliage object or leave a real tree still; a caster/receiver clock or amplitude mismatch would visibly misalign a leaf's shadow from the leaf itself. Pack-off output changing would violate the campaign's primary safety contract. | None — retail applies no vertex wind displacement to any geometry; `ProceduralSceneryIdAllocator` (bit 31 entity-id namespace, existing acdream mechanism, not retail) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -474,18 +474,26 @@ direction to read, and the register row says so.
|
||||||
|
|
||||||
### VM6 outcome (implementation landed 2026-08-22; owner visual gate outstanding)
|
### VM6 outcome (implementation landed 2026-08-22; owner visual gate outstanding)
|
||||||
|
|
||||||
Two commits (VM6a shader ABI v2 plumbing, VM6b the feature) shipped the exact
|
Two commits (VM6a shader ABI v2 plumbing, VM6b the feature) shipped the
|
||||||
design above. Settings (`BuiltInAtmosphericRenderPack.Settings()`):
|
design above; a same-day fix-round commit corrected the weather-table key.
|
||||||
`wind-enabled` (bool, default on), `wind-strength` (0–2×, default 1.0),
|
Settings (`BuiltInAtmosphericRenderPack.Settings()`): `wind-enabled` (bool,
|
||||||
`wind-direction-degrees` (0–360°, default 225 — no authored retail wind
|
default on), `wind-strength` (0–2×, default 1.0), `wind-direction-degrees`
|
||||||
direction exists to read), `wind-lean-metres` (default 0.25),
|
(0–360°, default 225 — no authored retail wind direction exists to read),
|
||||||
`wind-branch-metres` (default 0.15), `wind-flutter-metres` (default 0.05,
|
`wind-lean-metres` (default 0.25), `wind-branch-metres` (default 0.15),
|
||||||
forced to 0 on the Low preset), `wind-canopy-height-metres` (default 8). The
|
`wind-flutter-metres` (default 0.05, forced to 0 on the Low preset),
|
||||||
Clear/Cloudy/Overcast/Rainy mean/gust rows live in
|
`wind-canopy-height-metres` (default 8). The mean/gust rows live in
|
||||||
`AtmospherePolicyDeclaration.FoliageWindByDayGroup`, keyed by the SAME
|
`AtmospherePolicyDeclaration.FoliageWindByWeather`, keyed by NAME
|
||||||
day-group index convention `ActiveDayGroupMultipliers` already established
|
(`FoliageWindWeatherPoint.WeatherKind`, an exact ordinal match against
|
||||||
two lines above it in `AtmosphericPolicy()` (0 Clear / 1 Cloudy / 2 Overcast;
|
`AcDream.Core.World.WeatherKind`'s member names — `Clear` 0.25/0.15,
|
||||||
index 3 Rainy is new). Classification bits live in
|
`Overcast` 0.60/0.35, `Rain` 0.85/0.60, `Snow` 0.35/0.20, `Storm` 1.00/0.75),
|
||||||
|
not the raw `activeDayGroup` index the original design used: the index
|
||||||
|
carries no weather meaning by itself, and `WeatherState.cs` already
|
||||||
|
classifies each day group's authored DAT name into one of these five real
|
||||||
|
kinds — the same fact `AtmosphericFrameInputs.Weather` /
|
||||||
|
`uAtmosphereWeather.x` already threads through the frame, reused here
|
||||||
|
instead of re-guessed. `RenderPackAtmospherePolicyEvaluation.FoliageWind`
|
||||||
|
matches by `weather.ToString()` and falls back to the declared Clear row for
|
||||||
|
an unlisted kind. Classification bits live in
|
||||||
`FoliageWindClassification` (`AcDream.App.Rendering.Wb`): bit 1 (`0x2`)
|
`FoliageWindClassification` (`AcDream.App.Rendering.Wb`): bit 1 (`0x2`)
|
||||||
cutout foliage, bit 2 (`0x4`) trunk, computed once per (entity, subset) in
|
cutout foliage, bit 2 (`0x4`) trunk, computed once per (entity, subset) in
|
||||||
`WbDrawDispatcher.ClassifyBatches` (world receiver) and
|
`WbDrawDispatcher.ClassifyBatches` (world receiver) and
|
||||||
|
|
|
||||||
|
|
@ -420,7 +420,7 @@ internal sealed class AtmosphericPostProcessGraph :
|
||||||
worldMeshes.FoliageWindExclusions = _foliageWindExclusions;
|
worldMeshes.FoliageWindExclusions = _foliageWindExclusions;
|
||||||
bool isOutdoor = world.Roots.RenderSky && !world.Roots.CameraInsideCell;
|
bool isOutdoor = world.Roots.RenderSky && !world.Roots.CameraInsideCell;
|
||||||
AtmosphericFrameBufferBinding shadowAtmosphericFrame =
|
AtmosphericFrameBufferBinding shadowAtmosphericFrame =
|
||||||
BuildShadowAtmosphericFrameBinding(frame, activeDayGroup, isOutdoor);
|
BuildShadowAtmosphericFrameBinding(frame, foundation.Atmosphere.Kind, isOutdoor);
|
||||||
var input = new DirectionalSunShadowRenderInput(
|
var input = new DirectionalSunShadowRenderInput(
|
||||||
environment,
|
environment,
|
||||||
world.Camera.Camera.View,
|
world.Camera.Camera.View,
|
||||||
|
|
@ -480,12 +480,12 @@ internal sealed class AtmosphericPostProcessGraph :
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private AtmosphericFrameBufferBinding BuildShadowAtmosphericFrameBinding(
|
private AtmosphericFrameBufferBinding BuildShadowAtmosphericFrameBinding(
|
||||||
IGpuFrame frame,
|
IGpuFrame frame,
|
||||||
int activeDayGroup,
|
AcDream.Core.World.WeatherKind weather,
|
||||||
bool isOutdoor)
|
bool isOutdoor)
|
||||||
{
|
{
|
||||||
(Vector4 clockWind, Vector4 windAmplitude) = ResolveFoliageWind(
|
(Vector4 clockWind, Vector4 windAmplitude) = ResolveFoliageWind(
|
||||||
frame.Serial,
|
frame.Serial,
|
||||||
activeDayGroup,
|
weather,
|
||||||
isOutdoor);
|
isOutdoor);
|
||||||
GpuRingAllocation allocation = frame.AllocateRing(
|
GpuRingAllocation allocation = frame.AllocateRing(
|
||||||
AtmosphericFrameUniforms.SizeInBytes,
|
AtmosphericFrameUniforms.SizeInBytes,
|
||||||
|
|
@ -530,13 +530,15 @@ internal sealed class AtmosphericPostProcessGraph :
|
||||||
/// for its own animation clock — replaces it when set, which is every
|
/// for its own animation clock — replaces it when set, which is every
|
||||||
/// run but a differential/offline gate's.</para>
|
/// run but a differential/offline gate's.</para>
|
||||||
///
|
///
|
||||||
/// <para><b>The smoothing.</b> The per-day-group (mean, gust) target
|
/// <para><b>The smoothing.</b> The per-weather-kind (mean, gust) target
|
||||||
/// from <see cref="RenderPackAtmospherePolicyEvaluation.FoliageWind"/>
|
/// from <see cref="RenderPackAtmospherePolicyEvaluation.FoliageWind"/> —
|
||||||
/// eases toward its target using an exponential moving average over
|
/// keyed by the DAT-classified <see cref="AcDream.Core.World.WeatherKind"/>,
|
||||||
/// <see cref="AcDream.Core.World.WeatherSystem.TransitionSeconds"/> (10 s,
|
/// not the raw day-group index, which carries no weather meaning by
|
||||||
/// the same authored weather-transition constant retail-parity work
|
/// itself — eases toward its target using an exponential moving average
|
||||||
/// already established) — so a day-group change never snaps. This state
|
/// over <see cref="AcDream.Core.World.WeatherSystem.TransitionSeconds"/>
|
||||||
/// keeps evolving even while indoors or disabled; <c>wind-enabled</c>
|
/// (10 s, the same authored weather-transition constant retail-parity
|
||||||
|
/// work already established) — so a weather change never snaps. This
|
||||||
|
/// state keeps evolving even while indoors or disabled; <c>wind-enabled</c>
|
||||||
/// off or <c>!isOutdoor</c> instead multiplies the OUTPUT by an exact
|
/// off or <c>!isOutdoor</c> instead multiplies the OUTPUT by an exact
|
||||||
/// zero gate (never an asymptotic approach), so a settings toggle or a
|
/// zero gate (never an asymptotic approach), so a settings toggle or a
|
||||||
/// cell transition reads exactly zero on the very next frame, and
|
/// cell transition reads exactly zero on the very next frame, and
|
||||||
|
|
@ -545,7 +547,7 @@ internal sealed class AtmosphericPostProcessGraph :
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private (Vector4 ClockWind, Vector4 WindAmplitude) ResolveFoliageWind(
|
private (Vector4 ClockWind, Vector4 WindAmplitude) ResolveFoliageWind(
|
||||||
long frameSerial,
|
long frameSerial,
|
||||||
int activeDayGroup,
|
AcDream.Core.World.WeatherKind weather,
|
||||||
bool isOutdoor)
|
bool isOutdoor)
|
||||||
{
|
{
|
||||||
float clockSeconds = _windClockSecondsOverride
|
float clockSeconds = _windClockSecondsOverride
|
||||||
|
|
@ -558,8 +560,8 @@ internal sealed class AtmosphericPostProcessGraph :
|
||||||
1f);
|
1f);
|
||||||
(float targetMean, float targetGust) = RenderPackAtmospherePolicyEvaluation
|
(float targetMean, float targetGust) = RenderPackAtmospherePolicyEvaluation
|
||||||
.FoliageWind(
|
.FoliageWind(
|
||||||
Descriptor.AtmospherePolicy?.FoliageWindByDayGroup,
|
Descriptor.AtmospherePolicy?.FoliageWindByWeather,
|
||||||
activeDayGroup);
|
weather);
|
||||||
targetMean *= _foliageWind.Strength;
|
targetMean *= _foliageWind.Strength;
|
||||||
targetGust *= _foliageWind.Strength;
|
targetGust *= _foliageWind.Strength;
|
||||||
_windMean = RenderPackAtmospherePolicyEvaluation.EaseTowardTarget(
|
_windMean = RenderPackAtmospherePolicyEvaluation.EaseTowardTarget(
|
||||||
|
|
@ -689,7 +691,7 @@ internal sealed class AtmosphericPostProcessGraph :
|
||||||
: 0f;
|
: 0f;
|
||||||
(Vector4 clockWind, Vector4 windAmplitude) = ResolveFoliageWind(
|
(Vector4 clockWind, Vector4 windAmplitude) = ResolveFoliageWind(
|
||||||
frame.Serial,
|
frame.Serial,
|
||||||
inputs.ActiveDayGroup,
|
inputs.Weather,
|
||||||
inputs.IsOutdoor);
|
inputs.IsOutdoor);
|
||||||
var frameUniforms = new AtmosphericFrameUniforms(
|
var frameUniforms = new AtmosphericFrameUniforms(
|
||||||
new Vector4(
|
new Vector4(
|
||||||
|
|
|
||||||
|
|
@ -369,21 +369,25 @@ internal static class BuiltInAtmosphericRenderPack
|
||||||
new SunElevationResponsePoint(70, 0),
|
new SunElevationResponsePoint(70, 0),
|
||||||
new SunElevationResponsePoint(90, 0),
|
new SunElevationResponsePoint(90, 0),
|
||||||
],
|
],
|
||||||
// Campaign VM VM6: the plan's Clear/Cloudy/Overcast/Rainy rows,
|
// Campaign VM VM6, corrected in the fix round: keyed by the
|
||||||
// keyed by the SAME activeDayGroup index convention this pack
|
// DAT-classified AcDream.Core.World.WeatherKind, not the raw
|
||||||
// already established two lines above for ActiveDayGroupMultipliers
|
// activeDayGroup index — WeatherState.cs's existing name
|
||||||
// (0 brightest/clearest ... 2 dimmest). That table only needed
|
// classification (e.g. "Sunny" -> Clear, a name containing
|
||||||
// three rows; foliage wind needs a fourth (Rainy), so index 3 is
|
// "fog"/"cloud"/"overcast"/"dark"/"storm"/"snow"/"rain" -> the
|
||||||
// new here. Dereth's DAT declares many more named day groups than
|
// matching kind) already turns a day group's authored DAT name
|
||||||
// these four categories distinguish — an index absent from this
|
// into one of these five real weather kinds; that fact was
|
||||||
// table (including every index above 3) gets zero wind rather
|
// already threaded through AtmosphericFrameInputs.Weather /
|
||||||
// than guessing a category.
|
// uAtmosphereWeather.x, so this table reuses it instead of
|
||||||
FoliageWindByDayGroup =
|
// guessing a meaning for the raw index. Every WeatherKind member
|
||||||
|
// is declared; nothing falls through to the Clear fallback in
|
||||||
|
// production.
|
||||||
|
FoliageWindByWeather =
|
||||||
[
|
[
|
||||||
new FoliageWindDayGroupPoint(0, 0.25, 0.15), // Clear
|
new FoliageWindWeatherPoint("Clear", 0.25, 0.15),
|
||||||
new FoliageWindDayGroupPoint(1, 0.45, 0.30), // Cloudy
|
new FoliageWindWeatherPoint("Overcast", 0.60, 0.35),
|
||||||
new FoliageWindDayGroupPoint(2, 0.60, 0.35), // Overcast
|
new FoliageWindWeatherPoint("Rain", 0.85, 0.60),
|
||||||
new FoliageWindDayGroupPoint(3, 0.85, 0.60), // Rainy
|
new FoliageWindWeatherPoint("Snow", 0.35, 0.20),
|
||||||
|
new FoliageWindWeatherPoint("Storm", 1.00, 0.75),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using AcDream.Core.World;
|
||||||
using AcDream.Plugin.Abstractions.Rendering;
|
using AcDream.Plugin.Abstractions.Rendering;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Packs;
|
namespace AcDream.App.Rendering.Packs;
|
||||||
|
|
@ -45,26 +46,40 @@ internal static class RenderPackAtmospherePolicyEvaluation
|
||||||
fallback);
|
fallback);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Campaign VM VM6: exact day-group lookup for foliage wind — the same
|
/// Campaign VM VM6, corrected in the fix round: exact weather-kind
|
||||||
/// shape as <see cref="ActiveDayGroupMultiplier"/>'s exact-match pattern
|
/// lookup for foliage wind, keyed by the DAT-classified
|
||||||
/// (day groups are not ordered by "how windy," so there is nothing to
|
/// <see cref="WeatherKind"/> the frame already carries
|
||||||
/// interpolate across them). A day group absent from the table returns
|
/// (<c>AtmosphericFrameInputs.Weather</c> / <c>uAtmosphereWeather.x</c>)
|
||||||
/// (0, 0) — no wind is the safe default for an unclassified day group,
|
/// — not the raw <c>activeDayGroup</c> index, which carries no weather
|
||||||
/// not full wind. The caller is responsible for smoothing the resolved
|
/// meaning by itself. Matches by <c>weather.ToString()</c> (ordinal)
|
||||||
/// target over time; this method is a pure, stateless lookup.
|
/// against each declared <see cref="FoliageWindWeatherPoint.WeatherKind"/>
|
||||||
|
/// name. The five kinds are not ordered by "how windy," so this is an
|
||||||
|
/// exact match, never an interpolation across them. A kind absent from
|
||||||
|
/// the table falls back to the declared Clear row (a weather kind the
|
||||||
|
/// classifier could not resolve is closer to "no weather data" than to
|
||||||
|
/// "assume it's windy"); if Clear itself is undeclared, the fallback is
|
||||||
|
/// (0, 0). The caller is responsible for smoothing the resolved target
|
||||||
|
/// over time; this method is a pure, stateless lookup.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static (float Mean, float Gust) FoliageWind(
|
internal static (float Mean, float Gust) FoliageWind(
|
||||||
IReadOnlyList<FoliageWindDayGroupPoint>? points,
|
IReadOnlyList<FoliageWindWeatherPoint>? points,
|
||||||
int activeDayGroup)
|
WeatherKind weather)
|
||||||
{
|
{
|
||||||
if (points is null)
|
if (points is null)
|
||||||
return (0f, 0f);
|
return (0f, 0f);
|
||||||
foreach (FoliageWindDayGroupPoint point in points)
|
string kind = weather.ToString();
|
||||||
|
FoliageWindWeatherPoint? clear = null;
|
||||||
|
foreach (FoliageWindWeatherPoint point in points)
|
||||||
{
|
{
|
||||||
if (point.ActiveDayGroup == activeDayGroup)
|
if (string.Equals(point.WeatherKind, kind, StringComparison.Ordinal))
|
||||||
return ((float)point.Mean, (float)point.Gust);
|
return ((float)point.Mean, (float)point.Gust);
|
||||||
|
if (clear is null
|
||||||
|
&& string.Equals(point.WeatherKind, nameof(WeatherKind.Clear), StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
clear = point;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (0f, 0f);
|
return clear is { } fallback ? ((float)fallback.Mean, (float)fallback.Gust) : (0f, 0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1482,6 +1482,39 @@ internal static class RenderPackValidator
|
||||||
|| !IsFiniteNonNegative(value.Multiplier))
|
|| !IsFiniteNonNegative(value.Multiplier))
|
||||||
return Invalid($"Pack '{descriptor.Id}' has an invalid active-day-group mapping.");
|
return Invalid($"Pack '{descriptor.Id}' has an invalid active-day-group mapping.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Campaign VM VM6 fix round: FoliageWindByWeather is keyed by the
|
||||||
|
// DAT-classified AcDream.Core.World.WeatherKind name (exact,
|
||||||
|
// case-sensitive), not a raw day-group index — an unknown name or a
|
||||||
|
// repeated kind is rejected outright rather than silently ignored.
|
||||||
|
if (policy.FoliageWindByWeather is null)
|
||||||
|
return Invalid($"Pack '{descriptor.Id}' has a null foliage-wind weather-point list.");
|
||||||
|
var weatherKinds = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
foreach (FoliageWindWeatherPoint point in policy.FoliageWindByWeather)
|
||||||
|
{
|
||||||
|
if (!Enum.TryParse(
|
||||||
|
point.WeatherKind,
|
||||||
|
ignoreCase: false,
|
||||||
|
out AcDream.Core.World.WeatherKind parsedKind)
|
||||||
|
|| !Enum.IsDefined(parsedKind))
|
||||||
|
{
|
||||||
|
return Invalid(
|
||||||
|
$"Pack '{descriptor.Id}' declares an unknown foliage-wind weather "
|
||||||
|
+ $"kind '{point.WeatherKind}'.");
|
||||||
|
}
|
||||||
|
if (!weatherKinds.Add(point.WeatherKind))
|
||||||
|
{
|
||||||
|
return Invalid(
|
||||||
|
$"Pack '{descriptor.Id}' declares the foliage-wind weather kind "
|
||||||
|
+ $"'{point.WeatherKind}' more than once.");
|
||||||
|
}
|
||||||
|
if (!IsFiniteNonNegative(point.Mean) || !IsFiniteNonNegative(point.Gust))
|
||||||
|
{
|
||||||
|
return Invalid(
|
||||||
|
$"Pack '{descriptor.Id}' has an invalid foliage-wind mean/gust value "
|
||||||
|
+ $"for weather kind '{point.WeatherKind}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
return RenderPackValidationResult.Valid();
|
return RenderPackValidationResult.Valid();
|
||||||
|
|
||||||
RenderPackValidationResult ValidateCurve(
|
RenderPackValidationResult ValidateCurve(
|
||||||
|
|
|
||||||
|
|
@ -387,12 +387,25 @@ public sealed record ActiveDayGroupMultiplier(
|
||||||
double Multiplier);
|
double Multiplier);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Explicit mapping from an authored AC day group to the foliage-wind mean
|
/// Explicit mapping from a DAT-classified weather kind to the foliage-wind
|
||||||
/// and gust strength for that group (both in the declared [0,1] range before
|
/// mean and gust strength for that weather (both in the declared [0,1] range
|
||||||
/// the pack's global wind-strength setting scales them). Campaign VM VM6.
|
/// before the pack's global wind-strength setting scales them). Campaign VM
|
||||||
|
/// VM6, corrected in the fix round: the day-group's raw <c>activeDayGroup</c>
|
||||||
|
/// INDEX carries no weather meaning by itself — <c>WeatherState.cs</c>'s
|
||||||
|
/// existing name classification is what turns a day group's DAT name into
|
||||||
|
/// one of the five real weather kinds, and that is what the host already
|
||||||
|
/// threads through <c>AtmosphericFrameInputs.Weather</c> / <c>uAtmosphereWeather.x</c>.
|
||||||
|
/// This is the same fact, keyed correctly instead of by index.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record FoliageWindDayGroupPoint(
|
/// <param name="WeatherKind">
|
||||||
int ActiveDayGroup,
|
/// The exact member name of <c>AcDream.Core.World.WeatherKind</c> — one of
|
||||||
|
/// <c>"Clear"</c>, <c>"Overcast"</c>, <c>"Rain"</c>, <c>"Snow"</c>,
|
||||||
|
/// <c>"Storm"</c>. A plain string because this project (Plugin.Abstractions)
|
||||||
|
/// is BCL-only and cannot reference the Core enum; the host converts with
|
||||||
|
/// <c>WeatherKind.ToString()</c> (ordinal) when resolving.
|
||||||
|
/// </param>
|
||||||
|
public sealed record FoliageWindWeatherPoint(
|
||||||
|
string WeatherKind,
|
||||||
double Mean,
|
double Mean,
|
||||||
double Gust);
|
double Gust);
|
||||||
|
|
||||||
|
|
@ -424,18 +437,18 @@ public sealed record AtmospherePolicyDeclaration(
|
||||||
{ get; init; } = [];
|
{ get; init; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Campaign VM VM6: exact per-day-group foliage-wind mean/gust targets,
|
/// Campaign VM VM6: exact per-weather-kind foliage-wind mean/gust
|
||||||
/// looked up by the same categorical <c>activeDayGroup</c> id
|
/// targets, looked up by the DAT-classified
|
||||||
/// <see cref="ActiveDayGroupMultipliers"/> already keys on (an exact
|
/// <c>AcDream.Core.World.WeatherKind</c> (an exact match, not an
|
||||||
/// match, not an interpolation across day-group ids — day groups are not
|
/// interpolation — the five kinds are not ordered by "how windy"). The
|
||||||
/// ordered by "how windy"). The host smooths the resolved (mean, gust)
|
/// host smooths the resolved (mean, gust) target toward whatever this
|
||||||
/// target toward whatever this table returns over time using the
|
/// table returns over time using the existing weather delta-seconds
|
||||||
/// existing weather delta-seconds clock, so a day-group change never
|
/// clock, so a weather change never snaps. A kind absent from this table
|
||||||
/// snaps. A day group absent from this table gets zero wind. A pack
|
/// falls back to the declared Clear row, then to (0, 0) if Clear itself
|
||||||
/// using foliage wind should declare an entry for every day group it
|
/// is undeclared. Each declared <see cref="FoliageWindWeatherPoint.WeatherKind"/>
|
||||||
/// cares about distinguishing.
|
/// must be one of the five real kind names and must appear at most once.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<FoliageWindDayGroupPoint> FoliageWindByDayGroup
|
public IReadOnlyList<FoliageWindWeatherPoint> FoliageWindByWeather
|
||||||
{ get; init; } = [];
|
{ get; init; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1228,11 +1228,12 @@ public sealed class AtmosphericPostProcessGraphTests
|
||||||
var device = new RecordingGpuDevice();
|
var device = new RecordingGpuDevice();
|
||||||
using var graph = Graph(device, "medium", windClockSecondsOverride: 12f);
|
using var graph = Graph(device, "medium", windClockSecondsOverride: 12f);
|
||||||
graph.PrepareWorldTarget(640, 480, 1);
|
graph.PrepareWorldTarget(640, 480, 1);
|
||||||
// Rainy (index 3 in the built-in table) is the highest declared
|
// Storm is the highest declared mean/gust — the exact target does
|
||||||
// mean/gust — the exact target does not matter here, only that the
|
// not matter here, only that the gate still zeroes the output
|
||||||
// gate still zeroes the output despite a nonzero smoothed target.
|
// despite a nonzero smoothed target.
|
||||||
AtmosphericFrameInputs indoors = Inputs(640, 480, activeDayGroup: 3) with
|
AtmosphericFrameInputs indoors = Inputs(640, 480) with
|
||||||
{
|
{
|
||||||
|
Weather = WeatherKind.Storm,
|
||||||
IsOutdoor = false,
|
IsOutdoor = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1252,7 +1253,7 @@ public sealed class AtmosphericPostProcessGraphTests
|
||||||
windClockSecondsOverride: 12f,
|
windClockSecondsOverride: 12f,
|
||||||
userSettingOverrides: new Dictionary<string, string> { ["wind-enabled"] = "false" });
|
userSettingOverrides: new Dictionary<string, string> { ["wind-enabled"] = "false" });
|
||||||
graph.PrepareWorldTarget(640, 480, 1);
|
graph.PrepareWorldTarget(640, 480, 1);
|
||||||
AtmosphericFrameInputs outdoors = Inputs(640, 480, activeDayGroup: 3);
|
AtmosphericFrameInputs outdoors = Inputs(640, 480) with { Weather = WeatherKind.Storm };
|
||||||
|
|
||||||
AtmosphericFrameUniforms atmospheric = RenderAndReadFrameBlock(device, graph, outdoors);
|
AtmosphericFrameUniforms atmospheric = RenderAndReadFrameBlock(device, graph, outdoors);
|
||||||
|
|
||||||
|
|
@ -1290,7 +1291,7 @@ public sealed class AtmosphericPostProcessGraphTests
|
||||||
IGpuRenderTarget world = graph.PrepareWorldTarget(640, 480, 1);
|
IGpuRenderTarget world = graph.PrepareWorldTarget(640, 480, 1);
|
||||||
using IGpuFrame frame = device.BeginFrame();
|
using IGpuFrame frame = device.BeginFrame();
|
||||||
RecordWorldPass(frame, world);
|
RecordWorldPass(frame, world);
|
||||||
AtmosphericFrameInputs inputs = Inputs(640, 480, activeDayGroup: 3);
|
AtmosphericFrameInputs inputs = Inputs(640, 480) with { Weather = WeatherKind.Storm };
|
||||||
|
|
||||||
graph.RenderPostProcess(frame, in inputs);
|
graph.RenderPostProcess(frame, in inputs);
|
||||||
AtmosphericFrameUniforms first = ReadLastFrameBlock(device);
|
AtmosphericFrameUniforms first = ReadLastFrameBlock(device);
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,76 @@
|
||||||
using AcDream.App.Rendering.Packs;
|
using AcDream.App.Rendering.Packs;
|
||||||
|
using AcDream.Core.World;
|
||||||
using AcDream.Plugin.Abstractions.Rendering;
|
using AcDream.Plugin.Abstractions.Rendering;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering.Packs;
|
namespace AcDream.App.Tests.Rendering.Packs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Campaign VM VM6: <see cref="RenderPackAtmospherePolicyEvaluation.FoliageWind"/>
|
/// Campaign VM VM6: <see cref="RenderPackAtmospherePolicyEvaluation.FoliageWind"/>
|
||||||
/// (the exact per-day-group mean/gust lookup) and
|
/// (the exact per-weather-kind mean/gust lookup, keyed by the DAT-classified
|
||||||
/// <see cref="RenderPackAtmospherePolicyEvaluation.EaseTowardTarget"/> (the
|
/// <see cref="WeatherKind"/> — corrected in the fix round from the raw
|
||||||
/// smoothing step that keeps a day-group change from snapping).
|
/// <c>activeDayGroup</c> index, which carries no weather meaning by itself)
|
||||||
|
/// and <see cref="RenderPackAtmospherePolicyEvaluation.EaseTowardTarget"/>
|
||||||
|
/// (the smoothing step that keeps a weather change from snapping).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class RenderPackAtmospherePolicyEvaluationTests
|
public sealed class RenderPackAtmospherePolicyEvaluationTests
|
||||||
{
|
{
|
||||||
[Fact]
|
private static readonly FoliageWindWeatherPoint[] FiveKindTable =
|
||||||
public void FoliageWindLooksUpExactDayGroupMatchOnly()
|
[
|
||||||
|
new("Clear", 0.25, 0.15),
|
||||||
|
new("Overcast", 0.60, 0.35),
|
||||||
|
new("Rain", 0.85, 0.60),
|
||||||
|
new("Snow", 0.35, 0.20),
|
||||||
|
new("Storm", 1.00, 0.75),
|
||||||
|
];
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(WeatherKind.Clear, 0.25f, 0.15f)]
|
||||||
|
[InlineData(WeatherKind.Overcast, 0.60f, 0.35f)]
|
||||||
|
[InlineData(WeatherKind.Rain, 0.85f, 0.60f)]
|
||||||
|
[InlineData(WeatherKind.Snow, 0.35f, 0.20f)]
|
||||||
|
[InlineData(WeatherKind.Storm, 1.00f, 0.75f)]
|
||||||
|
public void FoliageWindResolvesEachOfTheFiveDeclaredWeatherKinds(
|
||||||
|
WeatherKind kind,
|
||||||
|
float expectedMean,
|
||||||
|
float expectedGust)
|
||||||
{
|
{
|
||||||
FoliageWindDayGroupPoint[] table =
|
(float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(
|
||||||
[
|
FiveKindTable,
|
||||||
new(0, 0.25, 0.15),
|
kind);
|
||||||
new(1, 0.45, 0.30),
|
|
||||||
new(2, 0.60, 0.35),
|
|
||||||
new(3, 0.85, 0.60),
|
|
||||||
];
|
|
||||||
|
|
||||||
(float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(table, 3);
|
Assert.Equal(expectedMean, mean);
|
||||||
|
Assert.Equal(expectedGust, gust);
|
||||||
Assert.Equal(0.85f, mean);
|
|
||||||
Assert.Equal(0.60f, gust);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FoliageWindReturnsZeroForADayGroupAbsentFromTheTable()
|
public void FoliageWindFallsBackToTheClearRowForAnUnlistedKind()
|
||||||
{
|
{
|
||||||
FoliageWindDayGroupPoint[] table = [new(0, 0.25, 0.15)];
|
// A table that only declares Clear and Storm — a kind absent from
|
||||||
|
// the table (Overcast here) falls back to the declared Clear row,
|
||||||
|
// not to (0, 0), since an unresolved weather kind is closer to "no
|
||||||
|
// weather data" than to "assume it's calm."
|
||||||
|
FoliageWindWeatherPoint[] table =
|
||||||
|
[
|
||||||
|
new("Clear", 0.25, 0.15),
|
||||||
|
new("Storm", 1.00, 0.75),
|
||||||
|
];
|
||||||
|
|
||||||
(float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(table, 99);
|
(float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(
|
||||||
|
table,
|
||||||
|
WeatherKind.Overcast);
|
||||||
|
|
||||||
|
Assert.Equal(0.25f, mean);
|
||||||
|
Assert.Equal(0.15f, gust);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FoliageWindReturnsZeroWhenNeitherTheKindNorClearIsDeclared()
|
||||||
|
{
|
||||||
|
FoliageWindWeatherPoint[] table = [new("Storm", 1.00, 0.75)];
|
||||||
|
|
||||||
|
(float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(
|
||||||
|
table,
|
||||||
|
WeatherKind.Overcast);
|
||||||
|
|
||||||
Assert.Equal(0f, mean);
|
Assert.Equal(0f, mean);
|
||||||
Assert.Equal(0f, gust);
|
Assert.Equal(0f, gust);
|
||||||
|
|
@ -42,26 +79,34 @@ public sealed class RenderPackAtmospherePolicyEvaluationTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FoliageWindReturnsZeroForANullTable()
|
public void FoliageWindReturnsZeroForANullTable()
|
||||||
{
|
{
|
||||||
(float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(null, 0);
|
(float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(
|
||||||
|
null,
|
||||||
|
WeatherKind.Clear);
|
||||||
|
|
||||||
Assert.Equal(0f, mean);
|
Assert.Equal(0f, mean);
|
||||||
Assert.Equal(0f, gust);
|
Assert.Equal(0f, gust);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FoliageWindDoesNotInterpolateBetweenDayGroupIndices()
|
public void FoliageWindMatchesByExactNameNotSubstringOrCase()
|
||||||
{
|
{
|
||||||
// Day-group ids are not ordered by "how windy" — index 1 sitting
|
// "Rain" must not match "Rainy" or any case variant — the lookup is
|
||||||
// between 0 and 2 in the table must not produce a value between
|
// an ordinal exact match against the WeatherKind member name.
|
||||||
// their mean/gust. This asserts the lookup is an exact match, not
|
FoliageWindWeatherPoint[] table =
|
||||||
// continuous interpolation across the index axis.
|
[
|
||||||
FoliageWindDayGroupPoint[] table = [new(0, 0.0, 0.0), new(5, 1.0, 1.0)];
|
new("Rainy", 0.99, 0.99),
|
||||||
|
new("rain", 0.99, 0.99),
|
||||||
|
new("Clear", 0.10, 0.05),
|
||||||
|
];
|
||||||
|
|
||||||
(float meanAtUnlistedMidpoint, float gustAtUnlistedMidpoint) =
|
(float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(
|
||||||
RenderPackAtmospherePolicyEvaluation.FoliageWind(table, 2);
|
table,
|
||||||
|
WeatherKind.Rain);
|
||||||
|
|
||||||
Assert.Equal(0f, meanAtUnlistedMidpoint);
|
// Neither "Rainy" nor "rain" matches WeatherKind.Rain.ToString()
|
||||||
Assert.Equal(0f, gustAtUnlistedMidpoint);
|
// ("Rain", exact case) — falls back to the declared Clear row.
|
||||||
|
Assert.Equal(0.10f, mean);
|
||||||
|
Assert.Equal(0.05f, gust);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,104 @@ public sealed class RenderPackSpirvValidatorTests
|
||||||
result.Reason);
|
result.Reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Campaign VM VM6 fix round: FoliageWindByWeather is keyed by the
|
||||||
|
// DAT-classified AcDream.Core.World.WeatherKind name (exact,
|
||||||
|
// case-sensitive), not the raw activeDayGroup index. An unknown name or
|
||||||
|
// a repeated kind must be rejected outright.
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FoliageWindByWeatherRejectsAnUnknownWeatherKindName()
|
||||||
|
{
|
||||||
|
RenderPackDescriptor descriptor = BuiltInAtmosphericRenderPack.Descriptor;
|
||||||
|
descriptor = descriptor with
|
||||||
|
{
|
||||||
|
AtmospherePolicy = descriptor.AtmospherePolicy! with
|
||||||
|
{
|
||||||
|
FoliageWindByWeather =
|
||||||
|
[
|
||||||
|
new FoliageWindWeatherPoint("Cloudy", 0.45, 0.30),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
RenderPackValidationResult result = RenderPackValidator.ValidateDescriptor(
|
||||||
|
descriptor,
|
||||||
|
RenderPackHostCapabilities.Conformance);
|
||||||
|
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.Contains("unknown foliage-wind weather", result.Reason, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FoliageWindByWeatherRejectsANonExactCaseWeatherKindName()
|
||||||
|
{
|
||||||
|
RenderPackDescriptor descriptor = BuiltInAtmosphericRenderPack.Descriptor;
|
||||||
|
descriptor = descriptor with
|
||||||
|
{
|
||||||
|
AtmospherePolicy = descriptor.AtmospherePolicy! with
|
||||||
|
{
|
||||||
|
FoliageWindByWeather = [new FoliageWindWeatherPoint("clear", 0.25, 0.15)],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
RenderPackValidationResult result = RenderPackValidator.ValidateDescriptor(
|
||||||
|
descriptor,
|
||||||
|
RenderPackHostCapabilities.Conformance);
|
||||||
|
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.Contains("unknown foliage-wind weather", result.Reason, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FoliageWindByWeatherRejectsADuplicateWeatherKind()
|
||||||
|
{
|
||||||
|
RenderPackDescriptor descriptor = BuiltInAtmosphericRenderPack.Descriptor;
|
||||||
|
descriptor = descriptor with
|
||||||
|
{
|
||||||
|
AtmospherePolicy = descriptor.AtmospherePolicy! with
|
||||||
|
{
|
||||||
|
FoliageWindByWeather =
|
||||||
|
[
|
||||||
|
new FoliageWindWeatherPoint("Clear", 0.25, 0.15),
|
||||||
|
new FoliageWindWeatherPoint("Clear", 0.30, 0.20),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
RenderPackValidationResult result = RenderPackValidator.ValidateDescriptor(
|
||||||
|
descriptor,
|
||||||
|
RenderPackHostCapabilities.Conformance);
|
||||||
|
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.Contains("more than once", result.Reason, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FoliageWindByWeatherAcceptsTheFiveDeclaredKindsOnce()
|
||||||
|
{
|
||||||
|
RenderPackDescriptor descriptor = BuiltInAtmosphericRenderPack.Descriptor;
|
||||||
|
descriptor = descriptor with
|
||||||
|
{
|
||||||
|
AtmospherePolicy = descriptor.AtmospherePolicy! with
|
||||||
|
{
|
||||||
|
FoliageWindByWeather =
|
||||||
|
[
|
||||||
|
new FoliageWindWeatherPoint("Clear", 0.25, 0.15),
|
||||||
|
new FoliageWindWeatherPoint("Overcast", 0.60, 0.35),
|
||||||
|
new FoliageWindWeatherPoint("Rain", 0.85, 0.60),
|
||||||
|
new FoliageWindWeatherPoint("Snow", 0.35, 0.20),
|
||||||
|
new FoliageWindWeatherPoint("Storm", 1.00, 0.75),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
RenderPackValidationResult result = RenderPackValidator.ValidateDescriptor(
|
||||||
|
descriptor,
|
||||||
|
RenderPackHostCapabilities.Conformance);
|
||||||
|
|
||||||
|
Assert.True(result.Success, result.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DirectionalShadowDepthRejectsSunDirectionAlias()
|
public void DirectionalShadowDepthRejectsSunDirectionAlias()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue