Aetheria/Assets/Shaders/Compute/ShieldField/ShieldPanel.shader
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

380 lines
14 KiB
GLSL

Shader "ShieldField/Panel"
{
Properties
{
_FillColorA ("Fill Tint A", Color) = (0.15, 0.45, 0.9, 0.28)
_FillColorB ("Fill Tint B", Color) = (0.30, 0.20, 0.8, 0.28)
_LineColor ("Outline", Color) = (0.6, 0.9, 1.0, 1)
_CrackColor ("Crack Emission", Color) = (1.0, 0.55, 0.25, 1)
_CompressColor ("Compression", Color) = (0.1, 0.3, 1.0, 1)
_TensionColor ("Tension", Color) = (1.0, 0.35, 0.15, 1)
_ConduitColor ("Conduit", Color) = (0.35, 0.8, 1.0, 1)
_LineWidthPx ("Outline px", Range(0.5, 8)) = 1.5
_CrackWidthPx ("Crack px", Range(0.5, 8)) = 1.0
_ConduitWidthPx ("Conduit px", Range(0.5, 8)) = 1.0
_FeatherPx ("Feather px", Range(0.5, 4)) = 1.2
_StrainScale ("In-plane strain", Float) = 0.35
_RippleScale ("Out-of-plane", Float) = 0.30
_StressColorGain("Stress emission", Float) = 1.6
_CrackGap ("Pre-break gap", Float) = 0.10
_ShardSpread ("Shard divergence", Float) = 1.40
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" }
Blend One One // additive; fragments output premultiplied colour
ZWrite Off
Cull Off
HLSLINCLUDE
#include "UnityCG.cginc"
#include "ShieldCommon.hlsl"
StructuredBuffer<CellStatic> _Cells;
StructuredBuffer<float2> _Verts;
StructuredBuffer<DualEdge> _Edges;
StructuredBuffer<CellState> _State;
StructuredBuffer<float> _U;
StructuredBuffer<float> _EdgeFlow;
StructuredBuffer<uint> _Visible;
float4x4 _PanelToWorld;
float _Time01; // absolute sim time; _Time.y is unreliable for indirect draws
float _CrackStart;
float _ShardLife;
uint _MaxShards;
float4 _FillColorA, _FillColorB, _LineColor, _CrackColor;
float4 _CompressColor, _TensionColor, _ConduitColor;
float _LineWidthPx, _CrackWidthPx, _ConduitWidthPx, _FeatherPx;
float _StrainScale, _RippleScale, _StressColorGain, _CrackGap, _ShardSpread;
struct Ctx
{
CellStatic c;
CellState s;
float u;
float age; // seconds since break, 0 while intact
float fade;
uint nSplits;
float fillT, lineT;
};
Ctx GetCtx(uint cell)
{
Ctx x;
x.c = _Cells[cell];
x.s = _State[cell];
x.u = _U[cell];
x.lineT = saturate(x.s.growth / 0.55);
x.fillT = saturate((x.s.growth - 0.30) / 0.70);
bool broken = x.s.breakTime >= 0.0;
x.age = broken ? max(_Time01 - x.s.breakTime, 0.0) : 0.0;
x.fade = broken ? saturate(1.0 - x.age / max(_ShardLife, 1e-3)) : 1.0;
x.nSplits = ShardCount(x.c.splitSeed, x.s.damage, x.s.released,
x.c.vertCount, _CrackStart, _MaxShards);
return x;
}
// Panel-space position of one corner of one fan triangle.
// corner 0 = centroid, 1 = vert[tri], 2 = vert[tri+1].
float3 CellPoint(Ctx x, uint tri, uint corner)
{
uint n = max(x.c.vertCount, 1u);
float2 p = (corner == 0)
? x.c.center
: _Verts[x.c.vertStart + ((tri + corner - 1) % n)];
float2 local = p - x.c.center;
// materialisation: the cell inflates from its own centroid
local *= EaseOutBack(x.fillT);
// the stress wave is visible in the geometry itself: tension dilates
// the cell, compression pinches it
local *= (1.0 + x.u * _StrainScale);
float z = x.u * _RippleScale;
if (x.nSplits > 0)
{
uint startTri;
uint sid = ShardOf(x.c.splitSeed, x.nSplits, n, tri, startTri);
float2 outward = _Verts[x.c.vertStart + startTri] - x.c.center;
outward = normalize(outward + float2(1e-5, 1e-5));
uint sSeed = x.c.splitSeed ^ (sid * 0x9E3779B9u);
float2 jit = Hash2(sSeed) - 0.5;
if (x.s.breakTime < 0.0)
{
// cracked but holding: shards ease apart just enough to open
// the chord lines
float gap = saturate((x.s.damage - _CrackStart) / 0.35);
local += outward * gap * _CrackGap * x.c.radius;
}
else
{
float2 drift = (outward + jit * 0.9) * x.age * _ShardSpread;
local = Rot2(local, x.s.rot * (0.5 + jit.y));
local += drift * x.c.radius;
z += jit.x * x.age * 0.4;
}
}
float3 pos = float3(x.c.center + local, z) + x.s.pos;
return mul(_PanelToWorld, float4(pos, 1.0)).xyz;
}
float3 StressEmission(float u, float glow)
{
float compress = saturate(-u * _StressColorGain);
float tension = saturate( u * _StressColorGain);
return _CompressColor.rgb * compress
+ _TensionColor.rgb * tension
+ _CrackColor.rgb * glow;
}
// ----------------------------------------------------------
// Screen-space line expansion. Both endpoints are projected, the
// perpendicular is taken in pixels, and the offset is scaled by w so it
// survives the perspective divide. Width is therefore exact in pixels at
// any distance or field of view.
// ----------------------------------------------------------
struct LineVary
{
float4 pos : SV_POSITION;
noperspective float dist : TEXCOORD0; // signed pixels from the line centre
float halfPx : TEXCOORD1;
float4 col : TEXCOORD2;
};
LineVary BuildLine(float3 wa, float3 wb, uint k, float widthPx, float4 col)
{
// D15: every path below does set all four fields, but the compute compiler's
// dataflow analysis doesn't trust that across the early-return branch and warns
// "potentially uninitialized variable (BuildLine)". Zero-init once instead of
// relying on the analysis.
LineVary o = (LineVary)0;
float4 ca = UnityWorldToClipPos(wa);
float4 cb = UnityWorldToClipPos(wb);
// behind the near plane on either end: collapse to a zero-area tri
if (ca.w <= 1e-4 || cb.w <= 1e-4 || col.a <= 0.001)
{
o.pos = float4(0, 0, 0, 1);
o.dist = 0; o.halfPx = 1; o.col = 0;
return o;
}
float2 halfRes = _ScreenParams.xy * 0.5;
float2 sa = ca.xy / ca.w * halfRes;
float2 sb = cb.xy / cb.w * halfRes;
float2 d = sb - sa;
float L = length(d);
d = (L > 1e-5) ? d / L : float2(1, 0);
float2 nrm = float2(-d.y, d.x);
float hw = widthPx * 0.5 + _FeatherPx;
float ext = hw; // overshoot the ends so joins close
// 6 vertices: (end, side) pairs forming two triangles
static const uint2 QUAD[6] = { uint2(0,0), uint2(1,0), uint2(0,1),
uint2(0,1), uint2(1,0), uint2(1,1) };
uint2 q = QUAD[k];
float side = q.y * 2.0 - 1.0;
float along = q.x * 2.0 - 1.0;
float4 cp = q.x ? cb : ca;
float2 offPx = nrm * side * hw + d * along * ext;
cp.xy += offPx / halfRes * cp.w;
o.pos = cp;
o.dist = side * hw;
o.halfPx = widthPx * 0.5;
o.col = col;
return o;
}
float4 ShadeLine(LineVary i) : SV_Target
{
float a = 1.0 - smoothstep(i.halfPx - _FeatherPx * 0.5,
i.halfPx + _FeatherPx * 0.5, abs(i.dist));
a *= i.col.a;
clip(a - 0.002);
return float4(i.col.rgb * a, a);
}
ENDHLSL
// ==========================================================
// Pass 0 — fill
// ==========================================================
Pass
{
Name "Fill"
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 4.5
struct V2F
{
float4 pos : SV_POSITION;
float4 col : TEXCOORD0;
};
V2F vert(uint vid : SV_VertexID, uint iid : SV_InstanceID)
{
V2F o;
uint cell = _Visible[iid];
Ctx x = GetCtx(cell);
uint tri = vid / 3, corner = vid % 3;
if (tri >= x.c.vertCount || x.fillT <= 0.0 || x.fade <= 0.0)
{
o.pos = float4(0, 0, 0, 1); o.col = 0; return o;
}
float3 w = CellPoint(x, tri, corner);
o.pos = UnityWorldToClipPos(w);
float4 base = lerp(_FillColorA, _FillColorB, (x.c.tileType & 1) ? 1.0 : 0.0);
float a = base.a * saturate(x.fillT) * x.fade;
// a depleted panel reads dimmer; temper is the visible charge
a *= lerp(0.45, 1.0, saturate(x.s.temper));
float3 rgb = base.rgb + StressEmission(x.u, x.s.glow);
o.col = float4(rgb, a);
return o;
}
float4 frag(V2F i) : SV_Target
{
clip(i.col.a - 0.002);
return float4(i.col.rgb * i.col.a, i.col.a);
}
ENDHLSL
}
// ==========================================================
// Pass 1 — outline. Perimeter edges plus the radial chords that
// separate shard groups. Chords fade in with damage well before the
// shards actually part, which is what produces the spidered-but-holding
// state between weak hits.
// ==========================================================
Pass
{
Name "Outline"
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 4.5
LineVary vert(uint vid : SV_VertexID, uint iid : SV_InstanceID)
{
uint cell = _Visible[iid];
Ctx x = GetCtx(cell);
uint seg = vid / 6, k = vid % 6;
uint n = x.c.vertCount;
if (seg >= n * 2 || x.lineT <= 0.0 || x.fade <= 0.0)
return BuildLine(0, 0, k, 1, 0);
bool radial = seg >= n;
uint ei = radial ? seg - n : seg;
float3 wa, wb;
float4 col;
float width;
if (radial)
{
bool chord = (x.nSplits > 0) && IsChord(x.c.splitSeed, x.nSplits, n, ei);
float a = chord ? saturate((x.s.damage - _CrackStart) / 0.25) : 0.0;
a *= x.fade;
wa = CellPoint(x, ei, 0);
wb = CellPoint(x, ei, 1);
col = float4(_CrackColor.rgb * (1.0 + x.s.glow * 2.0), _CrackColor.a * a);
width = _CrackWidthPx;
}
else
{
// during materialisation the ring draws itself on edge by edge
float a = saturate(x.lineT * (float)n - (float)ei) * x.fade;
wa = CellPoint(x, ei, 1);
wb = CellPoint(x, ei, 2);
col = float4(_LineColor.rgb + StressEmission(x.u, x.s.glow) * 0.7,
_LineColor.a * a);
width = _LineWidthPx;
}
return BuildLine(wa, wb, k, width, col);
}
float4 frag(LineVary i) : SV_Target { return ShadeLine(i); }
ENDHLSL
}
// ==========================================================
// Pass 2 — dual-graph conduits. Centroid-to-centroid arcs lit by the
// stress gradient flowing across them, so you can watch the wave route
// through the lattice and reflect off broken regions.
// ==========================================================
Pass
{
Name "Conduit"
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 4.5
float _ConduitGain;
LineVary vert(uint vid : SV_VertexID, uint iid : SV_InstanceID)
{
uint cell = _Visible[iid];
Ctx x = GetCtx(cell);
uint seg = vid / 6, k = vid % 6;
if (seg >= x.c.nbrCount || x.s.breakTime >= 0.0 || x.s.growth < 0.6)
return BuildLine(0, 0, k, 1, 0);
uint ei = x.c.nbrStart + seg;
DualEdge e = _Edges[ei];
if (e.nbr <= cell) return BuildLine(0, 0, k, 1, 0); // draw each arc once
CellState ns = _State[e.nbr];
if (ns.breakTime >= 0.0 || ns.growth < 0.6) return BuildLine(0, 0, k, 1, 0);
float flow = saturate(_EdgeFlow[ei] * _ConduitGain);
float a = flow * min(x.s.growth, ns.growth);
if (a <= 0.003) return BuildLine(0, 0, k, 1, 0);
CellStatic nc = _Cells[e.nbr];
float3 wa = mul(_PanelToWorld,
float4(x.c.center, x.u * _RippleScale, 1)).xyz + x.s.pos;
float3 wb = mul(_PanelToWorld,
float4(nc.center, _U[e.nbr] * _RippleScale, 1)).xyz + ns.pos;
float4 col = float4(_ConduitColor.rgb * (0.4 + flow * 2.0), _ConduitColor.a * a);
return BuildLine(wa, wb, k, _ConduitWidthPx, col);
}
float4 frag(LineVary i) : SV_Target { return ShadeLine(i); }
ENDHLSL
}
}
Fallback Off
}