• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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