Depth and Hidden Surfaces
In a 3D scene, nearer things hide farther ones. The space has to decide, cell by cell, which surface wins. It uses two tools: a depth buffer for surfaces, and layers for characters.
The idea
Depth is a point's distance from the camera. Smaller is nearer.
A depth buffer keeps, for every cell, the nearest surface found there so far. Each face is filled cell by cell, and a cell only takes the new face if it's nearer than what the cell already holds. Draw every shape into the same buffer and overlaps sort themselves out, even where two shapes pass through each other.

Back-face culling skips work before the buffer sees it. Each face of a closed shape has an outward normal, an arrow pointing straight out of it. A face whose normal points away from the camera is on the far side of the shape, hidden behind the near side, so it can be skipped without testing a single cell.

Mounted characters aren't drawn into the buffer. Each one is a single character on one cell, so the space orders them
with layers instead: with layer_by_depth on, a nearer character gets a higher layer and draws on top.
In TTE
The Depth example passes a sphere through a cube, or shows a cube's far side through its text. Save it as
depth.py:
"""Depth: a sphere passes through a cube, or a cube shows its far side through its text.
An Effect Guide example for the Depth and Hidden Surfaces chapter.
"""
from __future__ import annotations
from dataclasses import dataclass
from terminaltexteffects import easing
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.engine.space3d import Cube, Shaded, Space3D, Sphere, Textured
from terminaltexteffects.utils import argutils
def get_effect_resources() -> tuple[str, type[BaseEffect], type[BaseConfig]]:
"""Return the command name, effect class, and configuration class TTE registers."""
return "depth", Depth, DepthConfig
@dataclass
class DepthConfig(BaseConfig):
"""Configuration for the Depth effect."""
parser_spec = argutils.ParserSpec(
name="depth",
help="A sphere passes through a cube, or a cube shows its far side through its text.",
description="depth | A sphere passes through a cube, or a cube shows its far side through its text.",
epilog="Example: tte --canvas-width 80 --canvas-height 24 --anchor-text c depth --see-through",
)
see_through: bool = argutils.ArgSpec(
name="--see-through",
default=False,
action="store_true",
help="Draw one textured cube whose far side shows through the spaces in its text.",
) # pyright: ignore[reportAssignmentType]
"bool : Draw one textured cube whose far side shows through the spaces in its text."
class DepthIterator(BaseEffectIterator[DepthConfig]):
"""Produce the frames of the Depth effect."""
def __init__(self, effect: Depth) -> None:
"""Set up the bodies and queue their motion."""
super().__init__(effect)
self.space = Space3D(self.terminal)
if self.config.see_through:
cube = self.space.body(Cube(24)).draw(Textured("TTE 3D ", back="dim"))
cube.rotate("x", 20)
cube.spin("y", turns=1, frames=360, ease=easing.in_out_sine)
else:
cube = self.space.body(Cube(20)).draw(Shaded())
cube.set_rotation().rotate("y", 30).rotate("x", 20)
# The sphere starts on the left, slightly in front, and passes through the cube to the right.
sphere = self.space.body(Sphere(9), at=(-34, 0, 6)).draw(Shaded())
sphere.move_to(34, 0, -6, frames=300, ease=easing.in_out_sine)
cube.spin("y", turns=0.5, frames=300, ease=easing.in_out_sine)
self.text_shown = False
def __next__(self) -> str:
"""Play the motion, then swap the bodies for the text."""
if self.space.animating:
self.space.step()
elif not self.text_shown:
for body in self.space.bodies:
body.erase()
self.space.step() # Hides the surface characters once nothing is drawn.
for character in self.terminal.get_characters():
self.terminal.set_character_visibility(character, is_visible=True)
self.text_shown = True
else:
raise StopIteration
self.update()
return self.frame
class Depth(BaseEffect[DepthConfig]):
"""A sphere passes through a cube, or a cube shows its far side through its text."""
@property
def _config_cls(self) -> type[DepthConfig]:
return DepthConfig
@property
def _iterator_cls(self) -> type[DepthIterator]:
return DepthIterator
- Every drawn body in a space shares one depth buffer, so bodies hide each other correctly with no extra work.
Textured(back="dim")draws a second pass of the faces turned away, inback_color, and lets them show through the spaces in the text. It's the one place the space draws back faces on purpose.
The Card example mounts the input text on a plane and turns it once, which shows what happens to characters as they
turn edge-on. Save it as card.py:
"""Card: the input text turns like a card, showing how mounted characters thin out edge-on.
An Effect Guide example for the Depth and Hidden Surfaces chapter.
"""
from __future__ import annotations
from dataclasses import dataclass
from terminaltexteffects import easing
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.engine.space3d import Space3D
from terminaltexteffects.utils import argutils
def get_effect_resources() -> tuple[str, type[BaseEffect], type[BaseConfig]]:
"""Return the command name, effect class, and configuration class TTE registers."""
return "card", Card, CardConfig
@dataclass
class CardConfig(BaseConfig):
"""Configuration for the Card effect."""
parser_spec = argutils.ParserSpec(
name="card",
help="The input text turns like a card.",
description="card | The input text turns like a card.",
epilog="Example: tte --canvas-width 80 --canvas-height 24 --anchor-text c card --hide-back",
)
hide_back: bool = argutils.ArgSpec(
name="--hide-back",
default=False,
action="store_true",
help="Hide characters while the back of the card faces the viewer.",
) # pyright: ignore[reportAssignmentType]
"bool : Hide characters while the back of the card faces the viewer."
class CardIterator(BaseEffectIterator[CardConfig]):
"""Produce the frames of the Card effect."""
def __init__(self, effect: Card) -> None:
"""Mount the text on a plane and queue one slow turn."""
super().__init__(effect)
self.space = Space3D(self.terminal)
self.card = self.space.body()
characters = [c for c in self.terminal.get_characters() if c.input_symbol != " "]
self.card.add_chars(
characters,
self.card.text_plane(characters),
hide_when_facing_away=self.config.hide_back,
)
for character in characters:
self.terminal.set_character_visibility(character, is_visible=True)
self.active_characters.add(character)
self.card.hold(20).spin("y", turns=1, frames=360, ease=easing.in_out_sine).hold(20)
self.card.on_complete(lambda card: card.release_all())
def __next__(self) -> str:
"""Turn the card until it is back at rest."""
if self.space.animating or self.active_characters:
self.space.step()
self.update()
return self.frame
raise StopIteration
class Card(BaseEffect[CardConfig]):
"""The input text turns like a card."""
@property
def _config_cls(self) -> type[CardConfig]:
return CardConfig
@property
def _iterator_cls(self) -> type[CardIterator]:
return CardIterator
As the card turns edge-on, its columns crowd into fewer cells and only one character can show in each. Past edge-on
you see the back of the card, so the text reads mirrored. hide_when_facing_away=True hides each character while its
side faces away, as a solid card would.
Hidden wireframe lines
By default, Wireframe calls an edge front-facing when an adjacent face points toward the
camera. This is a fast approximation for convex solids; concave and custom meshes can have
front-facing edges behind another part of the mesh. Use
Wireframe(depth_test=True, hidden_color=None) for hidden-line removal against the same mesh's
filled depth buffer. A hidden color instead draws those occluded lines in that color.
The low-level geometry3d.wireframe accepts the same depth_test=True option. It adds a fill
pass within the viewport and tests depth at terminal cell centers, so cell rounding can affect silhouettes.
Under the hood
geometry3d.rasterize fills a mesh into a buffer and returns it:
from terminaltexteffects.utils import geometry3d as g3
buffer: dict = {}
g3.rasterize(cube_mesh, camera, rotation=turn, buffer=buffer)
g3.rasterize(sphere_mesh, camera, position=g3.Vec3(10, 0, 0), buffer=buffer)
for cell, fragment in buffer.items():
... # fragment.depth, fragment.normal, fragment.facing, fragment.tag, fragment.uv
cull="back", the default, skips faces turned away."front"keeps only those, and"none"keeps both.- Each
Fragmentholds the nearest surface's depth, normal, face tag and texture coordinates.
Characters are ordered by layer, then by creation order. A mount with layer_by_depth sets the character's layer to
its layer when mounted plus how much nearer than the origin it is, in whole columns, and restores it on release. The
rest of a column orders characters within the layer, so the nearer of two characters always draws on top, however
small the difference. Surface cells are ordered the same way and count a hair farther than they are, so text mounted
on a drawn body from layer 0 or above reads on top.
At cell resolution
- Text edge-on loses characters: a plane turned 60 degrees shows only half of its columns. Keep text near facing the viewer when it must be read, and move through edge-on quickly.
- Where two characters land in the same cell, the higher layer wins. With
layer_by_depthoff, the last-created character wins instead, which looks random on a turning body. - Only cell centers are tested, so a part of a shape thinner than a cell, such as a narrow rim seen edge-on, can fall between centers and vanish for a frame.
Mounted anchors at the near plane
mount.projected reports whether the anchor can be projected at its current pose. While the
anchor is closer than camera.near (including behind the camera), its character is automatically
hidden on its tick. The mount retains its last valid coordinate, depth, facing, and light readings;
check projected before using them. It restores visibility when the anchor returns, unless
backface hiding still applies. A character already hidden by the effect stays hidden. Releasing
a mount restores visibility only if the mount hid it.



