1/*
2 * Copyright (c) 2009-2010 jMonkeyEngine
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 * * Redistributions of source code must retain the above copyright
10 *   notice, this list of conditions and the following disclaimer.
11 *
12 * * Redistributions in binary form must reproduce the above copyright
13 *   notice, this list of conditions and the following disclaimer in the
14 *   documentation and/or other materials provided with the distribution.
15 *
16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17 *   may be used to endorse or promote products derived from this software
18 *   without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32package com.jme3.terrain.heightmap;
33
34import java.util.Random;
35import java.util.logging.Logger;
36
37/**
38 * <code>FluidSimHeightMap</code> generates a height map based using some
39 * sort of fluid simulation. The heightmap is treated as a highly viscous and
40 * rubbery fluid enabling to fine tune the generated heightmap using a number
41 * of parameters.
42 *
43 * @author Frederik Boelthoff
44 * @see <a href="http://www.gamedev.net/reference/articles/article2001.asp">Terrain Generation Using Fluid Simulation</a>
45 * @version $Id$
46 *
47 */
48public class FluidSimHeightMap extends AbstractHeightMap {
49
50    private static final Logger logger = Logger.getLogger(FluidSimHeightMap.class.getName());
51    private float waveSpeed = 100.0f;  // speed at which the waves travel
52    private float timeStep = 0.033f;  // constant time-step between each iteration
53    private float nodeDistance = 10.0f;   // distance between each node of the surface
54    private float viscosity = 100.0f; // viscosity of the fluid
55    private int iterations;    // number of iterations
56    private float minInitialHeight = -500; // min initial height
57    private float maxInitialHeight = 500; // max initial height
58    private long seed; // the seed for the random number generator
59    float coefA, coefB, coefC; // pre-computed coefficients in the fluid equation
60
61    /**
62     * Constructor sets the attributes of the hill system and generates the
63     * height map. It gets passed a number of tweakable parameters which
64     * fine-tune the outcome.
65     *
66     * @param size
67     *            size the size of the terrain to be generated
68     * @param iterations
69     *            the number of iterations to do
70     * @param minInitialHeight
71     *                        the minimum initial height of a terrain value
72     * @param maxInitialHeight
73     *                        the maximum initial height of a terrain value
74     * @param viscosity
75     *                        the viscosity of the fluid
76     * @param waveSpeed
77     *                        the speed at which the waves travel
78     * @param timestep
79     *                        the constant time-step between each iteration
80     * @param nodeDistance
81     *                        the distance between each node of the heightmap
82     * @param seed
83     *            the seed to generate the same heightmap again
84     * @throws JmeException
85     *             if size of the terrain is not greater that zero, or number of
86     *             iterations is not greater that zero, or the minimum initial height
87     *             is greater than the maximum (or the other way around)
88     */
89    public FluidSimHeightMap(int size, int iterations, float minInitialHeight, float maxInitialHeight, float viscosity, float waveSpeed, float timestep, float nodeDistance, long seed) throws Exception {
90        if (size <= 0 || iterations <= 0 || minInitialHeight >= maxInitialHeight) {
91            throw new Exception(
92                    "Either size of the terrain is not greater that zero, "
93                    + "or number of iterations is not greater that zero, "
94                    + "or minimum height greater or equal as the maximum, "
95                    + "or maximum height smaller or equal as the minimum.");
96        }
97
98        this.size = size;
99        this.seed = seed;
100        this.iterations = iterations;
101        this.minInitialHeight = minInitialHeight;
102        this.maxInitialHeight = maxInitialHeight;
103        this.viscosity = viscosity;
104        this.waveSpeed = waveSpeed;
105        this.timeStep = timestep;
106        this.nodeDistance = nodeDistance;
107
108        load();
109    }
110
111    /**
112     * Constructor sets the attributes of the hill system and generates the
113     * height map.
114     *
115     * @param size
116     *            size the size of the terrain to be generated
117     * @param iterations
118     *            the number of iterations to do
119     * @throws JmeException
120     *             if size of the terrain is not greater that zero, or number of
121     *             iterations is not greater that zero
122     */
123    public FluidSimHeightMap(int size, int iterations) throws Exception {
124        if (size <= 0 || iterations <= 0) {
125            throw new Exception(
126                    "Either size of the terrain is not greater that zero, "
127                    + "or number of iterations is not greater that zero");
128        }
129
130        this.size = size;
131        this.iterations = iterations;
132
133        load();
134    }
135
136
137    /*
138     * Generates a heightmap using fluid simulation and the attributes set by
139     * the constructor or the setters.
140     */
141    public boolean load() {
142        // Clean up data if needed.
143        if (null != heightData) {
144            unloadHeightMap();
145        }
146
147        heightData = new float[size * size];
148        float[][] tempBuffer = new float[2][size * size];
149        Random random = new Random(seed);
150
151        // pre-compute the coefficients in the fluid equation
152        coefA = (4 - (8 * waveSpeed * waveSpeed * timeStep * timeStep) / (nodeDistance * nodeDistance)) / (viscosity * timeStep + 2);
153        coefB = (viscosity * timeStep - 2) / (viscosity * timeStep + 2);
154        coefC = ((2 * waveSpeed * waveSpeed * timeStep * timeStep) / (nodeDistance * nodeDistance)) / (viscosity * timeStep + 2);
155
156        // initialize the heightmaps to random values except for the edges
157        for (int i = 0; i < size; i++) {
158            for (int j = 0; j < size; j++) {
159                tempBuffer[0][j + i * size] = tempBuffer[1][j + i * size] = randomRange(random, minInitialHeight, maxInitialHeight);
160            }
161        }
162
163        int curBuf = 0;
164        int ind;
165
166        float[] oldBuffer;
167        float[] newBuffer;
168
169        // Iterate over the heightmap, applying the fluid simulation equation.
170        // Although it requires knowledge of the two previous timesteps, it only
171        // accesses one pixel of the k-1 timestep, so using a simple trick we only
172        // need to store the heightmap twice, not three times, and we can avoid
173        // copying data every iteration.
174        for (int i = 0; i < iterations; i++) {
175            oldBuffer = tempBuffer[1 - curBuf];
176            newBuffer = tempBuffer[curBuf];
177
178            for (int y = 0; y < size; y++) {
179                for (int x = 0; x < size; x++) {
180                    ind = x + y * size;
181                    float neighborsValue = 0;
182                    int neighbors = 0;
183
184                    if (x > 0) {
185                        neighborsValue += newBuffer[ind - 1];
186                        neighbors++;
187                    }
188                    if (x < size - 1) {
189                        neighborsValue += newBuffer[ind + 1];
190                        neighbors++;
191                    }
192                    if (y > 0) {
193                        neighborsValue += newBuffer[ind - size];
194                        neighbors++;
195                    }
196                    if (y < size - 1) {
197                        neighborsValue += newBuffer[ind + size];
198                        neighbors++;
199                    }
200                    if (neighbors != 4) {
201                        neighborsValue *= 4 / neighbors;
202                    }
203                    oldBuffer[ind] = coefA * newBuffer[ind] + coefB
204                            * oldBuffer[ind] + coefC * (neighborsValue);
205                }
206            }
207
208            curBuf = 1 - curBuf;
209        }
210
211        // put the normalized heightmap into the range [0...255] and into the heightmap
212        for (int y = 0; y < size; y++) {
213            for (int x = 0; x < size; x++) {
214                heightData[x + y * size] = (float) (tempBuffer[curBuf][x + y * size]);
215            }
216        }
217        normalizeTerrain(NORMALIZE_RANGE);
218
219        logger.info("Created Heightmap using fluid simulation");
220
221        return true;
222    }
223
224    private float randomRange(Random random, float min, float max) {
225        return (random.nextFloat() * (max - min)) + min;
226    }
227
228    /**
229     * Sets the number of times the fluid simulation should be iterated over
230     * the heightmap. The more often this is, the less features (hills, etc)
231     * the terrain will have, and the smoother it will be.
232     *
233     * @param iterations
234     *            the number of iterations to do
235     * @throws JmeException
236     *             if iterations if not greater than zero
237     */
238    public void setIterations(int iterations) throws Exception {
239        if (iterations <= 0) {
240            throw new Exception(
241                    "Number of iterations is not greater than zero");
242        }
243        this.iterations = iterations;
244    }
245
246    /**
247     * Sets the maximum initial height of the terrain.
248     *
249     * @param maxInitialHeight
250     *                        the maximum initial height
251     * @see #setMinInitialHeight(int)
252     */
253    public void setMaxInitialHeight(float maxInitialHeight) {
254        this.maxInitialHeight = maxInitialHeight;
255    }
256
257    /**
258     * Sets the minimum initial height of the terrain.
259     *
260     * @param minInitialHeight
261     *                        the minimum initial height
262     * @see #setMaxInitialHeight(int)
263     */
264    public void setMinInitialHeight(float minInitialHeight) {
265        this.minInitialHeight = minInitialHeight;
266    }
267
268    /**
269     * Sets the distance between each node of the heightmap.
270     *
271     * @param nodeDistance
272     *                       the distance between each node
273     */
274    public void setNodeDistance(float nodeDistance) {
275        this.nodeDistance = nodeDistance;
276    }
277
278    /**
279     * Sets the time-speed between each iteration of the fluid
280     * simulation algortithm.
281     *
282     * @param timeStep
283     *                       the time-step between each iteration
284     */
285    public void setTimeStep(float timeStep) {
286        this.timeStep = timeStep;
287    }
288
289    /**
290     * Sets the viscosity of the simulated fuid.
291     *
292     * @param viscosity
293     *                      the viscosity of the fluid
294     */
295    public void setViscosity(float viscosity) {
296        this.viscosity = viscosity;
297    }
298
299    /**
300     * Sets the speed at which the waves trave.
301     *
302     * @param waveSpeed
303     *                      the speed at which the waves travel
304     */
305    public void setWaveSpeed(float waveSpeed) {
306        this.waveSpeed = waveSpeed;
307    }
308}
309