docs/bevy/examples/3d/clearcoat.rs 10.7 K · 331 lines · raw · history

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
20 use std::f32::consts::PI;
21
22 use 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
34 use radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
35
36 #[path = "../helpers/radio.rs"]
37 mod radio;
38
39 #[path = "../helpers/theme.rs"]
40 mod theme;
41
42 /// The size of each sphere.
43 const SPHERE_SCALE: f32 = 0.9;
44
45 /// The speed at which the spheres rotate, in radians per second.
46 const 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)]
50 enum LightMode {
51 #[default]
52 Point,
53 Directional,
54 }
55
56 /// Tags the example spheres.
57 #[derive(Component)]
58 struct ExampleSphere;
59
60 /// Entry point.
61 pub 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.
76 fn 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.
95 fn 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.
107 fn 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.
140 fn 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.
167 fn 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.
179 fn 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.
212 fn spawn_light(commands: &mut Commands) {
213 commands.spawn(create_point_light());
214 }
215
216 /// Spawns a camera with associated skybox and environment map.
217 fn 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.
243 fn 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.
259 fn 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.
268 fn 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.
285 fn 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.
316 fn 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.
325 fn create_directional_light() -> DirectionalLight {
326 DirectionalLight {
327 color: WHITE.into(),
328 illuminance: 1000.0,
329 ..default()
330 }
331 }