1 package net.sf.openrocket.gui.figure3d;
\r
3 import java.awt.BorderLayout;
\r
4 import java.awt.Color;
\r
5 import java.awt.Graphics2D;
\r
6 import java.awt.Point;
\r
7 import java.awt.Rectangle;
\r
8 import java.awt.RenderingHints;
\r
9 import java.awt.event.HierarchyEvent;
\r
10 import java.awt.event.HierarchyListener;
\r
11 import java.awt.event.MouseEvent;
\r
12 import java.awt.geom.AffineTransform;
\r
13 import java.awt.image.BufferedImage;
\r
14 import java.util.ArrayList;
\r
15 import java.util.Collection;
\r
16 import java.util.HashSet;
\r
17 import java.util.Set;
\r
19 import javax.media.opengl.GL;
\r
20 import javax.media.opengl.GL2;
\r
21 import javax.media.opengl.GLAutoDrawable;
\r
22 import javax.media.opengl.GLCapabilities;
\r
23 import javax.media.opengl.GLEventListener;
\r
24 import javax.media.opengl.GLProfile;
\r
25 import javax.media.opengl.awt.GLCanvas;
\r
26 import javax.media.opengl.fixedfunc.GLLightingFunc;
\r
27 import javax.media.opengl.fixedfunc.GLMatrixFunc;
\r
28 import javax.media.opengl.glu.GLU;
\r
29 import javax.swing.JLabel;
\r
30 import javax.swing.JPanel;
\r
31 import javax.swing.JPopupMenu;
\r
32 import javax.swing.SwingUtilities;
\r
33 import javax.swing.event.MouseInputAdapter;
\r
35 import net.sf.openrocket.gui.figureelements.CGCaret;
\r
36 import net.sf.openrocket.gui.figureelements.CPCaret;
\r
37 import net.sf.openrocket.gui.figureelements.FigureElement;
\r
38 import net.sf.openrocket.logging.LogHelper;
\r
39 import net.sf.openrocket.rocketcomponent.Configuration;
\r
40 import net.sf.openrocket.rocketcomponent.RocketComponent;
\r
41 import net.sf.openrocket.startup.Application;
\r
42 import net.sf.openrocket.util.Coordinate;
\r
43 import net.sf.openrocket.util.MathUtil;
\r
45 import com.jogamp.opengl.util.awt.Overlay;
\r
48 * @author Bill Kuker <bkuker@billkuker.com>
\r
50 public class RocketFigure3d extends JPanel implements GLEventListener {
\r
51 private static final long serialVersionUID = 1L;
\r
52 private static final LogHelper log = Application.getLogger();
\r
55 //this allows the GL canvas and things like the motor selection
\r
56 //drop down to z-order themselves.
\r
57 JPopupMenu.setDefaultLightWeightPopupEnabled(false);
\r
60 private static final double fovY = 15.0;
\r
61 private static double fovX = Double.NaN;
\r
62 private static final int CARET_SIZE = 20;
\r
64 private Configuration configuration;
\r
65 private GLCanvas canvas;
\r
69 private Overlay extrasOverlay, caretOverlay;
\r
70 private BufferedImage cgCaretRaster, cpCaretRaster;
\r
71 private volatile boolean redrawExtras = true;
\r
73 private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>();
\r
74 private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>();
\r
76 private double roll = 0;
\r
77 private double yaw = 0;
\r
79 Point pickPoint = null;
\r
80 MouseEvent pickEvent;
\r
82 float[] lightPosition = new float[] { 1, 4, 1, 0 };
\r
84 RocketRenderer rr = new RocketRenderer();
\r
86 public RocketFigure3d(Configuration config) {
\r
87 this.configuration = config;
\r
88 this.setLayout(new BorderLayout());
\r
91 * This achieves late GL initialization, so that if there is a crash
\r
92 * it does not occur until user selects 3D view, so as to not render
\r
93 * the application unusable.
\r
95 addHierarchyListener(new HierarchyListener() {
\r
97 public void hierarchyChanged(HierarchyEvent e) {
\r
98 log.verbose("GL - Calling initGLCanvas on hierarchy change");
\r
100 RocketFigure3d.this.removeHierarchyListener(this);
\r
105 private void initGLCanvas(){
\r
106 log.debug("Initializing RocketFigure3D OpenGL Canvas");
\r
108 log.debug("Setting up GL capabilities...");
\r
110 log.verbose("GL - Getting Default Profile");
\r
111 GLProfile glp = GLProfile.getDefault();
\r
113 log.verbose("GL - creating GLCapabilities");
\r
114 GLCapabilities caps = new GLCapabilities(glp);
\r
116 log.verbose("GL - setSampleBuffers");
\r
117 caps.setSampleBuffers(true);
\r
119 log.verbose("GL - setNumSamples");
\r
120 caps.setNumSamples(6);
\r
122 log.verbose("GL - setStencilBits");
\r
123 caps.setStencilBits(1);
\r
125 log.verbose("GL - Creating Canvas");
\r
126 canvas = new GLCanvas(caps);
\r
128 log.verbose("GL - Registering as GLEventListener on canvas");
\r
129 canvas.addGLEventListener(this);
\r
131 log.verbose("GL - Adding canvas to this JPanel");
\r
132 this.add(canvas, BorderLayout.CENTER);
\r
134 log.verbose("GL - Setting up mouse listeners");
\r
135 setupMouseListeners();
\r
137 log.verbose("GL - Rasterizine Carets"); //reticulating splines?
\r
140 } catch (Throwable t) {
\r
141 log.error("An error occurred creating 3d View", t);
\r
143 this.add(new JLabel("Unable to load 3d Libraries: "
\r
144 + t.getMessage()));
\r
149 * Set up the standard rendering hints on the Graphics2D
\r
151 private static void setRenderingHints(Graphics2D g){
\r
152 g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
\r
153 RenderingHints.VALUE_STROKE_NORMALIZE);
\r
154 g.setRenderingHint(RenderingHints.KEY_RENDERING,
\r
155 RenderingHints.VALUE_RENDER_QUALITY);
\r
156 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
\r
157 RenderingHints.VALUE_ANTIALIAS_ON);
\r
161 * Rasterize the carets into 2 buffered images that I can blit onto the
\r
162 * 3d display every redraw without all of the caret shape rendering overhead
\r
164 private void rasterizeCarets(){
\r
167 //Rasterize a CG Caret
\r
168 cgCaretRaster = new BufferedImage(CARET_SIZE, CARET_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
\r
169 g2d = cgCaretRaster.createGraphics();
\r
170 setRenderingHints(g2d);
\r
172 g2d.setBackground(new Color(0, 0, 0, 0));
\r
173 g2d.clearRect(0, 0, CARET_SIZE, CARET_SIZE);
\r
175 new CGCaret(CARET_SIZE/2,CARET_SIZE/2).paint(g2d, 1.0);
\r
179 //Rasterize a CP Caret
\r
180 cpCaretRaster = new BufferedImage(CARET_SIZE, CARET_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
\r
181 g2d = cpCaretRaster.createGraphics();
\r
182 setRenderingHints(g2d);
\r
184 g2d.setBackground(new Color(0, 0, 0, 0));
\r
185 g2d.clearRect(0, 0, CARET_SIZE, CARET_SIZE);
\r
187 new CPCaret(CARET_SIZE/2,CARET_SIZE/2).paint(g2d, 1.0);
\r
193 private void setupMouseListeners() {
\r
194 MouseInputAdapter a = new MouseInputAdapter() {
\r
197 MouseEvent pressEvent;
\r
200 public void mousePressed(MouseEvent e) {
\r
207 public void mouseClicked(MouseEvent e) {
\r
208 pickPoint = new Point(lastX, canvas.getHeight() - lastY);
\r
214 public void mouseDragged(MouseEvent e) {
\r
215 int dx = lastX - e.getX();
\r
216 int dy = lastY - e.getY();
\r
220 if (pressEvent.getButton() == MouseEvent.BUTTON1) {
\r
221 if (Math.abs(dx) > Math.abs(dy)) {
\r
222 setYaw(yaw - (float) dx / 100.0);
\r
224 if ( yaw > Math.PI/2.0 && yaw < 3.0*Math.PI/2.0 ){
\r
227 setRoll(roll - (float) dy / 100.0);
\r
230 lightPosition[0] -= 0.1f * dx;
\r
231 lightPosition[1] += 0.1f * dy;
\r
236 canvas.addMouseMotionListener(a);
\r
237 canvas.addMouseListener(a);
\r
240 public void setConfiguration(Configuration configuration) {
\r
241 this.configuration = configuration;
\r
246 public void display(GLAutoDrawable drawable) {
\r
247 GL2 gl = drawable.getGL().getGL2();
\r
248 GLU glu = new GLU();
\r
250 gl.glEnable(GL.GL_MULTISAMPLE);
\r
252 gl.glClearColor(1, 1, 1, 1);
\r
253 gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
\r
255 setupView(gl, glu);
\r
257 if (pickPoint != null) {
\r
258 gl.glDisable(GLLightingFunc.GL_LIGHTING);
\r
259 final RocketComponent picked = rr.pick(drawable, configuration,
\r
260 pickPoint, pickEvent.isShiftDown()?selection:null );
\r
261 if (csl != null && picked != null) {
\r
262 final MouseEvent e = pickEvent;
\r
263 SwingUtilities.invokeLater(new Runnable() {
\r
265 public void run() {
\r
266 csl.componentClicked(new RocketComponent[] { picked },
\r
274 gl.glClearColor(1, 1, 1, 1);
\r
275 gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
\r
277 gl.glEnable(GLLightingFunc.GL_LIGHTING);
\r
279 rr.render(drawable, configuration, selection);
\r
281 drawExtras(gl, glu);
\r
282 drawCarets(gl, glu);
\r
286 private void drawCarets(GL2 gl, GLU glu) {
\r
287 final Graphics2D og2d = caretOverlay.createGraphics();
\r
288 setRenderingHints(og2d);
\r
290 og2d.setBackground(new Color(0, 0, 0, 0));
\r
291 og2d.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
\r
292 caretOverlay.markDirty(0, 0, canvas.getWidth(), canvas.getHeight());
\r
294 // The existing relative Extras don't really work right for 3d.
\r
295 Coordinate pCP = project(cp, gl, glu);
\r
296 Coordinate pCG = project(cg, gl, glu);
\r
298 final int d = CARET_SIZE/2;
\r
300 //z order the carets
\r
301 if (pCG.z < pCP.z) {
\r
302 //Subtract half of the caret size, so they are centered ( The +/- d in each translate)
\r
303 //Flip the sense of the Y coordinate from GL to normal (Y+ up/down)
\r
304 og2d.drawRenderedImage(
\r
306 AffineTransform.getTranslateInstance((pCP.x - d),
\r
307 canvas.getHeight() - (pCP.y + d)));
\r
308 og2d.drawRenderedImage(
\r
310 AffineTransform.getTranslateInstance((pCG.x - d),
\r
311 canvas.getHeight() - (pCG.y + d)));
\r
313 og2d.drawRenderedImage(
\r
315 AffineTransform.getTranslateInstance((pCG.x - d),
\r
316 canvas.getHeight() - (pCG.y + d)));
\r
317 og2d.drawRenderedImage(
\r
319 AffineTransform.getTranslateInstance((pCP.x - d),
\r
320 canvas.getHeight() - (pCP.y + d)));
\r
324 gl.glEnable(GL.GL_BLEND);
\r
325 caretOverlay.drawAll();
\r
326 gl.glDisable(GL.GL_BLEND);
\r
330 * Draw the extras overlay to the gl canvas.
\r
331 * Re-blits the overlay every frame. Only re-renders the overlay
\r
334 private void drawExtras(GL2 gl, GLU glu){
\r
335 //Only re-render if needed
\r
336 // redrawExtras: Some external change (new simulation data) means
\r
337 // the data is out of date.
\r
338 // extrasOverlay.contentsLost(): For some reason the buffer with this
\r
340 if ( redrawExtras || extrasOverlay.contentsLost() ){
\r
341 log.debug("Redrawing Overlay");
\r
343 final Graphics2D og2d = extrasOverlay.createGraphics();
\r
344 setRenderingHints(og2d);
\r
346 og2d.setBackground(new Color(0, 0, 0, 0));
\r
347 og2d.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
\r
348 extrasOverlay.markDirty(0, 0, canvas.getWidth(), canvas.getHeight());
\r
350 for (FigureElement e : relativeExtra) {
\r
353 Rectangle rect = this.getVisibleRect();
\r
355 for (FigureElement e : absoluteExtra) {
\r
356 e.paint(og2d, 1.0, rect);
\r
360 redrawExtras = false;
\r
363 //Re-blit to gl canvas every time
\r
364 gl.glEnable(GL.GL_BLEND);
\r
365 extrasOverlay.drawAll();
\r
366 gl.glDisable(GL.GL_BLEND);
\r
370 public void dispose(GLAutoDrawable drawable) {
\r
371 log.verbose("GL - dispose() called");
\r
375 public void init(GLAutoDrawable drawable) {
\r
376 log.verbose("GL - init() called");
\r
379 GL2 gl = drawable.getGL().getGL2();
\r
380 gl.glClearDepth(1.0f); // clear z-buffer to the farthest
\r
382 gl.glDepthFunc(GL.GL_LEQUAL); // the type of depth test to do
\r
386 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_AMBIENT,
\r
387 new float[] { amb, amb, amb, 1 }, 0);
\r
388 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_DIFFUSE,
\r
389 new float[] { dif, dif, dif, 1 }, 0);
\r
390 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_SPECULAR,
\r
391 new float[] { dif, dif, dif, 1 }, 0);
\r
393 gl.glEnable(GLLightingFunc.GL_LIGHT1);
\r
394 gl.glEnable(GLLightingFunc.GL_LIGHTING);
\r
395 gl.glShadeModel(GLLightingFunc.GL_SMOOTH);
\r
397 gl.glEnable(GLLightingFunc.GL_NORMALIZE);
\r
399 extrasOverlay = new Overlay(drawable);
\r
400 caretOverlay = new Overlay(drawable);
\r
402 log.verbose("GL - init() complete");
\r
406 public void reshape(GLAutoDrawable drawable, int x, int y, int w, int h) {
\r
407 log.verbose("GL - reshape() called");
\r
408 GL2 gl = drawable.getGL().getGL2();
\r
409 GLU glu = new GLU();
\r
411 double ratio = (double) w / (double) h;
\r
412 fovX = fovY * ratio;
\r
414 gl.glMatrixMode(GLMatrixFunc.GL_PROJECTION);
\r
415 gl.glLoadIdentity();
\r
416 glu.gluPerspective(fovY, ratio, 0.05f, 100f);
\r
417 gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
\r
419 redrawExtras = true;
\r
420 log.verbose("GL - reshape() complete");
\r
423 @SuppressWarnings("unused")
\r
424 private static class Bounds {
\r
425 double xMin, xMax, xSize;
\r
426 double yMin, yMax, ySize;
\r
427 double zMin, zMax, zSize;
\r
432 * Calculates the bounds for the current configuration
\r
436 private Bounds calculateBounds() {
\r
437 Bounds ret = new Bounds();
\r
438 Collection<Coordinate> bounds = configuration.getBounds();
\r
439 for (Coordinate c : bounds) {
\r
440 ret.xMax = Math.max(ret.xMax, c.x);
\r
441 ret.xMin = Math.min(ret.xMin, c.x);
\r
443 ret.yMax = Math.max(ret.yMax, c.y);
\r
444 ret.yMin = Math.min(ret.yMin, c.y);
\r
446 ret.zMax = Math.max(ret.zMax, c.z);
\r
447 ret.zMin = Math.min(ret.zMin, c.z);
\r
449 double r = MathUtil.hypot(c.y, c.z);
\r
450 ret.rMax = Math.max(ret.rMax, r);
\r
452 ret.xSize = ret.xMax - ret.xMin;
\r
453 ret.ySize = ret.yMax - ret.yMin;
\r
454 ret.zSize = ret.zMax - ret.zMin;
\r
458 private void setupView(GL2 gl, GLU glu) {
\r
459 log.verbose("GL - setupView() called");
\r
460 gl.glLoadIdentity();
\r
462 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_POSITION,
\r
466 Bounds b = calculateBounds();
\r
468 // Calculate the distance needed to fit the bounds in both the X and Y
\r
470 // Add 10% for space around it.
\r
471 double dX = (b.xSize * 1.2 / 2.0)
\r
472 / Math.tan(Math.toRadians(fovX / 2.0));
\r
473 double dY = (b.rMax * 2.0 * 1.2 / 2.0)
\r
474 / Math.tan(Math.toRadians(fovY / 2.0));
\r
476 // Move back the greater of the 2 distances
\r
477 glu.gluLookAt(0, 0, Math.max(dX, dY), 0, 0, 0, 0, 1, 0);
\r
479 gl.glRotated(yaw * (180.0 / Math.PI), 0, 1, 0);
\r
480 gl.glRotated(roll * (180.0 / Math.PI), 1, 0, 0);
\r
482 // Center the rocket in the view.
\r
483 gl.glTranslated(-b.xMin - b.xSize / 2.0, 0, 0);
\r
485 //Change to LEFT Handed coordinates
\r
486 gl.glScaled(1, 1, -1);
\r
487 gl.glFrontFace(GL.GL_CW);
\r
489 //Flip textures for LEFT handed coords
\r
490 gl.glMatrixMode(GL.GL_TEXTURE);
\r
491 gl.glLoadIdentity();
\r
492 gl.glScaled(-1,1,1);
\r
493 gl.glTranslated(-1,0,0);
\r
494 gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
\r
496 log.verbose("GL - setupView() complete");
\r
500 * Call when the rocket has changed
\r
502 public void updateFigure() {
\r
503 log.debug("3D Figure Updated");
\r
508 private void internalRepaint(){
\r
509 log.verbose("GL - internalRepaint() called");
\r
511 if (canvas != null)
\r
513 log.verbose("GL - internalRepaint() complete");
\r
517 public void repaint() {
\r
518 log.verbose("GL - repaint() called");
\r
519 redrawExtras = true;
\r
521 log.verbose("GL - repaint() complete");
\r
524 private Set<RocketComponent> selection = new HashSet<RocketComponent>();
\r
526 public void setSelection(RocketComponent[] selection) {
\r
527 this.selection.clear();
\r
528 if (selection != null) {
\r
529 for (RocketComponent c : selection)
\r
530 this.selection.add(c);
\r
535 private void setRoll(double rot) {
\r
536 if (MathUtil.equals(roll, rot))
\r
538 this.roll = MathUtil.reduce360(rot);
\r
542 private void setYaw(double rot) {
\r
543 if (MathUtil.equals(yaw, rot))
\r
545 this.yaw = MathUtil.reduce360(rot);
\r
549 // ///////////// Extra methods
\r
551 private Coordinate project(Coordinate c, GL2 gl, GLU glu) {
\r
552 log.verbose("GL - project() called");
\r
553 double[] mvmatrix = new double[16];
\r
554 double[] projmatrix = new double[16];
\r
555 int[] viewport = new int[4];
\r
557 gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);
\r
558 gl.glGetDoublev(GLMatrixFunc.GL_MODELVIEW_MATRIX, mvmatrix, 0);
\r
559 gl.glGetDoublev(GLMatrixFunc.GL_PROJECTION_MATRIX, projmatrix, 0);
\r
561 double out[] = new double[4];
\r
562 glu.gluProject(c.x, c.y, c.z, mvmatrix, 0, projmatrix, 0, viewport, 0,
\r
565 log.verbose("GL - peoject() complete");
\r
566 return new Coordinate(out[0], out[1], out[2]);
\r
570 private Coordinate cp = new Coordinate(0, 0, 0);
\r
571 private Coordinate cg = new Coordinate(0, 0, 0);
\r
573 public void setCG(Coordinate cg) {
\r
575 redrawExtras = true;
\r
578 public void setCP(Coordinate cp) {
\r
580 redrawExtras = true;
\r
583 public void addRelativeExtra(FigureElement p) {
\r
584 relativeExtra.add(p);
\r
585 redrawExtras = true;
\r
588 public void removeRelativeExtra(FigureElement p) {
\r
589 relativeExtra.remove(p);
\r
590 redrawExtras = true;
\r
593 public void clearRelativeExtra() {
\r
594 relativeExtra.clear();
\r
595 redrawExtras = true;
\r
598 public void addAbsoluteExtra(FigureElement p) {
\r
599 absoluteExtra.add(p);
\r
600 redrawExtras = true;
\r
603 public void removeAbsoluteExtra(FigureElement p) {
\r
604 absoluteExtra.remove(p);
\r
605 redrawExtras = true;
\r
608 public void clearAbsoluteExtra() {
\r
609 absoluteExtra.clear();
\r
610 redrawExtras = true;
\r
613 private ComponentSelectionListener csl;
\r
615 public static interface ComponentSelectionListener {
\r
616 public void componentClicked(RocketComponent[] components, MouseEvent e);
\r
619 public void addComponentSelectionListener(
\r
620 ComponentSelectionListener newListener) {
\r
621 this.csl = newListener;
\r