package net.sf.openrocket.gui.figure3d.photo; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Rectangle; import java.awt.SplashScreen; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.image.BufferedImage; import java.util.Collection; import java.util.EventObject; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Vector; import javax.media.opengl.DebugGL2; 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.event.MouseInputAdapter; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.document.events.DocumentChangeEvent; import net.sf.openrocket.document.events.DocumentChangeListener; import net.sf.openrocket.gui.figure3d.RealisticRenderer; import net.sf.openrocket.gui.figure3d.RocketRenderer; import net.sf.openrocket.gui.figure3d.TextureCache; import net.sf.openrocket.gui.figure3d.photo.exhaust.FlameRenderer; import net.sf.openrocket.gui.main.Splash; import net.sf.openrocket.motor.Motor; import net.sf.openrocket.rocketcomponent.Configuration; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.Stage; import net.sf.openrocket.startup.Application; import net.sf.openrocket.startup.Preferences; import net.sf.openrocket.util.Color; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.StateChangeListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.jogamp.opengl.util.awt.AWTGLReadBufferUtil; public class PhotoPanel extends JPanel implements GLEventListener { private static final long serialVersionUID = 1L; private static final Logger log = LoggerFactory.getLogger(PhotoPanel.class); static { // this allows the GL canvas and things like the motor selection // drop down to z-order themselves. JPopupMenu.setDefaultLightWeightPopupEnabled(false); } private Configuration configuration; private Component canvas; private TextureCache textureCache = new TextureCache(); private double ratio; private boolean needUpdate = false; private List<ImageCallback> imageCallbacks = new java.util.Vector<PhotoPanel.ImageCallback>(); interface ImageCallback { public void performAction(BufferedImage i); } void addImageCallback(ImageCallback a) { imageCallbacks.add(a); repaint(); } private RocketRenderer rr; private PhotoSettings p; void setDoc(final OpenRocketDocument doc) { ((GLAutoDrawable) canvas).invoke(false, new GLRunnable() { @Override public boolean run(final GLAutoDrawable drawable) { PhotoPanel.this.configuration = doc.getDefaultConfiguration(); cachedBounds = null; rr = new RealisticRenderer(doc); rr.init(drawable); doc.getDefaultConfiguration().addChangeListener( new StateChangeListener() { @Override public void stateChanged(EventObject e) { log.debug("Repainting on config state change"); needUpdate = true; PhotoPanel.this.repaint(); } }); doc.addDocumentChangeListener(new DocumentChangeListener() { @Override public void documentChanged(DocumentChangeEvent event) { log.debug("Repainting on document change"); needUpdate = true; PhotoPanel.this.repaint(); } }); return false; } }); } PhotoSettings getSettings() { return p; } PhotoPanel() { this.setLayout(new BorderLayout()); p = new PhotoSettings(); // Fixes a linux / X bug: Splash must be closed before GL Init SplashScreen splash = Splash.getSplashScreen(); if (splash != null && splash.isVisible()) splash.close(); initGLCanvas(); setupMouseListeners(); p.addChangeListener(new StateChangeListener() { @Override public void stateChanged(EventObject e) { log.debug("Repainting on settings state change"); PhotoPanel.this.repaint(); } }); } private void initGLCanvas() { try { log.debug("Setting up GL capabilities..."); final GLProfile glp = GLProfile.get(GLProfile.GL2); final GLCapabilities caps = new GLCapabilities(glp); if (Application.getPreferences().getBoolean( Preferences.OPENGL_ENABLE_AA, true)) { caps.setSampleBuffers(true); 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); } ((GLAutoDrawable) canvas).addGLEventListener(this); this.add(canvas, BorderLayout.CENTER); } 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())); } } 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 mouseWheelMoved(MouseWheelEvent e) { p.setViewDistance(p.getViewDistance() + 0.1 * e.getWheelRotation()); } @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; final double height = canvas.getHeight(); final double width = canvas.getWidth(); final double x1 = (width - 2 * lastX) / width; final double y1 = (2 * lastY - height) / height; final double x2 = (width - 2 * e.getX()) / width; final double y2 = (2 * e.getY() - height) / height; p.setViewAltAz(p.getViewAlt() - (y1 - y2), p.getViewAz() + (x1 - x2)); lastX = e.getX(); lastY = e.getY(); } }; canvas.addMouseWheelListener(a); canvas.addMouseMotionListener(a); canvas.addMouseListener(a); } @Override public void paintImmediately(Rectangle r) { super.paintImmediately(r); if (canvas != null) ((GLAutoDrawable) canvas).display(); } @Override public void paintImmediately(int x, int y, int w, int h) { super.paintImmediately(x, y, w, h); if (canvas != null) ((GLAutoDrawable) canvas).display(); } /* * @Override public void repaint() { if (canvas != null) ((GLAutoDrawable) * canvas).display(); super.repaint(); } */ @Override public void display(final GLAutoDrawable drawable) { GL2 gl = drawable.getGL().getGL2(); if (needUpdate) rr.updateFigure(drawable); needUpdate = false; draw(drawable, 0); if (p.isMotionBlurred()) { Bounds b = calculateBounds(); float m = .6f; int c = 10; float d = (float) b.xSize / 25.0f; gl.glAccum(GL2.GL_LOAD, m); for (int i = 1; i <= c; i++) { draw(drawable, d / c * i); gl.glAccum(GL2.GL_ACCUM, (1.0f - m) / c); } gl.glAccum(GL2.GL_RETURN, 1.0f); } if (!imageCallbacks.isEmpty()) { BufferedImage i = (new AWTGLReadBufferUtil( GLProfile.get(GLProfile.GL2), false)) .readPixelsToBufferedImage(drawable.getGL(), 0, 0, drawable.getWidth(), drawable.getHeight(), true); final Vector<ImageCallback> cbs = new Vector<PhotoPanel.ImageCallback>( imageCallbacks); imageCallbacks.clear(); for (ImageCallback ia : cbs) { try { ia.performAction(i); } catch (Throwable t) { log.error("Image Callback {} threw", i, t); } } } } private static void convertColor(Color color, float[] out) { if (color == null) { out[0] = 1; out[1] = 1; out[2] = 0; } else { out[0] = (float) color.getRed() / 255f; out[1] = (float) color.getGreen() / 255f; out[2] = (float) color.getBlue() / 255f; } } private void draw(final GLAutoDrawable drawable, float dx) { GL2 gl = drawable.getGL().getGL2(); GLU glu = new GLU(); float[] color = new float[3]; gl.glEnable(GL.GL_MULTISAMPLE); convertColor(p.getSunlight(), color); float amb = (float) p.getAmbiance(); float dif = 1.0f - amb; float spc = 1.0f; gl.glLightfv( GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_AMBIENT, new float[] { amb * color[0], amb * color[1], amb * color[2], 1 }, 0); gl.glLightfv( GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_DIFFUSE, new float[] { dif * color[0], dif * color[1], dif * color[2], 1 }, 0); gl.glLightfv( GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_SPECULAR, new float[] { spc * color[0], spc * color[1], spc * color[2], 1 }, 0); convertColor(p.getSkyColor(), color); gl.glClearColor(color[0], color[1], color[2], 1); gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); gl.glMatrixMode(GLMatrixFunc.GL_PROJECTION); gl.glLoadIdentity(); glu.gluPerspective(p.getFov() * (180.0 / Math.PI), ratio, 0.1f, 50f); gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); // 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); gl.glLoadIdentity(); gl.glEnable(GL.GL_CULL_FACE); gl.glCullFace(GL.GL_BACK); gl.glFrontFace(GL.GL_CCW); // Draw the sky gl.glPushMatrix(); gl.glDisable(GLLightingFunc.GL_LIGHTING); gl.glDepthMask(false); gl.glRotated(p.getViewAlt() * (180.0 / Math.PI), 1, 0, 0); gl.glRotated(p.getViewAz() * (180.0 / Math.PI), 0, 1, 0); gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA); if (p.getSky() != null) { p.getSky().draw(gl, textureCache); } gl.glDepthMask(true); gl.glEnable(GLLightingFunc.GL_LIGHTING); gl.glPopMatrix(); if (rr == null) return; glu.gluLookAt(0, 0, p.getViewDistance(), 0, 0, 0, 0, 1, 0); gl.glRotated(p.getViewAlt() * (180.0 / Math.PI), 1, 0, 0); gl.glRotated(p.getViewAz() * (180.0 / Math.PI), 0, 1, 0); float[] lightPosition = new float[] { (float) Math.cos(p.getLightAlt()) * (float) Math.sin(p.getLightAz()),// (float) Math.sin(p.getLightAlt()),// (float) Math.cos(p.getLightAlt()) * (float) Math.cos(p.getLightAz()), // 0 }; gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_POSITION, lightPosition, 0); // Change to LEFT Handed coordinates gl.glScaled(1, 1, -1); gl.glFrontFace(GL.GL_CW); setupModel(gl); gl.glTranslated(dx - p.getAdvance(), 0, 0); if (p.isFlame()) { convertColor(p.getFlameColor(), color); gl.glLightfv(GLLightingFunc.GL_LIGHT2, GLLightingFunc.GL_AMBIENT, new float[] { 0, 0, 0, 1 }, 0); gl.glLightfv(GLLightingFunc.GL_LIGHT2, GLLightingFunc.GL_DIFFUSE, new float[] { color[0], color[1], color[2], 1 }, 0); gl.glLightfv(GLLightingFunc.GL_LIGHT2, GLLightingFunc.GL_SPECULAR, new float[] { color[0], color[1], color[2], 1 }, 0); Bounds b = calculateBounds(); gl.glLightf(GLLightingFunc.GL_LIGHT2, GLLightingFunc.GL_QUADRATIC_ATTENUATION, 20f); gl.glLightfv(GLLightingFunc.GL_LIGHT2, GLLightingFunc.GL_POSITION, new float[] { (float) (b.xMax + .1f), 0, 0, 1 }, 0); gl.glEnable(GLLightingFunc.GL_LIGHT2); } else { gl.glDisable(GLLightingFunc.GL_LIGHT2); gl.glLightfv(GLLightingFunc.GL_LIGHT2, GLLightingFunc.GL_DIFFUSE, new float[] { 0, 0, 0, 1 }, 0); } rr.render(drawable, configuration, new HashSet<RocketComponent>()); //Figure out the lowest stage shown final int currentStageNumber = configuration.getActiveStages()[configuration.getActiveStages().length-1]; final Stage currentStage = (Stage)configuration.getRocket().getChild(currentStageNumber); final String motorID = configuration.getFlightConfigurationID(); final Iterator<MotorMount> iterator = configuration.motorIterator(); motor: while (iterator.hasNext()) { final MotorMount mount = iterator.next(); //If this mount is not in currentStage continue on to the next one. RocketComponent parent = ((RocketComponent)mount); while ( null != (parent = parent.getParent()) ){ if ( parent instanceof Stage ){ if ( parent != currentStage ) continue motor; break; } } final Motor motor = mount.getMotorConfiguration().get(motorID).getMotor(); final double length = motor.getLength(); Coordinate[] position = ((RocketComponent) mount) .toAbsolute(new Coordinate(((RocketComponent) mount) .getLength() + mount.getMotorOverhang() - length)); for (int i = 0; i < position.length; i++) { gl.glPushMatrix(); gl.glTranslated(position[i].x + motor.getLength(), position[i].y, position[i].z); FlameRenderer.drawExhaust(gl, p, motor); gl.glPopMatrix(); } } gl.glDisable(GL.GL_BLEND); gl.glFrontFace(GL.GL_CCW); } @Override public void dispose(final GLAutoDrawable drawable) { log.trace("GL - dispose() called"); if (rr != null) rr.dispose(drawable); textureCache.dispose(drawable); } @Override public void init(final GLAutoDrawable drawable) { log.trace("GL - init()"); //drawable.setGL(new DebugGL2(drawable.getGL().getGL2())); 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 textureCache.init(drawable); // gl.glDisable(GLLightingFunc.GL_LIGHT1); FlameRenderer.init(gl); } @Override public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int w, final int h) { log.trace("GL - reshape()"); ratio = (double) w / (double) h; } @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 setupModel(final GL2 gl) { // Get the bounds final Bounds b = calculateBounds(); gl.glRotated(-p.getPitch() * (180.0 / Math.PI), 0, 0, 1); gl.glRotated(p.getYaw() * (180.0 / Math.PI), 0, 1, 0); gl.glRotated(p.getRoll() * (180.0 / Math.PI), 1, 0, 0); // Center the rocket in the view. gl.glTranslated(-b.xMin - b.xSize / 2.0, 0, 0); } }