package com.badlogic.gdx.tests.g3d;
import java.nio.ByteBuffer;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Mesh;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;
import com.badlogic.gdx.graphics.VertexAttributes;
import com.badlogic.gdx.graphics.g3d.utils.MeshBuilder;
import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder;
import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder.VertexInfo;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.GdxRuntimeException;
/** This is a test class, showing how one could implement a height field. See also {@link HeightMapTest}. Do not expect this to be
* a fully supported and implemented height field class.
* <p />
* Represents a HeightField, which is an evenly spaced grid of values, where each value defines the height on that position of the
* grid, so forming a 3D shape. Typically used for (relatively simple) terrains and such. See <a
* href="http://en.wikipedia.org/wiki/Heightmap">wikipedia</a> for more information.
* <p />
* A height field has a width and height, specifying the width and height of the grid. Points on this grid are specified using
* integer values, named "x" and "y". Do not confuse these with the x, y and z floating point values representing coordinates in
* world space.
* <p />
* The values of the heightfield are normalized. Meaning that they typically range from 0 to 1 (but they can be negative or more
* than one). The plane of the heightfield can be specified using the {@link #corner00}, {@link #corner01}, {@link #corner10} and
* {@link #corner11} members. Where `corner00` is the location on the grid at x:0, y;0, `corner01` at x:0, y:height-1, `corner10`
* at x:width-1, y:0 and `corner11` the location on the grid at x:width-1, y:height-1.
* <p />
* The height and direction of the field can be set using the {@link #magnitude} vector. Typically this should be the vector
* perpendicular to the heightfield. E.g. if the field is on the XZ plane, then the magnitude is typically pointing on the Y axis.
* The length of the `magnitude` specifies the height of the height field. In other words, the word coordinate of a point on the
* grid is specified as:
* <p />
* base[y * width + x] + magnitude * value[y * width + x]
* <p />
* Use the {@link #getPositionAt(Vector3, int, int)} method to get the coordinate of a specific point on the grid.
* <p />
* You can set this heightfield using the constructor or one of the `set` methods. E.g. by specifying an array of values or a
* {@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
* specify the value for each point on the field. Be aware that the total number of vertices cannot exceed 32k. Using a large
* height map will result in unpredicted results.
* <p />
* You can also manually modify the heightfield by directly accessing the {@link #data} member. The index within this array can be
* calculates as: `y * width + x`. E.g. `field.data[y * field.width + x] = value;`. When you modify the data then you can update
* the {@link #mesh} using the {@link #update()} method.
* <p />
* The {@link #mesh} member can be used to render the height field. The vertex attributes this mesh contains are specified in the
* constructor. There are two ways for generating the mesh: smooth and sharp.
* <p />
* Smooth can be forced by specifying `true` for the `smooth` argument of the constructor. Otherwise it will be based on whether
* the specified vertex attributes contains a normal attribute. If there is no normal attribute then the mesh will always be
* smooth (even when you specify `false` in the constructor). In this case the number of vertices is the same as the amount of
* grid points. Causing vertices to be shared amongst multiple faces.
* <p />
* Sharp will be used if the vertex attributes contains a normal attribute and you didnt specify `true` for the `smooth` argument
* of the constructor. This will cause the number of vertices to be around four times the amount grid points and each normal is
* estimated for each face instead of each point.
* @author Xoppa */
public class HeightField implements Disposable {
public final Vector2 uvOffset = new Vector2(0, 0);
public final Vector2 uvScale = new Vector2(1, 1);
public final Color color00 = new Color(Color.WHITE);
public final Color color10 = new Color(Color.WHITE);
public final Color color01 = new Color(Color.WHITE);
public final Color color11 = new Color(Color.WHITE);
public final Vector3 corner00 = new Vector3(0, 0, 0);
public final Vector3 corner10 = new Vector3(1, 0, 0);
public final Vector3 corner01 = new Vector3(0, 0, 1);
public final Vector3 corner11 = new Vector3(1, 0, 1);
public final Vector3 magnitude = new Vector3(0, 1, 0);
public final float[] data;
public final int width;
public final int height;
public final boolean smooth;
public final Mesh mesh;
private final float vertices[];
private final int stride;
private final int posPos;
private final int norPos;
private final int uvPos;
private final int colPos;
private final MeshPartBuilder.VertexInfo vertex00 = new MeshPartBuilder.VertexInfo();
private final MeshPartBuilder.VertexInfo vertex10 = new MeshPartBuilder.VertexInfo();
private final MeshPartBuilder.VertexInfo vertex01 = new MeshPartBuilder.VertexInfo();
private final MeshPartBuilder.VertexInfo vertex11 = new MeshPartBuilder.VertexInfo();
private final Vector3 tmpV1 = new Vector3();
private final Vector3 tmpV2 = new Vector3();
private final Vector3 tmpV3 = new Vector3();
private final Vector3 tmpV4 = new Vector3();
private final Vector3 tmpV5 = new Vector3();
private final Vector3 tmpV6 = new Vector3();
private final Vector3 tmpV7 = new Vector3();
private final Vector3 tmpV8 = new Vector3();
private final Vector3 tmpV9 = new Vector3();
private final Color tmpC = new Color();
public HeightField (boolean isStatic, final Pixmap map, boolean smooth, int attributes) {
this(isStatic, map.getWidth(), map.getHeight(), smooth, attributes);
set(map);
}
public HeightField (boolean isStatic, final ByteBuffer colorData, final Pixmap.Format format, int width, int height,
boolean smooth, int attributes) {
this(isStatic, width, height, smooth, attributes);
set(colorData, format);
}
public HeightField (boolean isStatic, final float[] data, int width, int height, boolean smooth, int attributes) {
this(isStatic, width, height, smooth, attributes);
set(data);
}
public HeightField (boolean isStatic, int width, int height, boolean smooth, int attributes) {
this(isStatic, width, height, smooth, MeshBuilder.createAttributes(attributes));
}
public HeightField (boolean isStatic, int width, int height, boolean smooth, VertexAttributes attributes) {
this.posPos = attributes.getOffset(Usage.Position, -1);
this.norPos = attributes.getOffset(Usage.Normal, -1);
this.uvPos = attributes.getOffset(Usage.TextureCoordinates, -1);
this.colPos = attributes.getOffset(Usage.ColorUnpacked, -1);
smooth = smooth || (norPos < 0); // cant have sharp edges without normals
this.width = width;
this.height = height;
this.smooth = smooth;
this.data = new float[width * height];
this.stride = attributes.vertexSize / 4;
final int numVertices = smooth ? width * height : (width - 1) * (height - 1) * 4;
final int numIndices = (width - 1) * (height - 1) * 6;
this.mesh = new Mesh(isStatic, numVertices, numIndices, attributes);
this.vertices = new float[numVertices * stride];
setIndices();
}
private void setIndices () {
final int w = width - 1;
final int h = height - 1;
short indices[] = new short[w * h * 6];
int i = -1;
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
final int c00 = smooth ? (y * width + x) : (y * 2 * w + x * 2);
final int c10 = c00 + 1;
final int c01 = c00 + (smooth ? width : w * 2);
final int c11 = c10 + (smooth ? width : w * 2);
indices[++i] = (short)c11;
indices[++i] = (short)c10;
indices[++i] = (short)c00;
indices[++i] = (short)c00;
indices[++i] = (short)c01;
indices[++i] = (short)c11;
}
}
mesh.setIndices(indices);
}
public void update () {
if (smooth) {
if (norPos < 0)
updateSimple();
else
updateSmooth();
} else
updateSharp();
}
private void updateSmooth () {
for (int x = 0; x < width; ++x) {
for (int y = 0; y < height; ++y) {
VertexInfo v = getVertexAt(vertex00, x, y);
getWeightedNormalAt(v.normal, x, y);
setVertex(y * width + x, v);
}
}
mesh.setVertices(vertices);
}
private void updateSimple () {
for (int x = 0; x < width; ++x) {
for (int y = 0; y < height; ++y) {
setVertex(y * width + x, getVertexAt(vertex00, x, y));
}
}
mesh.setVertices(vertices);
}
private void updateSharp () {
final int w = width - 1;
final int h = height - 1;
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
final int c00 = (y * 2 * w + x * 2);
final int c10 = c00 + 1;
final int c01 = c00 + w * 2;
final int c11 = c10 + w * 2;
VertexInfo v00 = getVertexAt(vertex00, x, y);
VertexInfo v10 = getVertexAt(vertex10, x + 1, y);
VertexInfo v01 = getVertexAt(vertex01, x, y + 1);
VertexInfo v11 = getVertexAt(vertex11, x + 1, y + 1);
v01.normal.set(v01.position).sub(v00.position).nor().crs(tmpV1.set(v11.position).sub(v01.position).nor());
v10.normal.set(v10.position).sub(v11.position).nor().crs(tmpV1.set(v00.position).sub(v10.position).nor());
v00.normal.set(v01.normal).lerp(v10.normal, .5f);
v11.normal.set(v00.normal);
setVertex(c00, v00);
setVertex(c10, v10);
setVertex(c01, v01);
setVertex(c11, v11);
}
}
mesh.setVertices(vertices);
}
/** Does not set the normal member! */
protected VertexInfo getVertexAt (final VertexInfo out, int x, int y) {
final float dx = (float)x / (float)(width - 1);
final float dy = (float)y / (float)(height - 1);
final float a = data[y * width + x];
out.position.set(corner00).lerp(corner10, dx).lerp(tmpV1.set(corner01).lerp(corner11, dx), dy);
out.position.add(tmpV1.set(magnitude).scl(a));
out.color.set(color00).lerp(color10, dx).lerp(tmpC.set(color01).lerp(color11, dx), dy);
out.uv.set(dx, dy).scl(uvScale).add(uvOffset);
return out;
}
public Vector3 getPositionAt (Vector3 out, int x, int y) {
final float dx = (float)x / (float)(width - 1);
final float dy = (float)y / (float)(height - 1);
final float a = data[y * width + x];
out.set(corner00).lerp(corner10, dx).lerp(tmpV1.set(corner01).lerp(corner11, dx), dy);
out.add(tmpV1.set(magnitude).scl(a));
return out;
}
public Vector3 getWeightedNormalAt (Vector3 out, int x, int y) {
// This commented code is based on http://www.flipcode.com/archives/Calculating_Vertex_Normals_for_Height_Maps.shtml
// Note that this approach only works for a heightfield on the XZ plane with a magnitude on the y axis
// float sx = data[(x < width - 1 ? x + 1 : x) + y * width] + data[(x > 0 ? x-1 : x) + y * width];
// if (x == 0 || x == (width - 1))
// sx *= 2f;
// float sy = data[(y < height - 1 ? y + 1 : y) * width + x] + data[(y > 0 ? y-1 : y) * width + x];
// if (y == 0 || y == (height - 1))
// sy *= 2f;
// float xScale = (corner11.x - corner00.x) / (width - 1f);
// float zScale = (corner11.z - corner00.z) / (height - 1f);
// float yScale = magnitude.len();
// out.set(-sx * yScale, 2f * xScale, sy*yScale*xScale / zScale).nor();
// return out;
// The following approach weights the normal of the four triangles (half quad) surrounding the position.
// A more accurate approach would be to weight the normal of the actual triangles.
int faces = 0;
out.set(0, 0, 0);
Vector3 center = getPositionAt(tmpV2, x, y);
Vector3 left = x > 0 ? getPositionAt(tmpV3, x - 1, y) : null;
Vector3 right = x < (width - 1) ? getPositionAt(tmpV4, x + 1, y) : null;
Vector3 bottom = y > 0 ? getPositionAt(tmpV5, x, y - 1) : null;
Vector3 top = y < (height - 1) ? getPositionAt(tmpV6, x, y + 1) : null;
if (top != null && left != null) {
out.add(tmpV7.set(top).sub(center).nor().crs(tmpV8.set(center).sub(left).nor()).nor());
faces++;
}
if (left != null && bottom != null) {
out.add(tmpV7.set(left).sub(center).nor().crs(tmpV8.set(center).sub(bottom).nor()).nor());
faces++;
}
if (bottom != null && right != null) {
out.add(tmpV7.set(bottom).sub(center).nor().crs(tmpV8.set(center).sub(right).nor()).nor());
faces++;
}
if (right != null && top != null) {
out.add(tmpV7.set(right).sub(center).nor().crs(tmpV8.set(center).sub(top).nor()).nor());
faces++;
}
if (faces != 0)
out.scl(1f / (float)faces);
else
out.set(magnitude).nor();
return out;
}
protected void setVertex (int index, VertexInfo info) {
index *= stride;
if (posPos >= 0) {
vertices[index + posPos + 0] = info.position.x;
vertices[index + posPos + 1] = info.position.y;
vertices[index + posPos + 2] = info.position.z;
}
if (norPos >= 0) {
vertices[index + norPos + 0] = info.normal.x;
vertices[index + norPos + 1] = info.normal.y;
vertices[index + norPos + 2] = info.normal.z;
}
if (uvPos >= 0) {
vertices[index + uvPos + 0] = info.uv.x;
vertices[index + uvPos + 1] = info.uv.y;
}
if (colPos >= 0) {
vertices[index + colPos + 0] = info.color.r;
vertices[index + colPos + 1] = info.color.g;
vertices[index + colPos + 2] = info.color.b;
vertices[index + colPos + 3] = info.color.a;
}
}
public void set (final Pixmap map) {
if (map.getWidth() != width || map.getHeight() != height) throw new GdxRuntimeException("Incorrect map size");
set(map.getPixels(), map.getFormat());
}
public void set (final ByteBuffer colorData, final Pixmap.Format format) {
set(heightColorsToMap(colorData, format, width, height));
}
public void set (float[] data) {
set(data, 0);
}
public void set (float[] data, int offset) {
if (this.data.length > (data.length - offset)) throw new GdxRuntimeException("Incorrect data size");
System.arraycopy(data, offset, this.data, 0, this.data.length);
update();
}
@Override
public void dispose () {
mesh.dispose();
}
/** Simply creates an array containing only all the red components of the data. */
public static float[] heightColorsToMap (final ByteBuffer data, final Pixmap.Format format, int width, int height) {
final int bytesPerColor = (format == Format.RGB888 ? 3 : (format == Format.RGBA8888 ? 4 : 0));
if (bytesPerColor == 0) throw new GdxRuntimeException("Unsupported format, should be either RGB8 or RGBA8");
if (data.remaining() < (width * height * bytesPerColor)) throw new GdxRuntimeException("Incorrect map size");
final int startPos = data.position();
byte[] source = null;
int sourceOffset = 0;
if (data.hasArray() && !data.isReadOnly()) {
source = data.array();
sourceOffset = data.arrayOffset() + startPos;
} else {
source = new byte[width * height * bytesPerColor];
data.get(source);
data.position(startPos);
}
float[] dest = new float[width * height];
for (int i = 0; i < dest.length; ++i) {
int v = source[sourceOffset + i * bytesPerColor];
v = v < 0 ? 256 + v : v;
dest[i] = (float)v / 255f;
}
return dest;
}
}