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>
135 lines
4.5 KiB
HLSL
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
|