Aetheria/Assets/Shaders/Compute/ShieldField/ShieldPanel.shader

380 lines
14 KiB
Text
Raw Normal View History

Land the six shield-panel sources, ported to built-in and compiling 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>
2026-09-18 06:08:04 +02:00
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)
{
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
// 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;
Land the six shield-panel sources, ported to built-in and compiling 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>
2026-09-18 06:08:04 +02:00
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
}