Skip to content

Tile Shader

Unstable API

The API for defining shaders is a first pass and is likely to change as we get usage feedback. See shader-default.md for more information.

ts
import { Toodle } from "@bloopjs/toodle";

const canvas = document.querySelector("canvas")!;

const toodle = await Toodle.attach(canvas, {
  filter: "linear",
  limits: { textureArrayLayers: 5 },
});

await toodle.assets.registerBundle("tiles", {
  textures: {
    mushroom: new URL("img/Mushroom.png", "https://toodle.gg"),
  },
  autoLoad: true,
});

const shader = toodle.QuadShader(
  "tile",
  3,
  /*wgsl*/ `

struct Tile {
  count: vec2f,
}

@fragment
fn frag(vertex: VertexOutput) -> @location(0) vec4f {
  // Compute atlas scale using screen-space derivatives
  // This tells us how much the atlas UV changes per unit of normalized UV
  let scale = vec2f(
    dpdx(vertex.engine_uv.x) / dpdx(vertex.engine_uv.z),
    dpdy(vertex.engine_uv.y) / dpdy(vertex.engine_uv.w)
  );

  // Derive atlas min (the UV at the bottom-left corner of the sprite in the atlas)
  let atlasMin = vertex.engine_uv.xy - vertex.engine_uv.zw * scale;

  // Tile the normalized UVs using fract() to wrap
  let tiledUv = fract(vertex.engine_uv.zw * vertex.tile_count);

  // Map tiled UVs back to atlas coordinates
  let tiledAtlasUv = atlasMin + tiledUv * scale;

  // Sample from the texture atlas using nearest sampling for pixel-perfect tiling
  let color = textureSample(textureArray, nearestSampler, tiledAtlasUv, vertex.engine_atlasIndex);

  // Reference linearSampler to prevent it from being optimized away
  _ = textureSample(textureArray, linearSampler, tiledAtlasUv, vertex.engine_atlasIndex);

  return color * vertex.engine_tint;
}
  `,
);

// Get the intrinsic size of the sprite
const spriteSize = toodle.assets.getSize("mushroom");

// Define the quad size (fills most of the canvas)
const quadSize = {
  width: toodle.resolution.width,
  height: toodle.resolution.height,
};

// Compute tile count: how many times the sprite fits in the quad
const tileCount = {
  x: quadSize.width / spriteSize.width,
  y: quadSize.height / spriteSize.height,
};

const quad = toodle.Quad("mushroom", {
  size: quadSize,
  shader,
  writeInstance: (array, offset) => {
    // Write tile_count as vec2f
    // offset - 2 accounts for the struct alignment (similar to fill shader)
    array.set([tileCount.x, tileCount.y], offset - 2);
  },
});

async function frame() {
  toodle.startFrame();
  toodle.draw(quad);
  toodle.endFrame();
  requestAnimationFrame(frame);
}

frame();