1 /*
2  * Copyright (c) 2009-2010 jMonkeyEngine
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  * * Redistributions of source code must retain the above copyright
10  *   notice, this list of conditions and the following disclaimer.
11  *
12  * * Redistributions in binary form must reproduce the above copyright
13  *   notice, this list of conditions and the following disclaimer in the
14  *   documentation and/or other materials provided with the distribution.
15  *
16  * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17  *   may be used to endorse or promote products derived from this software
18  *   without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 package com.jme3.terrain.geomipmap;
33 
34 import com.jme3.bounding.BoundingBox;
35 import com.jme3.export.InputCapsule;
36 import com.jme3.export.JmeExporter;
37 import com.jme3.export.JmeImporter;
38 import com.jme3.export.OutputCapsule;
39 import com.jme3.material.Material;
40 import com.jme3.math.FastMath;
41 import com.jme3.math.Vector2f;
42 import com.jme3.math.Vector3f;
43 import com.jme3.scene.Spatial;
44 import com.jme3.scene.control.UpdateControl;
45 import com.jme3.terrain.Terrain;
46 import com.jme3.terrain.geomipmap.lodcalc.LodCalculator;
47 import com.jme3.terrain.heightmap.HeightMap;
48 import com.jme3.terrain.heightmap.HeightMapGrid;
49 import java.io.IOException;
50 import java.util.HashSet;
51 import java.util.List;
52 import java.util.Set;
53 import java.util.concurrent.Callable;
54 import java.util.logging.Level;
55 import java.util.logging.Logger;
56 
57 /**
58  * TerrainGrid itself is an actual TerrainQuad. Its four children are the visible four tiles.
59  *
60  * The grid is indexed by cells. Each cell has an integer XZ coordinate originating at 0,0.
61  * TerrainGrid will piggyback on the TerrainLodControl so it can use the camera for its
62  * updates as well. It does this in the overwritten update() method.
63  *
64  * It uses an LRU (Least Recently Used) cache of 16 terrain tiles (full TerrainQuadTrees). The
65  * center 4 are the ones that are visible. As the camera moves, it checks what camera cell it is in
66  * and will attach the now visible tiles.
67  *
68  * The 'quadIndex' variable is a 4x4 array that represents the tiles. The center
69  * four (index numbers: 5, 6, 9, 10) are what is visible. Each quadIndex value is an
70  * offset vector. The vector contains whole numbers and represents how many tiles in offset
71  * this location is from the center of the map. So for example the index 11 [Vector3f(2, 0, 1)]
72  * is located 2*terrainSize in X axis and 1*terrainSize in Z axis.
73  *
74  * As the camera moves, it tests what cameraCell it is in. Each camera cell covers four quad tiles
75  * and is half way inside each one.
76  *
77  * +-------+-------+
78  * | 1     |     4 |    Four terrainQuads that make up the grid
79  * |    *..|..*    |    with the cameraCell in the middle, covering
80  * |----|--|--|----|    all four quads.
81  * |    *..|..*    |
82  * | 2     |     3 |
83  * +-------+-------+
84  *
85  * This results in the effect of when the camera gets half way across one of the sides of a quad to
86  * an empty (non-loaded) area, it will trigger the system to load in the next tiles.
87  *
88  * The tile loading is done on a background thread, and once the tile is loaded, then it is
89  * attached to the qrid quad tree, back on the OGL thread. It will grab the terrain quad from
90  * the LRU cache if it exists. If it does not exist, it will load in the new TerrainQuad tile.
91  *
92  * The loading of new tiles triggers events for any TerrainGridListeners. The events are:
93  *  -tile Attached
94  *  -tile Detached
95  *  -grid moved.
96  *
97  * These allow physics to update, and other operation (often needed for loading the terrain) to occur
98  * at the right time.
99  *
100  * @author Anthyon
101  */
102 public class TerrainGrid extends TerrainQuad {
103 
104     protected static final Logger log = Logger.getLogger(TerrainGrid.class.getCanonicalName());
105     protected Vector3f currentCamCell = Vector3f.ZERO;
106     protected int quarterSize; // half of quadSize
107     protected int quadSize;
108     protected HeightMapGrid heightMapGrid;
109     private TerrainGridTileLoader gridTileLoader;
110     protected Vector3f[] quadIndex;
111     protected Set<TerrainGridListener> listeners = new HashSet<TerrainGridListener>();
112     protected Material material;
113     protected LRUCache<Vector3f, TerrainQuad> cache = new LRUCache<Vector3f, TerrainQuad>(16);
114     private int cellsLoaded = 0;
115     private int[] gridOffset;
116     private boolean runOnce = false;
117 
118     protected class UpdateQuadCache implements Runnable {
119 
120         protected final Vector3f location;
121 
UpdateQuadCache(Vector3f location)122         public UpdateQuadCache(Vector3f location) {
123             this.location = location;
124         }
125 
126         /**
127          * This is executed if the camera has moved into a new CameraCell and will load in
128          * the new TerrainQuad tiles to be children of this TerrainGrid parent.
129          * It will first check the LRU cache to see if the terrain tile is already there,
130          * if it is not there, it will load it in and then cache that tile.
131          * The terrain tiles get added to the quad tree back on the OGL thread using the
132          * attachQuadAt() method. It also resets any cached values in TerrainQuad (such as
133          * neighbours).
134          */
run()135         public void run() {
136             for (int i = 0; i < 4; i++) {
137                 for (int j = 0; j < 4; j++) {
138                     int quadIdx = i * 4 + j;
139                     final Vector3f quadCell = location.add(quadIndex[quadIdx]);
140                     TerrainQuad q = cache.get(quadCell);
141                     if (q == null) {
142                         if (heightMapGrid != null) {
143                             // create the new Quad since it doesn't exist
144                             HeightMap heightMapAt = heightMapGrid.getHeightMapAt(quadCell);
145                             q = new TerrainQuad(getName() + "Quad" + quadCell, patchSize, quadSize, heightMapAt == null ? null : heightMapAt.getHeightMap());
146                             q.setMaterial(material.clone());
147                             log.log(Level.FINE, "Loaded TerrainQuad {0} from HeightMapGrid", q.getName());
148                         } else if (gridTileLoader != null) {
149                             q = gridTileLoader.getTerrainQuadAt(quadCell);
150                             // only clone the material to the quad if it doesn't have a material of its own
151                             if(q.getMaterial()==null) q.setMaterial(material.clone());
152                             log.log(Level.FINE, "Loaded TerrainQuad {0} from TerrainQuadGrid", q.getName());
153                         }
154                     }
155                     cache.put(quadCell, q);
156 
157                     if (isCenter(quadIdx)) {
158                         // if it should be attached as a child right now, attach it
159                         final int quadrant = getQuadrant(quadIdx);
160                         final TerrainQuad newQuad = q;
161                         // back on the OpenGL thread:
162                         getControl(UpdateControl.class).enqueue(new Callable() {
163 
164                             public Object call() throws Exception {
165                                 attachQuadAt(newQuad, quadrant, quadCell);
166                                 //newQuad.resetCachedNeighbours();
167                                 return null;
168                             }
169                         });
170                     }
171                 }
172             }
173 
174         }
175     }
176 
isCenter(int quadIndex)177     protected boolean isCenter(int quadIndex) {
178         return quadIndex == 9 || quadIndex == 5 || quadIndex == 10 || quadIndex == 6;
179     }
180 
getQuadrant(int quadIndex)181     protected int getQuadrant(int quadIndex) {
182         if (quadIndex == 5) {
183             return 1;
184         } else if (quadIndex == 9) {
185             return 2;
186         } else if (quadIndex == 6) {
187             return 3;
188         } else if (quadIndex == 10) {
189             return 4;
190         }
191         return 0; // error
192     }
193 
TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, TerrainGridTileLoader terrainQuadGrid, Vector2f offset, float offsetAmount)194     public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, TerrainGridTileLoader terrainQuadGrid,
195             Vector2f offset, float offsetAmount) {
196         this.name = name;
197         this.patchSize = patchSize;
198         this.size = maxVisibleSize;
199         this.stepScale = scale;
200         this.offset = offset;
201         this.offsetAmount = offsetAmount;
202         initData();
203         this.gridTileLoader = terrainQuadGrid;
204         terrainQuadGrid.setPatchSize(this.patchSize);
205         terrainQuadGrid.setQuadSize(this.quadSize);
206         addControl(new UpdateControl());
207     }
208 
TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, TerrainGridTileLoader terrainQuadGrid)209     public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, TerrainGridTileLoader terrainQuadGrid) {
210         this(name, patchSize, maxVisibleSize, scale, terrainQuadGrid, new Vector2f(), 0);
211     }
212 
TerrainGrid(String name, int patchSize, int maxVisibleSize, TerrainGridTileLoader terrainQuadGrid)213     public TerrainGrid(String name, int patchSize, int maxVisibleSize, TerrainGridTileLoader terrainQuadGrid) {
214         this(name, patchSize, maxVisibleSize, Vector3f.UNIT_XYZ, terrainQuadGrid);
215     }
216 
217     @Deprecated
TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, HeightMapGrid heightMapGrid, Vector2f offset, float offsetAmount)218     public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, HeightMapGrid heightMapGrid,
219             Vector2f offset, float offsetAmount) {
220         this.name = name;
221         this.patchSize = patchSize;
222         this.size = maxVisibleSize;
223         this.stepScale = scale;
224         this.offset = offset;
225         this.offsetAmount = offsetAmount;
226         initData();
227         this.heightMapGrid = heightMapGrid;
228         heightMapGrid.setSize(this.quadSize);
229         addControl(new UpdateControl());
230     }
231 
232     @Deprecated
TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, HeightMapGrid heightMapGrid)233     public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, HeightMapGrid heightMapGrid) {
234         this(name, patchSize, maxVisibleSize, scale, heightMapGrid, new Vector2f(), 0);
235     }
236 
237     @Deprecated
TerrainGrid(String name, int patchSize, int maxVisibleSize, HeightMapGrid heightMapGrid)238     public TerrainGrid(String name, int patchSize, int maxVisibleSize, HeightMapGrid heightMapGrid) {
239         this(name, patchSize, maxVisibleSize, Vector3f.UNIT_XYZ, heightMapGrid);
240     }
241 
TerrainGrid()242     public TerrainGrid() {
243     }
244 
initData()245     private void initData() {
246         int maxVisibleSize = size;
247         this.quarterSize = maxVisibleSize >> 2;
248         this.quadSize = (maxVisibleSize + 1) >> 1;
249         this.totalSize = maxVisibleSize;
250         this.gridOffset = new int[]{0, 0};
251 
252         /*
253          *        -z
254          *         |
255          *        1|3
256          *  -x ----+---- x
257          *        2|4
258          *         |
259          *         z
260          */
261         this.quadIndex = new Vector3f[]{
262             new Vector3f(-1, 0, -1), new Vector3f(0, 0, -1), new Vector3f(1, 0, -1), new Vector3f(2, 0, -1),
263             new Vector3f(-1, 0, 0), new Vector3f(0, 0, 0), new Vector3f(1, 0, 0), new Vector3f(2, 0, 0),
264             new Vector3f(-1, 0, 1), new Vector3f(0, 0, 1), new Vector3f(1, 0, 1), new Vector3f(2, 0, 1),
265             new Vector3f(-1, 0, 2), new Vector3f(0, 0, 2), new Vector3f(1, 0, 2), new Vector3f(2, 0, 2)};
266 
267     }
268 
269     /**
270      * @deprecated not needed to be called any more, handled automatically
271      */
initialize(Vector3f location)272     public void initialize(Vector3f location) {
273         if (this.material == null) {
274             throw new RuntimeException("Material must be set prior to call of initialize");
275         }
276         Vector3f camCell = this.getCamCell(location);
277         this.updateChildren(camCell);
278         for (TerrainGridListener l : this.listeners) {
279             l.gridMoved(camCell);
280         }
281     }
282 
283     @Override
update(List<Vector3f> locations, LodCalculator lodCalculator)284     public void update(List<Vector3f> locations, LodCalculator lodCalculator) {
285         // for now, only the first camera is handled.
286         // to accept more, there are two ways:
287         // 1: every camera has an associated grid, then the location is not enough to identify which camera location has changed
288         // 2: grids are associated with locations, and no incremental update is done, we load new grids for new locations, and unload those that are not needed anymore
289         Vector3f cam = locations.isEmpty() ? Vector3f.ZERO.clone() : locations.get(0);
290         Vector3f camCell = this.getCamCell(cam); // get the grid index value of where the camera is (ie. 2,1)
291         if (cellsLoaded > 1) {                  // Check if cells are updated before updating gridoffset.
292             gridOffset[0] = Math.round(camCell.x * (size / 2));
293             gridOffset[1] = Math.round(camCell.z * (size / 2));
294             cellsLoaded = 0;
295         }
296         if (camCell.x != this.currentCamCell.x || camCell.z != currentCamCell.z || !runOnce) {
297             // if the camera has moved into a new cell, load new terrain into the visible 4 center quads
298             this.updateChildren(camCell);
299             for (TerrainGridListener l : this.listeners) {
300                 l.gridMoved(camCell);
301             }
302         }
303         runOnce = true;
304         super.update(locations, lodCalculator);
305     }
306 
getCamCell(Vector3f location)307     public Vector3f getCamCell(Vector3f location) {
308         Vector3f tile = getTileCell(location);
309         Vector3f offsetHalf = new Vector3f(-0.5f, 0, -0.5f);
310         Vector3f shifted = tile.subtract(offsetHalf);
311         return new Vector3f(FastMath.floor(shifted.x), 0, FastMath.floor(shifted.z));
312     }
313 
314     /**
315      * Centered at 0,0.
316      * Get the tile index location in integer form:
317      * @param location world coordinate
318      */
getTileCell(Vector3f location)319     public Vector3f getTileCell(Vector3f location) {
320         Vector3f tileLoc = location.divide(this.getWorldScale().mult(this.quadSize));
321         return tileLoc;
322     }
323 
getGridTileLoader()324     public TerrainGridTileLoader getGridTileLoader() {
325         return gridTileLoader;
326     }
327 
removeQuad(int idx)328     protected void removeQuad(int idx) {
329         if (this.getQuad(idx) != null) {
330             for (TerrainGridListener l : listeners) {
331                 l.tileDetached(getTileCell(this.getQuad(idx).getWorldTranslation()), this.getQuad(idx));
332             }
333             this.detachChild(this.getQuad(idx));
334             cellsLoaded++; // For gridoffset calc., maybe the run() method is a better location for this.
335         }
336     }
337 
338     /**
339      * Runs on the rendering thread
340      */
attachQuadAt(TerrainQuad q, int quadrant, Vector3f quadCell)341     protected void attachQuadAt(TerrainQuad q, int quadrant, Vector3f quadCell) {
342         this.removeQuad(quadrant);
343 
344         q.setQuadrant((short) quadrant);
345         this.attachChild(q);
346 
347         Vector3f loc = quadCell.mult(this.quadSize - 1).subtract(quarterSize, 0, quarterSize);// quadrant location handled TerrainQuad automatically now
348         q.setLocalTranslation(loc);
349 
350         for (TerrainGridListener l : listeners) {
351             l.tileAttached(quadCell, q);
352         }
353         updateModelBound();
354 
355         for (Spatial s : getChildren()) {
356             if (s instanceof TerrainQuad) {
357                 TerrainQuad tq = (TerrainQuad)s;
358                 tq.resetCachedNeighbours();
359                 tq.fixNormalEdges(new BoundingBox(tq.getWorldTranslation(), totalSize*2, Float.MAX_VALUE, totalSize*2));
360             }
361         }
362     }
363 
364     @Deprecated
365     /**
366      * @Deprecated, use updateChildren
367      */
updateChildrens(Vector3f camCell)368     protected void updateChildrens(Vector3f camCell) {
369         updateChildren(camCell);
370     }
371 
372     /**
373      * Called when the camera has moved into a new cell. We need to
374      * update what quads are in the scene now.
375      *
376      * Step 1: touch cache
377      * LRU cache is used, so elements that need to remain
378      * should be touched.
379      *
380      * Step 2: load new quads in background thread
381      * if the camera has moved into a new cell, we load in new quads
382      * @param camCell the cell the camera is in
383      */
updateChildren(Vector3f camCell)384     protected void updateChildren(Vector3f camCell) {
385 
386         int dx = 0;
387         int dy = 0;
388         if (currentCamCell != null) {
389             dx = (int) (camCell.x - currentCamCell.x);
390             dy = (int) (camCell.z - currentCamCell.z);
391         }
392 
393         int xMin = 0;
394         int xMax = 4;
395         int yMin = 0;
396         int yMax = 4;
397         if (dx == -1) { // camera moved to -X direction
398             xMax = 3;
399         } else if (dx == 1) { // camera moved to +X direction
400             xMin = 1;
401         }
402 
403         if (dy == -1) { // camera moved to -Y direction
404             yMax = 3;
405         } else if (dy == 1) { // camera moved to +Y direction
406             yMin = 1;
407         }
408 
409         // Touch the items in the cache that we are and will be interested in.
410         // We activate cells in the direction we are moving. If we didn't move
411         // either way in one of the axes (say X or Y axis) then they are all touched.
412         for (int i = yMin; i < yMax; i++) {
413             for (int j = xMin; j < xMax; j++) {
414                 cache.get(camCell.add(quadIndex[i * 4 + j]));
415             }
416         }
417         // ---------------------------------------------------
418         // ---------------------------------------------------
419 
420         if (executor == null) {
421             // use the same executor as the LODControl
422             executor = createExecutorService();
423         }
424 
425         executor.submit(new UpdateQuadCache(camCell));
426 
427         this.currentCamCell = camCell;
428     }
429 
addListener(TerrainGridListener listener)430     public void addListener(TerrainGridListener listener) {
431         this.listeners.add(listener);
432     }
433 
getCurrentCell()434     public Vector3f getCurrentCell() {
435         return this.currentCamCell;
436     }
437 
removeListener(TerrainGridListener listener)438     public void removeListener(TerrainGridListener listener) {
439         this.listeners.remove(listener);
440     }
441 
442     @Override
setMaterial(Material mat)443     public void setMaterial(Material mat) {
444         this.material = mat;
445         super.setMaterial(mat);
446     }
447 
setQuadSize(int quadSize)448     public void setQuadSize(int quadSize) {
449         this.quadSize = quadSize;
450     }
451 
452     @Override
adjustHeight(List<Vector2f> xz, List<Float> height)453     public void adjustHeight(List<Vector2f> xz, List<Float> height) {
454         Vector3f currentGridLocation = getCurrentCell().mult(getLocalScale()).multLocal(quadSize - 1);
455         for (Vector2f vect : xz) {
456             vect.x -= currentGridLocation.x;
457             vect.y -= currentGridLocation.z;
458         }
459         super.adjustHeight(xz, height);
460     }
461 
462     @Override
getHeightmapHeight(int x, int z)463     protected float getHeightmapHeight(int x, int z) {
464         return super.getHeightmapHeight(x - gridOffset[0], z - gridOffset[1]);
465     }
466 
467     @Override
getNumMajorSubdivisions()468     public int getNumMajorSubdivisions() {
469         return 2;
470     }
471 
472     @Override
getMaterial(Vector3f worldLocation)473     public Material getMaterial(Vector3f worldLocation) {
474         if (worldLocation == null)
475             return null;
476         Vector3f tileCell = getTileCell(worldLocation);
477         Terrain terrain = cache.get(tileCell);
478         if (terrain == null)
479             return null; // terrain not loaded for that cell yet!
480         return terrain.getMaterial(worldLocation);
481     }
482 
483     @Override
read(JmeImporter im)484     public void read(JmeImporter im) throws IOException {
485         super.read(im);
486         InputCapsule c = im.getCapsule(this);
487         name = c.readString("name", null);
488         size = c.readInt("size", 0);
489         patchSize = c.readInt("patchSize", 0);
490         stepScale = (Vector3f) c.readSavable("stepScale", null);
491         offset = (Vector2f) c.readSavable("offset", null);
492         offsetAmount = c.readFloat("offsetAmount", 0);
493         gridTileLoader = (TerrainGridTileLoader) c.readSavable("terrainQuadGrid", null);
494         material = (Material) c.readSavable("material", null);
495         initData();
496         if (gridTileLoader != null) {
497             gridTileLoader.setPatchSize(this.patchSize);
498             gridTileLoader.setQuadSize(this.quadSize);
499         }
500     }
501 
502     @Override
write(JmeExporter ex)503     public void write(JmeExporter ex) throws IOException {
504         super.write(ex);
505         OutputCapsule c = ex.getCapsule(this);
506         c.write(gridTileLoader, "terrainQuadGrid", null);
507         c.write(size, "size", 0);
508         c.write(patchSize, "patchSize", 0);
509         c.write(stepScale, "stepScale", null);
510         c.write(offset, "offset", null);
511         c.write(offsetAmount, "offsetAmount", 0);
512         c.write(material, "material", null);
513     }
514 }
515