/* * Copyright (c) 2009-2012 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.fine("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; } }