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    image::ImageLoaderSettings,
27    light::Skybox,
28    math::vec3,
29    prelude::*,
30};
31
32/// The size of each sphere.
33const SPHERE_SCALE: f32 = 0.9;
34
35/// The speed at which the spheres rotate, in radians per second.
36const SPHERE_ROTATION_SPEED: f32 = 0.8;
37
38/// Which type of light we're using: a point light or a directional light.
39#[derive(Clone, Copy, PartialEq, Resource, Default)]
40enum LightMode {
41    #[default]
42    Point,
43    Directional,
44}
45
46/// Tags the example spheres.
47#[derive(Component)]
48struct ExampleSphere;
49
50/// Entry point.
51pub fn main() {
52    App::new()
53        .init_resource::<LightMode>()
54        .add_plugins(DefaultPlugins)
55        .add_systems(Startup, setup)
56        .add_systems(Update, animate_light)
57        .add_systems(Update, animate_spheres)
58        .add_systems(Update, (handle_input, update_help_text).chain())
59        .run();
60}
61
62/// Initializes the scene.
63fn setup(
64    mut commands: Commands,
65    mut meshes: ResMut<Assets<Mesh>>,
66    mut materials: ResMut<Assets<StandardMaterial>>,
67    asset_server: Res<AssetServer>,
68    light_mode: Res<LightMode>,
69) {
70    let sphere = create_sphere_mesh(&mut meshes);
71    spawn_car_paint_sphere(&mut commands, &mut materials, &asset_server, &sphere);
72    spawn_coated_glass_bubble_sphere(&mut commands, &mut materials, &sphere);
73    spawn_golf_ball(&mut commands, &asset_server);
74    spawn_scratched_gold_ball(&mut commands, &mut materials, &asset_server, &sphere);
75
76    spawn_light(&mut commands);
77    spawn_camera(&mut commands, &asset_server);
78    spawn_text(&mut commands, &light_mode);
79}
80
81/// Generates a sphere.
82fn create_sphere_mesh(meshes: &mut Assets<Mesh>) -> Handle<Mesh> {
83    // We're going to use normal maps, so make sure we've generated tangents, or
84    // else the normal maps won't show up.
85
86    let mut sphere_mesh = Sphere::new(1.0).mesh().build();
87    sphere_mesh
88        .generate_tangents()
89        .expect("Failed to generate tangents");
90    meshes.add(sphere_mesh)
91}
92
93/// Spawn a regular object with a clearcoat layer. This looks like car paint.
94fn spawn_car_paint_sphere(
95    commands: &mut Commands,
96    materials: &mut Assets<StandardMaterial>,
97    asset_server: &AssetServer,
98    sphere: &Handle<Mesh>,
99) {
100    commands
101        .spawn((
102            Mesh3d(sphere.clone()),
103            MeshMaterial3d(
104                materials.add(StandardMaterial {
105                    clearcoat: 1.0,
106                    clearcoat_perceptual_roughness: 0.1,
107                    normal_map_texture: Some(
108                        asset_server
109                            .load_builder()
110                            .with_settings(|settings: &mut ImageLoaderSettings| {
111                                settings.is_srgb = false;
112                            })
113                            .load("textures/BlueNoise-Normal.png"),
114                    ),
115                    metallic: 0.9,
116                    perceptual_roughness: 0.5,
117                    base_color: BLUE.into(),
118                    ..default()
119                }),
120            ),
121            Transform::from_xyz(-1.0, 1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
122        ))
123        .insert(ExampleSphere);
124}
125
126/// Spawn a semitransparent object with a clearcoat layer.
127fn spawn_coated_glass_bubble_sphere(
128    commands: &mut Commands,
129    materials: &mut Assets<StandardMaterial>,
130    sphere: &Handle<Mesh>,
131) {
132    commands
133        .spawn((
134            Mesh3d(sphere.clone()),
135            MeshMaterial3d(materials.add(StandardMaterial {
136                clearcoat: 1.0,
137                clearcoat_perceptual_roughness: 0.1,
138                metallic: 0.5,
139                perceptual_roughness: 0.1,
140                base_color: Color::srgba(0.9, 0.9, 0.9, 0.3),
141                alpha_mode: AlphaMode::Blend,
142                ..default()
143            })),
144            Transform::from_xyz(-1.0, -1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
145        ))
146        .insert(ExampleSphere);
147}
148
149/// Spawns an object with both a clearcoat normal map (a scratched varnish) and
150/// a main layer normal map (the golf ball pattern).
151///
152/// This object is in glTF format, using the `KHR_materials_clearcoat`
153/// extension.
154fn spawn_golf_ball(commands: &mut Commands, asset_server: &AssetServer) {
155    commands.spawn((
156        WorldAssetRoot(
157            asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/GolfBall/GolfBall.glb")),
158        ),
159        Transform::from_xyz(1.0, 1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
160        ExampleSphere,
161    ));
162}
163
164/// Spawns an object with only a clearcoat normal map (a scratch pattern) and no
165/// main layer normal map.
166fn spawn_scratched_gold_ball(
167    commands: &mut Commands,
168    materials: &mut Assets<StandardMaterial>,
169    asset_server: &AssetServer,
170    sphere: &Handle<Mesh>,
171) {
172    commands
173        .spawn((
174            Mesh3d(sphere.clone()),
175            MeshMaterial3d(
176                materials.add(StandardMaterial {
177                    clearcoat: 1.0,
178                    clearcoat_perceptual_roughness: 0.3,
179                    clearcoat_normal_texture: Some(
180                        asset_server
181                            .load_builder()
182                            .with_settings(|settings: &mut ImageLoaderSettings| {
183                                settings.is_srgb = false;
184                            })
185                            .load("textures/ScratchedGold-Normal.png"),
186                    ),
187                    metallic: 0.9,
188                    perceptual_roughness: 0.1,
189                    base_color: GOLD.into(),
190                    ..default()
191                }),
192            ),
193            Transform::from_xyz(1.0, -1.0, 0.0).with_scale(Vec3::splat(SPHERE_SCALE)),
194        ))
195        .insert(ExampleSphere);
196}
197
198/// Spawns a light.
199fn spawn_light(commands: &mut Commands) {
200    commands.spawn(create_point_light());
201}
202
203/// Spawns a camera with associated skybox and environment map.
204fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) {
205    commands
206        .spawn((
207            Camera3d::default(),
208            Hdr,
209            Projection::Perspective(PerspectiveProjection {
210                fov: 27.0 / 180.0 * PI,
211                ..default()
212            }),
213            Transform::from_xyz(0.0, 0.0, 10.0),
214            AcesFitted,
215        ))
216        .insert(Skybox {
217            brightness: 5000.0,
218            image: Some(asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2")),
219            ..default()
220        })
221        .insert(EnvironmentMapLight {
222            diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
223            specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
224            intensity: 2000.0,
225            ..default()
226        });
227}
228
229/// Spawns the help text.
230fn spawn_text(commands: &mut Commands, light_mode: &LightMode) {
231    commands.spawn((
232        light_mode.create_help_text(),
233        Node {
234            position_type: PositionType::Absolute,
235            bottom: px(12),
236            left: px(12),
237            ..default()
238        },
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/// Handles the user pressing Space to change the type of light from point to
267/// directional and vice versa.
268fn handle_input(
269    mut commands: Commands,
270    mut light_query: Query<Entity, Or<(With<PointLight>, With<DirectionalLight>)>>,
271    keyboard: Res<ButtonInput<KeyCode>>,
272    mut light_mode: ResMut<LightMode>,
273) {
274    if !keyboard.just_pressed(KeyCode::Space) {
275        return;
276    }
277
278    for light in light_query.iter_mut() {
279        match *light_mode {
280            LightMode::Point => {
281                *light_mode = LightMode::Directional;
282                commands
283                    .entity(light)
284                    .remove::<PointLight>()
285                    .insert(create_directional_light());
286            }
287            LightMode::Directional => {
288                *light_mode = LightMode::Point;
289                commands
290                    .entity(light)
291                    .remove::<DirectionalLight>()
292                    .insert(create_point_light());
293            }
294        }
295    }
296}
297
298/// Updates the help text at the bottom of the screen.
299fn update_help_text(mut text_query: Query<&mut Text>, light_mode: Res<LightMode>) {
300    for mut text in text_query.iter_mut() {
301        *text = light_mode.create_help_text();
302    }
303}
304
305/// Creates or recreates the moving point light.
306fn create_point_light() -> PointLight {
307    PointLight {
308        color: WHITE.into(),
309        intensity: 100000.0,
310        ..default()
311    }
312}
313
314/// Creates or recreates the moving directional light.
315fn create_directional_light() -> DirectionalLight {
316    DirectionalLight {
317        color: WHITE.into(),
318        illuminance: 1000.0,
319        ..default()
320    }
321}
322
323impl LightMode {
324    /// Creates the help text at the bottom of the screen.
325    fn create_help_text(&self) -> Text {
326        let help_text = match *self {
327            LightMode::Point => "Press Space to switch to a directional light",
328            LightMode::Directional => "Press Space to switch to a point light",
329        };
330
331        Text::new(help_text)
332    }
333}