org.shaman.terrain.polygonal.GraphToHeightmap.java Source code

Java tutorial

Introduction

Here is the source code for org.shaman.terrain.polygonal.GraphToHeightmap.java

Source

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package org.shaman.terrain.polygonal;

import org.shaman.terrain.Biome;
import Jama.Matrix;
import com.jme3.app.Application;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer;
import com.jme3.texture.FrameBuffer;
import com.jme3.texture.Image;
import com.jme3.util.BufferUtils;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import org.apache.commons.lang3.ArrayUtils;
import org.shaman.terrain.AbstractTerrainStep;
import org.shaman.terrain.Heightmap;
import org.shaman.terrain.Vectorfield;
import org.shaman.terrain.heightmap.Noise;
import org.shaman.terrain.sketch.SketchTerrain;

/**
 * Converts the graph to a heightmap.<br>
 * Input: a {@link Graph} with all the informations of elevation, temperature,
 * moisture, biomes.<br>
 * Output: a {@link Map} object with the heightmap, temperature and moisture
 * stored in {@link Heightmap} instances, using the keys from
 * {@link AbstractTerrainStep}.
 *
 * @author Sebastian Weiss
 */
public class GraphToHeightmap {
    private static final Logger LOG = Logger.getLogger(GraphToHeightmap.class.getName());
    /**
     * Maps the biomes to noise amplitude (0), noise roughness (1),
     * voronoi amplitude (2) and the factor of the height on the voronoi amplitude(3)
     */
    private static final EnumMap<Biome, double[]> BIOME_PROPERTIES = new EnumMap<>(Biome.class);
    static {
        BIOME_PROPERTIES.put(Biome.SNOW, new double[] { 0.5, 0.7, 0.7, 0.5 });
        BIOME_PROPERTIES.put(Biome.TUNDRA, new double[] { 0.5, 0.5, 0.5, 0.5 });
        BIOME_PROPERTIES.put(Biome.BARE, new double[] { 0.4, 0.4, 0.5, 0.5 });
        BIOME_PROPERTIES.put(Biome.SCORCHED, new double[] { 0.7, 0.3, 0, 0 });
        BIOME_PROPERTIES.put(Biome.TAIGA, new double[] { 0.4, 0.3, 0, 0 });
        BIOME_PROPERTIES.put(Biome.SHRUBLAND, new double[] { 0.5, 0.2, 0, 0 });
        BIOME_PROPERTIES.put(Biome.TEMPERATE_DESERT, new double[] { 0.1, 0.1, 0.3, 0 });
        BIOME_PROPERTIES.put(Biome.TEMPERATE_RAIN_FOREST, new double[] { 0.3, 0.2, 0, 0 });
        BIOME_PROPERTIES.put(Biome.TEMPERATE_DECIDUOUS_FOREST, new double[] { 0.3, 0.4, 0, 0 });
        BIOME_PROPERTIES.put(Biome.GRASSLAND, new double[] { 0.4, 0.5, 0, 0 });
        BIOME_PROPERTIES.put(Biome.TROPICAL_RAIN_FOREST, new double[] { 0.3, 0.2, 0.05, 0 });
        BIOME_PROPERTIES.put(Biome.TROPICAL_SEASONAL_FOREST, new double[] { 0.3, 0.2, 0, 0 });
        BIOME_PROPERTIES.put(Biome.SUBTROPICAL_DESERT, new double[] { 0.3, 0.6, 0.3, 0 });
        BIOME_PROPERTIES.put(Biome.BEACH, new double[] { 0.3, 0.5, 0.1, 0 });
        BIOME_PROPERTIES.put(Biome.LAKE, new double[] { 0.2, 0.2, 0, 0 });
        BIOME_PROPERTIES.put(Biome.OCEAN, new double[] { 0.2, 0.1, 0, 0 });
    }
    private static final int NOISE_OCTAVES = 6;
    private static final float NOISE_OCTAVE_FACTOR = 2;
    private static final float BASE_FREQUENCY = 1 / 32f;
    private static final float PERLIN_NOISE_SCALE = 0.2f;
    private static final float DISTORTION_FREQUENCY = 32;
    private static final float DISTORTION_AMPLITUDE = 0.01f;
    private static final float SMOOTHING_STEPS = 10;
    private static final int[][] NEIGHBORS = new int[][] { { 1, -1 }, { 1, 0 }, { 1, 1 }, { -1, -1 }, { -1, 0 },
            { -1, 1 }, { 0, 1 }, { 0, -1 } };
    private static final int VORONOI_CELL_COUNT = 32;
    private static final int VORONOI_POINTS_PER_CELL = 3;
    private static final float VORONOI_DISTORTION_FREQUENCY = 32;
    private static final float VORONOI_DISTORTION_AMPLITUDE = 0;//0.01f;
    private static final float VORONOI_SCALE = 1.5f;
    private static final float BIOMES_DISTORTION_FREQUENCY = 32;
    private static final float BIOMES_DISTORTION_AMPLITUDE = 0.005f;
    private static final double HEIGHT_SCALING = 1.5;

    //Input
    private final Graph graph;
    private final int size;
    private final Application app;
    private final Random rand;
    //Output
    private final Heightmap heightmap;
    private final Heightmap temperature;
    private final Heightmap moisture;
    private final Vectorfield biomes;
    private final Map<Object, Object> properties;
    //temporal values
    private float[][][] noise;

    public GraphToHeightmap(Graph graph, int size, Application app, long seed) {
        this.graph = graph;
        this.size = size;
        this.app = app;
        this.rand = new Random(seed);

        heightmap = new Heightmap(size);
        temperature = new Heightmap(size);
        moisture = new Heightmap(size);
        biomes = new Vectorfield(size, Biome.values().length);
        properties = new HashMap<>();
        properties.put(AbstractTerrainStep.KEY_HEIGHTMAP, heightmap);
        properties.put(AbstractTerrainStep.KEY_TEMPERATURE, temperature);
        properties.put(AbstractTerrainStep.KEY_MOISTURE, moisture);
        properties.put(AbstractTerrainStep.KEY_BIOMES, biomes);
        //      properties.put("PolygonalGraph", graph); //for backup

        calculate();
    }

    public Map<Object, Object> getResult() {
        return properties;
    }

    private void calculate() {
        calculateTemperatureAndMoisture();
        calculateElevation();
        calculateBiomeVectorfield();

        //      saveMaps();
    }

    private void calculateElevation() {
        calculateBaseElevation();

        //get noise parameters
        Geometry geom = createNoiseGeometry();
        noise = new float[size][size][4];
        renderColor(noise, geom, ColorRGBA.Black, 0, 1);
        LOG.info("noise properties calculated");

        addPerlinNoise();

        //      addVoronoiNoise();
    }

    private void addPerlinNoise() {
        float[] perlinFactors = new float[NOISE_OCTAVES];
        Noise[] noiseGenerators = new Noise[NOISE_OCTAVES];
        for (int i = 0; i < NOISE_OCTAVES; ++i) {
            noiseGenerators[i] = new Noise(rand.nextLong());
            perlinFactors[i] = (float) (BASE_FREQUENCY * Math.pow(NOISE_OCTAVE_FACTOR, i));
        }
        Heightmap values = new Heightmap(size);
        float min = Float.POSITIVE_INFINITY;
        float max = Float.NEGATIVE_INFINITY;
        for (int x = 0; x < size; ++x) {
            for (int y = 0; y < size; ++y) {
                float roughness = noise[x][y][1];
                //multi-fractal perlin noise
                double perlin = 0;
                for (int i = 0; i < NOISE_OCTAVES; ++i) {
                    perlin += noiseGenerators[i].noise(perlinFactors[i] * x, perlinFactors[i] * y)
                            / Math.pow(NOISE_OCTAVE_FACTOR, i * (1 - roughness));
                }
                values.setHeightAt(x, y, (float) perlin);
                min = (float) Math.min(perlin, min);
                max = (float) Math.max(perlin, max);
            }
        }
        float factor = 1f / (max - min);
        for (int x = 0; x < size; ++x) {
            for (int y = 0; y < size; ++y) {
                float amplitude = noise[x][y][0];
                float perlin = (values.getHeightAt(x, y) - min) * factor;
                perlin *= amplitude * PERLIN_NOISE_SCALE;
                heightmap.adjustHeightAt(x, y, perlin);
            }
        }
        LOG.info("perlin noise added");
    }

    private void addVoronoiNoise() {
        float cellSize = (float) size / (float) VORONOI_CELL_COUNT;
        List<Vector3f> pointList = new ArrayList<>();
        for (int x = 0; x < VORONOI_CELL_COUNT; ++x) {
            for (int y = 0; y < VORONOI_CELL_COUNT; ++y) {
                float nx = x * cellSize;
                float ny = y * cellSize;
                for (int i = 0; i < VORONOI_POINTS_PER_CELL; ++i) {
                    pointList.add(new Vector3f(nx + rand.nextFloat() * cellSize, ny + rand.nextFloat() * cellSize,
                            rand.nextFloat()));
                }
            }
        }
        Vector3f[] points = pointList.toArray(new Vector3f[pointList.size()]);
        LOG.info("voronoi point created");
        //now cycle through cells and find influencing hills
        Heightmap tmp = new Heightmap(size);
        Vector3f first = new Vector3f();
        Vector3f second = new Vector3f();
        float min = Float.POSITIVE_INFINITY;
        float max = Float.NEGATIVE_INFINITY;
        for (int x = 0; x < size; ++x) {
            for (int y = 0; y < size; ++y) {
                //sort points by distance to (x,y)
                final float px = x;
                final float py = y;
                findClosestTwoPoints(points, px, py, first, second);
                assert (dist(first, px, py) <= dist(second, px, py));
                //calc height
                float v = -1 * dist(first, px, py) + 1 * dist(second, px, py);
                tmp.setHeightAt(x, y, v);
                min = Math.min(v, min);
                max = Math.max(v, max);
            }
        }
        //normalize
        float factor = 1f / (max - min);
        for (int x = 0; x < size; ++x) {
            for (int y = 0; y < size; ++y) {
                float h = tmp.getHeightAt(x, y);
                tmp.setHeightAt(x, y, (h - min) * factor);
            }
        }
        LOG.info("voronoi cells calculated");
        //distort
        Noise distortionNoise = new Noise(rand.nextLong());
        Heightmap tmp2 = new Heightmap(size);
        for (int x = 0; x < size; ++x) {
            for (int y = 0; y < size; ++y) {
                float s = x / (float) size;
                float t = y / (float) size;
                float ss = (float) (s + VORONOI_DISTORTION_AMPLITUDE * 2 * distortionNoise
                        .noise(s * VORONOI_DISTORTION_FREQUENCY, t * VORONOI_DISTORTION_FREQUENCY, 0));
                float tt = (float) (t + VORONOI_DISTORTION_AMPLITUDE * 2 * distortionNoise
                        .noise(s * VORONOI_DISTORTION_FREQUENCY, t * VORONOI_DISTORTION_FREQUENCY, 3.4));
                float v = tmp.getHeightInterpolating(ss * size, tt * size);
                tmp2.setHeightAt(x, y, v);
            }
        }
        LOG.info("voronoi cells distorted");
        //apply
        for (int x = 0; x < size; ++x) {
            for (int y = 0; y < size; ++y) {
                float amplitude = noise[x][y][2];
                heightmap.adjustHeightAt(x, y, amplitude * tmp.getHeightAt(x, y) * VORONOI_SCALE);
            }
        }

        LOG.info("voronoi noise added");
    }

    private static void findClosestTwoPoints(Vector3f[] points, float px, float py, Vector3f first,
            Vector3f second) {
        float dist1 = Float.POSITIVE_INFINITY;
        float dist2 = Float.POSITIVE_INFINITY;
        for (Vector3f p : points) {
            float d = dist(p, px, py);
            if (d < dist1) {
                dist2 = dist1;
                second.set(first);
                dist1 = d;
                first.set(p);
            } else if (d < dist2) {
                dist2 = d;
                second.set(p);
            }
        }
    }

    private static float dist(Vector3f hillCenter, float px, float py) {
        return FastMath.sqrt((hillCenter.x - px) * (hillCenter.x - px) + (hillCenter.y - py) * (hillCenter.y - py))
                * hillCenter.z;
        //return ((hillCenter.x-px)*(hillCenter.x-px) + (hillCenter.y-py)*(hillCenter.y-py))*hillCenter.z;
    }

    private void calculateBaseElevation() {
        //assign elevation to oceans
        for (Graph.Corner c : graph.corners) {
            if (c.ocean) {
                c.elevation = -1;
            }
        }
        Queue<Graph.Corner> q = new ArrayDeque<>();
        for (Graph.Corner c : graph.corners) {
            if (c.coast) {
                q.add(c);
            }
        }
        while (!q.isEmpty()) {
            Graph.Corner c = q.poll();
            for (Graph.Corner r : c.adjacent) {
                float h = Math.max(-1, c.elevation - 0.2f);
                if (r.ocean && r.elevation < h) {
                    r.elevation = h;
                    q.add(r);
                }
            }
        }
        assignCenterElevations();
        //render
        Geometry geom = createElevationGeometry();
        Heightmap tmp = new Heightmap(size);
        render(tmp.getRawData(), geom, ColorRGBA.Black, -1, 1);
        //scale
        for (int x = 0; x < size; ++x) {
            for (int y = 0; y < size; ++y) {
                float h = tmp.getHeightAt(x, y);
                h = (float) (Math.signum(h) * Math.pow(Math.abs(h), HEIGHT_SCALING));
                tmp.setHeightAt(x, y, h);
            }
        }
        //distort
        Noise distortionNoise = new Noise(rand.nextLong());
        for (int x = 0; x < size; ++x) {
            for (int y = 0; y < size; ++y) {
                float s = x / (float) size;
                float t = y / (float) size;
                float ss = (float) (s + DISTORTION_AMPLITUDE * 2
                        * distortionNoise.noise(s * DISTORTION_FREQUENCY, t * DISTORTION_FREQUENCY, 0));
                float tt = (float) (t + DISTORTION_AMPLITUDE * 2
                        * distortionNoise.noise(s * DISTORTION_FREQUENCY, t * DISTORTION_FREQUENCY, 3.4));
                float v = tmp.getHeightInterpolating(ss * size, tt * size);
                heightmap.setHeightAt(x, y, v);
            }
        }
        //smooth
        for (int i = 0; i < SMOOTHING_STEPS; ++i) {
            smooth(heightmap);
        }
        //reset height
        for (Graph.Corner c : graph.corners) {
            if (c.ocean) {
                c.elevation = 0;
            }
        }
        assignCenterElevations();
        LOG.info("base elevation assigned");
    }

    private void smooth_old(Heightmap map) {
        Heightmap m = map.clone();
        float t = 2 / map.getSize();
        for (int x = 0; x < map.getSize(); ++x) {
            for (int y = 0; y < map.getSize(); ++y) {
                float h = map.getHeightAt(x, y);
                float[] hi = new float[NEIGHBORS.length];
                float[] di = new float[NEIGHBORS.length];
                float dmax = 0;
                float dtotal = 0;
                //compute slopes
                for (int i = 0; i < NEIGHBORS.length; ++i) {
                    hi[i] = m.getHeightAtClamping(x + NEIGHBORS[i][0], y + NEIGHBORS[i][1]);
                    di[i] = h - hi[i];
                    dmax = Math.max(dmax, di[i]);
                    if (di[i] > t) {
                        dtotal += di[i];
                    }
                }
                //move terrain
                float vsum = 0;
                for (int i = 0; i < NEIGHBORS.length; ++i) {
                    if (di[i] > t) {
                        float v = 0.25f * (dmax - t) * di[i] / dtotal;
                        map.adjustHeightAt(x + NEIGHBORS[i][0], y + NEIGHBORS[i][1], v);
                        vsum += v;
                    }
                }
                map.adjustHeightAt(x, y, -vsum);
            }
        }
    }

    private void smooth(Heightmap map) {
        Heightmap m = map.clone();
        float t = 2 / map.getSize();
        for (int x = 0; x < map.getSize(); ++x) {
            for (int y = 0; y < map.getSize(); ++y) {
                float h = map.getHeightAt(x, y);
                float nh = 0;
                //compute average
                for (int i = 0; i < NEIGHBORS.length; ++i) {
                    nh += m.getHeightAtClamping(x + NEIGHBORS[i][0], y + NEIGHBORS[i][1]);
                }
                nh /= NEIGHBORS.length;
                float diff = nh - h;
                diff *= 0.2f;
                map.adjustHeightAt(x, y, diff);
            }
        }
    }

    private void assignCenterElevations() {
        //assign elevation to centers
        for (Graph.Center c : graph.centers) {
            float elevation = 0;
            for (Graph.Corner corner : c.corners) {
                elevation += corner.elevation;
            }
            elevation /= c.corners.size();
            c.elevation = elevation;
        }
    }

    private void calculateTemperatureAndMoisture() {
        Geometry geom = createTemperatureGeometry();
        render(temperature.getRawData(), geom, ColorRGBA.White, 0, 1);
        LOG.info("temperature map created");

        geom = createMoistureGeometry();
        render(moisture.getRawData(), geom, ColorRGBA.Black, 0, 1);
        LOG.info("moisture map created");
    }

    private void calculateBiomeVectorfield() {
        float[][] tmp = new float[size][size];
        Vectorfield tmpBiomes = new Vectorfield(size, Biome.values().length);
        for (int i = 0; i < Biome.values().length; ++i) {
            Biome b = Biome.values()[i];
            Geometry geom = createBiomesGeometry(b);
            //render(tmp, geom, b==Biome.OCEAN ? ColorRGBA.White : ColorRGBA.Black, 0, 1);
            render(tmp, geom, ColorRGBA.Black, 0, 1);
            tmpBiomes.setLayer(i, tmp);
        }
        LOG.info("biome vectorfield rendered");
        //disturb
        Noise distortionNoise = new Noise(rand.nextLong());
        float[] v = new float[Biome.values().length];
        for (int x = 0; x < size; ++x) {
            for (int y = 0; y < size; ++y) {
                float s = x / (float) size;
                float t = y / (float) size;
                float ss = (float) (s + BIOMES_DISTORTION_AMPLITUDE * 2 * distortionNoise
                        .noise(s * BIOMES_DISTORTION_FREQUENCY, t * BIOMES_DISTORTION_FREQUENCY, 0));
                float tt = (float) (t + BIOMES_DISTORTION_AMPLITUDE * 2 * distortionNoise
                        .noise(s * BIOMES_DISTORTION_FREQUENCY, t * BIOMES_DISTORTION_FREQUENCY, 3.4));
                v = tmpBiomes.getVectorInterpolating(ss * size, tt * size, v);
                for (int i = 0; i < Biome.values().length; ++i) {
                    biomes.setVectorAt(x, y, v);
                }
            }
        }
        //TODO: maybe add smoothing
    }

    private Geometry createElevationGeometry() {
        ArrayList<Vector3f> posList = new ArrayList<>();
        ArrayList<Integer> indexList = new ArrayList<>();
        ArrayList<ColorRGBA> colorList = new ArrayList<>();
        Map<Graph.Corner, Integer> cornerIndices = new HashMap<>();
        for (Graph.Center c : graph.centers) {
            int i = posList.size();
            posList.add(new Vector3f(c.location.x, c.location.y, 1));
            colorList.add(new ColorRGBA(c.elevation / 2 + 0.5f, c.elevation / 2 + 0.5f, c.elevation / 2 + 0.5f, 1));
            cornerIndices.clear();
            for (Graph.Corner q : c.corners) {
                cornerIndices.put(q, posList.size());
                posList.add(new Vector3f(q.point.x, q.point.y, 1));
                colorList.add(
                        new ColorRGBA(q.elevation / 2 + 0.5f, q.elevation / 2 + 0.5f, q.elevation / 2 + 0.5f, 1));
            }
            for (Graph.Edge edge : c.borders) {
                indexList.add(i);
                indexList.add(cornerIndices.get(edge.v0));
                indexList.add(cornerIndices.get(edge.v1));
            }
        }
        Mesh mesh = new Mesh();
        mesh.setBuffer(VertexBuffer.Type.Position, 3,
                BufferUtils.createFloatBuffer(posList.toArray(new Vector3f[posList.size()])));
        mesh.setBuffer(VertexBuffer.Type.Color, 4,
                BufferUtils.createFloatBuffer(colorList.toArray(new ColorRGBA[colorList.size()])));
        mesh.setBuffer(VertexBuffer.Type.Index, 1, BufferUtils
                .createIntBuffer(ArrayUtils.toPrimitive(indexList.toArray(new Integer[indexList.size()]))));
        mesh.setMode(Mesh.Mode.Triangles);
        mesh.updateCounts();
        Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setBoolean("VertexColor", true);
        mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
        Geometry geom = new Geometry("elevationGeom", mesh);
        geom.setMaterial(mat);
        geom.setLocalScale(size);
        geom.setQueueBucket(RenderQueue.Bucket.Gui);
        geom.setCullHint(Spatial.CullHint.Never);
        return geom;
    }

    private Geometry createNoiseGeometry() {
        ArrayList<Vector3f> posList = new ArrayList<>(graph.centers.size());
        ArrayList<Integer> indexList = new ArrayList<>();
        ArrayList<ColorRGBA> colorList = new ArrayList<>(graph.centers.size());
        for (Graph.Center c : graph.centers) {
            double[] settings = BIOME_PROPERTIES.get(c.biome);
            float r = (float) settings[0];
            float g = (float) settings[1];
            float b = (float) (settings[2] + settings[3] * Math.max(0, c.elevation) * Math.max(0, c.elevation));
            //float b = Math.max(0, c.elevation); b*=b;
            ColorRGBA col = new ColorRGBA(r, g, b, 1);
            colorList.add(col);
            posList.add(new Vector3f(c.location.x, c.location.y, 0));
        }
        for (Graph.Corner c : graph.corners) {
            if (c.touches.size() != 3) {
                //LOG.log(Level.INFO, "corner {0} does not touch 3 centers but {1}", new Object[]{c, c.touches.size()});
                continue; //border
            } else {
                for (Graph.Center c2 : c.touches) {
                    indexList.add(c2.index);
                }
            }
        }
        Mesh mesh = new Mesh();
        mesh.setBuffer(VertexBuffer.Type.Position, 3,
                BufferUtils.createFloatBuffer(posList.toArray(new Vector3f[posList.size()])));
        mesh.setBuffer(VertexBuffer.Type.Color, 4,
                BufferUtils.createFloatBuffer(colorList.toArray(new ColorRGBA[colorList.size()])));
        mesh.setBuffer(VertexBuffer.Type.Index, 1, BufferUtils
                .createIntBuffer(ArrayUtils.toPrimitive(indexList.toArray(new Integer[indexList.size()]))));
        mesh.setMode(Mesh.Mode.Triangles);
        mesh.updateCounts();
        Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setBoolean("VertexColor", true);
        mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
        Geometry geom = new Geometry("elevationGeom", mesh);
        geom.setMaterial(mat);
        geom.setLocalScale(size);
        geom.setQueueBucket(RenderQueue.Bucket.Gui);
        geom.setCullHint(Spatial.CullHint.Never);
        return geom;
    }

    private Geometry createTemperatureGeometry() {
        ArrayList<Vector3f> posList = new ArrayList<>();
        ArrayList<Integer> indexList = new ArrayList<>();
        ArrayList<ColorRGBA> colorList = new ArrayList<>();
        Map<Graph.Corner, Integer> cornerIndices = new HashMap<>();
        for (Graph.Center c : graph.centers) {
            int i = posList.size();
            posList.add(new Vector3f(c.location.x, c.location.y, 1));
            colorList.add(new ColorRGBA(c.temperature, c.temperature, c.temperature, 1));
            cornerIndices.clear();
            for (Graph.Corner q : c.corners) {
                cornerIndices.put(q, posList.size());
                posList.add(new Vector3f(q.point.x, q.point.y, 1));
                colorList.add(new ColorRGBA(q.temperature, q.temperature, q.temperature, 1));
            }
            for (Graph.Edge edge : c.borders) {
                indexList.add(i);
                indexList.add(cornerIndices.get(edge.v0));
                indexList.add(cornerIndices.get(edge.v1));
            }
        }
        Mesh mesh = new Mesh();
        mesh.setBuffer(VertexBuffer.Type.Position, 3,
                BufferUtils.createFloatBuffer(posList.toArray(new Vector3f[posList.size()])));
        mesh.setBuffer(VertexBuffer.Type.Color, 4,
                BufferUtils.createFloatBuffer(colorList.toArray(new ColorRGBA[colorList.size()])));
        mesh.setBuffer(VertexBuffer.Type.Index, 1, BufferUtils
                .createIntBuffer(ArrayUtils.toPrimitive(indexList.toArray(new Integer[indexList.size()]))));
        mesh.setMode(Mesh.Mode.Triangles);
        mesh.updateCounts();
        Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setBoolean("VertexColor", true);
        mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
        Geometry geom = new Geometry("biomesGeom", mesh);
        geom.setMaterial(mat);
        geom.setLocalScale(size);
        geom.setQueueBucket(RenderQueue.Bucket.Gui);
        geom.setCullHint(Spatial.CullHint.Never);
        return geom;
    }

    private Geometry createMoistureGeometry() {
        //moisture
        ArrayList<Vector3f> posList = new ArrayList<>();
        ArrayList<Integer> indexList = new ArrayList<>();
        ArrayList<ColorRGBA> colorList = new ArrayList<>();
        Map<Graph.Corner, Integer> cornerIndices = new HashMap<>();
        ColorRGBA waterMoisture = new ColorRGBA(1, 1, 1, 1);
        ColorRGBA oceanMoisture = new ColorRGBA(0, 0, 0, 1);
        for (Graph.Center c : graph.centers) {
            int i = posList.size();
            posList.add(new Vector3f(c.location.x, c.location.y, 1));
            if (c.ocean) {
                colorList.add(oceanMoisture);
            } else if (c.water) {
                colorList.add(waterMoisture);
            } else {
                colorList.add(new ColorRGBA(1 - c.moisture, 1 - c.moisture, 1 - c.moisture, 1));
            }
            cornerIndices.clear();
            for (Graph.Corner q : c.corners) {
                cornerIndices.put(q, posList.size());
                posList.add(new Vector3f(q.point.x, q.point.y, 1));
                if (c.ocean) {
                    colorList.add(oceanMoisture);
                } else if (c.water) {
                    colorList.add(waterMoisture);
                } else {
                    colorList.add(new ColorRGBA(1 - q.moisture, 1 - q.moisture, 1 - q.moisture, 1));
                }
            }
            for (Graph.Edge edge : c.borders) {
                indexList.add(i);
                indexList.add(cornerIndices.get(edge.v0));
                indexList.add(cornerIndices.get(edge.v1));
            }
        }
        Mesh mesh = new Mesh();
        mesh.setBuffer(VertexBuffer.Type.Position, 3,
                BufferUtils.createFloatBuffer(posList.toArray(new Vector3f[posList.size()])));
        mesh.setBuffer(VertexBuffer.Type.Color, 4,
                BufferUtils.createFloatBuffer(colorList.toArray(new ColorRGBA[colorList.size()])));
        mesh.setBuffer(VertexBuffer.Type.Index, 1, BufferUtils
                .createIntBuffer(ArrayUtils.toPrimitive(indexList.toArray(new Integer[indexList.size()]))));
        mesh.setMode(Mesh.Mode.Triangles);
        mesh.updateCounts();
        Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setBoolean("VertexColor", true);
        mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
        Geometry geom = new Geometry("biomesGeom", mesh);
        geom.setMaterial(mat);
        geom.setLocalScale(size);
        geom.setQueueBucket(RenderQueue.Bucket.Gui);
        geom.setCullHint(Spatial.CullHint.Never);
        return geom;
    }

    private Geometry createBiomesGeometry(Biome slot) {
        //biomes
        ArrayList<Vector3f> posList = new ArrayList<>();
        ArrayList<Integer> indexList = new ArrayList<>();
        Map<Graph.Corner, Integer> cornerIndices = new HashMap<>();
        for (Graph.Center c : graph.centers) {
            if (c.biome != slot) {
                continue;
            }
            int i = posList.size();
            posList.add(new Vector3f(c.location.x, c.location.y, 1));
            cornerIndices.clear();
            for (Graph.Corner corner : c.corners) {
                cornerIndices.put(corner, posList.size());
                posList.add(new Vector3f(corner.point.x, corner.point.y, 1));
            }
            for (Graph.Edge edge : c.borders) {
                indexList.add(i);
                indexList.add(cornerIndices.get(edge.v0));
                indexList.add(cornerIndices.get(edge.v1));
            }
        }
        Mesh biomesMesh = new Mesh();
        biomesMesh.setBuffer(VertexBuffer.Type.Position, 3,
                BufferUtils.createFloatBuffer(posList.toArray(new Vector3f[posList.size()])));
        biomesMesh.setBuffer(VertexBuffer.Type.Index, 1, BufferUtils
                .createIntBuffer(ArrayUtils.toPrimitive(indexList.toArray(new Integer[indexList.size()]))));
        biomesMesh.setMode(Mesh.Mode.Triangles);
        biomesMesh.updateCounts();
        Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.White);
        mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
        Geometry geom = new Geometry("biomesGeom", biomesMesh);
        geom.setMaterial(mat);
        geom.setLocalScale(size);
        geom.setQueueBucket(RenderQueue.Bucket.Gui);
        geom.setCullHint(Spatial.CullHint.Never);
        return geom;
    }

    /**
     * Renders the given scene in a top-down manner in the given matrix
     *
     * @param matrix
     * @param scene
     */
    private void render(float[][] matrix, final Spatial scene, final ColorRGBA background, float min, float max) {
        final ByteBuffer data = BufferUtils.createByteBuffer(size * size * 4 * 4);
        try {
            app.enqueue(new Callable<Object>() {
                @Override
                public Object call() throws Exception {
                    //init
                    Camera cam = new Camera(size, size);
                    cam.setParallelProjection(true);
                    final ViewPort view = new ViewPort("Off", cam);
                    view.setBackgroundColor(background);
                    view.setClearFlags(true, true, true);
                    final FrameBuffer buffer = new FrameBuffer(size, size, 1);
                    buffer.setDepthBuffer(Image.Format.Depth);
                    buffer.setColorBuffer(Image.Format.RGBA32F);
                    view.setOutputFrameBuffer(buffer);
                    view.attachScene(scene);
                    //render
                    scene.setCullHint(Spatial.CullHint.Never);
                    scene.updateGeometricState();
                    view.setEnabled(true);
                    app.getRenderManager().renderViewPort(view, 0);
                    app.getRenderer().readFrameBufferWithFormat(buffer, data, Image.Format.RGBA32F);
                    return new Object();
                }
            }).get();
        } catch (InterruptedException | ExecutionException ex) {
            Logger.getLogger(GraphToHeightmap.class.getName()).log(Level.SEVERE, "unable to render", ex);
            return;
        }
        data.rewind();
        for (int y = 0; y < size; ++y) {
            for (int x = 0; x < size; ++x) {
                float v = data.getFloat();
                v *= (max - min);
                v += min;
                matrix[x][y] = v;
                data.getFloat();
                data.getFloat();
                data.getFloat();
            }
        }
    }

    /**
     * Renders the given scene in a top-down manner in the given matrix
     *
     * @param matrix
     * @param scene
     */
    private void renderColor(float[][][] matrix, final Spatial scene, final ColorRGBA background, float min,
            float max) {
        final ByteBuffer data = BufferUtils.createByteBuffer(size * size * 4 * 4);
        try {
            app.enqueue(new Callable<Object>() {
                @Override
                public Object call() throws Exception {
                    //init
                    Camera cam = new Camera(size, size);
                    cam.setParallelProjection(true);
                    final ViewPort view = new ViewPort("Off", cam);
                    view.setBackgroundColor(background);
                    view.setClearFlags(true, true, true);
                    final FrameBuffer buffer = new FrameBuffer(size, size, 1);
                    buffer.setDepthBuffer(Image.Format.Depth);
                    buffer.setColorBuffer(Image.Format.RGBA32F);
                    view.setOutputFrameBuffer(buffer);
                    view.attachScene(scene);
                    //render
                    scene.setCullHint(Spatial.CullHint.Never);
                    scene.updateGeometricState();
                    view.setEnabled(true);
                    app.getRenderManager().renderViewPort(view, 0);
                    app.getRenderer().readFrameBufferWithFormat(buffer, data, Image.Format.RGBA32F);
                    return new Object();
                }
            }).get();
        } catch (InterruptedException | ExecutionException ex) {
            Logger.getLogger(GraphToHeightmap.class.getName()).log(Level.SEVERE, "unable to render", ex);
            return;
        }
        data.rewind();
        for (int y = 0; y < size; ++y) {
            for (int x = 0; x < size; ++x) {
                float v;
                v = data.getFloat();
                v *= (max - min);
                v += min;
                matrix[x][y][0] = v;

                v = data.getFloat();
                v *= (max - min);
                v += min;
                matrix[x][y][1] = v;

                v = data.getFloat();
                v *= (max - min);
                v += min;
                matrix[x][y][2] = v;

                v = data.getFloat();
                v *= (max - min);
                v += min;
                matrix[x][y][3] = v;
            }
        }
    }

    private void saveMaps() {
        saveMatrix(temperature.getRawData(), "temperature.png", 0, 1);
        saveMatrix(moisture.getRawData(), "moisture.png", 0, 1);
        saveMatrix(heightmap.getRawData(), "elevation.png", -1.5f, 1.5f);
        saveColorMatrix(noise, "noise.png", 0, 1);

        float[][] tmp = new float[size][size];
        for (int i = 0; i < Biome.values().length; ++i) {
            tmp = biomes.getLayer(i, tmp);
            saveMatrix(tmp, "Biome" + Biome.values()[i] + ".png", 0, 1);
        }
    }

    private void saveMatrix(float[][] matrix, String filename, float min, float max) {
        byte[] buffer = new byte[size * size];
        int i = 0;
        for (int x = 0; x < size; ++x) {
            for (int y = 0; y < size; ++y) {
                buffer[i] = (byte) ((matrix[x][y] - min) * 255 / (max - min));
                i++;
            }
        }
        ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
        int[] nBits = { 8 };
        ColorModel cm = new ComponentColorModel(cs, nBits, false, true, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
        SampleModel sm = cm.createCompatibleSampleModel(size, size);
        DataBufferByte db = new DataBufferByte(buffer, size * size);
        WritableRaster raster = Raster.createWritableRaster(sm, db, null);
        BufferedImage result = new BufferedImage(cm, raster, false, null);
        try {
            ImageIO.write(result, "png", new File(filename));
        } catch (IOException ex) {
            Logger.getLogger(SketchTerrain.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void saveColorMatrix(float[][][] matrix, String filename, float min, float max) {
        byte[] buffer = new byte[size * size * 3];
        int i = 0;
        for (int x = 0; x < size; ++x) {
            for (int y = 0; y < size; ++y) {
                buffer[i] = (byte) ((matrix[x][y][0] - min) * 255 / (max - min));
                i++;
                buffer[i] = (byte) ((matrix[x][y][1] - min) * 255 / (max - min));
                i++;
                buffer[i] = (byte) ((matrix[x][y][2] - min) * 255 / (max - min));
                i++;
            }
        }
        ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB);
        int[] nBits = { 8, 8, 8 };
        ColorModel cm = new ComponentColorModel(cs, nBits, false, true, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
        SampleModel sm = cm.createCompatibleSampleModel(size, size);
        DataBufferByte db = new DataBufferByte(buffer, size * size * 3);
        WritableRaster raster = Raster.createWritableRaster(sm, db, null);
        BufferedImage result = new BufferedImage(cm, raster, false, null);
        try {
            ImageIO.write(result, "png", new File(filename));
        } catch (IOException ex) {
            Logger.getLogger(SketchTerrain.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}