Pages

JavaFX 3D Terrain Map Generation



                During the Christmas Season 2014, I got some free time for myself to do the things I love – watch movies, animes, and of course, coding. Office days won’t allow me to get into my coding hobby as I usually leave home at 8am, then get back at 8pm. Come the XMAS season… Oh I’d gone wild to do a lot of interesting coding stuff. First in my list was JavaFX 3D. It was not in my intention to create a full-blown 3D game. I just wanted to see how it would compare with the other Java 3D frameworks like JMonkey.


                So first, I decided to create a simple Terrain Map generator. I’m always fascinated with how one can create random mountain ranges through a little amount of code. I already did this in JMonkey and Processing, so I wanted to see how I could easily do this in FX.


                Before I started, I needed to have a refresher course on how it’s done.  Luckily, I have saved a bunch of PDF tutorials in my desktop folder. The ones that are most useful were the tutorials/books offered by Processing.org. Well, for me, reading Processing books are a must for those who wanna have in-depth knowledge in visualization.


                In my case the only concept that I needed to learn is the Perlin noise. Perlin noise is simply a noise function used to generate natural appearing random movements and textures. This is best for creating terrain/mountain range effects as well as clouds, smoke, and a lot more. For generating a terrain map, we need a 3-dimensional perlin noise. Fortunately, the Processing library already has a built-in function noise(). More luckily, Processing uses Java as its language, so I’m all free to use it in JavaFX. :p


                Next is to know how to code in JavaFX 3D. A quick tutorial in JavaFX 3D for Dummies  gave me all I needed to know. From there I learned that to create complex objects, TriangleMesh should be used.


                With all these things I studied, the following summarizes what I did as an output:


1:    private Mesh generateTerrain(int dimension, float noise, int scale){  
2:      float[][] generatedYValues = generateYValues(dimension, noise);  
3:      return createTerrainMesh(dimension, scale, generatedYValues);  
4:    }  
5:    private float[][] generateYValues(int dimension, float noise) {  
6:      float[][] generatedYValues = new float[dimension][dimension];  
7:      float xoff = 0.0f;  
8:      for (int x = 0; x < dimension; x++) {  
9:        float zoff = 0.0f;  
10:        for (int z = 0; z < dimension; z++) {  
11:          generatedYValues[x][z] = PApplet.map(utils.noise(xoff, zoff), 0, 1, 0, dimension);  
12:          zoff += noise;  
13:        }  
14:        xoff += noise;  
15:      }  
16:      return generatedYValues;  
17:    }  
18:    private TriangleMesh createTerrainMesh(int dimension, float scale, float[][] generatedYValues) {  
19:      ObservableFloatArray points = FXCollections.observableFloatArray();  
20:      ObservableIntegerArray faces = FXCollections.observableIntegerArray();  
21:      Integer[][] vertexID = new Integer[dimension][dimension];  
22:      int ctr = 0;  
23:      for (int x = 0; x < dimension; x++) {  
24:        for (int z = 0; z < dimension; z++) {  
25:          float tmpX = x * scale;  
26:          float tmpY = generatedYValues[x][z] * scale;  
27:          float tmpZ = z * scale;  
28:          if (z + 1 < dimension && x + 1 < dimension) {  
29:            //current  
30:            Integer vCurrent = vertexID[x][z];  
31:            Integer vDown = vertexID[x][z + 1];  
32:            Integer vRight = vertexID[x + 1][z];  
33:            if (vCurrent == null) {  
34:              points.addAll(tmpX);  
35:              points.addAll(tmpY);  
36:              points.addAll(tmpZ);  
37:              vertexID[x][z] = ctr++;  
38:              vCurrent = vertexID[x][z];  
39:            }  
40:            if (vDown == null) {  
41:              //point above  
42:              points.addAll(tmpX);  
43:              points.addAll(generatedYValues[x][z + 1] * scale);  
44:              points.addAll(tmpZ + scale);  
45:              vertexID[x][z + 1] = ctr++;  
46:              vDown = vertexID[x][z + 1];  
47:            }  
48:            if (vRight == null) {  
49:              //point to the right  
50:              points.addAll(tmpX + scale);  
51:              points.addAll(generatedYValues[x + 1][z] * scale);  
52:              points.addAll(tmpZ);  
53:              vertexID[x + 1][z] = ctr++;  
54:              vRight = vertexID[x + 1][z];  
55:            }  
56:            faces.addAll(vCurrent);  
57:            faces.addAll(0);  
58:            faces.addAll(vDown);  
59:            faces.addAll(0);  
60:            faces.addAll(vRight);  
61:            faces.addAll(0);  
62:          }  
63:          if (z - 1 >= 0 && x - 1 >= 0) {  
64:            Integer vCurrent = vertexID[x][z];  
65:            Integer vUp = vertexID[x][z - 1];  
66:            Integer vLeft = vertexID[x - 1][z];  
67:            if (vCurrent == null) {  
68:              //current  
69:              points.addAll(tmpX);  
70:              points.addAll(tmpY);  
71:              points.addAll(tmpZ);  
72:              vertexID[x][z] = ctr++;  
73:              vCurrent = vertexID[x][z];  
74:            }  
75:            if (vUp == null) {  
76:              //point to the left  
77:              points.addAll(tmpX - scale);  
78:              points.addAll(generatedYValues[x - 1][z] * scale);  
79:              points.addAll(tmpZ);  
80:              vertexID[x][z - 1] = ctr++;  
81:              vUp = vertexID[x][z - 1];  
82:            }  
83:            if (vLeft == null) {  
84:              //point below  
85:              points.addAll(tmpX);  
86:              points.addAll(generatedYValues[x][z - 1] * scale);  
87:              points.addAll(tmpZ - scale);  
88:              vertexID[x - 1][z] = ctr++;  
89:              vLeft = vertexID[x - 1][z];  
90:            }  
91:            faces.addAll(vCurrent);  
92:            faces.addAll(0);  
93:            faces.addAll(vUp);  
94:            faces.addAll(0);  
95:            faces.addAll(vLeft);  
96:            faces.addAll(0);  
97:          }  
98:        }  
99:      }  
100:      TriangleMesh mesh = new TriangleMesh();  
101:      mesh.getTexCoords().addAll(0, 0);  
102:      mesh.getPoints().addAll(points);  
103:      mesh.getFaces().addAll(faces);  
104:      return mesh;  
105:    }  


 So to generate a terrain map, all we need is to call generateTerrain() with the following parameters:
dimension - defines size of the map
noise - the higher the value, the more random it would look
scale -  increases/decreases the ratio of the map size.

 returns a TriangleMesh object that is then added to MeshView.


  
 A call to generateTerrain(200,10,0.01f) gives:
 













 generateTerrain(200,10,0.05f)
















 generateTerrain(200,10,0.00f)



















Source code here:
https://github.com/fxperiments/JavaFX3DTerrainMapGenerator 

francis valero

Phasellus facilisis convallis metus, ut imperdiet augue auctor nec. Duis at velit id augue lobortis porta. Sed varius, enim accumsan aliquam tincidunt, tortor urna vulputate quam, eget finibus urna est in augue.

1 comment:

  1. Hello,

    Your GitHub link is broken, can I get your source code from else where?
    I would like to try your implementation.

    ReplyDelete