Weird z-order in -z direction of heightmap shader (Solved: Was error in shader)

Godot Version

4.5.1

Question

I’m seeing some weird visual effects in de -z direction of my heightmap shader that I don’t really understand.


It seems the drawing order is incorrect when the camera faces the -z direction.

Heightmap was created like this:

	_heightmap_image = Image.create(region_size, region_size, false, Image.FORMAT_RF)
	for x in range(0, region_size):
		for y in range(0, region_size):
			_heightmap_image.set_pixel(x, y, Color(sin(deg_to_rad((x*10 + y*50) % 360)) * 2.0, 0.0, 0.0, 0.0))

This is my shader code:

shader_type spatial;

// Contains the positions of the heightmaps (can never be more than 4)
uniform ivec2[4] _heightmap_pos;
// Heightmap images themselves (can never be more than 4)
uniform highp sampler2DArray _height_maps : repeat_disable;
// Region size
uniform int _region_size = 1024;

// Active world position
uniform ivec2 _world_position = ivec2(512, 512);
// Active world size
uniform ivec2 _active_world_size = ivec2(1024, 1024);

varying vec2 basePos;

varying flat int _image_index;
varying flat ivec2 _local_heightmap_pos;

int get_heightmap_index(float x, float y) {
	ivec2 region_pos = ivec2(int(floor((float(_world_position.x) + x) / float(_region_size))), int(floor((float(_world_position.y) + y) / float(_region_size))));

	int index = 0;
	while (index < 4)
	{
		if (_heightmap_pos[index] == region_pos)
			return index;
		index++;
	}

	return -1;
}

vec3 get_normal(vec2 uv, float texelSize) {
	float t = texelFetch(_height_maps, ivec3(_local_heightmap_pos.x, _local_heightmap_pos.y - 1, 0), _image_index).r;
	float r = texelFetch(_height_maps, ivec3(_local_heightmap_pos.x + 1, _local_heightmap_pos.y, 0), _image_index).r;
	float l = texelFetch(_height_maps, ivec3(_local_heightmap_pos.x - 1, _local_heightmap_pos.y, 0), _image_index).r;
	float b = texelFetch(_height_maps, ivec3(_local_heightmap_pos.x, _local_heightmap_pos.y + 1, 0), _image_index).r;
	
	return -normalize(vec3(2.0 * (r - l), 2.0 * (b - t), -4.0));
}

void vertex() {
	// Get the correct image index
	_image_index = get_heightmap_index(UV.x * float(_active_world_size.x), UV.y * float(_active_world_size.y));

	// Calculate proper UV
	_local_heightmap_pos = ivec2(
		int(mod( (float(_world_position.x) + UV.x * float(_active_world_size.x)) , float(_region_size) )),
		int(mod( (float(_world_position.y) + UV.y * float(_active_world_size.y)) , float(_region_size) ))
	);

	if (_image_index != -1)
	{
		VERTEX.y = texelFetch(_height_maps, ivec3(_local_heightmap_pos.x, _local_heightmap_pos.y, 0), _image_index).r;
		NORMAL = get_normal(UV, 1.0);
	}
}

void fragment() {
	ALBEDO=vec3(1.0, 0.2 + mod(basePos.x * 0.1 * basePos.y, 0.5), 0.0);
}

Mesh that is used to display was created like this:

	_static_body_3d = StaticBody3D.new()
	_static_body_3d.collision_layer = 0b1000000000000001
	_static_body_3d.collision_mask  = 0b1000000000000001
	
	# Calculate mesh dimensions based on number of active chunks
	var mesh_dimension: float = ((active_chunk_range - 1) * 2 + 1) * fg_terrain.chunk_size
	
	_mesh_instance_3d = MeshInstance3D.new()
	#_mesh_instance_3d.custom_aabb.size
	
	_shader_material = ShaderMaterial.new()
	_shader_material.shader = preload("uid://dvmru2ujiob2d")
	_shader_material.set_shader_parameter("_height_pos", [Vector2(0,0), Vector2.ZERO, Vector2.ZERO, Vector2.ZERO])
	_shader_material.set_shader_parameter("_height_maps", fg_terrain._rid)
	_shader_material.set_shader_parameter("_region_size", fg_terrain.region_size)
	_shader_material.set_shader_parameter("_world_position", Vector2(mesh_dimension / 2, mesh_dimension / 2))
	_shader_material.set_shader_parameter("_active_world_size", Vector2(mesh_dimension, mesh_dimension))
	
	_plane_mesh = PlaneMesh.new()
	_plane_mesh.size = Vector2(mesh_dimension - 1, mesh_dimension - 1)
	_plane_mesh.subdivide_width = _plane_mesh.size.x - 1
	_plane_mesh.subdivide_depth = _plane_mesh.size.y - 1
	_plane_mesh.material = _shader_material
	
	_mesh_instance_3d.mesh = _plane_mesh
	
	_static_body_3d.add_child(_mesh_instance_3d)
	
	add_child(_static_body_3d)

Anybody has any idea what causes this problem? I’ve been googling around, but can’t seem to find a solution.

Try rendering the wireframe to see what’s actually happening with the geometry.

Here is a wireframe render.

the point where it partially disappears/changes order changes when moving the camera, and always starts at the crossover from +z to -z it seems. So I don’t think it’s the geometry itself. After all, it’s just a flat subdivided plane, with only the Y modified in height. So if it were the geometry, it would look the same when I rotate the camera.

For further debugging, render your heightmap as albedo without displacing the geometry. Btw, is there a specific reason why are you sampling with texelFetch() instead of texture()?

Here you go.

I believe texelFetch is somewhat faster than texture in this case because it skips additional filtering that is not needed for a heightmap (correct me if I’m wrong :slight_smile: ), so that’s why.

And same view, but WITH displacement

Here you can clearly see that the error starts occuring when crossing into the negative z

Still no clue as to why this is happening, hoping someone has an idea

Can you make a minimal reproduction project and share it?

1 Like

Alright, I’ve created a sample project, and there the problem did NOT occur.

After scratching my head several times, I found that a piece of shader code, that I thought I had disabled by a boolean parameter, was in fact turned on.

That part was meant to draw a small circle on the terrain where the mouse was, but because the other parameters were NOT set, but the code was accidentally enabled, this totally went haywire :face_with_peeking_eye:.

Thank you for your time @normalized, I’m really happy to finally have this figured out (was scratching my head for days already) :+1:

Alright, just to be a bit more precise. It wasn’t really that extra code. It was adding ALPHA=1.0 that caused the shader to work differently.

The real fix was either remove assigning something to the ALPHA, or adding the following at the top just below shader_type:

render_mode depth_draw_always, cull_disabled;

There was no ALPHA in the shader code you posted.

Yes, I removed that code (for brevity), because I though it was deactivated. But despite that, it mattered anyway. So sorry for that.

Of course it mattered. When you write to ALPHA the object is sent down the transparent (instead of opaque) rendering pipeline.

Well, that’s what I learned I guess :slight_smile:

Also this: Always provide full actual context. When you get stuck like this you don’t get to decide what is relevant :smiley:

Yeah, you are right :-). I tried to reduce the garbage as to not flood the thread, but if I wanted to do that, I had to do that first in my own code to make sure that the problem stil exists.