Reading from DEPTH_TEXTURE produces noticeable artifacts with MSAA enabled

GODOT VERSION:
4.3.dev6

==================
Hello all,
I have been facing some issues with shader development. Specifically, I have been developing two major shaders simultaneously. The one that I am facing trouble with is the ocean shader I am developing. It works great, and after weeks of progress I finally have a result I am happy with. One issue though is my Object - Foam System. In my shader I read from the DEPTH_TEXTURE ( in the form of hint_depth_texture of course) I then use the depth to make some water darker, but more noticeably, foam around the edges of objects which are partially submerged. here is a picture of the foam, on a simple 5x5x5 box mesh:

Pictures: (Imgur due to first post restrictions) Box Meshes and Foam - Album on Imgur

And here is an exact duplicate of the BoxMesh, fully out of the water:

Refer to Imgur link.

All is well in this picture, expect of course that anti aliasing is off. Now here are the exact same two boxes, in the same order, but with MSAA 2X enabled:

Refer to Imgur link.

As you can pretty clearly see, with MSAA enabled to any degree (2x, 4x, 8x) the depth texture “bleeds” onto objects which would otherwise not be included in said depth texture.
My water shader code is as follows, in full:
https://pastebin.com/wUasg5Bd
A similar topic, two years old at this point in time is listed:

GOOGLE SEARCH: “Is there a way to disable anti-aliasing on just the depth texture?”

Now this issue is easily solved a couple different ways, with there drawback listed as well:

  1. No anti aliasing / different anti aliasing method (FXAA, TAA, etc.) Best solution for me currently, but you know the drawbacks of these methods, blurred screen, less precise edge blurring, etc.
  2. Leave it, not that noticeable. First off, in other games foam may be a major factor. Second off, I have terrain (hills, specifically) that when looking over the terrain unto the water, you can see a clear line of white (foam) on the border between the apex of the terrain and the distant plane of water. THAT is a lot more noticeable than some small objects.
  3. Ditch the foam entirely. I like to think that I can keep a graphics feature that I love that I worked hard on, and that there is a fix or fix in the future. But maybe not.

Alas, I am not frustrated at anything, not the engine, nor the devs, nor graphics shenanigans. Because this is a relatively small issue, and not any one person’s fault. I just wanted to bring it to light, or rather back to light (given the two year old post) since I would love to use MSAA with my ocean shader. Other than that Godot is working amazing!

Thanks.

Relevant fragment code (from pastebin)
    //DEPTH STUFF - OBJECT FOAM, UNDERWATER DARKNESS//////////
    float depth = texture(DEPTH_TEXTURE, SCREEN_UV).r; 
    depth = PROJECTION_MATRIX[3][2] / (depth + PROJECTION_MATRIX[2][2]);
    depth = VERTEX.z+depth;
    
    //OBJECT FOAM
    ALBEDO = mix(vec3(1.0),ALBEDO,smoothstep(0.25,0.5, depth));
    
    depth *= 0.0002;
    depth = smoothstep(0.0003,0.00035,depth);
    depth = pow(1.8, depth);
    
    ALBEDO *= vec3(depth);
    //////////////////////////////////////////////////////////

For starters, this is a noticeable artifact for me - I imagine it’s even more noticeable with your hills.

I’m not yet familiar with the rendering pipeline in Godot so I won’t have a definitive answer to your problem. Are you certain that the depth pass is actually being anti-aliased; do you have proof? Not saying you’re wrong. I just want to verify the information.

If the depth pass is indeed anti-aliased, and there is no way to disable it, perhaps you should take this into account in your shader. Perhaps shrinking your foam “area” by 1 pixel to avoid the artifact would do the trick?

Not too sure here, but maybe you could use a 3x3 kernel on your depth value to produce a bitmask. The kernel would require all pixels to be within a certain range to be white - otherwise, black. Then using that bitmask, foam around edges will be removed.

Does that make sense?

Thank you for your comment and suggestions! I will be investigating further into the issue, and will post any updates. I imagine the issue is somewhat as follows:
Imagine you have a low depth area, which would produce foam. Now imagine that that low depth area is occluded by an object. Now the foam pixels are hidden. But now if the depth texture is anti aliased, or blurred somewhat, the “occluded” depth pixels now bleed outside their occluder, AKA the hills or whatever is in front of the water. Just a thought.

I sort of look at it in reverse. The occluder hides foam that should not be visible but because the occluder is anti-aliased, the foam is no longer occluded at the edges thus producing the artifact in your image(s).

One thing that may prove crucial to know is: how does MSAA work? I’m pretty sure it’s not a post-processing effect since it performs multi-sampling. Maybe it’s possible to find Godot’s implementation for MSAA and see what actually happens.

Let me know how you get on.

alright i found this after a little bit of digging in the repo you sent:

In this function for getting the depth layer, which I think this is what this function does, there is a parameter for msaa. So when you enable MSAA, the engine uses their MSAA ‘method’ on the depth texdture. So it appears not to be a bug, but a feature (dev joke lol). But in all seriousness, I wonder why they would do this, and I wonder if there is a way to prevent it? And little update, I tested my shader some more and found that the incorrect values of depth arent even the result of blurred occluders, but they are simply a hard line of the max depth value. So basically all objects, when overlayed on my water shader, are outlined with a max (or rather 0, for my shaders sake) value in the depth texture. so weird…

If you use MSAA on color, you have to use it on depth as well. Otherwise, you get an error from the graphics API and nothing will show up on screen.

The depth prepass also uses MSAA when needed for this reason.

1 Like

Good to know! I guess that narrows down your options quite a bit.

Sounds like an odd artifact.

In any case, since the depth texture will contain unusable values at the boundary of opaque meshes, you have to find a way to process the depth texture before using it.

Have you thought about whether the eroded bitmask I suggested would work for your case?

1 Like

Thank you @Calinou for the info. I didn’t realize that about rendering. @Sweatix I will look into your bitmask solution you suggested. I will update as usual if I figure anything out.

UPDATE!!!
I was able to fix the issue, using @Sweatix suggestion of a “bitmask” system. I now sample the left, right, bottom, and top pixels, check whether their depth meets the threshold, then if it doesn’t than the central pixel gets marked off for foam. The more checks you add, the less stray white pixels you will get, at the cost of performance. In order to improve performance, instead of running the check on ever single pixel, I first check whether the current pixel is even good for foam, and if it is, it gets the check. if not, no check, still no foam. Here is a function i put together for the issue, and the code in my fragment function:

float is_edge(vec2 pix_offset, vec2 viewport_size, vec2 screen_uv, mat4 inv_proj, float vert) {
	vec2 iuv = vec2(screen_uv.x * viewport_size.x, screen_uv.y * viewport_size.y);
	float neighbor_depth = texture(DEPTH_TEXTURE, (iuv + vec2(pix_offset.x, pix_offset.y)) / viewport_size).x;
	vec3 ndc = vec3(screen_uv * 2.0 - 1.0, neighbor_depth);
	vec4 view = inv_proj * vec4(ndc, 1.0);
	view.xyz /= view.w;
	float new_lin_depth = -view.z;
	new_lin_depth = vert + new_lin_depth;
	return step(2.0, new_lin_depth);
}
//DEPTH STUFF - OBJECT FOAM, UNDERWATER DARKNESS//////////
	float depth = texture(DEPTH_TEXTURE, SCREEN_UV).r;
	depth = PROJECTION_MATRIX[3][2] / (depth + PROJECTION_MATRIX[2][2]);
	depth = VERTEX.z+depth;
	
	depth = texture(DEPTH_TEXTURE, SCREEN_UV).x;
	vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
	vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
	view.xyz /= view.w;
	float linear_depth = -view.z;
	linear_depth = VERTEX.z + linear_depth;
	
	if (step(0.5, linear_depth) != 1.0) {
		linear_depth += 1000.0 * is_edge(vec2(-1.0, 0.0), VIEWPORT_SIZE, SCREEN_UV, INV_PROJECTION_MATRIX, VERTEX.z);
		linear_depth += 1000.0 * is_edge(vec2(1.0, 0.0), VIEWPORT_SIZE, SCREEN_UV, INV_PROJECTION_MATRIX, VERTEX.z);
		linear_depth += 1000.0 * is_edge(vec2(0.0, -1.0), VIEWPORT_SIZE, SCREEN_UV, INV_PROJECTION_MATRIX, VERTEX.z);
		linear_depth += 1000.0 * is_edge(vec2(0.0, 1.0), VIEWPORT_SIZE, SCREEN_UV, INV_PROJECTION_MATRIX, VERTEX.z);
		linear_depth += 1000.0 * is_edge(vec2(-1.0, -1.0), VIEWPORT_SIZE, SCREEN_UV, INV_PROJECTION_MATRIX, VERTEX.z);
		linear_depth += 1000.0 * is_edge(vec2(1.0, -1.0), VIEWPORT_SIZE, SCREEN_UV, INV_PROJECTION_MATRIX, VERTEX.z);
	}

Now this system definitely ends the life of some frames, but nothing too drastic. and for something like this, it is just a matter of freeing up enough frames to make up for the lost ones. also using msaa already hurts fps, so at this point you probably shouldnt be woryying about the lost frames. anyways, hope some other people find this helpful!

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.