reckfellENGINE

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#-проекта, не отдельная сцена.

  1. Скопируйте DepthHazePass.cs и DepthHaze.reckfellshader в корень asset:// и соберите C#.
  2. После добавления активной камеры в дерево вызовите camera.AddChild(new DepthHazePass()). Камера и проход должны принадлежать одному viewport.
  3. После _Ready самого прохода используйте SetDensity(value) для плотности или SetPreview(0/1/2) для дымки, расстояния и raw depth.
  4. При замене активной камеры подключите проход к новой камере. Полноэкранный меш, оставшийся у старой, не переносится автоматически.

Поместить проход в нужный viewport

DepthRoom3D создаёт комнату и добавляет DepthHazePass дочерним объектом её активной Camera3D. Материал читает screen_texture и depth_texture этого же viewport. Изображение, показанное через TextureRect в другом viewport, не переносит туда свой depth buffer.

C#
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 произвольного меша в другом месте мира.

C#
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, прозрачные поверхности во всех режимах, экспорт приложения и производительность не проверены.

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.