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