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
90 addHierarchyListener(new HierarchyListener() {
\r
92 public void hierarchyChanged(HierarchyEvent e) {
\r
94 RocketFigure3d.this.removeHierarchyListener(this);
\r
99 private void initGLCanvas(){
\r
100 log.debug("Initializing RocketFigure3D OpenGL Canvas");
\r
102 log.debug("Setting up GL capabilities...");
\r
103 GLProfile glp = GLProfile.getDefault();
\r
104 GLCapabilities caps = new GLCapabilities(glp);
\r
105 caps.setSampleBuffers(true);
\r
106 caps.setNumSamples(6);
\r
107 caps.setStencilBits(1);
\r
109 log.debug("Creating OpenGL Canvas");
\r
110 canvas = new GLCanvas(caps);
\r
112 canvas.addGLEventListener(this);
\r
113 this.add(canvas, BorderLayout.CENTER);
\r
115 setupMouseListeners();
\r
118 } catch (Throwable t) {
\r
119 log.error("An error occurred creating 3d View", t);
\r
121 this.add(new JLabel("Unable to load 3d Libraries: "
\r
122 + t.getMessage()));
\r
127 * Set up the standard rendering hints on the Graphics2D
\r
129 private static void setRenderingHints(Graphics2D g){
\r
130 g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
\r
131 RenderingHints.VALUE_STROKE_NORMALIZE);
\r
132 g.setRenderingHint(RenderingHints.KEY_RENDERING,
\r
133 RenderingHints.VALUE_RENDER_QUALITY);
\r
134 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
\r
135 RenderingHints.VALUE_ANTIALIAS_ON);
\r
139 * Rasterize the carets into 2 buffered images that I can blit onto the
\r
140 * 3d display every redraw without all of the caret shape rendering overhead
\r
142 private void rasterizeCarets(){
\r
145 //Rasterize a CG Caret
\r
146 cgCaretRaster = new BufferedImage(CARET_SIZE, CARET_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
\r
147 g2d = cgCaretRaster.createGraphics();
\r
148 setRenderingHints(g2d);
\r
150 g2d.setBackground(new Color(0, 0, 0, 0));
\r
151 g2d.clearRect(0, 0, CARET_SIZE, CARET_SIZE);
\r
153 new CGCaret(CARET_SIZE/2,CARET_SIZE/2).paint(g2d, 1.0);
\r
157 //Rasterize a CP Caret
\r
158 cpCaretRaster = new BufferedImage(CARET_SIZE, CARET_SIZE, BufferedImage.TYPE_4BYTE_ABGR);
\r
159 g2d = cpCaretRaster.createGraphics();
\r
160 setRenderingHints(g2d);
\r
162 g2d.setBackground(new Color(0, 0, 0, 0));
\r
163 g2d.clearRect(0, 0, CARET_SIZE, CARET_SIZE);
\r
165 new CPCaret(CARET_SIZE/2,CARET_SIZE/2).paint(g2d, 1.0);
\r
171 private void setupMouseListeners() {
\r
172 MouseInputAdapter a = new MouseInputAdapter() {
\r
175 MouseEvent pressEvent;
\r
178 public void mousePressed(MouseEvent e) {
\r
185 public void mouseClicked(MouseEvent e) {
\r
186 pickPoint = new Point(lastX, canvas.getHeight() - lastY);
\r
192 public void mouseDragged(MouseEvent e) {
\r
193 int dx = lastX - e.getX();
\r
194 int dy = lastY - e.getY();
\r
198 if (pressEvent.getButton() == MouseEvent.BUTTON1) {
\r
199 if (Math.abs(dx) > Math.abs(dy)) {
\r
200 setYaw(yaw - (float) dx / 100.0);
\r
202 if ( yaw > Math.PI/2.0 && yaw < 3.0*Math.PI/2.0 ){
\r
205 setRoll(roll - (float) dy / 100.0);
\r
208 lightPosition[0] -= 0.1f * dx;
\r
209 lightPosition[1] += 0.1f * dy;
\r
214 canvas.addMouseMotionListener(a);
\r
215 canvas.addMouseListener(a);
\r
218 public void setConfiguration(Configuration configuration) {
\r
219 this.configuration = configuration;
\r
224 public void display(GLAutoDrawable drawable) {
\r
225 GL2 gl = drawable.getGL().getGL2();
\r
226 GLU glu = new GLU();
\r
228 gl.glEnable(GL.GL_MULTISAMPLE);
\r
230 gl.glClearColor(1, 1, 1, 1);
\r
231 gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
\r
233 setupView(gl, glu);
\r
235 if (pickPoint != null) {
\r
236 gl.glDisable(GLLightingFunc.GL_LIGHTING);
\r
237 final RocketComponent picked = rr.pick(drawable, configuration,
\r
238 pickPoint, pickEvent.isShiftDown()?selection:null );
\r
239 if (csl != null && picked != null) {
\r
240 final MouseEvent e = pickEvent;
\r
241 SwingUtilities.invokeLater(new Runnable() {
\r
243 public void run() {
\r
244 csl.componentClicked(new RocketComponent[] { picked },
\r
252 gl.glClearColor(1, 1, 1, 1);
\r
253 gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
\r
255 gl.glEnable(GLLightingFunc.GL_LIGHTING);
\r
257 rr.render(drawable, configuration, selection);
\r
259 drawExtras(gl, glu);
\r
260 drawCarets(gl, glu);
\r
264 private void drawCarets(GL2 gl, GLU glu) {
\r
265 final Graphics2D og2d = caretOverlay.createGraphics();
\r
266 setRenderingHints(og2d);
\r
268 og2d.setBackground(new Color(0, 0, 0, 0));
\r
269 og2d.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
\r
270 caretOverlay.markDirty(0, 0, canvas.getWidth(), canvas.getHeight());
\r
272 // The existing relative Extras don't really work right for 3d.
\r
273 Coordinate pCP = project(cp, gl, glu);
\r
274 Coordinate pCG = project(cg, gl, glu);
\r
276 final int d = CARET_SIZE/2;
\r
278 //z order the carets
\r
279 if (pCG.z < pCP.z) {
\r
280 //Subtract half of the caret size, so they are centered ( The +/- d in each translate)
\r
281 //Flip the sense of the Y coordinate from GL to normal (Y+ up/down)
\r
282 og2d.drawRenderedImage(
\r
284 AffineTransform.getTranslateInstance((pCP.x - d),
\r
285 canvas.getHeight() - (pCP.y + d)));
\r
286 og2d.drawRenderedImage(
\r
288 AffineTransform.getTranslateInstance((pCG.x - d),
\r
289 canvas.getHeight() - (pCG.y + d)));
\r
291 og2d.drawRenderedImage(
\r
293 AffineTransform.getTranslateInstance((pCG.x - d),
\r
294 canvas.getHeight() - (pCG.y + d)));
\r
295 og2d.drawRenderedImage(
\r
297 AffineTransform.getTranslateInstance((pCP.x - d),
\r
298 canvas.getHeight() - (pCP.y + d)));
\r
302 gl.glEnable(GL.GL_BLEND);
\r
303 caretOverlay.drawAll();
\r
304 gl.glDisable(GL.GL_BLEND);
\r
308 * Draw the extras overlay to the gl canvas.
\r
309 * Re-blits the overlay every frame. Only re-renders the overlay
\r
312 private void drawExtras(GL2 gl, GLU glu){
\r
313 //Only re-render if needed
\r
314 // redrawExtras: Some external change (new simulation data) means
\r
315 // the data is out of date.
\r
316 // extrasOverlay.contentsLost(): For some reason the buffer with this
\r
318 if ( redrawExtras || extrasOverlay.contentsLost() ){
\r
319 log.debug("Redrawing Overlay");
\r
321 final Graphics2D og2d = extrasOverlay.createGraphics();
\r
322 setRenderingHints(og2d);
\r
324 og2d.setBackground(new Color(0, 0, 0, 0));
\r
325 og2d.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
\r
326 extrasOverlay.markDirty(0, 0, canvas.getWidth(), canvas.getHeight());
\r
328 for (FigureElement e : relativeExtra) {
\r
331 Rectangle rect = this.getVisibleRect();
\r
333 for (FigureElement e : absoluteExtra) {
\r
334 e.paint(og2d, 1.0, rect);
\r
338 redrawExtras = false;
\r
341 //Re-blit to gl canvas every time
\r
342 gl.glEnable(GL.GL_BLEND);
\r
343 extrasOverlay.drawAll();
\r
344 gl.glDisable(GL.GL_BLEND);
\r
348 public void dispose(GLAutoDrawable drawable) {
\r
352 public void init(GLAutoDrawable drawable) {
\r
355 GL2 gl = drawable.getGL().getGL2();
\r
356 gl.glClearDepth(1.0f); // clear z-buffer to the farthest
\r
358 gl.glDepthFunc(GL.GL_LEQUAL); // the type of depth test to do
\r
362 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_AMBIENT,
\r
363 new float[] { amb, amb, amb, 1 }, 0);
\r
364 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_DIFFUSE,
\r
365 new float[] { dif, dif, dif, 1 }, 0);
\r
366 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_SPECULAR,
\r
367 new float[] { dif, dif, dif, 1 }, 0);
\r
369 gl.glEnable(GLLightingFunc.GL_LIGHT1);
\r
370 gl.glEnable(GLLightingFunc.GL_LIGHTING);
\r
371 gl.glShadeModel(GLLightingFunc.GL_SMOOTH);
\r
373 gl.glEnable(GLLightingFunc.GL_NORMALIZE);
\r
375 extrasOverlay = new Overlay(drawable);
\r
376 caretOverlay = new Overlay(drawable);
\r
380 public void reshape(GLAutoDrawable drawable, int x, int y, int w, int h) {
\r
381 GL2 gl = drawable.getGL().getGL2();
\r
382 GLU glu = new GLU();
\r
384 double ratio = (double) w / (double) h;
\r
385 fovX = fovY * ratio;
\r
387 gl.glMatrixMode(GLMatrixFunc.GL_PROJECTION);
\r
388 gl.glLoadIdentity();
\r
389 glu.gluPerspective(fovY, ratio, 0.05f, 100f);
\r
390 gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
\r
392 redrawExtras = true;
\r
395 @SuppressWarnings("unused")
\r
396 private static class Bounds {
\r
397 double xMin, xMax, xSize;
\r
398 double yMin, yMax, ySize;
\r
399 double zMin, zMax, zSize;
\r
404 * Calculates the bounds for the current configuration
\r
408 private Bounds calculateBounds() {
\r
409 Bounds ret = new Bounds();
\r
410 Collection<Coordinate> bounds = configuration.getBounds();
\r
411 for (Coordinate c : bounds) {
\r
412 ret.xMax = Math.max(ret.xMax, c.x);
\r
413 ret.xMin = Math.min(ret.xMin, c.x);
\r
415 ret.yMax = Math.max(ret.yMax, c.y);
\r
416 ret.yMin = Math.min(ret.yMin, c.y);
\r
418 ret.zMax = Math.max(ret.zMax, c.z);
\r
419 ret.zMin = Math.min(ret.zMin, c.z);
\r
421 double r = MathUtil.hypot(c.y, c.z);
\r
422 ret.rMax = Math.max(ret.rMax, r);
\r
424 ret.xSize = ret.xMax - ret.xMin;
\r
425 ret.ySize = ret.yMax - ret.yMin;
\r
426 ret.zSize = ret.zMax - ret.zMin;
\r
430 private void setupView(GL2 gl, GLU glu) {
\r
431 gl.glLoadIdentity();
\r
433 gl.glLightfv(GLLightingFunc.GL_LIGHT1, GLLightingFunc.GL_POSITION,
\r
437 Bounds b = calculateBounds();
\r
439 // Calculate the distance needed to fit the bounds in both the X and Y
\r
441 // Add 10% for space around it.
\r
442 double dX = (b.xSize * 1.2 / 2.0)
\r
443 / Math.tan(Math.toRadians(fovX / 2.0));
\r
444 double dY = (b.rMax * 2.0 * 1.2 / 2.0)
\r
445 / Math.tan(Math.toRadians(fovY / 2.0));
\r
447 // Move back the greater of the 2 distances
\r
448 glu.gluLookAt(0, 0, Math.max(dX, dY), 0, 0, 0, 0, 1, 0);
\r
450 gl.glRotated(yaw * (180.0 / Math.PI), 0, 1, 0);
\r
451 gl.glRotated(roll * (180.0 / Math.PI), 1, 0, 0);
\r
453 // Center the rocket in the view.
\r
454 gl.glTranslated(-b.xMin - b.xSize / 2.0, 0, 0);
\r
456 //Change to LEFT Handed coordinates
\r
457 gl.glScaled(1, 1, -1);
\r
458 gl.glFrontFace(GL.GL_CW);
\r
460 //Flip textures for LEFT handed coords
\r
461 gl.glMatrixMode(GL.GL_TEXTURE);
\r
462 gl.glLoadIdentity();
\r
463 gl.glScaled(-1,1,1);
\r
464 gl.glTranslated(-1,0,0);
\r
465 gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
\r
469 * Call when the rocket has changed
\r
471 public void updateFigure() {
\r
472 log.debug("3D Figure Updated");
\r
477 private void internalRepaint(){
\r
479 if (canvas != null)
\r
484 public void repaint() {
\r
485 redrawExtras = true;
\r
489 private Set<RocketComponent> selection = new HashSet<RocketComponent>();
\r
491 public void setSelection(RocketComponent[] selection) {
\r
492 this.selection.clear();
\r
493 if (selection != null) {
\r
494 for (RocketComponent c : selection)
\r
495 this.selection.add(c);
\r
500 private void setRoll(double rot) {
\r
501 if (MathUtil.equals(roll, rot))
\r
503 this.roll = MathUtil.reduce360(rot);
\r
507 private void setYaw(double rot) {
\r
508 if (MathUtil.equals(yaw, rot))
\r
510 this.yaw = MathUtil.reduce360(rot);
\r
514 // ///////////// Extra methods
\r
516 private Coordinate project(Coordinate c, GL2 gl, GLU glu) {
\r
517 double[] mvmatrix = new double[16];
\r
518 double[] projmatrix = new double[16];
\r
519 int[] viewport = new int[4];
\r
521 gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);
\r
522 gl.glGetDoublev(GLMatrixFunc.GL_MODELVIEW_MATRIX, mvmatrix, 0);
\r
523 gl.glGetDoublev(GLMatrixFunc.GL_PROJECTION_MATRIX, projmatrix, 0);
\r
525 double out[] = new double[4];
\r
526 glu.gluProject(c.x, c.y, c.z, mvmatrix, 0, projmatrix, 0, viewport, 0,
\r
529 return new Coordinate(out[0], out[1], out[2]);
\r
532 private Coordinate cp = new Coordinate(0, 0, 0);
\r
533 private Coordinate cg = new Coordinate(0, 0, 0);
\r
535 public void setCG(Coordinate cg) {
\r
537 redrawExtras = true;
\r
540 public void setCP(Coordinate cp) {
\r
542 redrawExtras = true;
\r
545 public void addRelativeExtra(FigureElement p) {
\r
546 relativeExtra.add(p);
\r
547 redrawExtras = true;
\r
550 public void removeRelativeExtra(FigureElement p) {
\r
551 relativeExtra.remove(p);
\r
552 redrawExtras = true;
\r
555 public void clearRelativeExtra() {
\r
556 relativeExtra.clear();
\r
557 redrawExtras = true;
\r
560 public void addAbsoluteExtra(FigureElement p) {
\r
561 absoluteExtra.add(p);
\r
562 redrawExtras = true;
\r
565 public void removeAbsoluteExtra(FigureElement p) {
\r
566 absoluteExtra.remove(p);
\r
567 redrawExtras = true;
\r
570 public void clearAbsoluteExtra() {
\r
571 absoluteExtra.clear();
\r
572 redrawExtras = true;
\r
575 private ComponentSelectionListener csl;
\r
577 public static interface ComponentSelectionListener {
\r
578 public void componentClicked(RocketComponent[] components, MouseEvent e);
\r
581 public void addComponentSelectionListener(
\r
582 ComponentSelectionListener newListener) {
\r
583 this.csl = newListener;
\r