From 34137fac03ad39908c871289bf538812a27c6770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 16 Jul 2026 12:09:26 +0200 Subject: [PATCH] Fix TilemapGPULayer ignoring tile rotation TilemapGPULayer rendered rotated tiles unrotated. Tiled encodes 90-degree tile rotations using the anti-diagonal flip flag, which the parser resolves into `tile.rotation` (0, 90, 180 or 270 degrees) plus `tile.flipX`. But `generateLayerDataTexture` only packed `flipX`/`flipY` into the layer-data texture and dropped `rotation` entirely, so the shader had no way to know a tile was rotated. The visible result: any tile placed with a rotation (a very common way to build directional art such as arrows from a single base tile) rendered in its un-rotated orientation, and multi-tile rotated sprites appeared torn apart. The CPU `TilemapLayer` renders these correctly (its `TransformerTile` applies `element.rotation`), so it only affected the GPU layer. Encode the rotation as a 0-3 quadrant in bits 24-25 of the layer-data texel, and in the fragment shader rotate the sampling UV about the tile centre before applying the flip flags - matching the CPU path's `Rotate . Flip` composition. The empty/animated/flip flags are unaffected: the new bits are below them and `mod(flags, 4.0)` isolates the quadrant cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/renderer/webgl/shaders/TilemapGPULayer-frag.js | 10 ++++++++++ src/renderer/webgl/shaders/src/TilemapGPULayer.frag | 11 +++++++++++ src/tilemaps/TilemapGPULayer.js | 13 +++++++++++++ 3 files changed, 34 insertions(+) diff --git a/src/renderer/webgl/shaders/TilemapGPULayer-frag.js b/src/renderer/webgl/shaders/TilemapGPULayer-frag.js index 862dfd8564..e6f2e727d4 100644 --- a/src/renderer/webgl/shaders/TilemapGPULayer-frag.js +++ b/src/renderer/webgl/shaders/TilemapGPULayer-frag.js @@ -72,6 +72,16 @@ module.exports = [ ' /* Bit 29 is animation. */', ' bool animated = mod(flags, 64.0) > 31.0;', ' #endif', + ' /* Bits 24-25 hold the tile rotation as a 0-3 quadrant (0/90/180/270 deg). */', + ' float rotQuad = mod(flags, 4.0);', + ' if (rotQuad > 0.5)', + ' {', + ' vec2 c = uv - 0.5;', + ' float ang = -rotQuad * 1.5707963267948966;', + ' float cs = cos(ang);', + ' float sn = sin(ang);', + ' uv = vec2(cs * c.x - sn * c.y, sn * c.x + cs * c.y) + 0.5;', + ' }', ' if (flipX)', ' {', ' uv.x = 1.0 - uv.x;', diff --git a/src/renderer/webgl/shaders/src/TilemapGPULayer.frag b/src/renderer/webgl/shaders/src/TilemapGPULayer.frag index 8430963a03..55305a21f9 100644 --- a/src/renderer/webgl/shaders/src/TilemapGPULayer.frag +++ b/src/renderer/webgl/shaders/src/TilemapGPULayer.frag @@ -95,6 +95,17 @@ Tile getLayerData (vec2 coord) bool animated = mod(flags, 64.0) > 31.0; #endif + /* Bits 24-25 hold the tile rotation as a 0-3 quadrant (0/90/180/270 deg). */ + float rotQuad = mod(flags, 4.0); + if (rotQuad > 0.5) + { + vec2 c = uv - 0.5; + float ang = -rotQuad * 1.5707963267948966; + float cs = cos(ang); + float sn = sin(ang); + uv = vec2(cs * c.x - sn * c.y, sn * c.x + cs * c.y) + 0.5; + } + if (flipX) { uv.x = 1.0 - uv.x; diff --git a/src/tilemaps/TilemapGPULayer.js b/src/tilemaps/TilemapGPULayer.js index 39e73fd8f1..80d6e8c656 100644 --- a/src/tilemaps/TilemapGPULayer.js +++ b/src/tilemaps/TilemapGPULayer.js @@ -217,6 +217,19 @@ var TilemapGPULayer = new Class({ { index |= 0x20000000; } + + // Encode the tile rotation as a 0-3 quadrant (0, 90, 180, 270 + // degrees) in bits 24-25. Tiled stores 90-degree rotations via + // the anti-diagonal flip flag, which the parser resolves into + // `tile.rotation`; without this the shader would render rotated + // tiles (e.g. arrows built by rotating a base tile) unrotated. + var rotQuad = Math.round(tile.rotation / (Math.PI / 2)) % 4; + if (rotQuad < 0) + { + rotQuad += 4; + } + index |= rotQuad << 24; + if (tile.index === -1) { // Set the fourth bit to 1 to indicate an empty tile.