Skip to content

法线贴图与自发光

法线贴图——假凹凸

法线贴图(Normal Map)用一张 RGB 图编码每个像素的"面朝向",让平坦的网格看起来有凹凸细节,而不需要额外的几何。在 Bevy 里挂到 normal_map_texture 字段即可:

rust
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .run();
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    asset_server: Res<AssetServer>,
) {
    let normal_handle: Handle<Image> = asset_server.load_with_settings(
        "textures/parallax_example/cube_normal.png",
        |settings: &mut bevy::image::ImageLoaderSettings| settings.is_srgb = false,
    );

    let sphere_mesh = meshes.add(Sphere::new(0.8).mesh().uv(64, 36));

    commands.spawn((
        Mesh3d(sphere_mesh.clone()),
        MeshMaterial3d(materials.add(StandardMaterial {
            base_color: Color::srgb(0.6, 0.6, 0.6),
            perceptual_roughness: 0.4,
            normal_map_texture: Some(normal_handle),
            double_sided: true,
            ..default()
        })),
        Transform::from_xyz(-1.2, 0.8, 0.0),
    ));

    commands.spawn((
        Mesh3d(sphere_mesh.clone()),
        MeshMaterial3d(materials.add(StandardMaterial {
            base_color: Color::srgb(0.6, 0.6, 0.6),
            perceptual_roughness: 0.4,
            ..default()
        })),
        Transform::from_xyz(1.2, 0.8, 0.0),
    ));

    commands.spawn((
        Mesh3d(meshes.add(Plane3d::default().mesh().size(8.0, 8.0))),
        MeshMaterial3d(materials.add(StandardMaterial {
            base_color: Color::srgb(0.2, 0.2, 0.2),
            perceptual_roughness: 1.0,
            ..default()
        })),
    ));

    commands.spawn((
        DirectionalLight {
            illuminance: light_consts::lux::FULL_DAYLIGHT,
            contact_shadows_enabled: true,
            ..default()
        },
        Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, -0.6, 0.4, 0.0)),
    ));

    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(3.0, 2.5, 4.0).looking_at(Vec3::new(0.0, 0.4, 0.0), Vec3::Y),
    ));
}

Listing 24-2:左球挂法线贴图,右球纯平——同一光源下凹凸差异立现(examples/listing-24-02.rs)

console
cargo run -p ch24-pbr-materials --example listing-24-02

左边那颗球表面的凹凸是假的——几何上它仍然是完美球面,但法线贴图让光照以为每个像素的朝向不同,于是高光和阴影在表面"滚动"。右边的对照球是同一材质、同一粗糙度,没有法线贴图,光滑得像乒乓球。

法线贴图三件套

Bevy 的 PBR 着色器要求三样东西同时就位,法线贴图才生效:

  1. 法线贴图纹理——normal_map_texture
  2. UV 坐标——网格顶点必须有 UV,否则贴图不知道往哪铺
  3. 切线(Tangents)——网格顶点必须有切线数据,用来把法线从贴图空间变换到世界空间

如果法线贴图挂了但效果不明显(球面还是平的),大概率是缺切线。解决方法:

rust
mesh.generate_tangents().unwrap();

法线贴图是 DirectX 格式(Y 轴朝下)还是 OpenGL 格式(Y 轴朝上)?Bevy 默认按 OpenGL 处理。如果你的贴图是 DirectX 格式(比如从 Substance Painter 导出),设置 flip_normal_map_y: true

加载时注意 is_srgb

法线贴图存的是方向向量,不是颜色。如果按 sRGB 加载,GPU 会对值做 gamma 反转,法线方向就歪了。加载时必须关掉 sRGB:

rust
asset_server.load_with_settings(
    "path/to/normal.png",
    |settings: &mut ImageLoaderSettings| settings.is_srgb = false,
);

自发光——在黑暗里亮起来

emissive 字段让材质表面"自己发光"——不是照别人,是照相机。适合做屏幕、指示灯、霓虹灯管。

rust
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .run();
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    let sphere = meshes.add(Sphere::new(0.5).mesh().uv(32, 18));

    let emissive_colors = [
        (Color::srgb(1.0, 0.0, 0.0), "红"),
        (Color::srgb(0.0, 1.0, 0.0), "绿"),
        (Color::srgb(0.0, 0.0, 1.0), "蓝"),
        (Color::WHITE, "白"),
    ];

    for (i, (color, _name)) in emissive_colors.iter().enumerate() {
        let x = (i as f32 - 1.5) * 1.4;

        commands.spawn((
            Mesh3d(sphere.clone()),
            MeshMaterial3d(materials.add(StandardMaterial {
                base_color: Color::srgb(0.1, 0.1, 0.1),
                emissive: LinearRgba::from(*color) * 5.0,
                ..default()
            })),
            Transform::from_xyz(x, 0.5, 0.0),
        ));
    }

    commands.spawn((
        Mesh3d(meshes.add(Cuboid::new(3.0, 0.05, 3.0))),
        MeshMaterial3d(materials.add(StandardMaterial {
            base_color: Color::srgb(0.1, 0.1, 0.1),
            perceptual_roughness: 0.8,
            ..default()
        })),
        Transform::from_xyz(0.0, -0.01, 0.0),
    ));

    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(3.5, 2.5, 5.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
    ));
}

Listing 24-3:四颗自发光球——无灯光环境下自见(examples/listing-24-03.rs)

console
cargo run -p ch24-pbr-materials --example listing-24-03

注意这个例子里没有添加任何灯光。四颗球在完全黑暗的环境里依然可见,全靠 emissive。值的单位是 nits(坎德拉每平方米),LinearRgba::BLACK(默认)表示不发光。颜色通道可以远超 1.0——比如 LinearRgba::rgb(100.0, 0.0, 0.0) 是极亮的红光,后处理的 bloom 效果会在它周围产生辉光。

emissiveemissive_texture 的关系是乘法:最终自发光 = emissive × emissive_texture。如果只想用贴图控制发光区域,把 emissive 设为白色。

自发光不会照亮周围物体——它只影响本材质的像素值。想要物体真的当灯用,请用 PointLightSpotLight。自发光的定位是"视觉标记",不是"光源替代品"。