1 /*
2  * Canvas3DManager.java 25 oct. 07
3  *
4  * Sweet Home 3D, Copyright (c) 2007 Emmanuel PUYBARET / eTeks <info@eteks.com>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20 package com.eteks.sweethome3d.j3d;
21 
22 import java.awt.Graphics;
23 import java.awt.GraphicsConfigTemplate;
24 import java.awt.GraphicsConfiguration;
25 import java.awt.GraphicsDevice;
26 import java.awt.GraphicsEnvironment;
27 import java.awt.Window;
28 import java.awt.event.ActionEvent;
29 import java.awt.event.ActionListener;
30 import java.awt.event.HierarchyEvent;
31 import java.awt.event.HierarchyListener;
32 import java.awt.event.WindowAdapter;
33 import java.awt.event.WindowEvent;
34 import java.awt.event.WindowListener;
35 import java.awt.image.BufferedImage;
36 import java.util.concurrent.CountDownLatch;
37 import java.util.concurrent.TimeUnit;
38 
39 import javax.media.j3d.Canvas3D;
40 import javax.media.j3d.GraphicsConfigTemplate3D;
41 import javax.media.j3d.IllegalRenderingStateException;
42 import javax.media.j3d.ImageComponent2D;
43 import javax.media.j3d.RenderingError;
44 import javax.media.j3d.RenderingErrorListener;
45 import javax.media.j3d.Screen3D;
46 import javax.media.j3d.View;
47 import javax.media.j3d.VirtualUniverse;
48 import javax.swing.SwingUtilities;
49 import javax.swing.Timer;
50 
51 import com.eteks.sweethome3d.tools.OperatingSystem;
52 import com.sun.j3d.utils.universe.SimpleUniverse;
53 import com.sun.j3d.utils.universe.Viewer;
54 import com.sun.j3d.utils.universe.ViewingPlatform;
55 
56 /**
57  * Manager of <code>Canvas3D</code> instantiations and Java 3D error listeners.
58  * Note: this class is compatible with Java 3D 1.3 at runtime but requires Java 3D 1.5 to compile.
59  * @author Emmanuel Puybaret
60  */
61 public class Component3DManager {
62   private static final String CHECK_OFF_SCREEN_IMAGE_SUPPORT = "com.eteks.sweethome3d.j3d.checkOffScreenSupport";
63 
64   private static Component3DManager instance;
65 
66   private RenderingErrorObserver renderingErrorObserver;
67   // The Java 3D listener matching renderingErrorObserver
68   // (use Object class to ensure Component3DManager class can run with Java 3D 1.3.1)
69   private Object                 renderingErrorListener;
70   private Boolean                offScreenImageSupported;
71   private GraphicsConfiguration  defaultScreenConfiguration;
72   private int                    depthSize;
73 
Component3DManager()74   private Component3DManager() {
75     if (!GraphicsEnvironment.isHeadless()) {
76       // Request depth size equal to 24 if supported
77       int preferredDepthSize = 24;
78       try {
79         preferredDepthSize = Integer.valueOf(System.getProperty("com.eteks.sweethome3d.j3d.depthSize", "24"));
80       } catch (NumberFormatException ex) {
81         // Keep 24
82       }
83       GraphicsConfigTemplate3D template = createGraphicsConfigurationTemplate3D(preferredDepthSize);
84       this.defaultScreenConfiguration = createScreenConfiguration(template);
85       if (OperatingSystem.isWindows()) {
86         if (!isOffScreenImageSupported()) {
87           // Try again with default depth size
88           template = createGraphicsConfigurationTemplate3D(new GraphicsConfigTemplate3D().getDepthSize());
89           this.defaultScreenConfiguration = createScreenConfiguration(template);
90           // Reset offscreen support flag
91           this.offScreenImageSupported = null;
92         }
93       }
94       this.depthSize = template.getDepthSize();
95     } else {
96       this.offScreenImageSupported = Boolean.FALSE;
97     }
98   }
99 
100   /**
101    * Returns the template to configure the graphics of canvas 3D.
102    */
createGraphicsConfigurationTemplate3D(int preferredDepthSize)103   private GraphicsConfigTemplate3D createGraphicsConfigurationTemplate3D(int preferredDepthSize) {
104     if (System.getProperty("j3d.implicitAntialiasing") == null) {
105       System.setProperty("j3d.implicitAntialiasing", "true");
106     }
107 
108     GraphicsConfigTemplate3D template = new GraphicsConfigTemplate3D();
109     int defaultDepthSize = template.getDepthSize();
110     if (defaultDepthSize != preferredDepthSize) {
111       template.setDepthSize(preferredDepthSize);
112       if (!template.isGraphicsConfigSupported(
113           GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration())) {
114         template.setDepthSize(defaultDepthSize);
115       }
116     }
117 
118     // Try to get antialiasing
119     template.setSceneAntialiasing(GraphicsConfigTemplate3D.PREFERRED);
120 
121     // From http://www.java.net/node/683852
122     // Check if the user has set the Java 3D stereo option.
123     String stereo = System.getProperty("j3d.stereo");
124     if (stereo != null) {
125       if ("REQUIRED".equals(stereo)) {
126         template.setStereo(GraphicsConfigTemplate.REQUIRED);
127       } else if ("PREFERRED".equals(stereo)) {
128         template.setStereo(GraphicsConfigTemplate.PREFERRED);
129       }
130     }
131 
132     return template;
133   }
134 
135   /**
136    * Returns a screen configuration from the preferred template.
137    */
createScreenConfiguration(GraphicsConfigTemplate template)138   private GraphicsConfiguration createScreenConfiguration(GraphicsConfigTemplate template) {
139     GraphicsDevice defaultScreenDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
140     GraphicsConfiguration screenConfiguration = defaultScreenDevice.getBestConfiguration(template);
141     if (screenConfiguration == null) {
142       template = new GraphicsConfigTemplate3D();
143       screenConfiguration = defaultScreenDevice.getBestConfiguration(template);
144     }
145     return screenConfiguration;
146   }
147 
148   /**
149    * Returns the depth bits size of the Z-buffer.
150    */
getDepthSize()151   public int getDepthSize() {
152     return this.depthSize;
153   }
154 
155   /**
156    * Returns an instance of this singleton.
157    */
getInstance()158   public static Component3DManager getInstance() {
159     if (instance == null) {
160       instance = new Component3DManager();
161     }
162     return instance;
163   }
164 
165   /**
166    * Sets the current rendering error listener bound to <code>VirtualUniverse</code>.
167    */
setRenderingErrorObserver(RenderingErrorObserver observer)168   public void setRenderingErrorObserver(RenderingErrorObserver observer) {
169     try {
170       Class.forName("javax.media.j3d.RenderingErrorListener");
171       this.renderingErrorListener = RenderingErrorListenerManager.setRenderingErrorObserver(
172           observer, this.renderingErrorListener);
173       this.renderingErrorObserver = observer;
174     } catch (ClassNotFoundException ex) {
175       // As RenderingErrorListener and addRenderingErrorListener are available since Java 3D 1.5,
176       // use the default rendering error reporting if Sweet Home 3D is linked to a previous version
177     }
178   }
179 
180   /**
181    * Returns the current rendering error listener bound to <code>VirtualUniverse</code>.
182    */
getRenderingErrorObserver()183   public RenderingErrorObserver getRenderingErrorObserver() {
184     return this.renderingErrorObserver;
185   }
186 
187   /**
188    * Returns <code>true</code> if offscreen is supported in Java 3D on user system.
189    * Will always return <code>false</code> if <code>com.eteks.sweethome3d.j3d.checkOffScreenSupport</code>
190    * system is equal to <code>false</code>. By default, <code>com.eteks.sweethome3d.j3d.checkOffScreenSupport</code>
191    * is equal to <code>true</code>.
192    */
isOffScreenImageSupported()193   public boolean isOffScreenImageSupported() {
194     if (this.offScreenImageSupported == null) {
195       if ("false".equalsIgnoreCase(System.getProperty(CHECK_OFF_SCREEN_IMAGE_SUPPORT, "true"))) {
196         this.offScreenImageSupported = Boolean.FALSE;
197       } else if (OperatingSystem.isMacOSX()) {
198         // Avoid testing under macOS where it can lead to deadlocks during getOffScreenImage call with JOGL 2.4
199         this.offScreenImageSupported = Boolean.TRUE;
200       } else {
201         SimpleUniverse universe = null;
202         try {
203           // Create a universe bound to no canvas 3D
204           ViewingPlatform viewingPlatform = new ViewingPlatform();
205           Viewer viewer = new Viewer(new Canvas3D [0]);
206           universe = new SimpleUniverse(viewingPlatform, viewer);
207           // Create a dummy 3D image to check if it can be rendered in current Java 3D configuration
208           getOffScreenImage(viewer.getView(), 1, 1);
209           this.offScreenImageSupported = Boolean.TRUE;
210         } catch (IllegalRenderingStateException ex) {
211           this.offScreenImageSupported = Boolean.FALSE;
212         } catch (NullPointerException ex) {
213           this.offScreenImageSupported = Boolean.FALSE;
214         } catch (IllegalArgumentException ex) {
215           this.offScreenImageSupported = Boolean.FALSE;
216         } finally {
217           if (universe != null) {
218             universe.cleanup();
219           }
220         }
221       }
222     }
223     return this.offScreenImageSupported;
224   }
225 
226   /**
227    * Returns a new <code>canva3D</code> instance that will call <code>renderingObserver</code>
228    * methods during the rendering loop.
229    * @throws IllegalRenderingStateException  if the canvas 3D couldn't be created.
230    */
getCanvas3D(GraphicsConfiguration deviceConfiguration, boolean offscreen, final RenderingObserver renderingObserver)231   private Canvas3D getCanvas3D(GraphicsConfiguration deviceConfiguration,
232                                boolean offscreen,
233                                final RenderingObserver renderingObserver) {
234     GraphicsConfiguration configuration;
235     if (GraphicsEnvironment.isHeadless()) {
236       configuration = null;
237     } else if (deviceConfiguration == null
238                || deviceConfiguration.getDevice() == this.defaultScreenConfiguration.getDevice()) {
239       configuration = this.defaultScreenConfiguration;
240     } else {
241       GraphicsConfigTemplate3D template = createGraphicsConfigurationTemplate3D(this.depthSize);
242       configuration = deviceConfiguration.getDevice().getBestConfiguration(template);
243       if (configuration == null) {
244         configuration = deviceConfiguration.getDevice().getBestConfiguration(new GraphicsConfigTemplate3D());
245       }
246     }
247     if (configuration == null) {
248       throw new IllegalRenderingStateException("Can't create graphics environment for Canvas 3D");
249     }
250     try {
251       // Ensure unused canvases are freed
252       System.gc();
253 
254       // Create a Java 3D canvas
255       final Canvas3D canvas3D;
256       if (renderingObserver != null) {
257         canvas3D = new ObservedCanvas3D(configuration, offscreen, renderingObserver);
258       } else {
259         canvas3D = new Canvas3D(configuration, offscreen);
260       }
261 
262       if (!offscreen
263           && OperatingSystem.isLinux()
264           && OperatingSystem.isJavaVersionGreaterOrEqual("1.7")) {
265         // Add a listener to the parent window once known that will repaint the canvas in 100 ms
266         // when the window is activated / deactivated to avoid keeping an empty canvas
267         final WindowListener parentActivationListener = new WindowAdapter() {
268             private Timer timer;
269 
270             @Override
271             public void windowActivated(WindowEvent ev) {
272               if (this.timer == null) {
273                 this.timer = new Timer(100, new ActionListener() {
274                     public void actionPerformed(ActionEvent ev) {
275                       canvas3D.repaint();
276                     }
277                   });
278                 this.timer.setRepeats(false);
279               }
280               this.timer.restart();
281             }
282 
283             @Override
284             public void windowDeactivated(WindowEvent ev) {
285               windowActivated(null);
286             }
287           };
288         canvas3D.addHierarchyListener(new HierarchyListener() {
289             private Window parentWindow;
290 
291             public void hierarchyChanged(HierarchyEvent ev) {
292               Window window = SwingUtilities.windowForComponent(canvas3D);
293               if (window != null) {
294                 if (this.parentWindow != window) {
295                   window.addWindowListener(parentActivationListener);
296                 }
297               } else if (this.parentWindow != null) {
298                 this.parentWindow.removeWindowListener(parentActivationListener);
299               }
300               this.parentWindow = window;
301             }
302           });
303       }
304 
305       return canvas3D;
306     } catch (IllegalArgumentException ex) {
307       IllegalRenderingStateException ex2 = new IllegalRenderingStateException("Can't create Canvas 3D");
308       ex2.initCause(ex);
309       throw ex2;
310     }
311   }
312 
313   /**
314    * Returns a new on screen <code>canva3D</code> instance. The returned canvas 3D will be associated
315    * with the graphics configuration of the default screen device.
316    * @throws IllegalRenderingStateException  if the canvas 3D couldn't be created.
317    */
getOnscreenCanvas3D()318   public Canvas3D getOnscreenCanvas3D() {
319     return getOnscreenCanvas3D(null);
320   }
321 
322   /**
323    * Returns a new on screen <code>canva3D</code> instance which rendering will be observed
324    * with the given rendering observer. The returned canvas 3D will be associated with the
325    * graphics configuration of the default screen device.
326    * @param renderingObserver an observer of the 3D rendering process of the returned canvas.
327    *            Caution: The methods of the observer will be called in 3D rendering loop thread.
328    * @throws IllegalRenderingStateException  if the canvas 3D couldn't be created.
329    */
getOnscreenCanvas3D(RenderingObserver renderingObserver)330   public Canvas3D getOnscreenCanvas3D(RenderingObserver renderingObserver) {
331     return getCanvas3D(null, false, renderingObserver);
332   }
333 
334   /**
335    * Returns a new on screen <code>canva3D</code> instance which rendering will be observed
336    * with the given rendering observer.
337    * @param renderingObserver an observer of the 3D rendering process of the returned canvas.
338    *            Caution: The methods of the observer will be called in 3D rendering loop thread.
339    * @throws IllegalRenderingStateException  if the canvas 3D couldn't be created.
340    */
getOnscreenCanvas3D(GraphicsConfiguration deviceConfiguration, RenderingObserver renderingObserver)341   public Canvas3D getOnscreenCanvas3D(GraphicsConfiguration deviceConfiguration,
342                                       RenderingObserver renderingObserver) {
343     return getCanvas3D(deviceConfiguration, false, renderingObserver);
344   }
345 
346   /**
347    * Returns a new off screen <code>canva3D</code> at the given size.
348    * @throws IllegalRenderingStateException  if the canvas 3D couldn't be created.
349    *    To avoid this exception, call {@link #isOffScreenImageSupported() isOffScreenImageSupported()} first.
350    */
getOffScreenCanvas3D(int width, int height)351   public Canvas3D getOffScreenCanvas3D(int width, int height) {
352     Canvas3D offScreenCanvas = getCanvas3D(null, true, null);
353     // Configure canvas 3D for offscreen
354     Screen3D screen3D = offScreenCanvas.getScreen3D();
355     screen3D.setSize(width, height);
356     screen3D.setPhysicalScreenWidth(2f);
357     screen3D.setPhysicalScreenHeight(2f / width * height);
358     BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
359     ImageComponent2D imageComponent2D = new ImageComponent2D(ImageComponent2D.FORMAT_RGB, image);
360     imageComponent2D.setCapability(ImageComponent2D.ALLOW_IMAGE_READ);
361     offScreenCanvas.setOffScreenBuffer(imageComponent2D);
362     return offScreenCanvas;
363   }
364 
365   /**
366    * Returns an image at the given size of the 3D <code>view</code>.
367    * This image is created with an off screen canvas.
368    * @throws IllegalRenderingStateException  if the image couldn't be created.
369    */
getOffScreenImage(View view, int width, int height)370   public BufferedImage getOffScreenImage(View view, int width, int height)  {
371     Canvas3D offScreenCanvas = null;
372     RenderingErrorObserver previousRenderingErrorObserver = getRenderingErrorObserver();
373     try {
374       // Replace current rendering error observer by a listener that counts down
375       // a latch to check further if a rendering error happened during off screen rendering
376       // (rendering error listener is called from a notification thread)
377       final CountDownLatch latch = new CountDownLatch(1);
378       setRenderingErrorObserver(new RenderingErrorObserver() {
379           public void errorOccured(int errorCode, String errorMessage) {
380             latch.countDown();
381           }
382         });
383 
384       // Create an off screen canvas and bind it to view
385       offScreenCanvas = getOffScreenCanvas3D(width, height);
386       view.addCanvas3D(offScreenCanvas);
387 
388       // Render off screen canvas
389       offScreenCanvas.renderOffScreenBuffer();
390       offScreenCanvas.waitForOffScreenRendering();
391 
392       // If latch count becomes equal to 0 during the past instructions or in the coming 10 milliseconds,
393       // this means that a rendering error happened (for example, in case changing default depth size isn't supported)
394       if (latch.await(10, TimeUnit.MILLISECONDS)) {
395         throw new IllegalRenderingStateException("Off screen rendering unavailable");
396       }
397 
398       return offScreenCanvas.getOffScreenBuffer().getImage();
399     } catch (InterruptedException ex) {
400       IllegalRenderingStateException ex2 =
401           new IllegalRenderingStateException("Off screen rendering interrupted");
402       ex2.initCause(ex);
403       throw ex2;
404     } finally {
405       if (offScreenCanvas != null) {
406         view.removeCanvas3D(offScreenCanvas);
407         try {
408           // Free off screen buffer and context
409           offScreenCanvas.setOffScreenBuffer(null);
410         } catch (NullPointerException ex) {
411           // Java 3D 1.3 may throw an exception
412         }
413       }
414       // Reset previous rendering error listener
415       setRenderingErrorObserver(previousRenderingErrorObserver);
416     }
417   }
418 
419   /**
420    * An observer that receives error notifications in Java 3D.
421    */
422   public static interface RenderingErrorObserver {
errorOccured(int errorCode, String errorMessage)423     void errorOccured(int errorCode, String errorMessage);
424   }
425 
426   /**
427    * Manages Java 3D 1.5 <code>RenderingErrorListener</code> change matching the given
428    * rendering error observer.
429    */
430   private static class RenderingErrorListenerManager {
setRenderingErrorObserver(final RenderingErrorObserver observer, Object previousRenderingErrorListener)431     public static Object setRenderingErrorObserver(final RenderingErrorObserver observer,
432                                                    Object previousRenderingErrorListener) {
433       if (previousRenderingErrorListener != null) {
434         VirtualUniverse.removeRenderingErrorListener(
435             (RenderingErrorListener)previousRenderingErrorListener);
436       }
437       RenderingErrorListener renderingErrorListener = new RenderingErrorListener() {
438         public void errorOccurred(RenderingError error) {
439           observer.errorOccured(error.getErrorCode(), error.getErrorMessage());
440         }
441       };
442       VirtualUniverse.addRenderingErrorListener(renderingErrorListener);
443       return renderingErrorListener;
444     }
445   }
446 
447   /**
448    * An observer that receives notifications during the different steps
449    * of the loop rendering a canvas 3D.
450    */
451   public static interface RenderingObserver {
452     /**
453      * Called before <code>canvas3D</code> is rendered.
454      */
canvas3DPreRendered(Canvas3D canvas3D)455     public void canvas3DPreRendered(Canvas3D canvas3D);
456 
457     /**
458      * Called after <code>canvas3D</code> is rendered.
459      */
canvas3DPostRendered(Canvas3D canvas3D)460     public void canvas3DPostRendered(Canvas3D canvas3D);
461 
462     /**
463      * Called after <code>canvas3D</code> buffer is swapped.
464      */
canvas3DSwapped(Canvas3D canvas3D)465     public void canvas3DSwapped(Canvas3D canvas3D);
466   }
467 
468   /**
469    * A canvas 3D observed during its rendering.
470    */
471   private static class ObservedCanvas3D extends Canvas3D {
472     private final RenderingObserver renderingObserver;
473     private final boolean           paintDelayed;
474     private Timer timer;
475 
ObservedCanvas3D(GraphicsConfiguration graphicsConfiguration, boolean offScreen, RenderingObserver renderingObserver)476     private ObservedCanvas3D(GraphicsConfiguration graphicsConfiguration,
477                              boolean offScreen,
478                              RenderingObserver renderingObserver) {
479       super(graphicsConfiguration, offScreen);
480       this.renderingObserver = renderingObserver;
481       // Under Windows with Java 7 and above, delay the rendering of the canvas 3D when
482       // it's repainted (i.e. it's resized, moved or partially hidden) to avoid it to get grayed
483       this.paintDelayed = OperatingSystem.isWindows()
484           && OperatingSystem.isJavaVersionGreaterOrEqual("1.7");
485     }
486 
487     @Override
preRender()488     public void preRender() {
489       this.renderingObserver.canvas3DPreRendered(this);
490     }
491 
492     @Override
postRender()493     public void postRender() {
494       this.renderingObserver.canvas3DPostRendered(this);
495     }
496 
497     @Override
postSwap()498     public void postSwap() {
499       this.renderingObserver.canvas3DSwapped(this);
500     }
501 
502     @Override
paint(Graphics g)503     public void paint(Graphics g) {
504       if (this.paintDelayed) {
505         if (this.timer == null) {
506           this.timer = new Timer(100, new ActionListener() {
507               public void actionPerformed(ActionEvent ev) {
508                 Graphics g = getGraphics();
509                 ObservedCanvas3D.super.paint(g);
510                 g.dispose();
511               }
512             });
513           this.timer.setRepeats(false);
514         }
515         this.timer.restart();
516       } else {
517         super.paint(g);
518       }
519     }
520   }
521 }
522