https://github.com/pavlov-net/bevy_asset_preprocess
Headless CLI/library that compresses Bevy image assets to KTX2 (BCn/ASTC + mipmaps + zstd) via CompressedImageSaver
https://github.com/pavlov-net/bevy_asset_preprocess
asset-pipeline bevy bevy-engine game-development ktx2 mipmaps rust texture-compression
Last synced: 15 days ago
JSON representation
Headless CLI/library that compresses Bevy image assets to KTX2 (BCn/ASTC + mipmaps + zstd) via CompressedImageSaver
- Host: GitHub
- URL: https://github.com/pavlov-net/bevy_asset_preprocess
- Owner: pavlov-net
- Created: 2026-05-04T18:26:09.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-04T19:12:09.000Z (about 1 month ago)
- Last Synced: 2026-05-04T20:37:10.648Z (about 1 month ago)
- Topics: asset-pipeline, bevy, bevy-engine, game-development, ktx2, mipmaps, rust, texture-compression
- Language: Rust
- Size: 29.3 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# bevy_asset_preprocess
Headless CLI + library for pre-processing Bevy assets. Drives Bevy's
[`AssetProcessor`][ap] 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.
[ap]: https://docs.rs/bevy_asset/latest/bevy_asset/processor/struct.AssetProcessor.html
```bash
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][pr].
[pr]: https://github.com/bevyengine/bevy/pull/23567
## What it does
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` — registers `LoadTransformAndSave` as the default processor for `png` / `jpg` /
`jpeg` (this is `ImagePlugin`'s built-in behavior when the
`compressed_image_saver` feature is on).
- `GltfPlugin` and a manual `ShaderLoader` registration — pulled in
solely so the processor can resolve `bevy_gltf::loader::GltfLoader`
and `bevy_shader::shader::ShaderLoader` referenced by source `.meta`
files (or synthesized for files without metas).
- A hand-rolled stub for `bevy_seedling::sample::assets::SampleLoader`
with the right `TypePath` — the processor's lookup-by-name resolves
to it at meta deserialize time. We avoid pulling in `bevy_seedling`
because 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.
## Source `.meta` files are authoritative
`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.
## Build dependencies
- **clang** — `ctt-compressonator` (the encoder behind
`CompressedImageSaver`) uses `-march=knl`, which GCC ≥ 15 doesn't
recognize. Set `CXX=clang++` if the default compiler is GCC.
## Caveats
### Normal maps + BCn don't preserve unit length
`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.
### Wasm format choice
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.
### `.meta` sidecars in output
`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.
### `imported_assets/` directory
The processor writes a transaction log to
`/imported_assets/log` for crash recovery. It's harmless and
not needed by the game at runtime, but currently isn't cleaned up.
## Library use
```rust
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.
## License
MIT OR Apache-2.0, matching the Bevy ecosystem.