Character silhouettes
A look’s characterSilhouette draws a character that something opaque hides from the camera as a silhouette: a fill and an outline, where the character is hidden. The wall, the roof or the house draws as it is: nothing fades, cuts away or turns translucent. A character half behind a wall shows its silhouette on its hidden half alone. The diorama look turns it on for the character the player views. Every other built-in look leaves it off.
It suits a camera the player cannot pull in past a roof, as Camera2D’s diorama rig and any fixed or isometric camera are. The follow camera pulls its eye in front of a wall instead, so a third-person game seldom needs it.
Turn it on
Section titled “Turn it on”Give the look a characterSilhouette through an override, such as look={{ base: noon, characterSilhouette: { fillColor: "#f2e6c4" } }}, and the values you leave out come from defaultCharacterSilhouette. Pass characterSilhouette: false over the diorama look to turn it off. A scene changes it as it changes any other look value, as overriding a value says.
The look takes the following values:
| Value | What it does | Diorama | Default |
|---|---|---|---|
| fillColor | The fill’s colour, unlit and unfogged. In the look’s stack it draws before the tone mapping and the grade, which shift it. | #d9ccff, pale lilac |
#3d4a8c |
| fillOpacity | How much the fill covers the wall it shows through, 0 to 1: 1 is solid. | 0.55 | 1 |
| fillDetail | How much of the character’s own light and dark the fill keeps, 0 to 1, so hair, a face and clothes read through the wall. | 1 | 0 |
| outlineColor | The outline’s colour, round the hidden part. | #241820 |
#ffffff |
| outlineOpacity | How much the outline covers what it shows through, 0 to 1. | 1 | 1 |
| outlineWidth | The outline’s width, a whole number of pixels; 0 draws none. See the outline. | 1 | 2 |
| minHiddenShare | The share of the character’s drawn pixels that must be hidden before the silhouette shows, 0 to 1. See the rail. | 0.25 | 0.25 |
| characters | SilhouetteCharacters.ViewTarget, the character the player views, or SilhouetteCharacters.All, every character drawn. |
ViewTarget |
ViewTarget |
At fillDetail 1 the fill’s colour scales by 0.42 plus 0.8 times the square root of the hidden texel’s luma. At 0 the fill is one flat colour.
SilhouetteCharacters.All shows every player’s character and every NPC. In a game where players fight, every page then sees every enemy through walls.
A share or an opacity outside 0 to 1, a width that is not a whole number of 0 or more, or a colour that is not one throws in development. A deployed game draws no silhouette and logs why.
The outline
Section titled “The outline”The outline rings the hidden part of the character, outside the character’s shape. It never draws over the character’s own seen pixels, nor over another character standing in front.
outlineWidth counts the frame’s pixels. In a pixel-perfect Camera2D frame those are the art’s pixels, so 1 matches a sprite’s painted one-pixel line. Elsewhere it counts CSS pixels, scaled by the buffer’s own pixels to the CSS pixel, so it keeps its width on a phone’s dense screen and at a lowered render scale. The outline reads every pixel within its width, so its cost grows with the width’s square, and development warns past 8.
The rail
Section titled “The rail”A thin occluder, such as a bridge rail or a fence across the feet, hides a sliver of the character. A silhouette there draws a bar the player reads as nothing. minHiddenShare holds the silhouette back until that share of the character’s pixels is hidden: at 0.25, a quarter of the character.
The share is counted each frame on the GPU from every pixel the frame draws, before any styling, so the outline’s width never changes it. It must hold at or above minHiddenShare for 0.15 s before the silhouette shows. It must hold below the threshold less 0.05, or half the threshold, whichever is higher, for 0.15 s before it hides, as it does when the character draws no pixel at all. So a walk cycle beside a rail never flickers it.
A character’s silhouette starts hidden. At 0 it shows while any pixel is hidden, and at 1 only while every pixel is.
Show one character’s silhouette
Section titled “Show one character’s silhouette”setCharacterSilhouette(entity, isShown) shows or hides one character’s silhouette on this page, whatever the look’s characters. Call it with true for a teammate or a quest giver, false for a character the player must lose behind a house, and null to return to the look’s choice. It changes what this page draws alone, as setCameraSubject does. Each page decides for its own player, so a team game shows each player their own teammates. The look must show silhouettes for it to draw.
A scene tells apart 32 characters showing a silhouette at once. A 33rd shows none, and development reports it once.
What hides a character
Section titled “What hides a character”Anything that writes depth hides one: a block, a model, a placed object, ground cover, an Entity’s mesh, and glass or a transparent material that writes depth. The test is the depth the scene drew. An occluder must stand more than 0.25 m nearer the camera than the character’s own surface. So the ground under the character’s feet, a wall it leans on, a tuft of grass at its toes or the sword it holds through a door draws no silhouette.
What writes no depth never hides one, such as water and a blended sprite. Another character never hides one either. A character standing in front of a hidden one keeps its own pixels, since it stands in front, whether or not it shows a silhouette itself.
A camera inside a house sees through the house’s walls where they draw one side, as most do, so nothing hides the character. A house drawn double-sided hides it whole, and its whole silhouette draws.
The silhouette draws over everything the frame drew in front of the wall, smoke and particles included, as Roblox’s Highlight does.
How it draws
Section titled “How it draws”Each character’s draw holds a twin of every mesh and every sprite it draws: the same geometry, skeleton, frame and cutout. The twins stand on camera layer 29 alone, so the scene’s own render, its shadows and every capture leave them out. Every character takes twins while the look shows silhouettes, so a character that shows none still covers those behind it.
The look’s post-processing stack draws the silhouette in a pass of its own, after the depth of field and before the bloom and the grade. The pass draws the twins into a mask and reads from it, for each character, whether it is hidden and how much. It then draws the fill and the outline onto the frame. No blur ever reaches them, so the silhouette stays sharp in the tilt-shift band’s blur and in front of a blurred wall alike.
A look that shows silhouettes mounts its stack at every quality level, Minimum included, so the silhouette draws the same everywhere. A look with postProcessing: false leaves its stack to the scene, and a scene’s own <PostProcessing> draws the silhouette after its last child pass, before its tone mapping. With neither, nothing draws it, and development says so.
A twin copies a mesh’s map, its alpha cut, its colour, the side it draws and its clipping, each frame, so a change to the material shows in the silhouette. Cut-out hair keeps its shape, and a blended part cuts at half its alpha. A SkinnedMesh twin shares its skeleton and morphs, and an InstancedMesh twin shares its matrices, its morphs and its count. An avatar’s MToon takes a twin, but its outline does not. A body the camera fades fades its silhouette with it.
A material that writes no depth, as a name over the head or a ring on the ground, draws no silhouette, and a blended or additive sprite takes no twin. A ShaderMaterial of your own places its vertices where three’s shaders cannot follow, so it takes no twin; neither does a BatchedMesh or an InstancedMesh2, which place their parts from their own data. The first one a page meets is reported in development, naming its kind. A part hidden from the camera by its layers alone, rather than by visible, keeps its twin.
What the other engines do
Section titled “What the other engines do”Roblox’s Highlight draws a fill and an outline through walls with its DepthMode set to AlwaysOnTop, one model at a time, with FillColor, FillTransparency, OutlineColor and OutlineTransparency; its outline has no width setting. Unity’s URP draws a fill with Render Objects passes on a character layer, the first at Depth Greater. Unreal marks the character’s custom depth and stencil for a post-process material. Godot 4.5’s BaseMaterial3D has stencil x-ray and outline presets. PlayCanvas renders chosen entities into a texture, grows its edges and composites them. None holds a silhouette back by the share hidden. The engine sets it for every character from the look, with one call for one character.
What it costs
Section titled “What it costs”Each character draws its twins once more into the mask, and a skinned twin skins again. The pass counts the mask once for each character showing a silhouette, in 32 by 32 tiles, keeps each character’s state on the GPU, reads nothing back, and composites over the whole frame while any character shows one. At the Low and Minimum levels the look’s stack mounts for the silhouette alone, with its full-screen buffer.
The frame cost is not profiled yet.
Where it runs
Section titled “Where it runs”It draws on a page under WebGL. Under the engine’s experimental WebGPU renderer it draws none and says so once in the console. A room and a headless world draw no characters, so none.
Its values are in LookCharacterSilhouette.