mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-11 08:48:26 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user