The shader dithered and wrote depth, but its one pass had no LightMode, so under the deferred
camera it drew in the forward pass, after the depth texture is taken from the G-buffer. The
nebula march read no depth there and fogged straight over every dithered particle (missile
trails, explosions, lasers, guns). Same cause and cure as Stardust (2e9d97661): black,
unreflective and emissive in the G-buffer. The colour pass is now ForwardBase only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At BeforeImageEffectsOpaque the dust was drawn after the depth texture the nebula march reads, so the fog treated dust pixels as far background and wiped them out (operator report, 2026-10-29). A Deferred pass writes black, unreflective, emissive G-buffer texels and depth at AfterGBuffer. FlowFieldSceneVerify still expects the old event and needs updating.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rock height leaves the shader; DrawAhead owns it (next commit). Does not build alone:
ZoneRenderer and ActionGameManager still reference the deleted names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One zone node at the massive roots' centre of mass owns the only global term (SubzoneReach is the one place the subzone shape is decided: isolated-subzones). WellNode gains Extent and Anchor; a belt orbit's extent is its outermost asteroid. GravityField.SeriesFrom = 0.4 is emitted into the shader template and Gravity.cginc. Tests: the spec's WellLawTests; the old seam test is replaced by TheBandPotentialIsExactInSinglePrecision. GravityFieldParityVerify runs the multi-root fixture (second sun, moon inside the planet's Roche limit, far window): PASS, largest drawn tau 1.228, max error 0.01028 u. The dotnet suite has not been run at this commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gravity.cginc is GravityShader.Source() output. OrbitsFromFieldTests spell periods from GravityField.Pull composed as the law
(binary members feel the partner's pull at the chord, the outer body feels c(total mass)). Fixtures whose bodies carried
mass 0 under constant curves (WellFixture, the mining scenes) now carry a mass: the tree conserves mass. DrawnPoseTests'
rails use the Plummer pull. Builds; not yet run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The nebula is an [ImageEffectOpaque] OnRenderImage; a command buffer at BeforeForwardAlpha runs after its composite, so the
dust landed on top of the fog (B49). BeforeImageEffectsOpaque runs before it on the deferred camera. FlowFieldSceneVerify
now requires one Stardust buffer at the new event and none at BeforeForwardAlpha, and scene-stardust-under-fog compares the
dust's contribution with the cloud renderer off and on in an HDR target, with a fog control.
All commits build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both target cameras are deferred, so the AfterForwardOpaque buffer never drew. The render half of scene-stardust-uncullable now attaches-checks BeforeForwardAlpha, renders with cullingMask 1 (DrawProcedural is layer 0), and requires a near control (dust 100 below, size 5) to draw before the far probe is read, so a miss at 10000 can only be culling.
Builds: not compiled by this commit alone; verified in the Unity batchmode run reported with the cut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ambient Fog Light leaves Zone Brushes (root object, same pose) and Sector Tint.mat moves from
PowerBrush to Brushes/Ambient Brush: brightness falls as exp(-x^2 ln(1/_EdgeBrightness)), x in
units of Zone.PopulatedRadius. ZoneRenderer.FitAmbientLight, called by LoadZone, is the one
writer of the quad's size and _Reach (the radius where it is 1e-5 of the centre). AmbientEdgeVerify
renders the real scene object through that call. Builds in Unity 6000.3.24f1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deletes the bounded Graphics.DrawProcedural, its hand-inflated Bounds, the GravityCamera field (and its scene reference) and the dead OnRenderObject block. The camera replays the buffer at AfterForwardOpaque; it is attached on enable, detached on disable and destroy, and released. Unity compile not yet run for this commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VolumeRender and VolumeRenderCustom shaders, the four Fog Marching
materials, ZoneRenderer's FogMaterial/FogFarFadeFraction and its
_DepthCeiling/_DepthBlend writes, the ARPG scene values, and the
Volumetric.cginc _TintExponent, texFlow and tintGradient (with its
commented-out caller). The live nebula is VolumeCloudRenderer + CloudShader.
Unity compile not yet run on this commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Patch Displacement no longer culls layer 9; cloudDensity adds the surface displacement to the patch-only displacement.
GravityFieldParityVerify wiring checks the camera. Neither commit has been built when this is written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mirror test compared a normalised reading of two spellings and was beaten twice (a statement before the return, gravityHeight's sum). GravityShader writes the whole file (potential, zero-strength guard, reference term, height sum) from GravityField's constant (TailOffset); GravityShaderTests fails on any difference. GravityShaderMirrorTests deleted. GravityFieldParityVerify opens Main Menu.unity: no ZoneRenderer there, its Fog Surface Displacement camera is the menu's own brush presentation (layers 9 and 10) by spec.
Builds not yet verified on this commit; verified at the final merge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zone.WellReference owns each body's reference zero (GetHeight uses it). Gravity.cginc mirrors GravityField.Potential;
Brushes/Gravity Field draws depth = -h on layer 9 from a StructuredBuffer that GravityLowering.Lower fills from the
zone each frame; GravityLowering.CreateField makes the one quad. GravityFieldParityVerify renders it and compares every
texel with -Zone.GetHeight (max error 0.0059 u), the gradient with GetForce (0.03 degrees) and the wiring.
Deleted: the Gravity Well children of Planet, Gas Giant, Sun and PlanetOutpost prefabs, Wormhole 1's three gravity
children, their overrides in Main Menu and TestScene, five wormhole/wave materials and two brush shaders,
Planet.GravityWellDepth/Radius, BodyData keys 6-7, PlanetSettings.GravityDepth/GravityRadius (and the Settings.asset
keys). Surface Displacement culls layer 9 only. Headless build and tests not yet run at this commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FlowFieldSceneVerify opens ARPG.unity before anything touches the project's types, drives VolumeSampling and Stardust
Start/Update with a zone attached, and asserts no exception or logged error, FLOW_GLOBAL and FLOW_SLOPE enabled,
_FlowAmplitude non-zero, the stardust buffer changing with the flow and freezing with a paused zone, the noise clock,
and CloudShader pass 0 carrying the FLOW_* keywords. FlowFieldParityVerify now binds a ramp height so FLOW_SLOPE and its
pragma matter, and checks the noise follows _NebulaNoiseTime. The mutation harness runs both checks, with six new mutants.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
VolumeSampling created its flow keywords in static field initializers, which a scene runs inside a MonoBehaviour
construction where Unity refuses GlobalKeyword.Create; they are now created lazily from LowerFlow. FlowField.Scroll and
Cycle return phases strictly below 1. The nebula noise reads _NebulaNoiseTime (zone time) instead of _Time.y. New
FlowFieldTests pin the per-octave fold to the unfolded law across wraps, the cycle at days of zone time, and the phase edge.
The Unity checks for these are in the next commit; this one builds, its parity check does not yet read the noise clock.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- VolumeSampling registers FLOW_GLOBAL / FLOW_SLOPE with GlobalKeyword.Create (the constructor only looks up).
- FlowField.Scroll folds each Tri3D octave's phase modulo 1 in double (float2, [0,1)); Zone.Flow evaluates at Zone.FlowScroll.
- FlowField.Cycle: the nebula's flow cycle and Stardust's particle lifetime phase come from zone time (_FlowCycle, phase uniform), not _Time.y / Time.time.
- FlowFieldParityVerify reaches the keyword-gated flow(), fails on logged errors, tolerance 3e-4; mutants updated.
Not built or run when committed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The probe steps a Zone, lowers its flow through VolumeSampling.LowerFlow, dispatches the HLSL
globalFlow and slopeFlow, and compares with FlowField (max error 1.0e-4 of amplitude, tolerance
1e-3). All commits build; the Unity side was verified from this tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FlowField (ServerShared) is the flow law; Zone.Flow/FlowScroll read PlanetSettings.Flow at zone
time. The Flow block moves from DefaultEnvironment to PlanetSettings (ZoneEnvironment key 1 is
retired), VolumeSampling's frame-clock scroll accumulator and the Stardust leftover field are
deleted, and slopeFlow returns 0 for a zero gradient. Unity parity probe follows in the next
commit; this commit has not been built in Unity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The operator's play report on FieldShieldTest ("weak hits are very
janky, clicking outside the hex still makes it tremble and shatter,
pooling seems very rough") traced to two provable defects, not one
tuning constant:
- ShieldPanel.Hit had no bounds check at all. A point handed to it was
always enqueued and injected regardless of how far outside the
panel's own panelRadius it was.
- ShieldInterceptor.FindReusable decided reuse from a raw world-space
chord distance to the panel's stale spawn-time Position, using
PanelRadius*ReuseFraction (authored up to 2x) as the threshold. On a
non-uniformly scaled/curved envelope a chord across curvature is not
the panel's own in-plane extent, so a click clearly outside the
rendered tiling could still measure inside threshold and get routed
to Hit() -- which had nothing to reject it.
- Reusing a panel never refreshed its pooled DieAt, so a panel struck
again could still expire and get returned to the pool mid-fracture
("pooling seems very rough").
Fix: ShieldPanel.ContainsWorldPoint is now the one owner of "is this
point within my own radius" (panel-local in-plane distance vs.
panelRadius), used both as Hit's hard gate and as FindReusable's
candidacy test (with ReuseFraction, now capped at 1, narrowing but
never widening past the physical radius). Reuse now also extends
DieAt from the moment of the new hit.
Pinned in ShieldPanelCut3Verify.CheckHitOutsideRadiusDoesNotReachPanel:
a direct Hit() and a routed Absorb() outside a panel's own radius must
enqueue nothing and leave its state buffer untouched, and a genuinely
outside click opens its own panel. Deterministic (checks the pending
queue and no-op state, not temper drift) because breakBudgetFraction
paces fracture over many frames on purpose, so temper keeps drifting
from an earlier hit alone -- a drift-based check would have been
measuring that, not this rule (caught this against the check's own
first run).
All four ShieldPanelCut<N>Verify probes pass (batchmode, real GPU
device, no -nographics).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut 5's own probe passed while breaking Cut 3's reuse rule: with R7 holding
the rim, the through-thickness echo (KEcho/_EchoReflect) is the only
remaining source of sub-critical tension for a weak hit, and its magnitude
(-0.65) was weaker than the still-live residual compression velocity left
by the direct KInject kick by the time the echo fired. The echo's own
header comment says a free-surface reflection inverts with unit
magnitude, matching the pre-R7 rim's Dirichlet condition -- -0.65 was a
lossy fraction that could reduce but never flip the sign of u, so a weak
hit's u never crossed positive and KFracture's `u <= 0` guard skipped the
sub-critical erosion branch forever, no matter how many hits landed.
Raised echoReflect from -0.65 to -0.9 (ShieldPanel.cs) -- enough margin to
flip a weak hit's residual compression into net tension without needing a
delay/seeding change; echoDelaySteps and KInject's seeding were not the
defect. All four probes now pass, including Cut 3's reuse rule (temper
0.4180 -> 0.3997 on a reused panel).
Also lands the multi-hit-reuse rule Cut 5's own report flagged as unpinned:
CheckRepeatedWeakHitsBreakWhereOneDoesNot in ShieldPanelCut5Verify.cs.
Energy 15 (not 8, whose peak tension of ~0.14 sits under tensileStrength
0.2 and can never break at any repetition) breaks on hit 2 of a live,
reused panel while a cold single hit -- given equal idle time (720 steps,
matching the multi-hit run's total budget, to rule out "just needed more
time to ring out") -- never breaks. This is the Cut 3 reuse rule and the
temper mechanism producing the operator-facing payoff together.
Verified: ShieldPanelCut1Verify, Cut2Verify, Cut3Verify, Cut5Verify all
PASS via batchmode (graphics device required, not -nographics).
Operator watch: echoReflect at -0.9 is close to the physical "unit
inversion" ceiling implied by the header comment; if later tuning raises
hit energies or lowers damping, watch for the echo overshooting into an
immediate break on a single moderate hit (observed at -1.4 and even -1.0
in earlier tuning passes) rather than the intended multi-hit erosion.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R7 (operator ruling): the panel's emitters hold its edge, so the rim is no
longer a free surface. Deletes the openW ghost term end to end -- its only
consumer in ShieldSim.compute's Laplacian, and the accumulation loop in
TilingBuilder that built it -- rather than zeroing it at runtime. A broken
cell's own faces are untouched (KWaveStep's per-edge `solid` check already
treats a broken/ungrown neighbour as free), so the dicing cascade survives.
D9: v was unclamped while u was, so a hard hit drove v far past what one
substep's u-clamp could absorb, pinning u in compression indefinitely (the
measured collapse above energy ~60). Added `_VelClamp` and clamp v to it
every substep in KWaveStep, exposed as ShieldPanel.velocityClamp (100).
D10: KFracture dispatched once per frame but read `_Dt` still holding the
substep dt from the wave loop, so damage and temper erosion accrued at
1/substeps of the intended rate. Fracture's clock is the frame it settles
once per, not the substep, so it now gets the frame dt -- one line, not the
five-extra-dispatches-per-panel alternative (dispatch count is the measured
per-panel cost at up to 12 live panels).
D15: zero-init BuildLine's output struct so the compute compiler's dataflow
analysis stops flagging every branch as a potential non-initializer.
Tuning (ShieldPanel.cs Material header): tensileStrength 0.9->0.2,
temperErosion 2.2->12, damageGain 6->100, breakAt 0.75->0.08,
storedEnergyGain 0.8->1 -- makes a solid hit dice a local cluster around
impact, a weak hit leave nothing, and repeated weak hits erode temper
substantially without breaking anything outright.
Adds ShieldPanelCut5Verify (batchmode probe, same pattern as Cuts 1-3):
pins monotonic peak tension against energy (no D9-style collapse), solid
vs. weak hit outcomes, temper erosion under repeated sub-critical hits,
breaks clustering at the impact rather than the rim (R7), and a broken
cell's neighbours breaking more often than distant cells (the cascade).
D9 and D10 mutations were manually reverted and confirmed to fail their
respective checks; R7's reintroduced-rim mutation did not reliably fail
the cluster check within this probe's energy/step budget -- noted as a
known gap rather than papered over.
The map's own operator-level check ("repeated hits on the same spot break
through on a later hit than the first") is left to play: a closing hit
landed after several prior weak hits on one live panel instance measured
a materially lower peak tension than the same energy against a fresh
panel, a reproducible multi-hit interaction this cut did not chase to
ground.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
D17: KInject/KWaveStep/KFracture gate on growth, which only KUpdate writes and
only after this same frame's injection dispatch, so a panel struck the instant
it spawns swallowed its own creating hit. ResetSim now seeds bState directly
from a precomputed per-cell arm state (backdated by one growDuration) instead
of leaving KInit's zero in place, so the strike centre reads as already grown.
D20: _PanelSpawnTime was hardcoded to 0f every frame; it now tracks each
panel's own backdated arm instant, so growth keeps advancing correctly on the
frames after spawn instead of collapsing back under the gate.
Pool-rebuild trap (not map-numbered, but load-bearing for the whole pool
argument in §2.4): OnEnable unconditionally rebuilt and OnDisable
unconditionally released, so every SetActive cycle a pooled panel goes through
paid the 1.3-21ms Rebuild cost the pool exists to avoid. OnEnable now only
rebuilds when not yet built; release moves to OnDestroy. ResetSim is also
self-healing (builds if not yet built) rather than depending on OnEnable's
timing, which a batchmode probe showed is not synchronous with
Object.Instantiate in every context this runs in.
D21: Penrose's iteration count was CeilToInt(...)+1, always overshooting the
requested cellSize by a full extra phi-subdivision. Rounding to the nearest
achievable iteration count instead brings Penrose within 2-5x of Hex's cell
count at the same radius/cellSize, down from an order of magnitude.
TilingBuilder.cs gains a CellState mirror struct (matching ShieldCommon.hlsl's
CellState field-for-field) so ResetSim can write growth/temper/breakTime
directly and the Cut 3 verification probe can read them back without a new
readback path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut 1 of docs/shield-panel-cut.md: land ShieldPanel.cs, ShieldPanel.shader,
ShieldSim.compute, ShieldCommon.hlsl, TilingBuilder.cs and Tilings.cs under
Assets/Shaders/Compute/ShieldField/, alongside Lightning/ and Slime/.
Port (URP -> built-in, three substitutions per the map's §2.1 probe):
- drop the UniversalPipeline SubShader tag, add IgnoreProjector
- swap the URP Core.hlsl include for UnityCG.cginc
- TransformWorldToHClip -> UnityWorldToClipPos (3 call sites)
Drawing collapse (D2/D5): RenderParams has no shaderPass member in
6000.3.24f1, and a multi-pass material draws every SubShader pass from one
indirect call regardless, so the three per-pass args buffers and draw calls
collapse to one args buffer sized to the widest pass (Outline) and one
Graphics.RenderPrimitivesIndirect call. Deleted: argsOutline, argsConduit,
drawFill/drawOutline/drawConduits and their branches.
Also deleted: ShieldPanelTester (the click harness the operator ruled
against; it also can't run — activeInputHandler is New Input System only)
and [ExecuteAlways] (incompatible with a pool per D13; Rebuild stays on the
context menu).
Defects fixed:
- D1: TilingBuilder.cs's inner `dist` shadowed the outer one (CS0136);
renamed to nbrDist.
- D3: CellStatic was 15 floats against a declared Stride of 16; added pad2
on both the HLSL and C# sides.
- D4: `centroid` is reserved in the compute compiler; renamed the
ShieldCommon.hlsl field to `center` (kept the C# field name as-is; it's
not reserved there and the two sides don't need matching names, only
matching layout).
- D6: fixed the comment on _BreakBudget — it's two elements (count + cap),
not the "single element" the old comment claimed.
- D12: the two-element uint[] Update() allocated every frame is now a
cached field, cheaper still under a pool of many live panels.
- D18 (R5 — operator ruling, not deferred to a follow-up): Penrose ignored
panelRadius entirely — a fixed authored iteration count meant the tiling
was just uniformly rescaled, not resampled, so cell count never grew with
the panel. Every other tiling here uses cellSize to hold local cell size
constant while radius extends coverage; Penrose now derives its iteration
count the same way (each substitution divides edge length by phi, solved
for the count that brings a unit edge to ~cellSize once scaled to
radius). This retires the now-meaningless `penroseIterations` field and
the `subdivisions` parameter it threaded through Tilings.Generate.
- Also removed a dead `idx` counter in Tilings.Voronoi (D8's mention of
dead code; the Vector2.zero sentinel ambiguity itself is unfixed, same as
D7/D19 — Voronoi and TriHex stay recorded defects, not fixed, per R5).
Not fixed here, per the map: D7 (Voronoi is unusably slow) and D19 (TriHex
has zero dual edges) are recorded, not fixed. D9/D10 (fracture tuning) are
Cut 5. D17 (growth gate blocks the spawning hit) and D20 (_PanelSpawnTime
hardcoded to 0) are Cut 3.
Repo-specific fix not in the map: Assets/Scripts/UI/ContextMenu.cs defines
a global `ContextMenu` MonoBehaviour that shadows UnityEngine.ContextMenu,
so `[ContextMenu("Rebuild")]` doesn't compile here (CS0616) even though it
did in the map's throwaway probe project. Used the fully-qualified
`[UnityEngine.ContextMenu(...)]` instead.
Adds Assets/Scripts/Editor/ShieldPanelCut1Verify.cs, a batchmode probe (no
NUnit path exists per the map's Q7 — Tests.asmdef can't see
Assembly-CSharp): asserts zero shader/compute compile errors and that
Penrose cell count actually scales with panelRadius (D18). Also adds
tests/mutation_tests_shield_panel_cut1.py, which mutates the D18 fix, the
D4 fix, and the shaderPass deletion in turn and asserts the probe goes red
each time, restoring the source after each case (own file per the
convention mutation_tests_addressables_cut.py established, so as not to
touch item-provenance's or the addressables cut's harnesses).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut 8b. The Unity project compiles with 0 errors in 6000.3.24f1 batchmode.
Packages: manifest.json pins org.gamecult.cultlib at cultlib-unity-v1.0.57
and org.gamecult.caching.unity at caching-unity-v1.2.0, next to the 8a
CultMath tag. Aetheria.Shared.Unity references GameCult.CultLib and keeps
noEngineReferences.
The Database Tools (Assets/Scripts/CultCache/Editor) are deleted in this
commit, alongside their five ported drawers in
Assets/Scripts/Editor/CultCacheDrawers.cs, each an
[CultInspectorDrawer] ICultInspectorDrawer:
- InspectableType: popup of the attribute type's [Union] member names,
writing the string.
- InspectableColor: float3 and float4 through ColorField.
- InspectableAnimationCurve: float4[] and BezierCurve through CurveField.
- InspectableTemperature: Kelvin, Celsius and Fahrenheit fields over one
value.
- InspectableSchematicShape: size and cell toggles. The old drawer also
drew the item's schematic texture and tinted hull hardpoints, which need
the parent document. The tagged Studio hands a drawer only the value, so
those wait on the Studio exposing the open record.
Unity call sites:
- ActionGameManager opens the catalog through AetheriaStores; Cut 10 adds
the run and player stores.
- Refs resolve through ItemManager.GetData or Get(ref); orbit, planet and
belt maps key by CultRecordKey; identity is compared by reference or key.
- The PlayerSettings.msgpack reader and writer, which Cut 10 deletes, pass
CultDocumentMessagePackSerialization.OptionsFor, since the embedded run
reaches CultMath values and refs. Keyboard layouts need no resolver and
keep MessagePack's defaults until Cut 10.
- The input map uses the nested action to binding shape.
- PropertiesPanel reads CultInspectorRangeAttribute.
- NameTools loses its legacy name-file writer.
- Slime compares serialized settings instead of the deleted SHA1 helper.
- FieldDriver drops InspectorHeader.
- .gitignore covers the test project's build output.
Builds: Aetheria.Shared, AetherDb, Aetheria.Shared.Tests (6/6), Unity
batchmode (0 errors).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With using static CultMath.math, int2.zero binds to the int2() factory
method (CS0119). The alias existed for exactly that; it now names
CultMath.int2, as SectorMap does for float2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fluid, Slime, SlimeTestSpawner, LightningCompute and MouseRotate still
imported Unity.Mathematics and compiled only because Burst and Collections
pull the package in. Fluid, Slime and LightningCompute now use CultMath
(LightningCompute's snoise included, which CultMath carries as the same
Ashima simplex noise); SlimeTestSpawner and MouseRotate imported it without
using it, so the usings are gone. LightningCompute converts its float3
offsets with ToUnity() instead of the Unity.Mathematics implicit cast.
None of these are Burst-compiled or jobs. Their ComputeBuffer structs are
built from UnityEngine Vector2/Vector3 and floats, not math types, so no
buffer layout changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>