Skip to content

Commit 9f8cab0

Browse files
committed
grid cables, flow based thorn pulse
1 parent 2fed936 commit 9f8cab0

4 files changed

Lines changed: 75 additions & 51 deletions

File tree

LuaRules/Gadgets/Shaders/gfx_overdrive_cables.frag.glsl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -591,12 +591,12 @@ void main() {
591591
// at twig-local 0 and propagates through the twig at
592592
// TWIG_SWEEP_SPEED. Inter-twig stagger feels snappy while motion
593593
// *within* a twig stays comfortable.
594-
// Stagger by the twig root's distance-from-root (packed into gridData.y by
595-
// the GS), not the per-edge along — so the wave is one wavefront radiating
596-
// from the grid root, chaining across joints instead of restarting per
597-
// edge. The within-twig sweep below still uses edge-local spawnAlongMain.
598-
float globalRoot = gridData.y;
599-
float wavePassedElmos = mod(gameTime * CABLE_PROP_SPEED * (1.0 + 1.2 * maxxed) - globalRoot, CABLE_PROP_PERIOD / (1.0 + 0.5 * maxxed));
594+
// Stagger by the twig root's flow phase (packed into gridData.y by the GS),
595+
// not the per-edge along — so the wave follows power flow as one wavefront,
596+
// chaining across joints with aligned merges/splits instead of restarting
597+
// per edge. The within-twig sweep below still uses edge-local spawnAlongMain.
598+
float flowPhase = gridData.y;
599+
float wavePassedElmos = mod(gameTime * CABLE_PROP_SPEED * (1.0 + 1.2 * maxxed) - flowPhase, CABLE_PROP_PERIOD / (1.0 + 0.5 * maxxed));
600600
float subwavePos = TWIG_SWEEP_SPEED * (wavePassedElmos / CABLE_PROP_SPEED);
601601
float localAlong = along - spawnAlongMain;
602602
float d = localAlong - subwavePos;

LuaRules/Gadgets/Shaders/gfx_overdrive_cables.geom.glsl

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ out DataGS {
6565
vec2 cableUV;
6666
vec2 timeData;
6767
// (gridEfficiency, flow, isOwnAlly). For twigs (isBranch>0.5) .y is overloaded
68-
// to the twig root's distance-from-root (flow is unused there) → pulse stagger.
68+
// to the twig root's flow phase (flow is unused there) → pulse stagger.
6969
vec3 gridData;
7070
// Smooth per-vertex cable along-direction (3D). Linearly interpolated by
7171
// the rasteriser so the FS frame (perp3D/trueUp → cylinder normal) rotates
@@ -183,7 +183,7 @@ void emitVtx(vec3 wp, vec3 tangent3D, vec2 cuv,
183183
width = w;
184184
cableUV = cuv;
185185
timeData = td;
186-
gridData = grid.xyw; // .z (per-end rootDist) used in main() for twig globalRoot, not forwarded; see DataGS BUDGET NOTE
186+
gridData = grid.xyw; // .z (per-end flowPhase) used in main() for the twig pulse, not forwarded; see DataGS BUDGET NOTE
187187
cableTangent = tangent3D; // smooth along-dir → FS interpolates the lit frame
188188
spawnAlongMain = gOutSpawnAlong;
189189
gsSlot = gOutSlot;
@@ -528,11 +528,11 @@ void emitTwig(vec2 a, vec2 d, vec2 perpAB,
528528
float halfMainW, float widthVal, float effAmp, float seed,
529529
vec4 gridD, vec2 timeD, float cap, float tCenter,
530530
float spawnAlongMain, int twigIdx, float arcDh, int numSeg,
531-
float globalRoot) {
532-
// Pack this twig's distance-from-root into gridData.y (flow, unused by twigs)
533-
// for the FS pulse stagger — see the DataGS gridData note.
531+
float flowPhase) {
532+
// Pack this twig's flow phase into gridData.y (flow, unused by twigs) for the
533+
// FS pulse stagger — see the DataGS gridData note.
534534
vec4 gridTwig = gridD;
535-
gridTwig.y = globalRoot;
535+
gridTwig.y = flowPhase;
536536
// Resolve spawn point on the wiggly main path at tCenter so twigs root on
537537
// the visible cable.
538538
float lenAB = length(d);
@@ -802,16 +802,16 @@ void main() {
802802
}
803803
float tCenter = float(idxArr[bestK]) / float(G);
804804
float spawnAlongMain = alongArr[bestK];
805-
// This twig root's distance-from-root: lerp the two ends' values (gridData.z,
806-
// rootA on v0 / rootB on v1) by its fractional arc position, so it stays
807-
// continuous with both pylons regardless of which end is nearer the root.
808-
// totalArc > 0 always here (a twig only emits when len3D >= ~42).
809-
float rootFrac = spawnAlongMain / alongArr[numSeg];
810-
float baseRootA = gridD.z;
811-
float baseRootB = dataIn[1].vsGridData.z;
812-
float globalRoot = baseRootA + (baseRootB - baseRootA) * rootFrac;
805+
// This twig root's flow phase: lerp the two ends' values (gridData.z, phaseA
806+
// on v0 / phaseB on v1) by its fractional arc position, so it stays
807+
// continuous with both pylons and the wave follows flow across the joint.
808+
// arc > 0 always here (a twig only emits when len3D >= ~42).
809+
float phaseFrac = spawnAlongMain / alongArr[numSeg];
810+
float phaseA = gridD.z;
811+
float phaseB = dataIn[1].vsGridData.z;
812+
float flowPhase = phaseA + (phaseB - phaseA) * phaseFrac;
813813
emitTwig(a, d, perpAB, halfW, widthVal, effAmp, seed,
814814
gridD, timeD, cap, tCenter, spawnAlongMain, twigIdx, arcDh, numSeg,
815-
globalRoot);
815+
flowPhase);
816816
}
817817
}

LuaRules/Gadgets/Shaders/gfx_overdrive_cables.vert.glsl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
layout (location = 0) in vec2 vertPos; // (x, z) world coords
1212
layout (location = 1) in vec3 vertData; // (capacity, appearTime, witherTime)
13-
layout (location = 2) in vec4 vertGrid; // (gridEfficiency, flow, rootDist, isOwnAlly); .z = per-end distance-from-root (ex-bubblePhase)
13+
layout (location = 2) in vec4 vertGrid; // (gridEfficiency, flow, flowPhase, isOwnAlly); .z = per-end flow phase (ex-bubblePhase)
1414
layout (location = 3) in float vertSlot; // coverage SSBO slot (-1 = disabled)
1515

1616
out gl_PerVertex {

LuaRules/Gadgets/gfx_overdrive_cables.lua

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,20 +1390,17 @@ local function BuildMpCache()
13901390
end
13911391
end
13921392

1393-
-- Per-node tree-path distance from the component root. Forward pass: parent
1394-
-- precedes child in `order`, so the parent's distance is already final.
1395-
-- Topology-stable, so free to accumulate here. Drives the twig-pulse stagger
1396-
-- so thorn pulses chain across edges instead of restarting per cable.
1397-
local distFromRoot = {}
1393+
-- Length of each node's tree edge to its DFS parent. Topology-stable, so
1394+
-- computed once here; the per-tick flow-phase pass (ComputeMaxPotentials)
1395+
-- accumulates these with a sign from the current flow orientation.
1396+
local parentEdgeLen = {}
13981397
for i = 1, #order do
13991398
local u = order[i]
14001399
local pi = parentInTree[u]
1401-
if not pi then
1402-
distFromRoot[u] = 0
1403-
else
1400+
if pi then
14041401
local e = edges[pi.key]
14051402
local dx, dz = e.px - e.cx, e.pz - e.cz
1406-
distFromRoot[u] = distFromRoot[pi.parent] + sqrt(dx * dx + dz * dz)
1403+
parentEdgeLen[u] = sqrt(dx * dx + dz * dz)
14071404
end
14081405
end
14091406

@@ -1459,7 +1456,7 @@ local function BuildMpCache()
14591456
mpCache.subPmaxNonWind = subPmaxNonWind
14601457
mpCache.subWindCount = subWindCount
14611458
mpCache.subWindBase = subWindBase
1462-
mpCache.distFromRoot = distFromRoot
1459+
mpCache.parentEdgeLen = parentEdgeLen
14631460
mpCache.valid = true
14641461
end
14651462

@@ -1588,6 +1585,33 @@ local function ComputeMaxPotentials(flowMode)
15881585
end
15891586
end
15901587

1588+
-- Flow-phase potential φ, one per node: φ(child) − φ(parent) = ±len, signed by
1589+
-- the current flow orientation. On an MST (acyclic) this is always consistent,
1590+
-- so φ rises by edge length along every flow edge — the twig wave then follows
1591+
-- flow, with merges/splits aligning (no discontinuity). Recomputed per tick
1592+
-- (flow reorients); just adds/subtracts the precomputed parentEdgeLen.
1593+
local parentEdgeLen = mpCache.parentEdgeLen
1594+
local flowPhase = {}
1595+
for i = 1, #order do
1596+
local u = order[i]
1597+
local pi = parentInTree[u]
1598+
if not pi then
1599+
flowPhase[u] = 0
1600+
else
1601+
local p = pi.parent
1602+
local len = parentEdgeLen[u] or 0
1603+
local edge = edges[pi.key]
1604+
-- edge.parentID is the flow-source end. Aligned with the DFS parent
1605+
-- (flow p→u) → u is downstream, add; reversed (flow u→p) → subtract.
1606+
if edge and edge.parentID == p then
1607+
flowPhase[u] = flowPhase[p] + len
1608+
else
1609+
flowPhase[u] = flowPhase[p] - len
1610+
end
1611+
end
1612+
end
1613+
mpCache.flowPhase = flowPhase
1614+
15911615
if debugLog and #debugLog > 0 then
15921616
Spring.Echo("[OD-cables] capacities:")
15931617
for i = 1, #debugLog do Spring.Echo(debugLog[i]) end
@@ -1697,7 +1721,7 @@ local function SendAll()
16971721
end
16981722

16991723
-- Bin edges by ally, in one pass.
1700-
local distFromRoot = mpCache.distFromRoot
1724+
local flowPhase = mpCache.flowPhase
17011725
local perAlly = {}
17021726
for key, edge in pairs(edges) do
17031727
local ally = allyOfUnit[edge.parentID] or allyOfUnit[edge.childID]
@@ -1707,7 +1731,7 @@ local function SendAll()
17071731
pa = {
17081732
keys = {}, pxs = {}, pzs = {}, cxs = {}, czs = {},
17091733
caps = {}, maxxed = {}, flows = {}, effs = {},
1710-
rootAs = {}, rootBs = {}, n = 0,
1734+
phaseAs = {}, phaseBs = {}, n = 0,
17111735
}
17121736
perAlly[ally] = pa
17131737
end
@@ -1719,10 +1743,10 @@ local function SendAll()
17191743
pa.caps[i] = capacities[key] or 0
17201744
pa.maxxed[i] = (spGetUnitRulesParam(edge.parentID, "OD_gridMaxxed") or spGetUnitRulesParam(edge.childID, "OD_gridMaxxed")) == 1
17211745
pa.flows[i] = flows[key] or 0
1722-
-- Per-end distance-from-root for the twig pulse. v0 = parent end,
1723-
-- v1 = child end; keyed by node so flow reorientation just swaps them.
1724-
pa.rootAs[i] = distFromRoot[edge.parentID] or 0
1725-
pa.rootBs[i] = distFromRoot[edge.childID] or 0
1746+
-- Per-end flow phase for the twig pulse. v0 = parent end, v1 = child
1747+
-- end; keyed by node so flow reorientation just swaps them.
1748+
pa.phaseAs[i] = flowPhase[edge.parentID] or 0
1749+
pa.phaseBs[i] = flowPhase[edge.childID] or 0
17261750
-- Cached per-grid lookup; fall back to child end if parent's grid
17271751
-- is unknown. 0 → magenta in the shader (unit_mex_overdrive's
17281752
-- "no grid" sentinel).
@@ -1742,7 +1766,7 @@ local function SendAll()
17421766
keys = pa.keys, pxs = pa.pxs, pzs = pa.pzs,
17431767
cxs = pa.cxs, czs = pa.czs,
17441768
caps = pa.caps, maxxed = pa.maxxed, flows = pa.flows, effs = pa.effs,
1745-
rootAs = pa.rootAs, rootBs = pa.rootBs,
1769+
phaseAs = pa.phaseAs, phaseBs = pa.phaseBs,
17461770
})
17471771
alliesWithEdges[ally] = true
17481772
end
@@ -2348,10 +2372,10 @@ local function GenerateOrganicTree()
23482372
local eff = (e.eff or 0) * (e.maxxed and -1 or 1)
23492373
local flow = e.flow or 0
23502374
-- vertGrid.z (ex-bubblePhase, now dead — GS drops gridData.z) carries each
2351-
-- end's distance-from-root: v0 = rootA, v1 = rootB. GS lerps it to twig
2352-
-- roots to chain pulses across edges.
2353-
local rootA = e.rootA or 0
2354-
local rootB = e.rootB or 0
2375+
-- end's flow phase: v0 = phaseA, v1 = phaseB. GS lerps it to twig roots so
2376+
-- the pulse follows flow continuously across edges.
2377+
local phaseA = e.phaseA or 0
2378+
local phaseB = e.phaseB or 0
23552379
local isOwn = (fullview and 2) or (e.isOwnAlly and 1) or 0
23562380
-- Coverage SSBO slot index, or -1 to disable bit updates / lookups
23572381
-- on the GS side. Stored as float here for VBO layout simplicity;
@@ -2362,12 +2386,12 @@ local function GenerateOrganicTree()
23622386
-- Vertex 0: parent end (10 floats: pos2 + data3 + grid4 + slot)
23632387
verts[k+1] = e.px; verts[k+2] = e.pz
23642388
verts[k+3] = cap; verts[k+4] = appearTime; verts[k+5] = witherTime
2365-
verts[k+6] = eff; verts[k+7] = flow; verts[k+8] = rootA; verts[k+9] = isOwn
2389+
verts[k+6] = eff; verts[k+7] = flow; verts[k+8] = phaseA; verts[k+9] = isOwn
23662390
verts[k+10] = slot
2367-
-- Vertex 1: child end (same per-edge payload, except rootB for its end)
2391+
-- Vertex 1: child end (same per-edge payload, except phaseB for its end)
23682392
verts[k+11] = e.cx; verts[k+12] = e.cz
23692393
verts[k+13] = cap; verts[k+14] = appearTime; verts[k+15] = witherTime
2370-
verts[k+16] = eff; verts[k+17] = flow; verts[k+18] = rootB; verts[k+19] = isOwn
2394+
verts[k+16] = eff; verts[k+17] = flow; verts[k+18] = phaseB; verts[k+19] = isOwn
23712395
verts[k+20] = slot
23722396
k = k + 20
23732397
end
@@ -2612,10 +2636,10 @@ function OnCableTreeFull(data)
26122636
e.eff = data.effs and data.effs[i] or 0
26132637
e.px, e.pz = data.pxs[i], data.pzs[i]
26142638
e.cx, e.cz = data.cxs[i], data.czs[i]
2615-
-- Refresh with px/cx so the per-end root distances track the
2639+
-- Refresh with px/cx so the per-end flow phases track the
26162640
-- current parent/child orientation.
2617-
e.rootA = data.rootAs and data.rootAs[i] or 0
2618-
e.rootB = data.rootBs and data.rootBs[i] or 0
2641+
e.phaseA = data.phaseAs and data.phaseAs[i] or 0
2642+
e.phaseB = data.phaseBs and data.phaseBs[i] or 0
26192643
end
26202644
e.isOwnAlly = ownAlly
26212645
-- Late-bind a coverage slot if missing (e.g., gadget was reloaded
@@ -2649,8 +2673,8 @@ function OnCableTreeFull(data)
26492673
maxxed = data.maxxed[i],
26502674
flow = newFlow,
26512675
eff = data.effs and data.effs[i] or 0,
2652-
rootA = data.rootAs and data.rootAs[i] or 0,
2653-
rootB = data.rootBs and data.rootBs[i] or 0,
2676+
phaseA = data.phaseAs and data.phaseAs[i] or 0,
2677+
phaseB = data.phaseBs and data.phaseBs[i] or 0,
26542678
isOwnAlly = ownAlly,
26552679
appearFrame = af,
26562680
witherFrame = nil,

0 commit comments

Comments
 (0)