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.SplashScreen;
\r
10 import java.awt.event.MouseEvent;
\r
11 import java.awt.geom.AffineTransform;
\r
12 import java.awt.image.BufferedImage;
\r
13 import java.util.ArrayList;
\r
14 import java.util.Collection;
\r
15 import java.util.HashSet;
\r
16 import java.util.Set;
\r
18 import javax.media.opengl.GL;
\r
19 import javax.media.opengl.GL2;
\r
20 import javax.media.opengl.GLAutoDrawable;
\r
21 import javax.media.opengl.GLCapabilities;
\r
22 import javax.media.opengl.GLEventListener;
\r
23 import javax.media.opengl.GLProfile;
\r
24 import javax.media.opengl.awt.GLCanvas;
\r
25 import javax.media.opengl.fixedfunc.GLLightingFunc;
\r
26 import javax.media.opengl.fixedfunc.GLMatrixFunc;
\r
27 import javax.media.opengl.glu.GLU;
\r
28 import javax.swing.JLabel;
\r
29 import javax.swing.JPanel;
\r
30 import javax.swing.JPopupMenu;
\r
31 import javax.swing.SwingUtilities;
\r
32 import javax.swing.event.MouseInputAdapter;
\r
34 import net.sf.openrocket.gui.figureelements.CGCaret;
\r
35 import net.sf.openrocket.gui.figureelements.CPCaret;
\r
36 import net.sf.openrocket.gui.figureelements.FigureElement;
\r
37 import net.sf.openrocket.logging.LogHelper;
\r
38 import net.sf.openrocket.rocketcomponent.Configuration;
\r
39 import net.sf.openrocket.rocketcomponent.RocketComponent;
\r
40 import net.sf.openrocket.startup.Application;
\r
41 import net.sf.openrocket.util.Coordinate;
\r
42 import net.sf.openrocket.util.MathUtil;
\r
44 import com.jogamp.opengl.util.awt.Overlay;
\r
47 * @author Bill Kuker <bkuker@billkuker.com>
\r
49 public class RocketFigure3d extends JPanel implements GLEventListener {
\r
50 private static final long serialVersionUID = 1L;
\r
51 private static final LogHelper log = Application.getLogger();
\r
54 //this allows the GL canvas and things like the motor selection
\r
55 //drop down to z-order themselves.
\r
56 JPopupMenu.setDefaultLightWeightPopupEnabled(false);
\r
59 private static final double fovY = 15.0;
\r
60 private static double fovX = Double.NaN;
\r
61 private static final int CARET_SIZE = 20;
\r
63 private Configuration configuration;
\r
64 private GLCanvas canvas;
\r
68 private Overlay extrasOverlay, caretOverlay;
\r
69 private BufferedImage cgCaretRaster, cpCaretRaster;
\r
70 private volatile boolean redrawExtras = true;
\r
72 private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>();
\r
73 private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>();
\r
75 private double roll = 0;
\r
76 private double yaw = 0;
\r
78 Point pickPoint = null;
\r
79 MouseEvent pickEvent;
\r
81 float[] lightPosition = new float[] { 1, 4, 1, 0 };
\r
83 RocketRenderer rr = new RocketRenderer();
\r
85 public RocketFigure3d(Configuration config) {
\r
86 this.configuration = config;
\r
87 this.setLayout(new BorderLayout());
\r
89 //Only initizlize GL if 3d is enabled.
\r
90 if ( is3dEnabled() ){
\r
91 //Fixes a linux / X bug: Splash must be closed before GL Init
\r
92 SplashScreen splash = SplashScreen.getSplashScreen();
\r
93 if ( splash != null )
\r
101 * Return true if 3d view is enabled. This may be toggled by the user at
\r
105 public static boolean is3dEnabled(){
\r
106 return System.getProperty("openrocket.3d.disable") == null;
\r
109 private void initGLCanvas(){
\r
110 log.debug("Initializing RocketFigure3D OpenGL Canvas");
\r
112 log.debug("Setting up GL capabilities...");
\r
114 log.verbose("GL - Getting Default Profile");
\r
115 GLProfile glp = GLProfile.getDefault();
\r
117 log.verbose("GL - creating GLCapabilities");
\r
118 GLCapabilities caps = new GLCapabilities(glp);
\r
120 log.verbose("GL - setSampleBuffers");
\r
121 caps.setSampleBuffers(true);
\r
123 log.verbose("GL - setNumSamples");
\r
124 caps.setNumSamples(6);
\r
126 log.verbose("GL - setStencilBits");
\r
127 caps.setStencilBits(1);
\r
129 log.verbose("GL - Creating Canvas");
\r
130 canvas = new GLCanvas(caps);
\r
132 log.verbose("GL - Registering as GLEventListener on canvas");
\r
133 canvas.addGLEventListener(this);
\r
135 log.verbose("GL - Adding canvas to this JPanel");
\r
136 this.add(canvas, BorderLayout.CENTER);
\r
138 log.verbose("GL - Setting up mouse listeners");
\r
139 setupMouseListeners();
\r
141 log.verbose("GL - Rasterizine Carets"); //reticulating splines?
\r
144 } catch (Throwable t) {
\r
145 log.error("An error occurred creating 3d View", t);
\r
147 this.add(new JLabel("Unable to load 3d Libraries: "
\r
148 + t.getMessage()));
\r
153 * Set up the standard rendering hints on the Graphics2D
\r
155 private static void setRenderingHints(Graphics2D g){
\r
156 g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
\r
157 RenderingHints.VALUE_STROKE_NORMALIZE);
\r
158 g.setRenderingHint(RenderingHints.KEY_RENDERING,
\r
159 RenderingHints.VALUE_RENDER_QUALITY);
\r
160 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
\r
161 RenderingHints.VALUE_ANTIALIAS_ON);
\r
165 * Rasterize the carets into 2 buffered images that I can blit onto the
\r
166 * 3d display every redraw without all of the caret shape rendering overhead
\r
168 private void rasterizeCarets(){
\r
171 //Rasterize a CG Caret
\r
172 cgCaretRaster = new BufferedImage(CARET_SIZE, CARET_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
\r
173 g2d = cgCaretRaster.createGraphics();
\r
174 setRenderingHints(g2d);
\r
176 g2d.setBackground(new Color(0, 0, 0, 0));
\r
177 g2d.clearRect(0, 0, CARET_SIZE, CARET_SIZE);
\r
179 new CGCaret(CARET_SIZE/2,CARET_SIZE/2).paint(g2d, 1.0);
\r
183 //Rasterize a CP Caret
\r
184 cpCaretRaster = new BufferedImage(CARET_SIZE, CARET_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
\r
185 g2d = cpCaretRaster.createGraphics();
\r
186 setRenderingHints(g2d);
\r
188 g2d.setBackground(new Color(0, 0, 0, 0));
\r
189 g2d.clearRect(0, 0, CARET_SIZE, CARET_SIZE);
\r
191 new CPCaret(CARET_SIZE/2,CARET_SIZE/2).paint(g2d, 1.0);
\r
197 private void setupMouseListeners() {
\r
198 MouseInputAdapter a = new MouseInputAdapter() {
\r
201 MouseEvent pressEvent;
\r
204 public void mousePressed(MouseEvent e) {
\r
211 public void mouseClicked(MouseEvent e) {
\r
212 pickPoint = new Point(lastX, canvas.getHeight() - lastY);
\r
218 public void mouseDragged(MouseEvent e) {
\r
219 int dx = lastX - e.getX();
\r
220 int dy = lastY - e.getY();
\r
224 if (pressEvent.getButton() == MouseEvent.BUTTON1) {
\r
225 if (Math.abs(dx) > Math.abs(dy)) {
\r
226 setYaw(yaw - (float) dx / 100.0);
\r
228 if ( yaw > Math.PI/2.0 && yaw < 3.0*Math.PI/2.0 ){
\r
231 setRoll(roll - (float) dy / 100.0);
\r
234 lightPosition[0] -= 0.1f * dx;
\r
235 lightPosition[1] += 0.1f * dy;
\r
240 canvas.addMouseMotionListener(a);
\r
241 canvas.addMouseListener(a);
\r
244 public void setConfiguration(Configuration configuration) {
\r
245 this.configuration = configuration;
\r
250 public void display(GLAutoDrawable drawable) {
\r
251 GL2 gl = drawable.getGL().getGL2();
\r
252 GLU glu = new GLU();
\r
254 gl.glEnable(GL.GL_MULTISAMPLE);
\r
256 gl.glClearColor(1, 1, 1, 1);
\r
257 gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
\r
259 setupView(gl, glu);
\r
261 if (pickPoint != null) {
\r
262 gl.glDisable(GLLightingFunc.GL_LIGHTING);
\r
263 final RocketComponent picked = rr.pick(drawable, configuration,
\r
264 pickPoint, pickEvent.isShiftDown()?selection:null );
\r
265 if (csl != null && picked != null) {
\r
266 final MouseEvent e = pickEvent;
\r
267 SwingUtilities.invokeLater(new Runnable() {
\r
269 public void run() {
\r
270 csl.componentClicked(new RocketComponent[] { picked },
\r
278 gl.glClearColor(1, 1, 1, 1);
\r
279 gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
\r
281 gl.glEnable(GLLightingFunc.GL_LIGHTING);
\r
283 rr.render(drawable, configuration, selection);
\r
285 drawExtras(gl, glu);
\r
286 drawCarets(gl, glu);
\r
290 private void drawCarets(GL2 gl, GLU glu) {
\r
291 final Graphics2D og2d = caretOverlay.createGraphics();
\r
292 setRenderingHints(og2d);
\r
294 og2d.setBackground(new Color(0, 0, 0, 0));
\r
295 og2d.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
\r
296 caretOverlay.markDirty(0, 0, canvas.getWidth(), canvas.getHeight());
\r
298 // The existing relative Extras don't really work right for 3d.
\r
299 Coordinate pCP = project(cp, gl, glu);
\r
300 Coordinate pCG = project(cg, gl, glu);
\r
302 final int d = CARET_SIZE/2;
\r
304 //z order the carets
\r
305 if (pCG.z < pCP.z) {
\r
306 //Subtract half of the caret size, so they are centered ( The +/- d in each translate)
\r
307 //Flip the sense of the Y coordinate from GL to normal (Y+ up/down)
\r
308 og2d.drawRenderedImage(
\r
310 AffineTransform.getTranslateInstance((pCP.x - d),
\r
311 canvas.getHeight() - (pCP.y + d)));
\r
312 og2d.drawRenderedImage(
\r
314 AffineTransform.getTranslateInstance((pCG.x - d),
\r
315 canvas.getHeight() - (pCG.y + d)));
\r
317 og2d.drawRenderedImage(
\r
319 AffineTransform.getTranslateInstance((pCG.x - d),
\r
320 canvas.getHeight() - (pCG.y + d)));
\r
321 og2d.drawRenderedImage(
\r
323 AffineTransform.getTranslateInstance((pCP.x - d),
\r
324 canvas.getHeight() - (pCP.y + d)));
\r
328 gl.glEnable(GL.GL_BLEND);
\r
329 caretOverlay.drawAll();
\r
330 gl.glDisable(GL.GL_BLEND);
\r
334 * Draw the extras overlay to the gl canvas.
\r
335 * Re-blits the overlay every frame. Only re-renders the overlay
\r
338 private void drawExtras(GL2 gl, GLU glu){
\r
339 //Only re-render if needed
\r
340 // redrawExtras: Some external change (new simulation data) means
\r
341 // the data is out of date.
\r
342 // extrasOverlay.contentsLost(): For some reason the buffer with this
\r
344 if ( redrawExtras || extrasOverlay.contentsLost() ){
\r
345 log.debug("Redrawing Overlay");
\r
347 final Graphics2D og2d = extrasOverlay.createGraphics();
\r
348 setRenderingHints(og2d);
\r
350 og2d.setBackground(new Color(0, 0, 0, 0));
\r
351 og2d.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
\r
352 extrasOverlay.markDirty(0, 0, canvas.getWidth(), canvas.getHeight());
\r
354 for (FigureElement e : relativeExtra) {
\r
357 Rectangle rect = this.getVisibleRect();
\r
359 for (FigureElement e : absoluteExtra) {
\r
360 e.paint(og2d, 1.0, rect);
\r
364 redrawExtras = false;
\r
367 //Re-blit to gl canvas every time
\r
368 gl.glEnable(GL.GL_BLEND);
\r
369 extrasOverlay.drawAll();
\r
370 gl.glDisable(GL.GL_BLEND);
\r
374 public void dispose(GLAutoDrawable drawable) {
\r
375 log.verbose("GL - dispose() called");
\r
379 public void init(GLAutoDrawable drawable) {
\r
380 log.verbose("GL - init() called");
\r
383 GL2 gl = drawable.getGL().getGL2();
\r
384 gl.glClearDepth(1.0f); // clear z-buffer to the farthest
\r
386 gl.glDepthFunc(GL.GL_LEQUAL); // the type of depth test to do
\r
390 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_AMBIENT,
\r
391 new float[] { amb, amb, amb, 1 }, 0);
\r
392 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_DIFFUSE,
\r
393 new float[] { dif, dif, dif, 1 }, 0);
\r
394 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_SPECULAR,
\r
395 new float[] { dif, dif, dif, 1 }, 0);
\r
397 gl.glEnable(GLLightingFunc.GL_LIGHT1);
\r
398 gl.glEnable(GLLightingFunc.GL_LIGHTING);
\r
399 gl.glShadeModel(GLLightingFunc.GL_SMOOTH);
\r
401 gl.glEnable(GLLightingFunc.GL_NORMALIZE);
\r
403 extrasOverlay = new Overlay(drawable);
\r
404 caretOverlay = new Overlay(drawable);
\r
406 log.verbose("GL - init() complete");
\r
410 public void reshape(GLAutoDrawable drawable, int x, int y, int w, int h) {
\r
411 log.verbose("GL - reshape() called");
\r
412 GL2 gl = drawable.getGL().getGL2();
\r
413 GLU glu = new GLU();
\r
415 double ratio = (double) w / (double) h;
\r
416 fovX = fovY * ratio;
\r
418 gl.glMatrixMode(GLMatrixFunc.GL_PROJECTION);
\r
419 gl.glLoadIdentity();
\r
420 glu.gluPerspective(fovY, ratio, 0.05f, 100f);
\r
421 gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
\r
423 redrawExtras = true;
\r
424 log.verbose("GL - reshape() complete");
\r
427 @SuppressWarnings("unused")
\r
428 private static class Bounds {
\r
429 double xMin, xMax, xSize;
\r
430 double yMin, yMax, ySize;
\r
431 double zMin, zMax, zSize;
\r
436 * Calculates the bounds for the current configuration
\r
440 private Bounds calculateBounds() {
\r
441 Bounds ret = new Bounds();
\r
442 Collection<Coordinate> bounds = configuration.getBounds();
\r
443 for (Coordinate c : bounds) {
\r
444 ret.xMax = Math.max(ret.xMax, c.x);
\r
445 ret.xMin = Math.min(ret.xMin, c.x);
\r
447 ret.yMax = Math.max(ret.yMax, c.y);
\r
448 ret.yMin = Math.min(ret.yMin, c.y);
\r
450 ret.zMax = Math.max(ret.zMax, c.z);
\r
451 ret.zMin = Math.min(ret.zMin, c.z);
\r
453 double r = MathUtil.hypot(c.y, c.z);
\r
454 ret.rMax = Math.max(ret.rMax, r);
\r
456 ret.xSize = ret.xMax - ret.xMin;
\r
457 ret.ySize = ret.yMax - ret.yMin;
\r
458 ret.zSize = ret.zMax - ret.zMin;
\r
462 private void setupView(GL2 gl, GLU glu) {
\r
463 log.verbose("GL - setupView() called");
\r
464 gl.glLoadIdentity();
\r
466 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_POSITION,
\r
470 Bounds b = calculateBounds();
\r
472 // Calculate the distance needed to fit the bounds in both the X and Y
\r
474 // Add 10% for space around it.
\r
475 double dX = (b.xSize * 1.2 / 2.0)
\r
476 / Math.tan(Math.toRadians(fovX / 2.0));
\r
477 double dY = (b.rMax * 2.0 * 1.2 / 2.0)
\r
478 / Math.tan(Math.toRadians(fovY / 2.0));
\r
480 // Move back the greater of the 2 distances
\r
481 glu.gluLookAt(0, 0, Math.max(dX, dY), 0, 0, 0, 0, 1, 0);
\r
483 gl.glRotated(yaw * (180.0 / Math.PI), 0, 1, 0);
\r
484 gl.glRotated(roll * (180.0 / Math.PI), 1, 0, 0);
\r
486 // Center the rocket in the view.
\r
487 gl.glTranslated(-b.xMin - b.xSize / 2.0, 0, 0);
\r
489 //Change to LEFT Handed coordinates
\r
490 gl.glScaled(1, 1, -1);
\r
491 gl.glFrontFace(GL.GL_CW);
\r
493 //Flip textures for LEFT handed coords
\r
494 gl.glMatrixMode(GL.GL_TEXTURE);
\r
495 gl.glLoadIdentity();
\r
496 gl.glScaled(-1,1,1);
\r
497 gl.glTranslated(-1,0,0);
\r
498 gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
\r
500 log.verbose("GL - setupView() complete");
\r
504 * Call when the rocket has changed
\r
506 public void updateFigure() {
\r
507 log.debug("3D Figure Updated");
\r
512 private void internalRepaint(){
\r
513 log.verbose("GL - internalRepaint() called");
\r
515 if (canvas != null)
\r
517 log.verbose("GL - internalRepaint() complete");
\r
521 public void repaint() {
\r
522 log.verbose("GL - repaint() called");
\r
523 redrawExtras = true;
\r
525 log.verbose("GL - repaint() complete");
\r
528 private Set<RocketComponent> selection = new HashSet<RocketComponent>();
\r
530 public void setSelection(RocketComponent[] selection) {
\r
531 this.selection.clear();
\r
532 if (selection != null) {
\r
533 for (RocketComponent c : selection)
\r
534 this.selection.add(c);
\r
539 private void setRoll(double rot) {
\r
540 if (MathUtil.equals(roll, rot))
\r
542 this.roll = MathUtil.reduce360(rot);
\r
546 private void setYaw(double rot) {
\r
547 if (MathUtil.equals(yaw, rot))
\r
549 this.yaw = MathUtil.reduce360(rot);
\r
553 // ///////////// Extra methods
\r
555 private Coordinate project(Coordinate c, GL2 gl, GLU glu) {
\r
556 log.verbose("GL - project() called");
\r
557 double[] mvmatrix = new double[16];
\r
558 double[] projmatrix = new double[16];
\r
559 int[] viewport = new int[4];
\r
561 gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);
\r
562 gl.glGetDoublev(GLMatrixFunc.GL_MODELVIEW_MATRIX, mvmatrix, 0);
\r
563 gl.glGetDoublev(GLMatrixFunc.GL_PROJECTION_MATRIX, projmatrix, 0);
\r
565 double out[] = new double[4];
\r
566 glu.gluProject(c.x, c.y, c.z, mvmatrix, 0, projmatrix, 0, viewport, 0,
\r
569 log.verbose("GL - peoject() complete");
\r
570 return new Coordinate(out[0], out[1], out[2]);
\r
574 private Coordinate cp = new Coordinate(0, 0, 0);
\r
575 private Coordinate cg = new Coordinate(0, 0, 0);
\r
577 public void setCG(Coordinate cg) {
\r
579 redrawExtras = true;
\r
582 public void setCP(Coordinate cp) {
\r
584 redrawExtras = true;
\r
587 public void addRelativeExtra(FigureElement p) {
\r
588 relativeExtra.add(p);
\r
589 redrawExtras = true;
\r
592 public void removeRelativeExtra(FigureElement p) {
\r
593 relativeExtra.remove(p);
\r
594 redrawExtras = true;
\r
597 public void clearRelativeExtra() {
\r
598 relativeExtra.clear();
\r
599 redrawExtras = true;
\r
602 public void addAbsoluteExtra(FigureElement p) {
\r
603 absoluteExtra.add(p);
\r
604 redrawExtras = true;
\r
607 public void removeAbsoluteExtra(FigureElement p) {
\r
608 absoluteExtra.remove(p);
\r
609 redrawExtras = true;
\r
612 public void clearAbsoluteExtra() {
\r
613 absoluteExtra.clear();
\r
614 redrawExtras = true;
\r
617 private ComponentSelectionListener csl;
\r
619 public static interface ComponentSelectionListener {
\r
620 public void componentClicked(RocketComponent[] components, MouseEvent e);
\r
623 public void addComponentSelectionListener(
\r
624 ComponentSelectionListener newListener) {
\r
625 this.csl = newListener;
\r