43 releases (6 breaking)
Uses new Rust 2024
| 0.8.2 | Feb 20, 2026 |
|---|---|
| 0.7.0 | Feb 5, 2026 |
| 0.5.11 | Dec 29, 2025 |
| 0.5.1 | Nov 29, 2025 |
| 0.1.0-rc.0 | Jul 21, 2025 |
#222 in Game dev
565KB
14K
SLoC
bevy_feronia
Environment scattering tools and shaders/materials that prioritize visual fidelity/artistic freedom, a declarative API and modularity.
Who is this for?
In the current stage this is mostly for tinkerers and learners within the bevy ecosystem, but I am planning to use this for actual game dev myself eventually.
[!CAUTION] This package is in early development and in an experimentation stage. I wouldn't personally use this in production quite yet, but it's getting closer to that state incrementally.
Getting started
cargo add bevy_feronia
The possible use-cases are demonstrated in the examples
Setup
The setup depends on the use-case, but a typical setup would look like something like this:
app.add_plugins((
MeshMaterialAssetBackendPlugin,
// Or
SceneAssetBackendPlugin,
// ...
ExtendedWindAffectedScatterPlugin
));
The Scatter system needs to know when it can set up since it can depend on height mapping. You need to insert the setup state at some point.
[!NOTE]
In complex setups that load assets and bake a height map, this can be after theStartup.
app.insert_state(ScatterState::Setup)
Or
ns_height_map.set(HeightMapState::Setup);
ns_scatter.set(ScatterState::Setup);
For chunking or GPU driven culling to work, Center and CullComputeCamera need to be inserted on the Camera:
cmd.spawn((
(Camera::default(), Camera3d::default()),
(Center, CullComputeCamera),
// etc...
Defining layers
A ScatterItem's LODs are grouped by Name. If the names end in LOD_1 or lod1 etc., the LOD suffix will be stripped from the name to match it to the other lods of the asset.
[!CAUTION] When defining multiple
ScatterItemsperScatterLayerwithout names, a different asset will render whenLODsare changing, leading to visual bugs.
// Landscape
cmd.spawn((
MeshMaterial3d(materials.add(StandardMaterial {
base_color: GRAY_500.into(),
..default()
})),
Mesh3d(meshes.add(PlaneMeshBuilder::from_length(80.).build())),
ScatterRoot::default(),
// Scatter layers
children![(
// Make sure you use the correct `ScatterLayer` with the desired `ScatterLayerType`, e.g.,
// Standard, Extended or Instanced Material/Layer.
extension::scatter_layer("Wind Affected Layer"),
// Scatter Options
DistributionDensity(50.),
InstanceJitter::default(),
// You can define material options on the full layer here
WindAffected,
children![
(
// Or overwrite on the item, e.g.,
// WindAffected,
//
// CAUTION: If you have multiple assets, all lods that belong to each other need to have the same name!
//
// You can have multiple assets in each layer; as long as all LODs have the same name, they will be matched correctly.
Name::new("Wind Affected Example Item"),
MeshMaterial3d(materials.add(StandardMaterial::default())),
Mesh3d(mesh.clone()),
),
(
Name::new("Wind Affected Example Item"),
// We need to specify the LOD Level if it is not 0 (Highest level)
LevelOfDetail(1),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: RED_500.into(),
..default()
})),
Mesh3d(mesh.clone()),
),
]
)]
));
Scattering
Now you can start scattering! 🌱 🍃 🌿 🍀 🌳 🌲 🌴 🌺
cmd.trigger(Scatter::<ExtendedWindAffectedMaterial>::new(*root));
[!NOTE]
ScatterLayersand theirScatterItemsof the sameScatterTypeare always scattered in order, but layers of differentScatterTypescan be scattered at the same time.
Ordered Scattering
In complex scenes it is often required to scatter a complete hierarchy in order (rocks → trees/foliage → grass).
[!TIP] If an ordered scatter is still required, and you can't or don't want to scatter in parallel, observers need to be used to chain the scattering of
ScatterTypesin order.
fn scatter_on_keypress(
mut cmd: Commands,
keyboard_input: Res<ButtonInput<KeyCode>>,
root: Single<Entity, With<ScatterRoot>>
) {
if !keyboard_input.just_pressed(KeyCode::Space) {
return;
};
// Scatter the rocks.
cmd.trigger(Scatter::<StandardMaterial>::new(*root));
}
fn scatter_extended(
_: On<ScatterFinished<StandardMaterial>>,
mut cmd: Commands,
root: Single<Entity, With<ScatterRoot>>,
) {
// Scatter the foliage after the rocks.
cmd.trigger(Scatter::<ExtendedWindAffectedMaterial>::new(*root));
}
fn scatter_instanced(
_: On<ScatterFinished<ExtendedWindAffectedMaterial>>,
mut cmd: Commands,
root: Single<Entity, With<ScatterRoot>>,
) {
// Scatter the grass last so it doesn't grow on occupied areas.
cmd.trigger(Scatter::<InstancedWindAffectedMaterial>::new(*root));
}
Compatibility
There are very experimental releases before 0.5.0, but I wouldn't use them.
| bevy | bevy_feronia |
|---|---|
| 0.18 | 0.8+ |
| 0.17 | 0.7 |
License/Credits/Inspirations/References
The code is dual-licensed:
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
Feel free to copy the grass assets. All the other assets used in the examples are licensed assets.
[!IMPORTANT] If you intend to use them, make sure you comply with the license.
- Graswald for their amazing assets and
GScatter, which served as inspiration for the scatter tools. - Sucker Punch Productions for their Procedural Grass and Wind simulation in 'Ghost of Tsushima' and GDC Talks.
- bevy_procedural_grass by jadedbay
- warbler_grass by EmiOnGit
- GDC 2011 "Approximating Translucency"
- Blinn–Phong reflection model
- All the other assets
Alternatives
- If you want a lower level scattering API, just for scattering and sampling there is also
bevy_map_scatter, which I might use eventually as well but for now I want this crate to achieve a vision.
Roadmap
A bunch of issues are already open, but some of the larger milestones could be:
- Allow physics-based and other entities to impact the displacement/wind.
- Make use of compute shaders (Allow scattering on CPU and GPU, improve culling).
- Allow for multiple
ScatterRootswhen baking height maps (otherwise it should work... probably but it isn't tested yet)
Dependencies
~59–100MB
~1.5M SLoC