Exclude Objects From Post Processing Shader

Godot Version

4.6.1

Question

I’m using the following edge detection shader (by Leo Peltola) in my game to create a colored outline around objects.

shader_type spatial;
render_mode unshaded;

// MIT License. Made by Leo Peltola
// Inspired by https://threejs.org/examples/webgl_postprocessing_pixel.html

uniform sampler2D DEPTH_TEXTURE : hint_depth_texture, filter_linear_mipmap;
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap;
uniform sampler2D NORMAL_TEXTURE : hint_normal_roughness_texture, filter_nearest;

uniform sampler2D mask_texture : hint_default_black; // Assign your SubViewport Texture here

uniform bool shadows_enabled = true;
uniform bool highlights_enabled = true;
uniform float shadow_strength : hint_range(0.0, 1.0, 0.01) = 0.4;
uniform float highlight_strength : hint_range(0.0, 10.0, 0.01) = 0.1;
uniform vec3 highlight_color : source_color = vec3(1.);
uniform vec3 shadow_color : source_color = vec3(0.0);

varying mat4 model_view_matrix;

uniform float depth_threshold : hint_range(0.0, 10.0, 0.001) = 0.0f;

float getDepth(vec2 screen_uv, sampler2D depth_texture, mat4 inv_projection_matrix){
//	Credit: https://godotshaders.com/shader/depth-modulated-pixel-outline-in-screen-space/
	float raw_depth = texture(depth_texture, screen_uv)[0];
	vec3 normalized_device_coordinates = vec3(screen_uv * 2.0 - 1.0, raw_depth);
    vec4 view_space = inv_projection_matrix * vec4(normalized_device_coordinates, 1.0);
	view_space.xyz /= view_space.w;
	return -view_space.z;
}

void vertex(){
    model_view_matrix = VIEW_MATRIX * mat4(INV_VIEW_MATRIX[0], INV_VIEW_MATRIX[1], INV_VIEW_MATRIX[2], MODEL_MATRIX[3]);
}

void fragment() {
	vec2 e = vec2(1./VIEWPORT_SIZE.xy);

//	Shadows
	float depth_diff = 0.0;
	float neg_depth_diff = .5;
	if (shadows_enabled) {
		float depth = getDepth(SCREEN_UV, DEPTH_TEXTURE, INV_PROJECTION_MATRIX);
		float du = getDepth(SCREEN_UV+vec2(0., -1.)*e, DEPTH_TEXTURE, INV_PROJECTION_MATRIX);
		float dr = getDepth(SCREEN_UV+vec2(1., 0.)*e, DEPTH_TEXTURE, INV_PROJECTION_MATRIX);
		float dd = getDepth(SCREEN_UV+vec2(0., 1.)*e, DEPTH_TEXTURE, INV_PROJECTION_MATRIX);
		float dl = getDepth(SCREEN_UV+vec2(-1., 0.)*e, DEPTH_TEXTURE, INV_PROJECTION_MATRIX);
		depth_diff += clamp(du - depth, 0., 1.);
		depth_diff += clamp(dd - depth, 0., 1.);
		depth_diff += clamp(dr - depth, 0., 1.);
		depth_diff += clamp(dl - depth, 0., 1.);
		neg_depth_diff += depth - du;
		neg_depth_diff += depth - dd;
		neg_depth_diff += depth - dr;
		neg_depth_diff += depth - dl;
		neg_depth_diff = clamp(neg_depth_diff, 0., 1.);
		neg_depth_diff = clamp(smoothstep(0.5, 0.5, neg_depth_diff)*10., 0., 1.);
		depth_diff = smoothstep(0.2, 0.3, depth_diff);
//		ALBEDO = vec3(neg_depth_diff);
	}

	vec3 original_color = texture(SCREEN_TEXTURE, SCREEN_UV).rgb;
	vec3 final_shadow_color = mix(original_color, shadow_color, shadow_strength);
	vec3 final = original_color;

	if (shadows_enabled) {
		final = mix(final, final_shadow_color, depth_diff);
	}
	ALBEDO = final;

	float alpha_mask = depth_diff * float(shadows_enabled);
	ALPHA = clamp((alpha_mask) * 5., 0., 1.);
}

And it works great! Except, I’m trying to modify my custom grass shader to improve visuals, and it’s conflicting with the above code. Specifically, I’m trying to enable depth on my grass blades, instead of making them be considered transparent, both for performance and aesthetic reasons. I’m doing this by using ALPHA_SCISSOR_THRESHOLD = 0.5; on a render_mode depth_draw_opaque; enabled shader (I’ve tried all other depth related render modes and none did what I needed). But, as expected, now the edge shader detects the grass depth and draws an outline on it.

Before:

After:

I wanted to keep the grass with the depth effect, since it solves some weird visual quirks it had for being transparent.

Before, with sorting issues due to transparency:

After, with proper sorting between blades (all post processing effects disabled):

I’ve tried rendering objects that should receive the outline in a different Viewport and use it as a mask to enable/disable where to apply the post-processing, but it had it’s own caveats I couldn’t solve. I have no idea how to properly do this. Maybe this is an instance of using a per-object next-pass shader? Or is there a way of excluding select objects from the edge detection effect?

You may have to go per-object material, even writing to the stencil buffer will put your grass on the alpha pipeline with the same draw ordering issues.

Maybe there is a better way for you to do your grass? Multi mesh instead of a single mesh? Noise texture?

I’m actually using Multimesh for my grass, I spawn it procedurally over a plane mesh.

I actually got a very hacky solution. Since a per-object solution wouldn’t work on my project (due to stylistic reasons; a post-processing filter looks waaaaaaaaaay better for what I’m intending) I stuck with trying to use a second Viewport as a mask for what I wanted to outline.

The way I did that was to create a second Viewport and a second camera (that is identical to the player’s main one) and set it’s cull mask only to the wanted objects in the scene (basically everything but ground and grass). Then, I duplicated the ground, lowered it a tiny bit, and set it to a pure unshaded white color.

In my outline shader, I receive this new Viewport’s texture and filter only the objects by comparing the color of the pixels and its distance to pure white (vec3(1.0f)). Finally, I only allow the outline to be drawn for pixels above a threshold (currently a distance of 0.135 from pure white). Lo and behold:

Grass blades behaves perfectly as an opaque object; outline is perfectly matched to above ground allowed objects. The only downside is having a second Viewport and camera (in my case third, since I use another viewport for UI elements and another camera for animation Frustum Culling), which do have a non negligible performance impact. Yet, I had previously done some testing on my Intel Iris Xe powered laptop, and it was not bad enough for me to want scrap this method. I’ll still do some more testing to see whether it’s actually worth it and if there is any way to improve performance and regain some frames. I know that simply making the grass blades opaque did help a bit (but at most +4% on a CPU bottlenecked pc like my i5-10400f + 2080 Ti), probably because of very nasty overdraw.

I’ll mark this as resolved, but add to this if I find any better solution or optimization.

Some more tangible information about performance I just gathered with a quick benchmark:

i5 10400f + NVIDIA 2080Ti + 16GB RAM (Forward+ on Linux):

Before Modifications → After Modifications:

FPS
avg: 630 → 611 (-3%) [also, the new one was way more unstable]
max: 680 → 667 (-2%)
min: 580 → 555 (-4%)

Draw Calls: 114 → 213 (+87%)

Video Memory: 81.04MiB → 110.08MiB (+36%)

Also interesting to note that, the average time the GPU took to render all viewports was obviously similar (at most 3ms for both cases). But, the time the CPU took each frame was significantly higher, from 0.29ms to 0.51ms (+76%). It’s still ridiculously fast, but considerably less so, and that’s without any heavy CPU logic, or more advanced and demanding GPU workloads like particles or more than one Omnilight.