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
Hello,
ReplyDeleteYour GitHub link is broken, can I get your source code from else where?
I would like to try your implementation.