/*
* 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.terrainblock;
import com.jme.bounding.BoundingBox;
import com.jme.image.Image;
import com.jme.image.Texture;
import com.jme.math.Vector2f;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.TriMesh;
import com.jme.scene.state.TextureState;
import com.jme.system.DisplaySystem;
import com.jme.util.GameTaskQueue;
import com.jme.util.GameTaskQueueManager;
import com.jme.util.TextureKey;
import com.jme.util.TextureManager;
import com.jme.util.geom.BufferUtils;
import com.jmex.awt.swingui.ImageGraphics;
import org.geotools.renderer3d.utils.*;
import java.awt.image.BufferedImage;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.concurrent.Callable;
/**
* A rectangular terrain elevation mesh.
*
* @author Hans H�ggstr�m
*/
public final class TerrainMesh
extends TriMesh
{
//======================================================================
// Private Fields
private static int theTerrainMeshCounter = 0;
private final double myZ;
private final double mySkirtSize;
private final FloatBuffer myVertexes;
private final FloatBuffer myColors;
private final FloatBuffer myTextureCoordinates;
private final FloatBuffer myNormals;
private final IntBuffer myIndices;
private final int myNumberOfVertices;
private final int myNumberOfCells;
private final int myNumberOfIndices;
private final int mySizeY_cells;
private final int mySizeX_cells;
private final int mySizeY_vertices;
private final int mySizeX_vertices;
private double myX1;
private double myY1;
private double myX2;
private double myY2;
private TextureState myTextureState;
private Texture myTexture;
private ImageGraphics myTextureGraphics;
private TextureState myPlaceholderTextureState = null;
private final Object myTextureStateLock = new Object();
private boolean myPlaceholderTextureInUse = false;
//======================================================================
// Private Constants
private static final double SKIRT_SIZE_FACTOR = 0.1;
private static final BufferedImage PLACEHOLDER_PICTURE = ImageUtils.createPlaceholderPicture( 128, 128 );
private static final float DEFAULT_ANISO_LEVEL = 1.0f;
private static final int DEFAULT_TEXTURE_IMAGE_FORMAT = com.jme.image.Image.GUESS_FORMAT_NO_S3TC;
private static final BoundingRectangleImpl WHOLE_TEXTURE_AREA = new BoundingRectangleImpl( 0, 0, 1, 1 );
private static final Texture PLACEHOLDER_TEXTURE = TextureManager.loadTexture( PLACEHOLDER_PICTURE,
Texture.MM_LINEAR_LINEAR,
Texture.FM_LINEAR,
1,
Image.GUESS_FORMAT_NO_S3TC,
false );
//======================================================================
// Public Methods
//----------------------------------------------------------------------
// Constructors
/**
* @param sizeXCells Number of grid cells along the X side.
* @param sizeYCells Number of grid cells along the Y side.
* @param x1 first world coordinate.
* @param y1 first world coordinate.
* @param x2 second world coordinate. Should be larger than the first.
* @param y2 second world coordinate. Should be larger than the first.
* @param z the default height level.
*/
public TerrainMesh( final int sizeXCells,
final int sizeYCells,
final double x1,
final double y1,
final double x2,
final double y2,
final double z )
{
// JME seems to need an unique identifier for each node. NOTE: Not thread safe.
super( "TerrainMesh_" + theTerrainMeshCounter++ );
// Check parameters
ParameterChecker.checkPositiveNonZeroInteger( sizeXCells, "sizeXCells" );
ParameterChecker.checkPositiveNonZeroInteger( sizeYCells, "sizeYCells" );
ParameterChecker.checkNormalNumber( z, "z" );
// Assign fields from parameters
// Cells are the rectangular areas between four normal surface vertices. Does not include the rectangles in the downturned skirt.
// Vertices are the vertex points making up the grid corners of the mesh. Also includes the vertices used to make the downturned skirt.
mySizeX_cells = sizeXCells;
mySizeY_cells = sizeYCells;
mySizeX_vertices = mySizeX_cells + 1 + 2;
mySizeY_vertices = mySizeY_cells + 1 + 2;
myZ = z;
mySkirtSize = calculateSkirtSize();
// Calculate sizes
myNumberOfVertices = mySizeX_vertices * mySizeY_vertices;
myNumberOfCells = mySizeX_cells * mySizeY_cells;
myNumberOfIndices = ( mySizeX_vertices - 1 ) * ( mySizeY_vertices - 1 ) * 6;
// Create databuffers
myVertexes = BufferUtils.createVector3Buffer( myNumberOfVertices );
myColors = BufferUtils.createColorBuffer( myNumberOfVertices );
myTextureCoordinates = BufferUtils.createVector2Buffer( myNumberOfVertices );
myNormals = BufferUtils.createVector3Buffer( myNumberOfVertices );
myIndices = BufferUtils.createIntBuffer( myNumberOfIndices );
// Stich together the vertices into triangles
initializeIndices();
updateBounds( x1, y1, x2, y2 );
}
//----------------------------------------------------------------------
// Other Public Methods
/**
* Updates the positon and covered area of the terrain mesh.
* <p/>
* Called from the constructor, as well as when a TerrainMesh is re-used.
*
* @param x1 first world coordinate.
* @param y1 first world coordinate.
* @param x2 second world coordinate. Should be larger than the first.
* @param y2 second world coordinate. Should be larger than the first.
*/
public void updateBounds( final double x1,
final double y1,
final double x2,
final double y2 )
{
ParameterChecker.checkNormalNumber( x1, "x1" );
ParameterChecker.checkNormalNumber( y1, "y1" );
ParameterChecker.checkNormalNumber( x2, "x2" );
ParameterChecker.checkNormalNumber( y2, "y2" );
ParameterChecker.checkLargerThan( x2, "x2", x1, "x1" );
ParameterChecker.checkLargerThan( y2, "y2", y1, "y1" );
myX1 = x1;
myY1 = y1;
myX2 = x2;
myY2 = y2;
// Put vertices in a grid formation in the correct place in the world
initializeVetices();
// Initialize the TriMesh
setVertexBuffer( 0, myVertexes );
setColorBuffer( 0, myColors );
setTextureBuffer( 0, myTextureCoordinates );
setNormalBuffer( 0, myNormals );
setIndexBuffer( 0, myIndices );
// Update bounding box
setModelBound( new BoundingBox() );
updateModelBound();
}
/**
* Creates a texture from the specified image and applies it to this Terrainmesh.
*
* @param textureImage the image to create a texture from. If null, a placeholder texture is created.
*/
public void setTextureImage( final BufferedImage textureImage )
{
GameTaskQueueManager.getManager().getQueue( GameTaskQueue.UPDATE ).enqueue( new Callable<Object>()
{
public Object call() throws Exception
{
final Renderer renderer = DisplaySystem.getDisplaySystem().getRenderer();
initTexture( textureImage, renderer );
return null;
}
} );
}
/**
* @return the texture that this TerrainMesh is currently using, or null if it is using a placeholder texture.
*/
public Texture getTexture()
{
return myTexture;
}
public void setPlaceholderTexture( final Texture texture, final BoundingRectangle textureArea )
{
synchronized ( myTextureStateLock )
{
myPlaceholderTextureInUse = true;
if ( myTextureState != null )
{
// Update texture indexes
setTextureCoordinates( textureArea );
// Update the geometry
updateGeometricState( 0, true );
// Use placeholder if no texture specified
Texture textureToUse = texture;
if ( textureToUse == null )
{
textureToUse = PLACEHOLDER_TEXTURE;
}
// Set the texture
myTextureState.setTexture( textureToUse );
updateRenderState();
}
}
}
public boolean isPlaceholderTextureInUse()
{
return myPlaceholderTextureInUse;
}
//======================================================================
// Private Methods
private double calculateSkirtSize()
{
final double cellSizeX = ( myX2 - myX1 ) / mySizeX_cells;
final double cellSizeY = ( myY2 - myY1 ) / mySizeY_cells;
return Math.max( cellSizeX, cellSizeY ) * SKIRT_SIZE_FACTOR;
}
private void initializeVetices()
{
for ( int y = 0; y < mySizeY_vertices; y++ )
{
for ( int x = 0; x < mySizeX_vertices; x++ )
{
initializeVertex( x, y );
}
}
}
private void initializeVertex( final int x, final int y )
{
final int index = calculateMeshIndex( x, y );
// Calculate position
final float xPos = (float) MathUtils.interpolateClamp( x, 1, mySizeX_vertices - 2, myX1, myX2 );
final float yPos = (float) MathUtils.interpolateClamp( y, 1, mySizeY_vertices - 2, myY1, myY2 );
float zPos = (float) myZ;
// Fold the edges down to form a skirt, to avoid one pixel cracks between terrain blocks caused by rounding errors.
if ( isEdge( x, y ) )
{
zPos -= mySkirtSize;
}
// Stretch the texture across the terrain mesh, and have downturned edges have the same color as the edge
final float textureXPos = (float) MathUtils.interpolateClamp( x, 1, mySizeX_vertices - 2, 0, 1 );
final float textureYPos = (float) MathUtils.interpolateClamp( y, 1, mySizeY_vertices - 2, 1, 0 );
final Vector3f position = new Vector3f( xPos, yPos, zPos );
final Vector3f normal = new Vector3f( 0, 0, 1 );
final ColorRGBA color = new ColorRGBA( 1.0f, 1.0f, 1.0f, 1.0f );
final Vector2f textureCoordinate = new Vector2f( textureXPos, textureYPos );
BufferUtils.setInBuffer( position, myVertexes, index );
BufferUtils.setInBuffer( normal, myNormals, index );
BufferUtils.setInBuffer( color, myColors, index );
BufferUtils.setInBuffer( textureCoordinate, myTextureCoordinates, index );
}
private boolean isEdge( final int x, final int y )
{
return x == 0 ||
y == 0 ||
x == mySizeX_vertices - 1 ||
y == mySizeY_vertices - 1;
}
private int calculateMeshIndex( final int x, final int y )
{
return x + y * mySizeX_vertices;
}
private void initializeIndices()
{
// OPTIMIZE: Use triangle strips or fans to get more efficient results!
// Create indices indicating the connections
int index = 0;
for ( int y = 0; y < mySizeY_vertices - 1; y++ )
{
for ( int x = 0; x < mySizeX_vertices - 1; x++ )
{
final int topLeft = x + y * mySizeX_vertices;
final int topRight = ( x + 1 ) + y * mySizeX_vertices;
final int bottomLeft = x + ( y + 1 ) * mySizeX_vertices;
final int bottomRight = ( x + 1 ) + ( y + 1 ) * mySizeX_vertices;
myIndices.put( index++, topLeft );
myIndices.put( index++, bottomRight );
myIndices.put( index++, topRight );
myIndices.put( index++, bottomRight );
myIndices.put( index++, topLeft );
myIndices.put( index++, bottomLeft );
}
}
}
private void initTexture( BufferedImage textureImage, final Renderer renderer )
{
synchronized ( myTextureStateLock )
{
// The texture can be null e.g. if we ran out of memory
if ( textureImage == null )
{
textureImage = PLACEHOLDER_PICTURE;
}
// Remove any placeholder render state
if ( myPlaceholderTextureInUse )
{
/*
clearRenderState( RenderState.RS_TEXTURE );
*/
/*
myPlaceholderTextureState.setEnabled( false );
myPlaceholderTextureState.setTexture( null );
myPlaceholderTextureState = null;
*/
setTextureCoordinates( WHOLE_TEXTURE_AREA );
/*
*/
}
if ( myTextureGraphics == null )
{
// First time initializations:
// Create JME Image Renderer
myTextureGraphics = ImageGraphics.createInstance( textureImage.getWidth( null ),
textureImage.getHeight( null ),
0 );
myTextureGraphics.drawImage( textureImage, 0, 0, null );
myTextureGraphics.update();
// Create texture
myTexture = TextureManager.loadTexture( null,
createTextureKey( textureImage.hashCode() ),
myTextureGraphics.getImage() );
// Make sure this texture is not cached, as we will be updating it when the TerrainMesh is re-used
TextureManager.releaseTexture( myTexture );
// Clamp texture at edges (no wrapping)
myTexture.setWrap( Texture.WM_ECLAMP_S_ECLAMP_T );
myTexture.setMipmapState( Texture.MM_LINEAR_LINEAR );
createTextureRenderState( renderer, myTexture );
if ( myPlaceholderTextureInUse )
{
myTextureState.setTexture( myTexture, 0 );
}
}
else
{
// Release the previously reserved textures, so that they don't take up space on the 3D card
// NOTE: Maybe this also forces JME to re-upload the changed texture?
if ( !myPlaceholderTextureInUse )
{
myTextureState.deleteAll( true );
}
else
{
myTextureState.setTexture( myTexture, 0 );
myTextureState.deleteAll( true );
}
// Update the JME Image used by the texture
myTextureGraphics.drawImage( textureImage, 0, 0, null );
myTextureGraphics.update();
myTextureGraphics.update( myTexture );
// Make sure this texture is not cached, as we will be updating it when the TerrainMesh is re-used
TextureManager.releaseTexture( myTexture );
// Smoother look at low viewing angles
myTexture.setMipmapState( Texture.MM_LINEAR_LINEAR );
}
myPlaceholderTextureInUse = false;
}
}
private void createTextureRenderState( final Renderer renderer, final Texture texture )
{
myTextureState = renderer.createTextureState();
myTextureState.setEnabled( true );
myTextureState.setTexture( texture, 0 );
setRenderState( myTextureState );
updateRenderState();
}
private TextureKey createTextureKey( final int imageHashcode )
{
final TextureKey tkey = new TextureKey( null, Texture.MM_LINEAR, Texture.FM_LINEAR,
DEFAULT_ANISO_LEVEL, false, DEFAULT_TEXTURE_IMAGE_FORMAT );
tkey.setFileType( "" + imageHashcode );
return tkey;
}
private void setTextureCoordinates( BoundingRectangle boundingRectangle )
{
if ( boundingRectangle == null )
{
boundingRectangle = WHOLE_TEXTURE_AREA;
}
for ( int y = 0; y < mySizeY_vertices; y++ )
{
for ( int x = 0; x < mySizeX_vertices; x++ )
{
final int index = calculateMeshIndex( x, y );
final float textureXPos = (float) MathUtils.interpolateClamp( x,
1,
mySizeX_vertices - 2,
boundingRectangle.getX1(),
boundingRectangle.getX2() );
final float textureYPos = (float) MathUtils.interpolateClamp( y,
1,
mySizeY_vertices - 2,
boundingRectangle.getY2(),
boundingRectangle.getY1() );
final Vector2f textureCoordinate = new Vector2f( textureXPos, textureYPos );
BufferUtils.setInBuffer( textureCoordinate, myTextureCoordinates, index );
}
}
setTextureBuffer( 0, myTextureCoordinates );
}
}