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 */
32package com.jme3.terrain.geomipmap;
33
34import com.jme3.bounding.BoundingBox;
35import com.jme3.export.InputCapsule;
36import com.jme3.export.JmeExporter;
37import com.jme3.export.JmeImporter;
38import com.jme3.export.OutputCapsule;
39import com.jme3.material.Material;
40import com.jme3.math.FastMath;
41import com.jme3.math.Vector2f;
42import com.jme3.math.Vector3f;
43import com.jme3.scene.Spatial;
44import com.jme3.scene.control.UpdateControl;
45import com.jme3.terrain.Terrain;
46import com.jme3.terrain.geomipmap.lodcalc.LodCalculator;
47import com.jme3.terrain.heightmap.HeightMap;
48import com.jme3.terrain.heightmap.HeightMapGrid;
49import java.io.IOException;
50import java.util.HashSet;
51import java.util.List;
52import java.util.Set;
53import java.util.concurrent.Callable;
54import java.util.logging.Level;
55import 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 */
102public 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