Skip to content

Commit 3e54e4d

Browse files
authored
Create a new asset book page for asset processing. (#2442)
1 parent 9d28acc commit 3e54e4d

1 file changed

Lines changed: 278 additions & 0 deletions

File tree

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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

Comments
 (0)