综合案例:溶解效果
前面六节拆开了自定义材质的每个零件。这一节把它们组装成一个完整的效果:物体从边缘开始溶解,像被火焰吞噬一样逐渐消失,边缘发出橙色光芒。
这个效果用到了本章的全部知识:ShaderType uniform、#import 引擎模块、globals.time 动画、discard 丢弃像素、以及在 Rust 侧每帧更新 uniform 数据。
效果分析
溶解效果的原理很简单:
- 给每个像素生成一个噪声值(0 到 1 之间)
- 设定一个阈值(threshold),噪声值低于阈值的像素被
discard丢弃 - 阈值附近的像素涂上边缘颜色(发光效果)
- 让阈值随时间变化——物体就"溶解"了
关键在噪声函数。GPU 没有 rand()——我们需要一个确定性的、只依赖 UV 坐标的伪随机函数。
Rust 侧
use bevy::prelude::*;
use bevy::reflect::TypePath;
use bevy::render::render_resource::{AsBindGroup, ShaderType};
use bevy::shader::ShaderRef;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
MaterialPlugin::<DissolveMaterial>::default(),
))
.add_systems(Startup, setup)
.add_systems(Update, animate_dissolve)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<DissolveMaterial>>,
) {
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(1.5, 1.5, 1.5))),
MeshMaterial3d(materials.add(DissolveMaterial {
params: DissolveParams {
base_color: LinearRgba::new(0.1, 0.4, 0.8, 1.0),
edge_color: LinearRgba::new(1.0, 0.5, 0.0, 1.0),
threshold: 0.0,
edge_width: 0.08,
_padding: Vec2::ZERO,
},
})),
Transform::from_xyz(0.0, 0.75, 0.0),
));
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.5, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
));
commands.spawn((
DirectionalLight {
illuminance: 10000.0,
..default()
},
Transform::from_xyz(1.0, 1.0, 0.5).looking_at(Vec3::ZERO, Vec3::Y),
));
}
#[derive(ShaderType, Debug, Clone)]
struct DissolveParams {
base_color: LinearRgba,
edge_color: LinearRgba,
threshold: f32,
edge_width: f32,
_padding: Vec2,
}
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct DissolveMaterial {
#[uniform(0)]
params: DissolveParams,
}
impl Material for DissolveMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/dissolve.wgsl".into()
}
fn alpha_mode(&self) -> AlphaMode {
AlphaMode::Opaque
}
}
fn animate_dissolve(
time: Res<Time>,
mut materials: ResMut<Assets<DissolveMaterial>>,
) {
let t = time.elapsed_secs();
let cycle = (t * 0.3).fract();
let threshold = if cycle < 0.5 {
cycle * 2.0
} else {
2.0 - cycle * 2.0
};
for (_, material) in materials.iter_mut() {
material.params.threshold = threshold;
}
}Listing 36-6:溶解效果——Rust 侧
运行:
cargo run -p ch36-shaders --example listing-36-06方块会循环"溶解-恢复":阈值从 0 升到 1(溶解),再从 1 降到 0(恢复)。
DissolveParams 结构体
#[derive(ShaderType, Debug, Clone)]
struct DissolveParams {
base_color: LinearRgba, // 物体本体颜色
edge_color: LinearRgba, // 溶解边缘的发光颜色
threshold: f32, // 溶解阈值:噪声 < threshold 的像素被丢弃
edge_width: f32, // 边缘发光带的宽度
_padding: Vec2, // 补齐到 16 字节对齐
}两个颜色、两个控制参数、一个 padding——正好 48 字节(LinearRgba 是 16 字节,f32 是 4 字节,Vec2 是 8 字节),满足 GPU 的 16 字节对齐要求。
动画系统
fn animate_dissolve(time: Res<Time>, mut materials: ResMut<Assets<DissolveMaterial>>) {
let t = time.elapsed_secs();
let cycle = (t * 0.3).fract();
let threshold = if cycle < 0.5 {
cycle * 2.0
} else {
2.0 - cycle * 2.0
};
for (_, material) in materials.iter_mut() {
material.params.threshold = threshold;
}
}cycle 每 3.3 秒循环一次(t * 0.3 取小数部分)。前半段阈值从 0 升到 1(溶解),后半段从 1 降到 0(恢复)。三角波形状让溶解和恢复速度对称。
WGSL 侧
#import bevy_pbr::{
mesh_view_bindings::globals,
forward_io::VertexOutput,
}
struct DissolveParams {
base_color: vec4<f32>,
edge_color: vec4<f32>,
threshold: f32,
edge_width: f32,
_padding: vec2<f32>,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> params: DissolveParams;
fn hash(p: vec2<f32>) -> f32 {
var p3 = fract(vec3<f32>(p.x, p.y, p.x) * 0.1031);
p3 += dot(p3, vec3<f32>(p3.y + 33.33, p3.z + 33.33, p3.x + 33.33));
return fract((p3.x + p3.y) * p3.z);
}
@fragment
fn fragment(mesh: VertexOutput) -> @location(0) vec4<f32> {
let noise = hash(floor(mesh.uv * 80.0));
if noise < params.threshold {
discard;
}
let edge_dist = noise - params.threshold;
if edge_dist < params.edge_width {
let t = edge_dist / params.edge_width;
let glow = params.edge_color.rgb * (1.0 - t) * 2.0;
return vec4<f32>(params.base_color.rgb * t + glow, 1.0);
}
return params.base_color;
}Listing 36-6 对应的 WGSL:溶解着色器
噪声函数
fn hash(p: vec2<f32>) -> f32 {
var p3 = fract(vec3<f32>(p.x, p.y, p.x) * 0.1031);
p3 += dot(p3, vec3<f32>(p3.y + 33.33, p3.z + 33.33, p3.x + 33.33));
return fract((p3.x + p3.y) * p3.z);
}这是一个经典的 GPU 哈希函数——不保证密码学安全,但对视觉效果足够"随机"。输入是 UV 坐标,输出是 0 到 1 的浮点数。fract(取小数部分)和 dot(点积)的组合让微小的输入差异产生大幅的输出变化。
调用时传入 floor(mesh.uv * 80.0)——把 UV 空间划分为 80×80 的网格,每个格子内噪声值相同。这产生了"方块状溶解"的效果。如果你想要更细腻的溶解,增大网格分辨率;想要更粗犷的,减小它。
discard 与边缘发光
if noise < params.threshold {
discard;
}discard 是 WGSL 的片段杀手——执行后这个像素完全不写入渲染目标,相当于"透明"。这是溶解效果的核心:阈值以下的像素直接消失。
let edge_dist = noise - params.threshold;
if edge_dist < params.edge_width {
let t = edge_dist / params.edge_width;
let glow = params.edge_color.rgb * (1.0 - t) * 2.0;
return vec4<f32>(params.base_color.rgb * t + glow, 1.0);
}在阈值附近的窄带内(edge_width),基色和边缘色混合。越靠近溶解边界,t 越小,边缘色的权重越大——产生"边缘最亮、向内渐暗"的发光带。* 2.0 是增益系数,让发光更明显。
globals.time 的局限
注意这个例子没有用 globals.time——阈值动画完全在 Rust 侧完成。globals.time 的精度是 f32,一小时后归零。对于持续运行的游戏,用 Rust 侧的 Time 资源更可靠;globals.time 更适合"启动以来的短时动画"场景(如水波纹、呼吸灯)。
最终版
main.rs 和 listing-36-06.rs 内容相同。运行:
cargo run -p ch36-shaders这就是本章的最终成果:一个从零搭建的自定义材质管线——Rust 结构体定义数据布局,WGSL 着色器实现渲染逻辑,Material trait 把两者粘合在一起,uniform 每帧更新驱动动画。