Skip to content

昼夜切换台

将本章各知识点组合成一个完整的交互示例:太阳绕场景旋转,模拟从清晨到正午到黄昏再到深夜的完整昼夜周期。

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)在西方

联动系统 三套系统各司其职:

  1. update_daytime:处理键盘输入、推进时间相位
  2. apply_daylight:根据相位设置太阳位置、环境光亮度与颜色、相机背景色
  3. ui_text:更新 UI 显示当前时段与速度

apply_daylight 同时调节 GlobalAmbientLightbrightnesscolor——正午明亮偏白,午夜暗淡偏蓝——使单一方向光配合环境光变化产生丰富的昼夜视觉差异。

小结

  • 三种光源各有定位:DirectionalLight 模拟极远光源(太阳)、PointLight 模拟全向灯、SpotLight 模拟定向锥形灯
  • GlobalAmbientLight 为全场景提供无方向兜底照明
  • 阴影启用后需要调校 depth_biasnormal_bias 以避免 artifacts
  • CascadeShadowConfig 影响方向光阴影的精度分布
  • ShadowFilteringMethod 控制阴影边缘柔化方式,作为组件挂在相机上
  • EnvironmentMapLight 用立方体贴图提供反射环境,对金属材质效果最显著
  • 光照参数可在运行时通过 ResMut<T> 修改,适合昼夜循环等动态需求

练习

  1. 添加第二盏聚光灯:在场景中再加一盏聚光灯,位置在相机背后,颜色设为冷色(蓝紫),模拟月光补光效果。
  2. 调 bias:将 shadow_depth_bias 设为 0.0 观察 shadow acne;然后设为 1.0 观察 Peter Panning。找到你场景的最佳值。
  3. 环境贴图实验:准备一张 .hdr 文件,用 EnvironmentMapLight 替换场景的环境照明。
  4. 多云效果:修改 apply_daylight,使正午时 illuminance 只在 1,000—10,000 lux 间随机变化,模拟薄云遮挡。