Invisiblity Reveal Shader

Godot Version

v4.6.3.stable.steam [7d41c59c4]

Question

I’m working on a game where the player can reveal the world via noise (see sample video below).

I’d like to have arbitrary invisible objects only reveal-able via this noise functionality.

Presently, there is a screen space shader that has a reference to every sound-sphere in the game, represented as an SDF.

A few different approaches I’ve investigated / thought-up that do not make me happy are:

1 Fragment Shader per Invisible Object:

Each invisible object gets a reference to all of the SDFs per-frame and does a fragment shader calculation to determine if it should be rendered, this seems not good to me, but maybe is performance negligible.

Render the Invisible Objects to a Separate “Screen Space”?

Is it possible to render my invisible objects to a separate screen? (Subviewport?) and do the same screen-space (ideally in the same pass even) SDF function checking there, modify the colors, and overlay / combine the result?

What does the Stencil Buffer even do :sob: :

Stencil buffer (still not 100% on this guy) but, if an object A is occluded by object B, can I only render that object during that occlusion? But also, is being inside another object an occlusion or only be obscured by it, e.g. does not work in our case.

Source Code

I’ve modified Michael Watt’s shader quite a bit to get even this far, any support is greatly appreciated:

What’s wrong with what you currently have?

It can only reveal the “fog”, it doesn’t actually support this concept of hidden materials, e.g.:

Current Behavior:

If I have a room that is fog-less, every material will be rendered normally.

Desired Behavior:

If I have a room without fog, materials that are not “invisible” are rendered normally, but materials that are invisible would only be rendered if within my sound-sphere.

What’s “fog”?

Fog in this context is that shadow–y occlusion that you see in the video, it’s basically a color being overlaid on-top of the world unless it is inside a sound sphere.

The fog is not explicitly important here, I’m really only caring about, how to handle the invisibility masking stuff.

To support a more specific example:

I have my church as you can see in the video, without the sound you can see the opaque material against the skybox, if this invisibility material was applied, I’d want the castle to dissolve in (granted this would probably have weird issues with seeing the insides).

If that’s a post-processing effect how can you have a room that’s “without fog”?

I’d track another list of SDFs in which the fog should not appear, similar to the sound spheres. To be explicit, these invisible materials should not care about the fog. The question I’m trying to answer is,

How can I only reveal the parts of a mesh / material that are within some shape (defined by an SDF), in this case to create an invisibility effect.

Run the invisibility shader on all invisible geometry.

The question to that becomes, is that acceptable performance overhead?

If I have 10 SDFs that reveal the invisible geometry, and 100 invisible objects,

For now all my SDFs are spheres so I’m referring to the actual pipeline that I am running, but this should generalize if I understand correctly.

Each Frame:

  1. In game there are spheres generated by a manager it maps these to an array of sphere data (radius, position).
  2. Each invisible object:
    1. Asks the game manager: “please give me that sphere data”.
    2. For each sphere:
      1. Does a calculation to see if the vertex is in the sphere.
      2. Adds to mask if it is within the sphere.
    3. Uses that as an alpha mask.

Send sphere data to the shader and let the shader handle everything.
The number of objects is irrelevant then.

I noticed your shader is named “ubershader.gdshader” if you are applying this same shader material to every object then step 1. distributing the data is not per-object but per-material, if you do limit one material for all your objects then it’s a very low cost, otherwise it’s still not that much data. Some ocean simulations send huge textures to the GPU every frame, whereas a couple of spheres are 10 vec4s?

Step 2. is pretty standard shader behavior, per-vertex is going to much more efficient than fragment operations and it seems like you are aiming for low-poly so even less vertices to worry.

Step 3. alpha is always scary, maybe you can separate this noise-ring shader and use it as a material overlay instead of the base material/override; this still uses alpha, even more so, but the base materials remain opaque, this also may help you keep one material for your noise-ring as mentioned in step 1.

All in all this is very acceptable for performance

I think I vastly underestimate the power of parallel processing:

Material overlay is probably a good idea the alpha stuff seems pretty finicky.

Thank you!

// invisibility reveal
shader_type spatial;
render_mode depth_prepass_alpha;

varying vec3 world_vertex;

uniform int sphere_data_length;
uniform vec4 sphere_data[64];
uniform vec4 sphere_attributes_data[64];

void vertex() {
	world_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
}

void fragment() {
	float alpha = 0.;
	
	for (int idx = 0; idx < sphere_data_length; idx++) {
		vec4 sphere_datum = sphere_data[idx];
		vec4 sphere_attribute_datum = sphere_attributes_data[idx];
		
		float dist = length(sphere_datum.xyz - world_vertex);
		
		alpha += dist <= sphere_datum.w ? 1. : 0.;
	}
	
	alpha = clamp(alpha, 0., 1.);
	
	ALBEDO = vec3(1., 1., 1.);
	ALPHA = alpha;
}

It is easy to underestimate! GPUs will spin thousands of threads, even 10 year old gpus will have at least 500 ready to go.

This reduced script is very simple too, no branches (if the ternary is compiled down to a step function) and the hardest function to crunch is up to 64 lengths.

Alpha is still scary so be sure to profile, I’d watch out for the alpha sorting step since this effect is applied to many if not all meshes. If some part of alpha becomes too taxing you may be able to discard instead of assigning ALPHA, this doesn’t help much with GPU performance but gets the material out of the alpha pipeline so it may fix visual issues and again dodge the sorting cost.

void fragment() {
	float alpha = 0.;
	
	for (int idx = 0; idx < sphere_data_length; idx++) {
		vec4 sphere_datum = sphere_data[idx];
		vec4 sphere_attribute_datum = sphere_attributes_data[idx];
		
		float dist = length(sphere_datum.xyz - world_vertex);
		
		alpha += dist <= sphere_datum.w ? 1. : 0.;
	}

	// sampling some noise instead of 0.5 may look better
	if (alpha < 0.5) {
		discard;
	}
	
	ALBEDO = vec3(1., 1., 1.);
}

you da bomb :face_holding_back_tears: