Skip to content

WebGL multi-texture batch: rotated quads render transparent wedges or a neighbouring texture (exact float equality on interpolated outTexDatum in GetTexture.glsl) #7372

Description

@DainDwarf

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions