Atlases and Regions
The examples on this page use square from
Getting Started:
(defun square (size &optional (value 255))
"Return an image of SIZE by SIZE texels of one byte, each VALUE."
(make-array (list size size) :element-type '(unsigned-byte 8) :initial-element value))
Making an atlas
(atlas:make-atlas &key (page-size 1024) (channels 1) (padding 1) data) => atlas
| Argument | Meaning | Default |
|---|---|---|
:page-size |
The width and height of every page, in texels. | 1024 |
:channels |
The bytes of a texel, from 1 to 4: 1 for coverage, such as glyphs', 4 for colour. | 1 |
:padding |
The empty texels kept right of and below each image. | 1 |
:data |
Anything, carried on the atlas as atlas-data, such as the font whose glyphs it holds. |
nil |
atlas-page-size, atlas-channels, atlas-padding and atlas-data return
them. A renderer that holds a page reaches the atlas, and so its data,
through page-atlas.
cl-atlas does not interpret the bytes of a texel: they are whatever the texture's format will make of them, such as red, green, blue and alpha.
Images
An image is a 2D array of octets, (array (unsigned-byte 8) (* *)),
indexed by row and then by byte. Each row holds its texels from left to
right, :channels bytes each, so an image of 4 channels has rows four
times as long as it is wide:
;; 2 by 2 texels of red, green, blue and alpha.
(let* ((atlas (atlas:make-atlas :page-size 64 :channels 4))
(image (make-array '(2 8) :element-type '(unsigned-byte 8)
:initial-contents '((255 0 0 255 0 255 0 255)
(0 0 255 255 255 255 255 255))))
(region (atlas:atlas-add atlas image)))
(list (atlas:region-width region) (atlas:region-height region)))
;; => (2 2)
An image whose rows hold no whole number of texels signals atlas-error.
See Errors.
Adding images
(atlas:atlas-add atlas pixels &key data) => region
atlas-add packs the image and returns its region. The image's texels are
copied into the page: the array may be reused or changed afterwards.
:data rides along on the region, as region-data, such as a glyph's
offsets from its origin and its advance.
An empty image, of no rows or no columns, takes no room: its region has no page.
Regions
A region says where an image went. It never moves and never changes, so it may be kept and shared.
| Function | Returns |
|---|---|
(region-page region) |
The page it is on, or nil for an empty image. |
(region-x region), (region-y region) |
Its column and row on the page, from the top left, in texels. |
(region-width region), (region-height region) |
Its size in texels, without padding. |
(region-data region) |
What was given with it. |
(region-uv region) |
Four values, u0, v0, u1 and v1: its top left and bottom right corners as texture coordinates, from 0 to 1 across the page, with v growing down. Zeros for an empty image. |
(let* ((atlas (atlas:make-atlas :page-size 64))
(region (atlas:atlas-add atlas (square 16))))
(multiple-value-list (atlas:region-uv region)))
;; => (0.0 0.0 0.25 0.25)
The cache
Most images are made on demand: a glyph the first time it is drawn at a
size, an icon the first time it is shown. atlas-find-or-add keeps them by
a key:
(atlas:atlas-find-or-add atlas key function) => region
(atlas:atlas-find atlas key) => region or nil
- The first time
keyis asked for,atlas-find-or-addcallsfunctionwith no arguments to make the image. It returns the image, and as a second value, optionally, the region's data. The region is kept underkey. - Every time after, it returns the same region, and
functionis not called. atlas-findonly looks, and returnsnilfor a key never added.- Keys are compared with
equal, as a hash table of that test compares them: lists, strings and numbers work, numbers byeql, so1and1.0are different keys. - Images added with
atlas-addhave no key.
(let* ((atlas (atlas:make-atlas :page-size 64))
(first (atlas:atlas-find-or-add atlas '(:dot 4)
(lambda () (values (square 4) '(:advance 5)))))
(again (atlas:atlas-find-or-add atlas '(:dot 4)
(lambda () (error "Not called: the key is known.")))))
(list (eq first again) (atlas:region-data again) (atlas:atlas-find atlas '(:dot 5))))
;; => (T (:ADVANCE 5) NIL)
Looking up without allocating
atlas-find allocates nothing and keeps no reference to its key, so code
that runs every frame can make its key on the stack. Keep the key on the
heap for atlas-find-or-add, which stores it when the image is new:
(defun dot-region (atlas size)
"Return the region of a dot of SIZE, packing it the first time."
(let ((key (list :dot size)))
(declare (dynamic-extent key))
(or (atlas:atlas-find atlas key)
(atlas:atlas-find-or-add atlas (list :dot size) (lambda () (square size))))))
Once every dot is packed, dot-region allocates nothing.
Pages
A page is a square of page-size texels, page-channels bytes each, and
page-index is its place in the atlas, counting from 0. page-atlas is
the atlas it belongs to; a page keeps its atlas alive.
| Function | Returns |
|---|---|
(atlas-pages atlas) |
A fresh list of the pages, by index. |
(atlas-page-count atlas) |
How many pages there are. Allocates nothing. |
(atlas-page atlas index) |
The page at index. Allocates nothing. |
Code that runs every frame loops with atlas-page-count and atlas-page,
which allocate nothing, rather than over atlas-pages.
How packing works
Each image goes into the first page with room for it and its padding. When no page has room, the atlas adds a page. Pages never grow, so a region never moves, and the texture coordinates a renderer recorded for it stay right.
Within a page, a skyline packer fills the page from the top. The skyline is the lower edge of what is taken so far. An image goes where its top would be highest, leftmost among equals, and raises the skyline under it:
(let ((atlas (atlas:make-atlas :page-size 32)))
(loop for size in '(10 10 10 5)
collect (let ((region (atlas:atlas-add atlas (square size))))
(list (atlas:region-x region) (atlas:region-y region)))))
;; => ((0 0) (11 0) (0 11) (22 0))
The third square does not fit beside the first two, so it goes below them; the small fourth one still fits at the top right. Space under an overhang is lost, which costs little for glyphs, whose heights vary less than their number.
Choosing a page size and padding
- The largest image is the page size less the padding: the padding is kept below and right of every image, at the page's edges too. An atlas of pages of 16 with padding 1 takes images up to 15 by 15.
- Bigger pages mean fewer pages, so fewer textures to bind, and more memory held by a page that is mostly empty.
- Padding keeps filtering from reading a neighbour. Linear filtering reads one texel past an image's edge, which the default of 1 covers. Mipmaps blend wider: give more padding if you generate them for an atlas.
Clearing
atlas-clear forgets every page and every key. The pages after it are new,
and count from 0 again. Regions handed out before keep their pages, which
the atlas no longer lists:
(let ((atlas (atlas:make-atlas :page-size 32)))
(atlas:atlas-add atlas (square 20))
(let ((old (atlas:atlas-page atlas 0)))
(atlas:atlas-clear atlas)
(atlas:atlas-add atlas (square 4))
(list (atlas:atlas-page-count atlas)
(atlas:page-index (atlas:atlas-page atlas 0))
(eq old (atlas:atlas-page atlas 0)))))
;; => (1 0 NIL)
A renderer drops its textures for the old pages, and the regions it kept from before. An atlas only grows otherwise: there is no eviction of single images yet.