• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
unloadHeightMap()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      */
setHeightScale(float scale)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      */
setHeightAtPoint(float height, int x, int z)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      */
setSize(int size)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      */
setMagnificationFilter(float filter)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      */
getTrueHeightAtPoint(int x, int z)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      */
getScaledHeightAtPoint(int x, int z)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      */
getInterpolatedHeight(float x, float z)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      */
getHeightMap()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      */
getScaledHeightMap()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      */
getSize()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      */
save(String filename)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      */
normalizeTerrain(float value)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      */
findMinMaxHeights()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      */
erodeTerrain()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      */
flatten(byte flattening)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      */
smooth(float np)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      */
smooth(float np, int radius)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