Home | History | Annotate | Download | only in heightmap
      1 /*
      2  * Copyright (c) 2009-2010 jMonkeyEngine
      3  * All rights reserved.
      4  *
      5  * Redistribution and use in source and binary forms, with or without
      6  * modification, are permitted provided that the following conditions are
      7  * met:
      8  *
      9  * * Redistributions of source code must retain the above copyright
     10  *   notice, this list of conditions and the following disclaimer.
     11  *
     12  * * Redistributions in binary form must reproduce the above copyright
     13  *   notice, this list of conditions and the following disclaimer in the
     14  *   documentation and/or other materials provided with the distribution.
     15  *
     16  * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
     17  *   may be used to endorse or promote products derived from this software
     18  *   without specific prior written permission.
     19  *
     20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
     22  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     23  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
     24  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     25  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     26  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     27  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
     28  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
     29  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     30  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     31  */
     32 package com.jme3.terrain.heightmap;
     33 
     34 import java.io.DataOutputStream;
     35 import java.io.FileNotFoundException;
     36 import java.io.FileOutputStream;
     37 import java.io.IOException;
     38 import java.util.logging.Level;
     39 import java.util.logging.Logger;
     40 
     41 /**
     42  * <code>AbstractHeightMap</code> provides a base implementation of height
     43  * data for terrain rendering. The loading of the data is dependent on the
     44  * subclass. The abstract implementation provides a means to retrieve the height
     45  * data and to save it.
     46  *
     47  * It is the general contract that any subclass provide a means of editing
     48  * required attributes and calling <code>load</code> again to recreate a
     49  * heightfield with these new parameters.
     50  *
     51  * @author Mark Powell
     52  * @version $Id: AbstractHeightMap.java 4133 2009-03-19 20:40:11Z blaine.dev $
     53  */
     54 public abstract class AbstractHeightMap implements HeightMap {
     55 
     56     private static final Logger logger = Logger.getLogger(AbstractHeightMap.class.getName());
     57     /** Height data information. */
     58     protected float[] heightData = null;
     59     /** The size of the height map's width. */
     60     protected int size = 0;
     61     /** Allows scaling the Y height of the map. */
     62     protected float heightScale = 1.0f;
     63     /** The filter is used to erode the terrain. */
     64     protected float filter = 0.5f;
     65     /** The range used to normalize terrain */
     66     public static float NORMALIZE_RANGE = 255f;
     67 
     68     /**
     69      * <code>unloadHeightMap</code> clears the data of the height map. This
     70      * insures it is ready for reloading.
     71      */
     72     public void unloadHeightMap() {
     73         heightData = null;
     74     }
     75 
     76     /**
     77      * <code>setHeightScale</code> sets the scale of the height values.
     78      * Typically, the height is a little too extreme and should be scaled to a
     79      * smaller value (i.e. 0.25), to produce cleaner slopes.
     80      *
     81      * @param scale
     82      *            the scale to multiply height values by.
     83      */
     84     public void setHeightScale(float scale) {
     85         heightScale = scale;
     86     }
     87 
     88     /**
     89      * <code>setHeightAtPoint</code> sets the height value for a given
     90      * coordinate. It is recommended that the height value be within the 0 - 255
     91      * range.
     92      *
     93      * @param height
     94      *            the new height for the coordinate.
     95      * @param x
     96      *            the x (east/west) coordinate.
     97      * @param z
     98      *            the z (north/south) coordinate.
     99      */
    100     public void setHeightAtPoint(float height, int x, int z) {
    101         heightData[x + (z * size)] = height;
    102     }
    103 
    104     /**
    105      * <code>setSize</code> sets the size of the terrain where the area is
    106      * size x size.
    107      *
    108      * @param size
    109      *            the new size of the terrain.
    110      * @throws Exception
    111      *
    112      * @throws JmeException
    113      *             if the size is less than or equal to zero.
    114      */
    115     public void setSize(int size) throws Exception {
    116         if (size <= 0) {
    117             throw new Exception("size must be greater than zero.");
    118         }
    119 
    120         this.size = size;
    121     }
    122 
    123     /**
    124      * <code>setFilter</code> sets the erosion value for the filter. This
    125      * value must be between 0 and 1, where 0.2 - 0.4 produces arguably the best
    126      * results.
    127      *
    128      * @param filter
    129      *            the erosion value.
    130      * @throws Exception
    131      * @throws JmeException
    132      *             if filter is less than 0 or greater than 1.
    133      */
    134     public void setMagnificationFilter(float filter) throws Exception {
    135         if (filter < 0 || filter >= 1) {
    136             throw new Exception("filter must be between 0 and 1");
    137         }
    138         this.filter = filter;
    139     }
    140 
    141     /**
    142      * <code>getTrueHeightAtPoint</code> returns the non-scaled value at the
    143      * point provided.
    144      *
    145      * @param x
    146      *            the x (east/west) coordinate.
    147      * @param z
    148      *            the z (north/south) coordinate.
    149      * @return the value at (x,z).
    150      */
    151     public float getTrueHeightAtPoint(int x, int z) {
    152         //logger.info( heightData[x + (z*size)]);
    153         return heightData[x + (z * size)];
    154     }
    155 
    156     /**
    157      * <code>getScaledHeightAtPoint</code> returns the scaled value at the
    158      * point provided.
    159      *
    160      * @param x
    161      *            the x (east/west) coordinate.
    162      * @param z
    163      *            the z (north/south) coordinate.
    164      * @return the scaled value at (x, z).
    165      */
    166     public float getScaledHeightAtPoint(int x, int z) {
    167         return ((heightData[x + (z * size)]) * heightScale);
    168     }
    169 
    170     /**
    171      * <code>getInterpolatedHeight</code> returns the height of a point that
    172      * does not fall directly on the height posts.
    173      *
    174      * @param x
    175      *            the x coordinate of the point.
    176      * @param z
    177      *            the y coordinate of the point.
    178      * @return the interpolated height at this point.
    179      */
    180     public float getInterpolatedHeight(float x, float z) {
    181         float low, highX, highZ;
    182         float intX, intZ;
    183         float interpolation;
    184 
    185         low = getScaledHeightAtPoint((int) x, (int) z);
    186 
    187         if (x + 1 >= size) {
    188             return low;
    189         }
    190 
    191         highX = getScaledHeightAtPoint((int) x + 1, (int) z);
    192 
    193         interpolation = x - (int) x;
    194         intX = ((highX - low) * interpolation) + low;
    195 
    196         if (z + 1 >= size) {
    197             return low;
    198         }
    199 
    200         highZ = getScaledHeightAtPoint((int) x, (int) z + 1);
    201 
    202         interpolation = z - (int) z;
    203         intZ = ((highZ - low) * interpolation) + low;
    204 
    205         return ((intX + intZ) / 2);
    206     }
    207 
    208     /**
    209      * <code>getHeightMap</code> returns the entire grid of height data.
    210      *
    211      * @return the grid of height data.
    212      */
    213     public float[] getHeightMap() {
    214         return heightData;
    215     }
    216 
    217     /**
    218      * Build a new array of height data with the scaled values.
    219      * @return
    220      */
    221     public float[] getScaledHeightMap() {
    222         float[] hm = new float[heightData.length];
    223         for (int i=0; i<heightData.length; i++) {
    224             hm[i] = heightScale * heightData[i];
    225         }
    226         return hm;
    227     }
    228 
    229     /**
    230      * <code>getSize</code> returns the size of one side the height map. Where
    231      * the area of the height map is size x size.
    232      *
    233      * @return the size of a single side.
    234      */
    235     public int getSize() {
    236         return size;
    237     }
    238 
    239     /**
    240      * <code>save</code> will save the heightmap data into a new RAW file
    241      * denoted by the supplied filename.
    242      *
    243      * @param filename
    244      *            the file name to save the current data as.
    245      * @return true if the save was successful, false otherwise.
    246      * @throws Exception
    247      *
    248      * @throws JmeException
    249      *             if filename is null.
    250      */
    251     public boolean save(String filename) throws Exception {
    252         if (null == filename) {
    253             throw new Exception("Filename must not be null");
    254         }
    255         //open the streams and send the height data to the file.
    256         try {
    257             FileOutputStream fos = new FileOutputStream(filename);
    258             DataOutputStream dos = new DataOutputStream(fos);
    259             for (int i = 0; i < size; i++) {
    260                 for (int j = 0; j < size; j++) {
    261                     dos.write((int) heightData[j + (i * size)]);
    262                 }
    263             }
    264 
    265             fos.close();
    266             dos.close();
    267         } catch (FileNotFoundException e) {
    268             logger.log(Level.WARNING, "Error opening file {0}", filename);
    269             return false;
    270         } catch (IOException e) {
    271             logger.log(Level.WARNING, "Error writing to file {0}", filename);
    272             return false;
    273         }
    274 
    275         logger.log(Level.INFO, "Saved terrain to {0}", filename);
    276         return true;
    277     }
    278 
    279     /**
    280      * <code>normalizeTerrain</code> takes the current terrain data and
    281      * converts it to values between 0 and <code>value</code>.
    282      *
    283      * @param value
    284      *            the value to normalize to.
    285      */
    286     public void normalizeTerrain(float value) {
    287         float currentMin, currentMax;
    288         float height;
    289 
    290         currentMin = heightData[0];
    291         currentMax = heightData[0];
    292 
    293         //find the min/max values of the height fTemptemptempBuffer
    294         for (int i = 0; i < size; i++) {
    295             for (int j = 0; j < size; j++) {
    296                 if (heightData[i + j * size] > currentMax) {
    297                     currentMax = heightData[i + j * size];
    298                 } else if (heightData[i + j * size] < currentMin) {
    299                     currentMin = heightData[i + j * size];
    300                 }
    301             }
    302         }
    303 
    304         //find the range of the altitude
    305         if (currentMax <= currentMin) {
    306             return;
    307         }
    308 
    309         height = currentMax - currentMin;
    310 
    311         //scale the values to a range of 0-255
    312         for (int i = 0; i < size; i++) {
    313             for (int j = 0; j < size; j++) {
    314                 heightData[i + j * size] = ((heightData[i + j * size] - currentMin) / height) * value;
    315             }
    316         }
    317     }
    318 
    319     /**
    320      * Find the minimum and maximum height values.
    321      * @return a float array with two value: min height, max height
    322      */
    323     public float[] findMinMaxHeights() {
    324         float[] minmax = new float[2];
    325 
    326         float currentMin, currentMax;
    327         currentMin = heightData[0];
    328         currentMax = heightData[0];
    329 
    330         for (int i = 0; i < heightData.length; i++) {
    331             if (heightData[i] > currentMax) {
    332                 currentMax = heightData[i];
    333             } else if (heightData[i] < currentMin) {
    334                 currentMin = heightData[i];
    335             }
    336         }
    337         minmax[0] = currentMin;
    338         minmax[1] = currentMax;
    339         return minmax;
    340     }
    341 
    342     /**
    343      * <code>erodeTerrain</code> is a convenience method that applies the FIR
    344      * filter to a given height map. This simulates water errosion.
    345      *
    346      * @see setFilter
    347      */
    348     public void erodeTerrain() {
    349         //erode left to right
    350         float v;
    351 
    352         for (int i = 0; i < size; i++) {
    353             v = heightData[i];
    354             for (int j = 1; j < size; j++) {
    355                 heightData[i + j * size] = filter * v + (1 - filter) * heightData[i + j * size];
    356                 v = heightData[i + j * size];
    357             }
    358         }
    359 
    360         //erode right to left
    361         for (int i = size - 1; i >= 0; i--) {
    362             v = heightData[i];
    363             for (int j = 0; j < size; j++) {
    364                 heightData[i + j * size] = filter * v + (1 - filter) * heightData[i + j * size];
    365                 v = heightData[i + j * size];
    366                 //erodeBand(tempBuffer[size * i + size - 1], -1);
    367             }
    368         }
    369 
    370         //erode top to bottom
    371         for (int i = 0; i < size; i++) {
    372             v = heightData[i];
    373             for (int j = 0; j < size; j++) {
    374                 heightData[i + j * size] = filter * v + (1 - filter) * heightData[i + j * size];
    375                 v = heightData[i + j * size];
    376             }
    377         }
    378 
    379         //erode from bottom to top
    380         for (int i = size - 1; i >= 0; i--) {
    381             v = heightData[i];
    382             for (int j = 0; j < size; j++) {
    383                 heightData[i + j * size] = filter * v + (1 - filter) * heightData[i + j * size];
    384                 v = heightData[i + j * size];
    385             }
    386         }
    387     }
    388 
    389     /**
    390      * Flattens out the valleys. The flatten algorithm makes the valleys more
    391      * prominent while keeping the hills mostly intact. This effect is based on
    392      * what happens when values below one are squared. The terrain will be
    393      * normalized between 0 and 1 for this function to work.
    394      *
    395      * @param flattening
    396      *            the power of flattening applied, 1 means none
    397      */
    398     public void flatten(byte flattening) {
    399         // If flattening is one we can skip the calculations
    400         // since it wouldn't change anything. (e.g. 2 power 1 = 2)
    401         if (flattening <= 1) {
    402             return;
    403         }
    404 
    405         float[] minmax = findMinMaxHeights();
    406 
    407         normalizeTerrain(1f);
    408 
    409         for (int x = 0; x < size; x++) {
    410             for (int y = 0; y < size; y++) {
    411                 float flat = 1.0f;
    412                 float original = heightData[x + y * size];
    413 
    414                 // Flatten as many times as desired;
    415                 for (int i = 0; i < flattening; i++) {
    416                     flat *= original;
    417                 }
    418                 heightData[x + y * size] = flat;
    419             }
    420         }
    421 
    422         // re-normalize back to its oraginal height range
    423         float height = minmax[1] - minmax[0];
    424         normalizeTerrain(height);
    425     }
    426 
    427     /**
    428      * Smooth the terrain. For each node, its 8 neighbors heights
    429      * are averaged and will participate in the  node new height
    430      * by a factor <code>np</code> between 0 and 1
    431      *
    432      * You must first load() the heightmap data before this will have any effect.
    433      *
    434      * @param np
    435      *          The factor to what extend the neighbors average has an influence.
    436      *          Value of 0 will ignore neighbors (no smoothing)
    437      *          Value of 1 will ignore the node old height.
    438      */
    439     public void smooth(float np) {
    440         smooth(np, 1);
    441     }
    442 
    443     /**
    444      * Smooth the terrain. For each node, its X(determined by radius) neighbors heights
    445      * are averaged and will participate in the  node new height
    446      * by a factor <code>np</code> between 0 and 1
    447      *
    448      * You must first load() the heightmap data before this will have any effect.
    449      *
    450      * @param np
    451      *          The factor to what extend the neighbors average has an influence.
    452      *          Value of 0 will ignore neighbors (no smoothing)
    453      *          Value of 1 will ignore the node old height.
    454      */
    455     public void smooth(float np, int radius) {
    456         if (np < 0 || np > 1) {
    457             return;
    458         }
    459         if (radius == 0)
    460             radius = 1;
    461 
    462         for (int x = 0; x < size; x++) {
    463             for (int y = 0; y < size; y++) {
    464                 int neighNumber = 0;
    465                 float neighAverage = 0;
    466                 for (int rx = -radius; rx <= radius; rx++) {
    467                     for (int ry = -radius; ry <= radius; ry++) {
    468                         if (x+rx < 0 || x+rx >= size) {
    469                         continue;
    470                         }
    471                         if (y+ry < 0 || y+ry >= size) {
    472                             continue;
    473                         }
    474                         neighNumber++;
    475                         neighAverage += heightData[(x+rx) + (y+ry) * size];
    476                     }
    477                 }
    478 
    479                 neighAverage /= neighNumber;
    480                 float cp = 1 - np;
    481                 heightData[x + y * size] = neighAverage * np + heightData[x + y * size] * cp;
    482             }
    483         }
    484     }
    485 }
    486