summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Thompson <dthompson2@worcester.edu>2018-12-04 21:50:55 -0500
committerDavid Thompson <dthompson2@worcester.edu>2018-12-07 23:01:40 -0500
commit5dbd832c3ce65ff29232b4f4642e3d713bb1a7ec (patch)
tree7765d4465c942ef056f1c05c0936842154db3618
parent36c24d256316e9ccc774cff19117c592a7d12fca (diff)
Add particle rendering module.
* chickadee/render/particles.scm: New file. * Makefile.am (SOURCES): Add it. * doc/api.texi (Particles): New subsection.
-rw-r--r--Makefile.am1
-rw-r--r--chickadee/render/particles.scm476
-rw-r--r--doc/api.texi149
3 files changed, 626 insertions, 0 deletions
diff --git a/Makefile.am b/Makefile.am
index 2b27ba2..2933194 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -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
index 0000000..9d15ecd
--- /dev/null
+++ b/chickadee/render/particles.scm
@@ -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))
diff --git a/doc/api.texi b/doc/api.texi
index 6581ae7..490a532 100644
--- a/doc/api.texi
+++ b/doc/api.texi
@@ -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