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