Geometry 3D
Module: terminaltexteffects.utils.geometry3d
New in 0.16.0
geometry3d is available on development main and is not in the published 0.15.0 package.
Low-level building blocks for placing and drawing things in 3D on a terminal canvas: vectors, rotations, a camera, meshes, rasterization into a per-cell depth buffer, wireframe edges and Lambert shading.
World space is measured in terminal columns. The origin is the center of the canvas, +x points right, +y points
up, and +z points toward the viewer. A Camera projects world points onto canvas cells and corrects for cells
being about twice as tall as they are wide, so a cube looks like a cube. With the default settings, a cell lifted
onto the z=0 plane with Camera.cell_to_world() projects back onto the same cell.
import math
from terminaltexteffects.utils import geometry3d as g3
camera = g3.Camera(center_column=40.5, center_row=12.5)
cube = g3.box(20, 20, 20)
turn = g3.Rotation.about_y(math.radians(30)) * g3.Rotation.about_x(math.radians(20))
light = g3.Vec3(-0.5, 0.6, 1.0)
for cell, fragment in g3.rasterize(cube, camera, rotation=turn).items():
brightness = g3.lambert(fragment.normal, light) # 0 in shadow, 1 facing the light
Angles are in radians. Rotations compose with *: second * first applies first, then second.
rasterize and wireframe take bounds=g3.Bounds.for_canvas(canvas) to fill only cells on the canvas. Without
bounds they fill every cell a shape covers, which can be a great many for a shape close to the camera.
3D geometry primitives for placing and drawing things in 3D space on a terminal canvas.
World space is measured in terminal columns. The origin is the center of the canvas, +x points right, +y points
up, and +z points toward the viewer. Terminal cells are about twice as tall as they are wide, so one row spans
TERMINAL_ROW_SCALE world units vertically; Camera corrects for this when projecting, so a cube looks like a
cube rather than a tall box.
These are the low-level building blocks: vectors, rotations, a camera, meshes, rasterization into a per-cell depth buffer, wireframe edges and Lambert shading. Angles are in radians. Everything is pure Python.
Classes:
| Name | Description |
|---|---|
Vec3 |
An immutable 3D vector. |
Rotation |
A rotation stored as a unit quaternion. |
Projected |
A point projected onto the canvas, with its depth. |
Bounds |
A rectangle of canvas cells that drawing is limited to. |
Camera |
Projects world-space points onto canvas cells. |
Face |
One polygonal face of a mesh. |
Mesh |
Vertices and faces describing a solid. |
Fragment |
The nearest surface found in one cell by |
EdgeCell |
One cell of a wireframe edge found by |
Functions:
| Name | Description |
|---|---|
transform_points |
Rotates, scales and moves many points at once. |
box, prism, pyramid, tetrahedron, octahedron, icosahedron, cylinder, uv_sphere, torus |
Build meshes. |
rasterize |
Fills a mesh's visible faces into a per-cell depth buffer. |
wireframe |
Finds the cells along a mesh's edges. |
line_symbol |
Picks the symbol that best follows a projected line. |
lambert |
Returns how directly a surface faces a light. |
Matrix3 = tuple[float, float, float, float, float, float, float, float, float]
module-attribute
A 3x3 matrix in row-major order.
ORIGIN = Vec3(0.0, 0.0, 0.0)
module-attribute
The world origin, at the center of the canvas.
X_AXIS = Vec3(1.0, 0.0, 0.0)
module-attribute
The unit vector pointing right.
Y_AXIS = Vec3(0.0, 1.0, 0.0)
module-attribute
The unit vector pointing up.
Z_AXIS = Vec3(0.0, 0.0, 1.0)
module-attribute
The unit vector pointing toward the viewer.
Bounds
Bases: NamedTuple
A rectangle of canvas cells, inclusive on every side, that rasterize and wireframe are limited to.
Attributes:
| Name | Type | Description |
|---|---|---|
left |
int
|
First column. |
right |
int
|
Last column. |
bottom |
int
|
First row. |
top |
int
|
Last row. |
Source code in terminaltexteffects/utils/geometry3d.py
contains(coord)
Camera
dataclass
Projects world-space points onto canvas cells.
The camera sits on the +z axis, distance columns from the origin, looking toward it. Perspective projection
scales points by focal / depth; with the default focal equal to distance, points on the z=0 plane keep
their size, so a character lifted onto that plane with cell_to_world projects back onto its own cell.
Orthographic projection ignores depth for size, which flattens the view. World z increases toward the
viewer; camera depth is distance - z and decreases toward the viewer in both projections.
Settings are mutable and validated only on construction. When changing fields, keep the same positive-value
constraints and near < distance; assignments do not revalidate. focal=None follows the current distance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
center_column
|
float
|
Canvas column under the world origin. |
required |
center_row
|
float
|
Canvas row under the world origin. |
required |
distance
|
float
|
Distance from the camera to the origin, in columns. Smaller values exaggerate perspective. |
120.0
|
focal
|
float | None
|
Focal length; |
None
|
orthographic
|
bool
|
Project without perspective. |
False
|
near
|
float
|
Minimum camera depth, in columns. Points with |
1.0
|
row_scale
|
float
|
World units per row, correcting for tall cells. |
TERMINAL_ROW_SCALE
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in terminaltexteffects/utils/geometry3d.py
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 | |
__post_init__()
Validate the camera settings.
Source code in terminaltexteffects/utils/geometry3d.py
cell_to_world(coord, z=0.0)
Return the world point at world z z that a camera at rest shows in coord.
This is the exact inverse of project for both projections and any focal length, which is how characters
are lifted from their canvas cells into 3D. z is a world coordinate, not the camera depth distance - z.
Source code in terminaltexteffects/utils/geometry3d.py
for_canvas(canvas, **settings)
classmethod
Return a camera centered on canvas. settings are passed to Camera.
Source code in terminaltexteffects/utils/geometry3d.py
project(point)
Project a world point, or return None when its camera depth distance - point.z is below near.
Source code in terminaltexteffects/utils/geometry3d.py
project_points(points)
view_direction(point)
Return the unit vector from point toward the camera.
EdgeCell
Bases: NamedTuple
One cell of a wireframe edge found by wireframe.
Attributes:
| Name | Type | Description |
|---|---|---|
depth |
float
|
Distance from the camera. Smaller is nearer. |
front |
bool
|
Whether the edge borders a face turned toward the camera and, with |
symbol |
str
|
A line symbol following the edge's on-screen slope. |
Source code in terminaltexteffects/utils/geometry3d.py
Face
dataclass
One flat, convex polygonal face of a mesh.
Vertices are listed counterclockwise as seen from outside the solid, so the face's normal points outward.
Attributes:
| Name | Type | Description |
|---|---|---|
indices |
tuple[int, ...]
|
Indexes into the mesh's vertices, at least three. |
uvs |
tuple[tuple[float, float], ...] | None
|
Texture coordinates for each vertex, from 0 to 1, with v
increasing upward. |
tag |
int
|
A label the mesh constructor gives the face, such as the side number of a prism. |
Source code in terminaltexteffects/utils/geometry3d.py
__post_init__()
Validate the face.
Source code in terminaltexteffects/utils/geometry3d.py
Fragment
Bases: NamedTuple
The nearest surface rasterize found in one canvas cell.
Attributes:
| Name | Type | Description |
|---|---|---|
depth |
float
|
Distance from the camera. Smaller is nearer. |
normal |
Vec3
|
The face's world-space unit normal, for shading. |
facing |
bool
|
Whether the face points toward the camera. |
face |
int
|
Index of the face in the mesh. |
tag |
int
|
The face's tag. |
uv |
tuple[float, float] | None
|
Texture coordinates at the cell, or |
Source code in terminaltexteffects/utils/geometry3d.py
Mesh
dataclass
Vertices and faces describing a solid, centered on its own origin.
Built-in mesh factories expect positive dimensions. Dimensions are intentionally unchecked: zero can collapse faces, and negative values can reverse winding and normals. Their shading and texture behavior is not guaranteed. Custom faces should be flat, convex and consistently wound outward.
Attributes:
| Name | Type | Description |
|---|---|---|
vertices |
tuple[Vec3, ...]
|
Corner positions in the mesh's own space. |
faces |
tuple[Face, ...]
|
Faces built from those vertices. |
Source code in terminaltexteffects/utils/geometry3d.py
normals
property
Each face's outward unit normal, in the same order as faces.
__post_init__()
Validate the faces and compute their outward normals.
Source code in terminaltexteffects/utils/geometry3d.py
edges()
scaled(x, y=None, z=None)
Return a copy stretched by x, y and z along each axis. y and z default to x.
Source code in terminaltexteffects/utils/geometry3d.py
Projected
Bases: NamedTuple
A world-space point projected onto the canvas.
Attributes:
| Name | Type | Description |
|---|---|---|
column |
float
|
Canvas column, before rounding to a cell. |
row |
float
|
Canvas row, before rounding to a cell. |
depth |
float
|
Distance from the camera along its view axis. Smaller is nearer. |
Source code in terminaltexteffects/utils/geometry3d.py
coord
property
The canvas cell containing the point.
Rotation
dataclass
A rotation stored as a unit quaternion.
Quaternions compose without gimbal lock and interpolate smoothly with slerp. Build rotations with the
about_* constructors rather than setting the components directly. Rotation() is no rotation.
Combine rotations with *: second * first applies first, then second.
Attributes:
| Name | Type | Description |
|---|---|---|
w |
float
|
Scalar part. |
x |
float
|
Vector part, x component. |
y |
float
|
Vector part, y component. |
z |
float
|
Vector part, z component. |
Source code in terminaltexteffects/utils/geometry3d.py
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | |
__mul__(other)
Return the rotation that applies other, then this rotation.
Source code in terminaltexteffects/utils/geometry3d.py
about_axis(axis, angle)
staticmethod
Return a rotation of angle radians about axis, counterclockwise when looking down the axis.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in terminaltexteffects/utils/geometry3d.py
about_x(angle)
staticmethod
Return a rotation of angle radians about the x axis. Positive angles tip the top toward the viewer.
about_y(angle)
staticmethod
Return a rotation of angle radians about the y axis. Positive angles turn the front to the right.
about_z(angle)
staticmethod
Return a rotation of angle radians about the z axis. Positive angles turn counterclockwise.
apply(vector)
Return vector rotated by this rotation.
Source code in terminaltexteffects/utils/geometry3d.py
inverse()
matrix()
Return the equivalent 3x3 rotation matrix in row-major order, for transforming many points quickly.
Source code in terminaltexteffects/utils/geometry3d.py
normalized()
Return this rotation rescaled to unit length, correcting drift from many compositions.
Source code in terminaltexteffects/utils/geometry3d.py
slerp(other, t)
Return the rotation t of the way from this rotation to other along the shortest path.
t of 0 returns this rotation and 1 returns other. The turn proceeds at a constant angular speed, so an
easing function applied to t shapes the motion.
Source code in terminaltexteffects/utils/geometry3d.py
Vec3
dataclass
An immutable 3D vector or point in world space.
Attributes:
| Name | Type | Description |
|---|---|---|
x |
float
|
Right, in terminal columns. |
y |
float
|
Up, in terminal columns. |
z |
float
|
Toward the viewer, in terminal columns. |
Source code in terminaltexteffects/utils/geometry3d.py
__add__(other)
__mul__(scalar)
__neg__()
__rmul__(scalar)
__sub__(other)
__truediv__(scalar)
cross(other)
Return the cross product, a vector perpendicular to both.
Source code in terminaltexteffects/utils/geometry3d.py
dot(other)
Return the dot product, which is positive when both vectors point the same general way.
length()
lerp(other, t)
Return the point t of the way from this vector to other, where 0 is this vector and 1 is other.
Source code in terminaltexteffects/utils/geometry3d.py
normalized()
Return a vector of length 1 pointing the same way.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the vector has zero length, so it has no direction. |
Source code in terminaltexteffects/utils/geometry3d.py
box(width, height, depth)
Return a box centered on the origin. Each face is textured from 0 to 1 and tagged 0 to 5.
Tags: 0 front (+z), 1 right (+x), 2 back (-z), 3 left (-x), 4 top (+y), 5 bottom (-y).
Source code in terminaltexteffects/utils/geometry3d.py
cylinder(radius, height, segments=32, *, caps=True)
Return a cylinder around the y axis, built from segments flat sides.
The side is textured once around: u runs from 0 at the front, rightward around to 1, and v from 0 at the bottom to 1 at the top. Side faces are tagged 0; caps, when included, are tagged 1 (top) and 2 (bottom).
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in terminaltexteffects/utils/geometry3d.py
icosahedron(radius)
Return a regular icosahedron whose corners are radius from the origin.
Source code in terminaltexteffects/utils/geometry3d.py
lambert(normal, light)
Return how directly a surface faces a light, from 0 (edge-on or facing away) to 1 (facing it squarely).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
normal
|
Vec3
|
The surface's outward normal. |
required |
light
|
Vec3
|
Direction from the surface toward the light. |
required |
Source code in terminaltexteffects/utils/geometry3d.py
line_symbol(column_change, row_change, row_scale=TERMINAL_ROW_SCALE)
Return -, /, | or \, whichever best follows a line with this on-screen change.
The row change is scaled by row_scale first, so the choice follows the line's visible angle on tall cells.
Source code in terminaltexteffects/utils/geometry3d.py
octahedron(radius)
Return a regular octahedron whose corners are radius from the origin.
Source code in terminaltexteffects/utils/geometry3d.py
prism(sides, radius, height)
Return a prism with sides rectangular sides around the y axis, capped top and bottom.
radius is the distance from the axis to each corner. Side i is tagged i and textured from 0 to 1; going
around, side i + 1 is to the right of side i. The top cap is tagged sides and the bottom sides + 1.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in terminaltexteffects/utils/geometry3d.py
pyramid(sides, radius, height)
Return a pyramid with a sides-sided base, centered so its base and apex are height / 2 from the origin.
Side i is tagged i; the base is tagged sides.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in terminaltexteffects/utils/geometry3d.py
rasterize(mesh, camera, *, rotation=None, scale=1.0, position=ORIGIN, cull='back', buffer=None, bounds=None)
Fill the mesh's faces into a per-cell depth buffer and return it.
The mesh is scaled, rotated and moved to position, then each face is projected and every cell whose center
it covers is compared with what the buffer already holds; the nearer surface wins. Pass the same buffer to
several calls to draw several meshes into one view with correct overlap. Faces crossing the near plane
are clipped, with texture coordinates interpolated at the new corners.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mesh
|
Mesh
|
The solid to draw. |
required |
camera
|
Camera
|
Projects the mesh onto the canvas. |
required |
rotation
|
Rotation | None
|
Orientation of the mesh. Defaults to no rotation. |
None
|
scale
|
float
|
Uniform size multiplier. |
1.0
|
position
|
Vec3
|
Where the mesh's origin sits in world space. |
ORIGIN
|
cull
|
str
|
Skip faces turned |
'back'
|
buffer
|
dict[Coord, Fragment] | None
|
A buffer to draw into. Defaults to a new one. |
None
|
bounds
|
Bounds | None
|
Only fill cells inside these bounds, such as the canvas. Work outside them is skipped, so geometry that projects far off screen stays cheap. Defaults to no limit. |
None
|
Returns:
| Type | Description |
|---|---|
dict[Coord, Fragment]
|
dict[Coord, Fragment]: The nearest surface in each covered cell. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in terminaltexteffects/utils/geometry3d.py
tetrahedron(radius)
Return a regular tetrahedron whose corners are radius from the origin.
Source code in terminaltexteffects/utils/geometry3d.py
torus(major_radius, minor_radius, segments=36, sides=18)
Return a ring-shaped torus lying flat around the y axis.
major_radius is from the center of the hole to the center of the tube, and minor_radius is the tube's
radius. The tube is built from segments sections, each with sides faces.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in terminaltexteffects/utils/geometry3d.py
transform_points(points, rotation=None, *, scale=1.0, position=ORIGIN)
Return points scaled, then rotated, then moved to position.
The rotation is converted to a matrix once, so this is faster than calling Rotation.apply per point.
Source code in terminaltexteffects/utils/geometry3d.py
uv_sphere(radius, segments=24, rings=12)
Return a sphere built from segments slices around the y axis and rings bands from pole to pole.
Textured like a globe: u runs once around from the front, rightward, and v from 0 at the south pole to 1 at the north pole. Faces touching a pole are triangles.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in terminaltexteffects/utils/geometry3d.py
wireframe(mesh, camera, *, rotation=None, scale=1.0, position=ORIGIN, bounds=None, depth_test=False)
Return the cells along the mesh's edges.
Where edges cross, a front edge wins over a hidden one, and otherwise the nearer edge wins. Edges crossing
the near plane retain their visible segment. With bounds, only the cells of each edge inside them are
visited, so an edge reaching far off screen costs no more than one on it.
By default, front classifies adjacent face orientation, which only approximates visibility for concave
meshes. With depth_test=True, cells behind the mesh's filled surface are also classified as hidden.
This adds a rasterization pass, restricted to the same bounds.