Maybe try the following code in your pens and see if it works. It is your original with slightly modified HTML section and imports in JS.
They both appeared the same to me on Windows desktop computer, with WebGL running in Firefox and WebGPU running in Opera browser.
HTML
<script async src="https://cdn.jsdelivr.net/npm/es-module-shims@1.10.0/dist/es-module-shims.min.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.173.0/build/three.webgpu.min.js",
"three/webgpu": "https://cdn.jsdelivr.net/npm/three@0.173.0/build/three.webgpu.min.js",
"three/tsl": "https://cdn.jsdelivr.net/npm/three@0.173.0/build/three.tsl.min.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.173.0/examples/jsm/",
"three/examples/jsm/": "https://cdn.jsdelivr.net/npm/three@0.173.0/examples/jsm/"
}
}
</script>
JS
import {
Color,
DataArrayTexture,
DataTexture,
FloatType,
LinearSRGBColorSpace,
Mesh,
MeshBasicNodeMaterial,
Object3D,
PerspectiveCamera,
PlaneGeometry,
RedFormat,
Scene,
SRGBColorSpace,
TextureLoader,
WebGPURenderer,
} from "three";
import {
convertColorSpace,
div,
Fn,
If,
Loop,
max,
min,
mix,
sub,
texture as texture2d,
uv,
vec2,
vec4
} from "three/tsl";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
const TILE_AXIS = 4;
const TILE_ATLAS = [
{
"identifier": "tile-transparent",
"source": { "x": 5, "y": 5, "width": 120, "height": 120 },
"flipH": false,
"flipV": false
},
{
"identifier": "tile-delimiter",
"source": { "x": 135, "y": 135, "width": 120, "height": 120 },
"flipH": false,
"flipV": false
},
{
"identifier": "tile-hover",
"source": { "x": 5, "y": 135, "width": 120, "height": 120 },
"flipH": false,
"flipV": false
},
{
"identifier": "tile-path",
"source": { "x": 5, "y": 265, "width": 120, "height": 120 },
"flipH": false,
"flipV": false
},
{
"identifier": "tile-top-wall",
"source": { "x": 265, "y": 5, "width": 120, "height": 120 },
"flipH": false,
"flipV": true
},
{
"identifier": "tile-bottom-wall",
"source": { "x": 265, "y": 5, "width": 120, "height": 120 },
"flipH": false,
"flipV": false
},
{
"identifier": "tile-left-wall",
"source": { "x": 135, "y": 5, "width": 120, "height": 120 },
"flipH": false,
"flipV": false
},
{
"identifier": "tile-right-wall",
"source": { "x": 135, "y": 5, "width": 120, "height": 120 },
"flipH": true,
"flipV": false
},
{
"identifier": "tile-top_left-corner",
"source": { "x": 265, "y": 135, "width": 120, "height": 120 },
"flipH": false,
"flipV": true
},
{
"identifier": "tile-top_right-corner",
"source": { "x": 265, "y": 135, "width": 120, "height": 120 },
"flipH": true,
"flipV": true
},
{
"identifier": "tile-bottom_left-corner",
"source": { "x": 265, "y": 135, "width": 120, "height": 120 },
"flipH": false,
"flipV": false
},
{
"identifier": "tile-bottom_right-corner",
"source": { "x": 265, "y": 135, "width": 120, "height": 120 },
"flipH": true,
"flipV": false
},
{
"identifier": "tile-vertical-hallway",
"source": { "x": 135, "y": 265, "width": 120, "height": 120 },
"flipH": false,
"flipV": false
},
{
"identifier": "tile-horizontal-hallway",
"source": { "x": 265, "y": 265, "width": 120, "height": 120 },
"flipH": false,
"flipV": false
},
{
"identifier": "tile-top-dead_end",
"source": { "x": 135, "y": 395, "width": 120, "height": 120 },
"flipH": false,
"flipV": true
},
{
"identifier": "tile-bottom-dead_end",
"source": { "x": 135, "y": 395, "width": 120, "height": 120 },
"flipH": false,
"flipV": false
},
{
"identifier": "tile-left-dead_end",
"source": { "x": 265, "y": 395, "width": 120, "height": 120 },
"flipH": false,
"flipV": false
},
{
"identifier": "tile-right-dead_end",
"source": { "x": 265, "y": 395, "width": 120, "height": 120 },
"flipH": true,
"flipV": false
},
{
"identifier": "tile-block",
"source": { "x": 5, "y": 395, "width": 120, "height": 120 },
"flipH": false,
"flipV": false
}
];
const TILE_INDEXES = [
14, 8, 13, 9, 16, 13, 13, 4, 17, 8, 13, 4, 4, 17, 8, 9,
10, 11, 8, 7, 8, 13, 13, 7, 8, 11, 8, 11, 10, 13, 11, 12,
16, 9, 12, 12, 12, 16, 13, 7, 10, 9, 10, 13, 13, 9, 14, 12,
8, 11, 12, 15, 10, 13, 9, 10, 13, 5, 17, 8, 17, 12, 10, 11,
10, 9, 10, 4, 13, 13, 7, 16, 9, 8, 13, 5, 9, 12, 8, 17,
8, 5, 13, 11, 8, 9, 6, 13, 11, 12, 14, 8, 11, 12, 6, 9,
10, 9, 16, 9, 12, 10, 11, 8, 13, 11, 6, 11, 8, 11, 12, 12,
14, 10, 9, 12, 12, 8, 17, 12, 16, 13, 7, 8, 5, 9, 15, 12,
12, 16, 5, 11, 12, 10, 9, 12, 8, 13, 11, 10, 9, 10, 17, 12,
10, 4, 13, 9, 10, 9, 12, 12, 10, 4, 13, 4, 11, 14, 8, 11,
14, 10, 17, 10, 4, 11, 10, 11, 16, 11, 8, 5, 17, 10, 7, 14,
12, 8, 9, 14, 12, 8, 4, 17, 8, 13, 5, 13, 13, 13, 3, 11,
6, 11, 12, 12, 10, 11, 12, 8, 11, 8, 13, 13, 13, 13, 11, 14,
12, 16, 5, 5, 13, 9, 12, 15, 8, 5, 13, 17, 8, 13, 13, 7,
10, 9, 16, 9, 8, 11, 10, 9, 12, 8, 4, 9, 10, 13, 9, 12,
16, 5, 13, 11, 10, 13, 13, 11, 10, 11, 15, 10, 13, 13, 11, 15
];
const TILE_URL = "https://files.byloth.dev/codepen/grid.png";
const TILEMAP_OPTIONS = {
defaultTile: 2,
layers: 1,
precision: "lowp",
size: { width: 7.5, height: 7.5 },
tiles: { x: 16, y: 16 },
transparent: false
};
class TileMap extends Object3D
{
static get _DefaultOptions()
{
return {
defaultTile: 0,
layers: 1,
precision: "lowp",
size: { width: 1, height: 1 },
tiles: { x: 1, y: 1 },
transparent: false
};
}
#_mapPath;
#_options;
#_geometry;
#_material;
#_mesh;
#_map;
#_mapSize;
#_tileIndexes;
#_tileSources;
#_tileFlags;
#_tilesCount;
get tilesCount()
{
return this.#_tilesCount;
}
constructor(mapPath, options = undefined)
{
super();
this.#_tilesCount = 0;
this.#_mapPath = mapPath;
this.#_options = { ...TileMap._DefaultOptions, ...options };
const { precision, size, tiles, transparent } = this.#_options;
this.#_geometry = new PlaneGeometry(size.width, size.height, tiles.x, tiles.y);
this.#_material = new MeshBasicNodeMaterial({ precision, transparent });
this.#_mesh = new Mesh(this.#_geometry, this.#_material);
}
get #_colorShader()
{
const { width: mW, height: mH } = this.#_mapSize;
const { layers } = this.#_options;
const { x: tX, y: tY } = this.#_options.tiles;
const mapSize = vec2(mW, mH).toVar();
const tiles = vec2(tX, tY).toVar();
const getLod = () =>
{
const sizeUV = uv().mul(mapSize)
.toVar();
const dX = sizeUV.dFdx().length();
const dY = sizeUV.dFdy().length();
return max(dX, dY).log2();
};
const _tiles = tiles.sub(1)
.toVar();
const _readIndex = (layer, coords) => texture2d(this.#_tileIndexes, coords.div(_tiles)).depth(layer)
.toInt();
const _sources = vec2(this.#_tilesCount, TILE_AXIS).sub(1)
.toVar();
const _readSource = (index, property) => texture2d(this.#_tileSources, vec2(index, property).div(_sources))
.toInt();
const _readFlag = (index) => texture2d(this.#_tileFlags, vec2(index, 0).div(this.#_tilesCount - 1)).xy;
const getSource = (index) =>
{
const x = _readSource(index, 0);
const y = _readSource(index, 1);
const z = _readSource(index, 2);
const w = _readSource(index, 3);
return vec4(x, y, z, w);
};
const halfTexel = div(0.5, mapSize).toVar();
const getTile = (layer, coords) =>
{
const tileIndex = _readIndex(layer, coords).toVar();
const source = getSource(tileIndex).toVar();
const offset = source.xy.div(mapSize)
.add(halfTexel)
.toVar();
const size = source.zw.div(mapSize)
.sub(halfTexel)
.toVar();
const limit = offset.add(size)
.sub(halfTexel)
.toVar();
return {
min: offset,
max: limit,
getUV: (tileUV) =>
{
const _uv = tileUV.fract()
.toVar();
const flipped = _readFlag(tileIndex).toVar();
If(flipped.x, () => { _uv.x.assign(sub(1, _uv.x)); });
If(flipped.y, () => { _uv.y.assign(sub(1, _uv.y)); });
_uv.mulAssign(size)
.addAssign(offset);
return _uv;
}
};
};
return Fn((builder) =>
{
const vUV = uv().mul(tiles)
.clamp(halfTexel, tiles.sub(halfTexel))
.toVar();
const coords = vUV.floor()
.toVar();
const lod = getLod()
.toVar();
const result = vec4(0, 0, 0, 0).toVar();
Loop(layers, ({ i: layer }) =>
{
const tile = getTile(layer, coords);
const tileUV = tile.getUV(vUV)
.toVar();
const color = texture2d(this.#_map, tileUV, lod);
result.rgb.assign(mix(result.rgb, color.rgb, color.a));
result.a.assign(min(result.a.add(color.a), 1));
});
return convertColorSpace(result, SRGBColorSpace, LinearSRGBColorSpace);
});
}
async initialize(scene)
{
this.#_map = await new TextureLoader().loadAsync(this.#_mapPath);
const { width, height } = this.#_map.image;
const { layers } = this.#_options;
const { x: tX, y: tY } = this.#_options.tiles;
const tiles = TILE_ATLAS;
this.#_mapSize = { width, height };
this.#_tilesCount = tiles.length;
const indexes = new Float32Array((tX * tY) * layers);
for (let index = 0; index < indexes.length; index += 1)
{
indexes[index] = this.#_options.defaultTile;
}
this.#_tileIndexes = new DataArrayTexture(indexes, tX, tY, layers);
this.#_tileIndexes.format = RedFormat;
this.#_tileIndexes.type = FloatType;
this.#_tileIndexes.needsUpdate = true;
const sources = new Float32Array(tiles.length * TILE_AXIS);
const flags = new Uint8Array(tiles.length * 4);
for (let index = 0; index < tiles.length; index += 1)
{
const { source, flipH, flipV } = tiles[index];
const { x, y, width: z, height: w } = source;
sources[index] = x;
sources[(tiles.length * 1) + index] = (height - y) - w;
sources[(tiles.length * 2) + index] = z;
sources[(tiles.length * 3) + index] = w;
flags[(index * 4)] = flipH ? 255 : 0;
flags[(index * 4) + 1] = flipV ? 255 : 0;
}
this.#_tileSources = new DataTexture(sources, tiles.length, TILE_AXIS);
this.#_tileSources.format = RedFormat;
this.#_tileSources.type = FloatType;
this.#_tileSources.needsUpdate = true;
this.#_tileFlags = new DataTexture(flags, tiles.length, 1);
this.#_tileFlags.needsUpdate = true;
this.#_material.colorNode = this.#_colorShader();
this.add(this.#_mesh);
scene.add(this);
}
setTile(layer, positions, tileIndex)
{
const { x: tX, y: tY } = this.#_options.tiles;
const tSize = tX * tY;
if (!(positions instanceof Array))
{
positions = [positions];
}
for (let { x, y } of positions)
{
y = (tY - 1) - y;
const index = (tSize * layer) + (y * tX) + x;
this.#_tileIndexes.image.data[index] = tileIndex;
}
this.#_tileIndexes.needsUpdate = true;
}
}
async function main()
{
const renderer = new WebGPURenderer({ antialias: true });
renderer.setClearColor(new Color(0x3f5f7f));
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new Scene();
const camera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
const controls = new OrbitControls(camera, renderer.domElement);
window.addEventListener("resize", () =>
{
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});
const tileMap = new TileMap(TILE_URL, TILEMAP_OPTIONS);
await tileMap.initialize(scene);
for (let i = 0; i < TILE_INDEXES.length; i += 1)
{
tileMap.setTile(0, { x: i % 16, y: Math.floor(i / 16) }, TILE_INDEXES[i]);
}
renderer.setAnimationLoop(() =>
{
controls.update();
renderer.render(scene, camera);
});
}
window.addEventListener("DOMContentLoaded", main);