Malformed depth with CompositorEffect shader

Godot Version

4.5 dev3

Question

I’m trying to port my postprocessing shader to CompositorEffect and ran into issues with depth map that I just can’t figure out, it’s not very visible in high res editor view, but I’m rendering the image in low res and stretch it up with nearest filter for that pixelated look and it becomes very obvious then.
This is how depth map looks with normal spatial gdshader:

And this is the shader used:

shader_type spatial;
uniform sampler2D depthTexture : hint_depth_texture, filter_nearest;

varying vec2 _screenUV;

void vertex() {	POSITION = vec4(VERTEX.xy, 1.0, 1.0); }

vec4 GetViewSpacePosAndDepth(vec2 screenUV, mat4 invProjectionMatrix) {
	float depth = texture(depthTexture, screenUV).x;
	vec3 ndc = vec3(screenUV * 2.0 - 1.0, depth);
	vec4 view = invProjectionMatrix * vec4(ndc, 1.0);
	view.xyz /= view.w;
	return vec4(view.xyz, -view.z);
}

void fragment() {
	_screenUV = SCREEN_UV;
	vec2 texelSize = 1.0 / VIEWPORT_SIZE;
	vec4 basePosition = GetViewSpacePosAndDepth(_screenUV, INV_PROJECTION_MATRIX);
	ALBEDO = vec3(basePosition.w/50.);
}

void light() {
	DIFFUSE_LIGHT = vec3(1.0);
}

Next after porting it to CompositorEffect I get this, note the uneven edges on cube and weird shape of the sphere, everything seems shifted by a pixel down and to the right too:


And this is my shader:

#[compute]
#version 450
        
layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;

layout(rgba16f, binding = 0, set = 0) uniform image2D screen_tex;
layout(binding = 0, set = 1) uniform sampler2D depth_tex;
layout(binding = 0, set = 2) uniform sampler2D normal_tex;

layout(push_constant, std430) uniform Params {
        vec2 screen_size;
        mat4 inv_proj_mat;
} p;

vec4 normal_roughness_compatibility(vec4 p_normal_roughness) {
        float roughness = p_normal_roughness.w;
        if (roughness > 0.5) {
                roughness = 1.0 - roughness;
        }
        roughness /= (127.0 / 255.0);
        return vec4(normalize(p_normal_roughness.xyz * 2.0 - 1.0) * 0.5 + 0.5, roughness);
}

vec3 get_normal(vec2 uv) {
        vec4 normal_roughness_raw = texture(normal_tex, uv);
        return normal_roughness_compatibility(normal_roughness_raw).rgb;
}

vec4 get_view_space_data(vec2 uv) {
        float depth = texture(depth_tex, uv).x;
        vec3 ndc = vec3(uv * 2.0 - 1.0, depth);
        vec4 view = p.inv_proj_mat * vec4(ndc, 1.0);
        view.xyz /= view.w;
        return vec4(view.xyz, -view.z);
}

void main() {
        ivec2 pixel = ivec2(gl_GlobalInvocationID.xy);
        vec2 size = p.screen_size;
        vec2 uv = pixel / size;
        if(pixel.x >= size.x || pixel.y >= size.y) return;
        
        vec2 texel_size = 1. / size;
        
        vec4 base_position = get_view_space_data(uv);
        vec4 color = imageLoad(screen_tex, pixel);
		color.rgb = vec3(base_position.w/50.);
        imageStore(screen_tex, pixel, color);
}

And here’s my implementation of CompositorEffect:

@tool
class_name OutlineEffect extends CompositorEffect

@export var reloadShader: bool:
	set(value):
		reload_shader()

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

func _init() -> void:
	RenderingServer.call_on_render_thread(initialize_compute_shader)

func _notification(what: int) -> void:
	if what == NOTIFICATION_PREDELETE: 
		cleanup()
			
func _render_callback(_effect_callback_type: int, render_data: RenderData) -> void:
	if not rd: return
	var scene_buffers: RenderSceneBuffersRD = render_data.get_render_scene_buffers()
	var scene_data: RenderSceneDataRD = render_data.get_render_scene_data()
	if not scene_buffers or not scene_data: return
	
	var size: Vector2i = scene_buffers.get_internal_size()
	if size.x == 0 or size.y == 0: return
	
	var x_groups: int = ceil(size.x / 16.0)
	var y_groups: int = ceil(size.y / 16.0)
	
	var proj_mat: Projection = scene_data.get_cam_projection()
	var inv_proj_mat = proj_mat.inverse()

	var push_constants: PackedFloat32Array = PackedFloat32Array()
	push_constants.append(size.x)
	push_constants.append(size.y)
	push_constants.append(0.0)
	push_constants.append(0.0)
	
	for col in 4:
		for row in 4:
			push_constants.append(inv_proj_mat[col][row])
	
	for view in scene_buffers.get_view_count():
		var screen_tex: RID = scene_buffers.get_color_layer(view)
		var depth_tex: RID = scene_buffers.get_depth_layer(view)
		var normal_tex: RID = scene_buffers.get_texture_slice("forward_clustered", "normal_roughness", view, 0, 1, 1)
		
		var uniform: RDUniform = RDUniform.new()
		uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
		uniform.binding = 0
		uniform.add_id(screen_tex)

		var image_uniform_set: RID = UniformSetCacheRD.get_cache(shader, 0, [uniform])

		uniform = RDUniform.new()
		uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_SAMPLER_WITH_TEXTURE
		uniform.binding = 0
		uniform.add_id(sampler)
		uniform.add_id(depth_tex)
		
		var depth_uniform_set: RID = UniformSetCacheRD.get_cache(shader, 1, [uniform])

		uniform = RDUniform.new()
		uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_SAMPLER_WITH_TEXTURE
		uniform.binding = 0
		uniform.add_id(sampler)
		uniform.add_id(normal_tex)

		var normal_uniform_set: RID = UniformSetCacheRD.get_cache(shader, 2, [uniform])
		
		var compute_list: int = rd.compute_list_begin()
		rd.compute_list_bind_compute_pipeline(compute_list, pipeline)
		rd.compute_list_bind_uniform_set(compute_list, image_uniform_set, 0)
		rd.compute_list_bind_uniform_set(compute_list, depth_uniform_set, 1)
		rd.compute_list_bind_uniform_set(compute_list, normal_uniform_set, 2)
		rd.compute_list_set_push_constant(compute_list, push_constants.to_byte_array(), push_constants.size() * 4)
		rd.compute_list_dispatch(compute_list, x_groups, y_groups, 1)
		rd.compute_list_end()

func initialize_compute_shader() -> void:
	rd = RenderingServer.get_rendering_device()
	if not rd: return
	create_shader()

func create_shader() -> void:
	var glsl_file: RDShaderFile = load("res://OutlineShader.glsl")
	shader = rd.shader_create_from_spirv(glsl_file.get_spirv())
	pipeline = rd.compute_pipeline_create(shader)

	var sampler_state: RDSamplerState = RDSamplerState.new()
	sampler_state.min_filter = RenderingDevice.SAMPLER_FILTER_NEAREST
	sampler_state.mag_filter = RenderingDevice.SAMPLER_FILTER_NEAREST
	
	sampler = rd.sampler_create(sampler_state)

func cleanup() -> void:
	if shader.is_valid(): 
		rd.free_rid(shader)
	if sampler.is_valid():
		rd.free_rid(sampler)

func reload_shader() -> void:
	cleanup()
	create_shader()

Any ideas what’s going on? I’m really stuck on that one.

One additional image to show how it looks when I blend screen_tex and depth together, you can easily see the shift happening:


(sorry for another post, website wouldn’t let me attach another image to original one)

EDIT: I noticed the same issue with normal map. neither normal map nor depth map align with color map.

So I’m embarrassed to say that after days of hair pulling I figured out that issue was very simple: I messed up uv calculation. I should’ve added 0.5 to my pixel coordinates to make sure uv points to the center of the pixel, not the corner. so the correct uv is: vec2 uv = (vec2(pixel)+0.5) / size;