Table of Contents
- Engine Analysis
III) The Real Details
IV) How Transforms Work
- Tips and Tricks
- Using Xaos
- Flame vs. Fractal
- Final Remarks
For a long time, how fractals were created was very confusing to me. Regardless of the number of tutorials I looked at and the various explanations given, I could never really understand what was going on. So I decided that one day I would dive into the code and figure out just what was going on. What I discovered turned out to be very simple, very complicated, and very messy. While I have not intention of abandoning my own fractal program project, at least understanding how other programs work has already helped me become a better fractal artist.
As evidence that I’m getting better, I designed a fractal butterfly from only two XForms and NO final transform:
Naturally, of course, this butterfly can fly:
But enough with the talk, let’s open up the engine.
The program I’ll be examining is my favorite: JWildfire. The algorithm is inspired by (and mostly similar to) the Flam3 algorithm developed by Scott Draves (and from which we get original/raw fractal flames), but since JWildfire is written in Java and Andreas Maschke, its creator, has taken some liberties with the design, there are some differences in the code.
To get a basic understanding of JWildfire’s rendering process from the code yourself, you’ll need to read the following files, all of which are located in the JWildfire-master folder:src/org/jwildfire/create/tina/render
Note that JWildfire’s renderer is called “tina”, so anything related to rendering will be in that folder.
Files to read:
- A renderer (thread files), such as FlameRenderFlatThread.java. All are located in the subfolder “render”.
- The X-form class, path: base/XForm.java
- The XYZPoint class, path: base/XYZPoint.java
Each fractal is rendered from a set of points extracted from a square area at the center of your screen. It isn’t necessary to use all of them, so the renderer only extracts one random point every few. This square area is determined by the zoom and the number of points taken from it is determined by the setting “Pixels per unit”.
When you render a fractal, here are the steps the machine performs:
1) Take one random point from within the starting square area and give it to an X-form.
X-forms are sets of variations/transforms. All points passed to them are first modified by a matrix transformation (called the “pre-transform” or just simply “transform”).
2) For each successive variation in the X-form, pass the point to the variation.
3) Modify the point with the “post-transform” (a matrix transform) of the X-form.
4) If the X-form is to be drawn:
4a) Pass the point to the final transform X-form and repeat all steps but this one (obviously).
4b) Render the point to the screen. Here, it undergoes modification of its 3D coordinates to modify the gamma (“brightness” as far as we’re concerned) and make the final image appear 3D.
Points that have been passed to an X-form of draw mode “Opaque” are only drawn if the random number generator decides to give a number higher than the opacity setting of the X-form.
5) Pass the point to another X-form, randomly chosen from all of the other X-forms based on their weight and “xaos” (“weight” relative to the current X-form).
The Real Details
Now that you know the basic plan, here’s how things actually work.
The function we’re primarily concerned with is “iterate()” in the renderer file (e.g. FlameRenderFlatThread.java).
The first thing that happens is a call to preFuseIter(). This function is called periodically, but what it does is randomly generate a point between 1 and -1 on both the x and y axis. The z-axis value is set to zero.
If you read the code, you will see the variables “p” (or “pA”) and “q” (or “qA”), depending on the renderer code you analyze. These respectively correspond to the source point (the starting point, which becomes the pAffineTP after being modified by pre-transformation matrix of the X-form) and the destination point (the point drawn by the renderer, which comes from pVarTP and is modified by the post-transformation matrix of the X-form). Note that “p” is used for both source and destination point for normal drawing, whereas “q” is only used for drawing the point from the final transform.
The current X-form, stored in the variable “xf”, is set to the first X-form and it modifies the initial point by having its pre-transform, variations, and post-transform modify it. “xf” is set to the next X-form (or the first one if none are available) and the process is repeated for a set number of iterations, resulting in what should be a moderately random point.
HOWEVER, this whole process is only called every 10000 points or whenever the affine point is practically at infinity or out of range every 100 iterations.
Once preFuseIter() is done, we have a starting point, and thereafter, you don’t need to worry about the randomness of preFuseIter(). Frankly, I don’t know why it’s there other than to fix out-of-bounds points.
The heart of operations is what happens thereafter: all according to the logic.
Randomly select the next X-form form the table of X-forms. The code itself says Constants.NEXT_APPLIED_XFORM_TABLE_SIZE,
but I doubt this number is constant. It equals the number of X-forms you have. Edit: Each x-form has a 127 or so slot array (the “table”) for other x-forms. If the next x-form from this table refuses the point, the first x-form is given the point.
Now, immediately the variation/transform is applied to the point. This is called by XForm::transformPoint(context, affineTP, varTP, source point, destination point), in the file XForm.java.
This X-form function first modifies the source point by the pre-transform of the X-form and assigns the value to pAffineT (pAffineTP to all transforms). The X-form color is then assigned to pAffineT. Then, in successive order, each variation/transform in the X-form modifies the point. Finally, the destination point is modified by the post-transformation matrix of the X-form where it is spit back out.
At this point, despite all that hard work, JWildfire will decide if it is actually going to draw the point based on the setting. Why after? It turns out, you may want a transform to be effective while still being invisible.
Once the X-form is done with the point, the point is modified by the final transforms (if there are any). Maschke decided to do a bunch of stuff inline, but this is all dealing with projection into the third dimension and modifying the gamma (and thus the color), so it only plays a minor role with respect to the fractal’s general structure.
How Transforms Work
Whenever you use a transform, it is passed two variables of the type XYZPoint (which, obviously is a point in 3D): pAffineTP and pVarTP. pAffineTP is what coordinates of the source point (the point from screen or the last point from another X-form) will be assigned to. Unless you use a “pre_” transform, its value will always be zero. This is nice if you want to take and image and make it act like a sine wave irrespective of the actual contents.
There are three types of transforms (plus a fourth – mine at the end of this section): pre_, post_, and normal.
pre_ – These transforms exclusively modify pAffineTP and consequently the starting point. Note, the starting point is affected by pre-transforms and post-transforms of X-forms from which the point comes. Keep this in mind when linking transforms.
post_ – These transforms exclusively modify pVarTP and consequently the point drawn on screen and passed to other variations and X-forms.
normal – These transforms (most everything else) take values of pAffineTP, modify them, and store the result in pVarTP.
Notice, there is no transform that takes the values of pVarTP and uses them to set pAffineTP. Being a control freak who likes to base his designs on the principle of chaining results, I had to invent a new transform and consequently a new type:
post_to_pre – This transform takes values of pVarTP and uses them to modify pAffineTP.
The most straightforward version of post_to_pre, which I plan to contribute to JWildfire, is a simple assignment of pVarTP to pAffineTP.
============== post_to_pre_s ==============
pAffineTP.x = pVarTP.x;
pAffineTP.y = pVarTP.y;
pAffineTP.z = pVarTP.z;
// If you want to restart the drawings with new
// starting points, add this:
pVarTP.x = 0.0;
pVarTP.y = 0.0;
pVarTP.z = 0.0;
Since this isn’t integrated into JWildfire yet, I recommend just copying the code as it is and pasting into the transformPoint function in the code part of the variation “custom_wf”. When you select that transform, select the parameter “code”. The little button on the right with three dots on it should be available. Click this, and it will show you the code.
Tips and Tricks
If you’re new to fractals, you will probably find it easier to get something beautiful by luck rather than by design. Fear not, however, there are ways to get what you want, but like any art, you will have to sit down and study hard. Instead of coloring and lighting, you’ll be studying math. I suggest studying the variations of JWildfire in the folder:
However, before you venture off into the world of complex math, there are a couple of tips I’d like to share with you.
Making A Transform Independent
Suppose you want the starting points of an X-form to be independent of the other X-forms. If you recall, JWildfire randomly picks the starting X-form, which makes it difficult to rely on any starting point.
To make the X-form independent of other forms, one technique is to start the X-form with a blank custom variation – one that sets all of the coordinates the pAffineTP and the pVarTP to zero.
If you’re just looking to have other transforms act as invisible influences, then start your X-form with the post_to_pre_s variation/transform that I gave above.
Independence Through Xaos
Suppose you want to create two independent sets of X-forms and then use one (or both) to influence the other. Because of the way the fractal flame algorithm works, there must be some path of relative weights that leads to the X-form in order for it to be visible.
For example, if you only have two X-forms, call them “xf1” and “xf2”, in order to get both xf1 and xf2 to appear, your relative weights (xaos) will have to be as follows:
from xf2 = 1.0
from xf1 = 1.0
NOTICE: In JWildfire, the Rel. Weights (relative weights) tab lists the relative weights TO
FROM the other transforms NOT FROM TO them. If you don’t remember this, you will end up doing everything backwards like I used to do. Edit 3/4/12: I was doing things backwards for awhile, presuming “from” instead of “to”, although notably it works either way in many cases because the relationship is usually a two-way street between two transforms that don’t send anything to themselves.
Teaching you by example
Create two X-forms (via the button labelled “Add” in the Transformations tab). To the first X-form, add a simple linear3D variation with a value of 3. To the second X-form, add a julia3D (default settings).
Go to the xaos tab (“Rel. weights”). Select the first transform/X-form and set the relative weight for transform 1 to 0.0. Select the second X-form and set the relative weight for transform 2 to 0.0.
Your xaos tables should look like this:
x-form 1 = 0.0
x-form 2 = 1.0
x-form 1 = 1.0
x-form 2 = 0.0
You should get two concentric circles.
What did I do here? The first X-form I made entirely dependent on the values from second X-form. Since the second X-form only returns a circle (since that’s what the Julia creates by itself), the only points that the linear3D had to work with were those in a circle. Great!
But wait! We don’t know which transform JWildfire starts with, and if Julia acts on itself, it creates several concentric circles. Thus, for the Xaos for the second X-form (holding the Julia), we set the relative weight from itself to zero and let it get it’s points from the linear3D in the first X-form.
Now add a pdj to first X-form (do NOT remove the linear!) and play with its value, changing it slowly between 0.0 and 0.38.
What happened? The pdj only warped the circles. In fact, if you continued, you would not see the familiar pdj curly structure. Instead, you should have created was a couple of island shapes, whose structures you saw form from the circles. The fact that these islands aren’t identical is due to the fact that the pdj does not possess circular symmetry. Instead, it creates a jagged but nevertheless continuous curve.
Since the pdj affects the points from the linear3D, it only affects the circles. Notice, if you set the first X-form’s relative weight from itself, you get repetitions of the islands. But why are they all blurry? Remember – we’re altering the output by pdj, so every time it goes back to the linear3D transform, it’s at a slightly different position than it would be from center. The “error”, as it were, is compounded every time the X-form passes points to itself.
Now let’s consider the second X-form, possessing the Julia. Set the second X-form’s relative weight from itself to 1.0. Make sure that the first X-form’s relative weight from itself is set back to 0.0, otherwise things will be messy. Things here become really complicated quick, but there’s an explanation for it.
Just for clarity, your xaos tables should look like this:
x-form 1 = 0.0
x-form 2 = 1.0
x-form 1 = 1.0
x-form 2 = 1.0
You should at this point be seeing several squiggly rings. All of these rings, with the exception of one, are created with some influence from the first X-form. The only ring that does not have influence from the first X-form is obvious – it’s a circle.
Why are there pairs of rings? Remember that our basic structure was two rings, created by the first X-form’s linear3D creating an outer ring from the Julia.
It’s alittle tricky to see at first, but there are actually two main rings, each composed of three middle rings, which are in turn composed of two bands. There are two main rings and two bands because of what I just explained, but here’s how they are created:
The bands themselves are actually last to be created. The are, in fact, the most altered. The most outer band is actually NOT the most altered by pdj. Why? Remember that, because X-form 1 never takes from itself, the only time points can undergo a linear transform is from being passed by the second X-form (the Julia). The second X-form, however, calls itself (using up an “iteration” of the fractal generator), creating the second ring.
So the points created by the outer rings are composed something like this:
x-form 1 > x-form 2 > x-form 1 > x-form 2 > x-form 1
x-form 1 > x-form 2 > x-form 1 > x-form 2 > x-form 2
Noting that the Julia in X-form 2 will thicken itself.
To help you see the effect of Julia’s thickening itself, set the first X-form’s relative weight to itself to 1.0 for just a moment. You should be able to see faint rings. These are rings that have been created by the Julia calling itself and then being modified once, twice, maybe three times at most by the linear3D and pdj.
To help you see this better, create a new fractal and assign the X-form two Julia transforms with identical parameters. You should see concentric circles composed of pairs of bands.
Going back to the first fractal, reduce the pdj effect to zero. What do you see? Oh look! – It’s a pair of concentric rings, each composed of a pair of concentric rings, each composed of a pair of concentric rings. It’s our basic structure repeated on two more levels!
Recall, JWildfire continually passes points to X-forms until it reaches a set number of iterations or the next randomly-selected X-form from the table refuses to take the point (as given by relative weight).
Here’s a key trick: If you set the relative weight from X-form 1 to X-form 1 to 0.1, you’ll start to see faint rings because X-form 1 will now “sort of” accept points from itself. Set this back to zero and try the same thing for X-form 2, with the Julia. Notice, there are faint rings because X-form 2 is “sort of” accepting points from itself.
I don’t actually know why the mid-level inner rings are faint, but I advise you to create simple examples with Julia3D (with identical power), only changing the amount to see how xaos results in the concentric circles. When you’re finally tired of it and want to have fun, change the powers of the Julia. I recommend creating one X-form with two Julia3D of powers -5 and 4 (in that order) and changing their amounts.
Spoiler for the lazy:
Flame vs. Fractal
Even if you’re an avid “purist”, having a preference for fractal flames generated via Scott Draves’ algorithm, you might have observed that fractal flames are, in fact, not true fractals. True fractals are mathematical constructs that when you zoom in on them to infinity, there is always more detail to be discovered. While some fractal flames have the potential to be this way if the computer could continue xaos and reiterate the transforms forever, that’s doesn’t mean they qualify as true fractals.
However, JWildfire does have some transforms that match (as best as they can) real fractals, such as Julia and Mandelbrot, particularly those beginning with “fract_”. You can set up true Julia fractals with the variation/transform “fract_formula_julia_wf” that also allows you to modify the formula (but keep in mind that Julia sets the starting pixel point to “z” (in the formula z=z^n+c) rather than “c” as in Mandelbrots). A couple variations/transforms, “fract_salamander_wf” and “fract_julia_wf” are available, but they don’t allow you to control the formula. Notably, the variations/transforms labelled “julia” and not preceded by “fract_” create fancy wheels by repeating the patterns in circles.
As for Mandelbrots, all variations/transforms produce something resembling a Mandelbrot, but the variation labelled without “fract_” merely takes the points and produces a Mandelbrot outline with them.
IMPORTANT NOTE: Whenever you use any of these true-fractal variations or any of the Mandelbrot variations, it will eliminate the previous shape. Hence, you should generally use these prior to anything else.
Some of the experts I know recommend reading Scott Drave’s paper (flam3.pdf on his website), but frankly, I found it to be too vague when discussing transforms. Maybe what he said was the intention of the design, but it doesn’t necessarily help you understand how to produce them. The paper was informative with respect to the rendering. For example, the artificial 3D effect is created by controlling the gamma level on coloring using a logarithmic curve. It’s hard to explain this effect without a graph, which he uses.
If you’re looking to understand by example, 0bsidianfire from DeviantArt created reference images and notes to identify what he understands each transform does. You can view them here:
Do note that he uses a “base image”, which may be a fractal and not a picture, and thus the pVarTP has been altered. If this is the case, any images of pre_ transforms should be ignored.
That’s it! You’re all set to go conquer the world of fractals!