package net.sf.openrocket.gui.figure3d; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.SplashScreen; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Set; import javax.media.opengl.GL; import javax.media.opengl.GL2; import javax.media.opengl.GLAutoDrawable; import javax.media.opengl.GLCapabilities; import javax.media.opengl.GLEventListener; import javax.media.opengl.GLProfile; import javax.media.opengl.GLRunnable; import javax.media.opengl.awt.GLCanvas; import javax.media.opengl.awt.GLJPanel; import javax.media.opengl.fixedfunc.GLLightingFunc; import javax.media.opengl.fixedfunc.GLMatrixFunc; import javax.media.opengl.glu.GLU; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import javax.swing.event.MouseInputAdapter; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.gui.figureelements.CGCaret; import net.sf.openrocket.gui.figureelements.CPCaret; import net.sf.openrocket.gui.figureelements.FigureElement; import net.sf.openrocket.gui.main.Splash; import net.sf.openrocket.rocketcomponent.Configuration; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.startup.Application; import net.sf.openrocket.startup.Preferences; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.jogamp.opengl.util.awt.Overlay; /* * @author Bill Kuker <bkuker@billkuker.com> */ public class RocketFigure3d extends JPanel implements GLEventListener { public static final int TYPE_FIGURE = 0; public static final int TYPE_UNFINISHED = 1; public static final int TYPE_FINISHED = 2; private static final long serialVersionUID = 1L; private static final Logger log = LoggerFactory.getLogger(RocketFigure3d.class); static { //this allows the GL canvas and things like the motor selection //drop down to z-order themselves. JPopupMenu.setDefaultLightWeightPopupEnabled(false); } private static final double fovY = 15.0; private static double fovX = Double.NaN; private static final int CARET_SIZE = 20; private final OpenRocketDocument document; private final Configuration configuration; private Component canvas; private Overlay extrasOverlay, caretOverlay; private BufferedImage cgCaretRaster, cpCaretRaster; private volatile boolean redrawExtras = true; private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>(); private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>(); private double roll = 0; private double yaw = 0; Point pickPoint = null; MouseEvent pickEvent; float[] lightPosition = new float[] { 1, 4, 1, 0 }; RocketRenderer rr = new FigureRenderer(); public RocketFigure3d(final OpenRocketDocument document, final Configuration config) { this.document = document; this.configuration = config; this.setLayout(new BorderLayout()); //Only initizlize GL if 3d is enabled. if (is3dEnabled()) { //Fixes a linux / X bug: Splash must be closed before GL Init SplashScreen splash = Splash.getSplashScreen(); if (splash != null && splash.isVisible()) splash.close(); initGLCanvas(); } } public void flushTextureCaches() { ((GLAutoDrawable) canvas).invoke(true, new GLRunnable() { @Override public boolean run(GLAutoDrawable drawable) { rr.flushTextureCache(drawable); return false; } }); } /** * Return true if 3d view is enabled. This may be toggled by the user at * launch time. * @return */ public static boolean is3dEnabled() { //Allow disable by command line, if program won't even start if (System.getProperty("openrocket.3d.disable") != null) return false; //return by preference return Application.getPreferences().getBoolean(Preferences.OPENGL_ENABLED, true); } private void initGLCanvas() { log.debug("Initializing RocketFigure3D OpenGL Canvas"); try { log.debug("Setting up GL capabilities..."); log.trace("GL - Getting Default Profile"); final GLProfile glp = GLProfile.get(GLProfile.GL2); log.trace("GL - creating GLCapabilities"); final GLCapabilities caps = new GLCapabilities(glp); if (Application.getPreferences().getBoolean(Preferences.OPENGL_ENABLE_AA, true)) { log.trace("GL - setSampleBuffers"); caps.setSampleBuffers(true); log.trace("GL - setNumSamples"); caps.setNumSamples(6); } else { log.trace("GL - Not enabling AA by user pref"); } if (Application.getPreferences().getBoolean(Preferences.OPENGL_USE_FBO, false)) { log.trace("GL - Creating GLJPanel"); canvas = new GLJPanel(caps); } else { log.trace("GL - Creating GLCanvas"); canvas = new GLCanvas(caps); } log.trace("GL - Registering as GLEventListener on canvas"); ((GLAutoDrawable) canvas).addGLEventListener(this); log.trace("GL - Adding canvas to this JPanel"); this.add(canvas, BorderLayout.CENTER); log.trace("GL - Setting up mouse listeners"); setupMouseListeners(); log.trace("GL - Rasterizing Carets"); rasterizeCarets(); } catch (Throwable t) { log.error("An error occurred creating 3d View", t); canvas = null; this.add(new JLabel("Unable to load 3d Libraries: " + t.getMessage())); } } /** * Set up the standard rendering hints on the Graphics2D */ private static void setRenderingHints(Graphics2D g) { g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } /** * Rasterize the carets into 2 buffered images that I can blit onto the * 3d display every redraw without all of the caret shape rendering overhead */ private void rasterizeCarets() { Graphics2D g2d; //Rasterize a CG Caret cgCaretRaster = new BufferedImage(CARET_SIZE, CARET_SIZE, BufferedImage.TYPE_4BYTE_ABGR); g2d = cgCaretRaster.createGraphics(); setRenderingHints(g2d); g2d.setBackground(new Color(0, 0, 0, 0)); g2d.clearRect(0, 0, CARET_SIZE, CARET_SIZE); new CGCaret(CARET_SIZE / 2, CARET_SIZE / 2).paint(g2d, 1.0); g2d.dispose(); //Rasterize a CP Caret cpCaretRaster = new BufferedImage(CARET_SIZE, CARET_SIZE, BufferedImage.TYPE_4BYTE_ABGR); g2d = cpCaretRaster.createGraphics(); setRenderingHints(g2d); g2d.setBackground(new Color(0, 0, 0, 0)); g2d.clearRect(0, 0, CARET_SIZE, CARET_SIZE); new CPCaret(CARET_SIZE / 2, CARET_SIZE / 2).paint(g2d, 1.0); g2d.dispose(); } private void setupMouseListeners() { MouseInputAdapter a = new MouseInputAdapter() { int lastX; int lastY; MouseEvent pressEvent; @Override public void mousePressed(final MouseEvent e) { lastX = e.getX(); lastY = e.getY(); pressEvent = e; } @Override public void mouseClicked(final MouseEvent e) { pickPoint = new Point(lastX, canvas.getHeight() - lastY); pickEvent = e; internalRepaint(); } @Override public void mouseDragged(final MouseEvent e) { //You can get a drag without a press while a modal dialog is shown if (pressEvent == null) return; int dx = lastX - e.getX(); int dy = lastY - e.getY(); lastX = e.getX(); lastY = e.getY(); if (pressEvent.getButton() == MouseEvent.BUTTON1) { if (Math.abs(dx) > Math.abs(dy)) { setYaw(yaw - dx / 100.0); } else { if (yaw > Math.PI / 2.0 && yaw < 3.0 * Math.PI / 2.0) { dy = -dy; } setRoll(roll - dy / 100.0); } } else { lightPosition[0] -= 0.1f * dx; lightPosition[1] += 0.1f * dy; internalRepaint(); } } }; canvas.addMouseMotionListener(a); canvas.addMouseListener(a); } @Override public void display(final GLAutoDrawable drawable) { GL2 gl = drawable.getGL().getGL2(); GLU glu = new GLU(); gl.glEnable(GL.GL_MULTISAMPLE); gl.glClearColor(1, 1, 1, 1); gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); setupView(gl, glu); if (pickPoint != null) { gl.glDisable(GLLightingFunc.GL_LIGHTING); final RocketComponent picked = rr.pick(drawable, configuration, pickPoint, pickEvent.isShiftDown() ? selection : null); if (csl != null) { final MouseEvent e = pickEvent; SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (picked == null) { log.debug("unselecting"); csl.componentClicked(new RocketComponent[] {}, e); } else { csl.componentClicked(new RocketComponent[] { picked }, e); } } }); } pickPoint = null; gl.glClearColor(1, 1, 1, 1); gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); gl.glEnable(GLLightingFunc.GL_LIGHTING); } rr.render(drawable, configuration, selection); drawExtras(gl, glu); drawCarets(gl, glu); //GLJPanel with GLSL Flipper relies on this: gl.glFrontFace(GL.GL_CCW); } private void drawCarets(final GL2 gl, final GLU glu) { final Graphics2D og2d = caretOverlay.createGraphics(); setRenderingHints(og2d); og2d.setBackground(new Color(0, 0, 0, 0)); og2d.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); caretOverlay.markDirty(0, 0, canvas.getWidth(), canvas.getHeight()); // The existing relative Extras don't really work right for 3d. Coordinate pCP = project(cp, gl, glu); Coordinate pCG = project(cg, gl, glu); final int d = CARET_SIZE / 2; //z order the carets if (pCG.z < pCP.z) { //Subtract half of the caret size, so they are centered ( The +/- d in each translate) //Flip the sense of the Y coordinate from GL to normal (Y+ up/down) og2d.drawRenderedImage( cpCaretRaster, AffineTransform.getTranslateInstance((pCP.x - d), canvas.getHeight() - (pCP.y + d))); og2d.drawRenderedImage( cgCaretRaster, AffineTransform.getTranslateInstance((pCG.x - d), canvas.getHeight() - (pCG.y + d))); } else { og2d.drawRenderedImage( cgCaretRaster, AffineTransform.getTranslateInstance((pCG.x - d), canvas.getHeight() - (pCG.y + d))); og2d.drawRenderedImage( cpCaretRaster, AffineTransform.getTranslateInstance((pCP.x - d), canvas.getHeight() - (pCP.y + d))); } og2d.dispose(); gl.glEnable(GL.GL_BLEND); caretOverlay.drawAll(); gl.glDisable(GL.GL_BLEND); } /** * Draw the extras overlay to the gl canvas. * Re-blits the overlay every frame. Only re-renders the overlay * when needed. */ private void drawExtras(final GL2 gl, final GLU glu) { //Only re-render if needed // redrawExtras: Some external change (new simulation data) means // the data is out of date. // extrasOverlay.contentsLost(): For some reason the buffer with this // data is lost. if (redrawExtras || extrasOverlay.contentsLost()) { log.debug("Redrawing Overlay"); final Graphics2D og2d = extrasOverlay.createGraphics(); setRenderingHints(og2d); og2d.setBackground(new Color(0, 0, 0, 0)); og2d.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); extrasOverlay.markDirty(0, 0, canvas.getWidth(), canvas.getHeight()); for (FigureElement e : relativeExtra) { e.paint(og2d, 1); } Rectangle rect = this.getVisibleRect(); for (FigureElement e : absoluteExtra) { e.paint(og2d, 1.0, rect); } og2d.dispose(); redrawExtras = false; } //Re-blit to gl canvas every time gl.glEnable(GL.GL_BLEND); extrasOverlay.drawAll(); gl.glDisable(GL.GL_BLEND); } @Override public void dispose(final GLAutoDrawable drawable) { log.trace("GL - dispose() called"); rr.dispose(drawable); } @Override public void init(final GLAutoDrawable drawable) { log.trace("GL - init()"); final GL2 gl = drawable.getGL().getGL2(); gl.glClearDepth(1.0f); // clear z-buffer to the farthest gl.glDepthFunc(GL.GL_LESS); // the type of depth test to do float amb = 0.5f; float dif = 1.0f; gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_AMBIENT, new float[] { amb, amb, amb, 1 }, 0); gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_DIFFUSE, new float[] { dif, dif, dif, 1 }, 0); gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_SPECULAR, new float[] { dif, dif, dif, 1 }, 0); gl.glEnable(GLLightingFunc.GL_LIGHT1); gl.glEnable(GLLightingFunc.GL_LIGHTING); gl.glShadeModel(GLLightingFunc.GL_SMOOTH); gl.glEnable(GLLightingFunc.GL_NORMALIZE); rr.init(drawable); extrasOverlay = new Overlay(drawable); caretOverlay = new Overlay(drawable); } @Override public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int w, final int h) { log.trace("GL - reshape()"); final GL2 gl = drawable.getGL().getGL2(); final GLU glu = new GLU(); final double ratio = (double) w / (double) h; fovX = fovY * ratio; gl.glMatrixMode(GLMatrixFunc.GL_PROJECTION); gl.glLoadIdentity(); glu.gluPerspective(fovY, ratio, 0.1f, 50f); gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); redrawExtras = true; } @SuppressWarnings("unused") private static class Bounds { double xMin, xMax, xSize; double yMin, yMax, ySize; double zMin, zMax, zSize; double rMax; } private Bounds cachedBounds = null; /** * Calculates the bounds for the current configuration * * @return */ private Bounds calculateBounds() { if (cachedBounds != null) { return cachedBounds; } else { final Bounds b = new Bounds(); final Collection<Coordinate> bounds = configuration.getBounds(); for (Coordinate c : bounds) { b.xMax = Math.max(b.xMax, c.x); b.xMin = Math.min(b.xMin, c.x); b.yMax = Math.max(b.yMax, c.y); b.yMin = Math.min(b.yMin, c.y); b.zMax = Math.max(b.zMax, c.z); b.zMin = Math.min(b.zMin, c.z); double r = MathUtil.hypot(c.y, c.z); b.rMax = Math.max(b.rMax, r); } b.xSize = b.xMax - b.xMin; b.ySize = b.yMax - b.yMin; b.zSize = b.zMax - b.zMin; cachedBounds = b; return b; } } private void setupView(final GL2 gl, final GLU glu) { gl.glLoadIdentity(); gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_POSITION, lightPosition, 0); // Get the bounds final Bounds b = calculateBounds(); // Calculate the distance needed to fit the bounds in both the X and Y // direction // Add 10% for space around it. final double dX = (b.xSize * 1.2 / 2.0) / Math.tan(Math.toRadians(fovX / 2.0)); final double dY = (b.rMax * 2.0 * 1.2 / 2.0) / Math.tan(Math.toRadians(fovY / 2.0)); // Move back the greater of the 2 distances glu.gluLookAt(0, 0, Math.max(dX, dY), 0, 0, 0, 0, 1, 0); gl.glRotated(yaw * (180.0 / Math.PI), 0, 1, 0); gl.glRotated(roll * (180.0 / Math.PI), 1, 0, 0); // Center the rocket in the view. gl.glTranslated(-b.xMin - b.xSize / 2.0, 0, 0); //Change to LEFT Handed coordinates gl.glScaled(1, 1, -1); gl.glFrontFace(GL.GL_CW); //Flip textures for LEFT handed coords gl.glMatrixMode(GL.GL_TEXTURE); gl.glLoadIdentity(); gl.glScaled(-1, 1, 1); gl.glTranslated(-1, 0, 0); gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); } /** * Call when the rocket has changed */ public void updateFigure() { log.debug("3D Figure Updated"); cachedBounds = null; if (canvas != null) { ((GLAutoDrawable) canvas).invoke(true, new GLRunnable() { @Override public boolean run(GLAutoDrawable drawable) { rr.updateFigure(drawable); return false; } }); } } private void internalRepaint() { if (canvas != null) { ((GLAutoDrawable) canvas).display(); } super.repaint(); } @Override public void repaint() { redrawExtras = true; internalRepaint(); } private Set<RocketComponent> selection = new HashSet<RocketComponent>(); public void setSelection(final RocketComponent[] selection) { this.selection.clear(); if (selection != null) { for (RocketComponent c : selection) this.selection.add(c); } internalRepaint(); } private void setRoll(final double rot) { if (MathUtil.equals(roll, rot)) return; this.roll = MathUtil.reduce360(rot); internalRepaint(); } private void setYaw(final double rot) { if (MathUtil.equals(yaw, rot)) return; this.yaw = MathUtil.reduce360(rot); internalRepaint(); } // ///////////// Extra methods private Coordinate project(final Coordinate c, final GL2 gl, final GLU glu) { final double[] mvmatrix = new double[16]; final double[] projmatrix = new double[16]; final int[] viewport = new int[4]; gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0); gl.glGetDoublev(GLMatrixFunc.GL_MODELVIEW_MATRIX, mvmatrix, 0); gl.glGetDoublev(GLMatrixFunc.GL_PROJECTION_MATRIX, projmatrix, 0); final double out[] = new double[4]; glu.gluProject(c.x, c.y, c.z, mvmatrix, 0, projmatrix, 0, viewport, 0, out, 0); return new Coordinate(out[0], out[1], out[2]); } private Coordinate cp = new Coordinate(0, 0, 0); private Coordinate cg = new Coordinate(0, 0, 0); public void setCG(final Coordinate cg) { this.cg = cg; redrawExtras = true; } public void setCP(final Coordinate cp) { this.cp = cp; redrawExtras = true; } public void addRelativeExtra(final FigureElement p) { relativeExtra.add(p); redrawExtras = true; } public void removeRelativeExtra(final FigureElement p) { relativeExtra.remove(p); redrawExtras = true; } public void clearRelativeExtra() { relativeExtra.clear(); redrawExtras = true; } public void addAbsoluteExtra(final FigureElement p) { absoluteExtra.add(p); redrawExtras = true; } public void removeAbsoluteExtra(final FigureElement p) { absoluteExtra.remove(p); redrawExtras = true; } public void clearAbsoluteExtra() { absoluteExtra.clear(); redrawExtras = true; } private ComponentSelectionListener csl; public static interface ComponentSelectionListener { public void componentClicked(RocketComponent[] components, MouseEvent e); } public void addComponentSelectionListener( ComponentSelectionListener newListener) { this.csl = newListener; } public void setType(final int t) { //There is no canvas if there was an error while creating it. if (canvas == null) return; // The first time the user selects any 3d figure types, the canvas' internal _drawable // has not been realized. Unfortunately, there is a test in canvas.invoke which doesn't // execute the runnable if the drawable isn't realized. // In order to trump this, we test if the canvas has not been realized and initialize // the renderer accordingly. There is certainly a better way to do this. final RocketRenderer newRR; switch (t) { case TYPE_FINISHED: newRR = new RealisticRenderer(document); break; case TYPE_UNFINISHED: newRR = new UnfinishedRenderer(document); break; default: newRR = new FigureRenderer(); } if (canvas instanceof GLCanvas && !((GLCanvas) canvas).isRealized()) { rr = newRR; } else if (canvas instanceof GLJPanel && !((GLJPanel) canvas).isRealized()) { rr = newRR; } else { ((GLAutoDrawable) canvas).invoke(true, new GLRunnable() { @Override public boolean run(GLAutoDrawable drawable) { rr.dispose(drawable); rr = newRR; newRR.init(drawable); if (canvas instanceof GLJPanel) internalRepaint(); return false; } }); } } }