/* * 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.utils.canvas3d; import com.jme.renderer.Camera; import com.jme.renderer.Renderer; import com.jme.scene.Spatial; import com.jme.scene.state.CullState; import com.jme.system.DisplaySystem; import com.jmex.awt.JMECanvas; import com.jmex.awt.SimpleCanvasImpl; import org.geotools.renderer3d.navigationgestures.*; import org.geotools.renderer3d.utils.CursorChangerImpl; import org.geotools.renderer3d.utils.FpsCounter; import org.geotools.renderer3d.utils.ParameterChecker; import javax.swing.*; import java.awt.Canvas; import java.awt.Component; import java.awt.Dimension; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.util.HashSet; import java.util.Set; /** * A 3D Canvas, showing a 3D object in an AWT Canvas component. * <p/> * Allows registering Gestures, that can be used to navigate the 3D view (already has default gestures registered). * <p/> * Also allows adding FrameListeners, that are called after each rendering frame in the swing thread. * IDEA: Also call them (a different method) in the opengl thread, before rendering. * * @author Hans H�ggstr�m */ public final class Canvas3D { //====================================================================== // Private Fields private final Set<NavigationGesture> myNavigationGestures = new HashSet<NavigationGesture>(); private final CursorChangerImpl myCursorChanger = new CursorChangerImpl(); private final FpsCounter myFpsCounter = new FpsCounter(); private final Set<FrameListener> myFrameListeners = new HashSet<FrameListener>(); private final CameraAccessor myCameraAccessor = new CameraAccessor() { public Camera getCamera() { return Canvas3D.this.getCamera(); } }; private Spatial my3DNode = null; private Component myView3D = null; private Canvas myCanvas = null; private MyCanvasRenderer myCanvasRenderer = null; private float myViewDistance; //====================================================================== // Private Constants private static final int DEFAULT_WIDTH = 800; private static final int DEFAULT_HEIGHT = 600; private static final int CANVAS_REPAINT_INTERVAL_MS = 10; private static final int DEFAULT_VIEW_DISTANCE = 100000; //====================================================================== // Public Methods //---------------------------------------------------------------------- // Constructors /** * Creates a new empty 3D canvas. * <p/> * Use set3DNode to set a 3D object to show. */ public Canvas3D() { this( null ); } /** * Creates a new 3D canvas, showing the specified 3D node. * * @param a3dNode the 3D node to show on this canvas, or null not to show any node. */ public Canvas3D( final Spatial a3dNode ) { this( a3dNode, DEFAULT_VIEW_DISTANCE ); } public Canvas3D( final Spatial a3DNode, final float viewDistance ) { my3DNode = a3DNode; setViewDistance( viewDistance ); // Add default navigation gestures addNavigationGesture( new PanGesture() ); addNavigationGesture( new RotateGesture() ); addNavigationGesture( new MoveGesture() ); } //---------------------------------------------------------------------- // Other Public Methods /** * TODO: CHECK: Are the units meters? * * @return distance in screen units to the far clipping plane - 3D geometry beyond this distance is not shown. */ public float getViewDistance() { return myViewDistance; } /** * TODO: CHECK: Are the units meters? * * @param viewDistance distance in screen units to the far clipping plane - 3D geometry beyond this distance is not shown. */ public void setViewDistance( final float viewDistance ) { ParameterChecker.checkPositiveNonZeroNormalNumber( viewDistance, "viewDistance" ); myViewDistance = viewDistance; } /** * @return the 3D node currently shown, or null if none shown. */ public Spatial get3DNode() { return my3DNode; } /** * @return the camera associated wit this 3D canvas, if it has been created, otherwise null. */ public Camera getCamera() { if ( myCanvasRenderer != null ) { return myCanvasRenderer.getCamera(); } else { return null; } } /** * Adds the specified FrameListener. The listener is called after each frame is rendered in the swing thread. * * @param addedFrameListener should not be null or already added. */ public void addFrameListener( FrameListener addedFrameListener ) { ParameterChecker.checkNotNull( addedFrameListener, "addedFrameListener" ); ParameterChecker.checkNotAlreadyContained( addedFrameListener, myFrameListeners, "myFrameListeners" ); myFrameListeners.add( addedFrameListener ); } /** * Removes the specified FrameListener. * * @param removedFrameListener should not be null. * * @return true if the listener was found and removed, false if it was not found. */ public boolean removeFrameListener( FrameListener removedFrameListener ) { ParameterChecker.checkNotNull( removedFrameListener, "removedFrameListener" ); return myFrameListeners.remove( removedFrameListener ); } /** * @return the number of frames rendered per second, * or a negative value if the canvas has not yet been rendered. */ public double getFramesPerSecond() { return myCanvasRenderer.getFramesPerSecond(); } /** * @return number of seconds between the previous frame and the frame before that, * or a negative value if the canvas has not yet been rendered. */ public double getSecondsBetweenFrames() { return myCanvasRenderer.getSecondsBetweenFrames(); } /** * @param a3dNode the 3d node to show in this 3D canvas. */ public void set3DNode( final Spatial a3dNode ) { my3DNode = a3dNode; if ( myCanvasRenderer != null ) { myCanvasRenderer.setCanvasRootNode( a3dNode ); } } /** * @return an AWT component containing a view of the 3D node. */ public Component get3DView() { if ( myView3D == null ) { myView3D = createView3D(); } return myView3D; } /** * @param addedNavigationGesture some gesture that can be used to control the camera. */ public void addNavigationGesture( NavigationGesture addedNavigationGesture ) { ParameterChecker.checkNotNull( addedNavigationGesture, "addedNavigationGesture" ); ParameterChecker.checkNotAlreadyContained( addedNavigationGesture, myNavigationGestures, "myNavigationGestures" ); myNavigationGestures.add( addedNavigationGesture ); registerNavigationGestureListener( addedNavigationGesture ); } /** * @param removedNavigationGesture gesture to remove. */ public void removeNavigationGesture( NavigationGesture removedNavigationGesture ) { ParameterChecker.checkNotNull( removedNavigationGesture, "removedNavigationGesture" ); ParameterChecker.checkContained( removedNavigationGesture, myNavigationGestures, "myNavigationGestures" ); myNavigationGestures.remove( removedNavigationGesture ); unRegisterNavigationGestureListener( removedNavigationGesture ); } /** * Removes all registered navigation gestures, including the builtin ones. */ public void removeAllNavigationGestures() { myNavigationGestures.clear(); for ( NavigationGesture navigationGesture : myNavigationGestures ) { unRegisterNavigationGestureListener( navigationGesture ); } } //====================================================================== // Private Methods private void registerNavigationGestureListener( final NavigationGesture navigationGesture ) { if ( myCanvas != null ) { navigationGesture.init( myCanvas, myCursorChanger, myCameraAccessor ); } } private void unRegisterNavigationGestureListener( final NavigationGesture navigationGesture ) { if ( myCanvas != null ) { navigationGesture.deInit(); } } private Component createView3D() { final int width = DEFAULT_WIDTH; final int height = DEFAULT_HEIGHT; // Create the 3D canvas myCanvas = DisplaySystem.getDisplaySystem( "lwjgl" ).createCanvas( width, height ); myCanvas.setMinimumSize( new Dimension( 0, 0 ) ); // Make sure it is shrinkable myCursorChanger.setComponent( myCanvas ); final JMECanvas jmeCanvas = ( (JMECanvas) myCanvas ); // Set the renderer that renders the canvas contents myCanvasRenderer = new MyCanvasRenderer( width, height, my3DNode, myCanvas ); jmeCanvas.setImplementor( myCanvasRenderer ); // Add navigation gesture listeners to the created 3D canvas for ( NavigationGesture navigationGesture : myNavigationGestures ) { registerNavigationGestureListener( navigationGesture ); } // We need to repaint the component to see the updates, so we create a repaint calling thread final Thread repaintThread = new Thread( new MyRepainter( myCanvas ) ); repaintThread.setDaemon( true ); // Do not keep the JVM alive if only the repaint thread is left running repaintThread.start(); return myCanvas; } //====================================================================== // Inner Classes /** * A thread for repainting a swing canvas regularily to make it update the 3D view. */ private static final class MyRepainter implements Runnable { //====================================================================== // Private Fields private final Canvas myCanvas; //====================================================================== // Public Methods //---------------------------------------------------------------------- // Constructors public MyRepainter( final Canvas canvas ) { myCanvas = canvas; } //---------------------------------------------------------------------- // Runnable Implementation public void run() { while ( true ) { myCanvas.repaint(); // TODO: Instead of sleeping a fixed amount, we could try to sleep some amount to maintain some maximum FPS. try { Thread.sleep( CANVAS_REPAINT_INTERVAL_MS ); } catch ( InterruptedException e ) { // Ignore } } } } /** * A renderer that renders a 3D object in a 3D Canvas. */ private final class MyCanvasRenderer extends SimpleCanvasImpl { //====================================================================== // Private Fields private final Canvas myCanvas; private final Runnable myFrameListenerUpdater = new Runnable() { public void run() { final double secondsSinceLastFrame = myFpsCounter.getSecondsBetweenFrames(); for ( FrameListener frameListener : myFrameListeners ) { frameListener.onFrame( secondsSinceLastFrame ); } } }; private Spatial myCanvasRootNode; private boolean myAspectRatioNeedsCorrecting = true; //====================================================================== // Private Constants private static final float DEFAULT_FIELD_OF_VIEW_DEGREES = 45; //====================================================================== // Public Methods //---------------------------------------------------------------------- // Constructors /** * Creates a new renderer that renders the specified spatial in a 3D canvas. * * @param width initial size of the canvas. Should be larger than 0. * @param height initial size of the canvas. Should be larger than 0. * @param canvasRootNode the 3D object to render. * May be null, in which case nothing is rendered (black area) * @param canvas the canvas we are rendering to. Needed for listening to resize events. */ public MyCanvasRenderer( final int width, final int height, final Spatial canvasRootNode, final Canvas canvas ) { super( width, height ); ParameterChecker.checkPositiveNonZeroInteger( width, "width" ); ParameterChecker.checkPositiveNonZeroInteger( height, "height" ); ParameterChecker.checkNotNull( canvas, "canvas" ); myCanvasRootNode = canvasRootNode; myCanvas = canvas; // When the component is resized, adjust the size of the 3D viewport too. myCanvas.addComponentListener( new ComponentAdapter() { public void componentResized( ComponentEvent ce ) { resizeCanvas( myCanvas.getWidth(), myCanvas.getHeight() ); myAspectRatioNeedsCorrecting = true; } } ); } //---------------------------------------------------------------------- // Other Public Methods /** * @return the number of frames rendered per second, * or a negative value if the canvas has not yet been rendered. */ public double getFramesPerSecond() { return myFpsCounter.getFramesPerSecond(); } /** * @return number of seconds between the previous frame and the frame before that, * or a negative value if the canvas has not yet been rendered. */ public double getSecondsBetweenFrames() { return myFpsCounter.getSecondsBetweenFrames(); } /** * @param canvasRootNode the spatial to render with this CanvasRenderer. * May be null, in which case nothing is rendered (black area) */ public void setCanvasRootNode( final Spatial canvasRootNode ) { if ( rootNode != null && myCanvasRootNode != null ) { rootNode.detachChild( myCanvasRootNode ); } myCanvasRootNode = canvasRootNode; if ( rootNode != null && myCanvasRootNode != null ) { rootNode.attachChild( myCanvasRootNode ); } } @Override public void simpleSetup() { // Remove the back faces when rendering // REFACTOR: Actually the terrain is backwards at the moment, the camera is 'under' it. Flip it around at some point. final CullState cullState = DisplaySystem.getDisplaySystem().getRenderer().createCullState(); cullState.setCullMode( CullState.CS_FRONT ); rootNode.setRenderState( cullState ); if ( myCanvasRootNode != null ) { rootNode.attachChild( myCanvasRootNode ); } getCamera().setFrustumFar( myViewDistance ); } @Override public void simpleUpdate() { myFpsCounter.onFrame(); if ( !myFrameListeners.isEmpty() ) { SwingUtilities.invokeLater( myFrameListenerUpdater ); } } public void simpleRender() { // Setup aspect ratio for camera on the first frame (the camera is not created before the rendering starts) if ( myAspectRatioNeedsCorrecting ) { correctCameraAspectRatio(); myAspectRatioNeedsCorrecting = false; } } //====================================================================== // Private Methods /** * Sets the aspect ratio of the camera to the aspect ratio of the viewport size. */ private void correctCameraAspectRatio() { final Renderer renderer = getRenderer(); if ( renderer != null ) { // Get size on screen final float height = renderer.getHeight(); final float width = renderer.getWidth(); // Calculate aspect ratio float aspectRatio = 1; if ( height > 0 ) { aspectRatio = width / height; } // Set aspect ratio and field of view to camera final Camera camera = getCamera(); camera.setFrustumPerspective( DEFAULT_FIELD_OF_VIEW_DEGREES, aspectRatio, camera.getFrustumNear(), camera.getFrustumFar() ); } } } }