Skip to content

Transparent Pixel Cropping

Atlases are packed with a simple guillotine algorithm.

You can specify cropTransparentPixels when loading a texture bundle to have toodle crop out any excess transparent pixels.

In this example, we load the same texture into two bundles - one with transparent pixels cropped, and one without.

From the developer perspective, the size will appear the same. But from a rendering perspective, the cropped texture will take up less memory and be offset by Toodle to be in the same position as the un-cropped texture.

WARNING

This feature is new and has not been rigorously tested. Please let us know if you encounter unexpected behavior.

ts
import { Toodle } from "@blooper.gg/toodle";

const canvas = document.querySelector("canvas")!;
const toodle = await Toodle.attach(canvas, {
  limits: { textureArrayLayers: 5 },
});

const baseUrl = "https://toodle.gg";

// State for demo
const state = {
  useOptimized: true,
  showAtlas: false,
};

const texturesToCrop = {
  Mew: new URL("/img/MewTransparentExample.png", baseUrl),
  Mew1: new URL("/img/MewTransparentExample.png", baseUrl),
  Mew2: new URL("/img/MewTransparentExample.png", baseUrl),
  Mew3: new URL("/img/MewTransparentExample.png", baseUrl),
  Mew4: new URL("/img/MewTransparentExample.png", baseUrl),
  Mew5: new URL("/img/MewTransparentExample.png", baseUrl),
  Mew6: new URL("/img/MewTransparentExample.png", baseUrl),
  Mew7: new URL("/img/MewTransparentExample.png", baseUrl),
  Mew8: new URL("/img/MewTransparentExample.png", baseUrl),
  Mew9: new URL("/img/MewTransparentExample.png", baseUrl),
  Mew10: new URL("/img/MewTransparentExample.png", baseUrl),
};

const texturesUncropped = {
  MewTwo: new URL("/img/MewTransparentExample.png", baseUrl),
  MewTwo2: new URL("/img/MewTransparentExample.png", baseUrl),
  MewTwo3: new URL("/img/MewTransparentExample.png", baseUrl),
  MewTwo4: new URL("/img/MewTransparentExample.png", baseUrl),
  MewTwo5: new URL("/img/MewTransparentExample.png", baseUrl),
};

// Cropping of extra alpha pixels is opt-in for now
await toodle.assets.registerBundle("croppedTextures", {
  textures: texturesToCrop,
  cropTransparentPixels: true,
  autoLoad: true,
});

// By default, we will not crop the extra alpha pixels
// this may change in the future once cropping is well-tested
await toodle.assets.registerBundle("baseTextures", {
  textures: texturesUncropped,
  autoLoad: true,
});

const atlasPreviewShader = toodle.QuadShader(
  "texture atlas viewer",
  toodle.limits.instanceCount,
  /*wgsl*/ `
  @vertex
  fn vert(
    @builtin(vertex_index) VertexIndex: u32,
    @builtin(instance_index) InstanceIndex: u32,
    instance: InstanceData,
  ) -> VertexOutput {
    var output = default_vertex_shader(VertexIndex, InstanceIndex, instance);
    // set uv coordinates to range the whole atlas texture and not the bounds
    output.engine_uv.x = output.engine_uv.z;
    output.engine_uv.y = output.engine_uv.w;
    return output;
  }

  @fragment
  fn fragment(vertex: VertexOutput) -> @location(0) vec4f {
    let color = default_fragment_shader(vertex, nearestSampler);
    return mix(vec4f(1.0, 0.0, 1.0, 1.0), color, step(0.1, color.a));
  }
`,
);

const showTransparentPixelsShader = toodle.QuadShader(
  "show transparent pixels",
  toodle.limits.instanceCount,
  /*wgsl*/ `
@fragment
fn fragment(vertex: VertexOutput) -> @location(0) vec4f {
  let color = default_fragment_shader(vertex, nearestSampler);
  return mix(vec4f(1.0, 0.0, 1.0, 0.2), color, step(0.1, color.a));
}
  `,
);

function frame() {
  toodle.startFrame();

  if (!state.showAtlas) {
    const textureId = state.useOptimized ? "Mew" : "MewTwo";

    toodle.draw(
      toodle.Quad(textureId, { shader: showTransparentPixelsShader }),
    );
    toodle.draw(
      toodle
        .Quad(textureId, {
          shader: showTransparentPixelsShader,
          scale: 0.15,
        })
        .setBounds({
          left: -toodle.resolution.width / 2,
          top: toodle.resolution.height / 2,
        }),
    );

    toodle.draw(
      toodle
        .Quad(textureId, {
          shader: showTransparentPixelsShader,
          scale: 0.18,
        })
        .setBounds({
          right: toodle.resolution.width / 2,
          bottom: -toodle.resolution.height / 2,
        }),
    );

    toodle.draw(
      toodle
        .Quad(textureId, {
          shader: showTransparentPixelsShader,
          idealSize: { width: 50, height: 50 },
        })
        .setBounds({
          left: -toodle.resolution.width / 2,
          bottom: -toodle.resolution.height / 2,
        }),
    );

    toodle.draw(
      toodle
        .Quad(textureId, {
          shader: showTransparentPixelsShader,
          idealSize: { width: 50, height: 50 },
          rotation: 45,
        })
        .setBounds({
          right: toodle.resolution.width / 2,
          top: toodle.resolution.height / 2,
        }),
    );
  } else {
    if (state.useOptimized) {
      toodle.draw(
        toodle.Quad("Mew", {
          idealSize: { width: 400, height: 400 },
          shader: atlasPreviewShader,
        }),
      );
    } else {
      toodle.draw(
        toodle.Quad("MewTwo", {
          idealSize: { width: 400, height: 400 },
          shader: atlasPreviewShader,
        }),
      );
    }
  }

  toodle.endFrame();

  requestAnimationFrame(frame);
}

frame();

//
// UI elements for demo
//

canvas.before(
  makeButton("Cropped", (button) => {
    state.useOptimized = !state.useOptimized;
    button.textContent = state.useOptimized ? "Cropped" : "Uncropped";
  }),
);

canvas.before("<br />");

canvas.before(
  makeButton("Scene", (button) => {
    state.showAtlas = !state.showAtlas;
    button.textContent = state.showAtlas ? "Atlas" : "Scene";
  }),
);

function makeButton(
  text: string,
  onClick: (button: HTMLButtonElement) => void,
) {
  const button = document.createElement("button");
  button.textContent = text;
  button.style.marginRight = "2vw";
  button.style.backgroundColor = "whitesmoke";
  button.style.border = "1px solid grey";
  button.style.padding = "0.4rem";
  button.addEventListener("click", (e) => {
    onClick(e.currentTarget as HTMLButtonElement);
  });
  return button;
}