/* * 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.logging.Logger; /** * <code>ParticleDepositionHeightMap</code> creates a heightmap based on the * Particle Deposition algorithm based on Jason Shankel's paper from * "Game Programming Gems". A heightmap is created using a Molecular beam * epitaxy, or MBE, for depositing thin layers of atoms on a substrate. * We drop a sequence of particles and simulate their flow across a surface * of previously dropped particles. This creates a few high peaks, for further * realism we can define a caldera. Similar to the way volcano's form * islands, rock is deposited via lava, when the lava cools, it recedes * into the volcano, creating the caldera. * * @author Mark Powell * @version $Id$ */ public class ParticleDepositionHeightMap extends AbstractHeightMap { private static final Logger logger = Logger.getLogger(ParticleDepositionHeightMap.class.getName()); //Attributes. private int jumps; private int peakWalk; private int minParticles; private int maxParticles; private float caldera; /** * Constructor sets the attributes of the Particle Deposition * Height Map and then generates the map. * * @param size the size of the terrain where the area is size x size. * @param jumps number of areas to drop particles. Can also think * of it as the number of peaks. * @param peakWalk determines how much to agitate the drop point * during a creation of a single peak. The lower the number * the more the drop point will be agitated. 1 will insure * agitation every round. * @param minParticles defines the minimum number of particles to * drop during a single jump. * @param maxParticles defines the maximum number of particles to * drop during a single jump. * @param caldera defines the altitude to invert a peak. This is * represented as a percentage, where 0.0 will not invert * anything, and 1.0 will invert all. * * @throws JmeException if any value is less than zero, and * if caldera is not between 0 and 1. If minParticles is greater than * max particles as well. */ public ParticleDepositionHeightMap( int size, int jumps, int peakWalk, int minParticles, int maxParticles, float caldera) throws Exception { if (size <= 0 || jumps < 0 || peakWalk < 0 || minParticles > maxParticles || minParticles < 0 || maxParticles < 0) { throw new Exception( "values must be greater than zero, " + "and minParticles must be greater than maxParticles"); } if (caldera < 0.0f || caldera > 1.0f) { throw new Exception( "Caldera level must be " + "between 0 and 1"); } this.size = size; this.jumps = jumps; this.peakWalk = peakWalk; this.minParticles = minParticles; this.maxParticles = maxParticles; this.caldera = caldera; load(); } /** * <code>load</code> generates the heightfield using the Particle Deposition * algorithm. <code>load</code> uses the latest attributes, so a call * to <code>load</code> is recommended if attributes have changed using * the set methods. */ public boolean load() { int x, y; int calderaX, calderaY; int sx, sy; int tx, ty; int m; float calderaStartPoint; float cutoff; int dx[] = {0, 1, 0, size - 1, 1, 1, size - 1, size - 1}; int dy[] = {1, 0, size - 1, 0, size - 1, 1, size - 1, 1}; float[][] tempBuffer = new float[size][size]; //map 0 unmarked, unvisited, 1 marked, unvisited, 2 marked visited. int[][] calderaMap = new int[size][size]; boolean done; int minx, maxx; int miny, maxy; if (null != heightData) { unloadHeightMap(); } heightData = new float[size * size]; //create peaks. for (int i = 0; i < jumps; i++) { //pick a random point. x = (int) (Math.rint(Math.random() * (size - 1))); y = (int) (Math.rint(Math.random() * (size - 1))); //set the caldera point. calderaX = x; calderaY = y; int numberParticles = (int) (Math.rint( (Math.random() * (maxParticles - minParticles)) + minParticles)); //drop particles. for (int j = 0; j < numberParticles; j++) { //check to see if we should aggitate the drop point. if (peakWalk != 0 && j % peakWalk == 0) { m = (int) (Math.rint(Math.random() * 7)); x = (x + dx[m] + size) % size; y = (y + dy[m] + size) % size; } //add the particle to the piont. tempBuffer[x][y] += 1; sx = x; sy = y; done = false; //cause the particle to "slide" down the slope and settle at //a low point. while (!done) { done = true; //check neighbors to see if we are higher. m = (int) (Math.rint((Math.random() * 8))); for (int jj = 0; jj < 8; jj++) { tx = (sx + dx[(jj + m) % 8]) % (size); ty = (sy + dy[(jj + m) % 8]) % (size); //move to the neighbor. if (tempBuffer[tx][ty] + 1.0f < tempBuffer[sx][sy]) { tempBuffer[tx][ty] += 1.0f; tempBuffer[sx][sy] -= 1.0f; sx = tx; sy = ty; done = false; break; } } } //This point is higher than the current caldera point, //so move the caldera here. if (tempBuffer[sx][sy] > tempBuffer[calderaX][calderaY]) { calderaX = sx; calderaY = sy; } } //apply the caldera. calderaStartPoint = tempBuffer[calderaX][calderaY]; cutoff = calderaStartPoint * (1.0f - caldera); minx = calderaX; maxx = calderaX; miny = calderaY; maxy = calderaY; calderaMap[calderaX][calderaY] = 1; done = false; while (!done) { done = true; sx = minx; sy = miny; tx = maxx; ty = maxy; for (x = sx; x <= tx; x++) { for (y = sy; y <= ty; y++) { calderaX = (x + size) % size; calderaY = (y + size) % size; if (calderaMap[calderaX][calderaY] == 1) { calderaMap[calderaX][calderaY] = 2; if (tempBuffer[calderaX][calderaY] > cutoff && tempBuffer[calderaX][calderaY] <= calderaStartPoint) { done = false; tempBuffer[calderaX][calderaY] = 2 * cutoff - tempBuffer[calderaX][calderaY]; //check the left and right neighbors calderaX = (calderaX + 1) % size; if (calderaMap[calderaX][calderaY] == 0) { if (x + 1 > maxx) { maxx = x + 1; } calderaMap[calderaX][calderaY] = 1; } calderaX = (calderaX + size - 2) % size; if (calderaMap[calderaX][calderaY] == 0) { if (x - 1 < minx) { minx = x - 1; } calderaMap[calderaX][calderaY] = 1; } //check the upper and lower neighbors. calderaX = (x + size) % size; calderaY = (calderaY + 1) % size; if (calderaMap[calderaX][calderaY] == 0) { if (y + 1 > maxy) { maxy = y + 1; } calderaMap[calderaX][calderaY] = 1; } calderaY = (calderaY + size - 2) % size; if (calderaMap[calderaX][calderaY] == 0) { if (y - 1 < miny) { miny = y - 1; } calderaMap[calderaX][calderaY] = 1; } } } } } } } //transfer the new terrain into the height map. for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { setHeightAtPoint((float) tempBuffer[i][j], j, i); } } erodeTerrain(); normalizeTerrain(NORMALIZE_RANGE); logger.fine("Created heightmap using Particle Deposition"); return false; } /** * <code>setJumps</code> sets the number of jumps or peaks that will * be created during the next call to <code>load</code>. * @param jumps the number of jumps to use for next load. * @throws JmeException if jumps is less than zero. */ public void setJumps(int jumps) throws Exception { if (jumps < 0) { throw new Exception("jumps must be positive"); } this.jumps = jumps; } /** * <code>setPeakWalk</code> sets how often the jump point will be * aggitated. The lower the peakWalk, the more often the point will * be aggitated. * * @param peakWalk the amount to aggitate the jump point. * @throws JmeException if peakWalk is negative or zero. */ public void setPeakWalk(int peakWalk) throws Exception { if (peakWalk <= 0) { throw new Exception( "peakWalk must be greater than " + "zero"); } this.peakWalk = peakWalk; } /** * <code>setCaldera</code> sets the level at which a peak will be * inverted. * * @param caldera the level at which a peak will be inverted. This must be * between 0 and 1, as it is represented as a percentage. * @throws JmeException if caldera is not between 0 and 1. */ public void setCaldera(float caldera) throws Exception { if (caldera < 0.0f || caldera > 1.0f) { throw new Exception( "Caldera level must be " + "between 0 and 1"); } this.caldera = caldera; } /** * <code>setMaxParticles</code> sets the maximum number of particles * for a single jump. * @param maxParticles the maximum number of particles for a single jump. * @throws JmeException if maxParticles is negative or less than * the current number of minParticles. */ public void setMaxParticles(int maxParticles) { this.maxParticles = maxParticles; } /** * <code>setMinParticles</code> sets the minimum number of particles * for a single jump. * @param minParticles the minimum number of particles for a single jump. * @throws JmeException if minParticles are greater than * the current maxParticles; */ public void setMinParticles(int minParticles) throws Exception { if (minParticles > maxParticles) { throw new Exception( "minParticles must be less " + "than the current maxParticles"); } this.minParticles = minParticles; } }