Skip to main content

clearcoat/
clearcoat.rs

1//! Demonstrates the clearcoat PBR feature.
2//!
3//! Clearcoat is a separate material layer that represents a thin translucent
4//! layer over a material. Examples include (from the Filament spec [1]) car paint,
5//! soda cans, and lacquered wood.
6//!
7//! In glTF, clearcoat is supported via the `KHR_materials_clearcoat` [2]
8//! extension. This extension is well supported by tools; in particular,
9//! Blender's glTF exporter maps the clearcoat feature of its Principled BSDF
10//! node to this extension, allowing it to appear in Bevy.
11//!
12//! This Bevy example is inspired by the corresponding three.js example [3].
13//!
14//! [1]: https://google.github.io/filament/Filament.md.html#materialsystem/clearcoatmodel
15//!
16//! [2]: https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_clearcoat/README.md
17//!
18//! [3]: https://threejs.org/examples/webgl_materials_physical_clearcoat.html
19
20use std::f32::consts::PI;
21
22use bevy::{
23    camera::Hdr,
24    color::palettes::css::{BLUE, GOLD, WHITE},
25    core_pipeline::tonemapping::Tonemapping::AcesFitted,
26    feathers::{theme::UiTheme, FeathersPlugins},
27    image::ImageLoaderSettings,
28    light::Skybox,
29    math::vec3,
30    prelude::*,
31    ui_widgets::{radio_self_update, ValueChange},
32};
33
34use radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
35
36#[path = "../helpers/radio.rs"]
37mod radio;
38
39#[path = "../helpers/theme.rs"]
40mod theme;
41
42/// The size of each sphere.
43const SPHERE_SCALE: f32 = 0.9;
44
45/// The speed at which the spheres rotate, in radians per second.
46const SPHERE_ROTATION_SPEED: f32 = 0.8;
47
48/// Which type of light we're using: a point light or a directional light.
49#[derive(Clone, Copy, PartialEq, Resource, Default)]
50enum LightMode {
51    #[default]
52    Point,
53    Directional,
54}
55
56/// Tags the example spheres.
57#[derive(Component)]
58struct ExampleSphere;
59
60/// Entry point.
61pub fn main() {
62    App::new()
63        .init_resource::<LightMode>()
64        .add_plugins(DefaultPlugins)
65        .add_plugins(FeathersPlugins)
66        .insert_resource(UiTheme(theme::basic_example_theme(Color::WHITE)))
67        .add_systems(Startup, setup)
68        .add_systems(Update, animate_light)
69        .add_systems(Update, animate_spheres)
70        .add_observer(handle_selection_change)
71        .add_observer(radio_self_update)
72        .run();
73}
74
75/// Initializes the scene.
76fn setup(
77    mut commands: Commands,
78    mut meshes: ResMut<Assets<Mesh>>,
79    mut materials: ResMut<Assets<StandardMaterial>>,
80    asset_server: Res<AssetServer>,
81    light_mode: Res<LightMode>,
82) {
83    let sphere = create_sphere_mesh(&mut meshes);
84    spawn_car_paint_sphere(&mut commands, &mut materials, &asset_server, &sphere);
85    spawn_coated_glass_bubble_sphere(&mut commands, &mut materials, &sphere);
86    spawn_golf_ball(&mut commands, &asset_server);
87    spawn_scratched_gold_ball(&mut commands, &mut materials, &asset_server, &sphere);
88
89    spawn_light(&mut commands);
90    spawn_camera(&mut commands, &asset_server);
91    spawn_buttons(&mut commands, light_mode);
92}
93
94/// Generates a sphere.
95fn create_sphere_mesh(meshes: &mut Assets<Mesh>) -> Handle<Mesh> {
96    // We're going to use normal maps, so make sure we've generated tangents, or
97    // else the normal maps won't show up.
98
99    let mut sphere_mesh = Sphere::new(1.0).mesh().build();
100    sphere_mesh
101        .generate_tangents()
102        .expect("Failed to generate tangents");
103    meshes.add(sphere_mesh)
104}
105
106/// Spawn a regular object with a clearcoat layer. This looks like car paint.
107fn spawn_car_paint_sphere(
108    commands: &mut Commands,
109    materials: &mut Assets<StandardMaterial>,
110    asset_server: &AssetServer,
111    sphere: &Handle<Mesh>,
112) {
113    commands
114        .spawn((
115            Mesh3d(sphere.clone()),
116            MeshMaterial3d(
117                materials.add(StandardMaterial {
118                    clearcoat: 1.0,
119                    clearcoat_perceptual_roughness: 0.1,
120                    normal_map_texture: Some(
121                        asset_server
122                            .load_builder()
123                            .with_settings(|settings: &mut ImageLoaderSettings| {
124                                settings.is_srgb = false;
125                            })
126                            .load("textures/BlueNoise-Normal.png"),
127                    ),
128                    metallic: 0.9,
129                    perceptual_roughness: 0.5,
130                    base_color: BLUE.into(),
131                    ..default()
132                }),
133            ),
134            Transform::from_xyz(-1.0, 1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
135        ))
136        .insert(ExampleSphere);
137}
138
139/// Spawn a semitransparent object with a clearcoat layer.
140fn spawn_coated_glass_bubble_sphere(
141    commands: &mut Commands,
142    materials: &mut Assets<StandardMaterial>,
143    sphere: &Handle<Mesh>,
144) {
145    commands
146        .spawn((
147            Mesh3d(sphere.clone()),
148            MeshMaterial3d(materials.add(StandardMaterial {
149                clearcoat: 1.0,
150                clearcoat_perceptual_roughness: 0.1,
151                metallic: 0.5,
152                perceptual_roughness: 0.1,
153                base_color: Color::srgba(0.9, 0.9, 0.9, 0.3),
154                alpha_mode: AlphaMode::Blend,
155                ..default()
156            })),
157            Transform::from_xyz(-1.0, -1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
158        ))
159        .insert(ExampleSphere);
160}
161
162/// Spawns an object with both a clearcoat normal map (a scratched varnish) and
163/// a main layer normal map (the golf ball pattern).
164///
165/// This object is in glTF format, using the `KHR_materials_clearcoat`
166/// extension.
167fn spawn_golf_ball(commands: &mut Commands, asset_server: &AssetServer) {
168    commands.spawn((
169        WorldAssetRoot(
170            asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/GolfBall/GolfBall.glb")),
171        ),
172        Transform::from_xyz(1.0, 1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
173        ExampleSphere,
174    ));
175}
176
177/// Spawns an object with only a clearcoat normal map (a scratch pattern) and no
178/// main layer normal map.
179fn spawn_scratched_gold_ball(
180    commands: &mut Commands,
181    materials: &mut Assets<StandardMaterial>,
182    asset_server: &AssetServer,
183    sphere: &Handle<Mesh>,
184) {
185    commands
186        .spawn((
187            Mesh3d(sphere.clone()),
188            MeshMaterial3d(
189                materials.add(StandardMaterial {
190                    clearcoat: 1.0,
191                    clearcoat_perceptual_roughness: 0.3,
192                    clearcoat_normal_texture: Some(
193                        asset_server
194                            .load_builder()
195                            .with_settings(|settings: &mut ImageLoaderSettings| {
196                                settings.is_srgb = false;
197                            })
198                            .load("textures/ScratchedGold-Normal.png"),
199                    ),
200                    metallic: 0.9,
201                    perceptual_roughness: 0.1,
202                    base_color: GOLD.into(),
203                    ..default()
204                }),
205            ),
206            Transform::from_xyz(1.0, -1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
207        ))
208        .insert(ExampleSphere);
209}
210
211/// Spawns a light.
212fn spawn_light(commands: &mut Commands) {
213    commands.spawn(create_point_light());
214}
215
216/// Spawns a camera with associated skybox and environment map.
217fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) {
218    commands
219        .spawn((
220            Camera3d::default(),
221            Hdr,
222            Projection::Perspective(PerspectiveProjection {
223                fov: 27.0 / 180.0 * PI,
224                ..default()
225            }),
226            Transform::from_xyz(0.0, 0.0, 10.0),
227            AcesFitted,
228        ))
229        .insert(Skybox {
230            brightness: 5000.0,
231            image: Some(asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2")),
232            ..default()
233        })
234        .insert(EnvironmentMapLight {
235            diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
236            specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
237            intensity: 2000.0,
238            ..default()
239        });
240}
241
242/// Moves the light around.
243fn animate_light(
244    mut lights: Query<&mut Transform, Or<(With<PointLight>, With<DirectionalLight>)>>,
245    time: Res<Time>,
246) {
247    let now = time.elapsed_secs();
248    for mut transform in lights.iter_mut() {
249        transform.translation = vec3(
250            ops::sin(now * 1.4),
251            ops::cos(now * 1.0),
252            ops::cos(now * 0.6),
253        ) * vec3(3.0, 4.0, 3.0);
254        transform.look_at(Vec3::ZERO, Vec3::Y);
255    }
256}
257
258/// Rotates the spheres.
259fn animate_spheres(mut spheres: Query<&mut Transform, With<ExampleSphere>>, time: Res<Time>) {
260    let now = time.elapsed_secs();
261    for mut transform in spheres.iter_mut() {
262        transform.rotation = Quat::from_rotation_y(SPHERE_ROTATION_SPEED * now);
263    }
264}
265
266/// Spawns buttons at the bottom of the screen which allow the user to
267/// toggle occlusion culling on or off.
268fn spawn_buttons(commands: &mut Commands, light_mode: Res<LightMode>) {
269    commands.spawn_scene(bsn! {
270        @main_ui_node_scene()
271            Children [
272            @feathers_option_buttons(
273                "Toggle light type",
274                &[
275                    (LightMode::Directional, "Directional"),
276                    (LightMode::Point, "Point"),
277                ],
278                if *light_mode == LightMode::Directional { 0 } else { 1 },
279            )
280        ]
281    });
282}
283
284/// Updates the light mode when the user toggles a radio.
285fn handle_selection_change(
286    event: On<ValueChange<Entity>>,
287    mut commands: Commands,
288    mut light_query: Query<Entity, Or<(With<PointLight>, With<DirectionalLight>)>>,
289    new_value_query: Query<&RadioButtonOptionValue<LightMode>>,
290    mut light_mode: ResMut<LightMode>,
291) {
292    let Ok(RadioButtonOptionValue(selection)) = new_value_query.get(event.value) else {
293        return;
294    };
295
296    *light_mode = *selection;
297    for light in light_query.iter_mut() {
298        match *light_mode {
299            LightMode::Directional => {
300                commands
301                    .entity(light)
302                    .remove::<PointLight>()
303                    .insert(create_directional_light());
304            }
305            LightMode::Point => {
306                commands
307                    .entity(light)
308                    .remove::<DirectionalLight>()
309                    .insert(create_point_light());
310            }
311        }
312    }
313}
314
315/// Creates or recreates the moving point light.
316fn create_point_light() -> PointLight {
317    PointLight {
318        color: WHITE.into(),
319        intensity: 100000.0,
320        ..default()
321    }
322}
323
324/// Creates or recreates the moving directional light.
325fn create_directional_light() -> DirectionalLight {
326    DirectionalLight {
327        color: WHITE.into(),
328        illuminance: 1000.0,
329        ..default()
330    }
331}