Skip to content

bevy_image::TextureAtlasBuilder: Add image extrusion support #22775

@ickshonpe

Description

@ickshonpe

What problem does this solve or what need does it fill?

TextureAtlasBuilder currently packs images edge-to-edge, with support to optionally add padding between each image.

Padding works by inserting N (set using the TextureAtlasBuilder::padding, can be 2) transparent pixels to the right and bottom of each image in the atlas texture, increasing the spacing between atlas regions.

Padding is intended to reduce sampling artifacts but it's sometimes insufficient when used with:

  • linear filtering
  • scaled, rotated, or transformed sprites
  • mipmaps
  • sampling near boundaries

What solution would you like?

Add support for image extrusion to TextureAtlasBuilder and DynamicTextureAtlasBuilder.

Extrusion is where the border pixels of each image added to the atlas are duplicated outward by N pixels, creating a border that matches the edge color of the image. Corner regions are filled by duplicating the nearest corner pixel of the image. Then sampling across the boundary of the image will only read texels belonging to the image.

Implementation

The changes should be relatively straightforward and don't require any specialist rendering knowledge. Extrusion is not too different from the current padding fill except using the edge colors instead of transparency. The only awkwardish part is that padding is added only to the bottom and right edges of each sub-texture, whereas the extruded border needs to added on all four edges.

Basic example comparing padding and extrusion

use bevy::prelude::*;
use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat};
use bevy::window::WindowResolution;
use bevy_asset::RenderAssetUsages;
use std::f32::consts::PI;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                resolution: WindowResolution::new(800, 450).with_scale_factor_override(1.0),
                ..default()
            }),
            ..default()
        }))
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
    commands.spawn(Camera2d);

    // Generate a 64x64 image with 8 pixels of transparent padding along each edge
    let width: u32 = 64;
    let height: u32 = 64;
    let border: u32 = 8;

    let mut data = vec![0u8; (width * height * 4) as usize];
    for y in border..(height - border) {
        for x in border..(width - border) {
            let i = ((y * width + x) * 4) as usize;
            data[i + 0] = 255; // R
            data[i + 1] = 255; // G
            data[i + 2] = 255; // B
            data[i + 3] = 255; // A
        }
    }

    let image = Image::new(
        Extent3d {
            width,
            height,
            depth_or_array_layers: 1,
        },
        TextureDimension::D2,
        data,
        TextureFormat::Rgba8UnormSrgb,
        RenderAssetUsages::all(),
    );

    let image = images.add(image);

    commands.spawn((
        Sprite {
            image,
            // Draw a region of the sprite to emulate padding and extrusion.
            // The top and left edges of the region are at the transparent border boundary.
            // The bottom and right edges of the region are well inside the white interior of the image.
            rect: Some(Rect {
                min: Vec2::splat(border as f32),
                max: Vec2::splat(border as f32 + 16.),
            }),
            ..default()
        },
        // Transform the sprite to maximise haloing
        Transform {
            translation: Vec3::new(-0.5, -0.5, 0.),
            scale: Vec3::new(10., 10., 0.),
            rotation: Quat::from_rotation_z(PI / 8.),
            ..default()
        },
    ));
}

This creates a white image with a transparent boundary. To emulate a padded texture atlas image, the image's top and left edges of the region are at the transparent border boundary. And to emulate an extruded texture atlas image, its bottom and right edges of the region are well inside the white interior of the image. Then the image is displayed using a sprite with its transform set to maximize the observed artifacts:

Image

What alternative(s) have you considered?

You can manipulate the UVs or clamp them in the shader, but it's significantly more complicated and error prone.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-RenderingDrawing game state to the screenC-BugAn unexpected or incorrect behaviorC-FeatureA new feature, making something new possibleD-ModestA "normal" level of difficulty; suitable for simple features or challenging fixesS-Ready-For-ImplementationThis issue is ready for an implementation PR. Go for it!

    Type

    No type

    Projects

    Status

    Needs SME Triage

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions