Coding DeltaFields for G-Force: a tutorial

by Howard A. Landman


Introduction

What is G-Force?

G-Force is a music visualization system that can run standalone or as a plug-in for several popular jukebox programs such as WinAmp and iTunes. It is expandable: you can write your own configs for it, making it capable of things that its designer did not explicitly design into it. You can get the latest version from the G-Force home page.

What is a DeltaField?

The images produced by G-Force are generated by several independent components, collectively known as "configs": This page is a tutorial on writing DeltaFields only. You'll have to figure the other parts out from the G-Force documentation, or by looking at existing configs.


Parts of a DeltaField

Every DF is built from a few simple elements. Each element looks like an assignment statement in a programming language:

	LHS=RHS,

The Left Hand Side (LHS) tells you what is being given a value. The Right Hand Side (RHS) specifies the value to give it. These are separated by an equals sign with no white space on either side. Finally, each statement except the last ends with a comma to separate it from the next one. I'm not sure how robust the parser is, so it's best to keep each statement as one line.

There are two flavors of LHS. Some of them only take a number for a RHS. Others only take expressions enclosed in double quotes. Don't confuse the two.

Comments

DeltaFields (and other configs) use C++ style comments. Anything that follows "//" (two adjacent slashes) on a line is treated as a comment, and is discarded along with the slashes themselves. It's usually a good idea to put lots of comments in your configs, to identify them and to help other people understand what you did so they can write their own.

A comment can be on a line by itself, or can follow a statement. (It obviously can't precede a statement, because then the statement would get treated as part of the comment and be thrown out.)

Aspect Ratio

        Aspc=0,

or

        Aspc=1,

The first component of any DeltaField is the aspect ratio statement. It controls how the DF sees the screen coordinates when the screen is not perfectly square (i.e. almost all the time). Aspc can be set to zero or one.

If the aspect ratio is 0, then the lower left corner of the screen is at X=-1,Y=-1, and the upper right corner is at X=1,Y=1, no matter what the actual shape of the screen. I often use Aspc=0 for DFs that I want to fit the screen fully. The advantage is that, once you get the DF tuned to fit the screen at one size and shape, it will always still fit the screen at every other size and shape too. The disadvantage is that your DF will change shape if the screen is a different size than what you expected. For example, you might design something circular, and on someone else's machine it will become elliptical.

If the aspect ratio is 1, then the shortest dimension of the screen (usually the vertical or Y dimension) will get the range -1 to +1, and the other dimension will get whatever range it needs for each pixel to be "square". So for example on a 640x480 screen, Y would run from -1 to +1, but X would run from -640/480 = -4/3 to +4/3. The advantage is that shapes will be preserved, and the central square of the screen will look exactly as you intended, but the area outside of that square will be of different sizes on different machines and can sometimes be hard to predict.

As a general rule of thumb, use Aspc=1 if your DF creates a simple or regular pattern whose behavior outside of the central square can be easily predicted and is acceptable to you, or if you want to preserve shape exactly. Use Aspc=0 if you want your DF to always fill up the same fraction of the screen or have the same relationship to the edges of the screen.

Note that Aspc takes only a single digit without quotes! Also remember that it should always be the first statement in the DF.

A Variables

G-Force has several classes of temporary variables that you can use to hold intermediate values. The only ones you want to use in a DeltaField are the A and D variables.

A variables are only evaluated once, when the DF loads. (They will get re-evaluated again if the DF loads again, but that usually would be many minutes later.) They can use any of the built-in functions except for the ones related to the sound or to the location of the pixel. They can also use other A variables which have previously been set.

One curious thing is that, when you're setting a variable, you use its name in upper case, but when you're using it later, you use its name in lower case. This can be confusing at first. But here's a simple example:

        A0="rnd(1)",  // a random number between 0 and 1
A1="1-a0",

Because the A variables only get evaluated once, they should only be used for computation that is the same for every pixel. Also, for clarity, you should group all your A variables together and put them before your D variables (which get evaluated later).

D Variables

D variables are similar to A variables, except that they get re-evaluated for every pixel. Also, they can use information about the location of the pixel in the form of x, y, r, and theta. Remember that the center of the screen is always 0,0, and that x and y range over at least -1 to +1 (more if Aspc=1 and it's the long dimension of the screen).

D variables are not required. It is OK for your DF not to have any.

Source Variables

Now we come to the heart of the DeltaField, the Source variables. They tell what pixel's old value will become this pixel's new value (minus a bit of decay in intensity). You absolutely must specify the source location - that's the whole point of a DF. But there are two possible ways to do so. To use normal X and Y coordinates, assign values to srcX and srcY. To use polar coordinates, assign values to srcR and srcT. Never mix and match (although you can use x, y, r, and theta freely in the RHS expression).

I'd like to emphasize the point that the DeltaField specifies where flowing data comes from, not where it goes to. It's easy for a beginner to think the opposite. Each pixel on the screen gets a single entry in the DF table after it's computed, so it can only come from one place. However, multiple pixels can easily grab their data from the same place. In math terms, a DF can implement a one-to-one mapping or a one-to-many mapping, but not a many-to-one or many-to-many mapping. (Our Most Assiduous Reader will note that most fractals are many-to-one mappings, which I've just said G-Force DeltaFields cannot implement, and yet I myself have written DFs for Julia sets and other fractals. Be patient, all will become clear in a bit.)

It doesn't matter which order you specify srcX and srcY, or srcR and srcT, but they should always be after any A or D variables.

Version

Finally, at the very end of the DF, you specify what version of G-Force the DF was written for. Presumable Andy uses this for various black magic to ensure compatibility if he changes the way G-Force behaves. The Vers statement, like Aspc, takes a number without quotes, but here it is the version number multiplied by 100. For example, if you wrote the DF for G-Force 2.09, you would put:

        Vers=209

Many people just put 100, assuming that their DF will be backwards compatible with earlier versions of G-Force. This is presumptuous unless they have actually tested the DF with those versions. So, I don't recommend this practice, although many of my early DFs do it (I was coding by example).


Examples

Now that we have all the components, let's look at a few simple DFs.

Example DF 1: Nowhere

The simplest possible DeltaField is the one where each pixel gets its intensity from itself. This DF is included in the G-Force 2.0 distribution under the name Nowhere:

        Aspc=0,

        srcX="x",
        srcY="y",

        Vers=100

Even though nothing flows in this DF, it can still look interesting if the WaveShape drawing onto it moves.

An alternate way of coding Nowhere would use:

        srcR="r",
        srcT="theta",

Example DF 2: Go West

Now suppose we want to make a DeltaField where everything flows slowly to the left. Because the Y coordinate doesn't need to change, we can keep the srcY line from Nowhere. But obviously the srcX needs to change.

Recall that srcX specifies the X coordinate of the pixel we get our intensity from. So, if we want intensity to flow to the left, it must be coming from the right, or the direction of more positive X. So we can get a constant rate of flow, the same for all pixels, by specifying:

        srcX="x + 0.01",

This will cause each pixel to get its data from a pixel that is slightly to its right, and so patches of intensity will creep slowly leftward. How slowly? Well, it depends on the framerate and the size of the screen. For a square screen (or if Aspc=0), the screen width is 2.0, so it will take about 200 frames for something to go from the right edge to the left edge. If the frame rate is 25 frames per second, that should take about 8 seconds. But you usually don't need to be making calculations like that; it's enough to know that increasing the number will make it flow faster and decreasing it will make it flow slower. Once you have a DF more or less working, you can play with the parameters until it feels right.


Functions

Since DeltaFields are mathematical in nature, G-Force provides a variety of math functions for you to use as building blocks. In this section we'll look at each one of these and how to use it. (Note: I group some functions differently here than Andy does in the G-Force documentation.)

If you had trouble with some of these functions in high school (or if you haven't gone to high school yet :-), don't worry. It's actually a lot easier to understand them when you can see what they're doing, and G-Force gives you a simple and beautiful way to do that. You may find that you like trigonometry a lot better when it's making pretty pictures for you than you did when it was just squiggles on a blackboard.

Operators

All these are infix operators - you use them by putting them in between the two values on which they operate, e.g. "a + b", "a ^ b".

+Add a and b, return the sum
-Subtract b from a, return the difference
*Multiply a times b, return the product
/Divide a by b, return the quotient
%a modulo b, divide a by b and return the remainder. (This may only work well if a and b are integers, not sure.)
^Raise a to the b power. Equivalent to exp(b*log(a)) except that a=0 is legal (log(0) is undefined).

Note that there is also the so-called "unary minus" operator, which is a minus sign put in front of a number or variable. Although this uses the same character as the binary subtraction operator, in practice it's hard to confuse them. "-b + a" is the same thing as "a - b".

Parentheses

The binary operators above can lead to some confusion when chained together. For example, consider the expression "a - b - c". Should this be understood as "(a-b) - c"? Or should it instead be understood as "a - (b-c)" which is equal to "(a-b) + c"? Whenever you have any doubt about how something might be interpreted, use parentheses to make sure.

This is especially important when using operators which have differing priorities. Traditionally, exponentiation "binds tighter" than multiplication and division, which in turn bind tighter than addition and subtraction. So for example "a*b+c" is understood to mean "(a*b) + c", and "a*b^c" to mean "a * (b^c)". If you really want "a*(b+c)", you must use the parentheses. (Well, you could write "a*b+a*c", but this takes two multiplications instead of one and will make your DF load more slowly because the CPU will have to do more work to interpret it.) The rest of the functions either come with their own parentheses, or don't even take an argument, so they're not subject to this sort of confusion.

Clipping and Wrapping

abs(x)Absolute value. x if x>0 and -x if x<0. Equivalent to x*sgn(x).
sgn(x)Sign of x. 1 if x≥0 and -1 if x<0.
clip(x)0 if x<0, x if 0<x<1, and 1 if x>1.

More soon ...


Copyright ©2001 Howard A. Landman / howard@polyamory.org
Last updated 2001 September 27