Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Композитор

Композитор — це нова функція в Godot 4, яка дозволяє контролювати конвеєр відтворення під час відтворення вмісту Viewport.

Його можна налаштувати на вузлі WorldEnvironment, де він застосовується до всіх вікон перегляду, або його можна налаштувати на Camera3D і застосовувати лише до вікна перегляду, що використовує цю камеру.

Ресурс Compositor використовується для налаштування композитора. Щоб почати, створіть новий композитор на відповідному вузлі:

../../_images/new_compositor.webp

Примітка

Композитор наразі є функцією, яка підтримується лише модулями Mobile та Forward+.

Ефекти композитора

Ефекти композитора дозволяють вставляти додаткову логіку в конвеєр рендерингу на різних етапах. Це розширена функція, яка потребує високого рівня розуміння конвеєра візуалізації, щоб використовувати її якнайкраще.

Оскільки основна логіка ефекту композитора викликається з конвеєра візуалізації, важливо зазначити, що ця логіка, таким чином, працюватиме в потоці, у якому відбувається рендеринг. Потрібно бути обережним, щоб ми не зіткнулися з проблемами потоків.

Щоб проілюструвати, як використовувати ефекти композитора, ми створимо простий ефект постобробки, який дозволить вам написати власний код шейдера та застосувати його на весь екран через обчислювальний шейдер. Ви можете знайти готовий демонстраційний проект тут.

Ми починаємо зі створення нового сценарію під назвою post_process_shader.gd. Ми зробимо це інструментальним сценарієм, щоб ми могли бачити, як ефект композитора працює в редакторі. Нам потрібно розширити наш вузол із CompositorEffect. Ми також повинні дати нашому сценарію назву класу.

post_process_shader.gd
@tool
extends CompositorEffect
class_name PostProcessShader

Далі ми визначимо константу для нашого коду шаблону шейдера. Це шаблонний код, завдяки якому працює наш обчислювальний шейдер.

const template_shader: String = """
#version 450

// Invocations in the (x, y, z) dimension
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;

layout(rgba16f, set = 0, binding = 0) uniform image2D color_image;

// Our push constant
layout(push_constant, std430) uniform Params {
    vec2 raster_size;
    vec2 reserved;
} params;

// The code we want to execute in each invocation
void main() {
    ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
    ivec2 size = ivec2(params.raster_size);

    if (uv.x >= size.x || uv.y >= size.y) {
        return;
    }

    vec4 color = imageLoad(color_image, uv);

    #COMPUTE_CODE

    imageStore(color_image, uv, color);
}
"""

Щоб дізнатися більше про те, як працюють обчислювальні шейдери, перегляньте Using compute shaders.

Важливим тут є те, що для кожного пікселя на нашому екрані виконується наша функція main, і всередині неї ми завантажуємо поточне значення кольору нашого пікселя, виконуємо наш код користувача та записуємо наш змінений колір назад у наш колір зображення.

#COMPUTE_CODE замінюється нашим кодом користувача.

Щоб встановити наш код користувача, нам потрібна змінна експорту. Ми також визначимо кілька змінних сценарію, які будемо використовувати:

@export_multiline var shader_code: String = "":
    set(value):
        mutex.lock()
        shader_code = value
        shader_is_dirty = true
        mutex.unlock()

var rd: RenderingDevice
var shader: RID
var pipeline: RID

var mutex: Mutex = Mutex.new()
var shader_is_dirty: bool = true

Зверніть увагу на використання Mutex у нашому коді. Більшість наших реалізацій викликається механізмом візуалізації і, таким чином, працює в нашому потоці візуалізації.

Нам потрібно переконатися, що ми встановили наш новий шейдерний код і позначили наш шейдерний код як брудний, без того, щоб наш потік візуалізації одночасно отримував доступ до цих даних.

Далі ми ініціалізуємо наш ефект.

# Called when this resource is constructed.
func _init():
    effect_callback_type = EFFECT_CALLBACK_TYPE_POST_TRANSPARENT
    rd = RenderingServer.get_rendering_device()

Головне тут — це встановити наш effect_callback_type, який повідомляє механізму візуалізації, на якому етапі конвеєра візуалізації викликати наш код.

Примітка

Наразі ми маємо доступ лише до етапів конвеєра 3D-візуалізації!

Ми також отримуємо посилання на наш пристрій візуалізації, що стане в нагоді.

Нам також потрібно прибрати за собою, для цього ми реагуємо на сповіщення NOTIFICATION_PREDELETE:

# System notifications, we want to react on the notification that
# alerts us we are about to be destroyed.
func _notification(what):
    if what == NOTIFICATION_PREDELETE:
        if shader.is_valid():
            # Freeing our shader will also free any dependents such as the pipeline!
            rd.free_rid(shader)

Зауважте, що ми не використовуємо наш м’ютекс тут, хоча ми створюємо наш шейдер всередині нашого потоку візуалізації. Методи на нашому сервері візуалізації є потокобезпечними, і free_rid буде відкладено очищення шейдера до завершення будь-яких кадрів, які зараз візуалізуються.

Також зауважте, що ми не звільняємо наш трубопровід. Пристрій візуалізації виконує відстеження залежностей, і оскільки конвеєр залежить від шейдера, він буде автоматично звільнено, коли шейдер буде знищено.

З цього моменту наш код буде працювати в потоці візуалізації.

Наш наступний крок — допоміжна функція, яка перекомпілює шейдер, якщо код користувача було змінено.

# Check if our shader has changed and needs to be recompiled.
func _check_shader() -> bool:
    if not rd:
        return false

    var new_shader_code: String = ""

    # Check if our shader is dirty.
    mutex.lock()
    if shader_is_dirty:
        new_shader_code = shader_code
        shader_is_dirty = false
    mutex.unlock()

    # We don't have a (new) shader?
    if new_shader_code.is_empty():
        return pipeline.is_valid()

    # Apply template.
    new_shader_code = template_shader.replace("#COMPUTE_CODE", new_shader_code);

    # Out with the old.
    if shader.is_valid():
        rd.free_rid(shader)
        shader = RID()
        pipeline = RID()

    # In with the new.
    var shader_source: RDShaderSource = RDShaderSource.new()
    shader_source.language = RenderingDevice.SHADER_LANGUAGE_GLSL
    shader_source.source_compute = new_shader_code
    var shader_spirv: RDShaderSPIRV = rd.shader_compile_spirv_from_source(shader_source)

    if shader_spirv.compile_error_compute != "":
        push_error(shader_spirv.compile_error_compute)
        push_error("In: " + new_shader_code)
        return false

    shader = rd.shader_create_from_spirv(shader_spirv)
    if not shader.is_valid():
        return false

    pipeline = rd.compute_pipeline_create(shader)
    return pipeline.is_valid()

У верхній частині цього методу ми знову використовуємо наш м’ютекс, щоб захистити доступ до нашого коду шейдера користувача та прапор брудний. Ми робимо локальну копію коду шейдера користувача, якщо наш код шейдера користувача брудний.

Якщо у нас немає нового фрагмента коду, ми повертаємо true, якщо ми вже маємо дійсний конвеєр.

Якщо у нас є новий фрагмент коду, ми вставляємо його в код нашого шаблону, а потім компілюємо.

Попередження

Показаний тут код компілює наш новий код під час виконання. Це чудово підходить для прототипування, оскільки ми можемо відразу побачити ефект зміненого шейдера.

Це запобігає попередній компіляції та кешування цього шейдера, що може бути проблемою на деяких платформах, таких як консолі. Зауважте, що демонстраційний проект постачається з альтернативним прикладом, де файл glsl містить весь обчислювальний шейдер, і це використовується. За допомогою цього підходу Godot може попередньо скомпілювати та кешувати шейдер.

Нарешті, нам потрібно реалізувати наш зворотний виклик ефекту, механізм візуалізації викличе це на потрібному етапі рендерингу.

# Called by the rendering thread every frame.
func _render_callback(p_effect_callback_type, p_render_data):
    if rd and p_effect_callback_type == EFFECT_CALLBACK_TYPE_POST_TRANSPARENT and _check_shader():
        # Get our render scene buffers object, this gives us access to our render buffers.
        # Note that implementation differs per renderer hence the need for the cast.
        var render_scene_buffers: RenderSceneBuffersRD = p_render_data.get_render_scene_buffers()
        if render_scene_buffers:
            # Get our render size, this is the 3D render resolution!
            var size = render_scene_buffers.get_internal_size()
            if size.x == 0 and size.y == 0:
                return

            # We can use a compute shader here.
            var x_groups = (size.x - 1) / 8 + 1
            var y_groups = (size.y - 1) / 8 + 1
            var z_groups = 1

            # Push constant.
            var push_constant: PackedFloat32Array = PackedFloat32Array()
            push_constant.push_back(size.x)
            push_constant.push_back(size.y)
            push_constant.push_back(0.0)
            push_constant.push_back(0.0)

            # Loop through views just in case we're doing stereo rendering. No extra cost if this is mono.
            var view_count = render_scene_buffers.get_view_count()
            for view in range(view_count):
                # Get the RID for our color image, we will be reading from and writing to it.
                var input_image = render_scene_buffers.get_color_layer(view)

                # Create a uniform set.
                # This will be cached; the cache will be cleared if our viewport's configuration is changed.
                var uniform: RDUniform = RDUniform.new()
                uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
                uniform.binding = 0
                uniform.add_id(input_image)
                var uniform_set = UniformSetCacheRD.get_cache(shader, 0, [ uniform ])

                # Run our compute shader.
                var compute_list:= rd.compute_list_begin()
                rd.compute_list_bind_compute_pipeline(compute_list, pipeline)
                rd.compute_list_bind_uniform_set(compute_list, uniform_set, 0)
                rd.compute_list_set_push_constant(compute_list, push_constant.to_byte_array(), push_constant.size() * 4)
                rd.compute_list_dispatch(compute_list, x_groups, y_groups, z_groups)
                rd.compute_list_end()

На початку цього методу ми перевіряємо, чи є у нас пристрій візуалізації, чи є наш тип зворотного виклику правильним, а також перевіряємо, чи є у нас шейдер.

Примітка

Перевірка типу ефекту є лише механізмом безпеки. Ми встановили це у нашій функції _init, однак користувач може змінити це в інтерфейсі користувача.

Наш параметр p_render_data надає нам доступ до об’єкта, який містить дані, специфічні для кадру, який ми зараз візуалізуємо. Наразі нас цікавлять лише наші буфери сцени візуалізації, які надають нам доступ до всіх внутрішніх буферів, що використовуються механізмом рендерингу. Зауважте, що ми передаємо це в RenderSceneBuffersRD, щоб відкрити повний API для цих даних.

Далі ми отримуємо наш внутрішній розмір, який є роздільною здатністю наших буферів 3D-рендерінгу перед їх збільшенням (якщо застосовно), збільшення відбувається після виконання наших постпроцесів.

Виходячи з нашого внутрішнього розміру, ми обчислюємо розмір нашої групи, дивіться наш локальний розмір у нашому шейдері шаблону.

Ми також заповнюємо константу push, щоб наш шейдер знав наш розмір. Godot поки що не підтримує структури, тому ми використовуємо PackedFloat32Array для зберігання цих даних. Зверніть увагу, що ми повинні доповнити цей масив 16-байтовим вирівнюванням. Іншими словами, довжина нашого масиву має бути кратною 4.

Тепер ми переглядаємо наші перегляди, це на випадок, якщо ми використовуємо мультиракурсний рендеринг, який застосовний для стереорендерінгу (XR). У більшості випадків ми матимемо лише один вид.

Примітка

Немає ніякої переваги в продуктивності від використання multiview для постобробки тут, обробка переглядів окремо, як це, все одно дозволить GPU використовувати паралелізм, якщо це вигідно.

Далі ми отримуємо кольоровий буфер для цього перегляду. Це буфер, у який було відтворено нашу 3D-сцену.

Потім ми готуємо однорідний набір, щоб ми могли передати колірний буфер нашому шейдеру.

Зверніть увагу на використання нашого UniformSetCacheRD кешу, який гарантує, що ми можемо перевіряти наш уніфікований набір кожного кадру. Оскільки наш буфер кольорів може змінюватися від кадру до кадру, а наш уніфікований кеш автоматично очищатиме уніфіковані набори, коли буфери звільняються, це безпечний спосіб гарантувати, що ми не втратимо пам’ять або не використаємо застарілий набір.

Нарешті ми створюємо наш список обчислень, прив’язуючи наш конвеєр, прив’язуючи наш уніфікований набір, надсилаючи наші дані push-константи та викликаючи диспетчеризацію для наших груп.

Коли наш композитор завершено, тепер нам потрібно додати його до нашого композитора.

У нашому композиторі ми розгортаємо властивість ефектів композитора та натискаємо Додати елемент.

Тепер ми можемо додати наш ефект композитора:

../../_images/add_compositor_effect.webp

Після вибору нашого Post Process Shader нам потрібно встановити код користувача шейдера:

float gray = color.r * 0.2125 + color.g * 0.7154 + color.b * 0.0721;
color.rgb = vec3(gray);

Після того, як усе зроблено, наш результат буде у відтінках сірого.

../../_images/post_process_shader.webp

Примітка

Щоб отримати більш складний приклад постефектів, ознайомтеся з прикладом проекту Промені неба на основі радіального розмиття, створеного Бастіаном Олієм.