The Rendering Loop
This page contains advanced, low-level information for people who are ready to create their own custom effect algorithms. Not to scare you off, but don’t be discouraged if you are new to Afterglow, and this content seems rather difficult. Start experimenting in other places, and by the time you need this information, it will make much more sense!
And once you are ready to really dive deep, you can learn how to extend the rendering loop to incorporate completely new kinds of elements, such as integrating with Pangolin Beyond laser shows.
Frame Rendering Stages
When an afterglow show is running, that is, from when
(show/start!)
has been called, until
(show/stop!)
or
(show/stop-all!)
is called, there is a background task scheduled to run many times per
second, to calculate the next “frame” of control values to send to the
universes controlled by the show, and then send those values. The rate
at which this activity is scheduled is determined by the
refresh-interval
value established when the show was
created.
If not explicitly set as a parameter to (show/show)
, an interval of
25 milliseconds is used, causing the lights to be updated forty times
each second. If your DMX interface is running at a different rate, you
will want to configure your show to match it, so that you are getting
the best results possible without wasting computation on frames that
never get seen.
Once a show has started running, you can get a sense of how heavily it
is taxing your hardware by looking at the show’s :statistics atom:
|
(clojure.pprint/pprint @(:statistics *show*))
; {:afterglow-version "0.1.0-SNAPSHOT",
; :total-time 70429,
; :frames-sent 105828,
; :average-duration 0.6655044,
; :recent #amalloy/ring-buffer [30 (0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 1 0 0 1)],
; :recent-total 6,
; :recent-average 0.2}
; -> nil
This tells you how many milliseconds have been spent in the rendering loop while the show is running, how many frames of DMX data have been sent to the show universes, and the average number of milliseconds spent in the rendering loop. If the duration of a rendering cycle ever exceeds the refresh interval, Afterglow will log a warning that it is unable to keep up with the effects you are trying to run.
If you have a show’s web interface open, the
:recent
keys in this atom are used to display the Load bar at the top of the screen, which fills up and turns red as the time within each frame available for calculating and sending control values to the lights gets used up.
When it is time to determine the next set of values to send to the show’s patched fixtures, this is what happens:
-
The first thing Afterglow does is take a snapshot of the show metronome to identify a common point in time for all the effects to use in deciding how they should look. This enables a consistent, coordinated appearance for everything in the frame.
-
The next step is to zero out the buffers that will be used to send DMX data for each universe, so that if no effect tries to set a value for a particular channel on this frame, a zero value will be sent for it.
-
Then Afterglow loops over all Effects that are currently active for the show, and asks if any are ready to end, as described in the Effect Lifecycle section. If any are, they are removed from the list of active effects.
-
Then it makes another loop over any effects that did not end, to see how they want to affect the lights. It does this by calling, in order, each effect’s
generate
function, passing in the show and the metronome snapshot. In order to make the lights do things, each effect returns a list of Assigners specifying the things it wants to happen. These are gathered in the order they were returned by each effect. -
While being gathered, the assigners are separated into individual lists, divided first by the
kind
value of the assigner (:channel
,:function
,:color
,:pan-tilt
,:direction
, or:aim
, or some other value added by a rendering loop extension), and within each kind, further divided into lists based on the target ID that the assigner wants to affect. The structure of a target ID is up to the Assigner kind, and may be a simple number or tuple, depending on the needs of the Assigner implementation; the important thing from the Show’s perspective is that if two target IDs are equal to each other, the assigners are affecting the same element of the show.
Since the effects are run in priority order (lower priority first, with effects of the same priority running in the order in which they were added to the show), higher-priority and more recent effects’ assigners will come later in the gathered lists, and will get the chance to modify or veto any assigners from earlier and lower-priority effects which are trying to control the same thing. If the effect doesn’t want to do anything this frame, it can simply return an empty assigner list. -
Once all the assigners have been collected into their kind/target lists, each list is evaluated. The assigner kinds are processed in the order established by
show/resolution-order
and starts with low-level, single channel:channel
assigners, then moves up to more complex:function
,:color
,:pan-tilt
,:direction
, and:aim
assigners. (This order was chosen because different kinds of assigners might end up affecting the same DMX channel; the higher levels of abstraction are allowed to win by running last.) Within each kind, however, the lists can be processed in parallel, since they will all affect separate targets.To process a list, each assigner’s
assign
function is called, again passing in the show and the metronome snapshot, the target which is being assigned (a DMX channel in a show universe, or a fixture or fixture head, depending on the assigner kind), as well as the assignment value the effect wanted to establish (a number, color, or head direction, again depending on the assigner type), and the previous assignment (if any) that an earlier assigner wanted to set for this target. The assigner can decide what to do with the previous assignment: Ignore it, blend the current assignment with it somehow, or honor it, depending on the nature and configuration of the assigner. Theassign
function returns a single resolved value of the appropriate type for the assignment, and Afterglow records it, potentially to pass it to the next assigner on the list. The assigner can also veto any previous assignment and say that nothing should happen by returning an assignment withnil
for its value.The input to
assign
might be a Dynamic Parameter, and the resulting value may be as well, or the assigner may choose to resolve it into a non-dynamic value, in order to decide between or blend competing assignments.At the end of this process, Afterglow is left with a single assigned value for every target which any effect wanted to influence for the current frame.
-
Afterglow uses these assignment results to establish actual DMX values for the frame, using the
resolve-assignment
multimethod, whose implementations (which are specific for each of the possible assignment kinds) finally resolve any remaining dynamic parameters, and then turn abstractions like color objects and aiming vectors into appropriate DMX channel values for the target that is being assigned. -
Finally, the resulting buffers of DMX values, with zeros in any channels which were not affected by assigners, are sent to their corresponding universes, causing the lights to produce the desired effects.
Assigners
As described above, the role of an assigner in the rendering loop is to actually decide what value (color, direction, or the like) is going to be sent to a target (a lighting fixture head for more abstract assigners, or a simple DMX channel for Channel assigners), at a given point in time. It really is the heart of implementing an effect.
The assigner fulfills this responsibility by implementing the
assign
function in the
IAssigner
protocol. It is passed the show, the metronome snapshot which
identifies the point in musical time that has been reached, the target
being assigned, and the value that any earlier assigners of the same
type have decided should be assigned to the target.
It performs its magic, using the values established in setting up the effect, and the algorithm that the effect author designed, to come up with the resulting value that it wants assigned to the target, which may or may not be influenced by the previous assignment, and returns that value for Afterglow to either use, or pass on to the next assigner of that type in the effect chain.
The best way to understand this is probably to look at examples of
effects that ship with Afterglow, starting with simple ones like
color-effect
,
dimmer-effect
,
and
direction-effect
,
then slightly more complex
strobe
and
sparkle
effects, and on up to more sophisticated compound effects like
color-cycle-chase
,
and the spatially mapped elaborations of it like
iris-out-color-cycle-chase
.
Once you can understand how all of those pieces fit together, you will be ready to build your own complex and mesmerizing effects!
Channel Assigners
Channel assigners have a kind
of :channel
, and their target-id
is a tuple of universe ID and channel address, so [1 234]
would
represent an assignment to universe 1
, address 234
. The assignment
values they return are either a valid DMX data value (see next
paragraph), a dynamic parameter which will
resolve to a valid DMX data value, or nil
, meaning no assignment
should take place.
The DMX data value is a number in the range [0-256)
.
In other words, it can take any value from zero up to but not reaching
256. Non-integer values are supported, because the channel might be a
fine-channel which uses
two bytes to offer more precision in control than a single byte can
offer. In that case, the integer portion of the value is sent as the
most-significant byte on the main channel, and the fractional portion
is converted to a least-significant byte and sent on the fine channel.
If the channel does not have a fine channel attached to it, any
fractional part of the assigned value is simply discarded.
Channels can also be inverted, which means the DMX values are
reversed from the value being assigned. This is needed to support some
fixtures which have inverted dimmers, is established by the presence
of an :inverted-from
entry in the
channel specification,
and taken care of by
apply-channel-value
,
which is invoked by the channel assignment resolver, so channel
assigners do not need to worry about this detail, and can always work
in terms of non-inverted channel values. (This is important, for
example, when implementing highest-takes-precedence rules for a dimmer
channel. Bigger numbers will always mean brighter, even if at the last
step before sending them to the fixture they are inverted because of
the nature of the channel.)
Function Assigners
Function assigners have a kind
of :function
, and their target-id
is a tuple of the head or fixture ID and the function keyword, so [3
:strobe]
would represent an assignment to the fixture or head with ID
3, setting the value of that head’s :strobe
function. The
assignment values they return are either a percentage value, a
dynamic parameter which will resolve to a
percentage value, or nil
, meaning no assignment should take place.
When the assignment is resolved, the percentage is translated to an actual DMX value along the range defined in each fixture’s function specification. For example, if the function was defined as existing on the range 20-29 for a particular fixture, and the assigned percentage was 50.0, then the assignment for that fixture would send a value of 25 to the function’s channel.
Color Assigners
Color assigners have a kind
of :color
, and their target-id
is
the head or fixture ID; 42
would represent an assignment to the
fixture or head with ID 42. The assignment values they return are
either a color object, a
dynamic parameter which will
resolve to a color object, or nil
, meaning no assignment should take
place.
When the assignment is resolved, Afterglow uses all available color
channels in the target head to mix the specified color. It is
automatically able to use :color
intensity channels of type :red
,
:green
, :blue
, and :white
. It will also use any other :color
channels whose hue has been
specified in the fixture
definition.
If the head or fixture uses a color wheel to make colors, rather than
trying to mix colors using channel intensities, Afterglow will find
the color wheel hue
closest to the hue of the color being assigned, and send the function
value needed to set the color wheel to that position. The color wheel
hue has to be “close enough” to the assigned hue for
Afterglow to use it. By default, as long as the hue values are within
60° of each other (which is very lenient), Afterglow will use it.
You can adjust this tolerance by setting a different value in the show
variable :color-wheel-hue-tolerance
. The color being assigned must
also have a saturation of at least 40% for the color wheel to be
considered (this minimum saturation can be adjusted by setting a
different value in the show variable :color-wheel-min-saturation
).
Pan/Tilt Assigners
Pan/Tilt assigners have a kind
of :pan-tilt
, and their target-id
is the head or fixture ID; 68
would represent an assignment to the
fixture or head with ID 68. The assignment values they return are
either a javax.vecmath.Vector2d
, a
dynamic parameter which will
resolve to a Vector2d
object, or nil
, meaning no assignment should
take place.
When the assignment is resolved, the vector indicates the pan and tilt
angles away from the z
axis of the frame of
reference of the show to aim the fixture or head. Afterglow
translates this vector to the appropriate values to send to the
fixture’s pan and tilt channels to aim it in the specified direction,
if possible. Otherwise it gets as close as the fixture allows.
If multiple fixtures or heads are assigned the same pan-tilt vector, they will all be aimed in exactly the same direction, regardless of the location and orientation with which they were hung.
If there is an active Direction or Aim Assigner which affects the same target, it will run later, so its effects will be the ones that matter. |
Direction Assigners
Direction assigners have a kind
of :direction
, and their
target-id
is the head or fixture ID; 42
would represent an
assignment to the the fixture or head with ID 42. The assignment
values they return are either a javax.vecmath.Vector3d
, a
dynamic parameter which will
resolve to a Vector3d
object, or nil
, meaning no assignment should
take place.
When the assignment is resolved, the vector indicates the direction in the frame of reference of the show to aim the fixture or head. Afterglow translates this vector to the appropriate values to send to the fixture’s pan and tilt channels to aim it in the specified direction, if possible. Otherwise it gets as close as the fixture allows.
If multiple fixtures or heads are assigned the same direction vector, they will all be aimed in exactly the same direction, regardless of the location and orientation with which they were hung.
If there is an active Aim Assigner which affects the same target, it will run later, so its effects will be the ones that matter. |
Aim Assigners
Aim assigners have a kind
of :aim
, and their target-id
is the
head or fixture ID; 17
would represent an assignment to the fixture
or head with ID 17. The assignment values they return are either a
javax.vecmath.Point3d
, a dynamic
parameter which will resolve to a Point3d
object, or nil
,
meaning no assignment should take place.
When the assignment is resolved, the point identifies the precise location in the frame of reference of the show to aim the fixture or head. Afterglow translates this point to the appropriate values to send to the fixture’s pan and tilt channels to aim it at that exact spot, if possible. Otherwise it gets as close as the fixture allows.
If multiple fixtures or heads are assigned the same aiming point, they will all be aimed at exactly the same spot, regardless of the location and orientation with which they were hung.
Extensions
If you want Afterglow to control something that does not respond to
DMX values, you might be able to do so by extending the rendering
loop. There is an example of doing just this to control laser shows by
communicating with Pangolin’s Beyond software in the
afterglow.beyond
namespace, and another example in
afterglow.effects.show-variable
,
which creates effects that set show variables when they are run.
Introducing New Assigner Types
The first thing you need to do is identify the kinds of assigners that
your new effect types will need. They will need their own unique
kind
keywords, and a structure for their target-id
values which
lets Afterglow keep track of which assigners are affecting the same
value. The Beyond integration uses :beyond-color
and :beyond-cue
for kind
values. :beyond-color
is global, and thus uses a
target-id
that references the entire Beyond server instance. In
contrast, more than one :beyond-cue
can be active at once, so its
target-id
is composed of both the server ID and the cue coordinates.
Afterglow needs to be told how to handle your new kinds of assigners.
First, you need to establish the order in which they should be run by
calling
show/set-extension-resolution-order!
with your unique extension key and the list of all your assigner types
in the order in which they should be resolved. You need to do this
even if you don’t care about the order, or have only one new assigner
type, in order to get them added to stage 6 of the frame rendering
process, as described above. This is done towards the end of the
Beyond extension source, if you would like to see a concrete example.
Then you need to tell afterglow how to actually resolve one of your
assigners. You do this in the same way Afterglow registers its own
built-in assigners, by using defmethod
to add a new implementation
of the
resolve-assignment
multimethod, for your new assigner keyword. Again, the end of the
Beyond integration provides a concrete example.
Customizing Fades for your Assigner Types
If you want to support smooth fades between different values being
returned by your assigners, you will also want to defmethod
an
implementation of the
fade-between-assignments
multimethod. This is the last thing that the Beyond integration does.
If you do not provide an implementation of
fade-between-assignments tailored to your specific assigner kind ,
the default implementation is used: it simply selects whichever
assigner is on the side of the fade which is currently above 50%.
|
Buffering and Sending Your Frame Data
Chances are good that your extension will need to do some sort of
setup at the start of a frame before your assigners can be resolved,
and then will want to actually do something when the frame is rendered
and being sent to the lights. To accomplish these tasks, you register
functions with a show:
add-empty-buffer-fn!
tells the show to call the supplied function when a frame is about to
be rendered, allowing you to set up any buffers your assigners will
need, and
add-send-buffer-fn!
tells the show to call the supplied function when it is time to
actually send out the frame. The Beyond integration calls these in its
bind-to-show
function.
Having done all these things, it becomes possible to create cues which
launch or end Beyond laser cues, and effects which change the color of
the laser beam to match (or contrast with) colors being sent to the
lights, as well as effects which simply set show variables so that
other effects can respond to the fact that they are running. Perhaps
looking at these example implementations can help inspire your own
extension in a completely new direction! (Links to the namespaces' API
documentation are at the top of this section, and as
always, the API docs have view source
buttons which take you right
to the code that makes them work.)