Camera and Projection
The camera decides how 3D points become cells. Its one setting you'll change most is how strong the perspective is: how much nearer things grow and farther things shrink.
The idea
The camera sits on the +z axis, looking back at the origin. To draw a point, it projects it: it finds where the line from the point to the camera crosses the screen.
- With perspective, things farther away look smaller, and parallel lines running away from you converge. A point
at depth
dfrom the camera is scaled bydistance / d, so the z = 0 plane keeps its size, nearer points grow and farther ones shrink. - An orthographic view drops the scaling. Everything keeps its size whatever its depth, like a technical drawing. It's flat, but it's also stable: nothing swells as it turns toward you.

Perspective strength comes from the camera's distance. A camera close to the scene makes small changes in depth look large. A distant camera barely changes sizes at all.

In TTE
The Lens example places three cubes, each 26 columns to the right of and 60 columns farther away than the one
before. Save it as lens.py:
"""Lens: three cubes running away from the viewer, seen with a chosen perspective.
An Effect Guide example for the Camera and Projection 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
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 "lens", Lens, LensConfig
@dataclass
class LensConfig(BaseConfig):
"""Configuration for the Lens effect."""
parser_spec = argutils.ParserSpec(
name="lens",
help="Three cubes running away from the viewer.",
description="lens | Three cubes running away from the viewer.",
epilog="Example: tte --canvas-width 80 --canvas-height 24 --anchor-text c lens --perspective 2",
)
perspective: float = argutils.ArgSpec(
name="--perspective",
type=argutils.NonNegativeFloat.type_parser,
default=1.0,
metavar=argutils.NonNegativeFloat.METAVAR,
help="1 is natural, larger values exaggerate depth, and 0 is a flat orthographic view.",
) # pyright: ignore[reportAssignmentType]
"float : 1 is natural, larger values exaggerate depth, and 0 is a flat orthographic view."
class LensIterator(BaseEffectIterator[LensConfig]):
"""Produce the frames of the Lens effect."""
def __init__(self, effect: Lens) -> None:
"""Place three cubes from near and left to far and right, and spin them."""
super().__init__(effect)
self.space = Space3D(self.terminal, perspective=self.config.perspective)
# Each cube is 26 columns right of and 60 columns farther away than the one before it.
for x, z in ((-26, 0), (0, -60), (26, -120)):
cube = self.space.body(Cube(14), at=(x, 0, z)).draw(Shaded())
cube.rotate("x", 20)
cube.spin("y", turns=1, frames=240, ease=easing.in_out_sine)
self.text_shown = False
def __next__(self) -> str:
"""Spin the cubes, then swap them 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 Lens(BaseEffect[LensConfig]):
"""Three cubes running away from the viewer."""
@property
def _config_cls(self) -> type[LensConfig]:
return LensConfig
@property
def _iterator_cls(self) -> type[LensIterator]:
return LensIterator
Space3D(terminal, perspective=...) sets the strength:
1, the default, is a natural perspective. The camera is 120 columns from the origin.- Larger values move the camera closer.
2puts it 60 columns away, which exaggerates depth. 0is orthographic.
Under the hood
geometry3d.Camera does the projection. Space3D makes one centered on the
canvas, and you can read it as space.camera:
projected = self.space.camera.project(point) # None if the point is behind the camera.
if projected is not None:
cell = projected.coord
depth = projected.depth # Distance from the camera. Smaller is nearer.
distanceis how far the camera is from the origin, andfocal(the same asdistanceunless you set it) controls the scaling.nearclips points closer than that to the camera. Crossing faces and edges keep their visible portions; texture coordinates are interpolated at the clipped corners.
- orthographic=True turns the scaling off.
- view_direction(point) is the direction from a point toward the camera, which decides which faces turn toward it.
At cell resolution
Each projected point is drawn in the cell it falls in, so a point that moves smoothly in 3D moves on screen in whole-cell steps:

- Strong perspective makes near faces large and far faces tiny. Tiny faces become a few cells of noise, so keep perspective at 1 or below for small shapes.
- Perspective magnifies whatever comes toward you, including text on a surface. Letters on the near side of a large shape can double up (see Texture mapping). Gentler perspective, such as 0.5, reduces it.
- Orthographic views are good for text: a text plane turning in an orthographic view never grows past the size of the input.
- Nothing may come nearer than the camera's
neardistance. Withperspective=2the camera is only 60 columns away, so a body moved to z = 60 disappears.
Coordinates and mutable settings
World z increases toward the viewer. Camera depth is camera.distance - world_z,
so smaller depth is nearer. cell_to_world(coord, z=...) accepts world z; Projected.depth
and Mount.depth report camera depth. Both are measured in columns.
Camera settings are mutable. Construction checks positive distance, focal (when set), near
and row_scale, and requires near < distance. Assignments do not revalidate: preserve those
constraints when changing a field. With focal=None, focal length follows the current distance.


