import java.awt.Color; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import javax.media.opengl.GL; import javax.media.opengl.GL2; import static javax.media.opengl.GL2.*; import robotrace.Vector; /** * Implementation of the terrain. */ public class Terrain extends BetterBase { /** * The display list containing the terrain. */ private int terrainDisplayList; /** * The array containing the height map of the terrain. */ private float[][] heightMap; /** * The array containing all vertex normals of the terrain. */ private Vector[][] normalMap; /** * The value determining the alpha value of the water plane. */ private float waterAlpha; private final RobotRace race; /* * The array containing all trees of the terrain. */ private Tree[] terrainTrees; /** * Contains the name of the height map file. */ private final String heightMapFile; private boolean heightMapFileMode; private RaceTrack track; /** * Can be used to set up a display list. */ public Terrain(RobotRace race, RaceTrack track) { // Setup terrain variables this.race = race; this.terrainDisplayList = 0; this.heightMap = new float[41][41]; this.normalMap = new Vector[41][41]; this.waterAlpha = 0.30f; this.terrainTrees = new Tree[10]; this.heightMapFile = "heightmap.bmp"; this.heightMapFileMode = true; this.track = track; // Fill the height map array File f = new File("src/" + heightMapFile); if(f.exists()) { try { heightMap = readHeightMapFromFile(heightMapFile); } catch (IOException ex) { Logger.getLogger(Terrain.class.getName()).log(Level.SEVERE, null, ex); } } else { System.err.println("Heightmap file not found, formula heightmap generated instead..."); this.heightMapFileMode = false; fillHeightMapFormula(); } // Fill the array containing the normal vectors for drawing triangles for (int y = 0; y < 41; y++) { for (int x = 0; x < 41; x++) { normalMap[x][y] = calculateNormal(x, y); } } } /** * Generate the heightmap based on a formula. */ private void fillHeightMapFormula() { for (int y = 0; y < 41; y++) { for (int x = 0; x < 41; x++) { heightMap[x][y] = heightAt(x, y); } } } /** * Reads an image file and returns a heightmap based on the pixels. * * @param fileName name of the image file * @return float[][] height map array * @throws IOException */ private float[][] readHeightMapFromFile(String fileName) throws IOException { BufferedImage image = ImageIO.read(getClass().getResource(fileName)); float[][] heightArray = new float[41][41]; for (int y = 0; y < 41; y++) { for (int x = 0; x < 41; x++) { int rgb = image.getRGB(x, y); // Get red value (since it is black to white only one matters) Color col = new Color(rgb, true); int r = col.getRed(); // Now divide, so the height will be in the interval [-1, 1] float height = (r / 127.5f) - 1; heightArray[x][y] = height; } } return heightArray; } /** * Used to generate a new tree position. * * @return Vector containing random (x,y) position */ private Vector getRandomPosition() { Random r = new Random(); double distance; int x, y; // Make sure all trees are moved away from the track do { x = 19 - r.nextInt(39); y = 19 - r.nextInt(39); distance = Math.sqrt(x * x + y * y); } while (distance < 19); return new Vector(x, y, heightAt(x + 20, y + 20)); } /** * Checks whether the given position including the specified marge is free, * so a tree can placed. * * @param position Tree position * @param marge Marge to check * @return */ private boolean checkPositionFree(Vector position, float marge) { for (Tree tree : terrainTrees) { float x = (float) position.x(); float y = (float) position.y(); float z = (float) position.z(); if (tree != null) { if (Math.abs(tree.getPosition().x() - x) < marge) { return false; } if (Math.abs(tree.getPosition().y() - y) < marge) { return false; } if(x < - 19 || x > 19 || y < - 19 || y > 19) { return false; } // No trees allowed in water if (z < 0.25f) { return false; } } } return true; } /** * Create the terrain and store all calls in a display list * terrainDisplayList, so drawing the terrain is more efficient. */ public void createTerrain() { // Create display list this.terrainDisplayList = gl.glGenLists(1); // Initialize list gl.glNewList(terrainDisplayList, GL2.GL_COMPILE); // Set up texture mode gl.glDisable(GL_TEXTURE_2D); gl.glEnable(GL_TEXTURE_1D); Color[] textureColors = new Color[3]; // Set the colors for the texture (blue at the bottom, grass at top) textureColors[0] = new Color(28, 107, 220); // Water textureColors[2] = new Color(1, 166, 17); // Grass textureColors[1] = new Color(235, 220, 75); // Sand // Create the 1D texture int colorTextureID = create1DTexture(gl, textureColors); // Setting up texture gl.glBindTexture(GL_TEXTURE_1D, colorTextureID); // Create all points of the terrain based on the heightAt function for (int y = -20; y < 20; y++) { gl.glBegin(GL_TRIANGLE_STRIP); for (int x = -20; x < 20; x++) { // Normalize values for use with the arrays int arrayX = x + 20; int arrayY = y + 20; // Create all vertices Vector pointA = new Vector(x, y, heightMap[arrayX][arrayY]); Vector pointB = new Vector(x + 1, y, heightMap[arrayX + 1][arrayY]); Vector pointC = new Vector(x, y + 1, heightMap[arrayX][arrayY + 1]); Vector pointD = new Vector(x + 1, y + 1, heightMap[arrayX + 1][arrayY + 1]); /** * First set the texture coordinate based on the height of the * vertex, then set the normal based on the vertex (per-vertex) * and finally draw the vertex itself. */ gl.glTexCoord1f(getTextureCoordinate((float) pointA.z())); glNormal(normalMap[arrayX][arrayY]); glVertex(pointA); gl.glTexCoord1f(getTextureCoordinate((float) pointB.z())); glNormal(normalMap[arrayX + 1][arrayY]); glVertex(pointB); gl.glTexCoord1f(getTextureCoordinate((float) pointC.z())); glNormal(normalMap[arrayX][arrayY + 1]); glVertex(pointC); gl.glTexCoord1f(getTextureCoordinate((float) pointD.z())); glNormal(normalMap[arrayX + 1][arrayY + 1]); glVertex(pointD); } gl.glEnd(); } // Add water to the terrain at z = 0 gl.glBegin(GL_QUADS); // Create water plane vectors Vector pointA = new Vector(-20, -20, 0); Vector pointB = new Vector(-20, 20, 0); Vector pointC = new Vector(20, 20, 0); Vector pointD = new Vector(20, -20, 0); // Set grey color and transparant alpha value gl.glColor4f(100f, 100f, 100f, waterAlpha); // Draw vertices glVertex(pointA); glVertex(pointB); glVertex(pointC); glVertex(pointD); gl.glEnd(); // Set texture modes gl.glDisable(GL_TEXTURE_1D); gl.glEnable(GL_TEXTURE_2D); /** * End the list, everything in the list can be easily drawed by using * this display list. This is more efficient then just drawing * everything every time. */ gl.glEndList(); // Initialize all trees fillTreeArray(); createTrees(); System.out.println("Terrain created"); } /** * Fills the array with new valid trees. */ private void fillTreeArray() { // Fill the tree array with new trees for the terrain for (int i = 0; i < 10; i++) { Vector treePosition; // Generate a new position until one is found do { treePosition = getRandomPosition(); } while (!checkPositionFree(treePosition, 1f)); // The location is free, create scale and variation variable Random r = new Random(); int variation = r.nextInt(2); // 0 (inclusive) and 2 (exclusive) float scale = r.nextFloat() * 1.5f; // 0 - 1.5 terrainTrees[i] = new Tree(this, treePosition, scale, variation); } } /** * Creates (initializes) all trees stored in the terrainTrees arrays. */ private void createTrees() { for (Tree tree : terrainTrees) { tree.createTree(); } } /** * Determines the texture coordinate based on the height of a vertice so the * 1D textures can be displayed correctly. * * Add 1 to the height and divide by two (now we have [0-1] interval). Now * multiply by 0.8 so we have [0-0.76] and add 0.10 so we end up with [0.10 * - 0.86] for the correct texture display. * * @param height * @return 1D texture coordinate */ private float getTextureCoordinate(float height) { return (((height + 1) / 2) * 0.76f) + 0.10f; } /** * Creates a normal vector based on two input vectors. * * @param x Vector 1 * @param y Vector 2 * @return */ private Vector calculateNormal(int x, int y) { // Create three vectors (points) based on initial x and y (neighbors) Vector v0 = new Vector(x, y, heightMap[x][y]); Vector v1 = new Vector(x + 1, y, heightMap[x < 40 ? x + 1 : x][y]); Vector v2 = new Vector(x, y + 1, heightMap[x][y < 40 ? y + 1 : y]); // Create two vectors pointing from (x,y) to the other two 'points' Vector a = v2.subtract(v0); Vector b = v1.subtract(v0); // Cross product of these two vectors is the normal Vector normal = a.cross(b); // Invert the direction of the normal so it is pointing outwards and normalize it return normal.scale(-1).normalized(); } /** * Draws the terrain. */ public void draw() { gl.glCallList(terrainDisplayList); drawTrees(); } /** * Draw all the trees stored in the terrainTrees array on the terrain. */ private void drawTrees() { for (Tree tree : terrainTrees) { tree.drawTree(); } } /** * Computes the elevation of the terrain at ({@code x}, {@code y}). */ public final float heightAt(int x, int y) { float height; if(heightMapFileMode == false) { // Use the formula to return the height height = (float) (0.6f * Math.cos(0.3f * x + 0.2f * y) + 0.4f * Math.cos(x - 0.5f * y)); } else { // Use the values in the height array instead (file mode) height = (float) heightMap[x][y]; } return height; } /** * Creates a new 1D - texture. * * @param gl * @param colors * @return the texture ID for the generated texture. */ public int create1DTexture(GL2 gl, Color[] colors) { int[] texid = new int[]{-1}; gl.glGenTextures(1, texid, 0); ByteBuffer bb = ByteBuffer.allocateDirect(colors.length * 4).order(ByteOrder.nativeOrder()); for (Color color : colors) { int pixel = color.getRGB(); bb.put((byte) ((pixel >> 16) & 0xFF)); // Red component bb.put((byte) ((pixel >> 8) & 0xFF)); // Green component bb.put((byte) (pixel & 0xFF)); // Blue component bb.put((byte) ((pixel >> 24) & 0xFF)); // Alpha component } bb.flip(); gl.glBindTexture(GL_TEXTURE_1D, texid[0]); gl.glTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA8, colors.length, 0, GL_RGBA, GL_UNSIGNED_BYTE, bb); gl.glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); gl.glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); gl.glBindTexture(GL_TEXTURE_1D, 0); return texid[0]; } }