1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
|
<!DOCTYPE html SYSTEM "html.dtd">
<html><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"><title>About xmountains</title></head>
<body>
<h1> All about xmountains</h1>
<p>
<a href="https://spbooth.github.io/xmountains/">
Xmountains</a> is a fractal terrain generator written by
Stephen Booth
This document describes what it does and how it works.
</p>
<p>
The basic idea behind a fractal landscape is to generate a continuous surface
which varies in height randomly but with the random variation obeying a
particular statistical law.
</p>
<p>
In the case of a fractal landscape the average difference in height
between pairs of points separated by a distance <i>l</i> should go as
a power law in <i>l</i>. If you are only interested in random terrain
generation rather than fractal self-similarity then you could use other
functions of <i>l</i> that tend to zero as <i>l</i> tends to zero.
If the function does not tend to zero then the result will not be
continuous. I use the following definition:
</p>
<a name="fractal">
<i>
<pre> 2H
<|H1 - H2|> = l
</pre>
</i>
</a>
<p>
For values of H below 0.5 as <i>l</i> gets small the variation in height
is greater than the
horizontal distance between the points (<i>l</i>) so the surface becomes
space-filling. For values of H at 0.5 or above the surface becomes
smoother as H increases. This is because the variation in height at
short length scales is a smaller fraction of the variation at longer
length scales. When displaying the surface you usually re-scale the whole
lot by a constant factor so it is the relative variation that matters.
</p>
<p>
This program uses a modified form the mid-point displacement algorithm
</p>
<p>
The mid-point displacement algorithm is a recursive algorithm.
Starting from a crude outline of the surface more and more detail is
added at progressively smaller length scales.
Each iteration doubles the resolution of a 2 dimensional grid of altitudes.
</p>
<p>
This is done in 2 stages:
</p>
<pre>A B A B
stage1
---------> E
C D C D
A B A F B
stage2
E --------> G E H
C D C I D
</pre>
<p>
More detail can then be filled in by recursion:
</p>
<pre>
A F B A F B
stage1 * *
G E H --------> G E H
* *
C I D C I D
A F B A + F + B
* * stage2 + * + * +
G E H --------> G + E + H
* * + * + * +
C I D C + I + D
</pre>
<p>
The new points are generated by taking an average of the surrounding points
and adding a random offset. In order to generate the power law behaviour
described <a href="#fractal">above</a> the random offset is scaled by a factor
proportional to a power (2H in my notation) of the distance between the new
points and the
surrounding points. The results seem to be a little better if the random
offsets are taken from a gaussian distribution. The basic idea is very
similar to that used by the Koch snowflake.
</p>
<p>
The modifications to the standard algorithm are as follows:
There are three optional <a href="#regen">regeneration steps</a> to reduce
<a href="#crease">creasing</a>. These are controlled by the
<a href="#smooth">-s flag</a>.
</p>
<p>
The <a href="#cross">-x flag (cross update)</a> controls whether the
midpoints (E) are included
in the average when performing the stage2 update or if only the corner
points are used.
</p>
<h2> <a name="crease">Creasing</a> </h2>
<p>
When I started in this game "creasing" was a big problem with square
grid mid-point displacement surfaces
and explicit smoothing steps were introduced to get rid of these.
You can make my program revert to these early algorithms by specifying
"-s 0" Things are particularly bad if you calculate the middle of the sides
independently of the middle of the square ("-x" in my program)
This was not uncommon in the other programs I have seen.
</p>
<h3> <a name="cross">Cross updates</a></h3>
<p>
If you only use a pair of points to perform the average for the middle
of the sides then for any point that lies on one of the sides of a
large length scale square its value depends ONLY on the values of other
points that lie along that side. This line then forms a crease. Because
no information ever crosses this line it acts as a kind of "event
horizon". The heights of points on the crease are calculated
independently of everything else and then the surfaces on the 2 sides
form to match this line but the 2 surfaces are independent of each
other. The crease is a mis-match between the 2 independent surfaces that
are only constrained to follow a common boundary that has been specified
independently of each of them
<br>
<img src="crease.gif" alt="A fractal landscape with creases"/>
</p>
<p>
When you split the update into 2 steps:
</p><ol>
<li> generate the middle of the square with an average of the
4 corners and a random offset based on a length scale of L/sqrt(2).
</li>
<li> generate the sides as an average over 4 points (2 corner points and
2 middle of square points) followed by a random offset based on a
length scale of L/2.
</li>
</ol>
the information flows sideways into the sides of the large squares.
The problem does not go away but instead of creases along the sides of
the squares you get conical deformations at the corners of the squares:
<br>
<img src="cone.gif" alt="A fractal landscape with conical deformations"/>
<br>
This is an improvement but not the whole story.
<p></p>
<h3><a name="regen">Regeneration steps</a></h3>
<p>
The root cause of <a href="#crease">creasing</a> and the conical
deformations is that the position
of some points are fixed by the early (long-distance) iterations of the
fractal algorithm. We are trying to generate a surface where the average
difference in height between points on the surface is a function of the
distance between them. Ideally two points should have an equal effect on
each other. Instead we have implemented a hierarchy of points where the
information only flows in one direction.
</p>
<p>
The solution I came up with was to use the early iteration to set the
overall trend of the surface rather than setting the exact position of
particular points. This is done by a 2 steps forward/1 step back method.
In the original algorithm the results at the end of one iteration are
used to generate a new set of points with each iteration adding detail at a
length scale of half the previous iteration. I added a second stage to
each iteration that consisted of discarding all of the points generated
by previous iterations and regenerating them from the new points.
I think this is a lot closer to the desired result because every point
is the result of updates at all length scales.
<br>
<img src="normal.gif" alt="a fractal landscape"/>
<br>
However I have a
suspicion that it may result in a slight shift in the fractal dimension.
It would be nice to measure the fractal dimension produced as a function
of the input parameter to the algorithm.
</p>
<p>
A regeneration step recalculates the height of existing points using an
average and offset from a newer generation of points. In xmountains the three
regeneration steps are:
</p>
<dl>
<dt> Step 1 </dt>
<dd> recalculate corner points (A,B,C,D) from the midpoints (E)
after the stage1 update. </dd>
<dt> Step 2 </dt>
<dd> recalculate midpoints (E) from the edge points (F,G,H,I)
after the stage2 update </dd>
<dt> Step 3 </dt>
<dd> recalculate corner points (A,B,C,D) from the edge points (F,G,H,I)
after the stage2 update </dd>
</dl>
<p>
The regeneration stages are turned on by the smoothing parameter
<a name="smooth">(-s flag)</a>
</p>
<pre> flag Step-3 Step-2 Step-1
0 off off off
1 on off off
2 off on off
3 on on off
4 off off on
5 on off on
6 off on on
7 on on on
</pre>
<p>
The default is to just use step 3 (-s 1)
</p><p>
When performing the regeneration steps the random offset is added to an
average of the new points.
</p>
<p>
I came across a different solution in the xmnts program by
<em>
Paul Sharpe @ DEC, Reading, England.
</em>
who also modified the old points at each iteration, but he only added an
additional random offset to the old points.
</p>
<p>
I added this capability to my program. The -X and -Y parameters
trade off the two methods if you leave them at 0.0 (the default) you get
my algorithm. If you set them to 1.0 you get Paul's and values
in-between give you a mixture of the two, i.e. a weighted average of the
new points and the old value of the point.
</p>
<h3>How well does this all work</h3>
The relative importance of the different algorithms can easily be seen
if we remove the random element from the algorithm and replace the
gaussian random number with a constant value. Without any random
variation we should get a completely smooth surface. Without any
regeneration steps or cross updates we get a surface
consisting only of creases.
<br>
<img src="quilt.gif" alt="A surface that looks like a quilt"/>
<br>
The addition of cross updates generate
a very interesting surface.
<br>
<img src="cushion.gif" alt="a surface that looks like a cushion"/>
<br>
This is an
improvement as the surface has better rotational symmetry but it still
has significant artifacts. These are completely missing when regeneration
steps are used.
<h2> Pipelining </h2>
<p>
The other trick I came up with was to pipeline the code. This is also
responsible for much of the speed because it reduces swapping and
improves cache use. I don't think that anyone else does this but
the idea is straightforward once you think of it.
</p>
<p>
If you think of the normal algorithm it is possible to add extra detail
to a region of the surface without adding this level of detail to the
entire surface. Each iteration of the algorithm increases the number of
squares by 4. You can then add further detail to a restricted region of
the surface by only applying the next level of recursion to a subset of
these squares. Obviously some squares you choose not to update
will end up partially updated because they share sides with updated
squares.
</p>
<p>
As an aside this selective updating allows you to zoom-in on a surface
efficiently. You don't bother updating squares outside of your field of
view and you keep adding more detail to the squares you can see until
the sides of the smallest level squares subtends less than a maximum
angle at your point of view (i.e. the sides always look short).
</p>
<p>
What I do in xmountains is slightly different.
I only perform updates necessary to fully update the left hand
row of squares. These fully updated points are used to generate a column
of pixels in the output and then discarded. The new left hand edge is
not fully updated so additional updates are now required.
Imagine I have a level-1 surface as follows:
</p><pre>
1 1 1
1 1 1
1 1 1
</pre>
And that I have some method of generating new points on the right hand
side of this surface (I will show how I do this later but for the time
being assume this is the top iteration level and we can generate new
points by applying random offsets to a constant value).
<p></p>
<p>
I can use these points to fill in part of the left hand squares
</p><pre>
1 2 1 1
2 2
1 2 1 1
2 2
1 2 1 1
</pre>
Now scroll everything to the left
<pre>
1 1
1 1
1 1
</pre>
and add in a new third column on the right hand side
<pre>
1 1 1
1 1 1
1 1 1
</pre>
This returns us to our original state. This process can be repeated
indefinitely. For each column of points it inputs on the right it outputs
2 columns on the left with level of detail doubled. If you want to
perform N levels of update then you chain together N of these steps.
If you want to use a more complicated update (For example the
regeneration steps and the cross update) then the same basic approach
applies but the details get more complicated.
In the xmountains the equivalent to the step described above is a
routine called next_strip.
<h2>xmountain command line flags</h2>
<pre>xmountains: version 2.3
usage: xmountains -[bqgdPEmMrBZIASFTCapcevfRltxsXYH]
-b [false] use root window
-q [false] reset root window on exit
-g string window geometry
-d string display
-P filename write PID to file
-E [false] toggle explicit expose events
-m [false] print map
-M [false] implement reflections
-r int [20] # columns before scrolling
-B int [80] # shades in a colour band
-n int [245] # number of colours
-Z int [10] time to sleep before scrolling
-I float [40.000000] vertical angle of light
-A float [0.000000] horizontal angle of light
-S float [0.600000] vertical stretch
-T float [0.500000] vertical shift
-W float [0.000000] sealevel
-F int [1] reduce variation in the foreground
-G float [-1.000000] average foreground height
-C float [0.300000] contour parameter
-a float [2.500000] altitude of viewpoint
-p float [4.000000] distance of viewpoint
-c float [1.000000] contrast
-e float [0.300000] ambient light level
-v float [0.600000] vertical light level
Fractal options:
-f float [0.650000] fractal dimension
-R int [0] rng seed, read clock if 0
-l int [10] # levels of recursion
-t int [2] # non fractal iterations
-x [true] cross update
-s [1] smoothing (0-7)
-X float [0.000000] fraction of old value for rg2 & rg3
-Y float [0.000000] fraction of old value for rg1
-H print short description of algorithm.
</pre>
<h2>X window graphics</h2>
The algorithms and code for xmountains were originally developed for to
drive particular graphics hardware and then ported to the X-window
system. This history can still be seen in the following ways
<ul>
<li> All the options are given as command line flags.
</li><li> The code assume a CLUT based colour model rather than direct RGB values.
</li></ul>
Most of the suggestions I get for changes to xmountains involve either
adding a X control panel to set parameters or implementing 24-bit
graphics. Actually neither of these makes a great deal of sense.
<p>
To save memory the program does not retain any knowledge of currently
displayed surface. The only information that is retained is the final bitmap
image. This makes it very difficult to change the viewpoint while the
program is running (though you could probably get away with slow gradual
changes).
</p><p>
The program can already utilise 24-bit colour displays the default parameters
are set to use less than 256 colours but the -B or -n flags can be used
to increase the number of colours. The quality of the final image
remains pretty much the same though because there are already 80 shades
of each base colour. If you only ever run on a 24-bit display you may
want to try increasing the number of base colours by increasing N_BANDS
in paint.h and modifying the set_clut routine in artist.c
</p><p>
</p></body></html>
|