X-ray silhouettes
Open the live demo · Read the source · View on GitHub
In a game you sometimes want to see through a wall: an enemy behind a door, a teammate behind a crate. An x-ray silhouette paints the outline of what is hidden, in a flat colour, over whatever hides it. The parts you can already see keep their normal shading.
Step 1: Choose who gets a silhouette #
Every node has a layerMask, a set of bits. The x-ray setting names some bits,
and any node that shares one of them is treated as watched. The default mask is
1, so this page uses a different bit and keeps the default as well.
/// One bit of `layerMask`, picked so it does not collide with the default.
static const int _watched = 1 << 2;
Step 2: Hide something #
The monster stands two metres behind the wall, and its layerMask carries the
watched bit. Nothing else about it is special: it is an ordinary lit capsule.
Without x-ray the wall simply hides it.
final MeshNode monster = MeshNode(
DeviceMesh.upload(
device,
const CapsuleShape(radius: 0.4, height: 0.9).build(),
),
Material(
name: 'monster',
baseColor: Vector4(0.3, 0.7, 0.35, 1.0),
roughness: 0.5,
),
name: 'monster',
)..setPosition(0.0, 0.85, -2.0);
monster.layerMask = 1 | _watched;
Step 3: Turn it on #
XraySettings takes the mask and, if you like, a colour. The colour is in linear
light, before exposure. A mask of 0 means off, so the switch beside the picture
only changes that number. Turn X-ray on and an orange capsule shape appears on the
face of the wall. Drag the view sideways until the monster steps out from behind
the wall: the part in the open is lit as usual and only the hidden part is
painted.
xray: XraySettings(layerMask: enabled ? _watched : 0, color: _colour),
Only opaque things hide anything. A monster behind glass can be seen through the glass, so it gets no silhouette.
Step 4: See what it cost #
Each watched node is drawn twice more at the end of the scene pass. The first draw marks the stencil where the node is visible. The second paints the flat colour where the depth test says something is in front and the stencil says the node is not showing. The stencil is what keeps the visible half of a half-hidden monster lit.
Because it needs a stencil buffer, a device without one draws no silhouettes and the frame is the frame you would have had without the setting. The page checks the number of draws in the scene pass for both cases.
final int drawn = frame.passes
.firstWhere((FramePass p) => p.name == 'scene')
.drawCalls;
final int extra = enabled && _stencil ? 2 : 0;
if (drawn != _meshCount + extra) {
throw StateError(
'the scene pass made $drawn draws; $_meshCount meshes and '
'$extra x-ray draws were expected',
);
}