Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sprite and mesh alteration examples #15298

Merged
merged 14 commits into from
Sep 22, 2024
22 changes: 22 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,28 @@ category = "Application"
wasm = false

# Assets
[[example]]
name = "alter_mesh"
path = "examples/asset/alter_mesh.rs"
doc-scrape-examples = true

[package.metadata.example.alter_mesh]
name = "Alter Mesh"
description = "Shows how to modify the underlying asset of a Mesh after spawning."
category = "Assets"
wasm = false

[[example]]
name = "alter_sprite"
path = "examples/asset/alter_sprite.rs"
doc-scrape-examples = true

[package.metadata.example.alter_sprite]
name = "Alter Sprite"
description = "Shows how to modify texture assets after spawning."
category = "Assets"
wasm = false

[[example]]
name = "asset_loading"
path = "examples/asset/asset_loading.rs"
Expand Down
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ Example | Description

Example | Description
--- | ---
[Alter Mesh](../examples/asset/alter_mesh.rs) | Shows how to modify the underlying asset of a Mesh after spawning.
[Alter Sprite](../examples/asset/alter_sprite.rs) | Shows how to modify texture assets after spawning.
[Asset Decompression](../examples/asset/asset_decompression.rs) | Demonstrates loading a compressed asset
[Asset Loading](../examples/asset/asset_loading.rs) | Demonstrates various methods to load assets
[Asset Processing](../examples/asset/processing/asset_processing.rs) | Demonstrates how to process and load custom assets
Expand Down
231 changes: 231 additions & 0 deletions examples/asset/alter_mesh.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
//! Shows how to modify mesh assets after spawning.

use bevy::{
gltf::GltfLoaderSettings, input::common_conditions::input_just_pressed, prelude::*,
render::mesh::VertexAttributeValues, render::render_asset::RenderAssetUsages,
};

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, (setup, spawn_text))
.add_systems(
Update,
alter_handle.run_if(input_just_pressed(KeyCode::Space)),
)
.add_systems(
Update,
alter_mesh.run_if(input_just_pressed(KeyCode::Enter)),
)
.run();
}

#[derive(Component, Debug)]
enum Shape {
Cube,
Sphere,
}

impl Shape {
fn get_model_path(&self) -> String {
match self {
Shape::Cube => "models/cube/cube.gltf".into(),
Shape::Sphere => "models/sphere/sphere.gltf".into(),
}
}

fn set_next_variant(&mut self) {
*self = match self {
Shape::Cube => Shape::Sphere,
Shape::Sphere => Shape::Cube,
}
}
}

#[derive(Component, Debug)]
struct Left;

fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let left_shape = Shape::Cube;
let right_shape = Shape::Cube;

// In normal use, you can call `asset_server.load`, however see below for an explanation of
// `RenderAssetUsages`.
let left_shape_model = asset_server.load_with_settings(
GltfAssetLabel::Primitive {
mesh: 0,
// This field stores an index to this primitive in its parent mesh. In this case, we
// want the first one. You might also have seen the syntax:
//
// models/cube/cube.gltf#Scene0
//
// which accomplishes the same thing.
primitive: 0,
}
.from_asset(left_shape.get_model_path()),
// This is the default loader setting, ensuring both `RENDER_WORLD` and `MAIN_WORLD` are
// enabled. It's provided explicitly here only by way of demonstration. Unless you have
// specific requirements, it's usually safe to use the default.
//
// When needing to access a `Mesh` asset via `Res<Assets<Mesh>>`, a common mistake is to
// use `RENDER_WORLD` by itself, which can cause the asset to be missing from the resource.
// (`RENDER_WORLD` without `MAIN_WORLD` will result in the asset being unloaded from the
// asset server after it's been sent to the GPU.)
|settings: &mut GltfLoaderSettings| settings.load_meshes = RenderAssetUsages::all(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JMS55 Is this the kind of thing you had in mind? New territory for me, so feel free to provide more direction!

Copy link
Member

@janhohenheim janhohenheim Sep 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JMS55 will know better than me, but IME it is the other way around: usually you won't be mutating meshes at runtime, so it's best to set it to RENDER_WORLD only unless you have a good reason to set it to ALL. Although IIRC for footgun reasons, we left the default at ALL.

Copy link
Contributor Author

@bas-ie bas-ie Sep 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So JMS55 wrote:

but I'd prefer if we could explicitly set that to all() and call out why it's necessary, as I very frequently see people make the mistake of using RENDER_WORLD only and then being confused why their asset does not exist.

Perhaps if I make a point of only setting this to all for the asset whose mesh we're going to directly modify, and re-word the comment to say the default is "usually safe" unless you have more specific needs? I'll push that so you can have a read.

Copy link
Member

@janhohenheim janhohenheim Sep 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default is not usually safe, it is always safe, as the default is ALL. It's just twice as expensive than it needs to be for 99% of use cases, as nearly no one will be editing or inspecting meshes at runtime. Many people choose to use RENDER_WORLD instead of ALL for meshes, which is safe until you need to access the mesh on the CPU, which we do in this example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm with you now. OK, so maybe the second one we explicitly set it to RENDER_WORLD and add a further comment there. Maybe also modify the sprite example.

);

// Here, we rely on the default loader settings to achieve a similar result to the above.
let right_shape_model = asset_server.load(
GltfAssetLabel::Primitive {
mesh: 0,
primitive: 0,
}
.from_asset(right_shape.get_model_path()),
);

// Add a material asset directly to the materials storage
let material_handle = materials.add(StandardMaterial {
base_color: Color::srgb(0.6, 0.8, 0.6),
..default()
});

commands.spawn((
Left,
Name::new("Left Shape"),
PbrBundle {
mesh: left_shape_model,
material: material_handle.clone(),
transform: Transform::from_xyz(-3.0, 0.0, 0.0),
..default()
},
left_shape,
));

commands.spawn((
Name::new("Right Shape"),
PbrBundle {
mesh: right_shape_model,
material: material_handle,
transform: Transform::from_xyz(3.0, 0.0, 0.0),
..default()
},
right_shape,
));

commands.spawn((
Name::new("Point Light"),
PointLightBundle {
transform: Transform::from_xyz(4.0, 5.0, 4.0),
..default()
},
));

commands.spawn((
Name::new("Camera"),
Camera3dBundle {
transform: Transform::from_xyz(0.0, 3.0, 20.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
},
));
}

fn spawn_text(mut commands: Commands) {
commands
.spawn((
Name::new("Instructions"),
NodeBundle {
style: Style {
align_items: AlignItems::Start,
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Start,
width: Val::Percent(100.),
..default()
},
..default()
},
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
"Space: swap meshes by mutating a Handle<Mesh>",
TextStyle::default(),
));
parent.spawn(TextBundle::from_section(
"Return: mutate the mesh itself, changing all copies of it",
TextStyle::default(),
));
});
}

fn alter_handle(
asset_server: Res<AssetServer>,
mut right_shape: Query<(&mut Handle<Mesh>, &mut Shape), Without<Left>>,
) {
// Mesh handles, like other parts of the ECS, can be queried as mutable and modified at
// runtime. We only spawned one shape without the `Left` marker component.
let Ok((mut handle, mut shape)) = right_shape.get_single_mut() else {
return;
};

// Switch to a new Shape variant
shape.set_next_variant();

// Modify the handle associated with the Shape on the right side. Note that we will only
// have to load the same path from storage media once: repeated attempts will re-use the
// asset.
*handle = asset_server.load(
GltfAssetLabel::Primitive {
mesh: 0,
primitive: 0,
}
.from_asset(shape.get_model_path()),
);
}

fn alter_mesh(
mut is_mesh_scaled: Local<bool>,
left_shape: Query<&Handle<Mesh>, With<Left>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
// It's convenient to retrieve the asset handle stored with the shape on the left. However,
// we could just as easily have retained this in a resource or a dedicated component.
let Ok(handle) = left_shape.get_single() else {
return;
};

// Obtain a mutable reference to the Mesh asset.
let Some(mesh) = meshes.get_mut(handle) else {
return;
};

// Now we can directly manipulate vertices on the mesh. Here, we're just scaling in and out
// for demonstration purposes. This will affect all entities currently using the asset.
//
// To do this, we need to grab the stored attributes of each vertex. `Float32x3` just describes
// the format in which the attributes will be read: each position consists of an array of three
// f32 corresponding to x, y, and z.
//
// `ATTRIBUTE_POSITION` is a constant indicating that we want to know where the vertex is
// located in space (as opposed to which way its normal is facing, vertex color, or other
// details).
if let Some(VertexAttributeValues::Float32x3(positions)) =
mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION)
{
// Check a Local value (which only this system can make use of) to determine if we're
// currently scaled up or not.
let scale_factor = if *is_mesh_scaled { 0.5 } else { 2.0 };

for position in positions.iter_mut() {
// Apply the scale factor to each of x, y, and z.
position[0] *= scale_factor;
position[1] *= scale_factor;
position[2] *= scale_factor;
}

// Flip the local value to reverse the behaviour next time the key is pressed.
*is_mesh_scaled = !*is_mesh_scaled;
}
}
Loading