Table of Contents
2D Drawing
The system pyssel/2d draws rectangles, rounded rectangles, circles, lines,
textures and text, in units from the top left of the window, so they keep
their size on a scaled display, where edges stay sharp at the display's
pixels:
(asdf:load-system :pyssel/2d)
(defpackage #:shapes
(:use #:cl)
(:local-nicknames (#:p2d #:pyssel/2d) (#:tr #:cl-textrender)))
(in-package #:shapes)
(defvar *renderer*)
(defvar *font* (tr:make-font "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" :size 14))
(defun scene (canvas)
(p2d:fill-rect canvas 40 40 240 120 '(0.13 0.15 0.2) :radius 12)
(p2d:stroke-rect canvas 40 40 240 120 '(0.4 0.6 1) :thickness 2 :radius 12)
(p2d:fill-circle canvas 100 100 30 '(1 0.6 0.2))
(p2d:draw-line canvas 150 80 250 120 '(1 1 1 0.8) :thickness 3)
(p2d:draw-text canvas "Hello, Pyssel" 150 130 *font* '(1 1 1)))
(defun setup (engine)
(setf *renderer* (p2d:make-renderer engine))
(p2d:make-canvas *renderer* 'scene))
(defun draw (engine pass)
(declare (ignore engine))
(p2d:draw-canvases pass *renderer*))
(defvar *engine* (pyssel:start :title "Shapes" :width 320 :height 200 :redraw :on-demand
:setup 'setup :draw 'draw))
The example keeps its renderer and its font in variables, as the
triangle of Getting Started keeps its pipeline:
drawing with pyssel/2d itself, it has no node to keep them in. A game
draws from canvas items, which keep their own canvases.
A renderer draws canvases. A canvas's draw function, here scene, fills it;
it runs when the canvas is first drawn and again after queue-redraw, and
otherwise the canvas keeps what it drew as a recorded render bundle. Since
scene is given as a symbol, redefining it at the REPL redraws the canvas.
With :redraw :on-demand the engine draws nothing at all while nothing
changes. Given :x, :y, :width and :height, draw-canvases draws
a renderer's canvases into that rectangle of the target as if it were the
whole target, cut off at its edges, as a popup drawn inside its window is.
Given :transform, a cl-gmath dmat3x2, it draws them through it, as a
viewport's canvas transform zooms and pans and a canvas layer's turns: a
point x, y of the renderer's coordinates, where each canvas's
placement puts what it draws, is drawn where the transform takes it, in
the target's units, clips and all, each clip within the box around it so
taken. :scale is the zoom they record for: canvases record for the
display's scale times it, so a new scale draws again those that depend
on it, such as raster text, which stays sharp at every zoom; give the
scale the transform zooms by. A new transform records nothing, however
far it moves, turns or stretches them, a few hundred thousand units as
readily as one.
A canvas's placement, canvas-placement, a cl-gmath dmat3x2 that
setf sets and the canvas keeps a copy of, takes the coordinates its
draw function draws in to the renderer's, as a 2D node's global
transform places what the node draws: the identity by default. It too is
applied as the canvas is drawn, so a canvas that moves, turns or
stretches every frame records nothing again; only what was drawn on the
pixels is, when they move: what was drawn for the scale, as a polygon's
anti-aliased edges or an image rasterized for it, when the placement
turns or stretches the canvas, and what was drawn on the target's
pixels, as upright raster text and what snap-to-pixel snapped, also
when it moves the canvas by other than whole pixels. The renderer works
out each canvas's way to the target's pixels, its transform after its
placement, in double-floats each frame, and gives the GPU single-floats
near the view, so a canvas placed a million units out, under a transform
that pans there, is drawn as precisely as one near the origin, as
Godot's 2D renderer gives the GPU its items' final transforms.
prepare-canvases takes the same arguments, but draws nothing: it does
the frame's work before drawing, running the due canvases' draw
functions, uploading what they recorded and bringing the painters' data
up to date, and returns true if what draw-canvases would draw differs
from what the canvases were last prepared to draw: a canvas drawn anew,
made, destroyed, moved to another layer or clipped anew, a pipeline or
bind group made anew, a painter's data changed, a render target they draw
drawn anew, or another texture, transform, scale or rectangle.
draw-canvases with the same arguments later in the frame draws what was
prepared. A render target's :prepare calls it, so
that the target is drawn only when its canvases changed; it also draws
first the render targets those canvases draw.
-
Shapes:
fill-rectandstroke-rect, both with:radiusfor rounded corners, andstroke-rectwith:thickness, or:left,:top,:rightand:bottomfor each side;fill-circleandstroke-circle;draw-line; anddraw-texture, which draws a texture, or a region of it, tinted and filtered as asked, or a render target of the engine's, a texel to each of its pixels, as it was drawn into that frame and from its new texture once it is resized. Edges are anti-aliased. -
Polygons:
(p2d:fill-polygon canvas '(0 0 10 0 5 8) color)fills the polygon whose corners are given, x and y of each in turn, a list or a vector of reals, as Godot'sdraw_colored_polygondoes: a simple polygon, convex or not, its corners in either order. It is cut into triangles on the CPU and drawn by a painter of its own,polygon-painter; its outline is anti-aliased over a pixel, half in and half out, as the other shapes' edges are, unless:antialiasedisnil, and a canvas that fills polygons so is drawn again when its scale changes. A polygon whose sides cross fills as its triangles fall, as in Godot; fewer than three corners, or one that cannot be cut into triangles, as a figure of eight, is an error. A canvas that fills a polygon every frame allocates nothing once it has filled one with as many corners. -
Images:
(p2d:make-image width height pixels)makes an image of pixels in memory, four octets each, red, green, blue and alpha, rows top first, as cl-svgread's rasterizer gives them, anddraw-imagedraws it, or a region of it, asdraw-texturedraws a texture. Its:scale, 1 unless given, is how many of its pixels make a unit: an image rasterized at a display's scale has that scale, so it is drawn and laid out at its size in units,image-size, sharp. The renderer uploads an image to a texture the first time a canvas draws it, keeps the texture with the image, as a font keeps its glyphs', and frees it once the collector takes the image, so an image needs no destroying. No side may be longer than 8192 pixels, the largest texture every device holds. An image drawn from a source, as an icon from its SVG file, may be given:rasterize, a function of a scale that returns its pixels drawn anew at that scale, with their width and height, ornilwhere it cannot be drawn there. Drawn on a canvas recorded at another scale than its own, as in a viewport zoomed in, it is then drawn from a copy of itself rasterized at that scale, into the same rectangle, so it stays sharp at every zoom, as Godot'sDPITexturedraws its SVG at each viewport's oversampling; the canvas is drawn again when its scale changes, and a copy goes once no canvas draws it. An image made from pixels alone is scaled. -
Transforms:
translate,rotateandscalechange where what follows is drawn, andtransformapplies any 2D affine transform, a cl-gmathmat3x2;with-saved-transformkeeps them to its body.snap-to-pixelmoves the origin to the nearest pixel, so that what is drawn whole units from it has sharp edges, as a user interface wants. -
Colours are sRGB, as
(red green blue)or(red green blue alpha), each from 0 to 1, or a vector of them, tinted and blended as those values, as in Godot: the typep2d:color, which every drawing function declares, so a wrong colour is refused as atype-error;colorptells a colour. Textures are cl-gfx textures with:sampledin their usage. A plain format such as:rgba8-unormholds an image's bytes as image files do; an sRGB format such as:rgba8-srgbdraws the same. -
Text:
draw-textlays out a string in a cl-textrender font, wrapping it to:widthif given, and draws it with its top left at a point;draw-layoutdraws text laid out already withtr:layout-text, which is what to do for text drawn often. Both take:painter::raster, the default,:slugor:sdf, and the effects:outline-width,:outline-color,:shadow-x,:shadow-y,:shadow-softnessand:shadow-color, which draw with:sdf.draw-layoutalso takes:colors, a function of a glyph's index in the layout's text that gives the glyph's colour, ornilfor the layout's, so that one layout draws in several colours, as a code editor's highlighting does:(p2d:draw-layout canvas layout 4 6 '(1 1 1) :colors (lambda (index) (if (< index 5) '(1 0.5 0) nil))) -
Canvases of a lower
:layerare drawn first. Within a canvas, shapes are drawn in order, and neighbouring shapes that read the same texture are drawn together in one call. -
(p2d:set-canvas-clip canvas x y width height)draws a canvas only within a rectangle of the renderer's coordinates, where placements put canvases, the window's units unless:transformtakes them elsewhere, kept in double-floats; its edges are widened to whole pixels.clear-canvas-clipdraws all of it again. A clip records nothing again, so it may change every frame. -
(p2d:canvas-bounds canvas)gives the box around what a canvas draws, itsx,y,widthandheightin the coordinates its draw function draws in, wherever its placement puts it, eachnilfor a canvas that draws nothing, as an editor picks what it shows: a 2D node's own, so the box of a turned node turns with it. Given a cl-gmathdmat3x2, it gives the box in the coordinates that takes those to, such as the renderer's through the canvas's placement. It is the box of what the canvas drew last, text's outlines and shadows included, found from its recording when it is asked for, from any thread, so recording does no more work for it.
With :painter :raster, text is drawn from a glyph atlas the font keeps:
each glyph is rasterized once, by FreeType, at the display's scale and
hinted to its pixels, then packed into a texture, so text is as sharp as
the CPU rasterizer draws it. A canvas with such text is drawn again when
the display's scale changes. Under a transform that turns or scales, glyphs
are turned and scaled as images. This suits small, upright text, such as a
user interface's.
An engine's renderers share these glyphs, as Godot's viewports share their
fonts' caches: a popup drawn at the window's scale rasterizes nothing the
window has, and draws from the same textures. Glyphs are kept by scale, the
display's times the :scale draw-canvases is given, as a viewport's zoom:
a renderer holds the scale it draws at, and so does each canvas's drawing
that holds raster text, and once nothing holds a scale, its glyphs go,
textures and all, as Godot 4.5 frees an oversampling level's glyphs with
its last reference. A view zoomed in steps so keeps the glyphs of the zoom
it shows only. A canvas whose draw function failed keeps what it drew
before, and the glyphs of the scale it was drawn at with it.
With :painter :slug, glyphs are drawn from their curves on the GPU by the
Slug algorithm, through
cl-slug: sharp at any size, scale or
turn, but not hinted. This suits zoomed, turned or large text:
(defun title (canvas)
(p2d:translate canvas 160 100)
(p2d:rotate canvas -0.2)
(p2d:scale canvas 3)
(p2d:draw-text canvas "Pyssel" -40 -10 *font* '(1 0.8 0.3) :painter :slug))
Each glyph's curves are added once to its face's curve and band textures, which grow as needed.
With :painter :sdf, glyphs are drawn from multi-channel distance fields,
which cl-sdf makes and
cl-textrender/sdf packs into an atlas: sharp over a range of sizes, not
hinted, and with outlines, shadows and glows. Asking for any effect draws
with :sdf:
(defvar *title-font* (tr:make-font "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
:size 32))
(defun banner (canvas)
(p2d:draw-text canvas "Game over" 40 70 *title-font* '(1 0.85 0.3)
:outline-width 2 :outline-color '(0.5 0.15 0)
:shadow-x 3 :shadow-y 4 :shadow-softness 4 :shadow-color '(0 0 0 0.6)))
;; In SETUP, before the text is first drawn:
;; (p2d:prepare-glyphs *renderer* *title-font* "Game over")
:outline-width outlines the glyphs that far outside their edges, in
:outline-color, black by default. Any of the :shadow- arguments draws a
shadow: the glyphs and their outline moved :shadow-x right and
:shadow-y down, fading out over :shadow-softness, in :shadow-color,
black by default. A glow is a shadow that is not moved. Lengths are in
canvas units, so effects scale and turn with the text, and the outline's
width and the shadow's softness together reach at most a quarter of the
font size. A layout's shadows are drawn before its glyphs, so no shadow
covers a neighbouring glyph.
A glyph's field takes milliseconds to make, so it is made on the engine's
job pool the first time any text draws it, never in a draw function: text
whose fields are not made yet is drawn rasterized meanwhile, or, with
effects, which only fields draw, left out, and its canvas is drawn again
from the fields when they are made, a few frames later. prepare-glyphs
makes the fields of a string's glyphs ahead, so text known early, such as
a game's interface, is drawn from them at once; glyphs-pending-p says
whether any are still being made.
One field serves every size of a face: the fonts tr:face-font gives of
one face share them.
Fonts keep what the painters make for them, as Godot's fonts keep their glyph textures: the glyph atlases, the fields and the curves, and the GPU textures they are uploaded to. A canvas that draws a font keeps the font, and the collector frees it, its textures with it, once no canvas draws it and nothing else holds it; the textures are destroyed on the render thread. A font that outlives a renderer, as a theme's outlives an engine, keeps nothing of it once the renderer is released, which destroys the textures it made, and an image that outlives one keeps nothing of it either. Text in different fonts or sizes reads different textures, so it does not share a draw call.
Getting started
The core
Drawing
Scenes
User interface
Games and the editor
- Playing a Game
- Projects
- The Editor
- The File System Dock
- The Script Screen
- The REPL
- The Debugger
- Lisp Text
- Code Services
- Project Manager
- Undo and Redo
- Exporting
The project