From 0727c2cf4e480e16f93bc182b24b2597d810f3a0 Mon Sep 17 00:00:00 2001 From: Evan Pelle Date: Sun, 14 Jun 2026 00:28:00 +0000 Subject: [PATCH] Add a graphics setting to render names/troops in a bitmap Arial font NamePass can now render player names and troop counts in an Arial bitmap font as an alternative to the overpass-bold MSDF font, toggled live via settings.name.classicFont (mirrors the structure-level font toggle). The Arial atlas is built once at runtime with canvas 2D (there is no Arial asset to ship): each glyph is rasterized, its tight bbox found by a pixel scan, and the glyphs shelf-packed into a coverage atlas. It emits a ParsedAtlas with the same em size (48) and baseline (36) as the MSDF atlas, so it flows through the existing glyph-table/metrics/layout code and leaves name sizing, hit-testing, flag offsets and the icon/status passes unchanged. The name shader gains a uClassic branch that tints the coverage mask with the fill color (no outline, matching the old DOM-rendered names). On toggle, all name/troop lines are re-laid-out since glyph advances differ between the fonts. Exposed as name.classicFont in GraphicsOverrides (default false = MSDF) with a Classic Names toggle in the graphics settings. Co-Authored-By: Claude Opus 4.8 --- resources/lang/en.json | 2 + .../hud/layers/GraphicsSettingsModal.ts | 28 +++ src/client/render/gl/GraphicsOverrides.ts | 1 + src/client/render/gl/RenderOverrides.ts | 2 + src/client/render/gl/RenderSettings.ts | 2 + .../render/gl/passes/name-pass/ArialAtlas.ts | 184 ++++++++++++++++++ .../render/gl/passes/name-pass/TextProgram.ts | 93 ++++++--- .../render/gl/passes/name-pass/index.ts | 67 ++++++- src/client/render/gl/render-settings.json | 3 +- .../render/gl/shaders/name/name.frag.glsl | 19 +- 10 files changed, 370 insertions(+), 31 deletions(-) create mode 100644 src/client/render/gl/passes/name-pass/ArialAtlas.ts diff --git a/resources/lang/en.json b/resources/lang/en.json index 5dfce1e70..fb35d5134 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -516,6 +516,8 @@ "black": "Black", "classic_icons_desc": "Lighter outline with near-black interior", "classic_icons_label": "Classic icons", + "classic_names_desc": "Arial bitmap font for player names and troops (off uses the smooth font)", + "classic_names_label": "Classic names", "classic_numbers_desc": "Pixel font for structure level numbers (off uses the smooth font)", "classic_numbers_label": "Classic level numbers", "colored": "Colored", diff --git a/src/client/hud/layers/GraphicsSettingsModal.ts b/src/client/hud/layers/GraphicsSettingsModal.ts index 22ca56b20..3be6cde09 100644 --- a/src/client/hud/layers/GraphicsSettingsModal.ts +++ b/src/client/hud/layers/GraphicsSettingsModal.ts @@ -535,6 +535,14 @@ export class GraphicsSettingsModal extends LitElement implements Controller { this.patchName({ darkNames: !this.currentDarkNames() }); } + private currentClassicNames(): boolean { + return this.userSettings.graphicsOverrides().name?.classicFont ?? false; + } + + private onToggleClassicNames() { + this.patchName({ classicFont: !this.currentClassicNames() }); + } + private onResetClick() { this.userSettings.setGraphicsOverrides({}); this.requestUpdate(); @@ -550,6 +558,7 @@ export class GraphicsSettingsModal extends LitElement implements Controller { const hoverGlowAlpha = this.currentHoverGlowAlpha(); const namesColored = !this.currentDarkNames(); const iconSize = this.currentIconSize(); + const classicNames = this.currentClassicNames(); const classicIcons = this.currentClassicIcons(); const classicNumbers = this.currentClassicNumbers(); const highlightFill = this.currentHighlightFill(); @@ -800,6 +809,25 @@ export class GraphicsSettingsModal extends LitElement implements Controller { + +
diff --git a/src/client/render/gl/GraphicsOverrides.ts b/src/client/render/gl/GraphicsOverrides.ts index 453e371c5..c61adc009 100644 --- a/src/client/render/gl/GraphicsOverrides.ts +++ b/src/client/render/gl/GraphicsOverrides.ts @@ -7,6 +7,7 @@ export const GraphicsOverridesSchema = z nameScaleFactor: z.number(), cullThreshold: z.number(), darkNames: z.boolean(), + classicFont: z.boolean(), hoverFadeAlpha: z.number(), hoverGlowWidth: z.number(), hoverGlowAlpha: z.number(), diff --git a/src/client/render/gl/RenderOverrides.ts b/src/client/render/gl/RenderOverrides.ts index cd71ea9ed..c36e565a1 100644 --- a/src/client/render/gl/RenderOverrides.ts +++ b/src/client/render/gl/RenderOverrides.ts @@ -28,6 +28,8 @@ export function applyGraphicsOverrides( if (overrides.structure?.iconSize !== undefined) { settings.structure.iconSize = overrides.structure.iconSize; } + // Classic names: Arial bitmap font for names/troops (default false = MSDF). + settings.name.classicFont = overrides.name?.classicFont ?? false; if (overrides.structure?.classicIcons ?? true) { // Classic look (default): lighter player-colored shape behind a darkened // player-colored icon glyph (matching the old canvas renderer's diff --git a/src/client/render/gl/RenderSettings.ts b/src/client/render/gl/RenderSettings.ts index 6654e50a4..b6c701f31 100644 --- a/src/client/render/gl/RenderSettings.ts +++ b/src/client/render/gl/RenderSettings.ts @@ -265,6 +265,8 @@ export interface RenderSettings { hoverGlowWidth: number; /** Peak opacity of the hover glow (0 disables it). */ hoverGlowAlpha: number; + /** true = Arial bitmap font for names/troops, false = overpass MSDF. */ + classicFont: boolean; }; fx: { shockwaveRingWidth: number; diff --git a/src/client/render/gl/passes/name-pass/ArialAtlas.ts b/src/client/render/gl/passes/name-pass/ArialAtlas.ts new file mode 100644 index 000000000..ae7904803 --- /dev/null +++ b/src/client/render/gl/passes/name-pass/ArialAtlas.ts @@ -0,0 +1,184 @@ +/** + * Runtime Arial bitmap-font atlas generator. + * + * The "classic" name font is Arial rendered to a coverage atlas (white glyphs + * on transparent), as an alternative to the overpass-bold MSDF atlas. There's + * no Arial asset to ship and no offline bmfont tooling, so the atlas is built + * once at startup with canvas 2D: each glyph is rasterized, its tight bounding + * box is found by scanning pixels, and the glyphs are shelf-packed. + * + * Metrics are emitted with the SAME em size (48) and baseline (36) as the MSDF + * atlas, so name sizing, hit-testing, flag offsets and icon alignment are all + * unchanged — only the glyph shapes and advances differ. The returned shape is + * a ParsedAtlas, so it flows through buildGlyphTables / buildGlyphMetricsTex / + * layoutString exactly like the MSDF atlas. + */ + +import type { BMChar, ParsedAtlas } from "./Types"; + +// Match the MSDF atlas em/base so all downstream sizing stays identical. +const EM = 48; +const BASE = 36; +// Bold to match the weight of the overpass-bold name style. +const FONT = `bold ${EM}px Arial, "Liberation Sans", sans-serif`; + +const ATLAS_W = 1024; +const PAD = 2; // transparent gutter between packed glyphs + +// Codepoint coverage: ASCII + Latin-1 + Latin Extended-A (matches CHAR_RANGE), +// skipping the C0/C1 control gaps. Covers player names and troop labels. +function* codepoints(): Generator { + for (let c = 32; c <= 383; c++) { + if (c >= 127 && c <= 159) continue; + yield c; + } +} + +interface Measured { + code: number; + ch: string; + advance: number; + w: number; + h: number; + xoffset: number; + yoffset: number; + // Where the tight bbox landed in the measuring canvas (to re-place it later). + srcMinX: number; + srcMinY: number; +} + +export function generateArialBitmapAtlas(): { + atlas: ParsedAtlas; + canvas: HTMLCanvasElement; +} { + // --- measuring pass: rasterize each glyph, find its tight bbox --- + const CELL = 80; // > any 48px glyph incl. ascenders/descenders + const PEN_X = 8; + const PEN_Y = 56; // baseline inside the measuring cell + const tmp = document.createElement("canvas"); + tmp.width = CELL; + tmp.height = CELL; + const tctx = tmp.getContext("2d", { willReadFrequently: true })!; + tctx.font = FONT; + tctx.textBaseline = "alphabetic"; + tctx.textAlign = "left"; + tctx.fillStyle = "#fff"; + + const measured: Measured[] = []; + for (const code of codepoints()) { + const ch = String.fromCodePoint(code); + const advance = Math.ceil(tctx.measureText(ch).width); + tctx.clearRect(0, 0, CELL, CELL); + tctx.fillText(ch, PEN_X, PEN_Y); + const data = tctx.getImageData(0, 0, CELL, CELL).data; + + let minX = CELL; + let minY = CELL; + let maxX = -1; + let maxY = -1; + for (let y = 0; y < CELL; y++) { + for (let x = 0; x < CELL; x++) { + if (data[(y * CELL + x) * 4 + 3] > 8) { + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; + } + } + } + + if (maxX < 0) { + // No ink (e.g. space) — advance only, zero-size glyph. + measured.push({ + code, + ch, + advance, + w: 0, + h: 0, + xoffset: 0, + yoffset: 0, + srcMinX: 0, + srcMinY: 0, + }); + continue; + } + + measured.push({ + code, + ch, + advance, + w: maxX - minX + 1, + h: maxY - minY + 1, + xoffset: minX - PEN_X, // glyph left relative to the pen + yoffset: BASE - (PEN_Y - minY), // glyph top relative to the line top + srcMinX: minX, + srcMinY: minY, + }); + } + + // --- shelf packing into a fixed-width atlas --- + const chars: BMChar[] = []; + const placements: { m: Measured; ax: number; ay: number }[] = []; + let x = PAD; + let y = PAD; + let rowH = 0; + for (const m of measured) { + if (m.w === 0) { + chars.push(toBMChar(m, 0, 0)); + continue; + } + if (x + m.w + PAD > ATLAS_W) { + x = PAD; + y += rowH + PAD; + rowH = 0; + } + placements.push({ m, ax: x, ay: y }); + chars.push(toBMChar(m, x, y)); + x += m.w + PAD; + if (m.h > rowH) rowH = m.h; + } + const ATLAS_H = y + rowH + PAD; + + // --- render pass: draw each glyph at its packed slot --- + const canvas = document.createElement("canvas"); + canvas.width = ATLAS_W; + canvas.height = ATLAS_H; + const actx = canvas.getContext("2d")!; + actx.font = FONT; + actx.textBaseline = "alphabetic"; + actx.textAlign = "left"; + actx.fillStyle = "#fff"; + for (const { m, ax, ay } of placements) { + // Drawing at pen P put the bbox at (P + (srcMin - PEN)); solve for the pen + // that lands the tight top-left at (ax, ay). + const penX = ax - m.srcMinX + PEN_X; + const penY = ay - m.srcMinY + PEN_Y; + actx.fillText(m.ch, penX, penY); + } + + const atlas: ParsedAtlas = { + fontSize: EM, + base: BASE, + scaleW: ATLAS_W, + scaleH: ATLAS_H, + distanceRange: 0, + chars, + kernings: [], + }; + return { atlas, canvas }; +} + +function toBMChar(m: Measured, ax: number, ay: number): BMChar { + return { + id: m.code, + char: m.ch, + width: m.w, + height: m.h, + xoffset: m.xoffset, + yoffset: m.yoffset, + xadvance: m.advance, + x: ax, + y: ay, + page: 0, + }; +} diff --git a/src/client/render/gl/passes/name-pass/TextProgram.ts b/src/client/render/gl/passes/name-pass/TextProgram.ts index 66ff87ee1..e75671841 100644 --- a/src/client/render/gl/passes/name-pass/TextProgram.ts +++ b/src/client/render/gl/passes/name-pass/TextProgram.ts @@ -1,9 +1,16 @@ /** - * TextProgram — MSDF text rendering (player names + troop counts). + * TextProgram — text rendering for player names + troop counts. * - * Owns: shader program, uniform locations, MSDF atlas texture (async loaded). - * Shared textures (glyphMetrics, cursor, strings, playerData) are passed in - * and bound at draw time but not owned/deleted by this class. + * Supports two fonts, switchable per draw via settings.name.classicFont: + * - MSDF: the overpass-bold atlas (default), loaded async from CDN. + * - classic: an Arial bitmap coverage atlas built at runtime (set via + * setArialFont), drawn fill-only (no synthesized outline). + * Both fonts share the em size (48) and baseline (36), so name sizing and the + * sibling icon/status passes are unaffected by the choice. + * + * Owns: shader program, uniform locations, the MSDF atlas texture and the + * Arial atlas texture. Glyph-metric / cursor / string / player-data textures + * are passed in and bound at draw time but not owned here. */ import { assetUrl } from "src/core/AssetUrls"; @@ -28,14 +35,26 @@ export class TextProgram { private program: WebGLProgram; private textures: TextProgramTextures; - // Async-loaded MSDF atlas + // MSDF atlas (async-loaded) private atlasTex: WebGLTexture | null = null; private atlasReady = false; + private msdfScaleW: number; + private msdfScaleH: number; + private distanceRange: number; + + // Arial bitmap atlas (built at runtime, set via setArialFont) + private arialAtlasTex: WebGLTexture | null = null; + private arialMetricsTex: WebGLTexture | null = null; + private arialScaleW = 0; + private arialScaleH = 0; // Uniform locations private uCamera: WebGLUniformLocation; private uTime: WebGLUniformLocation; private uDistRange: WebGLUniformLocation; + private uAtlasScaleW: WebGLUniformLocation; + private uAtlasScaleH: WebGLUniformLocation; + private uClassic: WebGLUniformLocation; private uLerpSpeed: WebGLUniformLocation; private uCullThreshold: WebGLUniformLocation; private uNameScaleFactor: WebGLUniformLocation; @@ -52,8 +71,6 @@ export class TextProgram { private uHoverGlowWidth: WebGLUniformLocation; private uHoverGlowAlpha: WebGLUniformLocation; - private distanceRange: number; - constructor( gl: WebGL2RenderingContext, atlas: ParsedAtlas, @@ -62,6 +79,8 @@ export class TextProgram { this.gl = gl; this.textures = textures; this.distanceRange = atlas.distanceRange; + this.msdfScaleW = atlas.scaleW; + this.msdfScaleH = atlas.scaleH; this.program = createProgram( gl, @@ -77,25 +96,20 @@ export class TextProgram { gl.uniform1i(gl.getUniformLocation(this.program, "uStrings"), 3); gl.uniform1i(gl.getUniformLocation(this.program, "uPlayerData"), 4); - // Static uniforms + // Static uniforms — em size + baseline are shared by both fonts. gl.uniform1f( gl.getUniformLocation(this.program, "uFontSize")!, atlas.fontSize, ); - gl.uniform1f( - gl.getUniformLocation(this.program, "uAtlasScaleW")!, - atlas.scaleW, - ); - gl.uniform1f( - gl.getUniformLocation(this.program, "uAtlasScaleH")!, - atlas.scaleH, - ); gl.uniform1f(gl.getUniformLocation(this.program, "uBase")!, atlas.base); // Dynamic uniform locations this.uCamera = gl.getUniformLocation(this.program, "uCamera")!; this.uTime = gl.getUniformLocation(this.program, "uTime")!; this.uDistRange = gl.getUniformLocation(this.program, "uDistRange")!; + this.uAtlasScaleW = gl.getUniformLocation(this.program, "uAtlasScaleW")!; + this.uAtlasScaleH = gl.getUniformLocation(this.program, "uAtlasScaleH")!; + this.uClassic = gl.getUniformLocation(this.program, "uClassic")!; this.uLerpSpeed = gl.getUniformLocation(this.program, "uLerpSpeed")!; this.uCullThreshold = gl.getUniformLocation( this.program, @@ -142,8 +156,30 @@ export class TextProgram { this.loadAtlas(); } - get ready(): boolean { - return this.atlasReady; + /** True when the atlas for the requested font is uploaded and drawable. */ + isReady(classic: boolean): boolean { + return classic ? this.arialAtlasTex !== null : this.atlasReady; + } + + /** Install the runtime-built Arial bitmap atlas (coverage mask) + metrics. */ + setArialFont( + canvas: HTMLCanvasElement, + metricsTex: WebGLTexture, + scaleW: number, + scaleH: number, + ): void { + const gl = this.gl; + const tex = gl.createTexture()!; + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas); + this.arialAtlasTex = tex; + this.arialMetricsTex = metricsTex; + this.arialScaleW = scaleW; + this.arialScaleH = scaleH; } private loadAtlas(): void { @@ -172,16 +208,28 @@ export class TextProgram { ambient: number, highlightOwnerID: number, fadeOwnerID: number, + classic: boolean, ): void { - if (!this.atlasReady) return; + if (!this.isReady(classic)) return; const gl = this.gl; const ns = settings.name; gl.useProgram(this.program); + const atlasTex = classic ? this.arialAtlasTex! : this.atlasTex!; + const metricsTex = classic + ? this.arialMetricsTex! + : this.textures.glyphMetrics; + const scaleW = classic ? this.arialScaleW : this.msdfScaleW; + const scaleH = classic ? this.arialScaleH : this.msdfScaleH; + const distRange = classic ? 0 : this.distanceRange; + gl.uniformMatrix3fv(this.uCamera, false, cameraMatrix); gl.uniform1f(this.uTime, performance.now() / 1000); - gl.uniform1f(this.uDistRange, this.distanceRange); + gl.uniform1f(this.uDistRange, distRange); + gl.uniform1f(this.uAtlasScaleW, scaleW); + gl.uniform1f(this.uAtlasScaleH, scaleH); + gl.uniform1i(this.uClassic, classic ? 1 : 0); gl.uniform1f(this.uLerpSpeed, ns.lerpSpeed); gl.uniform1f(this.uCullThreshold, ns.cullThreshold); gl.uniform1f(this.uNameScaleFactor, ns.nameScaleFactor); @@ -202,9 +250,9 @@ export class TextProgram { gl.uniform1f(this.uHoverGlowAlpha, ns.hoverGlowAlpha); gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, this.atlasTex!); + gl.bindTexture(gl.TEXTURE_2D, atlasTex); gl.activeTexture(gl.TEXTURE1); - gl.bindTexture(gl.TEXTURE_2D, this.textures.glyphMetrics); + gl.bindTexture(gl.TEXTURE_2D, metricsTex); gl.activeTexture(gl.TEXTURE2); gl.bindTexture(gl.TEXTURE_2D, this.textures.cursor); gl.activeTexture(gl.TEXTURE3); @@ -225,5 +273,6 @@ export class TextProgram { const gl = this.gl; gl.deleteProgram(this.program); if (this.atlasTex) gl.deleteTexture(this.atlasTex); + if (this.arialAtlasTex) gl.deleteTexture(this.arialAtlasTex); } } diff --git a/src/client/render/gl/passes/name-pass/index.ts b/src/client/render/gl/passes/name-pass/index.ts index 6072c82de..358811939 100644 --- a/src/client/render/gl/passes/name-pass/index.ts +++ b/src/client/render/gl/passes/name-pass/index.ts @@ -29,6 +29,7 @@ import type { RenderSettings } from "../../RenderSettings"; import { createFullscreenQuad } from "../../utils/GlUtils"; import { renderTroops } from "../../../../Utils"; +import { generateArialBitmapAtlas } from "./ArialAtlas"; import type { GlyphTables } from "./AtlasData"; import { buildEmojiLookup, @@ -49,7 +50,7 @@ import { StatusIconProgram } from "./StatusIconProgram"; import { layoutString } from "./TextLayout"; import { TextProgram } from "./TextProgram"; import type { PlayerSlot } from "./Types"; -import { LINES_PER_PLAYER, MAX_CHARS } from "./Types"; +import { CHAR_RANGE, LINES_PER_PLAYER, MAX_CHARS } from "./Types"; // Flag quad aspect ratio — must match FLAG_CELL_W / FLAG_CELL_H in FlagAtlasArray.ts. const FLAG_ASPECT = 128 / 85; @@ -73,11 +74,19 @@ export class NamePass { private statusIconProgram: StatusIconProgram; private debugProgram: DebugProgram; - // Atlas + glyph data + // Atlas + glyph data. `glyph`/`kernTable` point at the active font; the MSDF + // and Arial tables are kept so layout can be recomputed when the font toggles. private glyph: GlyphTables; private kernTable: Int8Array; private fontSize: number; private fontBase: number; + private msdfGlyph: GlyphTables; + private msdfKern: Int8Array; + private arialGlyph: GlyphTables; + private arialKern: Int8Array; + private arialMetricsTex: WebGLTexture; + // Which font the cursor buffers are currently laid out for. + private classicFont = false; // Player management private playerByID: Map; @@ -130,6 +139,8 @@ export class NamePass { this.fontBase = atlas.base; this.glyph = buildGlyphTables(atlas.chars); this.kernTable = buildKernTable(atlas.kernings); + this.msdfGlyph = this.glyph; + this.msdfKern = this.kernTable; this.emojiCharToIndex = buildEmojiLookup(); // Runtime flag-image manager (TEXTURE_2D_ARRAY of player flags, fetched @@ -204,6 +215,25 @@ export class NamePass { this.playerDataTex, this.maxPlayers, ); + + // Classic Arial bitmap font: built once at runtime, same em/baseline as the + // MSDF atlas so sizing/icons are unchanged. Selected via name.classicFont. + const arial = generateArialBitmapAtlas(); + this.arialGlyph = buildGlyphTables(arial.atlas.chars); + this.arialKern = new Int8Array(CHAR_RANGE * CHAR_RANGE); // no kerning + this.arialMetricsTex = buildGlyphMetricsTex(gl, arial.atlas); + this.textProgram.setArialFont( + arial.canvas, + this.arialMetricsTex, + arial.atlas.scaleW, + arial.atlas.scaleH, + ); + + this.classicFont = settings.name.classicFont; + if (this.classicFont) { + this.glyph = this.arialGlyph; + this.kernTable = this.arialKern; + } } // ------------------------------------------------------------------------- @@ -643,8 +673,37 @@ export class NamePass { return 0; } + /** + * Switch the active text font when settings.name.classicFont changes, then + * re-lay-out every name + troop line — advances differ between the two fonts, + * so cursor positions and name half-widths must be recomputed. + */ + private syncFont(): void { + const classic = this.settings.name.classicFont; + if (classic === this.classicFont) return; + this.classicFont = classic; + this.glyph = classic ? this.arialGlyph : this.msdfGlyph; + this.kernTable = classic ? this.arialKern : this.msdfKern; + for (const slot of this.slots.values()) { + if (slot.nameLen > 0) { + slot.nameHalfWidth = this.uploadStringRow( + slot.index * LINES_PER_PLAYER, + slot.static.displayName, + ); + } + if (slot.troopLen > 0 && slot.lastTroopStr) { + this.uploadStringRow( + slot.index * LINES_PER_PLAYER + 1, + slot.lastTroopStr, + ); + } + this.writePlayerDataRow(slot); + } + } + draw(cameraMatrix: Float32Array, ambient: number): void { - if (!this.textProgram.ready) return; + this.syncFont(); + if (!this.textProgram.isReady(this.classicFont)) return; if (this.slots.size === 0) return; const gl = this.gl; @@ -704,6 +763,7 @@ export class NamePass { ambient, this.highlightOwnerID, fadeOwnerID, + this.classicFont, ); this.statusIconProgram.draw( cameraMatrix, @@ -730,6 +790,7 @@ export class NamePass { this.statusIconProgram.dispose(); this.debugProgram.dispose(); gl.deleteTexture(this.glyphMetricsTex); + gl.deleteTexture(this.arialMetricsTex); gl.deleteTexture(this.cursorTex); gl.deleteTexture(this.stringTex); gl.deleteTexture(this.playerDataTex); diff --git a/src/client/render/gl/render-settings.json b/src/client/render/gl/render-settings.json index 88125818d..1858961ad 100644 --- a/src/client/render/gl/render-settings.json +++ b/src/client/render/gl/render-settings.json @@ -217,7 +217,8 @@ "statusRowOffset": 1.4, "hoverFadeAlpha": 0.5, "hoverGlowWidth": 5, - "hoverGlowAlpha": 0.75 + "hoverGlowAlpha": 0.75, + "classicFont": false }, "fx": { "shockwaveRingWidth": 0.04, diff --git a/src/client/render/gl/shaders/name/name.frag.glsl b/src/client/render/gl/shaders/name/name.frag.glsl index c1c14610b..1991dc064 100644 --- a/src/client/render/gl/shaders/name/name.frag.glsl +++ b/src/client/render/gl/shaders/name/name.frag.glsl @@ -10,6 +10,7 @@ uniform float uOutlineUsePlayerColor; uniform float uFillUsePlayerColor; uniform float uHoverGlowWidth; // px the white hover glow extends past the outline uniform float uHoverGlowAlpha; // peak opacity of the hover glow +uniform int uClassic; // 1 = Arial bitmap coverage atlas, 0 = MSDF in vec2 vUV; in vec4 vPlayerColor; // player territory color (rgb) + alpha @@ -25,16 +26,24 @@ void main() { // Degenerate fragment — skip if (vPlayerColor.a <= 0.0) discard; + // Compute fill color: player color, or per-type grayscale shade + // (black for human, grayer for nation/bot). + vec3 fillColor = mix(vec3(vNameShade), vPlayerColor.rgb, uFillUsePlayerColor); + + // Classic Arial bitmap: the atlas is a coverage mask; tint it with the fill + // color, no outline (matching the old DOM-rendered names). + if (uClassic == 1) { + float coverage = texture(uAtlas, vUV).a; + if (coverage <= 0.0) discard; + fragColor = vec4(fillColor, vPlayerColor.a * coverage); + return; + } + // Border darkens with night: t² stays dark longer, snaps toward the // outline color late in the day cycle. float t = 1.0 - uNightAmbient; float borderT = t * t; - // Compute fill color: player color, or per-type grayscale shade - // (black for human, grayer for nation/bot). Applies in day and night. - vec3 defaultFill = vec3(vNameShade); - vec3 fillColor = mix(defaultFill, vPlayerColor.rgb, uFillUsePlayerColor); - vec3 msd = texture(uAtlas, vUV).rgb; float sd = median(msd.r, msd.g, msd.b);