/*
* 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.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* AbstractHeightMap
provides a base implementation of height
* data for terrain rendering. The loading of the data is dependent on the
* subclass. The abstract implementation provides a means to retrieve the height
* data and to save it.
*
* It is the general contract that any subclass provide a means of editing
* required attributes and calling load
again to recreate a
* heightfield with these new parameters.
*
* @author Mark Powell
* @version $Id: AbstractHeightMap.java 4133 2009-03-19 20:40:11Z blaine.dev $
*/
public abstract class AbstractHeightMap implements HeightMap {
private static final Logger logger = Logger.getLogger(AbstractHeightMap.class.getName());
/** Height data information. */
protected float[] heightData = null;
/** The size of the height map's width. */
protected int size = 0;
/** Allows scaling the Y height of the map. */
protected float heightScale = 1.0f;
/** The filter is used to erode the terrain. */
protected float filter = 0.5f;
/** The range used to normalize terrain */
public static float NORMALIZE_RANGE = 255f;
/**
* unloadHeightMap
clears the data of the height map. This
* insures it is ready for reloading.
*/
public void unloadHeightMap() {
heightData = null;
}
/**
* setHeightScale
sets the scale of the height values.
* Typically, the height is a little too extreme and should be scaled to a
* smaller value (i.e. 0.25), to produce cleaner slopes.
*
* @param scale
* the scale to multiply height values by.
*/
public void setHeightScale(float scale) {
heightScale = scale;
}
/**
* setHeightAtPoint
sets the height value for a given
* coordinate. It is recommended that the height value be within the 0 - 255
* range.
*
* @param height
* the new height for the coordinate.
* @param x
* the x (east/west) coordinate.
* @param z
* the z (north/south) coordinate.
*/
public void setHeightAtPoint(float height, int x, int z) {
heightData[x + (z * size)] = height;
}
/**
* setSize
sets the size of the terrain where the area is
* size x size.
*
* @param size
* the new size of the terrain.
* @throws Exception
*
* @throws JmeException
* if the size is less than or equal to zero.
*/
public void setSize(int size) throws Exception {
if (size <= 0) {
throw new Exception("size must be greater than zero.");
}
this.size = size;
}
/**
* setFilter
sets the erosion value for the filter. This
* value must be between 0 and 1, where 0.2 - 0.4 produces arguably the best
* results.
*
* @param filter
* the erosion value.
* @throws Exception
* @throws JmeException
* if filter is less than 0 or greater than 1.
*/
public void setMagnificationFilter(float filter) throws Exception {
if (filter < 0 || filter >= 1) {
throw new Exception("filter must be between 0 and 1");
}
this.filter = filter;
}
/**
* getTrueHeightAtPoint
returns the non-scaled value at the
* point provided.
*
* @param x
* the x (east/west) coordinate.
* @param z
* the z (north/south) coordinate.
* @return the value at (x,z).
*/
public float getTrueHeightAtPoint(int x, int z) {
//logger.info( heightData[x + (z*size)]);
return heightData[x + (z * size)];
}
/**
* getScaledHeightAtPoint
returns the scaled value at the
* point provided.
*
* @param x
* the x (east/west) coordinate.
* @param z
* the z (north/south) coordinate.
* @return the scaled value at (x, z).
*/
public float getScaledHeightAtPoint(int x, int z) {
return ((heightData[x + (z * size)]) * heightScale);
}
/**
* getInterpolatedHeight
returns the height of a point that
* does not fall directly on the height posts.
*
* @param x
* the x coordinate of the point.
* @param z
* the y coordinate of the point.
* @return the interpolated height at this point.
*/
public float getInterpolatedHeight(float x, float z) {
float low, highX, highZ;
float intX, intZ;
float interpolation;
low = getScaledHeightAtPoint((int) x, (int) z);
if (x + 1 >= size) {
return low;
}
highX = getScaledHeightAtPoint((int) x + 1, (int) z);
interpolation = x - (int) x;
intX = ((highX - low) * interpolation) + low;
if (z + 1 >= size) {
return low;
}
highZ = getScaledHeightAtPoint((int) x, (int) z + 1);
interpolation = z - (int) z;
intZ = ((highZ - low) * interpolation) + low;
return ((intX + intZ) / 2);
}
/**
* getHeightMap
returns the entire grid of height data.
*
* @return the grid of height data.
*/
public float[] getHeightMap() {
return heightData;
}
/**
* Build a new array of height data with the scaled values.
* @return
*/
public float[] getScaledHeightMap() {
float[] hm = new float[heightData.length];
for (int i=0; igetSize returns the size of one side the height map. Where
* the area of the height map is size x size.
*
* @return the size of a single side.
*/
public int getSize() {
return size;
}
/**
* save
will save the heightmap data into a new RAW file
* denoted by the supplied filename.
*
* @param filename
* the file name to save the current data as.
* @return true if the save was successful, false otherwise.
* @throws Exception
*
* @throws JmeException
* if filename is null.
*/
public boolean save(String filename) throws Exception {
if (null == filename) {
throw new Exception("Filename must not be null");
}
//open the streams and send the height data to the file.
try {
FileOutputStream fos = new FileOutputStream(filename);
DataOutputStream dos = new DataOutputStream(fos);
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
dos.write((int) heightData[j + (i * size)]);
}
}
fos.close();
dos.close();
} catch (FileNotFoundException e) {
logger.log(Level.WARNING, "Error opening file {0}", filename);
return false;
} catch (IOException e) {
logger.log(Level.WARNING, "Error writing to file {0}", filename);
return false;
}
logger.log(Level.INFO, "Saved terrain to {0}", filename);
return true;
}
/**
* normalizeTerrain
takes the current terrain data and
* converts it to values between 0 and value
.
*
* @param value
* the value to normalize to.
*/
public void normalizeTerrain(float value) {
float currentMin, currentMax;
float height;
currentMin = heightData[0];
currentMax = heightData[0];
//find the min/max values of the height fTemptemptempBuffer
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
if (heightData[i + j * size] > currentMax) {
currentMax = heightData[i + j * size];
} else if (heightData[i + j * size] < currentMin) {
currentMin = heightData[i + j * size];
}
}
}
//find the range of the altitude
if (currentMax <= currentMin) {
return;
}
height = currentMax - currentMin;
//scale the values to a range of 0-255
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
heightData[i + j * size] = ((heightData[i + j * size] - currentMin) / height) * value;
}
}
}
/**
* Find the minimum and maximum height values.
* @return a float array with two value: min height, max height
*/
public float[] findMinMaxHeights() {
float[] minmax = new float[2];
float currentMin, currentMax;
currentMin = heightData[0];
currentMax = heightData[0];
for (int i = 0; i < heightData.length; i++) {
if (heightData[i] > currentMax) {
currentMax = heightData[i];
} else if (heightData[i] < currentMin) {
currentMin = heightData[i];
}
}
minmax[0] = currentMin;
minmax[1] = currentMax;
return minmax;
}
/**
* erodeTerrain
is a convenience method that applies the FIR
* filter to a given height map. This simulates water errosion.
*
* @see setFilter
*/
public void erodeTerrain() {
//erode left to right
float v;
for (int i = 0; i < size; i++) {
v = heightData[i];
for (int j = 1; j < size; j++) {
heightData[i + j * size] = filter * v + (1 - filter) * heightData[i + j * size];
v = heightData[i + j * size];
}
}
//erode right to left
for (int i = size - 1; i >= 0; i--) {
v = heightData[i];
for (int j = 0; j < size; j++) {
heightData[i + j * size] = filter * v + (1 - filter) * heightData[i + j * size];
v = heightData[i + j * size];
//erodeBand(tempBuffer[size * i + size - 1], -1);
}
}
//erode top to bottom
for (int i = 0; i < size; i++) {
v = heightData[i];
for (int j = 0; j < size; j++) {
heightData[i + j * size] = filter * v + (1 - filter) * heightData[i + j * size];
v = heightData[i + j * size];
}
}
//erode from bottom to top
for (int i = size - 1; i >= 0; i--) {
v = heightData[i];
for (int j = 0; j < size; j++) {
heightData[i + j * size] = filter * v + (1 - filter) * heightData[i + j * size];
v = heightData[i + j * size];
}
}
}
/**
* Flattens out the valleys. The flatten algorithm makes the valleys more
* prominent while keeping the hills mostly intact. This effect is based on
* what happens when values below one are squared. The terrain will be
* normalized between 0 and 1 for this function to work.
*
* @param flattening
* the power of flattening applied, 1 means none
*/
public void flatten(byte flattening) {
// If flattening is one we can skip the calculations
// since it wouldn't change anything. (e.g. 2 power 1 = 2)
if (flattening <= 1) {
return;
}
float[] minmax = findMinMaxHeights();
normalizeTerrain(1f);
for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {
float flat = 1.0f;
float original = heightData[x + y * size];
// Flatten as many times as desired;
for (int i = 0; i < flattening; i++) {
flat *= original;
}
heightData[x + y * size] = flat;
}
}
// re-normalize back to its oraginal height range
float height = minmax[1] - minmax[0];
normalizeTerrain(height);
}
/**
* Smooth the terrain. For each node, its 8 neighbors heights
* are averaged and will participate in the node new height
* by a factor np
between 0 and 1
*
* You must first load() the heightmap data before this will have any effect.
*
* @param np
* The factor to what extend the neighbors average has an influence.
* Value of 0 will ignore neighbors (no smoothing)
* Value of 1 will ignore the node old height.
*/
public void smooth(float np) {
smooth(np, 1);
}
/**
* Smooth the terrain. For each node, its X(determined by radius) neighbors heights
* are averaged and will participate in the node new height
* by a factor np
between 0 and 1
*
* You must first load() the heightmap data before this will have any effect.
*
* @param np
* The factor to what extend the neighbors average has an influence.
* Value of 0 will ignore neighbors (no smoothing)
* Value of 1 will ignore the node old height.
*/
public void smooth(float np, int radius) {
if (np < 0 || np > 1) {
return;
}
if (radius == 0)
radius = 1;
for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {
int neighNumber = 0;
float neighAverage = 0;
for (int rx = -radius; rx <= radius; rx++) {
for (int ry = -radius; ry <= radius; ry++) {
if (x+rx < 0 || x+rx >= size) {
continue;
}
if (y+ry < 0 || y+ry >= size) {
continue;
}
neighNumber++;
neighAverage += heightData[(x+rx) + (y+ry) * size];
}
}
neighAverage /= neighNumber;
float cp = 1 - np;
heightData[x + y * size] = neighAverage * np + heightData[x + y * size] * cp;
}
}
}
}