diff --git a/src/MSDFBatchHandler.ts b/src/MSDFBatchHandler.ts index de9e988..cb67e1a 100644 --- a/src/MSDFBatchHandler.ts +++ b/src/MSDFBatchHandler.ts @@ -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. @@ -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. @@ -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 ────────────────────────────────────────────────────── @@ -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; @@ -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 { @@ -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); diff --git a/src/MSDFFontFile.ts b/src/MSDFFontFile.ts index 1994b26..3f14c1b 100644 --- a/src/MSDFFontFile.ts +++ b/src/MSDFFontFile.ts @@ -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 diff --git a/src/MSDFText.ts b/src/MSDFText.ts index 0ef0197..c4b190d 100644 --- a/src/MSDFText.ts +++ b/src/MSDFText.ts @@ -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. @@ -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; }, @@ -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; }, @@ -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; }, diff --git a/src/MSDFTextWebGLRenderer.ts b/src/MSDFTextWebGLRenderer.ts index add01f8..a8f2122 100644 --- a/src/MSDFTextWebGLRenderer.ts +++ b/src/MSDFTextWebGLRenderer.ts @@ -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; @@ -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; @@ -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; @@ -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; @@ -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); } /** @@ -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; @@ -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 @@ -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(); @@ -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); @@ -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) { @@ -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]; @@ -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, @@ -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, @@ -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 { @@ -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,