MSAA
Что уже должно быть понятно:
- нормали, освещение, instancing
- camera, depth buffer, render pipeline
- текстуры, bind groups
Что появится в этой главе:
- мультисэмплинг (MSAA): сглаживание краёв геометрии
sample_count: 4в pipeline и текстурахresolve_target— автоматическое сведение (resolve) мультисэмпловой текстуры в обычную- мультисэмпловые color и depth текстуры
Итог: сетка 3×3×3 кубов с гладкими краями без «лесенки»
Края треугольников выглядят ступенчатыми (aliasing) — пиксель либо принадлежит треугольнику, либо нет. На границе возникает резкий переход, который особенно заметен на геометрии с наклонными гранями. MSAA (Multisample Anti-Aliasing) сглаживает края, беря несколько сэмплов на пиксель.
Без MSAA пиксель на границе либо полностью принадлежит треугольнику, либо нет — возникает характерная «лесенка». MSAA вычисляет долю покрытия и смешивает цвет треугольника с фоном, создавая плавный переход.
Принцип
При sample_count = 4 GPU размещает 4 субпиксельных точки (сэмпла) в каждом пикселе. Расположение сэмплов зависит от GPU и не стандартизировано — обычно это паттерн, близкий к равномерному распределению. При растеризации GPU проверяет, какие сэмплы попали внутрь треугольника:
- Все 4 внутри — пиксель полностью покрыт, результат как обычно
- 2 из 4 внутри — пиксель на границе, итоговый цвет = 50% цвета треугольника + 50% фона
Фрагментный шейдер вызывается один раз на пиксель (не на каждый сэмпл). Результат шейдера размножается на все сэмплы, попавшие внутрь треугольника. Это делает MSAA значительно дешевле суперсэмплинга (SSAA), где шейдер выполняется для каждого сэмпла отдельно.
Pipeline: sample_count
В pipeline указываем количество сэмплов:
multisample: MultisampleState {
count: 4, // было 1
mask: !0,
alpha_to_coverage_enabled: false,
},count — степень двойки: 1 (без MSAA), 2, 4, 8. Чаще всего используют 4 — хорошее сглаживание при умеренных затратах. mask: !0 означает, что все сэмплы активны (редко нужно менять).
alpha_to_coverage_enabled: false — если включить, GPU использует alpha-канал фрагментного шейдера как маску покрытия: фрагмент с alpha = 0.5 «отключит» половину сэмплов. Это полезно для растровой транспарентности (листва, заборы), но для непрозрачных объектов остаётся false.
Мультисэмпловые текстуры
Цветовая и depth-текстуры тоже должны поддерживать мультисэмплинг. Обычная текстура хранит одно значение на пиксель — мультисэмпловая хранит sample_count значений:
// Color
let msaa_texture = ctx.device.create_texture(&TextureDescriptor {
sample_count: 4, // было 1
usage: TextureUsages::RENDER_ATTACHMENT,
format: ctx.surface_format,
..
});
// Depth
let depth_texture = ctx.device.create_texture(&TextureDescriptor {
sample_count: 4, // было 1
format: TextureFormat::Depth32Float,
usage: TextureUsages::RENDER_ATTACHMENT,
..
});Обе текстуры используют только RENDER_ATTACHMENT — мультисэмпловые текстуры нельзя сэмплировать в шейдере напрямую. Размер совпадает с размером окна.
При resize нужно пересоздать обе мультисэмпловые текстуры (color и depth) — как обычную depth-текстуру в главе про depth buffer. surface_config меняется, старые текстуры становятся неправильного размера.
Потребление памяти
При sample_count = 4 каждая текстура занимает в 4 раза больше видеопамяти: вместо одного значения на пиксель хранится четыре. Для 1080p-окна: color (RGBA8) ≈ 33 МБ вместо 8 МБ, depth (32-bit) ≈ 33 МБ вместо 8 МБ. Общий расход на MSAA ×4 — дополнительные ~50 МБ.
Resolve
Мультисэмпловая текстура содержит 4 значения на пиксель. Surface ожидает обычную текстуру — по одному значению. Преобразование называется resolve и задаётся через resolve_target:
color_attachments: &[Some(RenderPassColorAttachment {
view: &self.msaa_view, // мультисэмпловая текстура
resolve_target: Some(view), // surface view — куда сводить
ops: Operations {
load: LoadOp::Clear(Color::BLACK),
store: StoreOp::Store,
},
depth_slice: None,
})],GPU автоматически усредняет сэмплы и записывает результат в resolve_target после завершения render pass. Это аппаратная операция — шейдер не участвует.
Если resolve_target не указан (None), мультисэмпловая текстура остаётся как есть. Прочитать её в шейдере напрямую нельзя — для этого нужен resolve_target или явный resolve через compute shader. В нашем случае view — это surface texture view, и resolve записывает финальный результат прямо на экран.
Depth и MSAA
Depth-текстура тоже мультисэмпловая — каждый сэмпл имеет собственное значение глубины. Это важно для корректного depth test на границе треугольников: если часть сэмплов перекрыта другим треугольником, только они отбрасываются.
У depth attachment нет resolve_target — после render pass мультисэмпловая глубина просто отбрасывается. Она не нужна для дальнейшего рендера.
MSAA и render-to-texture
В главе про render-to-texture мы рендерили сцену в offscreen-текстуру, а затем применяли постпроцессинг. MSAA можно добавить к этому подходу: offscreen-текстура становится мультисэмпловой, resolve происходит в промежуточную текстуру, а постпроцессинг работает уже с resolved-изображением. Это стандартный паттерн: MSAA → resolve → postprocess.
Нельзя применить MSAA после постпроцессинга — к моменту постпроцессинга края уже «сплющены» в готовое изображение.
Ключевые отличия от обычного рендера
| Параметр | Без MSAA | MSAA ×4 |
|---|---|---|
Pipeline sample_count | 1 | 4 |
Color texture sample_count | 1 | 4 |
Depth texture sample_count | 1 | 4 |
resolve_target | None | Some(view) |
| Потребление памяти | ×1 | ×4 (color + depth) |
| Шейдерных вызовов | 1 на пиксель | 1 на пиксель |
Таблица показывает главное преимущество MSAA: при четырёхкратном увеличении памяти, количество вызовов фрагментного шейдера не меняется.
Все три параметра sample_count (pipeline, color texture, depth texture) должны совпадать. Если хотя бы один не совпадёт — ошибка создания pipeline или panic при рендере.
Другие методы сглаживания
MSAA — не единственный подход. Каждый метод имеет свои компромиссы:
| Метод | Принцип | Стоимость |
|---|---|---|
| MSAA | Субпиксельные сэмплы при растеризации | ×4 память, но шейдер вызывается 1 раз |
| SSAA | Рендер в разрешении ×2–×4, затем downscale | ×4–16 по всем ресурсам |
| FXAA | Постпроцессинг: поиск и размытие краёв на готовом изображении | Очень дёшево, размывает текстуры |
| TAA | Сэмплирование по шаблону между кадрами, накопление | Дёшево, но артефакты при движении |
MSAA — стандартный выбор для большинства приложений. FXAA и TAA реализуются как постпроцессинг и совместимы с render-to-texture подходом из прошлой главы. Полноценный TAA требует jitter-смещения projection матрицы и motion vectors — это выходит за рамки данного руководства.
Типичные ошибки
Несовпадение sample_count
sample_count должен быть одинаковым в трёх местах: pipeline (multisample.count), color texture и depth texture. Если хотя бы один не совпадёт — ошибка при создании pipeline или panic при рендере. Меняет значение — меняйте везде.
Забытый resolve_target
Мультисэмпловую текстуру нельзя отобразить напрямую — она хранит несколько значений на пиксель. Без resolve_target: Some(view) в RenderPassColorAttachment приложение упадёт при попытке отрисовать мультисэмпловую текстуру на экран.
Не пересозданы текстуры при resize
При изменении размера окна мультисэмпловые color и depth текстуры нужно пересоздать с новыми размерами. Старые текстуры останутся прежнего размера — результат будет обрезан или растянут.
Попробуйте сами
Упражнение 1
Поставьте sample_count: 1 во всех трёх местах (pipeline, color texture, depth texture) и сравните результат с MSAA ×4 — вы увидите «лесенку» на гранях кубов.
Упражнение 2
Попробуйте sample_count: 8 — если GPU поддерживает, края станут ещё более гладкими. Оцените разницу в потреблении памяти: при 1080p глубинная текстура вырастет с ~8 МБ до ~66 МБ.
Упражнение 3
Уберите resolve_target (поставьте None) и посмотрите на ошибку — это поможет запомнить, что мультисэмпловая текстура не может быть presented напрямую.
Что получилось
27 кубов с гладкими краями. Сравните с любой предыдущей главой — «лесенка» на гранях исчезла. MSAA влияет только на края треугольников — внутренние пиксели и текстуры остаются без изменений. Это основное ограничение: MSAA не сглаживает текстурные переходы или alpha-тест внутри треугольника.
Попробуем
- Поставить
sample_count: 1во всех трёх местах — вернуться к «лесенке» - Поставить
sample_count: 8— ещё более гладкие края (если GPU поддерживает) - Убрать
resolve_target(вернутьNone) — получить ошибку: мультисэмпловая текстура не может быть отображена напрямую - Включить
alpha_to_coverage_enabled: true— при полупрозрачных фрагментах результат изменится