aboutsummaryrefslogtreecommitdiff
path: root/engine/src/terrain/com/jme3/terrain/heightmap/FluidSimHeightMap.java
blob: ec2dfdfe00be9c80429abb243f9b1b8b0a4e4690 (plain)
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
/*
 * Copyright (c) 2009-2010 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.terrain.heightmap;

import java.util.Random;
import java.util.logging.Logger;

/**
 * <code>FluidSimHeightMap</code> generates a height map based using some
 * sort of fluid simulation. The heightmap is treated as a highly viscous and
 * rubbery fluid enabling to fine tune the generated heightmap using a number
 * of parameters.
 *
 * @author Frederik Boelthoff
 * @see <a href="http://www.gamedev.net/reference/articles/article2001.asp">Terrain Generation Using Fluid Simulation</a>
 * @version $Id$
 *
 */
public class FluidSimHeightMap extends AbstractHeightMap {

    private static final Logger logger = Logger.getLogger(FluidSimHeightMap.class.getName());
    private float waveSpeed = 100.0f;  // speed at which the waves travel
    private float timeStep = 0.033f;  // constant time-step between each iteration
    private float nodeDistance = 10.0f;   // distance between each node of the surface
    private float viscosity = 100.0f; // viscosity of the fluid
    private int iterations;    // number of iterations
    private float minInitialHeight = -500; // min initial height
    private float maxInitialHeight = 500; // max initial height
    private long seed; // the seed for the random number generator
    float coefA, coefB, coefC; // pre-computed coefficients in the fluid equation

    /**
     * Constructor sets the attributes of the hill system and generates the
     * height map. It gets passed a number of tweakable parameters which
     * fine-tune the outcome.
     *
     * @param size
     *            size the size of the terrain to be generated
     * @param iterations
     *            the number of iterations to do
     * @param minInitialHeight
     *                        the minimum initial height of a terrain value
     * @param maxInitialHeight
     *                        the maximum initial height of a terrain value
     * @param viscosity
     *                        the viscosity of the fluid
     * @param waveSpeed
     *                        the speed at which the waves travel
     * @param timestep
     *                        the constant time-step between each iteration
     * @param nodeDistance
     *                        the distance between each node of the heightmap
     * @param seed
     *            the seed to generate the same heightmap again
     * @throws JmeException
     *             if size of the terrain is not greater that zero, or number of
     *             iterations is not greater that zero, or the minimum initial height
     *             is greater than the maximum (or the other way around)
     */
    public FluidSimHeightMap(int size, int iterations, float minInitialHeight, float maxInitialHeight, float viscosity, float waveSpeed, float timestep, float nodeDistance, long seed) throws Exception {
        if (size <= 0 || iterations <= 0 || minInitialHeight >= maxInitialHeight) {
            throw new Exception(
                    "Either size of the terrain is not greater that zero, "
                    + "or number of iterations is not greater that zero, "
                    + "or minimum height greater or equal as the maximum, "
                    + "or maximum height smaller or equal as the minimum.");
        }

        this.size = size;
        this.seed = seed;
        this.iterations = iterations;
        this.minInitialHeight = minInitialHeight;
        this.maxInitialHeight = maxInitialHeight;
        this.viscosity = viscosity;
        this.waveSpeed = waveSpeed;
        this.timeStep = timestep;
        this.nodeDistance = nodeDistance;

        load();
    }

    /**
     * Constructor sets the attributes of the hill system and generates the
     * height map.
     *
     * @param size
     *            size the size of the terrain to be generated
     * @param iterations
     *            the number of iterations to do
     * @throws JmeException
     *             if size of the terrain is not greater that zero, or number of
     *             iterations is not greater that zero
     */
    public FluidSimHeightMap(int size, int iterations) throws Exception {
        if (size <= 0 || iterations <= 0) {
            throw new Exception(
                    "Either size of the terrain is not greater that zero, "
                    + "or number of iterations is not greater that zero");
        }

        this.size = size;
        this.iterations = iterations;

        load();
    }


    /*
     * Generates a heightmap using fluid simulation and the attributes set by
     * the constructor or the setters.
     */
    public boolean load() {
        // Clean up data if needed.
        if (null != heightData) {
            unloadHeightMap();
        }

        heightData = new float[size * size];
        float[][] tempBuffer = new float[2][size * size];
        Random random = new Random(seed);

        // pre-compute the coefficients in the fluid equation
        coefA = (4 - (8 * waveSpeed * waveSpeed * timeStep * timeStep) / (nodeDistance * nodeDistance)) / (viscosity * timeStep + 2);
        coefB = (viscosity * timeStep - 2) / (viscosity * timeStep + 2);
        coefC = ((2 * waveSpeed * waveSpeed * timeStep * timeStep) / (nodeDistance * nodeDistance)) / (viscosity * timeStep + 2);

        // initialize the heightmaps to random values except for the edges
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                tempBuffer[0][j + i * size] = tempBuffer[1][j + i * size] = randomRange(random, minInitialHeight, maxInitialHeight);
            }
        }

        int curBuf = 0;
        int ind;

        float[] oldBuffer;
        float[] newBuffer;

        // Iterate over the heightmap, applying the fluid simulation equation.
        // Although it requires knowledge of the two previous timesteps, it only
        // accesses one pixel of the k-1 timestep, so using a simple trick we only
        // need to store the heightmap twice, not three times, and we can avoid
        // copying data every iteration.
        for (int i = 0; i < iterations; i++) {
            oldBuffer = tempBuffer[1 - curBuf];
            newBuffer = tempBuffer[curBuf];

            for (int y = 0; y < size; y++) {
                for (int x = 0; x < size; x++) {
                    ind = x + y * size;
                    float neighborsValue = 0;
                    int neighbors = 0;

                    if (x > 0) {
                        neighborsValue += newBuffer[ind - 1];
                        neighbors++;
                    }
                    if (x < size - 1) {
                        neighborsValue += newBuffer[ind + 1];
                        neighbors++;
                    }
                    if (y > 0) {
                        neighborsValue += newBuffer[ind - size];
                        neighbors++;
                    }
                    if (y < size - 1) {
                        neighborsValue += newBuffer[ind + size];
                        neighbors++;
                    }
                    if (neighbors != 4) {
                        neighborsValue *= 4 / neighbors;
                    }
                    oldBuffer[ind] = coefA * newBuffer[ind] + coefB
                            * oldBuffer[ind] + coefC * (neighborsValue);
                }
            }

            curBuf = 1 - curBuf;
        }

        // put the normalized heightmap into the range [0...255] and into the heightmap
        for (int y = 0; y < size; y++) {
            for (int x = 0; x < size; x++) {
                heightData[x + y * size] = (float) (tempBuffer[curBuf][x + y * size]);
            }
        }
        normalizeTerrain(NORMALIZE_RANGE);

        logger.info("Created Heightmap using fluid simulation");

        return true;
    }

    private float randomRange(Random random, float min, float max) {
        return (random.nextFloat() * (max - min)) + min;
    }

    /**
     * Sets the number of times the fluid simulation should be iterated over
     * the heightmap. The more often this is, the less features (hills, etc)
     * the terrain will have, and the smoother it will be.
     *
     * @param iterations
     *            the number of iterations to do
     * @throws JmeException
     *             if iterations if not greater than zero
     */
    public void setIterations(int iterations) throws Exception {
        if (iterations <= 0) {
            throw new Exception(
                    "Number of iterations is not greater than zero");
        }
        this.iterations = iterations;
    }

    /**
     * Sets the maximum initial height of the terrain.
     *
     * @param maxInitialHeight
     *                        the maximum initial height
     * @see #setMinInitialHeight(int)
     */
    public void setMaxInitialHeight(float maxInitialHeight) {
        this.maxInitialHeight = maxInitialHeight;
    }

    /**
     * Sets the minimum initial height of the terrain.
     *
     * @param minInitialHeight
     *                        the minimum initial height
     * @see #setMaxInitialHeight(int)
     */
    public void setMinInitialHeight(float minInitialHeight) {
        this.minInitialHeight = minInitialHeight;
    }

    /**
     * Sets the distance between each node of the heightmap.
     *
     * @param nodeDistance
     *                       the distance between each node
     */
    public void setNodeDistance(float nodeDistance) {
        this.nodeDistance = nodeDistance;
    }

    /**
     * Sets the time-speed between each iteration of the fluid
     * simulation algortithm.
     *
     * @param timeStep
     *                       the time-step between each iteration
     */
    public void setTimeStep(float timeStep) {
        this.timeStep = timeStep;
    }

    /**
     * Sets the viscosity of the simulated fuid.
     *
     * @param viscosity
     *                      the viscosity of the fluid
     */
    public void setViscosity(float viscosity) {
        this.viscosity = viscosity;
    }

    /**
     * Sets the speed at which the waves trave.
     *
     * @param waveSpeed
     *                      the speed at which the waves travel
     */
    public void setWaveSpeed(float waveSpeed) {
        this.waveSpeed = waveSpeed;
    }
}