1 2package com.badlogic.gdx.tests.g3d; 3 4import java.nio.ByteBuffer; 5 6import com.badlogic.gdx.graphics.Color; 7import com.badlogic.gdx.graphics.Mesh; 8import com.badlogic.gdx.graphics.Pixmap; 9import com.badlogic.gdx.graphics.Pixmap.Format; 10import com.badlogic.gdx.graphics.VertexAttributes.Usage; 11import com.badlogic.gdx.graphics.VertexAttributes; 12import com.badlogic.gdx.graphics.g3d.utils.MeshBuilder; 13import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder; 14import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder.VertexInfo; 15import com.badlogic.gdx.math.Vector2; 16import com.badlogic.gdx.math.Vector3; 17import com.badlogic.gdx.utils.Disposable; 18import com.badlogic.gdx.utils.GdxRuntimeException; 19 20/** This is a test class, showing how one could implement a height field. See also {@link HeightMapTest}. Do not expect this to be 21 * a fully supported and implemented height field class. 22 * <p /> 23 * Represents a HeightField, which is an evenly spaced grid of values, where each value defines the height on that position of the 24 * grid, so forming a 3D shape. Typically used for (relatively simple) terrains and such. See <a 25 * href="http://en.wikipedia.org/wiki/Heightmap">wikipedia</a> for more information. 26 * <p /> 27 * A height field has a width and height, specifying the width and height of the grid. Points on this grid are specified using 28 * integer values, named "x" and "y". Do not confuse these with the x, y and z floating point values representing coordinates in 29 * world space. 30 * <p /> 31 * The values of the heightfield are normalized. Meaning that they typically range from 0 to 1 (but they can be negative or more 32 * than one). The plane of the heightfield can be specified using the {@link #corner00}, {@link #corner01}, {@link #corner10} and 33 * {@link #corner11} members. Where `corner00` is the location on the grid at x:0, y;0, `corner01` at x:0, y:height-1, `corner10` 34 * at x:width-1, y:0 and `corner11` the location on the grid at x:width-1, y:height-1. 35 * <p /> 36 * The height and direction of the field can be set using the {@link #magnitude} vector. Typically this should be the vector 37 * perpendicular to the heightfield. E.g. if the field is on the XZ plane, then the magnitude is typically pointing on the Y axis. 38 * The length of the `magnitude` specifies the height of the height field. In other words, the word coordinate of a point on the 39 * grid is specified as: 40 * <p /> 41 * base[y * width + x] + magnitude * value[y * width + x] 42 * <p /> 43 * Use the {@link #getPositionAt(Vector3, int, int)} method to get the coordinate of a specific point on the grid. 44 * <p /> 45 * You can set this heightfield using the constructor or one of the `set` methods. E.g. by specifying an array of values or a 46 * {@link Pixmap}. The latter can be used to load a HeightMap, which is an image loaded from disc of which each texel is used to 47 * specify the value for each point on the field. Be aware that the total number of vertices cannot exceed 32k. Using a large 48 * height map will result in unpredicted results. 49 * <p /> 50 * You can also manually modify the heightfield by directly accessing the {@link #data} member. The index within this array can be 51 * calculates as: `y * width + x`. E.g. `field.data[y * field.width + x] = value;`. When you modify the data then you can update 52 * the {@link #mesh} using the {@link #update()} method. 53 * <p /> 54 * The {@link #mesh} member can be used to render the height field. The vertex attributes this mesh contains are specified in the 55 * constructor. There are two ways for generating the mesh: smooth and sharp. 56 * <p /> 57 * Smooth can be forced by specifying `true` for the `smooth` argument of the constructor. Otherwise it will be based on whether 58 * the specified vertex attributes contains a normal attribute. If there is no normal attribute then the mesh will always be 59 * smooth (even when you specify `false` in the constructor). In this case the number of vertices is the same as the amount of 60 * grid points. Causing vertices to be shared amongst multiple faces. 61 * <p /> 62 * Sharp will be used if the vertex attributes contains a normal attribute and you didnt specify `true` for the `smooth` argument 63 * of the constructor. This will cause the number of vertices to be around four times the amount grid points and each normal is 64 * estimated for each face instead of each point. 65 * @author Xoppa */ 66public class HeightField implements Disposable { 67 public final Vector2 uvOffset = new Vector2(0, 0); 68 public final Vector2 uvScale = new Vector2(1, 1); 69 public final Color color00 = new Color(Color.WHITE); 70 public final Color color10 = new Color(Color.WHITE); 71 public final Color color01 = new Color(Color.WHITE); 72 public final Color color11 = new Color(Color.WHITE); 73 public final Vector3 corner00 = new Vector3(0, 0, 0); 74 public final Vector3 corner10 = new Vector3(1, 0, 0); 75 public final Vector3 corner01 = new Vector3(0, 0, 1); 76 public final Vector3 corner11 = new Vector3(1, 0, 1); 77 public final Vector3 magnitude = new Vector3(0, 1, 0); 78 79 public final float[] data; 80 public final int width; 81 public final int height; 82 public final boolean smooth; 83 public final Mesh mesh; 84 85 private final float vertices[]; 86 private final int stride; 87 88 private final int posPos; 89 private final int norPos; 90 private final int uvPos; 91 private final int colPos; 92 93 private final MeshPartBuilder.VertexInfo vertex00 = new MeshPartBuilder.VertexInfo(); 94 private final MeshPartBuilder.VertexInfo vertex10 = new MeshPartBuilder.VertexInfo(); 95 private final MeshPartBuilder.VertexInfo vertex01 = new MeshPartBuilder.VertexInfo(); 96 private final MeshPartBuilder.VertexInfo vertex11 = new MeshPartBuilder.VertexInfo(); 97 98 private final Vector3 tmpV1 = new Vector3(); 99 private final Vector3 tmpV2 = new Vector3(); 100 private final Vector3 tmpV3 = new Vector3(); 101 private final Vector3 tmpV4 = new Vector3(); 102 private final Vector3 tmpV5 = new Vector3(); 103 private final Vector3 tmpV6 = new Vector3(); 104 private final Vector3 tmpV7 = new Vector3(); 105 private final Vector3 tmpV8 = new Vector3(); 106 private final Vector3 tmpV9 = new Vector3(); 107 private final Color tmpC = new Color(); 108 109 public HeightField (boolean isStatic, final Pixmap map, boolean smooth, int attributes) { 110 this(isStatic, map.getWidth(), map.getHeight(), smooth, attributes); 111 set(map); 112 } 113 114 public HeightField (boolean isStatic, final ByteBuffer colorData, final Pixmap.Format format, int width, int height, 115 boolean smooth, int attributes) { 116 this(isStatic, width, height, smooth, attributes); 117 set(colorData, format); 118 } 119 120 public HeightField (boolean isStatic, final float[] data, int width, int height, boolean smooth, int attributes) { 121 this(isStatic, width, height, smooth, attributes); 122 set(data); 123 } 124 125 public HeightField (boolean isStatic, int width, int height, boolean smooth, int attributes) { 126 this(isStatic, width, height, smooth, MeshBuilder.createAttributes(attributes)); 127 } 128 129 public HeightField (boolean isStatic, int width, int height, boolean smooth, VertexAttributes attributes) { 130 this.posPos = attributes.getOffset(Usage.Position, -1); 131 this.norPos = attributes.getOffset(Usage.Normal, -1); 132 this.uvPos = attributes.getOffset(Usage.TextureCoordinates, -1); 133 this.colPos = attributes.getOffset(Usage.ColorUnpacked, -1); 134 smooth = smooth || (norPos < 0); // cant have sharp edges without normals 135 136 this.width = width; 137 this.height = height; 138 this.smooth = smooth; 139 this.data = new float[width * height]; 140 141 this.stride = attributes.vertexSize / 4; 142 143 final int numVertices = smooth ? width * height : (width - 1) * (height - 1) * 4; 144 final int numIndices = (width - 1) * (height - 1) * 6; 145 146 this.mesh = new Mesh(isStatic, numVertices, numIndices, attributes); 147 this.vertices = new float[numVertices * stride]; 148 149 setIndices(); 150 } 151 152 private void setIndices () { 153 final int w = width - 1; 154 final int h = height - 1; 155 short indices[] = new short[w * h * 6]; 156 int i = -1; 157 for (int y = 0; y < h; ++y) { 158 for (int x = 0; x < w; ++x) { 159 final int c00 = smooth ? (y * width + x) : (y * 2 * w + x * 2); 160 final int c10 = c00 + 1; 161 final int c01 = c00 + (smooth ? width : w * 2); 162 final int c11 = c10 + (smooth ? width : w * 2); 163 indices[++i] = (short)c11; 164 indices[++i] = (short)c10; 165 indices[++i] = (short)c00; 166 indices[++i] = (short)c00; 167 indices[++i] = (short)c01; 168 indices[++i] = (short)c11; 169 } 170 } 171 mesh.setIndices(indices); 172 } 173 174 public void update () { 175 if (smooth) { 176 if (norPos < 0) 177 updateSimple(); 178 else 179 updateSmooth(); 180 } else 181 updateSharp(); 182 } 183 184 private void updateSmooth () { 185 for (int x = 0; x < width; ++x) { 186 for (int y = 0; y < height; ++y) { 187 VertexInfo v = getVertexAt(vertex00, x, y); 188 getWeightedNormalAt(v.normal, x, y); 189 setVertex(y * width + x, v); 190 } 191 } 192 mesh.setVertices(vertices); 193 } 194 195 private void updateSimple () { 196 for (int x = 0; x < width; ++x) { 197 for (int y = 0; y < height; ++y) { 198 setVertex(y * width + x, getVertexAt(vertex00, x, y)); 199 } 200 } 201 mesh.setVertices(vertices); 202 } 203 204 private void updateSharp () { 205 final int w = width - 1; 206 final int h = height - 1; 207 for (int y = 0; y < h; ++y) { 208 for (int x = 0; x < w; ++x) { 209 final int c00 = (y * 2 * w + x * 2); 210 final int c10 = c00 + 1; 211 final int c01 = c00 + w * 2; 212 final int c11 = c10 + w * 2; 213 VertexInfo v00 = getVertexAt(vertex00, x, y); 214 VertexInfo v10 = getVertexAt(vertex10, x + 1, y); 215 VertexInfo v01 = getVertexAt(vertex01, x, y + 1); 216 VertexInfo v11 = getVertexAt(vertex11, x + 1, y + 1); 217 v01.normal.set(v01.position).sub(v00.position).nor().crs(tmpV1.set(v11.position).sub(v01.position).nor()); 218 v10.normal.set(v10.position).sub(v11.position).nor().crs(tmpV1.set(v00.position).sub(v10.position).nor()); 219 v00.normal.set(v01.normal).lerp(v10.normal, .5f); 220 v11.normal.set(v00.normal); 221 222 setVertex(c00, v00); 223 setVertex(c10, v10); 224 setVertex(c01, v01); 225 setVertex(c11, v11); 226 } 227 } 228 mesh.setVertices(vertices); 229 } 230 231 /** Does not set the normal member! */ 232 protected VertexInfo getVertexAt (final VertexInfo out, int x, int y) { 233 final float dx = (float)x / (float)(width - 1); 234 final float dy = (float)y / (float)(height - 1); 235 final float a = data[y * width + x]; 236 out.position.set(corner00).lerp(corner10, dx).lerp(tmpV1.set(corner01).lerp(corner11, dx), dy); 237 out.position.add(tmpV1.set(magnitude).scl(a)); 238 out.color.set(color00).lerp(color10, dx).lerp(tmpC.set(color01).lerp(color11, dx), dy); 239 out.uv.set(dx, dy).scl(uvScale).add(uvOffset); 240 return out; 241 } 242 243 public Vector3 getPositionAt (Vector3 out, int x, int y) { 244 final float dx = (float)x / (float)(width - 1); 245 final float dy = (float)y / (float)(height - 1); 246 final float a = data[y * width + x]; 247 out.set(corner00).lerp(corner10, dx).lerp(tmpV1.set(corner01).lerp(corner11, dx), dy); 248 out.add(tmpV1.set(magnitude).scl(a)); 249 return out; 250 } 251 252 public Vector3 getWeightedNormalAt (Vector3 out, int x, int y) { 253// This commented code is based on http://www.flipcode.com/archives/Calculating_Vertex_Normals_for_Height_Maps.shtml 254// Note that this approach only works for a heightfield on the XZ plane with a magnitude on the y axis 255// float sx = data[(x < width - 1 ? x + 1 : x) + y * width] + data[(x > 0 ? x-1 : x) + y * width]; 256// if (x == 0 || x == (width - 1)) 257// sx *= 2f; 258// float sy = data[(y < height - 1 ? y + 1 : y) * width + x] + data[(y > 0 ? y-1 : y) * width + x]; 259// if (y == 0 || y == (height - 1)) 260// sy *= 2f; 261// float xScale = (corner11.x - corner00.x) / (width - 1f); 262// float zScale = (corner11.z - corner00.z) / (height - 1f); 263// float yScale = magnitude.len(); 264// out.set(-sx * yScale, 2f * xScale, sy*yScale*xScale / zScale).nor(); 265// return out; 266 267// The following approach weights the normal of the four triangles (half quad) surrounding the position. 268// A more accurate approach would be to weight the normal of the actual triangles. 269 int faces = 0; 270 out.set(0, 0, 0); 271 272 Vector3 center = getPositionAt(tmpV2, x, y); 273 Vector3 left = x > 0 ? getPositionAt(tmpV3, x - 1, y) : null; 274 Vector3 right = x < (width - 1) ? getPositionAt(tmpV4, x + 1, y) : null; 275 Vector3 bottom = y > 0 ? getPositionAt(tmpV5, x, y - 1) : null; 276 Vector3 top = y < (height - 1) ? getPositionAt(tmpV6, x, y + 1) : null; 277 if (top != null && left != null) { 278 out.add(tmpV7.set(top).sub(center).nor().crs(tmpV8.set(center).sub(left).nor()).nor()); 279 faces++; 280 } 281 if (left != null && bottom != null) { 282 out.add(tmpV7.set(left).sub(center).nor().crs(tmpV8.set(center).sub(bottom).nor()).nor()); 283 faces++; 284 } 285 if (bottom != null && right != null) { 286 out.add(tmpV7.set(bottom).sub(center).nor().crs(tmpV8.set(center).sub(right).nor()).nor()); 287 faces++; 288 } 289 if (right != null && top != null) { 290 out.add(tmpV7.set(right).sub(center).nor().crs(tmpV8.set(center).sub(top).nor()).nor()); 291 faces++; 292 } 293 if (faces != 0) 294 out.scl(1f / (float)faces); 295 else 296 out.set(magnitude).nor(); 297 return out; 298 } 299 300 protected void setVertex (int index, VertexInfo info) { 301 index *= stride; 302 if (posPos >= 0) { 303 vertices[index + posPos + 0] = info.position.x; 304 vertices[index + posPos + 1] = info.position.y; 305 vertices[index + posPos + 2] = info.position.z; 306 } 307 if (norPos >= 0) { 308 vertices[index + norPos + 0] = info.normal.x; 309 vertices[index + norPos + 1] = info.normal.y; 310 vertices[index + norPos + 2] = info.normal.z; 311 } 312 if (uvPos >= 0) { 313 vertices[index + uvPos + 0] = info.uv.x; 314 vertices[index + uvPos + 1] = info.uv.y; 315 } 316 if (colPos >= 0) { 317 vertices[index + colPos + 0] = info.color.r; 318 vertices[index + colPos + 1] = info.color.g; 319 vertices[index + colPos + 2] = info.color.b; 320 vertices[index + colPos + 3] = info.color.a; 321 } 322 } 323 324 public void set (final Pixmap map) { 325 if (map.getWidth() != width || map.getHeight() != height) throw new GdxRuntimeException("Incorrect map size"); 326 set(map.getPixels(), map.getFormat()); 327 } 328 329 public void set (final ByteBuffer colorData, final Pixmap.Format format) { 330 set(heightColorsToMap(colorData, format, width, height)); 331 } 332 333 public void set (float[] data) { 334 set(data, 0); 335 } 336 337 public void set (float[] data, int offset) { 338 if (this.data.length > (data.length - offset)) throw new GdxRuntimeException("Incorrect data size"); 339 System.arraycopy(data, offset, this.data, 0, this.data.length); 340 update(); 341 } 342 343 @Override 344 public void dispose () { 345 mesh.dispose(); 346 } 347 348 /** Simply creates an array containing only all the red components of the data. */ 349 public static float[] heightColorsToMap (final ByteBuffer data, final Pixmap.Format format, int width, int height) { 350 final int bytesPerColor = (format == Format.RGB888 ? 3 : (format == Format.RGBA8888 ? 4 : 0)); 351 if (bytesPerColor == 0) throw new GdxRuntimeException("Unsupported format, should be either RGB8 or RGBA8"); 352 if (data.remaining() < (width * height * bytesPerColor)) throw new GdxRuntimeException("Incorrect map size"); 353 354 final int startPos = data.position(); 355 byte[] source = null; 356 int sourceOffset = 0; 357 if (data.hasArray() && !data.isReadOnly()) { 358 source = data.array(); 359 sourceOffset = data.arrayOffset() + startPos; 360 } else { 361 source = new byte[width * height * bytesPerColor]; 362 data.get(source); 363 data.position(startPos); 364 } 365 366 float[] dest = new float[width * height]; 367 for (int i = 0; i < dest.length; ++i) { 368 int v = source[sourceOffset + i * bytesPerColor]; 369 v = v < 0 ? 256 + v : v; 370 dest[i] = (float)v / 255f; 371 } 372 373 return dest; 374 } 375} 376