From 1f97f2b252fe65c849dcd9cddaf2fe66e29dda55 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 17 Jul 2026 18:27:58 -0700 Subject: [PATCH] fix(render): standardize gradient colorSize to game tiles (#4633) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Effect cosmetic attributes are meant to be standardized on **game tiles** for spatial values and **per-second** for rates. An audit of every effect attribute against its renderer consumer found one violation: the gradient trail's `colorSize`. - The trail shader computed the palette cycle as `colorSize * 4.0 * count`, so `colorSize: 1` produced ~4-tile bands — while `CosmeticSchemas.ts` already documented `colorSize` as "band width in tiles". - This PR removes the hidden ×4 in `trail.frag.glsl` and the mirrored factor in the structures gradient pace in `structure.frag.glsl`, so `colorSize` is now exactly the band width in tiles, and updates the pace-formula doc comment in `CosmeticSchemas.ts`. All other attributes were verified already standard: gradient `movementSpeed` (tiles/s), transition `frequency` (colors/s), spiral `radius` (tiles) / `rotationSpeed` (rad/s), explosion `size`/`thickness` (tiles), `speed` (tiles/s), `transitionSpeed` (colors/s). Every animated path runs off a `uTime` uniform that is genuinely in seconds. ## Catalog impact - Production `cosmetics.json` ships **zero** effects today, so there is no live migration. - Dev catalog gradient entries need `colorSize` ×4 to preserve their current look (`rainbow_trail` 0.2→0.8, `red_black_trail` 1→4, `tiel_red_gradient_nuke_trail` 0.5→2, `rwb_structure_gradient` 5→20). ## Test plan - `npx vitest tests/CosmeticSchemas.test.ts --run` — 51 passed (schema shape unchanged; this is a shader-semantics fix). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 --- src/client/render/gl/shaders/map-overlay/trail.frag.glsl | 6 ++---- src/client/render/gl/shaders/structure/structure.frag.glsl | 4 ++-- src/core/CosmeticSchemas.ts | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/client/render/gl/shaders/map-overlay/trail.frag.glsl b/src/client/render/gl/shaders/map-overlay/trail.frag.glsl index 0e10966de..da97519f6 100644 --- a/src/client/render/gl/shaders/map-overlay/trail.frag.glsl +++ b/src/client/render/gl/shaders/map-overlay/trail.frag.glsl @@ -68,12 +68,10 @@ void main() { } else { // gradient — cyclic gradient banded across the map (world-space diagonal), // scrolling over time so a moving trail shifts hue along it. colorSize - // scales the band width (colorSize = 1 ≈ 4 tiles per band); movementSpeed - // = tiles/sec the bands travel. + // = band width in tiles; movementSpeed = tiles/sec the bands travel. float colorSize = max(texelFetch(uEffect, ivec2(o, rowBase + 2), 0).a, 0.001); float movementSpeed = texelFetch(uEffect, ivec2(o, rowBase + 3), 0).a; - // 4.0 = tiles per band at colorSize 1; tune for default band thickness. - float cycle = colorSize * 4.0 * float(count); + float cycle = colorSize * float(count); float phase = fract((vWorldPos.x + vWorldPos.y - uTime * movementSpeed) / cycle); float f = phase * float(count); diff --git a/src/client/render/gl/shaders/structure/structure.frag.glsl b/src/client/render/gl/shaders/structure/structure.frag.glsl index e20ecb887..fe0d5c1c9 100644 --- a/src/client/render/gl/shaders/structure/structure.frag.glsl +++ b/src/client/render/gl/shaders/structure/structure.frag.glsl @@ -84,13 +84,13 @@ bool structuresEffectColor(int owner, out vec3 color) { // gradient is visible across the shape (world-space banding like the // trail's would put the entire icon inside one band and read as a flat // color). It scrolls along the diagonal at the trail-equivalent pace: - // one full slide every colorSize · 4 · count / movementSpeed seconds, + // one full slide every colorSize · count / movementSpeed seconds, // so both knobs keep their trail timing semantics. float colorSize = max(texelFetch(uEffect, ivec2(owner, rowBase + 2), 0).a, 0.001); float movementSpeed = texelFetch(uEffect, ivec2(owner, rowBase + 3), 0).a; float dn = (vLocalPos.x + vLocalPos.y) * 0.5; // icon diagonal, -0.5..0.5 float phase = - fract(dn - uTime * movementSpeed / (colorSize * 4.0 * float(count))); + fract(dn - uTime * movementSpeed / (colorSize * float(count))); float f = phase * float(count); int i = int(f) % count; int j = (i + 1) % count; diff --git a/src/core/CosmeticSchemas.ts b/src/core/CosmeticSchemas.ts index 77b9f3734..c88be56af 100644 --- a/src/core/CosmeticSchemas.ts +++ b/src/core/CosmeticSchemas.ts @@ -233,7 +233,7 @@ const NukeExplosionEffectSchema = CosmeticSchema.extend({ // separate schema, and the spatial semantics differ: // - "gradient": the palette spans each structure icon's diagonal once (a // visible gradient across the shape), sliding one full cycle every -// colorSize · 4 · count / movementSpeed seconds (the trail-equivalent pace). +// colorSize · count / movementSpeed seconds (the trail-equivalent pace). // - "transition": the whole icon is one color at a time, cross-fading through // the list. `frequency` = color changes per second. // Colors are unvalidated strings; the renderer drops any it can't parse (and