3D-постобработка: глубина и восстановление расстояния
Полноэкранный spatial-проход с reverse-Z, INV_PROJECTION_MATRIX и контролируемой дымкой.
Запустить пример
Скачать проект. Нужны Reckfell 1.0.0 с C# SDK и Forward+/Vulkan. Откройте project.reckfell, соберите C# и запустите Main.rscene. Все ресурсы включены: интерьер Pawn Shop от Poly Haven, CC0, с адаптированными материалами и светом.
У корня до запуска выберите Preview: Original — исходная комната, Distance — диагностическая глубина, Haze — дымка по расстоянию. Во время игры вызывайте SetPreview(mode) после _Ready.
Исходный кадр:
Та же камера с дымкой по глубине:
Подключить к своей сцене
Скачать два файла эффекта. Это компонент для существующего C#-проекта, не отдельная сцена.
- Скопируйте DepthHazePass.cs и DepthHaze.reckfellshader в корень asset:// и соберите C#.
- После добавления активной камеры в дерево вызовите camera.AddChild(new DepthHazePass()). Камера и проход должны принадлежать одному viewport.
- После _Ready самого прохода используйте SetDensity(value) для плотности или SetPreview(0/1/2) для дымки, расстояния и raw depth.
- При замене активной камеры подключите проход к новой камере. Полноэкранный меш, оставшийся у старой, не переносится автоматически.
Поместить проход в нужный viewport
DepthRoom3D создаёт комнату и добавляет DepthHazePass дочерним объектом её активной Camera3D. Материал читает screen_texture и depth_texture этого же viewport. Изображение, показанное через TextureRect в другом viewport, не переносит туда свой depth buffer.
using Reckfell;
public partial class DepthRoom3D : Entity3D
{
public enum Mode { Original, Distance, Haze }
[Export] public Mode Preview { get; set; } = Mode.Haze;
public PawnShop3D Room { get; private set; } = null!;
public DepthHazePass Pass { get; private set; } = null!;
public override void _Ready()
{
Room=new PawnShop3D();AddChild(Room);
Pass=new DepthHazePass();Room.ShotCamera.AddChild(Pass);
SetPreview(Preview);
}
public void SetPreview(Mode mode)
{
Pass.Visible=mode!=Mode.Original;
Pass.SetPreview(mode==Mode.Distance?1:0);
}
}QuadMesh имеет Size=(2,2), то есть координаты его вершин по X/Y занимают −1…1. vertex() записывает POSITION напрямую в clip space. В текущем reverse-Z проходе ближняя плоскость соответствует Z=1, поэтому используется vec4(VERTEX.xy, 1.0, 1.0), а не мировые координаты объекта.
CPU всё равно проверяет обычные границы меша до выполнения vertex(). Поэтому QuadMesh находится перед камерой как её потомок; ExtraCullMargin даёт запас, IgnoreOcclusionCulling исключает его из обычного occlusion culling. Одного присваивания POSITION недостаточно, чтобы исправить CPU-culling произвольного меша в другом месте мира.
using Reckfell;
public partial class DepthHazePass : MeshInstance3D
{
public ShaderMaterial Effect { get; private set; } = null!;
public override void _Ready()
{
Mesh=new QuadMesh { Size=new Vector2(2,2) };
Effect=new ShaderMaterial {
Shader=ResourceLoader.Load<Shader>("asset://DepthHaze.reckfellshader"),RenderPriority=127 };
MaterialOverride=Effect;
Position=new Vector3(0,0,-1);
ExtraCullMargin=1;
CastShadow=ShadowCastingSetting.Off;
GIMode=GIModeEnum.Disabled;
IgnoreOcclusionCulling=true;
}
public void SetPreview(int mode)=>Effect.SetShaderParameter("preview",Mathf.Clamp(mode,0,2));
public void SetDensity(float value)=>Effect.SetShaderParameter("haze_density",Mathf.Max(value,0));
}depth_draw_never не даёт проходу записать собственную глубину; depth_test_disabled позволяет перекрыть кадр; cull_disabled убирает зависимость от стороны quad. unshaded и fog_disabled исключают повторное освещение и штатный fog для самого прохода. CastShadow=Off и GIMode=Disabled не дают экранной геометрии участвовать в тенях и GI.
Восстановить расстояние
Полный DepthHaze.reckfellshader:
shader_type spatial;
render_mode unshaded, fog_disabled, depth_draw_never, depth_test_disabled, cull_disabled;
uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_linear;
uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest;
uniform int preview = 0; // 0: haze, 1: view-space depth, 2: raw depth
uniform float distance_range = 20.0;
uniform float haze_start = 2.0;
uniform float haze_density = 0.08;
uniform vec3 haze_color : source_color = vec3(0.46, 0.57, 0.66);
void vertex() {
POSITION = vec4(VERTEX.xy, 1.0, 1.0);
}
void fragment() {
float raw_depth = textureLod(depth_texture, SCREEN_UV, 0.0).r;
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
vec3 ndc = vec3(SCREEN_UV, raw_depth) * 2.0 - 1.0;
#else
vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, raw_depth);
#endif
vec4 view_position = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
view_position.xyz /= view_position.w;
float eye_depth = -view_position.z;
vec3 source = textureLod(screen_texture, SCREEN_UV, 0.0).rgb;
float haze = 1.0 - exp(-max(eye_depth - haze_start, 0.0) * max(haze_density, 0.0));
// A cleared reverse-Z depth value is background, not a finite surface sample.
if (raw_depth <= 0.000001) { haze = 0.0; }
vec3 result = mix(source, haze_color, haze);
if (preview == 1) { result = vec3(clamp(eye_depth / distance_range, 0.0, 1.0)); }
if (preview == 2) { result = vec3(raw_depth); }
ALBEDO = result;
ALPHA = 1.0;
}Значение depth_texture не является расстоянием. В текущем reverse-Z ближняя поверхность даёт большее значение, дальняя — меньшее; очищенный фон имеет 0. Для перспективной камеры зависимость нелинейная. Нельзя получить расстояние умножением raw_depth на Camera3D.Far или простым 1−raw_depth.
Перед умножением на INV_PROJECTION_MATRIX нужны координаты NDC. В Forward+/Vulkan глубина остаётся в диапазоне 0…1, а X/Y переводятся из SCREEN_UV в −1…1. В Compatibility/OpenGL в −1…1 переводятся все три компоненты. CURRENT_RENDERER выбирает ветку при компиляции; это не условие по названию GPU.
Далее shader умножает vec4(ndc,1) на INV_PROJECTION_MATRIX и делит XYZ на W. eye_depth=−view_position.z — положительное расстояние вдоль направления камеры. Это не длина луча до точки: у края экрана length(view_position.xyz) будет другим числом. Та же реконструкция проверена для Perspective и Orthogonal; для ортографической камеры исходная зависимость глубины уже линейная.
Разобрать эффект и диагностический режим
haze_start задаёт расстояние начала дымки в единицах мира. haze_density управляет скоростью смешивания: 0 сохраняет исходный цвет, положительное значение постепенно приближает дальние поверхности к haze_color. Контроллер не передаёт отрицательную плотность. Фон с очищенной глубиной исключён из эффекта.
Distance — диагностическая маска, не финальная картинка игры. Чем светлее поверхность, тем больше eye_depth:
В маске eye_depth делится на distance_range=20 и ограничивается диапазоном 0…1. После этого цвет проходит через тонмаппинг и цветовое кодирование. Серый канал сохранённого PNG нельзя напрямую считать численным расстоянием или исходным raw_depth. Очищенный фон в Distance выглядит белым из-за граничной реконструкции до Far; реальной поверхности там нет.
Контрольный стенд проверяет плоскости на 2, 6 и 12 единицах, изменение Near/Far и ортографического Size, а также перенос и поворот камеры. Проверка учитывает цветовое преобразование результата. На иллюстрациях — реальный рендер готового интерьера, а не эти контрольные плоскости.
Учитывать место копирования
В Forward+ screen/depth-копии для этого материала подготавливаются до последующей прозрачной отрисовки. Не считайте screen_texture финальным кадром со всеми transparent-объектами и интерфейсом. RenderPriority=127 не заставляет движок заново снять эти буферы после каждого прозрачного материала.
Обычное прозрачное стекло, частицы и другие материалы без нужной записи глубины могут не иметь собственной поверхности в depth_texture. Этот пример не решает обработку всех таких случаев. 2D BackBufferCopy из соседнего урока не является командой обновления 3D depth buffer. Для обработки уже показанной картинки без 3D-глубины используйте canvas_item-проход.
Диагностика
| Симптом | Проверка |
|---|---|
| эффект пропадает при движении | quad принадлежит активной камере, находится перед ней; CPU-culling и occlusion culling |
| почти чёрная raw-маска | нелинейная глубина и выбранные Near/Far; для расстояния используйте реконструкцию |
| расстояние неверно в другом рендерере | ветка преобразования Z в NDC |
| после смены Near/Far формула ломается | текущая INV_PROJECTION_MATRIX вместо вручную подобранного множителя |
| проход меняет освещение или тени | unshaded, fog_disabled, CastShadow=Off, GIMode=Disabled |
| интерфейс или стекло обрабатываются иначе | место screen/depth-копирования и фактическая запись глубины этими объектами |
Проверены численные контрольные случаи в Compatibility/OpenGL и Forward+/Vulkan. Полная комната и иллюстрации проверены только в Forward+: 2560×1440, внутренний масштаб 1.5, MSAA 8× и TAA. Normal buffer, XR, прозрачные поверхности во всех режимах, экспорт приложения и производительность не проверены.