Version
- Phaser Version: 4.2.1 (also present on
master)
- Operating system: Windows 11
- Browser: Chrome / Chromium 140 (headless and headed), WebGL renderer
Description
When several textured quads with different textures share one multi-texture batch and any of them is rotated (even by 1°), parts of the rotated quads render as transparent wedges, and some fragments render with a neighbouring texture from the same batch. Axis-aligned quads are never affected. Quads that all share one texture are never affected either.
Text objects are the easiest way to hit it, because every Text owns its own texture, so a screen with a few dozen Text objects on a slightly rotated parent (a fanned hand of cards, in our case) shows torn text and letters from other Text objects.
Cause
src/renderer/webgl/shaders/src/GetTexture.glsl selects the sampler with exact float equality on an interpolated varying:
if (outTexDatum == 0.0) return texture2D(uMainSampler[0], texCoord);
#define ELSE_TEX_CASE(INDEX) else if (outTexDatum == float(INDEX)) return texture2D(uMainSampler[INDEX], texCoord);
...
else return vec4(0.0, 0.0, 0.0, 0.0);
outTexDatum is a varying float written from the inTexDatum attribute in Multi.vert. All four vertices of a quad carry the same integer, but the rasteriser interpolates it across each triangle. For an axis-aligned quad the interpolation happens to be exact; for a rotated quad it drifts by a rounding error, so fragments either match no case (→ vec4(0.0), the transparent wedges) or the adjacent case (→ the wrong texture). GetTexRes.glsl reads the same varying (float texId = outTexDatum;).
I verified the corners reaching BatchHandlerQuad#batch are exact parallelograms, so the transform side is fine; the problem is purely the comparison in the fragment shader.
Suggested fix
Compare with a tolerance (WebGL1 has no flat varyings), e.g.
if (outTexDatum < 0.5) return texture2D(uMainSampler[0], texCoord);
#define ELSE_TEX_CASE(INDEX) else if (outTexDatum < float(INDEX) + 0.5) return texture2D(uMainSampler[INDEX], texCoord);
or int(floor(outTexDatum + 0.5)) == INDEX. I hot-patched the GetTexture addition this way through the program manager at runtime and the tearing disappeared completely at 1° and at 45°. Setting renderNodes.setMaxParallelTextureUnits(1) (single-texture batches take the TEXTURE_COUNT == 1 path, no comparison) also removes it, which is the workaround we use meanwhile.
Example Test Code
Top five rows: 60 Text objects rotated by 1°. Bottom five rows: the same 60 at angle 0. Each has its own opaque background colour, so any dark wedge or foreign colour inside a box comes from the renderer. On my machine roughly a third of the rotated boxes are torn on every frame; the unrotated rows are perfect.
<!doctype html>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/phaser@4.2.1/dist/phaser.min.js"></script>
<body style="margin:0;background:#111">
<script>
new Phaser.Game({
type: Phaser.WEBGL,
width: 800,
height: 360,
backgroundColor: '#111111',
scene: {
create() {
const place = (row, angle) => {
for (let i = 0; i < 60; i++) {
this.add.text(20 + (i % 12) * 64, row + Math.floor(i / 12) * 26, 'w' + i, {
fontFamily: 'sans-serif',
fontSize: '12px',
color: '#000000',
backgroundColor: 'hsl(' + (i * 6) + ',100%,50%)',
padding: { x: 8, y: 3 },
}).setAngle(angle);
}
};
place(20, 1); // rotated by 1 degree: torn
place(190, 0); // axis-aligned: intact
},
},
});
</script>
Additional Information
- Not related to
Text specifically: tinted Images in a rotated container were intact only because they all shared __WHITE and therefore compiled to the single-sampler path; give them different textures and they should tear the same way.
- Independent of device pixel ratio,
Text resolution, containers, strokes, or the number of objects; it only needs ≥ 2 textures in the batch entry and a non-axis-aligned quad.
Version
master)Description
When several textured quads with different textures share one multi-texture batch and any of them is rotated (even by 1°), parts of the rotated quads render as transparent wedges, and some fragments render with a neighbouring texture from the same batch. Axis-aligned quads are never affected. Quads that all share one texture are never affected either.
Textobjects are the easiest way to hit it, because everyTextowns its own texture, so a screen with a few dozenTextobjects on a slightly rotated parent (a fanned hand of cards, in our case) shows torn text and letters from otherTextobjects.Cause
src/renderer/webgl/shaders/src/GetTexture.glslselects the sampler with exact float equality on an interpolated varying:outTexDatumis avarying floatwritten from theinTexDatumattribute inMulti.vert. All four vertices of a quad carry the same integer, but the rasteriser interpolates it across each triangle. For an axis-aligned quad the interpolation happens to be exact; for a rotated quad it drifts by a rounding error, so fragments either match no case (→vec4(0.0), the transparent wedges) or the adjacent case (→ the wrong texture).GetTexRes.glslreads the same varying (float texId = outTexDatum;).I verified the corners reaching
BatchHandlerQuad#batchare exact parallelograms, so the transform side is fine; the problem is purely the comparison in the fragment shader.Suggested fix
Compare with a tolerance (WebGL1 has no
flatvaryings), e.g.or
int(floor(outTexDatum + 0.5)) == INDEX. I hot-patched theGetTextureaddition this way through the program manager at runtime and the tearing disappeared completely at 1° and at 45°. SettingrenderNodes.setMaxParallelTextureUnits(1)(single-texture batches take theTEXTURE_COUNT == 1path, no comparison) also removes it, which is the workaround we use meanwhile.Example Test Code
Top five rows: 60
Textobjects rotated by 1°. Bottom five rows: the same 60 at angle 0. Each has its own opaque background colour, so any dark wedge or foreign colour inside a box comes from the renderer. On my machine roughly a third of the rotated boxes are torn on every frame; the unrotated rows are perfect.Additional Information
Textspecifically: tintedImages in a rotated container were intact only because they all shared__WHITEand therefore compiled to the single-sampler path; give them different textures and they should tear the same way.Textresolution, containers, strokes, or the number of objects; it only needs ≥ 2 textures in the batch entry and a non-axis-aligned quad.