|
| 1 | ++++ |
| 2 | +title = "Asset Processing" |
| 3 | +insert_anchor_links = "right" |
| 4 | +[extra] |
| 5 | +weight = 4 |
| 6 | ++++ |
| 7 | + |
| 8 | +So far, we've conceptualized assets as just "stuff on disk you want to put in memory". In practice |
| 9 | +though there are two "states" of assets: the "raw" files you acquire during development (from |
| 10 | +the internet, from your artists, etc), and the "processed", game-ready assets. For some assets, |
| 11 | +these are the same thing - you just ship whatever asset files you get. However, some assets need to |
| 12 | +be converted into a form that is more appropriate for use in a game. |
| 13 | + |
| 14 | +As an example, using an ultra-high resolution texture in your game is (probably) not what you want. |
| 15 | +This increases download sizes, slows down loading speeds, and likely reduces rendering performance. |
| 16 | +This would be your "raw" file. Usually, these high-quality versions are compressed, downscaled, etc, |
| 17 | +before being used in a game. This final form is our game-ready version. |
| 18 | + |
| 19 | +We can enable **asset processing** to perform this conversion. When enabled, assets in your `assets` |
| 20 | +folder are automatically processed by the registered processors to produce the game-ready versions |
| 21 | +of assets. Your game will automatically use the processed assets (without needing to change anything |
| 22 | +else). |
| 23 | + |
| 24 | +[% callout(type="important") %] |
| 25 | + |
| 26 | +While it is possible to enable asset processing just before publishing to do steps like compression, |
| 27 | +we **strongly** recommend users choose at the beginning of their project whether to use processing |
| 28 | +or not. In general, processing is **not** guaranteed to be transparent. For example, the "game-ready |
| 29 | +version" of a `.gltf` file could actually be a `Scene` asset, which is not the same as the `Gltf` |
| 30 | +asset - these have entirely different structures! There is no guarantee (intentionally) that the |
| 31 | +processed asset has the same type as the original asset. Another way to think of asset processing is |
| 32 | +as an **import process**: importing a file could import it as another type entirely. |
| 33 | + |
| 34 | +{% end %} |
| 35 | + |
| 36 | +The first step to enable asset processing is to set the `AssetPlugin::mode` accordingly: |
| 37 | + |
| 38 | +```rust |
| 39 | +fn main() { |
| 40 | + App::new() |
| 41 | + .add_plugins(DefaultPlugins.set(AssetPlugin { |
| 42 | + mode: AssetMode::Processed, |
| 43 | + ..Default::default() |
| 44 | + })) |
| 45 | + .run(); |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +This tells Bevy to use the processed versions of assets. To process assets during development, set |
| 50 | +the `asset_processor` feature on `bevy`/`bevy_asset` (this should be **unset** when publishing your |
| 51 | +game). |
| 52 | + |
| 53 | +Now all that's left to do is to add processors to start processing assets. For example, enabling the |
| 54 | +`compressed_image_saver` feature will automatically add an asset processor for `.png` files to |
| 55 | +compress and write them (internally) as the `.basisu` file format. |
| 56 | + |
| 57 | +{% callout(type="warning") %} |
| 58 | + |
| 59 | +By default, processed assets are written to `imported_assets/` (as opposed to the "regular" assets |
| 60 | +directory of `assets/`). **Do not** check in the processed assets as to your version control. The |
| 61 | +`imported_assets/` directory _should be totally ephemeral_. You should be able to delete it and Bevy |
| 62 | +will automatically reprocess the assets. In other words, the "source of truth" for your assets |
| 63 | +during development are those in the `assets/` directory. |
| 64 | + |
| 65 | +In contrast, when publishing, you should be publishing your `imported_assets/` directory, not the |
| 66 | +`assets/` directory. `imported_assets/` contains the game-ready assets which is what your published |
| 67 | +game should use! |
| 68 | + |
| 69 | +{% end %} |
| 70 | + |
| 71 | +## Writing your own Asset Processors |
| 72 | + |
| 73 | +Bevy provides some common processors, but there are plenty of game-specific (or even asset-specific) |
| 74 | +processors that may be needed by users. If you need a processor that Bevy doesn't provide, you can |
| 75 | +make one! |
| 76 | + |
| 77 | +The [`Process`] trait is the most low-level trait for asset processing. The [`Process`] |
| 78 | +implementation is given a `&mut dyn Reader` and must write the processed asset to a `&mut dyn Writer`. |
| 79 | +This is the most flexible (if cumbersome) version of this interface. Implementing [`Process`] |
| 80 | +directly can allow you to optimize for all sorts of use cases (for example, processing small chunks |
| 81 | +of the reader to avoid needing to load the entire file into memory). |
| 82 | + |
| 83 | +In practice though, most users just need the simplest sort of processing: load the file, change the |
| 84 | +asset in some way (even maybe changing its type), then save it back out. For this, we have the |
| 85 | +[`LoadTransformAndSave`] type. You specify the [`AssetLoader`] to load with, the [`AssetTransformer`] |
| 86 | +to apply, and the [`AssetSaver`] to save with, and you will have a new [`Process`] implementation. |
| 87 | + |
| 88 | +For now, we will assume we have these three: `MyLoader`, `MyTransformer`, and `MySaver` (we'll |
| 89 | +describe these more after). To register the processor, use `register_asset_processor`: |
| 90 | + |
| 91 | +```rust |
| 92 | +// This type alias is **not** required. However it makes using the processor a little easier! |
| 93 | +type MyProcessor = LoadTransformAndSave<MyLoader, MyTransformer, MySaver>; |
| 94 | +// It's up to you to create the transformer and saver. In this case, we assume they both have a |
| 95 | +// `new` function. The loader must be separately registered (see below). |
| 96 | +app.register_asset_processor(MyProcessor::new(MyTransformer::new(), MySaver::new())); |
| 97 | +``` |
| 98 | + |
| 99 | +At this point, the processor can be used through meta files (see more below). However, most |
| 100 | +processors are just meant to be applied to all assets with a certain extension. This can be done |
| 101 | +quite easily: |
| 102 | + |
| 103 | +```rust |
| 104 | +// That type alias came in handy! We can reuse it here to set which extension it should process. |
| 105 | +app.set_default_processor::<MyProcessor>("my_file_extension"); |
| 106 | +// We can also register more extensions to process. |
| 107 | +app.set_default_processor::<MyProcessor>("smolext"); |
| 108 | +``` |
| 109 | + |
| 110 | +Now any asset like `blah.my_file_extension` or `cute.smolext` will automatically be processed by our |
| 111 | +processor. |
| 112 | + |
| 113 | +### AssetLoader |
| 114 | + |
| 115 | +This is exactly the same asset loader as discussed in [`Custom Assets`](custom_assets.md). Note that |
| 116 | +`LoadTransformAndSave` can only use your asset loader if it is registered. |
| 117 | + |
| 118 | +### AssetTransformer |
| 119 | + |
| 120 | +This trait takes an [`AssetInput`] type and converts it into an [`AssetOutput`] type. How you do |
| 121 | +this conversion is totally up to you! In the example below, we just call the `NewAssetType::new` |
| 122 | +method with the original asset value: |
| 123 | + |
| 124 | +```rust |
| 125 | +#[derive(TypePath)] |
| 126 | +struct MyTransformer; |
| 127 | + |
| 128 | +impl AssetTransformer for MyTransformer { |
| 129 | + type AssetInput = MyAssetType; |
| 130 | + type AssetOutput = NewAssetType; |
| 131 | + type Settings = (); |
| 132 | + type Error = BevyError; |
| 133 | + |
| 134 | + async fn transform<'a>( |
| 135 | + &'a self, |
| 136 | + mut asset: TransformedAsset<Self::AssetInput>, |
| 137 | + _settings: &Self::Settings, |
| 138 | + ) -> Result<TransformedAsset<Self::AssetOutput>, Self::Error> { |
| 139 | + // Do something to produce the output type. Here we imagine our asset type has a `new` |
| 140 | + // function that takes `&MyAssetType`. |
| 141 | + let new_asset = NewAssetType::new(asset.get()); |
| 142 | + |
| 143 | + // Using `replace_asset` keeps all the "subassets" from the initial asset load. In practice, |
| 144 | + // this means all the handles in the original `asset` will remain valid. |
| 145 | + Ok(asset.replace_asset(new_asset)) |
| 146 | + } |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +Of course, [`AssetInput`] must match the [`AssetLoader::Asset`] type from our loader. |
| 151 | + |
| 152 | +### AssetSaver |
| 153 | + |
| 154 | +This trait takes a [`TransformedAsset`] and writes it as bytes to the provided [`Writer`]. How it |
| 155 | +writes these bytes is up to you! This depends heavily on the file format. Most often this is done |
| 156 | +using the [`serde`] crate to generate serialization and deserialization implementations, followed by |
| 157 | +a format crate like [`ron`]. |
| 158 | + |
| 159 | +In addition, the [`AssetSaver`] must specify what loader the output is meant to be loaded as. This |
| 160 | +ensures that processed assets are loaded with a loader that can actually read the format that was |
| 161 | +written. |
| 162 | + |
| 163 | +```rust |
| 164 | +#[derive(TypePath)] |
| 165 | +struct MySaver; |
| 166 | + |
| 167 | +impl AssetSaver for MySaver { |
| 168 | + type Asset = NewAssetType; |
| 169 | + type Settings = (); |
| 170 | + type Error = BevyError; |
| 171 | + type OutputLoader = NewAssetLoader; |
| 172 | + |
| 173 | + async fn save( |
| 174 | + &self, |
| 175 | + writer: &mut Writer, |
| 176 | + asset: SavedAsset<'_, '_, Self::Asset>, |
| 177 | + _settings; &Self::Settings, |
| 178 | + _asset_path: AssetPath<'_>, |
| 179 | + ) -> Result<NewAssetLoader::Settings, BevyError> { |
| 180 | + // Note: this is a simplified example where we assume we have no "subassets". If we did, |
| 181 | + // we'd likely need to "encode" those subassets in some way in our output data. |
| 182 | + let ron_string = ron::to_string(asset.get())?; |
| 183 | + writer.write_all(&ron_string).await?; |
| 184 | + Ok(NewAssetLoader::Settings::default()) |
| 185 | + } |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +Just as with [`AssetLoader`]s, your type needs to be "encoded" somehow, whether through [`serde`] or |
| 190 | +whatever else. It may be necessary to create a "serializable" version of your asset. This is |
| 191 | +described in more detail in [`Custom Assets`](custom_assets.md). |
| 192 | + |
| 193 | +## Meta Files |
| 194 | + |
| 195 | +Throughout the description of our [`LoadTransformAndSave`] implementation, we've skipped over the |
| 196 | +various `Settings` associated types. These allow your processor to expose settings that can change |
| 197 | +how an individual asset is processed. With [`LoadTransformAndSave`], you can change the settings of |
| 198 | +each individual stage (as in, load, transform, and save). But how do you configure these settings |
| 199 | +for an asset? |
| 200 | + |
| 201 | +Meta files! Meta files allow you to define whether an asset should be processed/loaded, with what |
| 202 | +processor/loader, and the settings for that processor/loader. The default meta file for an asset can |
| 203 | +be created using [`AssetProcessor::write_default_meta_file_for_path`]. For an asset at |
| 204 | +`path/to/my/asset.ext`, the meta file is written to `path/to/my/asset.ext.meta`. Below is an example |
| 205 | +of such a meta file: |
| 206 | + |
| 207 | +```ron |
| 208 | +( |
| 209 | + meta_format_version: "1.0", |
| 210 | + asset: Process( |
| 211 | + processor: "LoadTransformAndSave<MyLoader, MyTransformer, MySaver>", |
| 212 | + settings: ( |
| 213 | + loader_settings: (), |
| 214 | + transformer_settings: (), |
| 215 | + saver_settings: (), |
| 216 | + ), |
| 217 | + ), |
| 218 | +) |
| 219 | +``` |
| 220 | + |
| 221 | +This meta file will process its asset with the [`LoadTransformAndSave`] processor we created in the |
| 222 | +previous section. It will also do so with the given settings for all three of our stages. In our |
| 223 | +example, the settings were just `()`, so we don't have anything interesting to configure here. But |
| 224 | +we could! |
| 225 | + |
| 226 | +As mentioned earlier, we can use **any** processor that has been registered, not just ones that have |
| 227 | +been registered as a default processor for some file extension. This allows you to create processors |
| 228 | +that target individual assets, rather than all assets of a particular extension. |
| 229 | + |
| 230 | +### "Load" Meta Files |
| 231 | + |
| 232 | +Meta files can also be used to configure settings of [`AssetLoader`]s during loading rather than |
| 233 | +processing. This also adds a way to "opt out" of processing a particular asset, by specifying that |
| 234 | +it should be loaded instead. Below is such a meta file. |
| 235 | + |
| 236 | +```ron |
| 237 | +( |
| 238 | + meta_format_version: "1.0", |
| 239 | + asset: Load( |
| 240 | + loader: "MyLoader", |
| 241 | + settings: (), |
| 242 | + ), |
| 243 | +) |
| 244 | +``` |
| 245 | + |
| 246 | +This ensures that the asset this meta file is associated with will be loaded using `MyLoader` |
| 247 | +instead of processed with our processor. We can also specify the settings (though just like our |
| 248 | +processor, its settings are boring since they are just `()`). |
| 249 | + |
| 250 | +"Load" meta files can be used **even without asset processing**. They are very powerful when |
| 251 | +combined with configurable [`AssetLoader`]s. |
| 252 | + |
| 253 | +{% callout(type="info") %} |
| 254 | + |
| 255 | +A common usecase for these "load" meta files is to set the [`RenderAssetUsages`] for an asset. |
| 256 | +Setting render assets like meshes or textures to use [`RenderAssetUsages::RENDER_WORLD`] only allows |
| 257 | +mesh and texture data to only exist in the GPU, freeing RAM on the CPU to do whatever else. **This |
| 258 | +can be a very worthwhile optimization**. Of course this comes with caveats: since the data is no |
| 259 | +longer present on the CPU, it can't be used by regular systems. Most meshes and textures aren't used |
| 260 | +in this way anyway though. |
| 261 | + |
| 262 | +{% end %} |
| 263 | + |
| 264 | +[`Process`]: https://docs.rs/bevy/latest/bevy/asset/processor/trait.Process.html |
| 265 | +[`LoadTransformAndSave`]: https://docs.rs/bevy/latest/bevy/asset/processor/struct.LoadTransformAndSave.html |
| 266 | +[`AssetLoader`]: https://docs.rs/bevy/latest/bevy/asset/trait.AssetLoader.html |
| 267 | +[`AssetTransformer`]: https://docs.rs/bevy/latest/bevy/asset/transformer/trait.AssetTransformer.html |
| 268 | +[`AssetSaver`]: https://docs.rs/bevy/latest/bevy/asset/saver/trait.AssetSaver.html |
| 269 | +[`AssetInput`]: https://docs.rs/bevy/latest/bevy/asset/transformer/trait.AssetTransformer.html#associatedtype.AssetInput |
| 270 | +[`AssetOutput`]: https://docs.rs/bevy/latest/bevy/asset/transformer/trait.AssetTransformer.html#associatedtype.AssetOutput |
| 271 | +[`AssetLoader::Asset`]: https://docs.rs/bevy/latest/bevy/asset/trait.AssetLoader.html#associatedtype.Asset |
| 272 | +[`TransformedAsset`]: https://docs.rs/bevy/latest/bevy/asset/transformer/struct.TransformedAsset.html |
| 273 | +[`Writer`]: https://docs.rs/bevy/latest/bevy/asset/io/type.Writer.html |
| 274 | +[`serde`]: https://docs.rs/serde/latest/serde/ |
| 275 | +[`ron`]: https://docs.rs/ron/latest/ron/ |
| 276 | +[`AssetProcessor::write_default_meta_file_for_path`]: https://docs.rs/bevy/latest/bevy/asset/processor/struct.AssetProcessor.html#method.write_default_meta_file_for_path |
| 277 | +[`RenderAssetUsages`]: https://docs.rs/bevy/latest/bevy/asset/struct.RenderAssetUsages.html |
| 278 | +[`RenderAssetUsages::RENDER_WORLD`]: https://docs.rs/bevy/latest/bevy/asset/struct.RenderAssetUsages.html#associatedconstant.RENDER_WORLD |
0 commit comments