Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/MSDFBatchHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const SimpleFragmentShader = [
'',
'uniform sampler2D uMainSampler;',
'uniform vec2 uUnitRange;', // distanceRange / atlasSize — per texture, never per glyph.
'uniform float uPixelArt;', // 0 = SDF glyph lane, 1 = bitmap glyph lane. Per draw, like uUnitRange.
'',
'varying vec2 outTexCoord;',
'varying vec4 outColor;', // Fill colour + alpha; a zero alpha frees .rgb as the two-tone inner colour.
Expand Down Expand Up @@ -198,6 +199,16 @@ const SimpleFragmentShader = [
' }',
' else',
' {',
// Pixel-art mode: coverage is the texel alpha; no ramp (tone 0), no fade guard.
' if (uPixelArt > 0.5)',
' {',
' fillCoverage = texel.a;',
' outlineCoverage = texel.a;',
' tone = 0.0;',
' fade = 1.0;',
' }',
' else',
' {',
// ── Glyph lane: distance field ─────────────────────────────────────
' float px = max(0.5 * dot(uUnitRange, screenTexSize), 1.0);',
' float weight = outParams.r - (128.0 / 255.0);', // Signed fraction of the range; 128 is neutral.
Expand Down Expand Up @@ -235,6 +246,7 @@ const SimpleFragmentShader = [
// soft glow has real alpha down in that region, so any nonzero softness
// byte suppresses the fade; hard edges keep it.
' fade = max(smoothstep(0.0, 0.2, outlineDist), softStep);',
' }',
' }',
'',
// ── One composite ──────────────────────────────────────────────────────
Expand Down Expand Up @@ -300,7 +312,10 @@ type DrawingContext = any;
interface MSDFBatchHandlerInstance {
_currentTexture: WebGLTextureWrapper | null;
_unitRange: [number, number];
_pixelArt: number;

setPixelArt(v: number): void;
hasPixelArtChanged(v: number): boolean;
instanceCount: number;
instancesPerBatch: number;
bytesPerInstance: number;
Expand Down Expand Up @@ -360,6 +375,17 @@ class MSDFBatchHandler extends PhaserBatchHandler {
const self = this as unknown as MSDFBatchHandlerInstance;
self._currentTexture = null;
self._unitRange = [4 / 512, 4 / 512];
self._pixelArt = 0;
}

setPixelArt(v: number): void {
const self = this as unknown as MSDFBatchHandlerInstance;
self._pixelArt = v;
}

hasPixelArtChanged(v: number): boolean {
const self = this as unknown as MSDFBatchHandlerInstance;
return self._pixelArt !== v;
}

setUnitRange(x: number, y: number): void {
Expand Down Expand Up @@ -397,6 +423,7 @@ class MSDFBatchHandler extends PhaserBatchHandler {

programManager.setUniform('uMainSampler', 0);
programManager.setUniform('uUnitRange', self._unitRange);
programManager.setUniform('uPixelArt', self._pixelArt);

drawingContext.renderer.setProjectionMatrixFromDrawingContext(drawingContext);
programManager.setUniform('uProjectionMatrix', drawingContext.renderer.projectionMatrix.val);
Expand Down
7 changes: 5 additions & 2 deletions src/MSDFFontFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,13 @@ export const MSDFFontFile = new Class({
const img = image.data;
const wrap = gl.CLAMP_TO_EDGE;

// Use NEAREST filtering and bypass the SDF lane in pixel-art mode.
const filter = this.loader.systems.game.config.pixelArt === true ? gl.NEAREST : gl.LINEAR;

const glTexture = renderer.createTexture2D(
0, // mipLevel
gl.LINEAR, // minFilter — MSDF requires linear filtering
gl.LINEAR, // magFilter
filter, // minFilter — LINEAR for MSDF, NEAREST in pixel-art mode
filter, // magFilter
wrap, wrap, // wrapT, wrapS
gl.RGBA, // format
img, // source image element
Expand Down
23 changes: 23 additions & 0 deletions src/MSDFText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,20 @@ function warnNeedsMtsdf(text: any, effectName: string): boolean {
return false;
}

function warnOnceIfPixelArt(text: any, effectName: string): void {
if (text.scene.game.config.pixelArt !== true) return;
if (!text._pixelArtWarnings) {
text._pixelArtWarnings = {};
}
if (!text._pixelArtWarnings[effectName]) {
text._pixelArtWarnings[effectName] = true;
console.warn(
`[MSDFText] "${effectName}" is distance-field based and is ignored ` +
`in pixel-art mode (game config "pixelArt: true").`
);
}
}

/**
* Element-wise equality of two size-scale maps (either may be `null`, meaning
* "uniform size"). A rebuild is only worth paying for when this says no.
Expand Down Expand Up @@ -1095,6 +1109,9 @@ export const MSDFText: MSDFTextStatic = new Class({
*/
setWeight: function (weight: number) {
this.weight = weight;
if (weight !== 0) {
warnOnceIfPixelArt(this, 'weight');
}
return this;
},

Expand Down Expand Up @@ -1752,6 +1769,9 @@ export const MSDFText: MSDFTextStatic = new Class({
if (this.outlineSoftness > 0) {
warnNeedsMtsdf(this, 'soft outline');
}
if (width > 0 || this.outlineSoftness > 0) {
warnOnceIfPixelArt(this, 'outline');
}
return this;
},

Expand Down Expand Up @@ -1880,6 +1900,9 @@ export const MSDFText: MSDFTextStatic = new Class({
if (this.shadowSoftness > 0) {
warnNeedsMtsdf(this, 'soft shadow');
}
if (this.shadowSoftness > 0 || this.shadowSpread > 0) {
warnOnceIfPixelArt(this, 'soft/spread shadow');
}
return this;
},

Expand Down
63 changes: 42 additions & 21 deletions src/MSDFTextWebGLRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ interface FontBinding {
unitY: number;
invRange: number;
isMtsdf: boolean;
/** 1 = pixel-art mode (bitmap glyph lane), 0 = SDF. Global, read per render. */
pixelArt: number;
/** Object-level params, pre-packed for this font. Static mode only. */
staticParams: number;
staticShadowParams: number;
Expand All @@ -234,10 +236,13 @@ function resolveBindings(src: any, baseTexture: any): number {
while (bindings.length < count) {
bindings.push({
texture: null, font: null, unitX: 0, unitY: 0, invRange: 0, isMtsdf: false,
pixelArt: 0,
staticParams: 0, staticShadowParams: 0
});
}

const pixelArt = src.scene.game.config.pixelArt === true ? 1 : 0;

for (let i = 0; i < count; i++) {
const data = runFonts[i].data;
const range = data.distanceField.distanceRange;
Expand All @@ -250,6 +255,7 @@ function resolveBindings(src: any, baseTexture: any): number {
b.unitY = range / data.atlasHeight;
b.invRange = 1 / range;
b.isMtsdf = data.distanceField.fieldType === 'mtsdf';
b.pixelArt = pixelArt;
}

return count;
Expand All @@ -274,6 +280,11 @@ function packStaticParams(src: any, count: number): void {

for (let i = 0; i < count; i++) {
const b = bindings[i];
if (b.pixelArt) {
b.staticParams = packParams(0, 0, 0, 0);
b.staticShadowParams = packParams(0, 0, 0, 0);
continue;
}
const inv = b.invRange;
const outSoft = b.isMtsdf ? src.outlineSoftness : 0;
const shSoft = b.isMtsdf ? src.shadowSoftness : 0;
Expand Down Expand Up @@ -397,12 +408,15 @@ function configureFont(
batchHandler: MSDFBatchHandlerInstance,
drawingContext: any,
unitRangeX: number,
unitRangeY: number
unitRangeY: number,
pixelArt: number
): void {
if (batchHandler.hasUnitRangeChanged(unitRangeX, unitRangeY)) {
if (batchHandler.hasUnitRangeChanged(unitRangeX, unitRangeY) ||
batchHandler.hasPixelArtChanged(pixelArt)) {
batchHandler.run(drawingContext);
}
batchHandler.setUnitRange(unitRangeX, unitRangeY);
batchHandler.setPixelArt(pixelArt);
}

/**
Expand Down Expand Up @@ -568,7 +582,7 @@ function submitDecorations(
if (r.pass !== pass) continue;

const b = bindings[r.fontIdx];
if (multiFont) configureFont(batchHandler, drawingContext, b.unitX, b.unitY);
if (multiFont) configureFont(batchHandler, drawingContext, b.unitX, b.unitY, b.pixelArt);

rectQuad.x = r.x;
rectQuad.y = r.y;
Expand Down Expand Up @@ -658,7 +672,7 @@ function submitDecorationStates(
if (s.pass !== pass || !s.visible) continue;

const b = bindings[s.fontIdx];
if (multiFont) configureFont(batchHandler, drawingContext, b.unitX, b.unitY);
if (multiFont) configureFont(batchHandler, drawingContext, b.unitX, b.unitY, b.pixelArt);

// A dashed rect spans one unit of U per dash; the phase slides that origin.
// Wrapped into [0, 1) — exact, since the pattern repeats every unit — so a
Expand Down Expand Up @@ -803,8 +817,9 @@ function MSDFTextWebGLRenderer(
// binding, so the effects degrade to the standard look per *run*.
const fontCount = resolveBindings(src, baseTexture);
const multiFont = fontCount > 1;
const pixelArt = bindings[0].pixelArt === 1;
if (!multiFont) {
configureFont(batchHandler, drawingContext, bindings[0].unitX, bindings[0].unitY);
configureFont(batchHandler, drawingContext, bindings[0].unitX, bindings[0].unitY, bindings[0].pixelArt);
}

const hasOutline = src.hasOutline();
Expand Down Expand Up @@ -845,7 +860,7 @@ function MSDFTextWebGLRenderer(
const aTL = cA * objAlpha.topLeft, aTR = cA * objAlpha.topRight;
const aBL = cA * objAlpha.bottomLeft, aBR = cA * objAlpha.bottomRight;

const oc = src.outlineColor, oA = hasOutline ? src.outlineAlpha : 0;
const oc = src.outlineColor, oA = hasOutline && !pixelArt ? src.outlineAlpha : 0;
outlineBuf.topLeft = packColor(oc, oA * objAlpha.topLeft);
outlineBuf.topRight = packColor(oc, oA * objAlpha.topRight);
outlineBuf.bottomLeft = packColor(oc, oA * objAlpha.bottomLeft);
Expand Down Expand Up @@ -912,7 +927,7 @@ function MSDFTextWebGLRenderer(
//
// A two-tone outline forces layering: the inner colour rides the fill
// attribute, which a combined fill+outline quad has already spoken for.
const layered = (src.outlineLayered || src.outlineInnerColor >= 0) && (hasOutline || perGlyph);
const layered = !pixelArt && (src.outlineLayered || src.outlineInnerColor >= 0) && (hasOutline || perGlyph);

// ── Highlight pass — pills behind everything, the shadow included. ───────
if (hasDecorations) {
Expand Down Expand Up @@ -942,7 +957,7 @@ function MSDFTextWebGLRenderer(
if (perGlyph && !glyphs![i].visible) continue;

const b = bindings[char.fontIdx];
if (multiFont) configureFont(batchHandler, drawingContext, b.unitX, b.unitY);
if (multiFont) configureFont(batchHandler, drawingContext, b.unitX, b.unitY, b.pixelArt);

if (perGlyph) {
const g = glyphs![i];
Expand All @@ -951,11 +966,13 @@ function MSDFTextWebGLRenderer(
// it dilates the same median(rgb) edge a thick outline does — so
// it rides `width`, the one channel a shadow quad leaves idle, and
// needs no clamp.
const softness = b.isMtsdf ? g.shadow.softness : zeroCorners;
const rounded = b.isMtsdf ? g.shadow.rounded : zeroCorners;
const softness = b.isMtsdf && !pixelArt ? g.shadow.softness : zeroCorners;
const rounded = b.isMtsdf && !pixelArt ? g.shadow.rounded : zeroCorners;
const weight = pixelArt ? zeroCorners : g.weight;
const spread = pixelArt ? zeroCorners : g.shadow.spread;
packAspect(shadowBuf, g.shadow.color, g.shadow.alpha);
packToneAspect(shadowToneBuf, g.shadow.innerColor);
packParamsAspect(shadowParams, g.weight, rounded, g.shadow.spread, softness, b.invRange);
packParamsAspect(shadowParams, weight, rounded, spread, softness, b.invRange);
submitOneGlyph(drawingContext, batchHandler, b.texture, quad,
g.x + swapDX + g.shadow.x, g.y + swapDY + g.shadow.y,
g.scaleX, g.scaleY, g.rotation, g.skew,
Expand All @@ -979,16 +996,18 @@ function MSDFTextWebGLRenderer(
if (perGlyph && !glyphs![i].visible) continue;

const b = bindings[char.fontIdx];
if (multiFont) configureFont(batchHandler, drawingContext, b.unitX, b.unitY);
if (multiFont) configureFont(batchHandler, drawingContext, b.unitX, b.unitY, b.pixelArt);

if (perGlyph) {
const g = glyphs![i];
const quad = resolveGlyphQuad(char, g.glyph, b.font);
const rounded = b.isMtsdf ? g.outline.rounded : zeroCorners;
const softness = b.isMtsdf ? g.outline.softness : zeroCorners;
packOutlineAspect(outlineBuf, g.outline.color, g.outline.alpha, g.outline.width, softness);
const rounded = b.isMtsdf && !pixelArt ? g.outline.rounded : zeroCorners;
const softness = b.isMtsdf && !pixelArt ? g.outline.softness : zeroCorners;
const outlineWidth = pixelArt ? zeroCorners : g.outline.width;
const weight = pixelArt ? zeroCorners : g.weight;
packOutlineAspect(outlineBuf, g.outline.color, g.outline.alpha, outlineWidth, softness);
packToneAspect(outlineToneBuf, g.outline.innerColor);
packParamsAspect(glyphParams, g.weight, rounded, g.outline.width, softness, b.invRange);
packParamsAspect(glyphParams, weight, rounded, outlineWidth, softness, b.invRange);
submitOneGlyph(drawingContext, batchHandler, b.texture, quad,
g.x + swapDX, g.y + swapDY, g.scaleX, g.scaleY, g.rotation, g.skew,
g.skewPivot, g.offsetX, g.offsetY,
Expand Down Expand Up @@ -1022,16 +1041,18 @@ function MSDFTextWebGLRenderer(
if (perGlyph && !glyphs![i].visible) continue;

const b = bindings[char.fontIdx];
if (multiFont) configureFont(batchHandler, drawingContext, b.unitX, b.unitY);
if (multiFont) configureFont(batchHandler, drawingContext, b.unitX, b.unitY, b.pixelArt);

if (perGlyph) {
const g = glyphs![i];
const quad = resolveGlyphQuad(char, g.glyph, b.font);
const rounded = b.isMtsdf ? g.outline.rounded : zeroCorners;
const softness = b.isMtsdf ? g.outline.softness : zeroCorners;
const rounded = b.isMtsdf && !pixelArt ? g.outline.rounded : zeroCorners;
const softness = b.isMtsdf && !pixelArt ? g.outline.softness : zeroCorners;
const outlineWidth = pixelArt ? zeroCorners : g.outline.width;
const weight = pixelArt ? zeroCorners : g.weight;
let outlineData = zeroColor;
if (combined) {
packOutlineAspect(outlineBuf, g.outline.color, g.outline.alpha, g.outline.width, softness);
packOutlineAspect(outlineBuf, g.outline.color, g.outline.alpha, outlineWidth, softness);
packFillAspect(fillBuf, g.fill.color, g.fill.alpha, g.outline.color);
outlineData = outlineBuf;
} else {
Expand All @@ -1040,7 +1061,7 @@ function MSDFTextWebGLRenderer(
// In the layered case the outline layer is already drawn and this
// quad's outline alpha is zero, so its width and softness are inert
// here — passed anyway, so both branches share one pack.
packParamsAspect(glyphParams, g.weight, rounded, g.outline.width, softness, b.invRange);
packParamsAspect(glyphParams, weight, rounded, outlineWidth, softness, b.invRange);
submitOneGlyph(drawingContext, batchHandler, b.texture, quad,
g.x + swapDX, g.y + swapDY, g.scaleX, g.scaleY, g.rotation, g.skew,
g.skewPivot, g.offsetX, g.offsetY,
Expand Down