Home | History | Annotate | Download | only in geomipmap
      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 
    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          */
    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 
    177     protected boolean isCenter(int quadIndex) {
    178         return quadIndex == 9 || quadIndex == 5 || quadIndex == 10 || quadIndex == 6;
    179     }
    180 
    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 
    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 
    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 
    213     public TerrainGrid(String name, int patchSize, int maxVisibleSize, TerrainGridTileLoader terrainQuadGrid) {
    214         this(name, patchSize, maxVisibleSize, Vector3f.UNIT_XYZ, terrainQuadGrid);
    215     }
    216 
    217     @Deprecated
    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
    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
    238     public TerrainGrid(String name, int patchSize, int maxVisibleSize, HeightMapGrid heightMapGrid) {
    239         this(name, patchSize, maxVisibleSize, Vector3f.UNIT_XYZ, heightMapGrid);
    240     }
    241 
    242     public TerrainGrid() {
    243     }
    244 
    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      */
    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
    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 
    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      */
    319     public Vector3f getTileCell(Vector3f location) {
    320         Vector3f tileLoc = location.divide(this.getWorldScale().mult(this.quadSize));
    321         return tileLoc;
    322     }
    323 
    324     public TerrainGridTileLoader getGridTileLoader() {
    325         return gridTileLoader;
    326     }
    327 
    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      */
    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      */
    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      */
    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 
    430     public void addListener(TerrainGridListener listener) {
    431         this.listeners.add(listener);
    432     }
    433 
    434     public Vector3f getCurrentCell() {
    435         return this.currentCamCell;
    436     }
    437 
    438     public void removeListener(TerrainGridListener listener) {
    439         this.listeners.remove(listener);
    440     }
    441 
    442     @Override
    443     public void setMaterial(Material mat) {
    444         this.material = mat;
    445         super.setMaterial(mat);
    446     }
    447 
    448     public void setQuadSize(int quadSize) {
    449         this.quadSize = quadSize;
    450     }
    451 
    452     @Override
    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
    463     protected float getHeightmapHeight(int x, int z) {
    464         return super.getHeightmapHeight(x - gridOffset[0], z - gridOffset[1]);
    465     }
    466 
    467     @Override
    468     public int getNumMajorSubdivisions() {
    469         return 2;
    470     }
    471 
    472     @Override
    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
    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
    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