mirror of
https://github.com/bevyengine/bevy.git
synced 2026-07-01 00:05:45 -04:00
61127f6d01
# Objective
- In 0.18, we had 10 different functions that load assets (I'm not even
counting `load_folder`).
- In 0.19, we've even added `load_erased` - but it unfortunately doesn't
support all the features that the other variants support.
- We apparently needed `load_acquire_with_settings_override` which 1)
loads the asset, 2) uses the settings provided, 3) allows reading
unapproved asset paths, and 4) drops a guard once the load completes.
- That's fine if that's necessary. But we needed to create an explicit
variant for that.
- We need fewer load paths!
## Solution
- Create a builder.
- Store all these options dynamically instead of statically handling
each case.
- Have the caller choose a particular "kind" of load when they are
ready: `load`, `load_erased`, `load_untyped`, or `load_untyped_async`.
- I intentionally didn't provide a `load_async` or `load_erased_async`,
since those can be replicated using `load`/`load_erased` +
`AssetServer::wait_for_asset_id` to get the exact same effect.
I am also intentionally leaving `NestedLoader` untouched in this PR, but
a followup will duplicate this API for `NestedLoader`, which should make
it easier to understand.
Unlike the `NestedLoader` API, we aren't doing any type-state craziness,
so the docs are much more clear: users don't need to understand how
type-state stuff works, they just call the handful of methods on the
type. The "cost" here is we now need to be careful about including the
cross product of loads between static asset type, runtime asset type, or
dynamic asset type, crossed with deferred or async. In theory, if we
added more kinds on either side, we would need to expand this cross
product a lot. In practice though, it seems unlikely there will be any
more variants there. (maybe there could be a blocking variant? I don't
think this is a popular opinion though).
A big con here is some somewhat common calls are now more verbose.
Specifically, `asset_server.load_with_settings()` has become
`asset_server.load_builder().with_settings().load()`. I am not really
concerned about this though, since it really isn't that painful.
## Testing
- Tests all pass!
---
## Showcase
Now instead of:
```rust
asset_server.load_acquire_with_settings_override("some_path", |settings: &mut GltfLoaderSettings| { ... }, my_lock_guard);
```
You can instead do:
```rust
asset_server.load_builder()
.with_guard(my_lock_guard)
.with_settings(|settings: &mut GltfLoaderSettings| { ... })
.override_unapproved()
.load("some_path");
```
We also now cover more variants! For example, you can now load an asset
untyped with a guard, or with override_unapproved, etc.
---------
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
334 lines
11 KiB
Rust
334 lines
11 KiB
Rust
//! Demonstrates the clearcoat PBR feature.
|
|
//!
|
|
//! Clearcoat is a separate material layer that represents a thin translucent
|
|
//! layer over a material. Examples include (from the Filament spec [1]) car paint,
|
|
//! soda cans, and lacquered wood.
|
|
//!
|
|
//! In glTF, clearcoat is supported via the `KHR_materials_clearcoat` [2]
|
|
//! extension. This extension is well supported by tools; in particular,
|
|
//! Blender's glTF exporter maps the clearcoat feature of its Principled BSDF
|
|
//! node to this extension, allowing it to appear in Bevy.
|
|
//!
|
|
//! This Bevy example is inspired by the corresponding three.js example [3].
|
|
//!
|
|
//! [1]: https://google.github.io/filament/Filament.md.html#materialsystem/clearcoatmodel
|
|
//!
|
|
//! [2]: https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_clearcoat/README.md
|
|
//!
|
|
//! [3]: https://threejs.org/examples/webgl_materials_physical_clearcoat.html
|
|
|
|
use std::f32::consts::PI;
|
|
|
|
use bevy::{
|
|
camera::Hdr,
|
|
color::palettes::css::{BLUE, GOLD, WHITE},
|
|
core_pipeline::tonemapping::Tonemapping::AcesFitted,
|
|
image::ImageLoaderSettings,
|
|
light::Skybox,
|
|
math::vec3,
|
|
prelude::*,
|
|
};
|
|
|
|
/// The size of each sphere.
|
|
const SPHERE_SCALE: f32 = 0.9;
|
|
|
|
/// The speed at which the spheres rotate, in radians per second.
|
|
const SPHERE_ROTATION_SPEED: f32 = 0.8;
|
|
|
|
/// Which type of light we're using: a point light or a directional light.
|
|
#[derive(Clone, Copy, PartialEq, Resource, Default)]
|
|
enum LightMode {
|
|
#[default]
|
|
Point,
|
|
Directional,
|
|
}
|
|
|
|
/// Tags the example spheres.
|
|
#[derive(Component)]
|
|
struct ExampleSphere;
|
|
|
|
/// Entry point.
|
|
pub fn main() {
|
|
App::new()
|
|
.init_resource::<LightMode>()
|
|
.add_plugins(DefaultPlugins)
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, animate_light)
|
|
.add_systems(Update, animate_spheres)
|
|
.add_systems(Update, (handle_input, update_help_text).chain())
|
|
.run();
|
|
}
|
|
|
|
/// Initializes the scene.
|
|
fn setup(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
asset_server: Res<AssetServer>,
|
|
light_mode: Res<LightMode>,
|
|
) {
|
|
let sphere = create_sphere_mesh(&mut meshes);
|
|
spawn_car_paint_sphere(&mut commands, &mut materials, &asset_server, &sphere);
|
|
spawn_coated_glass_bubble_sphere(&mut commands, &mut materials, &sphere);
|
|
spawn_golf_ball(&mut commands, &asset_server);
|
|
spawn_scratched_gold_ball(&mut commands, &mut materials, &asset_server, &sphere);
|
|
|
|
spawn_light(&mut commands);
|
|
spawn_camera(&mut commands, &asset_server);
|
|
spawn_text(&mut commands, &light_mode);
|
|
}
|
|
|
|
/// Generates a sphere.
|
|
fn create_sphere_mesh(meshes: &mut Assets<Mesh>) -> Handle<Mesh> {
|
|
// We're going to use normal maps, so make sure we've generated tangents, or
|
|
// else the normal maps won't show up.
|
|
|
|
let mut sphere_mesh = Sphere::new(1.0).mesh().build();
|
|
sphere_mesh
|
|
.generate_tangents()
|
|
.expect("Failed to generate tangents");
|
|
meshes.add(sphere_mesh)
|
|
}
|
|
|
|
/// Spawn a regular object with a clearcoat layer. This looks like car paint.
|
|
fn spawn_car_paint_sphere(
|
|
commands: &mut Commands,
|
|
materials: &mut Assets<StandardMaterial>,
|
|
asset_server: &AssetServer,
|
|
sphere: &Handle<Mesh>,
|
|
) {
|
|
commands
|
|
.spawn((
|
|
Mesh3d(sphere.clone()),
|
|
MeshMaterial3d(
|
|
materials.add(StandardMaterial {
|
|
clearcoat: 1.0,
|
|
clearcoat_perceptual_roughness: 0.1,
|
|
normal_map_texture: Some(
|
|
asset_server
|
|
.load_builder()
|
|
.with_settings(|settings: &mut ImageLoaderSettings| {
|
|
settings.is_srgb = false;
|
|
})
|
|
.load("textures/BlueNoise-Normal.png"),
|
|
),
|
|
metallic: 0.9,
|
|
perceptual_roughness: 0.5,
|
|
base_color: BLUE.into(),
|
|
..default()
|
|
}),
|
|
),
|
|
Transform::from_xyz(-1.0, 1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
|
|
))
|
|
.insert(ExampleSphere);
|
|
}
|
|
|
|
/// Spawn a semitransparent object with a clearcoat layer.
|
|
fn spawn_coated_glass_bubble_sphere(
|
|
commands: &mut Commands,
|
|
materials: &mut Assets<StandardMaterial>,
|
|
sphere: &Handle<Mesh>,
|
|
) {
|
|
commands
|
|
.spawn((
|
|
Mesh3d(sphere.clone()),
|
|
MeshMaterial3d(materials.add(StandardMaterial {
|
|
clearcoat: 1.0,
|
|
clearcoat_perceptual_roughness: 0.1,
|
|
metallic: 0.5,
|
|
perceptual_roughness: 0.1,
|
|
base_color: Color::srgba(0.9, 0.9, 0.9, 0.3),
|
|
alpha_mode: AlphaMode::Blend,
|
|
..default()
|
|
})),
|
|
Transform::from_xyz(-1.0, -1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
|
|
))
|
|
.insert(ExampleSphere);
|
|
}
|
|
|
|
/// Spawns an object with both a clearcoat normal map (a scratched varnish) and
|
|
/// a main layer normal map (the golf ball pattern).
|
|
///
|
|
/// This object is in glTF format, using the `KHR_materials_clearcoat`
|
|
/// extension.
|
|
fn spawn_golf_ball(commands: &mut Commands, asset_server: &AssetServer) {
|
|
commands.spawn((
|
|
WorldAssetRoot(
|
|
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/GolfBall/GolfBall.glb")),
|
|
),
|
|
Transform::from_xyz(1.0, 1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
|
|
ExampleSphere,
|
|
));
|
|
}
|
|
|
|
/// Spawns an object with only a clearcoat normal map (a scratch pattern) and no
|
|
/// main layer normal map.
|
|
fn spawn_scratched_gold_ball(
|
|
commands: &mut Commands,
|
|
materials: &mut Assets<StandardMaterial>,
|
|
asset_server: &AssetServer,
|
|
sphere: &Handle<Mesh>,
|
|
) {
|
|
commands
|
|
.spawn((
|
|
Mesh3d(sphere.clone()),
|
|
MeshMaterial3d(
|
|
materials.add(StandardMaterial {
|
|
clearcoat: 1.0,
|
|
clearcoat_perceptual_roughness: 0.3,
|
|
clearcoat_normal_texture: Some(
|
|
asset_server
|
|
.load_builder()
|
|
.with_settings(|settings: &mut ImageLoaderSettings| {
|
|
settings.is_srgb = false;
|
|
})
|
|
.load("textures/ScratchedGold-Normal.png"),
|
|
),
|
|
metallic: 0.9,
|
|
perceptual_roughness: 0.1,
|
|
base_color: GOLD.into(),
|
|
..default()
|
|
}),
|
|
),
|
|
Transform::from_xyz(1.0, -1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
|
|
))
|
|
.insert(ExampleSphere);
|
|
}
|
|
|
|
/// Spawns a light.
|
|
fn spawn_light(commands: &mut Commands) {
|
|
commands.spawn(create_point_light());
|
|
}
|
|
|
|
/// Spawns a camera with associated skybox and environment map.
|
|
fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) {
|
|
commands
|
|
.spawn((
|
|
Camera3d::default(),
|
|
Hdr,
|
|
Projection::Perspective(PerspectiveProjection {
|
|
fov: 27.0 / 180.0 * PI,
|
|
..default()
|
|
}),
|
|
Transform::from_xyz(0.0, 0.0, 10.0),
|
|
AcesFitted,
|
|
))
|
|
.insert(Skybox {
|
|
brightness: 5000.0,
|
|
image: Some(asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2")),
|
|
..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: 2000.0,
|
|
..default()
|
|
});
|
|
}
|
|
|
|
/// Spawns the help text.
|
|
fn spawn_text(commands: &mut Commands, light_mode: &LightMode) {
|
|
commands.spawn((
|
|
light_mode.create_help_text(),
|
|
Node {
|
|
position_type: PositionType::Absolute,
|
|
bottom: px(12),
|
|
left: px(12),
|
|
..default()
|
|
},
|
|
));
|
|
}
|
|
|
|
/// Moves the light around.
|
|
fn animate_light(
|
|
mut lights: Query<&mut Transform, Or<(With<PointLight>, With<DirectionalLight>)>>,
|
|
time: Res<Time>,
|
|
) {
|
|
let now = time.elapsed_secs();
|
|
for mut transform in lights.iter_mut() {
|
|
transform.translation = vec3(
|
|
ops::sin(now * 1.4),
|
|
ops::cos(now * 1.0),
|
|
ops::cos(now * 0.6),
|
|
) * vec3(3.0, 4.0, 3.0);
|
|
transform.look_at(Vec3::ZERO, Vec3::Y);
|
|
}
|
|
}
|
|
|
|
/// Rotates the spheres.
|
|
fn animate_spheres(mut spheres: Query<&mut Transform, With<ExampleSphere>>, time: Res<Time>) {
|
|
let now = time.elapsed_secs();
|
|
for mut transform in spheres.iter_mut() {
|
|
transform.rotation = Quat::from_rotation_y(SPHERE_ROTATION_SPEED * now);
|
|
}
|
|
}
|
|
|
|
/// Handles the user pressing Space to change the type of light from point to
|
|
/// directional and vice versa.
|
|
fn handle_input(
|
|
mut commands: Commands,
|
|
mut light_query: Query<Entity, Or<(With<PointLight>, With<DirectionalLight>)>>,
|
|
keyboard: Res<ButtonInput<KeyCode>>,
|
|
mut light_mode: ResMut<LightMode>,
|
|
) {
|
|
if !keyboard.just_pressed(KeyCode::Space) {
|
|
return;
|
|
}
|
|
|
|
for light in light_query.iter_mut() {
|
|
match *light_mode {
|
|
LightMode::Point => {
|
|
*light_mode = LightMode::Directional;
|
|
commands
|
|
.entity(light)
|
|
.remove::<PointLight>()
|
|
.insert(create_directional_light());
|
|
}
|
|
LightMode::Directional => {
|
|
*light_mode = LightMode::Point;
|
|
commands
|
|
.entity(light)
|
|
.remove::<DirectionalLight>()
|
|
.insert(create_point_light());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Updates the help text at the bottom of the screen.
|
|
fn update_help_text(mut text_query: Query<&mut Text>, light_mode: Res<LightMode>) {
|
|
for mut text in text_query.iter_mut() {
|
|
*text = light_mode.create_help_text();
|
|
}
|
|
}
|
|
|
|
/// Creates or recreates the moving point light.
|
|
fn create_point_light() -> PointLight {
|
|
PointLight {
|
|
color: WHITE.into(),
|
|
intensity: 100000.0,
|
|
..default()
|
|
}
|
|
}
|
|
|
|
/// Creates or recreates the moving directional light.
|
|
fn create_directional_light() -> DirectionalLight {
|
|
DirectionalLight {
|
|
color: WHITE.into(),
|
|
illuminance: 1000.0,
|
|
..default()
|
|
}
|
|
}
|
|
|
|
impl LightMode {
|
|
/// Creates the help text at the bottom of the screen.
|
|
fn create_help_text(&self) -> Text {
|
|
let help_text = match *self {
|
|
LightMode::Point => "Press Space to switch to a directional light",
|
|
LightMode::Directional => "Press Space to switch to a point light",
|
|
};
|
|
|
|
Text::new(help_text)
|
|
}
|
|
}
|