bevy/examples/3d/visibility_range.rs
Joona Aalto 54006b107b
Migrate meshes and materials to required components (#15524)
# Objective

A big step in the migration to required components: meshes and
materials!

## Solution

As per the [selected
proposal](https://hackmd.io/@bevy/required_components/%2Fj9-PnF-2QKK0on1KQ29UWQ):

- Deprecate `MaterialMesh2dBundle`, `MaterialMeshBundle`, and
`PbrBundle`.
- Add `Mesh2d` and `Mesh3d` components, which wrap a `Handle<Mesh>`.
- Add `MeshMaterial2d<M: Material2d>` and `MeshMaterial3d<M: Material>`,
which wrap a `Handle<M>`.
- Meshes *without* a mesh material should be rendered with a default
material. The existence of a material is determined by
`HasMaterial2d`/`HasMaterial3d`, which is required by
`MeshMaterial2d`/`MeshMaterial3d`. This gets around problems with the
generics.

Previously:

```rust
commands.spawn(MaterialMesh2dBundle {
    mesh: meshes.add(Circle::new(100.0)).into(),
    material: materials.add(Color::srgb(7.5, 0.0, 7.5)),
    transform: Transform::from_translation(Vec3::new(-200., 0., 0.)),
    ..default()
});
```

Now:

```rust
commands.spawn((
    Mesh2d(meshes.add(Circle::new(100.0))),
    MeshMaterial2d(materials.add(Color::srgb(7.5, 0.0, 7.5))),
    Transform::from_translation(Vec3::new(-200., 0., 0.)),
));
```

If the mesh material is missing, previously nothing was rendered. Now,
it renders a white default `ColorMaterial` in 2D and a
`StandardMaterial` in 3D (this can be overridden). Below, only every
other entity has a material:

![Näyttökuva 2024-09-29
181746](https://github.com/user-attachments/assets/5c8be029-d2fe-4b8c-ae89-17a72ff82c9a)

![Näyttökuva 2024-09-29
181918](https://github.com/user-attachments/assets/58adbc55-5a1e-4c7d-a2c7-ed456227b909)

Why white? This is still open for discussion, but I think white makes
sense for a *default* material, while *invalid* asset handles pointing
to nothing should have something like a pink material to indicate that
something is broken (I don't handle that in this PR yet). This is kind
of a mix of Godot and Unity: Godot just renders a white material for
non-existent materials, while Unity renders nothing when no materials
exist, but renders pink for invalid materials. I can also change the
default material to pink if that is preferable though.

## Testing

I ran some 2D and 3D examples to test if anything changed visually. I
have not tested all examples or features yet however. If anyone wants to
test more extensively, it would be appreciated!

## Implementation Notes

- The relationship between `bevy_render` and `bevy_pbr` is weird here.
`bevy_render` needs `Mesh3d` for its own systems, but `bevy_pbr` has all
of the material logic, and `bevy_render` doesn't depend on it. I feel
like the two crates should be refactored in some way, but I think that's
out of scope for this PR.
- I didn't migrate meshlets to required components yet. That can
probably be done in a follow-up, as this is already a huge PR.
- It is becoming increasingly clear to me that we really, *really* want
to disallow raw asset handles as components. They caused me a *ton* of
headache here already, and it took me a long time to find every place
that queried for them or inserted them directly on entities, since there
were no compiler errors for it. If we don't remove the `Component`
derive, I expect raw asset handles to be a *huge* footgun for users as
we transition to wrapper components, especially as handles as components
have been the norm so far. I personally consider this to be a blocker
for 0.15: we need to migrate to wrapper components for asset handles
everywhere, and remove the `Component` derive. Also see
https://github.com/bevyengine/bevy/issues/14124.

---

## Migration Guide

Asset handles for meshes and mesh materials must now be wrapped in the
`Mesh2d` and `MeshMaterial2d` or `Mesh3d` and `MeshMaterial3d`
components for 2D and 3D respectively. Raw handles as components no
longer render meshes.

Additionally, `MaterialMesh2dBundle`, `MaterialMeshBundle`, and
`PbrBundle` have been deprecated. Instead, use the mesh and material
components directly.

Previously:

```rust
commands.spawn(MaterialMesh2dBundle {
    mesh: meshes.add(Circle::new(100.0)).into(),
    material: materials.add(Color::srgb(7.5, 0.0, 7.5)),
    transform: Transform::from_translation(Vec3::new(-200., 0., 0.)),
    ..default()
});
```

Now:

```rust
commands.spawn((
    Mesh2d(meshes.add(Circle::new(100.0))),
    MeshMaterial2d(materials.add(Color::srgb(7.5, 0.0, 7.5))),
    Transform::from_translation(Vec3::new(-200., 0., 0.)),
));
```

If the mesh material is missing, a white default material is now used.
Previously, nothing was rendered if the material was missing.

The `WithMesh2d` and `WithMesh3d` query filter type aliases have also
been removed. Simply use `With<Mesh2d>` or `With<Mesh3d>`.

---------

Co-authored-by: Tim Blackbird <justthecooldude@gmail.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
2024-10-01 21:33:17 +00:00

328 lines
10 KiB
Rust

//! Demonstrates visibility ranges, also known as HLODs.
use std::f32::consts::PI;
use bevy::{
input::mouse::MouseWheel,
math::vec3,
pbr::{light_consts::lux::FULL_DAYLIGHT, CascadeShadowConfigBuilder},
prelude::*,
render::view::VisibilityRange,
};
// Where the camera is focused.
const CAMERA_FOCAL_POINT: Vec3 = vec3(0.0, 0.3, 0.0);
// Speed in units per frame.
const CAMERA_KEYBOARD_ZOOM_SPEED: f32 = 0.05;
// Speed in radians per frame.
const CAMERA_KEYBOARD_PAN_SPEED: f32 = 0.01;
// Speed in units per frame.
const CAMERA_MOUSE_MOVEMENT_SPEED: f32 = 0.25;
// The minimum distance that the camera is allowed to be from the model.
const MIN_ZOOM_DISTANCE: f32 = 0.5;
// The visibility ranges for high-poly and low-poly models respectively, when
// both models are being shown.
static NORMAL_VISIBILITY_RANGE_HIGH_POLY: VisibilityRange = VisibilityRange {
start_margin: 0.0..0.0,
end_margin: 3.0..4.0,
};
static NORMAL_VISIBILITY_RANGE_LOW_POLY: VisibilityRange = VisibilityRange {
start_margin: 3.0..4.0,
end_margin: 8.0..9.0,
};
// A visibility model that we use to always show a model (until the camera is so
// far zoomed out that it's culled entirely).
static SINGLE_MODEL_VISIBILITY_RANGE: VisibilityRange = VisibilityRange {
start_margin: 0.0..0.0,
end_margin: 8.0..9.0,
};
// A visibility range that we use to completely hide a model.
static INVISIBLE_VISIBILITY_RANGE: VisibilityRange = VisibilityRange {
start_margin: 0.0..0.0,
end_margin: 0.0..0.0,
};
// Allows us to identify the main model.
#[derive(Component, Debug, Clone, Copy, PartialEq)]
enum MainModel {
// The high-poly version.
HighPoly,
// The low-poly version.
LowPoly,
}
// The current mode.
#[derive(Default, Resource)]
struct AppStatus {
// Whether to show only one model.
show_one_model_only: Option<MainModel>,
}
// Sets up the app.
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Bevy Visibility Range Example".into(),
..default()
}),
..default()
}))
.init_resource::<AppStatus>()
.add_systems(Startup, setup)
.add_systems(
Update,
(
move_camera,
set_visibility_ranges,
update_help_text,
update_mode,
),
)
.run();
}
// Set up a simple 3D scene. Load the two meshes.
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
asset_server: Res<AssetServer>,
app_status: Res<AppStatus>,
) {
// Spawn a plane.
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0))),
MeshMaterial3d(materials.add(Color::srgb(0.1, 0.2, 0.1))),
));
// Spawn the two HLODs.
commands
.spawn(SceneBundle {
scene: asset_server
.load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
..default()
})
.insert(MainModel::HighPoly);
commands
.spawn(SceneBundle {
scene: asset_server.load(
GltfAssetLabel::Scene(0)
.from_asset("models/FlightHelmetLowPoly/FlightHelmetLowPoly.gltf"),
),
..default()
})
.insert(MainModel::LowPoly);
// Spawn a light.
commands.spawn((
DirectionalLight {
illuminance: FULL_DAYLIGHT,
shadows_enabled: true,
..default()
},
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, PI * -0.15, PI * -0.15)),
CascadeShadowConfigBuilder {
maximum_distance: 30.0,
first_cascade_far_bound: 0.9,
..default()
}
.build(),
));
// Spawn a camera.
commands
.spawn(Camera3dBundle {
transform: Transform::from_xyz(0.7, 0.7, 1.0).looking_at(CAMERA_FOCAL_POINT, Vec3::Y),
..default()
})
.insert(EnvironmentMapLight {
diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
intensity: 150.0,
..default()
});
// Create the text.
commands.spawn(
TextBundle {
text: app_status.create_text(),
..default()
}
.with_style(Style {
position_type: PositionType::Absolute,
bottom: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
);
}
// We need to add the `VisibilityRange` components manually, as glTF currently
// has no way to specify visibility ranges. This system watches for new meshes,
// determines which `Scene` they're under, and adds the `VisibilityRange`
// component as appropriate.
fn set_visibility_ranges(
mut commands: Commands,
mut new_meshes: Query<Entity, Added<Mesh3d>>,
parents: Query<(Option<&Parent>, Option<&MainModel>)>,
) {
// Loop over each newly-added mesh.
for new_mesh in new_meshes.iter_mut() {
// Search for the nearest ancestor `MainModel` component.
let (mut current, mut main_model) = (new_mesh, None);
while let Ok((parent, maybe_main_model)) = parents.get(current) {
if let Some(model) = maybe_main_model {
main_model = Some(model);
break;
}
match parent {
Some(parent) => current = **parent,
None => break,
}
}
// Add the `VisibilityRange` component.
match main_model {
Some(MainModel::HighPoly) => {
commands
.entity(new_mesh)
.insert(NORMAL_VISIBILITY_RANGE_HIGH_POLY.clone())
.insert(MainModel::HighPoly);
}
Some(MainModel::LowPoly) => {
commands
.entity(new_mesh)
.insert(NORMAL_VISIBILITY_RANGE_LOW_POLY.clone())
.insert(MainModel::LowPoly);
}
None => {}
}
}
}
// Process the movement controls.
fn move_camera(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut mouse_wheel_events: EventReader<MouseWheel>,
mut cameras: Query<&mut Transform, With<Camera3d>>,
) {
let (mut zoom_delta, mut theta_delta) = (0.0, 0.0);
// Process zoom in and out via the keyboard.
if keyboard_input.pressed(KeyCode::KeyW) || keyboard_input.pressed(KeyCode::ArrowUp) {
zoom_delta -= CAMERA_KEYBOARD_ZOOM_SPEED;
} else if keyboard_input.pressed(KeyCode::KeyS) || keyboard_input.pressed(KeyCode::ArrowDown) {
zoom_delta += CAMERA_KEYBOARD_ZOOM_SPEED;
}
// Process left and right pan via the keyboard.
if keyboard_input.pressed(KeyCode::KeyA) || keyboard_input.pressed(KeyCode::ArrowLeft) {
theta_delta -= CAMERA_KEYBOARD_PAN_SPEED;
} else if keyboard_input.pressed(KeyCode::KeyD) || keyboard_input.pressed(KeyCode::ArrowRight) {
theta_delta += CAMERA_KEYBOARD_PAN_SPEED;
}
// Process zoom in and out via the mouse wheel.
for event in mouse_wheel_events.read() {
zoom_delta -= event.y * CAMERA_MOUSE_MOVEMENT_SPEED;
}
// Update the camera transform.
for transform in cameras.iter_mut() {
let transform = transform.into_inner();
let direction = transform.translation.normalize_or_zero();
let magnitude = transform.translation.length();
let new_direction = Mat3::from_rotation_y(theta_delta) * direction;
let new_magnitude = (magnitude + zoom_delta).max(MIN_ZOOM_DISTANCE);
transform.translation = new_direction * new_magnitude;
transform.look_at(CAMERA_FOCAL_POINT, Vec3::Y);
}
}
// Toggles modes if the user requests.
fn update_mode(
mut meshes: Query<(&mut VisibilityRange, &MainModel)>,
keyboard_input: Res<ButtonInput<KeyCode>>,
mut app_status: ResMut<AppStatus>,
) {
// Toggle the mode as requested.
if keyboard_input.just_pressed(KeyCode::Digit1) || keyboard_input.just_pressed(KeyCode::Numpad1)
{
app_status.show_one_model_only = None;
} else if keyboard_input.just_pressed(KeyCode::Digit2)
|| keyboard_input.just_pressed(KeyCode::Numpad2)
{
app_status.show_one_model_only = Some(MainModel::HighPoly);
} else if keyboard_input.just_pressed(KeyCode::Digit3)
|| keyboard_input.just_pressed(KeyCode::Numpad3)
{
app_status.show_one_model_only = Some(MainModel::LowPoly);
} else {
return;
}
// Update the visibility ranges as appropriate.
for (mut visibility_range, main_model) in meshes.iter_mut() {
*visibility_range = match (main_model, app_status.show_one_model_only) {
(&MainModel::HighPoly, Some(MainModel::LowPoly))
| (&MainModel::LowPoly, Some(MainModel::HighPoly)) => {
INVISIBLE_VISIBILITY_RANGE.clone()
}
(&MainModel::HighPoly, Some(MainModel::HighPoly))
| (&MainModel::LowPoly, Some(MainModel::LowPoly)) => {
SINGLE_MODEL_VISIBILITY_RANGE.clone()
}
(&MainModel::HighPoly, None) => NORMAL_VISIBILITY_RANGE_HIGH_POLY.clone(),
(&MainModel::LowPoly, None) => NORMAL_VISIBILITY_RANGE_LOW_POLY.clone(),
}
}
}
// A system that updates the help text.
fn update_help_text(mut text_query: Query<&mut Text>, app_status: Res<AppStatus>) {
for mut text in text_query.iter_mut() {
*text = app_status.create_text();
}
}
impl AppStatus {
// Creates and returns help text reflecting the app status.
fn create_text(&self) -> Text {
Text::from_section(
format!(
"\
{} (1) Switch from high-poly to low-poly based on camera distance
{} (2) Show only the high-poly model
{} (3) Show only the low-poly model
Press 1, 2, or 3 to switch which model is shown
Press WASD or use the mouse wheel to move the camera",
if self.show_one_model_only.is_none() {
'>'
} else {
' '
},
if self.show_one_model_only == Some(MainModel::HighPoly) {
'>'
} else {
' '
},
if self.show_one_model_only == Some(MainModel::LowPoly) {
'>'
} else {
' '
},
),
TextStyle::default(),
)
}
}