I'm looking to create a Terrain editor. I have a terrain class, I use this to make Terrain Blocks, Which can load next to each other. I also have a heightmap class to load a heightmap for the terrain instead just for now atleast. On top of that I have mouse box selector.
Because the terrain is made to be one mesh, And my mouse box selector only works for one mesh, It selects the whole terrain instead of one tile. Is there any way to fix this?
Here is the Terrain Class
package org.lwjglb.engine.items;
import de.matthiasmann.twl.utils.PNGDecoder;
import java.nio.ByteBuffer;
import org.joml.Vector3f;
import org.lwjglb.engine.graph.HeightMapMesh;
public class Terrain {
private final GameItem[] gameItems;
private final int terrainSize;
private final int verticesPerCol;
private final int verticesPerRow;
private final HeightMapMesh heightMapMesh;
/**
* It will hold the bounding box for each terrain block
*/
private final Box2D[][] boundingBoxes;
/**
* A Terrain is composed by blocks, each block is a GameItem constructed
* from a HeightMap.
*
* @param terrainSize The number of blocks will be terrainSize * terrainSize
* @param scale The scale to be applied to each terrain block
* @param minY The minimum y value, before scaling, of each terrain block
* @param maxY The maximum y value, before scaling, of each terrain block
* @param heightMapFile
* @param textureFile
* @param textInc
* @throws Exception
*/
public Terrain(int terrainSize, float scale, float minY, float maxY, String heightMapFile, String textureFile, int textInc) throws Exception {
this.terrainSize = terrainSize;
gameItems = new GameItem[terrainSize * terrainSize];
PNGDecoder decoder = new PNGDecoder(getClass().getResourceAsStream(heightMapFile));
int height = decoder.getHeight();
int width = decoder.getWidth();
ByteBuffer buf = ByteBuffer.allocateDirect(
4 * decoder.getWidth() * decoder.getHeight());
decoder.decode(buf, decoder.getWidth() * 4, PNGDecoder.Format.RGBA);
buf.flip();
// The number of vertices per column and row
verticesPerCol = width - 1;
verticesPerRow = height - 1;
heightMapMesh = new HeightMapMesh(minY, maxY, buf, width, height, textureFile, textInc);
boundingBoxes = new Box2D[terrainSize][terrainSize];
for (int row = 0; row < terrainSize; row++) {
for (int col = 0; col < terrainSize; col++) {
float xDisplacement = (col - ((float) terrainSize - 1) / (float) 2) * scale * HeightMapMesh.getXLength();
float zDisplacement = (row - ((float) terrainSize - 1) / (float) 2) * scale * HeightMapMesh.getZLength();
GameItem terrainBlock = new GameItem(heightMapMesh.getMesh());
terrainBlock.setScale(scale);
terrainBlock.setPosition(xDisplacement, 0, zDisplacement);
gameItems[row * terrainSize + col] = terrainBlock;
boundingBoxes[row][col] = getBoundingBox(terrainBlock);
}
}
}
public float getHeight(Vector3f position) {
float result = Float.MIN_VALUE;
// For each terrain block we get the bounding box, translate it to view coodinates
// and check if the position is contained in that bounding box
Box2D boundingBox = null;
boolean found = false;
GameItem terrainBlock = null;
for (int row = 0; row < terrainSize && !found; row++) {
for (int col = 0; col < terrainSize && !found; col++) {
terrainBlock = gameItems[row * terrainSize + col];
boundingBox = boundingBoxes[row][col];
found = boundingBox.contains(position.x, position.z);
//System.out.println(terrainBlock.getMeshes().length);
}
}
// If we have found a terrain block that contains the position we need
// to calculate the height of the terrain on that position
if (found) {
Vector3f[] triangle = getTriangle(position, boundingBox, terrainBlock);
result = interpolateHeight(triangle[0], triangle[1], triangle[2], position.x, position.z);
}
return result;
}
protected Vector3f[] getTriangle(Vector3f position, Box2D boundingBox, GameItem terrainBlock) {
// Get the column and row of the heightmap associated to the current position
float cellWidth = boundingBox.width / (float) verticesPerCol;
float cellHeight = boundingBox.height / (float) verticesPerRow;
int col = (int) ((position.x - boundingBox.x) / cellWidth);
int row = (int) ((position.z - boundingBox.y) / cellHeight);
Vector3f[] triangle = new Vector3f[3];
triangle[1] = new Vector3f(
boundingBox.x + col * cellWidth,
getWorldHeight(row + 1, col, terrainBlock),
boundingBox.y + (row + 1) * cellHeight);
triangle[2] = new Vector3f(
boundingBox.x + (col + 1) * cellWidth,
getWorldHeight(row, col + 1, terrainBlock),
boundingBox.y + row * cellHeight);
if (position.z < getDiagonalZCoord(triangle[1].x, triangle[1].z, triangle[2].x, triangle[2].z, position.x)) {
triangle[0] = new Vector3f(
boundingBox.x + col * cellWidth,
getWorldHeight(row, col, terrainBlock),
boundingBox.y + row * cellHeight);
} else {
triangle[0] = new Vector3f(
boundingBox.x + (col + 1) * cellWidth,
getWorldHeight(row + 2, col + 1, terrainBlock),
boundingBox.y + (row + 1) * cellHeight);
}
return triangle;
}
protected float getDiagonalZCoord(float x1, float z1, float x2, float z2, float x) {
float z = ((z1 - z2) / (x1 - x2)) * (x - x1) + z1;
return z;
}
protected float getWorldHeight(int row, int col, GameItem gameItem) {
float y = heightMapMesh.getHeight(row, col);
return y * gameItem.getScale() + gameItem.getPosition().y;
}
protected float interpolateHeight(Vector3f pA, Vector3f pB, Vector3f pC, float x, float z) {
// Plane equation ax+by+cz+d=0
float a = (pB.y - pA.y) * (pC.z - pA.z) - (pC.y - pA.y) * (pB.z - pA.z);
float b = (pB.z - pA.z) * (pC.x - pA.x) - (pC.z - pA.z) * (pB.x - pA.x);
float c = (pB.x - pA.x) * (pC.y - pA.y) - (pC.x - pA.x) * (pB.y - pA.y);
float d = -(a * pA.x + b * pA.y + c * pA.z);
// y = (-d -ax -cz) / b
float y = (-d - a * x - c * z) / b;
return y;
}
/**
* Gets the bounding box of a terrain block
*
* @param terrainBlock A GameItem instance that defines the terrain block
* @return The boundingg box of the terrain block
*/
private Box2D getBoundingBox(GameItem terrainBlock) {
float scale = terrainBlock.getScale();
Vector3f position = terrainBlock.getPosition();
float topLeftX = HeightMapMesh.STARTX * scale + position.x;
float topLeftZ = HeightMapMesh.STARTZ * scale + position.z;
float width = Math.abs(HeightMapMesh.STARTX * 2) * scale;
float height = Math.abs(HeightMapMesh.STARTZ * 2) * scale;
Box2D boundingBox = new Box2D(topLeftX, topLeftZ, width, height);
return boundingBox;
}
public GameItem[] getGameItems() {
return gameItems;
}
static class Box2D {
public float x;
public float y;
public float width;
public float height;
public Box2D(float x, float y, float width, float height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public boolean contains(float x2, float y2) {
return x2 >= x
&& y2 >= y
&& x2 < x + width
&& y2 < y + height;
}
}
}
Here is the HeightMap file
package org.lwjglb.engine.graph;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.joml.Vector3f;
import org.lwjglb.engine.Utils;
public class HeightMapMesh {
private static final int MAX_COLOUR = 255 * 255 * 255;
public static final float STARTX = -0.5f;
public static final float STARTZ = -0.5f;
private final float minY;
private final float maxY;
private final Mesh mesh;
private final float[][] heightArray;
public HeightMapMesh(float minY, float maxY, ByteBuffer heightMapImage, int width, int height, String textureFile, int textInc) throws Exception {
this.minY = minY;
this.maxY = maxY;
heightArray = new float[height][width];
Texture texture = new Texture(textureFile);
float incx = getXLength() / (width - 1);
float incz = getZLength() / (height - 1);
List<Float> positions = new ArrayList();
List<Float> textCoords = new ArrayList();
List<Integer> indices = new ArrayList();
int tiles = 0;
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
// Create vertex for current position
positions.add(STARTX + col * incx); // x
float currentHeight = getHeight(col, row, width, heightMapImage);
heightArray[row][col] = currentHeight;
positions.add(currentHeight); //y
positions.add(STARTZ + row * incz); //z
// Set texture coordinates
textCoords.add((float) textInc * (float) col / (float) width);
textCoords.add((float) textInc * (float) row / (float) height);
// Create indices
if (col < width - 1 && row < height - 1) {
int leftTop = row * width + col;
int leftBottom = (row + 1) * width + col;
int rightBottom = (row + 1) * width + col + 1;
int rightTop = row * width + col + 1;
indices.add(leftTop);
indices.add(leftBottom);
indices.add(rightTop);
indices.add(rightTop);
indices.add(leftBottom);
indices.add(rightBottom);
}
tiles++;
}
}
float[] posArr = Utils.listToArray(positions);
int[] indicesArr = indices.stream().mapToInt(i -> i).toArray();
float[] textCoordsArr = Utils.listToArray(textCoords);
float[] normalsArr = calcNormals(posArr, width, height);
this.mesh = new Mesh(posArr, textCoordsArr, normalsArr, indicesArr);
Material material = new Material(texture, 0.0f);
mesh.setMaterial(material);
System.out.println(tiles + " Tiles created");
}
public Mesh getMesh() {
return mesh;
}
public float getHeight(int row, int col) {
float result = 0;
if ( row >= 0 && row < heightArray.length ) {
if ( col >= 0 && col < heightArray[row].length ) {
result = heightArray[row][col];
}
}
return result;
}
public static float getXLength() {
return Math.abs(-STARTX*2);
}
public static float getZLength() {
return Math.abs(-STARTZ*2);
}
private float[] calcNormals(float[] posArr, int width, int height) {
Vector3f v0 = new Vector3f();
Vector3f v1 = new Vector3f();
Vector3f v2 = new Vector3f();
Vector3f v3 = new Vector3f();
Vector3f v4 = new Vector3f();
Vector3f v12 = new Vector3f();
Vector3f v23 = new Vector3f();
Vector3f v34 = new Vector3f();
Vector3f v41 = new Vector3f();
List<Float> normals = new ArrayList<>();
Vector3f normal = new Vector3f();
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
if (row > 0 && row < height -1 && col > 0 && col < width -1) {
int i0 = row*width*3 + col*3;
v0.x = posArr[i0];
v0.y = posArr[i0 + 1];
v0.z = posArr[i0 + 2];
int i1 = row*width*3 + (col-1)*3;
v1.x = posArr[i1];
v1.y = posArr[i1 + 1];
v1.z = posArr[i1 + 2];
v1 = v1.sub(v0);
int i2 = (row+1)*width*3 + col*3;
v2.x = posArr[i2];
v2.y = posArr[i2 + 1];
v2.z = posArr[i2 + 2];
v2 = v2.sub(v0);
int i3 = (row)*width*3 + (col+1)*3;
v3.x = posArr[i3];
v3.y = posArr[i3 + 1];
v3.z = posArr[i3 + 2];
v3 = v3.sub(v0);
int i4 = (row-1)*width*3 + col*3;
v4.x = posArr[i4];
v4.y = posArr[i4 + 1];
v4.z = posArr[i4 + 2];
v4 = v4.sub(v0);
v1.cross(v2, v12);
v12.normalize();
v2.cross(v3, v23);
v23.normalize();
v3.cross(v4, v34);
v34.normalize();
v4.cross(v1, v41);
v41.normalize();
normal = v12.add(v23).add(v34).add(v41);
normal.normalize();
} else {
normal.x = 0;
normal.y = 1;
normal.z = 0;
}
normal.normalize();
normals.add(normal.x);
normals.add(normal.y);
normals.add(normal.z);
}
}
return Utils.listToArray(normals);
}
private float getHeight(int x, int z, int width, ByteBuffer buffer) {
int argb = getRGB(x, z, width, buffer);
return this.minY + Math.abs(this.maxY - this.minY) * ((float) argb / (float) MAX_COLOUR);
}
public static int getRGB(int x, int z, int width, ByteBuffer buffer) {
byte r = buffer.get(x * 4 + 0 + z * 4 * width);
byte g = buffer.get(x * 4 + 1 + z * 4 * width);
byte b = buffer.get(x * 4 + 2 + z * 4 * width);
byte a = buffer.get(x * 4 + 3 + z * 4 * width);
int argb = ((0xFF & a) << 24) | ((0xFF & r) << 16)
| ((0xFF & g) << 8) | (0xFF & b);
return argb;
}
}
And the Mouse selector
package org.lwjglb.game;
import org.joml.Intersectionf;
import org.joml.Matrix4f;
import org.joml.Vector2d;
import org.joml.Vector2f;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.lwjglb.engine.Window;
import org.lwjglb.engine.graph.Camera;
import org.lwjglb.engine.items.GameItem;
public class MouseBoxSelectionDetector extends CameraBoxSelectionDetector {
private final Matrix4f invProjectionMatrix;
private final Matrix4f invViewMatrix;
private final Vector3f mouseDir;
private final Vector4f tmpVec;
public MouseBoxSelectionDetector() {
super();
invProjectionMatrix = new Matrix4f();
invViewMatrix = new Matrix4f();
mouseDir = new Vector3f();
tmpVec = new Vector4f();
}
public boolean selectGameItem(GameItem[] gameItems, Window window, Vector2d mousePos, Camera camera) {
// Transform mouse coordinates into normalized spaze [-1, 1]
int wdwWitdh = window.getWidth();
int wdwHeight = window.getHeight();
float x = (float)(2 * mousePos.x) / (float)wdwWitdh - 1.0f;
float y = 1.0f - (float)(2 * mousePos.y) / (float)wdwHeight;
float z = -1.0f;
invProjectionMatrix.set(window.getProjectionMatrix());
invProjectionMatrix.invert();
tmpVec.set(x, y, z, 1.0f);
tmpVec.mul(invProjectionMatrix);
tmpVec.z = -1.0f;
tmpVec.w = 0.0f;
Matrix4f viewMatrix = camera.getViewMatrix();
invViewMatrix.set(viewMatrix);
invViewMatrix.invert();
tmpVec.mul(invViewMatrix);
mouseDir.set(tmpVec.x, tmpVec.y, tmpVec.z);
return selectGameItem(gameItems, camera.getPosition(), mouseDir);
}
}
and just in case you need it
package org.lwjglb.game;
import java.awt.geom.Point2D;
import org.joml.Intersectionf;
import org.joml.Vector2f;
import org.joml.Vector3f;
import org.lwjglb.engine.graph.Camera;
import org.lwjglb.engine.items.GameItem;
public class CameraBoxSelectionDetector {
private final Vector3f max;
private final Vector3f min;
private final Vector2f nearFar;
private Vector3f dir;
private double brushSize = 1.0;
private double maxBrushSize = 10.0;
private int maxSelected = 500;
public CameraBoxSelectionDetector() {
dir = new Vector3f();
min = new Vector3f();
max = new Vector3f();
nearFar = new Vector2f();
}
public void selectGameItem(GameItem[] gameItems, Camera camera) {
dir = camera.getViewMatrix().positiveZ(dir).negate();
selectGameItem(gameItems, camera.getPosition(), dir);
}
protected boolean selectGameItem(GameItem[] gameItems, Vector3f center, Vector3f dir) {
boolean selected = false;
GameItem selectedGameItem[] = new GameItem[maxSelected]; //Amount to select
float closestDistance = Float.POSITIVE_INFINITY;
outterloop:
for (GameItem gameItem : gameItems) {
gameItem.setSelected(false);
min.set(gameItem.getPosition());
max.set(gameItem.getPosition());
min.add(-gameItem.getScale(), -gameItem.getScale(), -gameItem.getScale());
max.add(gameItem.getScale(), gameItem.getScale(), gameItem.getScale());
if (Intersectionf.intersectRayAab(center, dir, min, max, nearFar) && nearFar.x < closestDistance) {
closestDistance = nearFar.x;
selectedGameItem[0] = gameItem;
}
else if (selectedGameItem[0] != null && selectedGameItem[1] == null) {
setCircleSelect(gameItems, selectedGameItem);
}
}
for(int i = 0; i < selectedGameItem.length; i++) {
if (selectedGameItem[i] != null) {
//System.out.println("SELECTED: " + i);
selectedGameItem[i].setSelected(true);
selectedGameItem[i] = null;
selected = true;
}
}
return selected;
}
private void setCircleSelect(GameItem[] gameItems, GameItem[] selectedGameItem) {
for(GameItem gameItem : gameItems) {
//double distance = Math.sqrt(Math.pow((gameItem.getPosition().x - selectedGameItem[0].getPosition().x), 2) + Math.pow((gameItem.getPosition().y - selectedGameItem[0].getPosition().y), 2));
double x1 = selectedGameItem[0].getPosition().x;
double z1 = selectedGameItem[0].getPosition().z;
double x2 = gameItem.getPosition().x;
double z2 = gameItem.getPosition().z;
double distance = Point2D.distance(x1, z1, x2, z2);
//System.out.println(distance);
if(distance <= brushSize) {
//Poop
//System.out.println("GOT X:" + gameItem.getPosition().x + " Y:" + gameItem.getPosition().y + "Z:" + gameItem.getPosition().z);
for(int i = 0; i < selectedGameItem.length; i++) {
if (selectedGameItem[i] == null) {
//System.out.println("i: " + i);
selectedGameItem[i] = gameItem;
break;
}
}
}
}
}
public void setBrushSize(double brushSize) {
this.brushSize = brushSize;
}
public double getBrushSize() {
return brushSize;
}
public double getMaxBrushSize() {
return maxBrushSize;
}
}
Thanks for any help! It's been driving me crazy. Just an extra question, After I get this working can someone point me in the right direction on how to put this to a custom file to load in another Java opengl program.