Skip to content

Instantly share code, notes, and snippets.

@jrenner
Created May 4, 2014 09:31
Show Gist options
  • Save jrenner/900df1873640c1329297 to your computer and use it in GitHub Desktop.
Save jrenner/900df1873640c1329297 to your computer and use it in GitHub Desktop.
HeightMapModel
package org.jrenner.tac;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Mesh;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.VertexAttributes;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
import com.badlogic.gdx.graphics.g3d.utils.MeshBuilder;
import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder;
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
import com.badlogic.gdx.math.Intersector;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.math.collision.Ray;
import com.badlogic.gdx.utils.*;
import com.badlogic.gdx.utils.StringBuilder;
import org.jrenner.tac.utils.Time;
import org.jrenner.tac.utils.Tools;
import java.util.LinkedList;
public class HeightMapModel {
private Time time = new Time();
public HeightMap heightMap;
private Array<Model> models = new Array<>();
private Texture groundTexture;
private Material groundMat;
Color specular = new Color(0.1f, 0.1f, 0.1f, 1f);
public ObjectMap<GroundChunk, Array<Triangle>> groundTriangles = new ObjectMap<>();
public HeightMapModel(HeightMap hm) {
this.heightMap = hm;
String groundTexName = "hm1_paint.png";
groundTexture = new Texture(Gdx.files.internal(groundTexName), true);
groundTexture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat);
groundTexture.setFilter(Texture.TextureFilter.MipMapLinearNearest, Texture.TextureFilter.Linear);
groundMat = new Material(TextureAttribute.createDiffuse(groundTexture)
, ColorAttribute.createSpecular(specular)
);
createGround();
}
private void createGround() {
vertexInfos = new MeshPartBuilder.VertexInfo[heightMap.heights.length][heightMap.heights[0].length];
int trisHeight = (heightMap.getWidth() - 1);
int trisWidth = (heightMap.getWidth() - 1) * 2;
System.out.printf("tris zHeight,xWidth: %d, %d\n", trisHeight, trisWidth);
tris = new Triangle[trisHeight][trisWidth];
setVertexPositions(); // iterate through height map points, setting world coordinates
buildTriangles(); // abstraction of the triangles that create the mesh
// useful for calculating vertex normals, since each triangle stores a face normal
// but somewhat wasteful of memory
// TODO: optimize
calculateVertexNormals(); // calculate vertex normals for per-vertex lighting
time.start("create ground chunks");
final int chunkSize = 32;
int z = 0;
int zRemain = (int) Math.ceil(heightMap.getDepth() / chunkSize);
if (zRemain == 0) zRemain = 1;
int baseXRemain = (int) Math.ceil(heightMap.getWidth() / chunkSize);
if (baseXRemain == 0) baseXRemain = 1;
System.out.println("z chunks: " + zRemain + ", x chunks: " + baseXRemain);
while (zRemain > 0) {
int xRemain = baseXRemain;
int x = 0;
while (xRemain > 0) {
buildGroundModels(x, z, chunkSize);
xRemain--;
x += chunkSize*2;
}
zRemain--;
z += chunkSize;
}
time.stop();
}
private void buildGroundModels(final int startX, final int startZ, final int chunkSize) {
LinkedList<MeshPartBuilder.VertexInfo> vertexQueue = new LinkedList<>();
MeshBuilder meshb = new MeshBuilder();
long attributes = VertexAttributes.Usage.Position | VertexAttributes.Usage.TextureCoordinates | VertexAttributes.Usage.Normal;
float baseZ = startZ * heightMap.getWidthScale();
float baseX = startX * heightMap.getWidthScale();
ModelBuilder builder = new ModelBuilder();
int zCols = 0;
int xRows = 0;
int lastXRowsCount = -1;
int count = 0;
builder.begin();
Material material;
Array<Triangle> triangles = new Array<>();
meshb.begin(attributes);
boolean addedFirst = false;
for (int z = startZ; z < tris.length && z - startZ < chunkSize; z++) {
zCols++;
xRows = 0;
// TRIANGLE_STRIP draws likes this /\/\/\/\/\/\/\
// so we cap it with a first and last vertex to make rect shaped like this |/\/\/\/\|
// first vertex
// start the row
Triangle top = null;
Triangle bottom = null;
top = tris[z][startX];
if (vertexQueue.size() > 0) {
// start with a degenerate triangle to jump to a new row
MeshPartBuilder.VertexInfo last = vertexQueue.peekLast();
vertexQueue.addLast(vertexQueue.getLast());
vertexQueue.add(top.a);
}
vertexQueue.add(top.a);
vertexQueue.add(top.b);
//meshb.index(meshb.vertex(top.a));
//meshb.index(meshb.vertex(top.b));
for (int x = startX; x < tris[0].length && x - startX < chunkSize * 2; x += 2) {
xRows++;
top = tris[z][x];
bottom = tris[z][x + 1];
// add triangles to be associated with gameobject, this is for pickray and getting mouse to world coords
if (!triangles.contains(top, true)) {
triangles.add(top);
}
if (!triangles.contains(bottom, true)) {
triangles.add(bottom);
}
vertexQueue.addLast(top.c);
vertexQueue.addLast(bottom.a);
//meshb.index(meshb.vertex(top.c));
//meshb.index(meshb.vertex(bottom.a));
}
if (xRows != lastXRowsCount && lastXRowsCount != -1) {
throw new GdxRuntimeException("x row count discrepancy, this row: " + xRows + " , last: " + lastXRowsCount);
}
lastXRowsCount = xRows;
}
MeshPartBuilder.VertexInfo last = null;
while (!vertexQueue.isEmpty()) {
MeshPartBuilder.VertexInfo vert = vertexQueue.removeFirst();
if (last != null && vert.position.equals(last.position)) {
// degenerate triangles for TRIANGLE_STRIP will make many dupes, that is fine
//System.out.println("duplicate: " + Tools.fmt(vert.position));
}
last = vert;
meshb.index(meshb.vertex(vert));
}
Mesh mesh = meshb.end();
material = groundMat;
builder.part(Integer.toString(count), mesh, GL20.GL_TRIANGLE_STRIP, material);
Model model = builder.end();
//Model model = builder.createFromMesh(mesh, GL20.GL_TRIANGLES, groundMat);
//Material myMat = new Material(ColorAttribute.createDiffuse(randColor()));
models.add(model);
GroundChunk inst = new GroundChunk(model);
groundTriangles.put(inst, triangles);
//inst.transform.setTranslation(baseX, 0f, baseZ);
/*Tools.print(inst.center, "center");
Tools.print(inst.dimensions, "dimensions");*/
System.out.printf("built chunk model (%d meshes): numVerts: %d, vertexSize: %d, numIndices: %d\n",
model.meshes.size, model.meshes.first().getNumVertices(), model.meshes.first().getVertexSize(), model.meshes.first().getNumIndices());
}
private void buildTriangles() {
//System.out.printf("chunk: startXZ: %d, %d -- length: %d\n", startX, startZ, length);
int count = 0;
MeshPartBuilder.VertexInfo right;
MeshPartBuilder.VertexInfo down;
MeshPartBuilder.VertexInfo downRight;
for (int z = 0; z < vertexInfos.length - 1; z++) {
for (int x = 0; x < (vertexInfos[0].length - 1); x++) {
MeshPartBuilder.VertexInfo vert = vertexInfos[z][x];
right = vertexInfos[z][x+1];
down = vertexInfos[z+1][x];
downRight = vertexInfos[z+1][x+1];
// each z row has two trianagles forming a rect
int xIdx = x*2;
Triangle top = new Triangle(z, xIdx, vert, down, right);
Triangle bottom = new Triangle(z, xIdx+1, downRight, right, down);
count += 2;
}
}
System.out.println("built " + count + " triangles");
}
private Triangle getTriangleAtWorldCoords(float x, float z) {
float y = heightMap.getInterpolatedHeight(x, z);
int ix = (int) (x * 2 / heightMap.getHeightScale());
int iz = (int) (z / heightMap.getWidthScale());
if (iz > tris.length -1) iz = tris.length - 1;
if (ix > tris[0].length - 1) ix = tris[0].length - 1;
Triangle tri = null;
try {
tri = tris[iz][ix];
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
System.out.printf("ix: %d, iz: %d\n", ix, iz);
System.exit(1);
}
tri.getCenter(tmp);
tmp.y += 10f;
// for debug
//mouseMarker.transform.setToTranslation(tmp);
//System.out.println(tri);
return tri;
}
private Triangle[] neighborStorage = new Triangle[4];
private Triangle[] getTrianglesNeighboringVertex(int baseZ, int baseX) {
Triangle[] neighbors = neighborStorage;
// base triangle index
int x = baseX * 2;
int z = baseZ;
int maxZ = tris.length - 1;
int maxX = tris[0].length - 1;
int slot = 0;
// bottom row
for (int i = -1; i <= 0; i++) {
x--;
if (z < 0 || x < 0 || z > maxZ || x > maxX) {
neighbors[slot++] = null;
} else {
neighbors[slot++] = tris[z][x];
}
}
// top row
z -= 1;
for (int i = -1; i <= 0; i++) {
x = baseX - i;
if (z < 0 || x < 0 || z > maxZ || x > maxX) {
neighbors[slot++] = null;
} else {
neighbors[slot++] = tris[z][x];
}
}
return neighbors;
}
private void setVertexPositions() {
time.start("setVertexPositions");
// store vertex info for second pass where we calculate vertex normals for per-vertex lighting
float[][] pts = heightMap.heights;
float blockSize = heightMap.getWidthScale();
float heightScale = heightMap.getHeightScale();
float width = heightMap.getWidthWorld();
float depth = heightMap.getDepthWorld();
for (int z = 0; z < pts.length; z++) {
for (int x = 0; x < pts[z].length; x++) {
float y = pts[z][x];
MeshPartBuilder.VertexInfo thisVert = new MeshPartBuilder.VertexInfo();
thisVert.setPos(x * blockSize, y * heightScale, z * blockSize);
// set texture UV
float u = (x * blockSize) / width;
float v = (z * blockSize) / depth;
float scl = heightScale;
scl = 1f;
thisVert.setUV(u * scl, v * scl);
vertexInfos[z][x] = thisVert;
}
}
time.stop();
}
/** Return vertex normal based on average of surrounding triangle face normals */
private Vector3 calculateNormalForVertex(int z, int x) {
tmp.set(0f, 0f, 0f);
Triangle[] neighbors = getTrianglesNeighboringVertex(z, x);
int triCount = 0;
for (int i = 0; i < neighbors.length; i++) {
Triangle tri = neighbors[i];
if (tri != null) {
triCount++;
tmp.add(tri.faceNormal);
}
}
tmp.scl(1f / triCount);
return tmp;
}
private void calculateVertexNormals() {
time.start("calculate vertex normals");
int count = 0;
for (int z = 0; z < vertexInfos.length; z++) {
for (int x = 0; x < vertexInfos[z].length; x++) {
MeshPartBuilder.VertexInfo vert = vertexInfos[z][x];
// calculate normals
vert.setNor(calculateNormalForVertex(z, x));
}
}
System.out.print("vertex count: " + count + ", ");
time.stop();
}
public MeshPartBuilder.VertexInfo[] getVertexInfoSquareFromPos(float xf, float zf) {
int x = MathUtils.floor(xf / Main.heightMapModel.heightMap.getHeightScale());
int z = MathUtils.floor(zf / Main.heightMapModel.heightMap.getWidthScale());
squareVerts[0] = vertexInfos[z][x];
squareVerts[1] = vertexInfos[z+1][x];
squareVerts[2] = vertexInfos[z+1][x+1];
squareVerts[3] = vertexInfos[z][x+1];
return squareVerts;
}
public GroundChunk getGroundChunkAtPoint(int screenX, int screenY) {
Ray ray = Main.view.cam.getPickRay(screenX, screenY);
int result = -1;
float distance = -1;
for (int i = 0; i < Lists.groundChunks.size; ++i) {
final GroundChunk chunk = Lists.groundChunks.get(i);
float dist2 = ray.origin.dst2(chunk.position);
if (distance >= 0f && dist2 > distance) continue;
if (Intersector.intersectRaySphere(ray, chunk.position, chunk.radius, null)) {
result = i;
distance = dist2;
}
}
if (result == -1) return null;
return Lists.groundChunks.get(result);
}
MeshPartBuilder.VertexInfo[][] vertexInfos;
MeshPartBuilder.VertexInfo[] squareVerts = new MeshPartBuilder.VertexInfo[4];
public Triangle[][] tris;
public class Triangle {
int zidx, xidx;
Vector3 faceNormal = new Vector3();
MeshPartBuilder.VertexInfo a, b, c;
public Triangle(int triZ, int triX, MeshPartBuilder.VertexInfo a, MeshPartBuilder.VertexInfo b, MeshPartBuilder.VertexInfo c) {
zidx = triZ;
xidx = triX;
try {
tris[triZ][triX] = this;
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
Tools.sleep(500);
System.out.println("tried ZX: " + triZ + ", " + triX);
System.out.println("length ZX: " + tris.length + ", " + tris[0].length);
System.exit(5);
}
this.a = a;
this.b = b;
this.c = c;
this.calculateFaceNormal();
}
public void flatShadedNormals() {
this.calculateFaceNormal();
a.setNor(faceNormal);
b.setNor(faceNormal);
c.setNor(faceNormal);
}
public void calculateFaceNormal() {
/*tmp.set(c.position).sub(a.position);
tmp2.set(b.position).sub(a.position);
tmp.crs(tmp2).nor();
faceNormal.set(tmp);*/
tmp.set(b.position).sub(a.position);
tmp2.set(c.position).sub(a.position);
float x = tmp.y * tmp2.z - tmp.z * tmp2.y;
float y = tmp.z * tmp2.x - tmp.x - tmp2.z;
float z = tmp.x * tmp2.y - tmp.y * tmp2.x;
faceNormal.set(x, y, z).nor();
}
@Override
public String toString() {
com.badlogic.gdx.utils.StringBuilder sb = new StringBuilder();
sb.append(String.format("Triangle[Z:%d, X:%d]\n", zidx, xidx));
sb.append(Tools.fmt(a.position, "a")).append("\n");
sb.append(Tools.fmt(b.position, "b")).append("\n");
sb.append(Tools.fmt(c.position, "c")).append("\n");
sb.append(Tools.fmt(getCenter(tmp), "center")).append("\n");
return sb.toString();
}
public Vector3 getCenter(Vector3 out) {
out.set(a.position).add(b.position).add(c.position);
return out.scl(1/3f);
}
public Vector3 getInterpolatedNormal(Vector3 out) {
out.set(a.normal).add(b.normal).add(c.normal);
return out.scl(1/3f);
}
}
private static Vector3 tmp = new Vector3();
private static Vector3 tmp2 = new Vector3();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment