Bevy 像素完美渲染:pixel_grid_snap 双相机管线
仅仅设置 ImagePlugin::default_nearest() 并不能解决所有像素美学问题。当游戏窗口缩放为非整数倍比例时,像素仍然会出现子像素失真(边缘在不同屏幕像素之间分布不均)。官方示例 pixel_grid_snap 提供了完整的解决方案。
核心思路
- 用一个低分辨率相机(如 160×90)将游戏世界渲染到一张 off-screen 纹理(canvas)
- 用第二个相机把这张纹理作为 Sprite 渲染到屏幕
- 窗口缩放时只允许整数倍比例,避免子像素模糊
这种方案的好处是你可以把游戏世界渲染在一个很低的分辨率,然后整数倍放大到屏幕,看起来就是完美的像素风格。
关键组件与系统
Canvas 纹理创建
use bevy::render::render_resource::{
Extent3d, TextureDescriptor, TextureDimension, TextureFormat,
TextureUsages,
};
const RES_WIDTH: u32 = 160;
const RES_HEIGHT: u32 = 90;
fn create_canvas_image(images: &mut Assets<Image>) -> Handle<Image> {
let canvas_size = Extent3d {
width: RES_WIDTH,
height: RES_HEIGHT,
..default()
};
let mut canvas = Image {
texture_descriptor: TextureDescriptor {
label: None,
size: canvas_size,
dimension: TextureDimension::D2,
format: TextureFormat::Bgra8UnormSrgb,
mip_level_count: 1,
sample_count: 1,
usage: TextureUsages::TEXTURE_BINDING
| TextureUsages::COPY_DST
| TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
},
..default()
};
canvas.resize(canvas_size);
images.add(canvas)
}在 Bevy 0.18+ 中,可用
Image::new_target_texture()简化上述繁琐的TextureDescriptor手动配置。
双相机与 RenderLayers
官方示例的核心机制是 RenderLayers 隔离:
| 实体 | RenderLayer | 渲染目标 | 作用 |
|---|---|---|---|
| 游戏相机 | layer(0) | RenderTarget::Image(canvas) | 把游戏世界渲染到 canvas |
| 游戏实体 | 默认 | layer 0 | 被游戏相机捕获 |
| Canvas Sprite | layer(1) | 屏幕 | 把 canvas 纹理当作背景渲染 |
| 外层相机 | layer(1) | 屏幕 | 把 canvas sprite 渲染到屏幕 |
如果没有 RenderLayers 隔离:
- 游戏相机会把 canvas sprite 也渲染进 canvas,产生递归/视觉错误
- 外层相机会直接渲染游戏世界实体,低分辨率内容导致模糊
窗口缩放算法
fn fit_canvas(
mut resize_events: EventReader<WindowResized>,
mut projection: Single<&mut Projection, With<OuterCamera>>,
) {
let Projection::Orthographic(projection) = &mut **projection else {
return;
};
for window_resized in resize_events.read() {
let h_scale = window_resized.width / RES_WIDTH as f32;
let v_scale = window_resized.height / RES_HEIGHT as f32;
// round() 确保只用整数倍比例
projection.scale = 1.0 / h_scale.min(v_scale).round();
}
}关键是 round() 而不是 floor() — 官方示例使用 round() 来确保缩放比例是整数倍,避免子像素失真。
完整 main() 注册
fn main() {
App::new()
.insert_resource(Msaa::Off) // 全局关闭抗锯齿
.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
.add_systems(Startup, setup_camera)
.add_systems(Update, fit_canvas)
.run();
}版本注意事项
- Bevy 0.15+: 使用
Camera2d、EventReader<WindowResized>、Single<&mut Projection> - Bevy 0.17+: 窗口事件移至缓冲消息系统,需改用
MessageReader<WindowResized> - Bevy 0.18+: 可用
Image::new_target_texture()简化 canvas 创建
业界对照
| 游戏 | 内部分辨率 | 渲染方式 |
|---|---|---|
| Celeste | 320×180 | off-screen 渲染 + 整数倍放大 |
| Stardew Valley | 16×16 瓦片 | 固定虚拟分辨率 + Nearest |
| Shovel Knight | 400×240 | 严格整数倍缩放 |
相关页面
- pixel-art-texture-filtering — GPU 纹理过滤原理深度分析
- bevy-image-filtering-nearest-vs-linear — Bevy ImagePlugin API 对比
- bevy — Bevy 引擎实体页