Aetheria/Assets/Shaders/Compute/ShieldField/ShieldCommon.hlsl
Meta Vens bdb45f6521 Cut 5: hold the rim, clamp v, fix fracture's clock, tune to spec
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>
2026-09-18 20:04:05 +02:00

135 lines
4.5 KiB
HLSL

#ifndef SHIELD_COMMON_INCLUDED
#define SHIELD_COMMON_INCLUDED
#define MAX_VERTS 8 // must match Limits.MaxVerts in TilingBuilder.cs
#define TAU 6.28318530718
#define PI 3.14159265359
// ---- fracture pattern ids (must match FracturePattern in ShieldPanel.cs)
#define PAT_RADIAL 0
#define PAT_STAR 1
#define PAT_LANCE 2
#define PAT_SPIRAL 3
#define PAT_CRYSTAL 4
#define PAT_FIZZLE 5
struct CellStatic
{
float2 center;
float radius;
float spawnDelay;
uint vertStart;
uint vertCount;
uint nbrStart;
uint nbrCount;
float toughness;
uint tileType;
uint splitSeed;
float area; // Laplacian denominator
// R7: openW (rim free-surface ghost weight) removed — the panel's emitters hold its edge.
float pad0;
float pad1;
float pad2;
float pad3;
};
struct DualEdge
{
uint nbr;
float2 mid;
float2 dir;
float len;
float w; // finite-volume weight, len / |c_i - c_j|
};
struct CellState
{
float3 vel;
float spin;
float3 pos; // displacement from rest, panel space (z = out of plane)
float rot;
float damage; // 0..1 persistent
float breakTime; // <0 while intact
float glow; // transient crack emission
float growth; // 0..1 materialisation
float temper; // remaining surface compression — the "charge" that resists initiation
float released; // stored energy dumped at break; sets fragment fineness
float peakTension; // running max, for visualisation
float pad2;
};
// ------------------------------------------------------------------
// hashing
// ------------------------------------------------------------------
uint Hash(uint x)
{
x ^= x >> 16; x *= 0x7feb352du;
x ^= x >> 15; x *= 0x846ca68bu;
x ^= x >> 16; return x;
}
float Hash01(uint x) { return Hash(x) * (1.0 / 4294967296.0); }
float2 Hash2(uint x) { return float2(Hash01(x), Hash01(x ^ 0x68bc21ebu)); }
// ------------------------------------------------------------------
// shard partitioning
//
// A cell is drawn as a fan of vertCount triangles around its centroid.
// A shard is a contiguous run of those triangles. Split boundaries are
// derived from the cell seed, so every shader agrees without storage.
// ------------------------------------------------------------------
// Fragment count scales with the stored elastic energy released at the moment
// of breaking -- the same reason a highly tempered pane dices into gravel while
// an annealed one splits into a few big shards. `released` is 0 for a cell that
// is merely cracked and not yet broken, so the chords appear before separation.
uint ShardCount(uint seed, float damage, float released, uint vertCount,
float crackStart, uint maxShards)
{
if (damage <= crackStart) return 0;
float t = saturate(max(damage - crackStart, 0.0) / max(1.0 - crackStart, 1e-4));
t = saturate(t * 0.4 + released * 1.2);
uint hi = min(vertCount, maxShards);
if (hi < 2) return 0;
uint n = (uint)round(lerp(2.0, (float)hi, t * (0.55 + 0.45 * Hash01(seed))));
return clamp(n, 2u, hi);
}
// Returns the shard index for fan triangle `tri`; startTri receives the
// first triangle of that shard (used as the shard's motion reference).
uint ShardOf(uint seed, uint nSplits, uint vertCount, uint tri, out uint startTri)
{
uint count = 0, prevB = 0, maxB = 0;
[loop] for (uint k = 0; k < nSplits; k++)
{
uint b = Hash(seed ^ (k * 0x9E3779B9u + 0x2545F491u)) % max(vertCount, 1u);
maxB = max(maxB, b);
if (b <= tri) { count++; prevB = max(prevB, b); }
}
startTri = (count == 0) ? maxB : prevB;
return (count == 0) ? (nSplits - 1) : (count - 1);
}
// Is fan edge `ei` (the radial chord from centroid to vertex ei) a shard boundary?
bool IsChord(uint seed, uint nSplits, uint vertCount, uint ei)
{
[loop] for (uint k = 0; k < nSplits; k++)
if ((Hash(seed ^ (k * 0x9E3779B9u + 0x2545F491u)) % max(vertCount, 1u)) == ei) return true;
return false;
}
// ------------------------------------------------------------------
// easing
// ------------------------------------------------------------------
float EaseOutBack(float t)
{
float u = t - 1.0;
return 1.0 + 2.70158 * u * u * u + 1.70158 * u * u;
}
float EaseOutCubic(float t) { float u = 1.0 - t; return 1.0 - u * u * u; }
float2 Rot2(float2 p, float a)
{
float s = sin(a), c = cos(a);
return float2(p.x * c - p.y * s, p.x * s + p.y * c);
}
#endif