1 2 package com.badlogic.gdx.tests.g3d; 3 4 import java.nio.ByteBuffer; 5 6 import com.badlogic.gdx.graphics.Color; 7 import com.badlogic.gdx.graphics.Mesh; 8 import com.badlogic.gdx.graphics.Pixmap; 9 import com.badlogic.gdx.graphics.Pixmap.Format; 10 import com.badlogic.gdx.graphics.VertexAttributes.Usage; 11 import com.badlogic.gdx.graphics.VertexAttributes; 12 import com.badlogic.gdx.graphics.g3d.utils.MeshBuilder; 13 import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder; 14 import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder.VertexInfo; 15 import com.badlogic.gdx.math.Vector2; 16 import com.badlogic.gdx.math.Vector3; 17 import com.badlogic.gdx.utils.Disposable; 18 import 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 */ 66 public 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 HeightField(boolean isStatic, final Pixmap map, boolean smooth, int attributes)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 HeightField(boolean isStatic, final ByteBuffer colorData, final Pixmap.Format format, int width, int height, boolean smooth, int attributes)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 HeightField(boolean isStatic, final float[] data, int width, int height, boolean smooth, int attributes)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 HeightField(boolean isStatic, int width, int height, boolean smooth, int attributes)125 public HeightField (boolean isStatic, int width, int height, boolean smooth, int attributes) { 126 this(isStatic, width, height, smooth, MeshBuilder.createAttributes(attributes)); 127 } 128 HeightField(boolean isStatic, int width, int height, boolean smooth, VertexAttributes attributes)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 setIndices()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 update()174 public void update () { 175 if (smooth) { 176 if (norPos < 0) 177 updateSimple(); 178 else 179 updateSmooth(); 180 } else 181 updateSharp(); 182 } 183 updateSmooth()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 updateSimple()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 updateSharp()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! */ getVertexAt(final VertexInfo out, int x, int y)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 getPositionAt(Vector3 out, int x, int y)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 getWeightedNormalAt(Vector3 out, int x, int y)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