/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.renderer3d;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.util.LoggingSystem;
import org.geotools.map.MapContext;
import org.geotools.renderer3d.navigationgestures.NavigationGesture;
import org.geotools.renderer3d.provider.texture.MapTextureRenderer;
import org.geotools.renderer3d.provider.texture.impl.TextureProvider;
import org.geotools.renderer3d.provider.texture.impl.TextureProviderImpl;
import org.geotools.renderer3d.terrainblock.TerrainBlock;
import org.geotools.renderer3d.terrainblock.TerrainBlockFactory;
import org.geotools.renderer3d.utils.ParameterChecker;
import org.geotools.renderer3d.utils.canvas3d.Canvas3D;
import org.geotools.renderer3d.utils.canvas3d.FrameListener;
import org.geotools.renderer3d.utils.quadtree.QuadTree;
import org.geotools.renderer3d.utils.quadtree.QuadTreeImpl;
import org.geotools.renderer3d.utils.quadtree.QuadTreeListener;
import org.geotools.renderer3d.utils.quadtree.QuadTreeNode;
import java.awt.Color;
import java.awt.Component;
import java.util.logging.Level;
/**
* TODO: Keeps track of all the terrain blocks, calculate the size/distance ratio for them when the camera moves.
* The size is the length of a side of a terrain block in meters.
* The distance is the distance from the camera to its center point (on the ground elevation surface) in meters.
* (NOTE: All values and thresholds can be stored as squares to avoid a square root computation)
* <ul>
* <li>If some expanded terrain block is too far away and too small (size/distance under some threshold), collapse it
* <li>If some collapsed terrain block is too close and too big (size/distance over some threshold), and it is above the minimum terrain block size, expand it
* <li>If the size/distance ratio for the root node is under some threshold, create a parent, in the direction of the camera
* </ul>
* <p/>
* TODO: Add expand, collapse, and extendRoot methods to the terrain blocks, and a keepExpanded flag to the QuadTreeNode.
* TODO: Maybe add a generic data object that can be associated with quad tree nodes also, and a way to visit those for
* a quad tree node and all its sub nodes. That would allow the camera checks. There could also be a clear subtree
* method that removes the data objects from a subtree.
*
* @author Hans H�ggstr�m
*/
public final class Renderer3DImpl
implements Renderer3D
{
//======================================================================
// Private Fields
private final Canvas3D myCanvas3D = new Canvas3D();
private final TerrainBlockFactory myTerrainBlockFactory;
private final int myTextureSize;
private final double myVisibilityDistance;
private final Vector3f myPreviousCameraPosition = new Vector3f();
private MapContext myMapContext = null;
private QuadTree<TerrainBlock> myQuadTree;
private Node myTerrainNode = null;
private Spatial myRootSpatial;
//======================================================================
// Private Constants
private static final int DEFAULT_TERRAIN_BLOCK_SIZE_IN_GRIDS = 32;
private static final int DEFAULT_TEXTURE_SIZE = 128;
private static final double DEFAULT_VISIBILITY_DISTANCE = 100000.0;
private static final double MINIMUM_NODE_SIZE_M = 50;
private static final double EXPANSION_THRESHOLD = 2.0;
private static final double COLLAPSING_THRESHOLD = 1.5;
//======================================================================
// Public Methods
//----------------------------------------------------------------------
// Constructors
/**
* Creates a new Renderer3D with 1 km default size for the initial terrain blocks.
*/
public Renderer3DImpl()
{
this( DEFAULT_VISIBILITY_DISTANCE );
}
/**
* Creates a new Renderer3D.
*
* @param visibilityDistance_m distance that should be visible, in meters.
*/
public Renderer3DImpl( final double visibilityDistance_m )
{
this( null, visibilityDistance_m );
}
/**
* Creates a new Renderer3D with 1 km default size for the initial terrain blocks.
*
* @param mapContextToRender the map context that is used to get the layers to render in the 3D view.
*/
public Renderer3DImpl( final MapContext mapContextToRender )
{
this( mapContextToRender, DEFAULT_VISIBILITY_DISTANCE );
}
/**
* Creates a new Renderer3D.
*
* @param mapContextToRender the map context that is used to get the layers to render in the 3D view.
* @param visibilityDistance_m distance that should be visible, in meters.
*/
public Renderer3DImpl( final MapContext mapContextToRender,
final double visibilityDistance_m )
{
this( mapContextToRender,
new MapTextureRenderer( mapContextToRender, Color.WHITE ),
DEFAULT_TERRAIN_BLOCK_SIZE_IN_GRIDS, DEFAULT_TEXTURE_SIZE, visibilityDistance_m );
}
/**
* Creates a new Renderer3D.
*
* @param mapContextToRender the map context that is used to get the layers to render in the 3D view.
* @param textureRenderer the renderer that renders chunks of the ground texture on request.
* @param terrainBlockSizeInGrids number of grid cells along the side of a TerrainBlock.
* @param textureSize the size to use for the texture for each terrain block, per side, in pixels.
* @param visibilityDistance_m distance that there should be terrain visible in each direction from the camera,
* in display units (TODO: CHECK: meters?).
*/
public Renderer3DImpl( final MapContext mapContextToRender,
final MapTextureRenderer textureRenderer,
final int terrainBlockSizeInGrids,
final int textureSize, final double visibilityDistance_m )
{
ParameterChecker.checkNotNull( mapContextToRender, "mapContextToRender" );
ParameterChecker.checkPositiveNonZeroInteger( terrainBlockSizeInGrids, "terrainBlockSizeInGrids" );
ParameterChecker.checkPositiveNonZeroInteger( textureSize, "textureSize" );
ParameterChecker.checkPositiveNonZeroNormalNumber( visibilityDistance_m, "visibilityDistance_m" );
myTextureSize = textureSize;
myVisibilityDistance = visibilityDistance_m;
myCanvas3D.setViewDistance( (float) myVisibilityDistance );
final TextureProvider mapTextureProvider = new TextureProviderImpl( textureRenderer,
textureSize,
Color.WHITE );
/*
// DEBUG
final TextureProvider mapTextureProvider = new TextureProviderImpl( new DummyTextureRenderer(),
textureSize,
Color.GRAY );
*/
myTerrainBlockFactory = new TerrainBlockFactory( terrainBlockSizeInGrids,
mapTextureProvider,
myTextureSize );
setMapContext( mapContextToRender );
myQuadTree.addQuadTreeListener( new QuadTreeListener<TerrainBlock>()
{
public void onRootChanged( final QuadTreeNode<TerrainBlock> newRoot )
{
if ( myTerrainNode != null )
{
update3DModel();
}
}
} );
// Filter out internal trace info and debug data from JME (for some reason they use INFO level for that)
LoggingSystem.getLoggingSystem().setLevel( Level.WARNING );
initExpansionAndCollapsionHandler();
}
//----------------------------------------------------------------------
// Renderer3D Implementation
public MapContext getMapContext()
{
return myMapContext;
}
public void setMapContext( final MapContext mapContext )
{
if ( myMapContext != mapContext )
{
myMapContext = mapContext;
// Clear the old quadtree and start building a new one, with the data from the new context.
myQuadTree = new QuadTreeImpl<TerrainBlock>( myVisibilityDistance / 1000, myTerrainBlockFactory );
}
}
public Component get3DView()
{
initializeTerrainNodeIfNeeded();
return myCanvas3D.get3DView();
}
public Spatial get3DNode()
{
initializeTerrainNodeIfNeeded();
return myTerrainNode;
}
public void addNavigationGesture( final NavigationGesture addedNavigationGesture )
{
myCanvas3D.addNavigationGesture( addedNavigationGesture );
}
public void removeNavigationGesture( final NavigationGesture removedNavigationGesture )
{
myCanvas3D.removeNavigationGesture( removedNavigationGesture );
}
public void removeAllNavigationGestures()
{
myCanvas3D.removeAllNavigationGestures();
}
public void addFrameListener( final FrameListener addedFrameListener )
{
myCanvas3D.addFrameListener( addedFrameListener );
}
public boolean removeFrameListener( final FrameListener removedFrameListener )
{
return myCanvas3D.removeFrameListener( removedFrameListener );
}
//======================================================================
// Private Methods
private void initExpansionAndCollapsionHandler()
{
addFrameListener( new FrameListener()
{
public void onFrame( final double secondsSinceLastFrame )
{
final Camera camera = myCanvas3D.getCamera();
if ( camera != null )
{
final Vector3f currentCameraPosition = camera.getLocation();
if ( !currentCameraPosition.equals( myPreviousCameraPosition ) )
{
// Grow quad tree if necessary
myQuadTree.getRootNode().growToInclude( currentCameraPosition.x,
currentCameraPosition.y,
myVisibilityDistance );
// Go through quad tree nodes, recalculate their size/distance ratios,
// and expand or collapse them as needed.
checkNode( myQuadTree.getRootNode(), currentCameraPosition );
myPreviousCameraPosition.set( currentCameraPosition );
}
}
}
} );
}
private void checkNode( QuadTreeNode<TerrainBlock> node, final Vector3f currentCameraPosition )
{
final TerrainBlock terrainBlock = node.getNodeData();
if ( terrainBlock != null )
{
// Calculate size/distance
final double size = node.getBounds().getSizeAveraged();
final double squaredSize = size * size;
final double squaredDistance = terrainBlock.getCenter().distanceSquared( currentCameraPosition );
final double comparsionFactor = squaredSize / squaredDistance;
// Expand if needed, and if the node is larger than the minimum node size
if ( comparsionFactor > EXPANSION_THRESHOLD && size > MINIMUM_NODE_SIZE_M )
{
node.setExpanded( true );
}
// Collapse if needed
if ( comparsionFactor < COLLAPSING_THRESHOLD )
{
node.setExpanded( false );
}
// Recursively call children, if it has them
final int num = node.getNumberOfChildren();
for ( int i = 0; i < num; i++ )
{
final QuadTreeNode<TerrainBlock> child = node.getChild( i );
if ( child != null )
{
checkNode( child, currentCameraPosition );
}
}
}
}
private void initializeTerrainNodeIfNeeded()
{
if ( myTerrainNode == null )
{
myTerrainNode = new Node();
update3DModel();
myCanvas3D.set3DNode( myTerrainNode );
}
}
private void update3DModel()
{
if ( myRootSpatial != null )
{
myTerrainNode.detachChild( myRootSpatial );
}
myRootSpatial = myQuadTree.getRootNode().getNodeData().getSpatial();
if ( myRootSpatial != null )
{
myTerrainNode.attachChild( myRootSpatial );
}
}
}