html, body { margin: 0; }
import { Random, range } from "https://esm.sh/@byloth/core";

import { Color, DataArrayTexture, Mesh, LinearMipmapLinearFilter, LinearFilter, MeshBasicNodeMaterial, NearestFilter, PlaneGeometry, PerspectiveCamera, Scene, TextureLoader, Vector4, WebGPURenderer } from "https://esm.sh/three/webgpu";
import { div, Fn, sub, texture, uniformArray, uv, vec2 } from "https://esm.sh/three/tsl";

import { OrbitControls } from "https://esm.sh/three/addons/controls/OrbitControls.js";

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 MAP_WIDTH = 520;
    const MAP_HEIGHT = 520;

    const STAGE_WIDTH = 5;
    const STAGE_HEIGHT = 5;

    const STAGE_TILES_X = 4;
    const STAGE_TILES_Y = 4;
    const STAGE_TILES_COUNT = STAGE_TILES_X * STAGE_TILES_Y;

    const TILES_X = 4;
    const TILES_Y = 4;
    const TILES_COUNT = TILES_X * TILES_Y;

    const TILE_OUTER_WIDTH = MAP_WIDTH / TILES_X;
    const TILE_OUTER_HEIGHT = MAP_HEIGHT / TILES_Y;
    const TILE_OUTER_PIXELS = TILE_OUTER_WIDTH * TILE_OUTER_HEIGHT;

    const TILE_INNER_WIDTH = 120;
    const TILE_INNER_HEIGHT = 120;

    const COLOR_DEPTH = 4;

    const geometry = new PlaneGeometry(STAGE_WIDTH, STAGE_HEIGHT, STAGE_TILES_X, STAGE_TILES_Y);

    const packedTexture = await new TextureLoader().loadAsync("https://files.byloth.dev/grid.png");

    // packedTexture.magFilter = NearestFilter;
    // packedTexture.minFilter = NearestFilter;

    const _tmpCanvas = document.createElement("canvas");
    _tmpCanvas.width = MAP_WIDTH;
    _tmpCanvas.height = MAP_HEIGHT;

    const _tmpContext = _tmpCanvas.getContext("2d");
    _tmpContext.drawImage(packedTexture.image, 0, 0);

    const imgData = _tmpContext.getImageData(0, 0, MAP_WIDTH, MAP_HEIGHT).data;
    const textureData = new Uint8Array(TILE_OUTER_PIXELS * TILES_COUNT * COLOR_DEPTH);

    for (const yIndex of range(TILES_Y))
    {
        for (const xIndex of range(TILES_X))
        {
            const index = (yIndex * TILES_X) + xIndex;

            const frame = {
                x: xIndex * TILE_OUTER_WIDTH,
                y: yIndex * TILE_OUTER_HEIGHT,
                w: TILE_OUTER_WIDTH,
                h: TILE_OUTER_HEIGHT
            };

            for (const y of range(TILE_OUTER_HEIGHT))
            {
                for (const x of range(TILE_OUTER_WIDTH))
                {
                    const sourceIndex = ((frame.y + y) * MAP_WIDTH + (frame.x + x)) * COLOR_DEPTH;
                    const targetIndex = ((index * TILE_OUTER_PIXELS) + (y * TILE_OUTER_WIDTH + x)) * COLOR_DEPTH;

                    for (const depth of range(COLOR_DEPTH))
                    {
                        textureData[targetIndex + depth] = imgData[sourceIndex + depth];
                    }
                }
            }
        }
    }

    const map = new DataArrayTexture(textureData, TILE_OUTER_WIDTH, TILE_OUTER_HEIGHT, TILES_COUNT);

    map.needsUpdate = true;
    map.magFilter = LinearFilter;
    map.minFilter = LinearMipmapLinearFilter;

    const material = new MeshBasicNodeMaterial({ precision: "highp", transparent: true });

    const tileArray = new Array(STAGE_TILES_COUNT);
    for (const yIndex of range(STAGE_TILES_Y))
    {
        for (const xIndex of range(STAGE_TILES_X))
        {
            const index = (yIndex * STAGE_TILES_X) + xIndex;

            tileArray[index] = Random.Integer(TILES_COUNT);
        }
    }

    const mapSize = vec2(MAP_WIDTH, MAP_HEIGHT).toVar();
    const tileCount = vec2(STAGE_TILES_X, STAGE_TILES_Y).toVar();

    const outerTileSize = vec2(TILE_OUTER_WIDTH, TILE_OUTER_HEIGHT).toVar();
    const innerTileSize = vec2(TILE_INNER_WIDTH, TILE_INNER_HEIGHT).toVar();
    const innerTileRatio = innerTileSize.div(outerTileSize)
        .toVar();

    const tileBuffer = uniformArray(tileArray);

    material.colorNode = Fn(() =>
    {
        const vUv = uv().mul(tileCount)
            .toVar();

        const tileCoords = vUv.floor()
            .toVar();

        const tileIndex = tileCoords.y
            .mul(tileCount.x)
            .add(tileCoords.x)
            .toInt();

        const tileDepth = tileBuffer.element(tileIndex)
            .toVar();

        const tileOffset = vec2(5).div(outerTileSize);

        const _halfOverSize = div(0.5, innerTileSize).toVar();
      
        const tileUv = vUv.fract()
            .mul(innerTileRatio)
            .add(tileOffset)
            .clamp(_halfOverSize, sub(1, _halfOverSize));

        return texture(map, tileUv).depth(tileDepth);
    })();

    /* setInterval(() =>
    {
        const index = (Random.Integer(STAGE_TILES_Y) * STAGE_TILES_X) + Random.Integer(STAGE_TILES_X);

        const element = tileBuffer.array[index];

        element.x = Random.Integer(TILES_X) * TILE_OUTER_WIDTH + 5;
        element.y = Random.Integer(TILES_Y) * TILE_OUTER_HEIGHT + 5;

    }, 250); */

    scene.add(new Mesh(geometry, material));

    renderer.setAnimationLoop(() =>
    {
        controls.update();
        renderer.render(scene, camera);
    });
}

window.addEventListener("DOMContentLoaded", main);
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.