The theory to masking in 3D is similar to 2D, though instead of compositing images together, we’re going to add one image onto another where appropriate.
You do not need a viewport for the main view. We’re not mixing images but instead adding one on top of another.
But you do need a viewport for the mask and one for the x-ray view.
One crucial difference between 2D and 3D Viewports in Godot is that all Viewports in 3D need their own camera.
In 2D, everything is flat, at the same Z-depth, and the coordinates (0, 0) correspond to the screen’s top left. In 3D, cameras hold essential information like the field of view, how close and far to the camera objects can be, and what entities should even draw at all. That’s why you need to create a camera for each 3D viewport.
You can see this limitation by creating a new 3D scene and running the game: the scene will only show the background color until you add a camera to the tree! That is because at the root of the scene tree is a Viewport, and it cannot display anything without a camera.
To learn more about viewports, read the viewport guide in the official documentation.
We need two components for each extra view above the first one: a Viewport
to render in and a subject to render with.
For the Viewport, it’s a case of creating a ViewportContainer
with Stretch set to true with a child Viewport. This viewport is for a 3D purpose in a 3D scene, so check Keep 3d Linear in the Viewport’s properties.
You must also set the Usage to either 3D or 3D No-Effects. Both are valid, but 3D No-Effects will not draw anything for post-processing, which is fine in our case. We are doing custom post-processing, so we can use that drawing to save some video memory.
Also, check Transparent Bg so that anywhere that isn’t drawn won’t show up as black on screen.
Remember: add a camera to this viewport! Otherwise, the viewport won’t render anything, resulting in a black image.
This camera’s Cull Mask should have layer 1 and 2 removed. Layer 1 is the main scene, and layer 2 is going to be our mask. The choice is arbitrary, but whatever choice you make here must reflect in the XRay and Mask objects.
For the subject, create the normal view like any regular Godot scene: Add a ModelInstance
s or a collection of them and assign your models and materials.
Where it gets more interesting is adding the X-ray version of the subject. We need a way to draw it twice on two separate 3D layers, and the only way to do that is by adding a separate mesh instance.
Duplicate the model and make it a child of the original to share its position, rotation, and scale.
Under VisualInstance’s Layers, to match the XRay camera, remove layer 1 and enable layer 3.
What the XRay version of the model resembles is up to you. It could be wireframe, outline, black and white, weird magical aura effect, or normal model tinted green. You don’t even have to use the same model. In the example above, I went with an inner-glow effect by using a fresnel effect.
shader_type spatial; //Blacks should be fully black, so no ambient light render_mode ambient_light_disabled; //Variables to control the color, intensity, and sharpness of the inner glow effect uniform float xray_color_intensity = 1.0; uniform float outline_sharpness = 2.0; uniform vec4 outline_color : hint_color = vec4(1.0); void fragment() { //Calculate a fresnel effect: the dot product between the surface normal and the camera normal. //Wherever it is facing, the camera should be close to 1, and wherever it is facing 90 degrees from //the camera should be close to 0. We invert it by subtracting that from 1, and then sharpen it with // a multiplication. That makes the edges glow. float fresnel_dot = 1.0 - dot(NORMAL, VIEW) * outline_sharpness; //The albedo is pure, *transparent* black. ALBEDO = vec3(0.0); //Smoothly clamp the dot between a 0..1 range, and multiply by the x-ray color for the final glowing look. EMISSION = smoothstep(0, 1, fresnel_dot) * outline_color.rgb * xray_color_intensity; }
Like in 2D, the mask is a black and white image with the data of what we want or do not want to draw. If you want to draw the subject at all times (detective vision) and not just through walls, omit the mask entirely and move on to the next step.
But in this case, we want to draw the subject through walls. We thus need to know when the subject is behind a wall. We can do that by drawing the subject and the obstacles. Wherever the subject is white in the mask, then the XRay should not draw, and whenever it is black, it should draw.
Create a new ViewportContainer
controlled Viewport
with the same settings as the XRay, but this time, the Camera
should have its Cull Mask turn off layer 1 and 3.
Duplicate the subject as a child of the original and assign it a SpatialMaterial with Unshaded checked and pure white color. Please make sure the Mask duplicate has its Layers property set to Layer 2.
Duplicate your walls in much the same way, but assign them Unshaded SpatialMaterial
s that are pure black and place them on Layer 2.
To control the cameras in the Viewport
s, you can use RemoteTransform
nodes. Add them as a child of the main camera and set the Remote Path property to point to the cameras under their respective viewport. The script rotates the camera in a circle around the subject for the demo but you can do without.
We now have everything we need to apply the XRay image and combine it onto the main view selectively using the mask.
Add a new TextureRect
that takes up the whole screen either by setting its Right and Bottom anchors to 1, and set it to Expand. OpenGL renders viewports upside down, so you will need to also tick on Flip V. Use the XRay view as its texture, and apply a new CanvasItem
shader.
The shader’s job is to sample from the stencil mask and the XRay view (its texture) and change the texture’s transparency based on white pixels. If there are, the alpha should be 0. If there are none, set it to the XRay’s alpha.
shader_type canvas_item; //blend_add ensures that the values from the XRay are added together with the main view's //pixels to give a nice, vivid glow. The default is multiply, which won't look right. render_mode blend_add; //Our mask's viewport texture uniform sampler2D stencil_view : hint_black; void fragment() { //Assign the XRay view as the main color. You could stop here if you wanted the effect //up at all times. The blend_add would give your subjects a healthy inner glow at all times. COLOR.rgb = texture(TEXTURE, UV).rgb; //Sample from the mask float mask = texture(stencil_view, SCREEN_UV).r; //Take the max between 0 and 1.0 - mask. Wherever the mask is white (~1.0), the result will be //near or under 0. Wherever it is black (~0), the result will be near 1. That way, the XRay //effect shows up only behind walls. COLOR.a = max(0.0, 1.0 - mask); }