Headless CLI + library for pre-processing Bevy assets. Drives Bevy's
AssetProcessor from a one-shot binary so you can bake compressed
textures (and process every other asset type whose loader you have a
plugin for) at build time, without launching a game.
cargo run --release -- assets/ assets-baked/
# Hash-based change detection skips unchanged inputs; pass --force to
# reprocess every image regardless of cache.
cargo run --release -- --force assets/ assets-baked/Status: prototype. Tracks bevy main — depends on the
compressed_image_saver feature added in bevyengine/bevy#23567.
The crate spins up a minimal App (no renderer, no window, no game
logic) configured with AssetMode::Processed and a small set of asset
plugins:
ImagePlugin— registersLoadTransformAndSave<ImageLoader, _, CompressedImageSaver>as the default processor forpng/jpg/jpeg(this isImagePlugin's built-in behavior when thecompressed_image_saverfeature is on).GltfPluginand a manualShaderLoaderregistration — pulled in solely so the processor can resolvebevy_gltf::loader::GltfLoaderandbevy_shader::shader::ShaderLoaderreferenced by source.metafiles (or synthesized for files without metas).- A hand-rolled stub for
bevy_seedling::sample::assets::SampleLoaderwith the rightTypePath— the processor's lookup-by-name resolves to it at meta deserialize time. We avoid pulling inbevy_seedlingbecause its plugin only registers the real loader after starting a cpal audio stream, which fails on Linux CI without an audio device.
Other asset types (scenes, custom loaders, etc.) need their own plugin
or stub registration added to run_bake_app, otherwise their source
.meta files fail to deserialize.
AssetProcessor then walks the input tree and produces an output tree
at the same relative paths. Images get compressed to KTX2 in place
(texture.png → output/texture.png containing KTX2 bytes), with a
.meta sidecar carrying the runtime ImageLoaderSettings. Non-image
files fall through the processor's no-processor branch and are
byte-copied unchanged with a matching .meta.
The game then runs in AssetMode::Processed and reads from the output
tree; the .meta sidecars tell the asset server which loader to use
for each file.
AssetProcessor reads source .meta files to decide what to do with
each asset. There are three actions:
AssetAction::Process { processor, settings }— run the named processor (e.g.LoadTransformAndSave<...>to compress an image).AssetAction::Load { loader, settings }— byte-copy the file and emit a meta in the output that points at the named loader.AssetAction::Ignore— skip the file entirely.
When a file has no source .meta, the processor synthesizes one based
on extension: it picks a default processor if one is registered for the
extension, otherwise a default loader, otherwise Ignore.
Practical consequence: if you want a .png compressed, its
source meta must say Process (or there must be no source meta — the
processor's default for png/jpg/jpeg is the compression
processor). If the source meta says Load, the file is byte-copied
uncompressed.
- clang —
ctt-compressonator(the encoder behindCompressedImageSaver) uses-march=knl, which GCC ≥ 15 doesn't recognize. SetCXX=clang++if the default compiler is GCC.
CompressedImageSaver has no "this is a normal map" hint; CTT
compresses every channel independently. Renormalize in your shader
after sampling/blending, or pre-compress with a normal-aware tool.
Bevy's in-engine usage of the saver has the same property.
WebGPU's BC support exists on desktop Chrome/Edge/Firefox but not
Safari/mobile. If you target broad wasm, you'll want
compressed_image_saver_universal (UASTC/Basis, no mipmaps) — Bevy
doesn't yet have a story for serving format-variants per device.
AssetProcessor writes a .meta next to every processed asset
holding the loader settings (sampler, sRGB flag, etc.). They're tiny
and load-time authoritative; ship them with your processed assets.
The processor writes a transaction log to
<output>/imported_assets/log for crash recovery. It's harmless and
not needed by the game at runtime, but currently isn't cleaned up.
use bevy_asset_preprocess::{preprocess, PreprocessConfig};
let stats = preprocess(
Path::new("assets"),
Path::new("assets-baked"),
&PreprocessConfig { force: false },
)?;The cli feature gates walkdir and the bevy-asset-preprocess
binary; library consumers can default-features = false to drop both.
MIT OR Apache-2.0, matching the Bevy ecosystem.