feat(render): weather-driven foliage wind for procedural scenery, shadows follow (Campaign VM VM6b)
Procedural-scenery foliage (trees/bushes — entity ids in the ProceduralSceneryIdAllocator's 0x8XXYYIII namespace) sways with weather in mesh_atmospheric.vert and all four directional_shadow_world_* caster vertex shaders, both calling the identical new foliage_wind.glsl include so the shadow moves with the leaf by construction. Classification (FoliageWindClassification, AcDream.App.Rendering.Wb): two new BatchData.flags bits, computed once per (entity, subset) from four inputs — entity id (bit 31 for procedural scenery), the pack's declared FoliageExclusions membership, the subset's TranslucencyKind, and ObjectRenderData.HasCutoutSubset (computed once per mesh at build time, not per frame). Bit 1 marks an alpha-cutout leaf subset; bit 2 marks an opaque trunk subset (only when its own mesh also owns a cutout subset, so rocks stay still). WbDrawDispatcher.ClassifyBatches (world receiver) and AddDirectionalShadowBatches (caster) call this with the same four inputs, so casters and receivers classify identically without needing to share state. Retail's mesh_modern/terrain_modern/mesh_detail pipelines never read these bits, so pack-off output is unaffected. Motion model (foliage_wind.glsl, mirrored bit-for-bit in the new FoliageWindModel for hermetic CPU tests): height-squared-scaled slow lean for every foliage subset, plus branch swing and per-vertex-hash-decorrelated flutter for cutout subsets only. AtmosphericPostProcessGraph.ResolveFoliageWind resolves the wind block once per frame.Serial — advanced by whichever of RenderDirectionalShadows (which runs first) or RenderPostProcess is called first that frame, with the second reading the already-advanced state, which is what keeps the caster and receiver reading byte-identical clock/strength values. The per-day-group mean/gust target (AtmospherePolicyDeclaration. FoliageWindByDayGroup, keyed by the same day-group index convention ActiveDayGroupMultipliers already established: Clear/Cloudy/Overcast/Rainy) eases toward its target over WeatherSystem.TransitionSeconds (10s) so a weather change never snaps; wind-enabled off or indoor instead gates the OUTPUT to an exact zero (not an asymptotic approach) so a settings toggle or cell transition is immediate. The wind clock is a Stopwatch started at graph construction (monotonic, session-relative magnitude for GPU sin() accuracy), overridable by the same ACDREAM_SKY_PHASE_SECONDS pin SkyRenderer already uses, for deterministic offline gates. New settings: wind-enabled, wind-strength, wind-direction-degrees (225° default — no authored retail wind direction exists to read), wind-lean-metres, wind-branch-metres, wind-flutter-metres (0 on Low), wind-canopy-height-metres. Register row IA-25 files this as an intentional, strictly opt-in divergence: retail applies no per-vertex wind displacement to any geometry. Known, accepted limitation: classification is per mesh-subset (one BatchData.flags word per indirect-draw batch), not per entity instance, so the rare case of one mesh subset being reachable from both a procedural-scenery and a non-scenery placement would classify all of that subset's instances alike. Tests: FoliageWindClassificationTests (the full classification matrix), FoliageWindModelTests (identity on non-foliage/calm-wind/base-vertex, canopy-top displacement bound, z-never-increases, trunk has no flutter term), RenderPackAtmospherePolicyEvaluationTests (exact day-group lookup, no interpolation across day-group ids, easing convergence without overshoot or discontinuity), AtmosphericShaderAbiTests (each of the five shaders calls acdreamFoliageDisplace exactly once; mesh_modern/terrain/mesh_detail call it never), and four AtmosphericPostProcessGraphTests additions (indoor/disabled exact-zero gating, settings-to-UBO wiring, same-frame-Serial idempotency — the last proxies the caster/receiver agreement invariant without needing this hermetic harness's WbDrawDispatcher/TerrainModernRenderer dependency chain to exercise RenderDirectionalShadows directly). App hermetic filter: 6015/6017 (the same 2 pre-existing failures as VM6a, confirmed unrelated). Core.Tests hermetic: 4697/4697. RenderPackValidator.Tests: 30/30. Full solution Debug and Release builds green. Shader recompile touched exactly the 5 edited files' .spv (plus manifest); the retail oracle set and every other pack shader are byte-identical. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
0930c35d1d
commit
39e8408c7d
34 changed files with 1536 additions and 64 deletions
|
|
@ -296,7 +296,12 @@ internal sealed class FrameRootCompositionPhase
|
|||
renderPackController = new AcDream.App.Rendering.Packs.RenderPackController(
|
||||
renderPackCatalog.Snapshot,
|
||||
new AcDream.App.Rendering.Packs.AtmosphericRenderPackRuntimeFactory(
|
||||
host.GpuDevice),
|
||||
host.GpuDevice,
|
||||
// Campaign VM VM6: the same ACDREAM_SKY_PHASE_SECONDS pin
|
||||
// SkyRenderer uses, so the foliage-wind clock is
|
||||
// deterministic under the same differential/offline
|
||||
// gates — see ResolveFoliageWind's doc comment.
|
||||
d.Options.SkyAnimationPhaseSeconds),
|
||||
new AcDream.App.Rendering.Packs.RenderPackReceiverPipelineCoordinator(
|
||||
foundation.Terrain!,
|
||||
live.DrawDispatcher!),
|
||||
|
|
|
|||
|
|
@ -725,7 +725,11 @@ internal sealed class DirectionalSunShadowRenderer : IDirectionalShadowReceiverS
|
|||
batch.TextureSlot.Index,
|
||||
0u,
|
||||
batch.TextureLayer,
|
||||
DirectionalShadowBatchFlags.Encode(batch.Material));
|
||||
// Campaign VM VM6: batch.FoliageFlags rides alongside
|
||||
// the existing alpha-cutout bit in the same word the
|
||||
// caster shaders read as BatchData.flags.
|
||||
DirectionalShadowBatchFlags.Encode(batch.Material)
|
||||
| batch.FoliageFlags);
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> batchBytes = MemoryMarshal.AsBytes(
|
||||
|
|
|
|||
|
|
@ -65,6 +65,65 @@ internal readonly record struct AtmosphericPostProcessSettings(
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign VM VM6: the resolved foliage-wind pack settings — the CPU-side
|
||||
/// counterpart of <c>uAtmosphereWindAmplitude</c> plus the global strength/
|
||||
/// direction/enabled inputs that scale it. Resolution mirrors
|
||||
/// <see cref="AtmosphericPostProcessSettings.FromDescriptor"/> exactly
|
||||
/// (declaration default, then preset override, then user override).
|
||||
/// </summary>
|
||||
internal readonly record struct FoliageWindSettings(
|
||||
bool Enabled,
|
||||
float Strength,
|
||||
float DirectionDegrees,
|
||||
float LeanMetres,
|
||||
float BranchMetres,
|
||||
float FlutterMetres,
|
||||
float CanopyHeightMetres)
|
||||
{
|
||||
internal static FoliageWindSettings FromDescriptor(
|
||||
RenderPackDescriptor descriptor,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string>? userSettingOverrides = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(descriptor);
|
||||
ArgumentNullException.ThrowIfNull(preset);
|
||||
return new FoliageWindSettings(
|
||||
ReadBool(descriptor, preset, userSettingOverrides, RenderSettingSemantic.WindEnabled, true),
|
||||
Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.WindStrength, 1f),
|
||||
Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.WindDirectionDegrees, 225f),
|
||||
Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.WindLeanMetres, 0.25f),
|
||||
Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.WindBranchMetres, 0.15f),
|
||||
Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.WindFlutterMetres, 0.05f),
|
||||
Read(descriptor, preset, userSettingOverrides, RenderSettingSemantic.WindCanopyHeightMetres, 8f));
|
||||
}
|
||||
|
||||
private static float Read(
|
||||
RenderPackDescriptor descriptor,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string>? userSettingOverrides,
|
||||
RenderSettingSemantic semantic,
|
||||
float fallback)
|
||||
{
|
||||
RenderSettingDeclaration? setting = descriptor.Settings.FirstOrDefault(value =>
|
||||
value.Semantic == semantic);
|
||||
if (setting is null)
|
||||
return fallback;
|
||||
string value = RenderPackSettingResolution.Resolve(setting, preset, userSettingOverrides);
|
||||
return RenderPackSettingValueCodec.TryEncode(setting, value, out float encoded)
|
||||
? encoded
|
||||
: fallback;
|
||||
}
|
||||
|
||||
private static bool ReadBool(
|
||||
RenderPackDescriptor descriptor,
|
||||
RenderQualityPreset preset,
|
||||
IReadOnlyDictionary<string, string>? userSettingOverrides,
|
||||
RenderSettingSemantic semantic,
|
||||
bool fallback) =>
|
||||
Read(descriptor, preset, userSettingOverrides, semantic, fallback ? 1f : 0f) > 0.5f;
|
||||
}
|
||||
|
||||
internal interface IAtmosphericWorldGraphRuntime : IRenderPackRuntime
|
||||
{
|
||||
IGpuRenderTarget PrepareWorldTarget(int width, int height, int sampleCount);
|
||||
|
|
@ -112,6 +171,26 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
private readonly AtmosphericPostProcessSettings _settings;
|
||||
private readonly float _shadowStrength;
|
||||
private readonly PackSettingsUniforms _packSettings;
|
||||
// Campaign VM VM6: foliage wind. _foliageWindExclusions is resolved once
|
||||
// at construction from the descriptor's declared policy (immutable for
|
||||
// the pack's lifetime, like every other setting here); _windClock is a
|
||||
// monotonic, session-relative (NOT wall-clock, NOT since-boot) seconds
|
||||
// source — see BuildFoliageWindClockAndAmplitude's doc comment for why.
|
||||
// The remaining fields are the per-frame smoothing state: advanced at
|
||||
// most once per frame.Serial regardless of which of
|
||||
// RenderDirectionalShadows/RenderPostProcess runs first that frame, so
|
||||
// the caster and receiver always read byte-identical values within one
|
||||
// frame (see D2/D4 — the shadow must move with the leaf).
|
||||
private readonly FoliageWindSettings _foliageWind;
|
||||
private readonly IReadOnlySet<uint> _foliageWindExclusions;
|
||||
private readonly float? _windClockSecondsOverride;
|
||||
private readonly System.Diagnostics.Stopwatch _windClock =
|
||||
System.Diagnostics.Stopwatch.StartNew();
|
||||
private long _windFrameSerial = -1;
|
||||
private float _windClockSeconds;
|
||||
private float _windLastAdvanceClockSeconds;
|
||||
private float _windMean;
|
||||
private float _windGust;
|
||||
private readonly DirectionalSunShadowRenderer _directionalShadows;
|
||||
private readonly VolumetricShaftRenderer? _volumetric;
|
||||
private readonly bool _fuseLowPostProcess;
|
||||
|
|
@ -150,14 +229,16 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
IRenderPackAssets assets,
|
||||
RenderQualityPreset preset,
|
||||
AtmosphericPostProcessSettings? settings = null,
|
||||
IReadOnlyDictionary<string, string>? userSettingOverrides = null)
|
||||
IReadOnlyDictionary<string, string>? userSettingOverrides = null,
|
||||
float? windClockSecondsOverride = null)
|
||||
: this(
|
||||
device,
|
||||
descriptor,
|
||||
RenderPackShaderAssets.Validate(descriptor, assets),
|
||||
preset,
|
||||
settings,
|
||||
userSettingOverrides)
|
||||
userSettingOverrides,
|
||||
windClockSecondsOverride)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +248,8 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
ValidatedRenderPackShaderAssets assets,
|
||||
RenderQualityPreset preset,
|
||||
AtmosphericPostProcessSettings? settings = null,
|
||||
IReadOnlyDictionary<string, string>? userSettingOverrides = null)
|
||||
IReadOnlyDictionary<string, string>? userSettingOverrides = null,
|
||||
float? windClockSecondsOverride = null)
|
||||
{
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor));
|
||||
|
|
@ -224,6 +306,14 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
descriptor,
|
||||
preset,
|
||||
userSettingOverrides);
|
||||
_foliageWind = FoliageWindSettings.FromDescriptor(
|
||||
descriptor,
|
||||
preset,
|
||||
userSettingOverrides);
|
||||
_foliageWindExclusions =
|
||||
(descriptor.AtmospherePolicy?.FoliageExclusions
|
||||
?? (IReadOnlyList<uint>)[]).ToHashSet();
|
||||
_windClockSecondsOverride = windClockSecondsOverride;
|
||||
_packSettings = PackSettingsUniforms.Create(
|
||||
descriptor,
|
||||
preset,
|
||||
|
|
@ -323,8 +413,14 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
* _shadowStrength,
|
||||
0f,
|
||||
1f));
|
||||
// Campaign VM VM6: self-contained by construction — set every frame
|
||||
// from the currently-active pack's declared policy so a pack switch
|
||||
// or deactivation can never leave a stale exclusion set applied to
|
||||
// the dispatcher (see FoliageWindExclusions's doc comment).
|
||||
worldMeshes.FoliageWindExclusions = _foliageWindExclusions;
|
||||
bool isOutdoor = world.Roots.RenderSky && !world.Roots.CameraInsideCell;
|
||||
AtmosphericFrameBufferBinding shadowAtmosphericFrame =
|
||||
BuildShadowAtmosphericFrameBinding(frame);
|
||||
BuildShadowAtmosphericFrameBinding(frame, activeDayGroup, isOutdoor);
|
||||
var input = new DirectionalSunShadowRenderInput(
|
||||
environment,
|
||||
world.Camera.Camera.View,
|
||||
|
|
@ -380,12 +476,17 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
/// <see cref="DirectionalSunShadowRenderer.RenderPrepared"/>. Only the
|
||||
/// two appended VM6 members carry real content; the caster shaders never
|
||||
/// read the other seven (sun/weather/policy/inverse-view-projection are
|
||||
/// receiver-only concerns), so this method leaves them zeroed rather
|
||||
/// than duplicating <see cref="RenderPostProcess"/>'s computation.
|
||||
/// receiver-only concerns).
|
||||
/// </summary>
|
||||
private AtmosphericFrameBufferBinding BuildShadowAtmosphericFrameBinding(
|
||||
IGpuFrame frame)
|
||||
IGpuFrame frame,
|
||||
int activeDayGroup,
|
||||
bool isOutdoor)
|
||||
{
|
||||
(Vector4 clockWind, Vector4 windAmplitude) = ResolveFoliageWind(
|
||||
frame.Serial,
|
||||
activeDayGroup,
|
||||
isOutdoor);
|
||||
GpuRingAllocation allocation = frame.AllocateRing(
|
||||
AtmosphericFrameUniforms.SizeInBytes,
|
||||
GpuRingUsage.Uniform);
|
||||
|
|
@ -397,10 +498,8 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
Vector4.Zero,
|
||||
Vector4.Zero,
|
||||
Matrix4x4.Identity,
|
||||
// VM6a: ABI v2 plumbing only. VM6b fills the real weather-driven
|
||||
// wind block here; no caster shader reads it yet.
|
||||
Vector4.Zero,
|
||||
Vector4.Zero);
|
||||
clockWind,
|
||||
windAmplitude);
|
||||
MemoryMarshal.Write(allocation.Data, in uniforms);
|
||||
return new AtmosphericFrameBufferBinding(
|
||||
allocation.Buffer,
|
||||
|
|
@ -408,6 +507,91 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
(uint)AtmosphericFrameUniforms.SizeInBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign VM VM6: resolves this frame's <c>uAtmosphereClockWind</c>/
|
||||
/// <c>uAtmosphereWindAmplitude</c> values, advancing the weather-driven
|
||||
/// smoothing state at most once per <paramref name="frameSerial"/>. Both
|
||||
/// <see cref="RenderDirectionalShadows"/> (which runs first) and
|
||||
/// <see cref="RenderPostProcess"/> call this for the SAME frame, so the
|
||||
/// second caller reads the already-advanced state rather than
|
||||
/// re-advancing it — this is what keeps the caster and receiver's clock
|
||||
/// and smoothed wind strength byte-identical within one frame (D2/D4:
|
||||
/// the shadow must move with the leaf).
|
||||
///
|
||||
/// <para><b>The clock.</b> Not wall time and not "seconds since
|
||||
/// process/system start" — <see cref="_windClock"/> is a
|
||||
/// <see cref="System.Diagnostics.Stopwatch"/> started when this graph
|
||||
/// was constructed, so its magnitude stays small (session-relative) for
|
||||
/// GPU single-precision <c>sin()</c> accuracy over a long play session,
|
||||
/// and it is genuinely monotonic (unlike <c>DateTime.UtcNow</c>).
|
||||
/// <see cref="_windClockSecondsOverride"/> — threaded from
|
||||
/// <c>RuntimeOptions.SkyAnimationPhaseSeconds</c> (<c>ACDREAM_SKY_PHASE_SECONDS</c>),
|
||||
/// the same pin <see cref="AcDream.App.Rendering.Sky.SkyRenderer"/> uses
|
||||
/// for its own animation clock — replaces it when set, which is every
|
||||
/// run but a differential/offline gate's.</para>
|
||||
///
|
||||
/// <para><b>The smoothing.</b> The per-day-group (mean, gust) target
|
||||
/// from <see cref="RenderPackAtmospherePolicyEvaluation.FoliageWind"/>
|
||||
/// eases toward its target using an exponential moving average over
|
||||
/// <see cref="AcDream.Core.World.WeatherSystem.TransitionSeconds"/> (10 s,
|
||||
/// the same authored weather-transition constant retail-parity work
|
||||
/// already established) — so a day-group 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
|
||||
/// zero gate (never an asymptotic approach), so a settings toggle or a
|
||||
/// cell transition reads exactly zero on the very next frame, and
|
||||
/// resuming outdoors/enabled picks the smoothed state back up without a
|
||||
/// spin-up glitch.</para>
|
||||
/// </summary>
|
||||
private (Vector4 ClockWind, Vector4 WindAmplitude) ResolveFoliageWind(
|
||||
long frameSerial,
|
||||
int activeDayGroup,
|
||||
bool isOutdoor)
|
||||
{
|
||||
float clockSeconds = _windClockSecondsOverride
|
||||
?? (float)_windClock.Elapsed.TotalSeconds;
|
||||
if (_windFrameSerial != frameSerial)
|
||||
{
|
||||
float deltaSeconds = Math.Clamp(
|
||||
clockSeconds - _windLastAdvanceClockSeconds,
|
||||
0f,
|
||||
1f);
|
||||
(float targetMean, float targetGust) = RenderPackAtmospherePolicyEvaluation
|
||||
.FoliageWind(
|
||||
Descriptor.AtmospherePolicy?.FoliageWindByDayGroup,
|
||||
activeDayGroup);
|
||||
targetMean *= _foliageWind.Strength;
|
||||
targetGust *= _foliageWind.Strength;
|
||||
_windMean = RenderPackAtmospherePolicyEvaluation.EaseTowardTarget(
|
||||
_windMean,
|
||||
targetMean,
|
||||
deltaSeconds,
|
||||
AcDream.Core.World.WeatherSystem.TransitionSeconds);
|
||||
_windGust = RenderPackAtmospherePolicyEvaluation.EaseTowardTarget(
|
||||
_windGust,
|
||||
targetGust,
|
||||
deltaSeconds,
|
||||
AcDream.Core.World.WeatherSystem.TransitionSeconds);
|
||||
_windClockSeconds = clockSeconds;
|
||||
_windLastAdvanceClockSeconds = clockSeconds;
|
||||
_windFrameSerial = frameSerial;
|
||||
}
|
||||
|
||||
float gate = _foliageWind.Enabled && isOutdoor ? 1f : 0f;
|
||||
float directionRadians = _foliageWind.DirectionDegrees * (MathF.PI / 180f);
|
||||
var clockWind = new Vector4(
|
||||
_windClockSeconds,
|
||||
_windMean * gate,
|
||||
_windGust * gate,
|
||||
directionRadians);
|
||||
var windAmplitude = new Vector4(
|
||||
_foliageWind.LeanMetres,
|
||||
_foliageWind.BranchMetres,
|
||||
_foliageWind.FlutterMetres,
|
||||
_foliageWind.CanopyHeightMetres);
|
||||
return (clockWind, windAmplitude);
|
||||
}
|
||||
|
||||
public IGpuRenderTarget PrepareWorldTarget(
|
||||
int width,
|
||||
int height,
|
||||
|
|
@ -503,6 +687,10 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
float rayStrength = inputs.SunIsOnScreen && inputs.IsOutdoor
|
||||
? Math.Clamp(_settings.SunRayStrength * sunPolicy, 0f, 4f)
|
||||
: 0f;
|
||||
(Vector4 clockWind, Vector4 windAmplitude) = ResolveFoliageWind(
|
||||
frame.Serial,
|
||||
inputs.ActiveDayGroup,
|
||||
inputs.IsOutdoor);
|
||||
var frameUniforms = new AtmosphericFrameUniforms(
|
||||
new Vector4(
|
||||
inputs.SunScreenUv,
|
||||
|
|
@ -526,12 +714,8 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
shadowElevationPolicy,
|
||||
volumetricElevationPolicy),
|
||||
inputs.InverseViewProjection,
|
||||
// Campaign VM VM6a: ABI v2 plumbing only — written here so the
|
||||
// 192-byte block is always fully populated, but mesh_atmospheric.vert
|
||||
// does not read these two members yet (VM6b wires the real
|
||||
// weather-driven values and the shader read together).
|
||||
Vector4.Zero,
|
||||
Vector4.Zero);
|
||||
clockWind,
|
||||
windAmplitude);
|
||||
GpuRingAllocation frameBlock;
|
||||
GpuRingAllocation settingsBlock;
|
||||
GpuRingAllocation fusedSunPassBlock = default;
|
||||
|
|
@ -1679,12 +1863,20 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
}
|
||||
}
|
||||
|
||||
internal sealed class AtmosphericRenderPackRuntimeFactory(IGpuDevice device) :
|
||||
internal sealed class AtmosphericRenderPackRuntimeFactory(
|
||||
IGpuDevice device,
|
||||
float? skyPhaseSecondsOverride = null) :
|
||||
IRenderPackRuntimeFactory
|
||||
{
|
||||
private readonly IGpuDevice _device = device
|
||||
?? throw new ArgumentNullException(nameof(device));
|
||||
|
||||
// Campaign VM VM6: threaded to every built-in-graph instance this
|
||||
// factory produces — see AtmosphericPostProcessGraph.ResolveFoliageWind's
|
||||
// doc comment for why the foliage-wind clock needs this same pin
|
||||
// AcDream.App.Rendering.Sky.SkyRenderer already uses.
|
||||
private readonly float? _skyPhaseSecondsOverride = skyPhaseSecondsOverride;
|
||||
|
||||
public IRenderPackRuntime Build(
|
||||
RenderPackDescriptor descriptor,
|
||||
IRenderPackAssets assets,
|
||||
|
|
@ -1736,7 +1928,8 @@ internal sealed class AtmosphericRenderPackRuntimeFactory(IGpuDevice device) :
|
|||
descriptor,
|
||||
assets,
|
||||
preset,
|
||||
userSettingOverrides: userSettingOverrides);
|
||||
userSettingOverrides: userSettingOverrides,
|
||||
windClockSecondsOverride: _skyPhaseSecondsOverride);
|
||||
}
|
||||
|
||||
bool declaredDirectionalShadowGraph = descriptor.Passes.Count(pass =>
|
||||
|
|
|
|||
|
|
@ -309,6 +309,33 @@ internal static class BuiltInAtmosphericRenderPack
|
|||
null,
|
||||
[])
|
||||
with { Semantic = RenderSettingSemantic.AutomaticQuality },
|
||||
// Campaign VM VM6: foliage wind. wind-strength is a global multiplier
|
||||
// over the per-day-group mean/gust table (AtmospherePolicy below);
|
||||
// wind-direction-degrees has no authored retail wind direction to
|
||||
// read (register row: "no authored wind direction" — foliage wind is
|
||||
// itself a render-only approximation), so it is a plain pack default.
|
||||
new RenderSettingDeclaration(
|
||||
"wind-enabled",
|
||||
"Foliage wind",
|
||||
RenderSettingKind.Boolean,
|
||||
"true",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
[])
|
||||
with { Semantic = RenderSettingSemantic.WindEnabled },
|
||||
Float("wind-strength", "Foliage wind strength", RenderSettingSemantic.WindStrength,
|
||||
1.0, 0, 2, 0.05),
|
||||
Float("wind-direction-degrees", "Foliage wind direction (degrees)",
|
||||
RenderSettingSemantic.WindDirectionDegrees, 225, 0, 360, 5),
|
||||
Float("wind-lean-metres", "Foliage lean amplitude (metres)",
|
||||
RenderSettingSemantic.WindLeanMetres, 0.25, 0, 1, 0.01),
|
||||
Float("wind-branch-metres", "Foliage branch-swing amplitude (metres)",
|
||||
RenderSettingSemantic.WindBranchMetres, 0.15, 0, 1, 0.01),
|
||||
Float("wind-flutter-metres", "Foliage flutter amplitude (metres)",
|
||||
RenderSettingSemantic.WindFlutterMetres, 0.05, 0, 0.5, 0.005),
|
||||
Float("wind-canopy-height-metres", "Foliage canopy height (metres)",
|
||||
RenderSettingSemantic.WindCanopyHeightMetres, 8, 2, 30, 0.5),
|
||||
];
|
||||
|
||||
private static AtmospherePolicyDeclaration AtmospherePolicy() => new(
|
||||
|
|
@ -342,6 +369,22 @@ internal static class BuiltInAtmosphericRenderPack
|
|||
new SunElevationResponsePoint(70, 0),
|
||||
new SunElevationResponsePoint(90, 0),
|
||||
],
|
||||
// Campaign VM VM6: the plan's Clear/Cloudy/Overcast/Rainy rows,
|
||||
// keyed by the SAME activeDayGroup index convention this pack
|
||||
// already established two lines above for ActiveDayGroupMultipliers
|
||||
// (0 brightest/clearest ... 2 dimmest). That table only needed
|
||||
// three rows; foliage wind needs a fourth (Rainy), so index 3 is
|
||||
// new here. Dereth's DAT declares many more named day groups than
|
||||
// these four categories distinguish — an index absent from this
|
||||
// table (including every index above 3) gets zero wind rather
|
||||
// than guessing a category.
|
||||
FoliageWindByDayGroup =
|
||||
[
|
||||
new FoliageWindDayGroupPoint(0, 0.25, 0.15), // Clear
|
||||
new FoliageWindDayGroupPoint(1, 0.45, 0.30), // Cloudy
|
||||
new FoliageWindDayGroupPoint(2, 0.60, 0.35), // Overcast
|
||||
new FoliageWindDayGroupPoint(3, 0.85, 0.60), // Rainy
|
||||
],
|
||||
};
|
||||
|
||||
private static RenderResourceDeclaration Image(
|
||||
|
|
@ -443,6 +486,10 @@ internal static class BuiltInAtmosphericRenderPack
|
|||
// The renderer recognizes this bounded preset fact; it remains
|
||||
// visible here instead of becoming a hidden cascade constant.
|
||||
new RenderQualitySettingOverride("sun-ray-strength", id == "low" ? "0.4" : "0.55"),
|
||||
// Campaign VM VM6: Low is lean + branch only, no flutter — the
|
||||
// per-vertex hash term is the cheapest of the three to drop
|
||||
// and the least visible at Low's other reduced settings.
|
||||
new RenderQualitySettingOverride("wind-flutter-metres", id == "low" ? "0" : "0.05"),
|
||||
],
|
||||
maxMiB * 1024 * 1024,
|
||||
gpuP50,
|
||||
|
|
|
|||
91
src/AcDream.App/Rendering/Packs/FoliageWindModel.cs
Normal file
91
src/AcDream.App/Rendering/Packs/FoliageWindModel.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
|
||||
namespace AcDream.App.Rendering.Packs;
|
||||
|
||||
/// <summary>
|
||||
/// CPU-side mirror of <c>foliage_wind.glsl</c>'s <c>acdreamFoliageDisplace</c>,
|
||||
/// added by Campaign VM VM6.
|
||||
///
|
||||
/// <para>The GLSL is the source of truth for what actually renders — this
|
||||
/// class exists only so the motion model's shape (identity on non-foliage
|
||||
/// flags, identity when the wind is calm, zero displacement at the trunk
|
||||
/// base, bounded displacement at the canopy top, no flutter on a trunk
|
||||
/// subset, never-increasing height) can be pinned by fast CPU tests instead
|
||||
/// of a GPU capture, matching the same pattern as
|
||||
/// <see cref="AtmosphericColorPipeline"/>. Any change to
|
||||
/// <c>acdreamFoliageDisplace</c> — the gust envelope constants, the lean/
|
||||
/// branch/flutter formulas, the bend-shortening term, the per-vertex hash —
|
||||
/// MUST be mirrored here in the same commit, or this class silently stops
|
||||
/// proving what the shader does.</para>
|
||||
/// </summary>
|
||||
internal static class FoliageWindModel
|
||||
{
|
||||
private const uint FoliageMask =
|
||||
FoliageWindClassification.CutoutFoliageFlag | FoliageWindClassification.TrunkFlag;
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors <c>acdreamFoliageDisplace</c> exactly. <paramref name="clockWind"/>
|
||||
/// is (elapsed seconds, wind mean [0..1], wind gust [0..1], wind
|
||||
/// direction radians); <paramref name="amplitude"/> is (lean m, branch
|
||||
/// m, flutter m, max canopy height m) — the same layout as
|
||||
/// <c>uAtmosphereClockWind</c>/<c>uAtmosphereWindAmplitude</c>.
|
||||
/// </summary>
|
||||
internal static Vector3 Displace(
|
||||
Vector3 worldPos,
|
||||
Vector3 instanceOrigin,
|
||||
uint batchFlags,
|
||||
Vector4 clockWind,
|
||||
Vector4 amplitude)
|
||||
{
|
||||
if ((batchFlags & FoliageMask) == 0u)
|
||||
return worldPos;
|
||||
|
||||
float t = clockWind.X;
|
||||
float mean = clockWind.Y;
|
||||
float gust = clockWind.Z;
|
||||
float dirA = clockWind.W;
|
||||
|
||||
var dir = new Vector2(MathF.Cos(dirA), MathF.Sin(dirA));
|
||||
var perp = new Vector2(-dir.Y, dir.X);
|
||||
|
||||
float h = Math.Clamp(
|
||||
(worldPos.Z - instanceOrigin.Z) / MathF.Max(amplitude.W, 0.5f),
|
||||
0f,
|
||||
1f);
|
||||
float k = h * h;
|
||||
|
||||
float ph = Vector2.Dot(
|
||||
new Vector2(instanceOrigin.X, instanceOrigin.Y),
|
||||
new Vector2(0.137f, 0.291f));
|
||||
|
||||
float g = 0.5f
|
||||
+ (0.5f * MathF.Sin((0.05f * t) + ph))
|
||||
+ (0.25f * MathF.Sin((0.13f * t) + (1.7f * ph)));
|
||||
float s = mean + (gust * g);
|
||||
|
||||
float lean = k * amplitude.X * s * (0.8f + (0.2f * MathF.Sin((0.35f * t) + ph)));
|
||||
Vector2 d = dir * lean;
|
||||
|
||||
if ((batchFlags & FoliageWindClassification.CutoutFoliageFlag) != 0u)
|
||||
{
|
||||
float branch = k * amplitude.Y * s * MathF.Sin((1.1f * t) + ph + (2.0f * h));
|
||||
var worldXY = new Vector2(worldPos.X, worldPos.Y);
|
||||
float vh = Frac(
|
||||
MathF.Sin(Vector2.Dot(worldXY, new Vector2(12.9898f, 78.233f))) * 43758.5453f);
|
||||
float flutter = h * amplitude.Z * s * MathF.Sin((6.0f * t) + (7.0f * vh));
|
||||
d += (dir * branch)
|
||||
+ (perp * 0.35f * branch)
|
||||
+ (new Vector2(MathF.Cos(6.2832f * vh), MathF.Sin(6.2832f * vh)) * flutter);
|
||||
}
|
||||
|
||||
Vector3 p = worldPos;
|
||||
p.X += d.X;
|
||||
p.Y += d.Y;
|
||||
p.Z -= 0.5f * Vector2.Dot(d, d) / MathF.Max(h * amplitude.W, 0.5f);
|
||||
return p;
|
||||
}
|
||||
|
||||
/// <summary>GLSL <c>fract</c>: always non-negative, matches <c>x - floor(x)</c>.</summary>
|
||||
private static float Frac(float x) => x - MathF.Floor(x);
|
||||
}
|
||||
|
|
@ -44,6 +44,50 @@ internal static class RenderPackAtmospherePolicyEvaluation
|
|||
static value => (float)value,
|
||||
fallback);
|
||||
|
||||
/// <summary>
|
||||
/// Campaign VM VM6: exact day-group lookup for foliage wind — the same
|
||||
/// shape as <see cref="ActiveDayGroupMultiplier"/>'s exact-match pattern
|
||||
/// (day groups are not ordered by "how windy," so there is nothing to
|
||||
/// interpolate across them). A day group absent from the table returns
|
||||
/// (0, 0) — no wind is the safe default for an unclassified day group,
|
||||
/// not full wind. The caller is responsible for smoothing the resolved
|
||||
/// target over time; this method is a pure, stateless lookup.
|
||||
/// </summary>
|
||||
internal static (float Mean, float Gust) FoliageWind(
|
||||
IReadOnlyList<FoliageWindDayGroupPoint>? points,
|
||||
int activeDayGroup)
|
||||
{
|
||||
if (points is null)
|
||||
return (0f, 0f);
|
||||
foreach (FoliageWindDayGroupPoint point in points)
|
||||
{
|
||||
if (point.ActiveDayGroup == activeDayGroup)
|
||||
return ((float)point.Mean, (float)point.Gust);
|
||||
}
|
||||
return (0f, 0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign VM VM6: one exponential-moving-average step toward
|
||||
/// <paramref name="target"/>, used to smooth the foliage-wind mean/gust
|
||||
/// strength across a day-group change so it never snaps. Pure and
|
||||
/// stateless — the caller owns <paramref name="current"/> across calls.
|
||||
/// <paramref name="deltaSeconds"/> should already be clamped to a sane
|
||||
/// per-frame bound by the caller (a huge delta — e.g. after a long
|
||||
/// pause — would otherwise jump the rate to 1 and snap anyway).
|
||||
/// </summary>
|
||||
internal static float EaseTowardTarget(
|
||||
float current,
|
||||
float target,
|
||||
float deltaSeconds,
|
||||
float transitionSeconds)
|
||||
{
|
||||
float rate = transitionSeconds <= 0f
|
||||
? 1f
|
||||
: Math.Clamp(deltaSeconds / transitionSeconds, 0f, 1f);
|
||||
return current + ((target - current) * rate);
|
||||
}
|
||||
|
||||
internal static float VolumetricShaft(
|
||||
IReadOnlyList<SunElevationResponsePoint>? points,
|
||||
float elevationDegrees,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
#extension GL_ARB_shader_draw_parameters : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
#include "atmospheric_common.glsl"
|
||||
#include "foliage_wind.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
|
|
@ -31,10 +33,19 @@ flat out uint vShadowTextureLayer;
|
|||
|
||||
void main() {
|
||||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
vec4 worldPosition = Instances[instanceIndex].transform * vec4(aPosition, 1.0);
|
||||
mat4 model = Instances[instanceIndex].transform;
|
||||
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vec4 worldPosition = model * vec4(aPosition, 1.0);
|
||||
// Campaign VM VM6: weather-driven foliage sway — see foliage_wind.glsl.
|
||||
// A no-op unless batch.flags carries the cutout (0x2) classification bit.
|
||||
worldPosition.xyz = acdreamFoliageDisplace(
|
||||
worldPosition.xyz,
|
||||
model[3].xyz,
|
||||
batch.flags,
|
||||
uAtmosphereClockWind,
|
||||
uAtmosphereWindAmplitude);
|
||||
gl_Position = uShadowWorldToClip[uRenderPass] * worldPosition;
|
||||
|
||||
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vShadowTexCoord = aTexCoord;
|
||||
vShadowTextureIndex = batch.textureIndex;
|
||||
vShadowTextureLayer = batch.textureLayer;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
#extension GL_EXT_multiview : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
#include "atmospheric_common.glsl"
|
||||
#include "foliage_wind.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
|
|
@ -29,9 +31,18 @@ flat out uint vShadowTextureLayer;
|
|||
|
||||
void main() {
|
||||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
vec4 worldPosition = Instances[instanceIndex].transform * vec4(aPosition, 1.0);
|
||||
gl_Position = uShadowWorldToClip[int(gl_ViewIndex)] * worldPosition;
|
||||
mat4 model = Instances[instanceIndex].transform;
|
||||
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vec4 worldPosition = model * vec4(aPosition, 1.0);
|
||||
// Campaign VM VM6: weather-driven foliage sway — see foliage_wind.glsl.
|
||||
// A no-op unless batch.flags carries the cutout (0x2) classification bit.
|
||||
worldPosition.xyz = acdreamFoliageDisplace(
|
||||
worldPosition.xyz,
|
||||
model[3].xyz,
|
||||
batch.flags,
|
||||
uAtmosphereClockWind,
|
||||
uAtmosphereWindAmplitude);
|
||||
gl_Position = uShadowWorldToClip[int(gl_ViewIndex)] * worldPosition;
|
||||
vShadowTexCoord = aTexCoord;
|
||||
vShadowTextureIndex = batch.textureIndex;
|
||||
vShadowTextureLayer = batch.textureLayer;
|
||||
|
|
|
|||
|
|
@ -2,20 +2,44 @@
|
|||
#extension GL_ARB_shader_draw_parameters : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
#include "atmospheric_common.glsl"
|
||||
#include "foliage_wind.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in vec2 aTexCoord;
|
||||
|
||||
struct InstanceData { mat4 transform; };
|
||||
// Campaign VM VM6: this pipeline had no BatchData binding before — an opaque
|
||||
// caster never needed a texture lookup. It reads batch.flags here purely for
|
||||
// the foliage-wind trunk classification bit (0x4); textureIndex/textureLayer
|
||||
// stay unread.
|
||||
struct BatchData {
|
||||
uint textureIndex;
|
||||
uint _pad;
|
||||
uint textureLayer;
|
||||
uint flags;
|
||||
};
|
||||
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
||||
InstanceData Instances[];
|
||||
};
|
||||
layout(std430, binding = 1) readonly buffer BatchBuffer {
|
||||
BatchData Batches[];
|
||||
};
|
||||
|
||||
uniform int uDrawIDOffset;
|
||||
uniform int uRenderPass;
|
||||
|
||||
void main() {
|
||||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
vec4 worldPosition = Instances[instanceIndex].transform * vec4(aPosition, 1.0);
|
||||
mat4 model = Instances[instanceIndex].transform;
|
||||
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vec4 worldPosition = model * vec4(aPosition, 1.0);
|
||||
worldPosition.xyz = acdreamFoliageDisplace(
|
||||
worldPosition.xyz,
|
||||
model[3].xyz,
|
||||
batch.flags,
|
||||
uAtmosphereClockWind,
|
||||
uAtmosphereWindAmplitude);
|
||||
gl_Position = uShadowWorldToClip[uRenderPass] * worldPosition;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,43 @@
|
|||
#extension GL_EXT_multiview : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
#include "atmospheric_common.glsl"
|
||||
#include "foliage_wind.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in vec2 aTexCoord;
|
||||
|
||||
struct InstanceData { mat4 transform; };
|
||||
// Campaign VM VM6: this pipeline had no BatchData binding before — an opaque
|
||||
// caster never needed a texture lookup. It reads batch.flags here purely for
|
||||
// the foliage-wind trunk classification bit (0x4); textureIndex/textureLayer
|
||||
// stay unread.
|
||||
struct BatchData {
|
||||
uint textureIndex;
|
||||
uint _pad;
|
||||
uint textureLayer;
|
||||
uint flags;
|
||||
};
|
||||
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
||||
InstanceData Instances[];
|
||||
};
|
||||
layout(std430, binding = 1) readonly buffer BatchBuffer {
|
||||
BatchData Batches[];
|
||||
};
|
||||
|
||||
uniform int uDrawIDOffset;
|
||||
|
||||
void main() {
|
||||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
vec4 worldPosition = Instances[instanceIndex].transform * vec4(aPosition, 1.0);
|
||||
mat4 model = Instances[instanceIndex].transform;
|
||||
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vec4 worldPosition = model * vec4(aPosition, 1.0);
|
||||
worldPosition.xyz = acdreamFoliageDisplace(
|
||||
worldPosition.xyz,
|
||||
model[3].xyz,
|
||||
batch.flags,
|
||||
uAtmosphereClockWind,
|
||||
uAtmosphereWindAmplitude);
|
||||
gl_Position = uShadowWorldToClip[int(gl_ViewIndex)] * worldPosition;
|
||||
}
|
||||
|
|
|
|||
91
src/AcDream.App/Rendering/Shaders/foliage_wind.glsl
Normal file
91
src/AcDream.App/Rendering/Shaders/foliage_wind.glsl
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#ifndef ACDREAM_FOLIAGE_WIND_GLSL
|
||||
#define ACDREAM_FOLIAGE_WIND_GLSL
|
||||
|
||||
// Campaign VM VM6 (2026-08-22): weather-driven foliage sway for procedural
|
||||
// scenery (trees/bushes — see ProceduralSceneryIdAllocator, bit 31 of the
|
||||
// entity id). Shared verbatim between mesh_atmospheric.vert (the world
|
||||
// receiver) and the four directional_shadow_world_*.vert casters so a
|
||||
// displaced leaf's shadow moves with it by construction — the caster and
|
||||
// receiver read the identical uAtmosphereClockWind/uAtmosphereWindAmplitude
|
||||
// inputs from atmospheric_common.glsl and call this exact function.
|
||||
//
|
||||
// batchFlags bit 1 (0x2, FOLIAGE_CUTOUT) marks an alpha-cutout leaf/frond
|
||||
// subset of a procedural-scenery entity; bit 2 (0x4, FOLIAGE_TRUNK) marks an
|
||||
// opaque subset of a procedural-scenery entity that also owns a cutout
|
||||
// subset (a tree trunk, not a rock). Both bits are computed once in
|
||||
// WbDrawDispatcher.ClassifyBatches / AddDirectionalShadowBatches (host
|
||||
// C#) — this include never re-derives them from geometry.
|
||||
//
|
||||
// Motion model (see docs/plans/2026-08-22-visualmaster-campaign.md's VM6
|
||||
// section for the full derivation): the whole tree leans slowly with the
|
||||
// mean+gust wind, scaled by height-squared so the base never moves; cutout
|
||||
// subsets additionally get a faster branch swing and a fast, per-vertex
|
||||
// decorrelated flutter. Bend shortens the vertical extent (p.z -= ...) so a
|
||||
// bent canopy sinks slightly instead of stretching — the cheap
|
||||
// length-preserving correction. Normals are NOT rotated: Gouraud on a
|
||||
// flipped-normal cutout leaf would flicker, and AC's flat-lit foliage does
|
||||
// not need it.
|
||||
//
|
||||
// Budget: ~25 ALU ops per foliage vertex (early-out for every non-foliage
|
||||
// vertex — one uint AND and a branch), zero CPU cost, zero extra draw
|
||||
// submissions. See FoliageWindModel (AcDream.App.Rendering.Wb) for the
|
||||
// bit-exact CPU mirror used by the hermetic conformance tests.
|
||||
vec3 acdreamFoliageDisplace(
|
||||
vec3 worldPos,
|
||||
vec3 instanceOrigin,
|
||||
uint batchFlags,
|
||||
vec4 clockWind,
|
||||
vec4 amp)
|
||||
{
|
||||
if ((batchFlags & 0x6u) == 0u)
|
||||
return worldPos;
|
||||
|
||||
float t = clockWind.x;
|
||||
float mean = clockWind.y;
|
||||
float gust = clockWind.z;
|
||||
float dirA = clockWind.w;
|
||||
|
||||
vec2 dir = vec2(cos(dirA), sin(dirA));
|
||||
vec2 perp = vec2(-dir.y, dir.x);
|
||||
|
||||
// 0 at the instance's own base, 1 at the declared canopy top. Height is
|
||||
// measured from the instance ORIGIN (the translation column of its
|
||||
// transform), not the vertex's local Z, so a leaned tree's own leaves
|
||||
// still measure height from the trunk base correctly.
|
||||
float h = clamp((worldPos.z - instanceOrigin.z) / max(amp.w, 0.5), 0.0, 1.0);
|
||||
float k = h * h;
|
||||
|
||||
// Per-tree phase from world position so neighbouring trees fall out of
|
||||
// step with each other.
|
||||
float ph = dot(instanceOrigin.xy, vec2(0.137, 0.291));
|
||||
|
||||
// Gust envelope: two slow sines beat against each other so gusts arrive
|
||||
// and leave rather than pulsing at one fixed period.
|
||||
float g = 0.5 + 0.5 * sin(0.05 * t + ph) + 0.25 * sin(0.13 * t + 1.7 * ph);
|
||||
float s = mean + gust * g;
|
||||
|
||||
// Slow whole-tree lean, height-squared scaled.
|
||||
float lean = k * amp.x * s * (0.8 + 0.2 * sin(0.35 * t + ph));
|
||||
vec2 d = dir * lean;
|
||||
|
||||
if ((batchFlags & 0x2u) != 0u)
|
||||
{
|
||||
// Cutout (leaves): branch swing at ~1 Hz with a phase that runs up
|
||||
// the tree, plus a fast per-vertex flutter decorrelated by a hash of
|
||||
// the vertex's own world XY so leaves on the same tree do not move
|
||||
// in lockstep.
|
||||
float branch = k * amp.y * s * sin(1.1 * t + ph + 2.0 * h);
|
||||
float vh = fract(sin(dot(worldPos.xy, vec2(12.9898, 78.233))) * 43758.5453);
|
||||
float flutter = h * amp.z * s * sin(6.0 * t + 7.0 * vh);
|
||||
d += dir * branch + perp * 0.35 * branch
|
||||
+ vec2(cos(6.2832 * vh), sin(6.2832 * vh)) * flutter;
|
||||
}
|
||||
|
||||
vec3 p = worldPos;
|
||||
p.xy += d;
|
||||
// Bend shortens the vertical extent instead of stretching it.
|
||||
p.z -= 0.5 * dot(d, d) / max(h * amp.w, 0.5);
|
||||
return p;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
#extension GL_ARB_shader_draw_parameters : require
|
||||
|
||||
#include "directional_shadow_common.glsl"
|
||||
#include "atmospheric_common.glsl"
|
||||
#include "foliage_wind.glsl"
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
|
|
@ -324,7 +326,21 @@ void main() {
|
|||
? instanceSelectionLighting[instanceIndex]
|
||||
: vec2(0.0, 1.0);
|
||||
|
||||
BatchData b = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
|
||||
vec4 worldPos = model * vec4(aPosition, 1.0);
|
||||
// Campaign VM VM6: weather-driven foliage sway. acdreamFoliageDisplace is
|
||||
// a no-op unless b.flags carries the cutout (0x2) or trunk (0x4)
|
||||
// classification bit — see foliage_wind.glsl. Applied before gl_Position
|
||||
// so clip distances, lighting, and the fragment stage all see the
|
||||
// displaced position; the four directional-shadow caster vertex shaders
|
||||
// call the identical include so the shadow moves with the same vertex.
|
||||
worldPos.xyz = acdreamFoliageDisplace(
|
||||
worldPos.xyz,
|
||||
model[3].xyz,
|
||||
b.flags,
|
||||
uAtmosphereClockWind,
|
||||
uAtmosphereWindAmplitude);
|
||||
gl_Position = uViewProjection * worldPos;
|
||||
|
||||
// Phase U.3: per-instance clip gate. instanceClipSlot is indexed by the
|
||||
|
|
@ -354,10 +370,11 @@ void main() {
|
|||
: 0u;
|
||||
vTexCoord = aTexCoord;
|
||||
|
||||
BatchData b = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
// Campaign V slice V6e: forward the table SLOT untouched. V2 looked the
|
||||
// handle up here and passed the handle; the lookup now lives at the sample
|
||||
// site in mesh_modern.frag, which is the only form Vulkan can express.
|
||||
// (b was fetched earlier, before worldPos, so acdreamFoliageDisplace
|
||||
// could read its flags — Campaign VM VM6.)
|
||||
vTextureIndex = b.textureIndex;
|
||||
vTextureLayer = b.textureLayer;
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -151,7 +151,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "8331583c1d63ee59b3f7898e25b2e9730df98fd9da28273661caad948fb46ae5",
|
||||
"sourceSha256": "fe67e9bff0e528b08fa699f0be17d1c49660ad63ab34f7ac7dd31f79a2253006",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -167,7 +167,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "a4bb3b86283af310a22d3943dd1afadb8a72b42c9e5501efc9abf2b5124a1446",
|
||||
"sourceSha256": "2a155a7964ff15bd936394b8ffd3a331d5598e140ca36268759520bab86b1bdc",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -183,7 +183,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "c2586df5f09518c40d052bbacdfff2c00a82b1eefec4868749d0580571198067",
|
||||
"sourceSha256": "74b1aa64396dd74185d644de7e9f40626c9422f335da60deb7c606f2a45d92c2",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -199,7 +199,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "35b4d524153623691ab523a049212aa35551f96c169c28c08f62d93e31d9e68d",
|
||||
"sourceSha256": "f15425837f7a8f84337f4493989bcd7cb1af87f916f7678fc016947f68d26be5",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
@ -215,7 +215,7 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "d4f8bc12ee84379ced84f5b703cf8be95798a23a7063773e214bc1eb6ebbb65e",
|
||||
"sourceSha256": "7e3e49483024145bb71929f715a68087380e5126e15b5bef2c1c5c7d5e2e3516",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,7 +27,15 @@ internal readonly record struct CachedBatch(
|
|||
Matrix4x4 RestPose,
|
||||
Vector3 LocalSortCenter = default,
|
||||
WbDrawDispatcher.InstanceGroup? Group = null,
|
||||
long GroupRegistration = 0);
|
||||
long GroupRegistration = 0,
|
||||
// Campaign VM VM6: the foliage-wind classification bits ClassifyBatches
|
||||
// computed for this subset (0 for anything that isn't procedural-scenery
|
||||
// foliage). Preserved here — not just on the live InstanceGroup — so a
|
||||
// cache-hit replay that recreates an evicted InstanceGroup (see
|
||||
// WbDrawDispatcher.ApplyCacheHitDirect's stale-registration path)
|
||||
// re-stamps the correct flags instead of leaving a freshly-recreated
|
||||
// group at its zero default.
|
||||
uint FoliageFlags = 0u);
|
||||
|
||||
/// <summary>
|
||||
/// Immutable retail-picking descriptor for one static entity part. The cache
|
||||
|
|
|
|||
62
src/AcDream.App/Rendering/Wb/FoliageWindClassification.cs
Normal file
62
src/AcDream.App/Rendering/Wb/FoliageWindClassification.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using AcDream.Core.Meshing;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign VM VM6: the two <c>BatchData.flags</c> bits that mark a world
|
||||
/// draw batch as swaying foliage — bit 0 remains #226's built-mesh marker
|
||||
/// (see <c>mesh_detail.frag</c>'s <c>vBatchFlags & 1u</c> check) and is
|
||||
/// never touched here. <c>mesh_atmospheric.vert</c> and the four
|
||||
/// <c>directional_shadow_world_*.vert</c> caster shaders read these bits
|
||||
/// through the shared <c>foliage_wind.glsl</c> include
|
||||
/// (<c>acdreamFoliageDisplace</c>'s <c>batchFlags & 0x6u</c> gate); the
|
||||
/// retail <c>mesh_modern</c> pipelines never read them, so setting them does
|
||||
/// not change pack-off output.
|
||||
/// </summary>
|
||||
internal static class FoliageWindClassification
|
||||
{
|
||||
/// <summary>
|
||||
/// Bit 1: the subset is an alpha-cutout/ClipMap material AND its owning
|
||||
/// entity is procedural scenery (see <see cref="Classify"/>). Leaves,
|
||||
/// fronds, bushes, grass tufts.
|
||||
/// </summary>
|
||||
internal const uint CutoutFoliageFlag = 0x2u;
|
||||
|
||||
/// <summary>
|
||||
/// Bit 2: the subset is opaque AND its owning entity is procedural
|
||||
/// scenery AND that same entity owns at least one cutout subset — a
|
||||
/// tree trunk or branch, not a rock (rocks have no cutout subset).
|
||||
/// </summary>
|
||||
internal const uint TrunkFlag = 0x4u;
|
||||
|
||||
private const uint SceneryEntityIdBit = 0x8000_0000u;
|
||||
|
||||
/// <summary>
|
||||
/// Pure classification: no allocation, no scan. <paramref name="meshHasCutoutSubset"/>
|
||||
/// is expected to be computed once per GfxObj/Setup mesh and cached with
|
||||
/// the mesh record (<see cref="AcDream.App.Rendering.Wb.ObjectRenderData.HasCutoutSubset"/>),
|
||||
/// not recomputed per call.
|
||||
/// </summary>
|
||||
internal static uint Classify(
|
||||
uint entityId,
|
||||
bool isExcluded,
|
||||
TranslucencyKind translucency,
|
||||
bool meshHasCutoutSubset)
|
||||
{
|
||||
if (!IsProceduralScenery(entityId) || isExcluded)
|
||||
return 0u;
|
||||
if (translucency == TranslucencyKind.ClipMap)
|
||||
return CutoutFoliageFlag;
|
||||
if (translucency == TranslucencyKind.Opaque && meshHasCutoutSubset)
|
||||
return TrunkFlag;
|
||||
return 0u;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bit 31 of the entity id — see <c>ProceduralSceneryIdAllocator</c>'s
|
||||
/// <c>0x8XXYYIII</c> namespace. No other consumer decodes more than this
|
||||
/// one bit.
|
||||
/// </summary>
|
||||
internal static bool IsProceduralScenery(uint entityId) =>
|
||||
(entityId & SceneryEntityIdBit) != 0u;
|
||||
}
|
||||
|
|
@ -40,6 +40,19 @@ namespace AcDream.App.Rendering.Wb
|
|||
public uint VBO { get; set; }
|
||||
public int VertexCount { get; set; }
|
||||
public List<ObjectRenderBatch> Batches { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Campaign VM VM6: true when at least one of <see cref="Batches"/>
|
||||
/// is an alpha-cutout (<c>TranslucencyKind.ClipMap</c>) material —
|
||||
/// computed once here, when the mesh is built/cached, not re-scanned
|
||||
/// per instance or per frame. Used by
|
||||
/// <see cref="FoliageWindClassification.Classify"/> to tell a tree's
|
||||
/// opaque trunk (a procedural-scenery entity whose mesh has a cutout
|
||||
/// subset) apart from a rock (a procedural-scenery entity whose mesh
|
||||
/// has none).
|
||||
/// </summary>
|
||||
public bool HasCutoutSubset { get; set; }
|
||||
|
||||
internal GlobalMeshAllocation? GlobalAllocation { get; set; }
|
||||
public bool IsSetup { get; set; }
|
||||
public List<(ulong GfxObjId, Matrix4x4 Transform)> SetupParts { get; set; } = new();
|
||||
|
|
@ -2154,6 +2167,9 @@ namespace AcDream.App.Rendering.Wb
|
|||
{
|
||||
VertexCount = meshData.Vertices.Length,
|
||||
Batches = renderBatches,
|
||||
HasCutoutSubset = renderBatches.Any(
|
||||
static batch => batch.Translucency
|
||||
== AcDream.Core.Meshing.TranslucencyKind.ClipMap),
|
||||
GlobalAllocation = globalAllocation,
|
||||
ParticleEmitters = meshData.ParticleEmitters,
|
||||
DIDDegrade = meshData.DIDDegrade,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,12 @@ internal readonly record struct DirectionalShadowPreparedBatch(
|
|||
GpuTextureSlot TextureSlot,
|
||||
uint TextureLayer,
|
||||
CullMode CullMode,
|
||||
DirectionalShadowCasterMaterial Material);
|
||||
DirectionalShadowCasterMaterial Material,
|
||||
// Campaign VM VM6: FoliageWindClassification.CutoutFoliageFlag /
|
||||
// TrunkFlag — the identical word WbDrawDispatcher.ClassifyBatches
|
||||
// computed for the matching world-receiver subset, so the caster and
|
||||
// receiver agree by construction (see foliage_wind.glsl).
|
||||
uint FoliageFlags = 0u);
|
||||
|
||||
internal readonly record struct DirectionalShadowPreparedRun(
|
||||
int StartCommand,
|
||||
|
|
@ -279,7 +284,8 @@ internal sealed class DirectionalShadowPreparedDraws
|
|||
uint textureLayer,
|
||||
CullMode cullMode,
|
||||
DirectionalShadowCasterMaterial material,
|
||||
in Matrix4x4 transform)
|
||||
in Matrix4x4 transform,
|
||||
uint foliageFlags = 0u)
|
||||
{
|
||||
DirectionalShadowTransformSource source = default;
|
||||
Add(
|
||||
|
|
@ -291,7 +297,8 @@ internal sealed class DirectionalShadowPreparedDraws
|
|||
cullMode,
|
||||
material,
|
||||
in transform,
|
||||
in source);
|
||||
in source,
|
||||
foliageFlags);
|
||||
}
|
||||
|
||||
public void Add(
|
||||
|
|
@ -303,7 +310,8 @@ internal sealed class DirectionalShadowPreparedDraws
|
|||
CullMode cullMode,
|
||||
DirectionalShadowCasterMaterial material,
|
||||
in Matrix4x4 transform,
|
||||
in DirectionalShadowTransformSource transformSource)
|
||||
in DirectionalShadowTransformSource transformSource,
|
||||
uint foliageFlags = 0u)
|
||||
{
|
||||
if (!_building)
|
||||
throw new InvalidOperationException(
|
||||
|
|
@ -331,7 +339,8 @@ internal sealed class DirectionalShadowPreparedDraws
|
|||
? textureLayer
|
||||
: 0u,
|
||||
cullMode,
|
||||
material),
|
||||
material,
|
||||
foliageFlags),
|
||||
transform,
|
||||
transformSource);
|
||||
}
|
||||
|
|
@ -433,7 +442,8 @@ internal sealed class DirectionalShadowPreparedDraws
|
|||
key.TextureSlot,
|
||||
key.TextureLayer,
|
||||
key.CullMode,
|
||||
key.Material);
|
||||
key.Material,
|
||||
key.FoliageFlags);
|
||||
if (key.Material is DirectionalShadowCasterMaterial.Opaque)
|
||||
opaqueCommands++;
|
||||
commandIndex++;
|
||||
|
|
@ -860,7 +870,11 @@ internal sealed class DirectionalShadowPreparedDraws
|
|||
GpuTextureSlot TextureSlot,
|
||||
uint TextureLayer,
|
||||
CullMode CullMode,
|
||||
DirectionalShadowCasterMaterial Material);
|
||||
DirectionalShadowCasterMaterial Material,
|
||||
// Campaign VM VM6: part of the sort/group key (not just the prepared
|
||||
// batch payload) so two subsets that would otherwise share a group
|
||||
// never coalesce under different foliage classification.
|
||||
uint FoliageFlags = 0u);
|
||||
|
||||
private readonly record struct DirectionalShadowSourceDraw(
|
||||
DirectionalShadowDrawKey Key,
|
||||
|
|
@ -889,9 +903,18 @@ internal sealed class DirectionalShadowPreparedDraws
|
|||
order = x.IndexCount.CompareTo(y.IndexCount);
|
||||
if (order != 0) return order;
|
||||
order = x.TextureSlot.Index.CompareTo(y.TextureSlot.Index);
|
||||
if (order != 0) return order;
|
||||
order = x.TextureLayer.CompareTo(y.TextureLayer);
|
||||
// Campaign VM VM6: tie-break on FoliageFlags so entries sharing
|
||||
// every other key field but differing only in classification
|
||||
// (the rare case a mesh subset is reachable from both a
|
||||
// procedural-scenery and a non-scenery placement) still sort
|
||||
// into one contiguous, exact-key-matched run instead of an
|
||||
// unstable-sort-dependent scatter. The grouping loop below keys
|
||||
// on exact DirectionalShadowDrawKey equality regardless.
|
||||
return order != 0
|
||||
? order
|
||||
: x.TextureLayer.CompareTo(y.TextureLayer);
|
||||
: x.FoliageFlags.CompareTo(y.FoliageFlags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1171,6 +1194,17 @@ public sealed partial class WbDrawDispatcher
|
|||
textureLayer = texture.Layer;
|
||||
}
|
||||
|
||||
// Campaign VM VM6: the identical classification
|
||||
// WbDrawDispatcher.ClassifyBatches computes for the matching
|
||||
// world-receiver subset, so the caster and receiver agree by
|
||||
// construction — the shadow moves with the same displaced
|
||||
// vertex the receiver draws.
|
||||
uint foliageFlags = FoliageWindClassification.Classify(
|
||||
candidate.LocalEntityId,
|
||||
FoliageWindExclusions.Contains(meshRef.GfxObjId),
|
||||
batch.Translucency,
|
||||
renderData.HasCutoutSubset);
|
||||
|
||||
_directionalShadowDraws.Add(
|
||||
batch.FirstIndex,
|
||||
checked((int)batch.BaseVertex),
|
||||
|
|
@ -1180,7 +1214,8 @@ public sealed partial class WbDrawDispatcher
|
|||
batch.CullMode,
|
||||
material,
|
||||
in model,
|
||||
in transformSource);
|
||||
in transformSource,
|
||||
foliageFlags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -483,6 +483,20 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
/// </summary>
|
||||
public bool AlphaToCoverage { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign VM VM6: object/GfxObj ids the active render pack's
|
||||
/// <c>AtmospherePolicyDeclaration.FoliageExclusions</c> declares —
|
||||
/// scenery meshes that must never sway even though their entity id and
|
||||
/// material would otherwise classify them as foliage. Empty (no
|
||||
/// exclusions) when no atmospheric pack is active. Assigned every frame
|
||||
/// by <c>AtmosphericPostProcessGraph.RenderDirectionalShadows</c> from
|
||||
/// the currently-selected pack's declaration — self-contained by
|
||||
/// construction, so a pack switch or deactivation can never leave a
|
||||
/// stale exclusion set applied.
|
||||
/// </summary>
|
||||
public IReadOnlySet<uint> FoliageWindExclusions { get; set; } =
|
||||
System.Collections.Frozen.FrozenSet<uint>.Empty;
|
||||
|
||||
// Phase U.3: per-instance clip-slot data (binding=3 on the RHI ring). One
|
||||
// uint per instance selecting its CellClip slot. In U.3 this is ALL ZEROS
|
||||
// (every instance → slot 0 → no-clip), so the render is identical to
|
||||
|
|
@ -2292,7 +2306,8 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
TextureIndex: g.TextureSlot.Index,
|
||||
TextureLayer: g.TextureLayer,
|
||||
Translucency: g.Translucency,
|
||||
CullMode: g.CullMode);
|
||||
CullMode: g.CullMode,
|
||||
FoliageFlags: g.FoliageFlags);
|
||||
|
||||
internal readonly record struct InstanceLayoutCounts(
|
||||
int VisibleInstances,
|
||||
|
|
@ -2981,11 +2996,15 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
internal static void ApplyCacheHit(
|
||||
EntityCacheEntry entry,
|
||||
Matrix4x4 entityWorld,
|
||||
Action<GroupKey, Matrix4x4, Vector3> appendInstance)
|
||||
Action<GroupKey, Matrix4x4, Vector3, uint> appendInstance)
|
||||
{
|
||||
foreach (var cached in entry.Batches)
|
||||
{
|
||||
appendInstance(cached.Key, cached.RestPose * entityWorld, cached.LocalSortCenter);
|
||||
appendInstance(
|
||||
cached.Key,
|
||||
cached.RestPose * entityWorld,
|
||||
cached.LocalSortCenter,
|
||||
cached.FoliageFlags);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3014,7 +3033,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
GroupRegistration = group.Registration,
|
||||
};
|
||||
}
|
||||
AppendInstanceToGroup(group!, model, cached.LocalSortCenter);
|
||||
AppendInstanceToGroup(group!, model, cached.LocalSortCenter, cached.FoliageFlags);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3125,10 +3144,14 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
/// creates an <see cref="InstanceGroup"/> for the given key in
|
||||
/// <c>_groups</c> and appends the per-instance world matrix.
|
||||
/// </summary>
|
||||
private void AppendInstanceToGroup(GroupKey key, Matrix4x4 model, Vector3 localSortCenter)
|
||||
private void AppendInstanceToGroup(
|
||||
GroupKey key,
|
||||
Matrix4x4 model,
|
||||
Vector3 localSortCenter,
|
||||
uint foliageFlags)
|
||||
{
|
||||
InstanceGroup grp = GetOrCreateInstanceGroup(key);
|
||||
AppendInstanceToGroup(grp, model, localSortCenter);
|
||||
AppendInstanceToGroup(grp, model, localSortCenter, foliageFlags);
|
||||
}
|
||||
|
||||
private InstanceGroup GetOrCreateInstanceGroup(GroupKey key)
|
||||
|
|
@ -3164,9 +3187,16 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
private void AppendInstanceToGroup(
|
||||
InstanceGroup grp,
|
||||
Matrix4x4 model,
|
||||
Vector3 localSortCenter)
|
||||
Vector3 localSortCenter,
|
||||
uint foliageFlags)
|
||||
{
|
||||
grp.LastUsedFrame = _groupFrame;
|
||||
// Campaign VM VM6: re-stamp on every append (idempotent — the value
|
||||
// is a deterministic function of the mesh subset the group's key
|
||||
// already identifies) so a stale-registration replay that recreates
|
||||
// an evicted InstanceGroup (see ApplyCacheHitDirect above) never
|
||||
// leaves it at its zero default.
|
||||
grp.FoliageFlags = foliageFlags;
|
||||
grp.Matrices.Add(model);
|
||||
grp.LocalSortCenters.Add(localSortCenter);
|
||||
grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++);
|
||||
|
|
@ -3351,13 +3381,23 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
|
||||
grp.Opacities.Add(opacityMultiplier); // #188 — parallel to Matrices
|
||||
grp.SelectionLighting.Add(_currentEntitySelectionLighting);
|
||||
// Campaign VM VM6: classify from the RAW (pre-#188-promotion)
|
||||
// batch.Translucency — a mid-fade trunk is still a trunk, it
|
||||
// just landed in the alpha-blend group instead of opaque.
|
||||
uint foliageFlags = FoliageWindClassification.Classify(
|
||||
entity.LocalEntityId,
|
||||
FoliageWindExclusions.Contains(meshRef.GfxObjId),
|
||||
batch.Translucency,
|
||||
renderData.HasCutoutSubset);
|
||||
grp.FoliageFlags = foliageFlags;
|
||||
collector?.Add(new CachedBatch(
|
||||
key,
|
||||
texSlot,
|
||||
restPose,
|
||||
renderData.SortCenter,
|
||||
grp,
|
||||
grp.Registration));
|
||||
grp.Registration,
|
||||
foliageFlags));
|
||||
}
|
||||
return allTexturesReady;
|
||||
}
|
||||
|
|
@ -3565,7 +3605,11 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
uint TextureIndex,
|
||||
uint TextureLayer,
|
||||
TranslucencyKind Translucency,
|
||||
CullMode CullMode = CullMode.CounterClockwise);
|
||||
CullMode CullMode = CullMode.CounterClockwise,
|
||||
// Campaign VM VM6: FoliageWindClassification.CutoutFoliageFlag /
|
||||
// TrunkFlag, OR'd into BatchDataPublic.Flags alongside #226's bit 0
|
||||
// by BuildIndirectArrays. Zero for every non-foliage group.
|
||||
uint FoliageFlags = 0u);
|
||||
|
||||
/// <summary>
|
||||
/// Public mirror of the per-group <see cref="BatchData"/> uploaded to the SSBO.
|
||||
|
|
@ -3631,7 +3675,10 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
// #226: this is the built-mesh path. Retail DrawMesh passes
|
||||
// curr_detail_surface through RenderMeshSubset for opaque,
|
||||
// ClipMap, alpha, additive and inverse-alpha subsets alike.
|
||||
Flags = 1u,
|
||||
// Campaign VM VM6: bits 1/2 (FoliageWindClassification) ride
|
||||
// in the same word — mesh_modern never reads them, so this
|
||||
// is a no-op for the retail pack-off path.
|
||||
Flags = 1u | g.FoliageFlags,
|
||||
};
|
||||
|
||||
if (IsOpaque(g.Translucency))
|
||||
|
|
@ -3798,6 +3845,17 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
public CullMode CullMode;
|
||||
public int FirstInstance; // offset into the shared instance VBO (in instances, not bytes)
|
||||
public int InstanceCount;
|
||||
|
||||
// Campaign VM VM6: foliage-wind classification bits for every
|
||||
// instance in this group (0x2 cutout / 0x4 trunk / 0 neither), OR'd
|
||||
// into BatchData.flags alongside #226's bit 0 at BuildIndirectArrays.
|
||||
// Group-level, not per-instance, because BatchData is read once per
|
||||
// draw call (Batches[gl_DrawIDARB]) — every instance sharing one
|
||||
// mesh-subset draw shares its classification. Set by ClassifyBatches
|
||||
// on a fresh classification and re-stamped by AppendInstanceToGroup
|
||||
// on every cache-hit replay (see CachedBatch.FoliageFlags).
|
||||
public uint FoliageFlags;
|
||||
|
||||
public float SortDistance; // squared distance from camera to first instance, for opaque sort
|
||||
public readonly List<Matrix4x4> Matrices = new();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue