WebGL multi-texture batch: rotated quads render transparent wedges or a neighbouring texture (exact float equality on interpolated outTexDatum in GetTexture.glsl)
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 40.3k
- Forks
- 7.2k
- PR merge metrics
- No merged PRs in 30d
Description
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
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. - Independent of device pixel ratio,
Textresolution, containers, strokes, or the number of objects; it only needs ≥ 2 textures in the batch entry and a non-axis-aligned quad.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with src/renderer/webgl/shaders/src/GetTexture.glsl and trace outTexDatum back through Multi.vert; also inspect the same varying in GetTexRes.glsl. Reproduce the issue with the provided 60-Text example using rotated quads and multiple textures. Done means rotated multi-texture quads render without transparent wedges or neighbouring textures while axis-aligned and single-texture cases remain intact.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- computer-graphics
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100