Skip to content

Instantly share code, notes, and snippets.

@jrenner
Created August 30, 2014 03:55
Show Gist options
  • Save jrenner/8c83532a1a3748951607 to your computer and use it in GitHub Desktop.
Save jrenner/8c83532a1a3748951607 to your computer and use it in GitHub Desktop.
HeightMap.java
package org.jrenner.tac;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.GdxRuntimeException;
import org.jrenner.tac.utils.Tools;
/** Heightmap (heightfield) implementation with adjustable height/width scaling and iterative smoothing
* @author jrenner */
public class HeightMap {
/** 2d array of heights for each data point */
float[][] heights;
/** vertical scaling factor for height */
private float heightScale;
/** scaling for width on the x-z plane (distance between each data point) */
private float widthScale;
/** computed minimum height in the heightmap */
private float min;
/** computed maximum height in the heightmap */
private float max;
/** width (x) of the height map in data points, not world units */
private int width;
/** depth (z) of the height map in data points, not world units */
private int depth;
/** center of the heightmap in world units */
private Vector3 center = new Vector3();
/** number of data points */
private int numPoints;
/**
* Creates a heightmap with default values
* @param file - 8-bit PNG file heightmap data
*/
public HeightMap(FileHandle file) {
this(file, 1f, 1f, true, 0);
}
/**
*
* @param file 8-bit PNG file heightmap data
* @param heightScale vertical scaling (y axis)
* @param widthScale width on the x-z plane (distance between points)
* @param whiteHigh height is represented by white (instead of black)
*/
public HeightMap(FileHandle file, float heightScale, float widthScale, boolean whiteHigh, int smoothingPasses) {
// TODO whiteHigh seems to breaking the normals
this.heightScale = heightScale;
this.widthScale = widthScale;
Pixmap pix = new Pixmap(file);
if (pix.getFormat() != Pixmap.Format.Alpha) {
throw new GdxRuntimeException("Pixmap must be format Pixmap.Alpha (8-bit Grayscale), not: " + pix.getFormat());
}
int pixWidth = pix.getWidth();
int pixHeight = pix.getHeight();
int w = pixWidth;
int h = pixHeight;
System.out.println("w,h: " + w + ", " + h);
heights = new float[h][w];
boolean countWidth = true;
for (int z = 0; z < pixHeight; z ++) {
depth++;
for (int x = 0; x < pixWidth; x ++) {
if (countWidth) width++;
//System.out.printf("pix value: (%d, %d): %d\n", x, z, pix.getPixel(x, z));
int height;
if (whiteHigh) {
height = 256 - (-1 * pix.getPixel(x, z));
} else {
height = -1 * pix.getPixel(x, z);
}
heights[z][x] = height;
numPoints++;
}
countWidth = false;
}
smoothVertexPositions(smoothingPasses);
updateDimensions();
}
private void setMinMaxHeights() {
min = Float.MAX_VALUE;
max = Float.MIN_VALUE;
for (int z = 0; z < heights.length; z++) {
for (int x = 0; x < heights[0].length; x++) {
float y = heights[z][x];
if (y < min) min = y;
if (y > max) max = y;
}
}
}
private void updateDimensions() {
setMinMaxHeights();
setCenter();
}
/** Create smoother terrain using averaging of height values
* @param passes number of smoothing passes (higher = smoother) */
private void smoothVertexPositions(int passes) {
for (int i = 0; i < passes; i++) {
// smooth along x
for (int z = 0; z < heights.length; z++) {
for (int x = 1; x < heights[z].length - 1; x += 1) {
float prev = heights[z][x - 1];
float y = heights[z][x];
float next = heights[z][x + 1];
float yAvg = (next + prev) / 2f;
heights[z][x] = (y + yAvg) / 2f;
}
}
// smooth along z
for (int x = 0; x < heights[0].length; x++) {
for (int z = 1; z < heights.length - 1; z += 1) {
float prev = heights[z - 1][x];
float y = heights[z][x];
float next = heights[z + 1][x];
float yAvg = (next + prev) / 2f;
heights[z][x] = (y + yAvg) / 2f;
}
}
}
updateDimensions();
}
private void setCenter() {
center.set(getWidthWorld() / 2, (min + max) / 2, getDepthWorld() / 2);
}
/** Get the center of the heightmap in world units */
public Vector3 getCenter() {
setCenter();
return center;
}
public float[][] getData() {
return heights;
}
public int getNumberOfPoints() {
return numPoints;
}
/** get the vertical height scaling */
public float getHeightScale() {
return heightScale;
}
/** get the width between data points in world units */
public float getWidthScale() {
return widthScale;
}
/** parameters assume heightmap's origin is at world coordinates x:0, z: 0
* @return true if point (x,z) is within the bounds of this heightmap */
public boolean containsPoint(float xf, float zf) {
xf /= widthScale;
zf /= widthScale;
int x = (int) Math.floor(xf);
int z = (int) Math.floor(zf);
if (x < 0 || z < 0) return false;
if (z > heights.length - 1) return false;
if (x > heights[z].length - 1) return false;
return true;
}
private Vector3 tmp = new Vector3();
private Vector3 tmp2 = new Vector3();
private Vector3 tmp3 = new Vector3();
private Vector3 tmp4 = new Vector3();
/** get height for single point at x,z coords, accounting for scale, does not interpolate using neighbors
* parameters assume heightmap starts at origin 0,0 */
public float getHeight(float xf, float zf) {
int x = worldCoordToIndex(xf);
int z = worldCoordToIndex(zf);
//System.out.println("getting height for: "+x+", "+z);
if (x < 0) x = 0;
if (z < 0) z = 0;
if (z >= heights.length) {
//System.out.println("WARN getHeight z index out of bounds: " + z);
z = heights.length - 1;
}
if (x >= heights[z].length) {
//System.out.println("WARN getHeight x index out of bounds: " + x);
x = heights[z].length - 1;
}
return heights[z][x] * heightScale;
}
/**
* Get the interpolated height for x,z coords, accounting for scale, interpolated using neighbors.
* This will give the interpolated height when the parameters lie somewhere between actual heightmap data points.
* parameters assume heightmap's origin is at world coordinates x:0, z: 0
* @return the scale-adjusted interpolated height at specified world coordinates */
public float getInterpolatedHeight(float xf, float zf) {
Vector3 a = tmp;
Vector3 b = tmp2;
Vector3 c = tmp3;
Vector3 d = tmp4;
float baseX = (float) Math.floor(xf / widthScale);
float baseZ = (float) Math.floor(zf / widthScale);
float x = baseX * widthScale;
float z = baseZ * widthScale;
float x2 = x + widthScale;
float z2 = z + widthScale;
a.set(x, getHeight(x , z2), z2);
b.set(x, getHeight(x , z), z);
c.set(x2, getHeight(x2, z), z);
d.set(x2, getHeight(x2, z2), z2);
float zFrac = 1f - (zf - z) / widthScale;
float xFrac = (xf - x) / widthScale;
float y = (1f - zFrac) * ((1-xFrac) * a.y + xFrac * d.y)
+ zFrac * ((1-xFrac) * b.y + xFrac * c.y);
return y;
}
private int worldCoordToIndex(float f) {
return (int) Math.floor(f / widthScale);
}
/** @return lowest elevation in the height map */
public float getMin() {
return min;
}
/** @return height elevation in the height map */
public float getMax() {
return max;
}
/** @return length of the map on the x axis in number of points */
public int getWidth() {
return width;
}
/** @return length of the map on the z axis in number of points */
public int getDepth() {
return depth;
}
/** @return length of the map on the x axis in world units */
public float getWidthWorld() {
return width * widthScale;
}
/** @return length of the map on the z axis in world units */
public float getDepthWorld() {
return depth * widthScale;
}
/** @return the number of data points in the height map */
public int getNumPoints() {
return width * depth;
}
@Override
public String toString() {
getCenter();
return String.format("[HeightMap] min/max: %.1f, %.1f - World w/h: %.1f, %.1f - points: %d, center: %.1f, %.1f, %.1f",
min, max, getWidthWorld(), getDepthWorld(), getNumPoints(), center.x, center.y, center.z);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment