昼夜切换台
将本章各知识点组合成一个完整的交互示例:太阳绕场景旋转,模拟从清晨到正午到黄昏再到深夜的完整昼夜周期。
rust
use std::f32::consts::PI;
use bevy::{
light::CascadeShadowConfigBuilder,
prelude::*,
text::TextFont,
};
const DAY_DURATION: f32 = 60.0;
#[derive(Resource)]
struct DayTime {
phase: f32,
paused: bool,
speed: f32,
}
impl Default for DayTime {
fn default() -> Self {
Self { phase: 0.25, paused: false, speed: 1.0 }
}
}
fn main() {
App::new()
.insert_resource(DayTime::default())
.insert_resource(GlobalAmbientLight {
color: Color::srgb(0.1, 0.1, 0.3),
brightness: 10.0,
..default()
})
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, (update_daytime, apply_daylight, ui_text))
.run();
}
#[derive(Component)]
struct SunLabel;
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// 地面
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(12.0, 12.0))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: Color::srgb(0.4, 0.4, 0.4),
perceptual_roughness: 1.0,
..default()
})),
));
// 场景中的物体
for (x, z, color) in [
(-2.0, -1.5, Color::srgb(0.1, 0.5, 0.9)),
(2.0, -1.5, Color::srgb(0.9, 0.2, 0.2)),
(0.0, 1.5, Color::srgb(0.2, 0.8, 0.3)),
] {
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(0.8, 0.8, 0.8))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: color,
..default()
})),
Transform::from_xyz(x, 0.4, z),
));
}
// 金属球
commands.spawn((
Mesh3d(meshes.add(Sphere::new(0.6).mesh().uv(32, 18))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: Color::srgb(0.9, 0.7, 0.2),
metallic: 0.9,
perceptual_roughness: 0.3,
..default()
})),
Transform::from_xyz(0.0, 0.6, -1.5),
));
// DirectionalLight
commands.spawn((
DirectionalLight {
illuminance: light_consts::lux::FULL_DAYLIGHT,
contact_shadows_enabled: true,
..default()
},
Transform::IDENTITY,
CascadeShadowConfigBuilder {
num_cascades: 4,
maximum_distance: 20.0,
first_cascade_far_bound: 4.0,
..default()
}
.build(),
));
// 相机
commands.spawn((
Camera3d::default(),
Camera {
clear_color: ClearColorConfig::Custom(Color::srgb(0.1, 0.1, 0.2)),
..default()
},
Transform::from_xyz(5.0, 4.0, 7.0).looking_at(Vec3::ZERO, Vec3::Y),
));
// UI
commands.spawn((
Text::default(),
TextFont { font_size: FontSize::Px(20.0), ..default() },
TextColor(Color::WHITE),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
SunLabel,
children![TextSpan::new("")],
));
}
fn update_daytime(
time: Res<Time>,
key_input: Res<ButtonInput<KeyCode>>,
mut day: ResMut<DayTime>,
) {
if key_input.just_pressed(KeyCode::Space) {
day.paused = !day.paused;
}
if key_input.just_pressed(KeyCode::ArrowUp) {
day.speed = (day.speed * 2.0).min(8.0);
}
if key_input.just_pressed(KeyCode::ArrowDown) {
day.speed = (day.speed / 2.0).max(0.25);
}
if !day.paused {
day.phase += time.delta_secs() * day.speed / DAY_DURATION;
if day.phase > 1.0 {
day.phase -= 1.0;
}
}
}
fn apply_daylight(
day: Res<DayTime>,
mut directional: Query<&mut Transform, With<DirectionalLight>>,
mut ambient: ResMut<GlobalAmbientLight>,
mut camera_query: Query<&mut Camera>,
) {
// 太阳绕 Y 轴旋转
let angle = day.phase * PI * 2.0;
let height = (day.phase * PI * 2.0).sin().max(-0.2);
let elevation = height.max(0.0) * 6.0 + 2.0;
let sun_pos = Vec3::new(angle.cos() * 8.0, elevation, angle.sin() * 8.0);
for mut transform in &mut directional {
*transform = Transform::from_translation(sun_pos).looking_at(Vec3::ZERO, Vec3::Y);
}
// 环境光随日照变化
let daylight = (day.phase * PI * 2.0).sin().max(0.0);
ambient.brightness = daylight * 80.0 + 2.0;
ambient.color = Color::srgb(
0.3 + daylight * 0.7,
0.3 + daylight * 0.7,
0.5 + daylight * 0.5,
);
// 天空色
for mut camera in &mut camera_query {
camera.clear_color = ClearColorConfig::Custom(Color::srgb(
0.1 + daylight * 0.5,
0.1 + daylight * 0.5,
0.2 + daylight * 0.4,
));
}
}
fn ui_text(day: Res<DayTime>, label: Single<Entity, With<SunLabel>>, mut writer: TextUiWriter) {
let phase = day.phase;
let phase_name = if phase < 0.125 || phase >= 0.875 {
"深夜"
} else if phase < 0.25 {
"凌晨"
} else if phase < 0.375 {
"清晨"
} else if phase < 0.5 {
"上午"
} else if phase < 0.625 {
"正午"
} else if phase < 0.75 {
"下午"
} else {
"黄昏"
};
*writer.text(*label, 0) = format!(
"时间:{:.0}%\n时段:{phase_name}\n速度:{:.1}×\n\n\
空格:暂停/继续\n上:加速 下:减速",
phase * 100.0,
day.speed,
);
}Listing 22-7:昼夜切换台——完整的昼夜循环演示
运行方式
console
cargo run -p ch22-lighting操作
| 按键 | 功能 |
|---|---|
| 空格 | 暂停/继续时间推进 |
| ↑ | 加速(2×、4×、8×) |
| ↓ | 减速(1/2×、1/4×、1/8×) |
设计要点
时间相位 phase 是一个 [0, 1) 的循环值,通过 DayTime Resource 管理。相位到太阳位置的映射:
phase × 2π得到水平旋转角sin(phase × 2π)得到太阳高度角- 日出(phase=0.25)在东方,正午(phase=0.5)在天顶,日落(phase=0.75)在西方
联动系统 三套系统各司其职:
update_daytime:处理键盘输入、推进时间相位apply_daylight:根据相位设置太阳位置、环境光亮度与颜色、相机背景色ui_text:更新 UI 显示当前时段与速度
apply_daylight 同时调节 GlobalAmbientLight 的 brightness 和 color——正午明亮偏白,午夜暗淡偏蓝——使单一方向光配合环境光变化产生丰富的昼夜视觉差异。
小结
- 三种光源各有定位:
DirectionalLight模拟极远光源(太阳)、PointLight模拟全向灯、SpotLight模拟定向锥形灯 GlobalAmbientLight为全场景提供无方向兜底照明- 阴影启用后需要调校
depth_bias与normal_bias以避免 artifacts CascadeShadowConfig影响方向光阴影的精度分布ShadowFilteringMethod控制阴影边缘柔化方式,作为组件挂在相机上EnvironmentMapLight用立方体贴图提供反射环境,对金属材质效果最显著- 光照参数可在运行时通过
ResMut<T>修改,适合昼夜循环等动态需求
练习
- 添加第二盏聚光灯:在场景中再加一盏聚光灯,位置在相机背后,颜色设为冷色(蓝紫),模拟月光补光效果。
- 调 bias:将
shadow_depth_bias设为 0.0 观察 shadow acne;然后设为 1.0 观察 Peter Panning。找到你场景的最佳值。 - 环境贴图实验:准备一张 .hdr 文件,用
EnvironmentMapLight替换场景的环境照明。 - 多云效果:修改
apply_daylight,使正午时illuminance只在 1,000—10,000 lux 间随机变化,模拟薄云遮挡。