blob: 63dccc6bee52a22db082b060bf81120115432df6 [file] [log] [blame]
/*
* 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.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.info("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;
}
}