Add steady glow effect beneath hydrogen bomb

Render a soft radial glow underneath the hydrogen bomb sprite in
UnitPass. H-bomb instances draw an enlarged quad (hBombGlowScale) so
there's room for the halo; a cell-space UV remap keeps the sprite at its
normal size while the margin becomes glow area. The glow is a steady
(non-pulsing) radial falloff in a warm amber, alpha-blended underneath
the sprite and suppressed in alt/affiliation view.

Detection uses a HYDROGEN_BOMB_COL shader define derived from
UNIT_ORDER, so it tracks the atlas layout rather than hard-coding the
column. All other units are unaffected (scale 1, same fillrate); this
stays a single program / two instanced draw calls.

Glow color, scale, strength, and falloff are exposed in
render-settings.json for live tuning via the debug GUI.
This commit is contained in:
evanpelle
2026-06-05 19:47:07 -07:00
parent ee8c28331b
commit 385b4dd686
6 changed files with 94 additions and 15 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 B

After

Width:  |  Height:  |  Size: 578 B

+7
View File
@@ -169,6 +169,13 @@ export interface RenderSettings {
angryR: number;
angryG: number;
angryB: number;
// Steady soft glow rendered underneath the hydrogen bomb
hBombGlowScale: number; // quad enlargement factor (1 = no glow room)
hBombGlowR: number;
hBombGlowG: number;
hBombGlowB: number;
hBombGlowStrength: number; // peak opacity of the glow
hBombGlowInner: number; // radial falloff start (0..1, quad-space)
};
name: {
lerpSpeed: number;
+34 -2
View File
@@ -82,6 +82,9 @@ const UNIT_ORDER = [
const ATLAS_COLS = UNIT_ORDER.length;
/** Atlas column of the hydrogen bomb — drives the GPU glow halo. */
const HYDROGEN_BOMB_COL = UNIT_ORDER.indexOf(UT_HYDROGEN_BOMB);
// ---------------------------------------------------------------------------
// Instance data layout
// ---------------------------------------------------------------------------
@@ -175,6 +178,10 @@ export class UnitPass {
private uFlickerSpeed: WebGLUniformLocation;
private uAngryColor: WebGLUniformLocation;
private uAltView: WebGLUniformLocation;
private uHBombGlowScale: WebGLUniformLocation;
private uHBombGlowColor: WebGLUniformLocation;
private uHBombGlowStrength: WebGLUniformLocation;
private uHBombGlowInner: WebGLUniformLocation;
private affiliationTex: WebGLTexture | null = null;
private altView = false;
@@ -229,8 +236,8 @@ export class UnitPass {
// Compile shaders
this.program = createProgram(
gl,
shaderSrc(unitVertSrc, { ATLAS_COLS }),
shaderSrc(unitFragSrc, { PALETTE_SIZE: getPaletteSize() }),
shaderSrc(unitVertSrc, { ATLAS_COLS, HYDROGEN_BOMB_COL }),
shaderSrc(unitFragSrc, { PALETTE_SIZE: getPaletteSize(), ATLAS_COLS }),
);
this.uCamera = gl.getUniformLocation(this.program, "uCamera")!;
this.uTick = gl.getUniformLocation(this.program, "uTick")!;
@@ -239,6 +246,22 @@ export class UnitPass {
this.uAngryColor = gl.getUniformLocation(this.program, "uAngryColor")!;
this.uAltView = gl.getUniformLocation(this.program, "uAltView")!;
this.uHBombGlowScale = gl.getUniformLocation(
this.program,
"uHBombGlowScale",
)!;
this.uHBombGlowColor = gl.getUniformLocation(
this.program,
"uHBombGlowColor",
)!;
this.uHBombGlowStrength = gl.getUniformLocation(
this.program,
"uHBombGlowStrength",
)!;
this.uHBombGlowInner = gl.getUniformLocation(
this.program,
"uHBombGlowInner",
)!;
// Texture unit bindings
gl.useProgram(this.program);
@@ -470,6 +493,15 @@ export class UnitPass {
gl.uniform1f(this.uFlickerSpeed, us.flickerSpeed);
gl.uniform3f(this.uAngryColor, us.angryR, us.angryG, us.angryB);
gl.uniform1i(this.uAltView, this.altView ? 1 : 0);
gl.uniform1f(this.uHBombGlowScale, us.hBombGlowScale);
gl.uniform3f(
this.uHBombGlowColor,
us.hBombGlowR,
us.hBombGlowG,
us.hBombGlowB,
);
gl.uniform1f(this.uHBombGlowStrength, us.hBombGlowStrength);
gl.uniform1f(this.uHBombGlowInner, us.hBombGlowInner);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.paletteTex);
+7 -1
View File
@@ -179,7 +179,13 @@
"flickerSpeed": 0.3,
"angryR": 0.784,
"angryG": 0,
"angryB": 0
"angryB": 0,
"hBombGlowScale": 2.2,
"hBombGlowR": 1.0,
"hBombGlowG": 0.72,
"hBombGlowB": 0.15,
"hBombGlowStrength": 0.5,
"hBombGlowInner": 0.45
},
"name": {
"lerpSpeed": 10,
@@ -8,12 +8,17 @@ uniform float uTick;
uniform float uFlickerSpeed;
uniform vec3 uAngryColor;
uniform int uAltView;
uniform vec3 uHBombGlowColor;
uniform float uHBombGlowStrength;
uniform float uHBombGlowInner;
in vec2 vLocalPos;
in vec2 vAtlasUV;
in vec2 vQuadPos;
in vec2 vCellUV;
flat in float vAtlasCol;
flat in float vOwnerID;
flat in float vFlags;
flat in float vHash;
flat in float vGlow;
out vec4 fragColor;
@@ -34,10 +39,29 @@ const vec3 FLICKER_COLORS[4] = vec3[4](
);
void main() {
vec4 texel = texture(uAtlas, vAtlasUV);
// The sprite lives in the central cell-space region [0,1]; for the enlarged
// hydrogen-bomb quad, anything outside that range is glow-only margin.
vec4 texel = vec4(0.0);
bool inSprite = vCellUV.x >= 0.0 && vCellUV.x <= 1.0 &&
vCellUV.y >= 0.0 && vCellUV.y <= 1.0;
if (inSprite) {
vec2 atlasUV = vec2((vAtlasCol + vCellUV.x) / float(ATLAS_COLS), vCellUV.y);
texel = texture(uAtlas, atlasUV);
}
// Discard fully transparent pixels
if (texel.a < 0.01) discard;
// Outside the sprite: render the steady soft glow under the hydrogen bomb,
// otherwise discard. Glow is suppressed in alt (affiliation) view.
if (texel.a < 0.01) {
if (vGlow > 0.5 && uAltView == 0) {
float d = length(vQuadPos - 0.5) * 2.0; // 0 at center → ~1 at quad edge
float g = (1.0 - smoothstep(uHBombGlowInner, 1.0, d)) * uHBombGlowStrength;
if (g > 0.001) {
fragColor = vec4(uHBombGlowColor, g);
return;
}
}
discard;
}
float gray = texel.r;
@@ -10,12 +10,15 @@ layout(location = 2) in vec2 aInstFlags; // atlasIdx (uint8→float), flags (uin
uniform mat3 uCamera;
uniform float uUnitSize;
uniform float uHBombGlowScale; // quad enlargement for the hydrogen bomb glow halo
out vec2 vLocalPos;
out vec2 vAtlasUV;
out vec2 vQuadPos; // quad coords [0,1] — drives the radial glow falloff
out vec2 vCellUV; // sprite cell coords; the central 1/scale region is the sprite
flat out float vAtlasCol;
flat out float vOwnerID;
flat out float vFlags; // 0.0 = normal, 1.0 = flicker, 2.0 = angry
flat out float vHash; // per-instance hash for flicker phase offset
flat out float vGlow; // 1.0 if this instance is a hydrogen bomb (draw glow), else 0.0
void main() {
float worldX = aInstPos.x;
@@ -24,13 +27,20 @@ void main() {
float atlasCol = aInstFlags.x;
vFlags = aInstFlags.y;
vAtlasCol = atlasCol;
// Position-based hash so each unit flickers independently
vHash = fract(worldX * 0.1731 + worldY * 0.3179);
// Hydrogen bombs render an enlarged quad so there's room for a glow halo
// around the sprite. All other units keep scale 1 (no behavior change).
float isHBomb = step(abs(atlasCol - float(HYDROGEN_BOMB_COL)), 0.5);
vGlow = isHBomb;
float scale = mix(1.0, uHBombGlowScale, isHBomb);
// UNIT_SIZE is in world-space tiles — no zoom division needed.
// Units scale with the map like territory tiles do.
float halfSize = uUnitSize * 0.5;
float halfSize = uUnitSize * 0.5 * scale;
vec2 center = vec2(worldX + 0.5, worldY + 0.5);
vec2 worldPos = center + (aPos - 0.5) * halfSize * 2.0;
@@ -38,9 +48,9 @@ void main() {
vec3 clip = uCamera * vec3(worldPos, 1.0);
gl_Position = vec4(clip.xy, 0.0, 1.0);
vLocalPos = aPos;
vQuadPos = aPos;
// Atlas UV: map quad [0,1] to the correct column
float colU = (atlasCol + aPos.x) / float(ATLAS_COLS);
vAtlasUV = vec2(colU, aPos.y);
// Map the enlarged quad back to sprite cell space: the central 1/scale
// portion is the sprite, anything outside [0,1] is glow-only margin.
vCellUV = (aPos - 0.5) * scale + 0.5;
}