Table of Contents
Painters
Shapes are drawn by painters, each with its own shaders and instance format. A
renderer has three: quad-painter for shapes, textures and raster text,
slug-painter and sdf-painter. A canvas records instances of any of them in
the order they are drawn; neighbouring instances of one painter that read the
same texture are one draw call.
A painter of your own subclasses painter, names its clsl program, whose
vertex shader's inputs are an instance's, and says how many floats an
instance has; drawing functions find it with canvas-painter and record
instances with add-instance. In a package that uses CL and CLSL, with
p2d for pyssel/2d:
(define-shader solid-vertex :vertex ((rect :vec4) (color :vec4))
(let* ((index (vertex-index))
(corner (vec2 (float (mod index 2)) (float (floor index 2)))))
(values (p2d:clip-position (mix (swizzle rect :xy) (swizzle rect :zw) corner))
color)))
(define-shader solid-fragment :fragment ((color :vec4 :interpolation :flat))
(vec4 (* (swizzle color :xyz) (w color)) (w color)))
(define-program solid-program
:vertex solid-vertex :fragment solid-fragment)
(defclass solid-painter (p2d:painter) ()
(:default-initargs :program 'solid-program :instance-floats 8))
(defun fill-solid (canvas x0 y0 x1 y1 color)
(let ((painter (p2d:canvas-painter canvas 'solid-painter)))
(multiple-value-bind (instances start) (p2d:add-instance canvas painter nil :linear)
(multiple-value-bind (ax ay) (p2d:transform-point canvas x0 y0)
(multiple-value-bind (bx by) (p2d:transform-point canvas x1 y1)
(replace instances (list ax ay bx by) :start1 start)
(replace instances color :start1 (+ start 4)))))))
(defun panel (canvas)
(fill-solid canvas 20 20 140 80 '(0.2 0.6 1.0 1.0)))
;; (p2d:make-canvas renderer 'panel)
(p2d:canvas-painter canvas 'solid-painter) gives the painter of that
class of the canvas's renderer, made the first time the renderer is asked
for one, and the same painter after. So fill-solid keeps no painter in a
variable and draws in any engine, as a node's draw method draws both in
the editor's viewport and in a play, each with a renderer of its own.
renderer-painter asks a renderer, and the built-in classes,
quad-painter, polygon-painter, slug-painter and sdf-painter, give
the renderer's own. The renderer makes a painter with make-instance and no initargs,
so its class gives :program and :instance-floats in its
:default-initargs.
Asking never waits for the render thread, so a draw function may ask:
draw functions run on the engine's job pool while the render thread waits
for them, and a painter asked for first is made at once, on the thread
that asks, and its pipeline on the render thread before the frame draws
what was recorded with it. Until then it is not among
renderer-painters. If a painter's pipeline cannot be made, as when its
shaders do not compile, the canvases that drew with it keep what they drew
before, as when a draw function fails: canvas-error says why. The
renderer keeps the failure: asking for the class again signals the same
error at once, without compiling anything, so a canvas warns of it once.
Once a clsl definition is redefined, or the class gives another
:program or :instance-floats, asking makes the painter anew, so a fix
takes at once.
Each instance is drawn as a triangle strip of :vertex-count vertices, 4
by default. Its floats are the vertex shader's inputs, side by side, as
clsl:program-vertex-buffers lays them out, and a painter whose
:instance-floats differ from what its shader reads is refused when its
pipeline is made. The vertex shader places its vertices with
p2d:clip-position, which takes a point of the units a canvas records in
to clip space, through the transform the canvas is drawn through, so the
painter's shapes move, zoom, pan and turn with the others;
p2d:pixel-vector gives the pixels a vector of those units covers, as
anti-aliasing over a pixel needs. The shaders may read *target-size*,
*canvas-transform*, a :mat3x2 from those units to the target's
pixels, which each canvas has its own of, worked out in double-floats so
that it holds numbers near the view, and *linear-target*, pyssel/2d's
uniforms of the :frame group, and give colours of premultiplied alpha.
Uniforms of a painter's own belong in the :draw group or one of their
own: the :frame group holds 256 bytes at most, and a painter whose
shaders read more of it is refused as its pipeline is made.
A painter whose batches read textures gives
their :draw group from a method on painter-bind-group, updates its GPU
data each frame in a method on prepare-painter, and what the batches
drawn anew read in one on prepare-batch, and releases it in one on
release-painter. prepare-painter returns true if it destroyed bind
groups it gave before, and, as a second value, true if its data changed
what its instances draw, as a uniform that follows the time does: a
render target that shows them is then drawn again, where
one whose canvases changed nothing keeps its picture.
(defmethod p2d:prepare-painter ((painter wave-painter))
;; WRITE-TIME-UNIFORM, the painter's own, writes the time its shader waves by.
(write-time-uniform painter (pyssel:engine-time
(p2d:renderer-engine (p2d:painter-renderer painter))))
(values nil t)) ; what it draws changes in every frame
A painter says where an instance lies in a method on
instance-parallelogram, from the floats recorded for it: the
parallelogram its drawing lies within, as a corner and two sides in the
units the canvas records in, six single-floats. canvas-bounds finds the box around a
canvas from them, and the editor picks what is drawn by that box. Without
a method of its own, a painter's instances count for nothing there, and
its renderer warns with painter-without-bounds as it makes the painter;
a method that returns nil, for a painter whose drawing has no place,
says so on purpose and silences it. solid-painter's instances begin
with their rectangle's corners:
(defmethod p2d:instance-parallelogram ((painter solid-painter) instances start)
(let ((x0 (aref instances start)) (y0 (aref instances (+ start 1)))
(x1 (aref instances (+ start 2))) (y1 (aref instances (+ start 3))))
(values x0 y0 (- x1 x0) 0.0 0.0 (- y1 y0))))
A renderer is released when its engine stops, the textures it made for
fonts and all;
destroy-renderer drops one sooner, while the engine goes on.
Draw functions run on the engine's job pool, several at once, so a draw
function draws only on its own canvas. If one signals an error, its canvas
keeps what it drew before, the engine warns with canvas-not-redrawn
unless the canvas failed with that same error last, and canvas-error
returns the error until a redraw succeeds. A renderer's
renderer-error-version moves on whenever a canvas's error comes, goes or
changes, so what shows which canvases failed notices without looking at
each. A frame in which no canvas changes allocates nothing, and neither
does redrawing a canvas once its storage has grown to fit.
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