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.
Checking the stable version of the documentation...
합성기
합성기는 :ref:`뷰포트 <class_Viewport>`의 콘텐츠를 렌더링할 때 렌더링 파이프라인을 제어할 수 있는 Godot 4의 새로운 기능입니다.
모든 뷰포트에 적용되는 WorldEnvironment 노드에서 구성하거나 :ref:`Camera3D <class_Camera3D>`에서 구성하고 해당 카메라를 사용하는 뷰포트에만 적용할 수 있습니다.
Compositor 리소스는 합성기를 구성하는 데 사용됩니다. 시작하려면 적절한 노드에 새 컴포지터를 만듭니다.
참고
합성기는 현재 Mobile 및 Forward+ 렌더러에서만 지원되는 기능입니다.
효과 추가하기
합성기 효과를 사용하면 다양한 단계에서 렌더링 파이프라인에 추가 논리를 삽입할 수 있습니다. 이는 최상의 이점을 활용하려면 렌더링 파이프라인에 대한 높은 수준의 이해가 필요한 고급 기능입니다.
컴포지터 효과의 핵심 논리는 렌더링 파이프라인에서 호출되므로 이 논리는 렌더링이 수행되는 스레드 내에서 실행된다는 점에 유의하는 것이 중요합니다. 스레딩 문제가 발생하지 않도록 주의를 기울여야 합니다.
컴포지터 효과를 사용하는 방법을 설명하기 위해 자체 셰이더 코드를 작성하고 계산 셰이더를 통해 이 전체 화면을 적용할 수 있는 간단한 사후 처리 효과를 만들어 보겠습니다. 완성된 데모 프로젝트는 `여기 <https://github.com/godotengine/godot-demo-projects/tree/master/compute/post_shader>`_에서 찾을 수 있습니다.
``post_process_shader.gd``라는 새로운 스크립트를 만드는 것부터 시작합니다. 이 도구를 스크립트 도구로 만들어 편집기에서 합성기 효과가 작동하는 것을 볼 수 있습니다. :ref:`CompositorEffect <class_CompositorEffect>`에서 노드를 확장해야 합니다. 또한 스크립트에 클래스 이름을 지정해야 합니다.
@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);
}
"""
컴퓨트 셰이더가 작동하는 방식에 대한 자세한 정보는 컴퓨트 셰이더 사용하기를 확인해 주세요.
여기서 중요한 점은 화면의 모든 픽셀에 대해 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()
이 방법의 맨 위에서 다시 뮤텍스를 사용하여 사용자 셰이더 코드 및 is dirty 플래그에 대한 액세스를 보호합니다. 사용자 셰이더 코드가 더러운 경우 사용자 셰이더 코드의 로컬 복사본을 만듭니다.
새 코드 조각이 없으면 이미 유효한 파이프라인이 있으면 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 함수에서 설정했지만 사용자가 UI에서 이를 변경할 수 있습니다.
p_render_data 매개변수를 사용하면 현재 렌더링 중인 프레임과 관련된 데이터를 보유하는 객체에 액세스할 수 있습니다. 현재 우리는 렌더링 엔진에서 사용하는 모든 내부 버퍼에 대한 액세스를 제공하는 렌더링 씬 버퍼에만 관심이 있습니다. 전체 API를 이 데이터에 노출하기 위해 이를 :ref:`RenderSceneBuffersRD <class_RenderSceneBuffersRD>`로 캐스팅했습니다.
다음으로 업스케일링 전(해당하는 경우) 3D 렌더 버퍼의 해상도인 ``internal size``를 얻습니다. 업스케일링은 포스트 프로세스가 실행된 후에 발생합니다.
내부 크기에서 그룹 크기를 계산하고 템플릿 셰이더에서 로컬 크기를 확인하세요.
또한 셰이더가 크기를 알 수 있도록 푸시 상수를 채웁니다. Godot는 여기서 아직 구조체를 지원하지 않으므로 ``PackedFloat32Array``를 사용하여 이 데이터를 저장합니다. 이 배열을 16바이트 정렬로 채워야 한다는 점에 유의하세요. 즉, 배열의 길이는 4의 배수여야 합니다.
이제 뷰를 반복합니다. 이는 스테레오 렌더링(XR)에 적용할 수 있는 멀티뷰 렌더링을 사용하는 경우입니다. 대부분의 경우 하나의 뷰만 갖게 됩니다.
참고
여기서는 사후 처리를 위해 멀티뷰를 사용하는 것이 성능상의 이점이 없습니다. 이와 같이 뷰를 별도로 처리하면 GPU가 여전히 병렬 처리를 사용할 수 있습니다.
다음으로 이 뷰에 대한 색상 버퍼를 얻습니다. 이것은 3D 씬이 렌더링된 버퍼입니다.
그런 다음 색상 버퍼를 셰이더에 전달할 수 있도록 균일한 세트를 준비합니다.
각 프레임마다 균일한 세트를 확인할 수 있도록 보장하는 UniformSetCacheRD 캐시의 사용에 주목하세요. 색상 버퍼는 프레임마다 바뀔 수 있고 균일 캐시는 버퍼가 해제될 때 자동으로 균일 세트를 정리하므로 이는 메모리 누수나 오래된 세트 사용을 방지하는 안전한 방법입니다.
마지막으로 파이프라인을 바인딩하고, 유니폼 세트를 바인딩하고, 푸시 상수 데이터를 푸시하고, 그룹에 대한 디스패치를 호출하여 컴퓨팅 목록을 구축합니다.
컴포지터 효과가 완료되었으면 이제 이를 컴포지터에 추가해야 합니다.
합성기에서 합성기 효과 속성을 확장하고 ``Add Element``를 누릅니다.
이제 컴포지터 효과를 추가할 수 있습니다.
``PostProcessShader``를 선택한 후 사용자 셰이더 코드를 설정해야 합니다.
float gray = color.r * 0.2125 + color.g * 0.7154 + color.b * 0.0721;
color.rgb = vec3(gray);
모든 작업이 완료되면 출력은 회색조로 표시됩니다.
참고
포스트 효과의 고급 예를 보려면 Bastiaan Olij가 만든 방사형 흐림 기반 하늘 광선 예제 프로젝트를 확인하세요.