/* * 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.io.DataOutputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; /** * <code>AbstractHeightMap</code> 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 <code>load</code> 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; /** * <code>unloadHeightMap</code> clears the data of the height map. This * insures it is ready for reloading. */ public void unloadHeightMap() { heightData = null; } /** * <code>setHeightScale</code> 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; } /** * <code>setHeightAtPoint</code> 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; } /** * <code>setSize</code> 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; } /** * <code>setFilter</code> 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; } /** * <code>getTrueHeightAtPoint</code> 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.fine( heightData[x + (z*size)]); return heightData[x + (z * size)]; } /** * <code>getScaledHeightAtPoint</code> 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); } /** * <code>getInterpolatedHeight</code> 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); } /** * <code>getHeightMap</code> 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; i<heightData.length; i++) { hm[i] = heightScale * heightData[i]; } return hm; } /** * <code>getSize</code> 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; } /** * <code>save</code> 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. FileOutputStream fos = null; DataOutputStream dos = null; try { fos = new FileOutputStream(filename); 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; } finally { if (fos != null) { fos.close(); } if (dos != null) { dos.close(); } } logger.log(Level.FINE, "Saved terrain to {0}", filename); return true; } /** * <code>normalizeTerrain</code> takes the current terrain data and * converts it to values between 0 and <code>value</code>. * * @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; } /** * <code>erodeTerrain</code> 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 <code>np</code> 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 <code>np</code> 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; } } } }