Skip to content

双面、深度偏移与 Clearcoat

双面渲染与 cull_mode

默认情况下,Bevy 背面剔除(cull_mode: Some(Face::Back))——三角形只有正面可见。这对封闭几何体(球、立方体)完全够用,但遇到薄片(纸、布、单面墙)就有问题:翻到背面就穿帮了。

cull_modedouble_sided 两个字段配合解决这个问题:

rust
use bevy::prelude::*;

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

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    let plane = meshes.add(Plane3d::default().mesh().size(2.0, 2.0));

    // 默认:cull_mode = Some(Face::Back),单面,背面剔除
    commands.spawn((
        Mesh3d(plane.clone()),
        MeshMaterial3d(materials.add(StandardMaterial {
            base_color: Color::srgb(0.8, 0.2, 0.2),
            ..default()
        })),
        Transform::from_xyz(-2.5, 1.0, 0.0),
    ));

    // cull_mode = None:两面都渲染,但仍按默认法线着色
    commands.spawn((
        Mesh3d(plane.clone()),
        MeshMaterial3d(materials.add(StandardMaterial {
            base_color: Color::srgb(0.2, 0.8, 0.2),
            cull_mode: None,
            ..default()
        })),
        Transform::from_xyz(0.0, 1.0, 0.0),
    ));

    // double_sided + cull_mode = None:背面法线翻转,光照正确
    commands.spawn((
        Mesh3d(plane.clone()),
        MeshMaterial3d(materials.add(StandardMaterial {
            base_color: Color::srgb(0.2, 0.2, 0.8),
            double_sided: true,
            cull_mode: None,
            ..default()
        })),
        Transform::from_xyz(2.5, 1.0, 0.0),
    ));

    // Ground
    commands.spawn((
        Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.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.5, 0.5, 0.0)),
    ));

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

fn rotate_camera(time: Res<Time>, mut query: Query<&mut Transform, With<Camera3d>>) {
    for mut transform in &mut query {
        let angle = time.elapsed_secs() * 0.25;
        let radius = 7.0;
        transform.translation.x = angle.sin() * radius;
        transform.translation.z = angle.cos() * radius;
        transform.translation.y = 4.0;
        *transform = transform.looking_at(Vec3::new(0.0, 0.5, 0.0), Vec3::Y);
    }
}

Listing 24-5:三块平面——默认单面、取消剔除、双面渲染(examples/listing-24-05.rs)

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

相机绕圈,让你从正反两面观察:

  • 左(红):默认行为。cull_mode = Some(Face::Back),背面不渲染。从背面看,平面消失了。
  • 中(绿)cull_mode = None,正反两面都渲染。但注意:背面的法线方向没变,所以从背面看时光照是反的(凹凸反转)。
  • 右(蓝)double_sided = true + cull_mode = None。背面法线被自动翻转,光照正确。

double_sided 不会自动取消剔除——你需要同时设置 cull_mode: None,否则背面的面片依然被 GPU 丢弃,法线翻转了但面片不存在。反之亦然:只设 cull_mode: None 而不设 double_sided,背面会渲染但光照错误。

Face 类型来自 bevy::render::render_resource::Face,不在 prelude 里。设 cull_mode: None 时不需要它;只有显式指定 Some(Face::Front)Some(Face::Back) 时才需要导入:

rust
use bevy::render::render_resource::Face;

深度偏移

两个共面的面片(比如贴花贴在墙上)会出现 Z-fighting——像素在两个面之间闪烁。depth_bias 字段给材质一个深度偏移,正值让物体"靠近"相机:

rust
StandardMaterial {
    base_color: Color::srgba(0.0, 0.0, 0.0, 0.5),
    alpha_mode: AlphaMode::Blend,
    depth_bias: 0.1,
    ..default()
}

depth_bias 影响渲染排序和深度写入,是解决 Z-fighting 的简单手段。但它不是万能的——过大的偏移会让物体看起来"浮"在表面上。更好的做法是在建模时避免共面。

Clearcoat——清漆层

现实世界很多表面有透明涂层:车漆、上釉陶瓷、打蜡的木头、涂了清漆的桌面。clearcoat 在主 PBR 层之上叠了一层薄薄的反射层,模拟这种效果:

rust
use bevy::prelude::*;

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

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

    // 无 clearcoat(对照组)
    commands.spawn((
        Mesh3d(sphere.clone()),
        MeshMaterial3d(materials.add(StandardMaterial {
            base_color: Color::srgb(0.8, 0.2, 0.2),
            metallic: 0.0,
            perceptual_roughness: 0.4,
            ..default()
        })),
        Transform::from_xyz(-2.0, 0.8, 0.0),
    ));

    // clearcoat = 1.0,光滑涂层
    commands.spawn((
        Mesh3d(sphere.clone()),
        MeshMaterial3d(materials.add(StandardMaterial {
            base_color: Color::srgb(0.8, 0.2, 0.2),
            metallic: 0.0,
            perceptual_roughness: 0.4,
            clearcoat: 1.0,
            clearcoat_perceptual_roughness: 0.1,
            ..default()
        })),
        Transform::from_xyz(0.0, 0.8, 0.0),
    ));

    // clearcoat = 1.0,粗糙涂层
    commands.spawn((
        Mesh3d(sphere.clone()),
        MeshMaterial3d(materials.add(StandardMaterial {
            base_color: Color::srgb(0.8, 0.2, 0.2),
            metallic: 0.0,
            perceptual_roughness: 0.4,
            clearcoat: 1.0,
            clearcoat_perceptual_roughness: 0.6,
            ..default()
        })),
        Transform::from_xyz(2.0, 0.8, 0.0),
    ));

    // Ground
    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.5, 0.3, 0.0)),
    ));

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

fn rotate_camera(time: Res<Time>, mut query: Query<&mut Transform, With<Camera3d>>) {
    for mut transform in &mut query {
        let angle = time.elapsed_secs() * 0.3;
        let radius = 5.5;
        transform.translation.x = angle.sin() * radius;
        transform.translation.z = angle.cos() * radius;
        transform.translation.y = 2.5;
        *transform = transform.looking_at(Vec3::new(0.0, 0.4, 0.0), Vec3::Y);
    }
}

Listing 24-6:三颗红球——无涂层、光滑涂层、粗糙涂层(examples/listing-24-06.rs)

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

三个球的底层材质完全相同(红色、非金属、粗糙度 0.4):

  • :无 clearcoat(clearcoat = 0.0,默认)。就是普通的红色哑光球。
  • clearcoat = 1.0clearcoat_perceptual_roughness = 0.1。清漆层光滑,表面多了一层锐利的白色高光——这就是车漆的效果:底下是红色金属漆,上面是一层透明亮漆。
  • clearcoat = 1.0clearcoat_perceptual_roughness = 0.6。清漆层粗糙,高光被摊开——像旧的哑光清漆。

clearcoat 的范围是 [0.0, 1.0],控制涂层的可见强度。clearcoat_perceptual_roughness 控制涂层的粗糙度,用法和主层的 perceptual_roughness 一样。清漆层默认粗糙度是 0.5——如果想要"亮漆"效果,记得把它调低。