Add particle rendering module. wip-particles
authorDavid Thompson <dthompson2@worcester.edu>
Wed, 5 Dec 2018 02:50:55 +0000 (21:50 -0500)
committerDavid Thompson <dthompson2@worcester.edu>
Sat, 8 Dec 2018 04:01:40 +0000 (23:01 -0500)
* chickadee/render/particles.scm: New file.
* Makefile.am (SOURCES): Add it.
* doc/api.texi (Particles): New subsection.

Makefile.am
chickadee/render/particles.scm [new file with mode: 0644]
doc/api.texi

index 2b27ba2..2933194 100644 (file)
@@ -69,6 +69,7 @@ SOURCES =                                     \
   chickadee/render/tiled.scm                   \
   chickadee/render/scene.scm                   \
   chickadee/render/asset.scm                   \
+  chickadee/render/particles.scm               \
   chickadee/render.scm                         \
   chickadee/scripting/agenda.scm               \
   chickadee/scripting/script.scm               \
diff --git a/chickadee/render/particles.scm b/chickadee/render/particles.scm
new file mode 100644 (file)
index 0000000..9d15ecd
--- /dev/null
@@ -0,0 +1,476 @@
+;;; Chickadee Game Toolkit
+;;; Copyright © 2018 David Thompson <davet@gnu.org>
+;;;
+;;; Chickadee is free software: you can redistribute it and/or modify
+;;; it under the terms of the GNU General Public License as published
+;;; by the Free Software Foundation, either version 3 of the License,
+;;; or (at your option) any later version.
+;;;
+;;; Chickadee is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;;; General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with this program.  If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+(define-module (chickadee render particles)
+  #:use-module (rnrs bytevectors)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-4)
+  #:use-module (srfi srfi-9)
+  #:use-module (srfi srfi-9 gnu)
+  #:use-module (system foreign)
+  #:use-module (chickadee math)
+  #:use-module (chickadee math matrix)
+  #:use-module (chickadee math rect)
+  #:use-module (chickadee math vector)
+  #:use-module (chickadee render)
+  #:use-module (chickadee render buffer)
+  #:use-module (chickadee render color)
+  #:use-module (chickadee render shader)
+  #:use-module (chickadee render texture)
+  #:export (make-particle-emitter
+            particle-emitter?
+            particle-emitter-spawn-area
+            particle-emitter-rate
+            particle-emitter-life
+            particle-emitter-done?
+            make-particles
+            particles?
+            particles-capacity
+            particles-size
+            particles-texture
+            particles-blend-mode
+            particles-color
+            particles-spawn-area
+            add-particle-emitter
+            remove-particle-emitter
+            update-particles
+            draw-particles*
+            draw-particles))
+
+(define-record-type <particle-emitter>
+  (%make-particle-emitter spawn-area rate life)
+  particle-emitter?
+  (spawn-area particle-emitter-spawn-area)
+  (rate particle-emitter-rate)
+  (life particle-emitter-life set-particle-emitter-life!))
+
+(define* (make-particle-emitter spawn-area rate #:optional duration)
+  "Return a new particle emitter that spawns RATE particles per frame
+within SPAWN-AREA (a rectangle or 2D vector) for DURATION frames.  If
+DURATION is not specified, the emitter will spawn particles
+indefinitely."
+  (%make-particle-emitter spawn-area rate duration))
+
+(define (update-particle-emitter emitter)
+  "Advance the lifecycle of EMITTER."
+  (let ((life (particle-emitter-life emitter)))
+    (when life
+      (set-particle-emitter-life! emitter (- life 1)))))
+
+(define (particle-emitter-done? emitter)
+  "Return #t if EMITTER has finished emitting particles."
+  (let ((life (particle-emitter-life emitter)))
+    (and life (<= life 0))))
+
+(define-record-type <particles>
+  (%make-particles capacity size buffer shader vertex-array
+                   texture animation-rows animation-columns
+                   speed-range acceleration-range direction-range
+                   blend-mode start-color end-color lifetime
+                   sort emitters)
+  particles?
+  (capacity particles-capacity)
+  (size particles-size set-particles-size!)
+  (buffer particles-buffer)
+  (shader particles-shader)
+  (vertex-array particles-vertex-array)
+  (texture particles-texture set-particles-texture!)
+  (animation-rows particles-animation-rows)
+  (animation-columns particles-animation-columns)
+  (speed-range particles-speed-range set-particles-speed-range!)
+  (acceleration-range particles-acceleration-range
+                      set-particles-acceleration-range!)
+  (direction-range particles-direction-range set-particles-direction-range!)
+  (blend-mode particles-blend-mode set-particles-blend-mode!)
+  (start-color particles-start-color set-particles-start-color!)
+  (end-color particles-end-color set-particles-end-color!)
+  (lifetime particles-lifetime set-particles-lifetime!)
+  (sort particles-sort set-particles-sort!)
+  (emitters particles-emitters set-particles-emitters!))
+
+(define (add-particle-emitter particles emitter)
+  "Add EMITTER to PARTICLES."
+  (set-particles-emitters! particles
+                           (cons emitter (particles-emitters particles))))
+
+(define (remove-particle-emitter particles emitter)
+  "Remove EMITTER from PARTICLES."
+  (set-particles-emitters! particles
+                           (delete emitter (particles-emitters particles))))
+
+(define (make-particles-shader)
+  (strings->shader
+   "
+#version 130
+
+in vec2 position;
+in vec2 tex;
+in vec2 offset;
+in float life;
+out vec2 frag_tex;
+out float t;
+uniform mat4 mvp;
+uniform int lifetime;
+uniform int animationRows;
+uniform int animationColumns;
+
+void main(void) {
+    t = life / lifetime;
+    int numTiles = animationRows * animationColumns;
+    int tile = int(numTiles * (1.0 - t));
+    float tx = float(tile % animationColumns) / animationColumns;
+    float ty = float(tile / animationColumns) / animationRows;
+    float tw = 1.0 / animationColumns;
+    float th = 1.0 / animationRows;
+    frag_tex = vec2(tx, ty) + tex * vec2(tw, th);
+    gl_Position = mvp * vec4(position.xy + offset, 0.0, 1.0);
+}
+"
+   "
+#version 130
+
+in vec2 frag_tex;
+in float t;
+uniform sampler2D color_texture;
+uniform vec4 startColor;
+uniform vec4 endColor;
+
+void main (void) {
+    gl_FragColor = mix(endColor, startColor, t) * texture2D(color_texture, frag_tex);
+}
+"))
+
+(define (make-particles-vertex-array capacity width height texture buffer)
+  (let* ((indices (make-typed-buffer #:type 'scalar
+                                     #:component-type 'unsigned-int
+                                     #:divisor 0
+                                     #:buffer (make-buffer
+                                               (u32vector 0 3 2 0 2 1)
+                                               #:target 'index)))
+         (verts (make-typed-buffer #:type 'vec2
+                                   #:component-type 'float
+                                   #:divisor 0
+                                   #:buffer (make-buffer
+                                             ;; TODO: use the texture
+                                             ;; size in pixels.
+                                             (let ((hw (/ width 2.0))
+                                                   (hh (/ height 2.0)))
+                                               (f32vector (- hw) (- hh)
+                                                          hw (- hh)
+                                                          hw hh
+                                                          (- hw) hh))
+                                             #:target 'vertex)))
+         (tex (make-typed-buffer #:type 'vec2
+                                 #:component-type 'float
+                                 #:divisor 0
+                                 #:buffer (make-buffer
+                                           (let ((tex (texture-gl-tex-rect
+                                                       texture)))
+                                             (f32vector 0 0
+                                                        1 0
+                                                        1 1
+                                                        0 1))
+                                           #:target 'vertex)))
+         (pos (make-typed-buffer #:name "particle position buffer"
+                                 #:buffer buffer
+                                 #:type 'vec2
+                                 #:component-type 'float
+                                 #:length capacity
+                                 #:divisor 1))
+         (life (make-typed-buffer #:name "particle life remaining buffer"
+                                  #:buffer buffer
+                                  #:type 'scalar
+                                  #:component-type 'int
+                                  #:offset 24
+                                  #:length capacity
+                                  #:divisor 1)))
+    (make-vertex-array #:indices indices
+                       #:attributes `((0 . ,verts)
+                                      (1 . ,tex)
+                                      (2 . ,pos)
+                                      (3 . ,life)))))
+
+(define* (make-particles capacity #:key
+                         (blend-mode 'alpha)
+                         (start-color white)
+                         (end-color (make-color 0.0 0.0 0.0 0.0))
+                         (texture null-texture)
+                         (animation-rows 1)
+                         (animation-columns 1)
+                         (width (if (texture-null? texture)
+                                    8.0
+                                    (inexact->exact
+                                     (floor
+                                      (/ (texture-width texture)
+                                         animation-columns)))))
+                         (height (if (texture-null? texture)
+                                     8.0
+                                     (inexact->exact
+                                      (floor
+                                       (/ (texture-height texture)
+                                          animation-rows)))))
+                         (speed-range (vec2 0.1 1.0))
+                         (acceleration-range (vec2 0.0 0.1))
+                         (direction-range (vec2 0.0 (* 2 pi)))
+                         (lifetime 30)
+                         sort)
+  "Return a new particle system that may contain up to CAPACITY
+particles.  Achieving the desired particle effect involves tweaking
+the following keyword arguments as needed:
+
+- BLEND-MODE: Pixel blending mode.  'alpha' by default.
+
+- START-COLOR: The tint color of the particle at the beginning of its
+life.  White by default.
+
+- END-COLOR: The tint color of the particle at the end of of its life.
+Completely transparent by default for a fade-out effect.  The color in
+the middle of a particle's life will be an interpolation of
+START-COLOR and END-COLOR.
+
+- TEXTURE: The texture applied to the particles.  The texture may be
+subdivided into many animation frames.
+
+- ANIMATION-ROWS: How many animation frame rows there are in the
+texture.  Default is 1.
+
+- ANIMATION-COLUMNS: How many animation frame columns there are in the
+texture.  Default is 1.
+
+- WIDTH: The width of each particle.  By default, the width of an
+animation frame (in pixels) is used.
+
+- HEIGHT: The height of each particle.  By default, the height of an
+animation frame (in pixels) is used.
+
+- SPEED-RANGE: A 2D vector containing the min and max particle speed.
+Each particle will have a speed chosen at random from this range.  By
+default, speed ranges from 0.1 to 1.0.
+
+- ACCELERATION-RANGE: A 2D vector containing the min and max particle
+acceleration.  Each particle will have an acceleration chosen at
+random from this range.  By default, acceleration ranges from 0.0 to
+0.1.
+
+- DIRECTION-RANGE: A 2D vector containing the min and max particle
+direction as an angle in radians.  Each particle will have a direction
+chosen at random from this range.  By default, the range covers all
+possible angles.
+
+- LIFETIME: How long each particle lives, measured in updates. 30 by
+default.
+
+- SORT: 'youngest' if youngest particle should be drawn last or
+'oldest' for the reverse.  By default, no sorting is applied at all."
+  (let* ((stride (+ (* 4 2) ; position - 2x f32
+                    (* 4 2) ; velocity - 2x f32
+                    (* 4 2) ; acceleration - 2x f32
+                    4)) ; life remaining - 1x s32
+                 (buffer (make-buffer #f
+                                      #:name "packed particle data"
+                                      ;; One extra element to use as
+                                      ;; swap space for sorting
+                                      ;; particles.
+                                      #:length (* stride (+ capacity 1))
+                                      #:stride stride
+                                      #:usage 'stream)))
+    (%make-particles capacity
+                     0
+                     buffer
+                     (make-particles-shader)
+                     (make-particles-vertex-array capacity
+                                                  width
+                                                  height
+                                                  texture
+                                                  buffer)
+                     texture
+                     animation-rows
+                     animation-columns
+                     speed-range
+                     acceleration-range
+                     direction-range
+                     blend-mode
+                     start-color
+                     end-color
+                     lifetime
+                     sort
+                     '())))
+
+(define (update-particles particles)
+  "Advance the simulation of PARTICLES."
+  (let* ((buffer (particles-buffer particles))
+         (va (particles-vertex-array particles))
+         (pos (assq-ref (vertex-array-attributes va) 2))
+         (speed-range (particles-speed-range particles))
+         (acceleration-range (particles-acceleration-range particles))
+         (direction-range (particles-direction-range particles))
+         (sort (particles-sort particles))
+         (lifetime (particles-lifetime particles))
+         (float-ref bytevector-ieee-single-native-ref)
+         (float-set! bytevector-ieee-single-native-set!)
+         (int-ref bytevector-s32-native-ref)
+         (int-set! bytevector-s32-native-set!)
+         (y-offset 4)
+         (dx-offset 8)
+         (dy-offset 12)
+         (ddx-offset 16)
+         (ddy-offset 20)
+         (life-offset 24))
+    (with-mapped-buffer buffer
+      (let* ((bv (buffer-data buffer))
+             (stride (buffer-stride buffer))
+             (current-size (particles-size particles)))
+        ;; Remove particles in batches since often a bunch of
+        ;; contiguous particles die at the same time.
+        (define (kill-range start end len)
+          (when start
+            (bytevector-copy! bv len
+                              bv start
+                              (- end start))))
+        ;; Update existing particles, removing dead ones.
+        (let loop ((i 0)
+                   (len (* current-size stride))
+                   (kill-start #f))
+          (if (< i len)
+              (let ((life (- (int-ref bv (+ i life-offset)) 1)))
+                (cond
+                 ((<= life 0)
+                  (loop (+ i stride) (- len stride) (or kill-start i)))
+                 (kill-start
+                  (kill-range kill-start i len)
+                  (loop kill-start len #f))
+                 (else
+                  (let ((x (float-ref bv i))
+                        (y (float-ref bv (+ i y-offset)))
+                        (dx (float-ref bv (+ i dx-offset)))
+                        (dy (float-ref bv (+ i dy-offset)))
+                        (ddx (float-ref bv (+ i ddx-offset)))
+                        (ddy (float-ref bv (+ i ddy-offset))))
+                    (int-set! bv (+ i life-offset) life)
+                    (float-set! bv i (+ x dx))
+                    (float-set! bv (+ i y-offset) (+ y dy))
+                    (float-set! bv (+ i dx-offset) (+ dx ddx))
+                    (float-set! bv (+ i dy-offset) (+ dy ddy))
+                    (loop (+ i stride) len #f)))))
+              (if kill-start
+                  (begin
+                    (kill-range kill-start len len)
+                    (loop kill-start len #f))
+                  (set-particles-size! particles (/ len stride)))))
+        ;; Add particles from each active emitter and then remove
+        ;; emitters that have completed.
+        (let ((sx (vec2-x speed-range))
+              (sy (vec2-y speed-range))
+              (ax (vec2-x acceleration-range))
+              (ay (vec2-y acceleration-range))
+              (dx (vec2-x direction-range))
+              (dy (vec2-y direction-range))
+              (emitters (particles-emitters particles))
+              (len (- (bytevector-length bv) stride)))
+          (define (emit emitter any-done?)
+            (let* ((size (particles-size particles))
+                   (spawn-area (particle-emitter-spawn-area emitter))
+                   (rate (particle-emitter-rate emitter))
+                   (rx (rect-x spawn-area))
+                   (ry (rect-y spawn-area))
+                   (rw (rect-width spawn-area))
+                   (rh (rect-height spawn-area))
+                   (start (* size stride))
+                   (end (min (+ start (* rate stride)) len)))
+              (let loop ((i start))
+                (if (< i end)
+                    (let* ((speed (+ (* (random:uniform) (- sy sx)) sx))
+                           (accel (+ (* (random:uniform) (- ay ax)) ax))
+                           (dir (+ (* (random:uniform) (- dy dx)) dx))
+                           (dir-x (cos dir))
+                           (dir-y (sin dir)))
+                      (float-set! bv i (+ rx (* (random:uniform) rw)))
+                      (float-set! bv (+ i y-offset)
+                                  (+ ry (* (random:uniform) rh)))
+                      (float-set! bv (+ i dx-offset) (* dir-x speed))
+                      (float-set! bv (+ i dy-offset) (* dir-y speed))
+                      (float-set! bv (+ i ddx-offset) (* dir-x accel))
+                      (float-set! bv (+ i ddy-offset) (* dir-y accel))
+                      (int-set! bv (+ i life-offset) lifetime)
+                      (loop (+ i stride)))
+                    (begin
+                      (set-particles-size! particles (/ end stride))
+                      (update-particle-emitter emitter)
+                      (or any-done? (particle-emitter-done? emitter)))))))
+          (when (fold emit #f emitters)
+            (set-particles-emitters! particles
+                                     (remove particle-emitter-done? emitters))))
+        ;; Sort particles.
+        (when sort
+          (let ((compare (cond
+                          ((eq? sort 'young)
+                           (lambda (i j)
+                             (< (int-ref bv (+ i life-offset))
+                                (int-ref bv (+ j life-offset)))))
+                          ((eq? sort 'old)
+                           (lambda (i j)
+                             (> (int-ref bv (+ i life-offset))
+                                (int-ref bv (+ j life-offset)))))
+                          (else
+                           (error "unknown particle sorting method" sort))))
+                (tmp (* (particles-capacity particles) stride)))
+            (define (swap i j)
+              (bytevector-copy! bv i bv tmp stride)
+              (bytevector-copy! bv j bv i stride)
+              (bytevector-copy! bv tmp bv j stride))
+            ;; In the benchmarks I've done, insertion sort has
+            ;; performed much better than quicksort here.  The number
+            ;; of comparisons and swaps is much fewer.
+            (define (sort start end)
+              (let outer ((i (+ start stride)))
+                (when (< i end)
+                  (let inner ((j i))
+                    (when (and (> j start)
+                               (compare j (- j stride)))
+                      (swap (- j stride) j)
+                      (inner (- j stride))))
+                  (outer (+ i stride)))))
+            (sort 0 (* (particles-size particles) stride))))))))
+
+(define draw-particles*
+  (let ((mvp (make-null-matrix4)))
+    (lambda (particles matrix)
+      "Render PARTICLES with MATRIX applied."
+      (let ((size (particles-size particles))
+            (va (particles-vertex-array particles)))
+        (with-blend-mode (particles-blend-mode particles)
+          (with-texture 0 (particles-texture particles)
+            (gpu-apply/instanced (particles-shader particles)
+                                 va
+                                 size
+                                 #:mvp (if matrix
+                                           (begin
+                                             (matrix4-mult! mvp matrix
+                                                            (current-projection))
+                                             mvp)
+                                           (current-projection))
+                                 #:startColor (particles-start-color particles)
+                                 #:endColor (particles-end-color particles)
+                                 #:lifetime (particles-lifetime particles)
+                                 #:animationRows
+                                 (particles-animation-rows particles)
+                                 #:animationColumns
+                                 (particles-animation-columns particles))))))))
+
+(define (draw-particles particles)
+  "Render PARTICLES."
+  (draw-particles* particles #f))
index 6581ae7..490a532 100644 (file)
@@ -1277,6 +1277,7 @@ blocks to implement additional rendering techniques.
 * Tile Maps::                   Draw 2D tile maps.
 * Lines and Shapes::            Draw line segments and polygons.
 * Fonts::                       Drawing text.
+* Particles::                   Pretty little flying pieces!
 * Blending and Depth Testing::  Control how pixels are combined.
 * Vertex Arrays::               Create 2D/3D models.
 * Shaders::                     Create custom GPU programs.
@@ -1663,6 +1664,154 @@ Refer to @code{draw-sprite} (@pxref{Sprites}) for information about
 the other arguments.
 @end deffn
 
+@node Particles
+@subsection Particles
+
+Effects like smoke, fire, sparks, etc. are often achieved by animating
+lots of little, short-lived sprites known as ``particles''.  In fact,
+all of these effects, and more, can be accomplished by turning a few
+configuration knobs in a ``particle system''.  A particle system takes
+care of managing the many miniscule moving morsels so the developer
+can quickly produce an effect and move on with their life.  The
+@code{(chickadee render particles)} module provides an API for
+manipulating particle systems.
+
+Below is an example of a very simple particle system that utilizes
+nearly all of the default configuration settings:
+
+@example
+(use-modules (chickadee render particles))
+(define texture (load-image "particle.png"))
+(define particles (make-particles 2000 #:texture texture))
+@end example
+
+In order to put particles into a particle system, a particle
+``emitter'' is needed.  Emitters know where to spawn new particles,
+how many of them to spawn, and for how long they should do it.
+
+Below is an example of an emitter that spawns 16 particles per frame
+at the coordinates @code{(320, 240)}:
+
+@example
+(use-modules (chickadee math vector))
+(define emitter (make-particle-emitter (vec2 320.0 240.0) 16))
+(add-particle-emitter particles emitter)
+@end example
+
+To see all of the tweakable knobs and switches, read on!
+
+@deffn {Procedure} make-particles @var{capacity} [#:blend-mode @code{alpha}] @
+       [#:color white] [#:end-color transparent] [#:texture] @
+       [#:animation-rows 1] [#:animation-columns 1] [#:width] [#:height] @
+       [#:speed-range (vec2 0.1 1.0)] [#:acceleration-range (vec2 0.0 0.1)] @
+       [#:direction-range (vec2 0 (* 2 pi))] [#:lifetime 30] [#:sort]
+
+Return a new particle system that may contain up to @var{capacity}
+particles.  Achieving the desired particle effect involves tweaking
+the following keyword arguments as needed:
+
+- @var{blend-mode}: Pixel blending mode.  @code{alpha} by default.
+(@pxref{Blending and Depth Testing} for more about blend modes).
+
+- @var{start-color}: The tint color of the particle at the beginning of its
+life.  White by default.
+
+- @var{end-color}: The tint color of the particle at the end of of its
+life.  Completely transparent by default for a fade-out effect.  The
+color in the middle of a particle's life will be an interpolation of
+@var{start-color} and @var{end-color}.
+
+- @var{texture}: The texture applied to the particles.  The texture
+may be subdivided into many animation frames.
+
+- @var{animation-rows}: How many animation frame rows there are in the
+texture.  Default is 1.
+
+- @var{animation-columns}: How many animation frame columns there are
+in the texture.  Default is 1.
+
+- @var{width}: The width of each particle.  By default, the width of
+an animation frame (in pixels) is used.
+
+- @var{height}: The height of each particle.  By default, the height
+of an animation frame (in pixels) is used.
+
+- @var{speed-range}: A 2D vector containing the min and max particle
+speed.  Each particle will have a speed chosen at random from this
+range.  By default, speed ranges from 0.1 to 1.0.
+
+- @var{acceleration-range}: A 2D vector containing the min and max
+particle acceleration.  Each particle will have an acceleration chosen
+at random from this range.  By default, acceleration ranges from 0.0
+to 0.1.
+
+- @var{direction-range}: A 2D vector containing the min and max
+particle direction as an angle in radians.  Each particle will have a
+direction chosen at random from this range.  By default, the range
+covers all possible angles.
+
+- @var{lifetime}: How long each particle lives, measured in
+updates. 30 by default.
+
+- @var{sort}: @code{youngest} if youngest particle should be drawn
+last or @code{oldest} for the reverse.  By default, no sorting is
+applied at all.
+@end deffn
+
+@deffn {Procedure} particles? @var{obj}
+Return @code{#t} if @var{obj} is a particle system.
+@end deffn
+
+@deffn {Procedure} update-particles @var{particles}
+Advance the simulation of @var{particles}.
+@end deffn
+
+@deffn {Procedure} draw-particles @var{particles}
+Render @var{particles}.
+@end deffn
+
+@deffn {Procedure} draw-particles* @var{particles} @var{matrix}
+Render @var{particles} with @var{matrix} applied.
+@end deffn
+
+@deffn {Procedure} make-particle-emitter @var{spawn-area} @
+       @var{rate} [@var{duration}]
+
+Return a new particle emitter that spawns @var{rate} particles per
+frame within @var{spawn-area} (a rectangle or 2D vector) for
+@var{duration} frames.  If @var{duration} is not specified, the
+emitter will spawn particles indefinitely.
+@end deffn
+
+@deffn {Procedure} particle-emitter? @var{obj}
+Return @code{#t} if @var{obj} is a particle emitter.
+@end deffn
+
+@deffn {Procedure} particle-emitter-spawn-area @var{emitter}
+Return the spawn area for @var{emitter}.
+@end deffn
+
+@deffn {Procedure} particle-emitter-rate @var{emitter}
+Return the number of particles that @var{emitter} will spawn per
+frame.
+@end deffn
+
+@deffn {Procedure} particle-emitter-life @var{emitter}
+Return the number of frames remaining in @var{emitter}'s lifespan.
+@end deffn
+
+@deffn {Procedure} particle-emitter-done? @var{emitter}
+Return @code{#t} if @var{emitter} has finished spawning particlces.
+@end deffn
+
+@deffn {Procedure} add-particle-emitter @var{particles} @var{emitter}
+Add @var{emitter} to @var{particles}.
+@end deffn
+
+@deffn {Procedure} remove-particle-emitter @var{particles} @var{emitter}
+Remove @var{emitter} to @var{particles}
+@end deffn
+
 @node Blending and Depth Testing
 @subsection Blending and Depth Testing