package teamcomm.gui; import com.jogamp.opengl.GL; import com.jogamp.opengl.GL2; import com.jogamp.opengl.GLAutoDrawable; import com.jogamp.opengl.GLCapabilities; import com.jogamp.opengl.GLEventListener; import com.jogamp.opengl.GLProfile; import com.jogamp.opengl.awt.GLCanvas; import com.jogamp.opengl.util.AnimatorBase; import com.jogamp.opengl.util.FPSAnimator; import common.Log; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.nio.FloatBuffer; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenu; import javax.swing.event.MouseInputAdapter; import teamcomm.PluginLoader; import teamcomm.data.GameState; import teamcomm.data.RobotState; import teamcomm.data.event.TeamEvent; import teamcomm.data.event.TeamEventListener; import teamcomm.gui.drawings.Drawing; import teamcomm.gui.drawings.PerPlayer; import teamcomm.gui.drawings.Static; /** * Class for the 3-dimensional field view. * * @author Felix Thielke */ public class View3D implements GLEventListener, TeamEventListener { private static final int ANIMATION_FPS = 60; private final GLCanvas canvas; private final AnimatorBase animator; private final Camera camera = new Camera(); private final int[] teamNumbers = new int[]{PluginLoader.TEAMNUMBER_COMMON, PluginLoader.TEAMNUMBER_COMMON}; private final Set<RobotState> leftRobots = new HashSet<>(); private final Set<RobotState> rightRobots = new HashSet<>(); private static final Comparator<Drawing> drawingComparator = new Comparator<Drawing>() { @Override public int compare(final Drawing o1, final Drawing o2) { // opaque objects have priority over transparent objects if (o1.hasAlpha() && !o2.hasAlpha()) { return 1; } if (!o1.hasAlpha() && o2.hasAlpha()) { return -1; } // higher priorities are drawn earlier return o2.getPriority() - o1.getPriority(); } }; private final List<Drawing> drawings = new LinkedList<>(); private final JMenu drawingsMenu = new JMenu("Drawings"); private int width = 0; private int height = 0; /** * Constructor. */ public View3D() { // Initialize GL canvas and animator final GLProfile glp = GLProfile.get(GLProfile.GL2); final GLCapabilities caps = new GLCapabilities(glp); canvas = new GLCanvas(caps); canvas.addGLEventListener(this); // Setup camera movement final MouseInputAdapter listener = new MouseInputAdapter() { private int[] lastPos = null; @Override public void mousePressed(final MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { lastPos = new int[]{e.getX(), e.getY()}; } } @Override public void mouseReleased(final MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { lastPos = null; } } @Override public void mouseDragged(final MouseEvent e) { if (lastPos != null) { final float factor = 1.0f / 5.0f; camera.addPhi((e.getX() - lastPos[0]) * factor); camera.addTheta(-(e.getY() - lastPos[1]) * factor); lastPos = new int[]{e.getX(), e.getY()}; } } }; canvas.addMouseListener(listener); canvas.addMouseMotionListener(listener); canvas.addMouseWheelListener(new MouseWheelListener() { @Override public void mouseWheelMoved(final MouseWheelEvent e) { camera.addRadius(-e.getWheelRotation() * 0.05f); } }); // Start rendering animator = new FPSAnimator(canvas, ANIMATION_FPS); animator.start(); } /** * Terminates the field view. */ public void terminate() { animator.stop(); canvas.destroy(); } /** * Returns the AWT canvas the field view is drawn on. * * @return AWT canvas */ public GLCanvas getCanvas() { return canvas; } /** * Initializes the field view. * * @param glad drawable */ @Override public void init(final GLAutoDrawable glad) { final GL2 gl = glad.getGL().getGL2(); // enable depth test gl.glClearDepth(1.0f); gl.glDepthFunc(GL.GL_LEQUAL); gl.glEnable(GL.GL_DEPTH_TEST); // avoid rendering the backside of surfaces gl.glEnable(GL.GL_CULL_FACE); gl.glCullFace(GL.GL_BACK); gl.glFrontFace(GL.GL_CCW); // Enable lightning, texturing and smooth shading gl.glEnable(GL2.GL_LIGHTING); gl.glEnable(GL.GL_MULTISAMPLE); gl.glEnable(GL.GL_TEXTURE_2D); gl.glPolygonMode(GL2.GL_FRONT, GL2.GL_FILL); gl.glShadeModel(GL2.GL_SMOOTH); gl.glColorMaterial(GL2.GL_FRONT, GL2.GL_AMBIENT_AND_DIFFUSE); // gl.glHint(GL2.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST); // Initialize projection matrix reshape(glad, canvas.getBounds().x, canvas.getBounds().y, canvas.getBounds().width, canvas.getBounds().height); // setup light gl.glEnable(GL2.GL_COLOR_MATERIAL); gl.glLightModelfv(GL2.GL_LIGHT_MODEL_AMBIENT, FloatBuffer.wrap(new float[]{0.2f, 0.2f, 0.2f, 1.0f})); gl.glLightModelf(GL2.GL_LIGHT_MODEL_LOCAL_VIEWER, GL.GL_TRUE); gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_AMBIENT, FloatBuffer.wrap(new float[]{0.5f, 0.5f, 0.5f, 1.0f})); gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_DIFFUSE, FloatBuffer.wrap(new float[]{1.0f, 1.0f, 1.0f, 1.0f})); gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_SPECULAR, FloatBuffer.wrap(new float[]{1.0f, 1.0f, 1.0f, 1.0f})); gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_POSITION, FloatBuffer.wrap(new float[]{0.0f, 0.0f, 9.0f, 1.0f})); gl.glLightf(GL2.GL_LIGHT0, GL2.GL_CONSTANT_ATTENUATION, 1.0f); gl.glLightf(GL2.GL_LIGHT0, GL2.GL_LINEAR_ATTENUATION, 0.0f); gl.glLightf(GL2.GL_LIGHT0, GL2.GL_QUADRATIC_ATTENUATION, 0.0f); gl.glLightf(GL2.GL_LIGHT0, GL2.GL_SPOT_CUTOFF, 180.0f); gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_SPOT_DIRECTION, FloatBuffer.wrap(new float[]{0.f, 0.f, -1.f})); gl.glLightf(GL2.GL_LIGHT0, GL2.GL_SPOT_EXPONENT, 0.0f); gl.glEnable(GL2.GL_LIGHT0); // Set clear color gl.glClearColor(0.6f, 0.6f, 0.65f, 1.0f); // Setup common drawings drawings.addAll(PluginLoader.getInstance().getCommonDrawings()); Collections.sort(drawings, drawingComparator); for (final Drawing d : drawings) { d.initialize(gl); } updateDrawingsMenu(); // Listen for robot events GameState.getInstance().addListener(this); } /** * Performs cleanup after the canvas was disposed. * * @param glad drawable */ @Override public void dispose(final GLAutoDrawable glad) { } /** * Draws the field view. * * @param glad drawable */ @Override public void display(final GLAutoDrawable glad) { final GL2 gl = glad.getGL().getGL2(); // Clear buffers gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); // Position camera gl.glLoadIdentity(); camera.positionCamera(gl); // Render drawings synchronized (drawings) { final Iterator<Drawing> it = drawings.iterator(); while (it.hasNext()) { final Drawing d = it.next(); // Initialize if needed try { d.initialize(gl); } catch (final Throwable e) { it.remove(); Log.error(e.getClass().getSimpleName() + " was thrown while initializing drawing " + d.getClass().getName() + ": " + e.getMessage()); continue; } // Draw if (d.isActive()) { if (d instanceof Static) { try { ((Static) d).draw(gl, camera); } catch (final Throwable e) { Log.error(e.getClass().getSimpleName() + " was thrown while drawing custom drawing " + d.getClass().getName() + ": " + e.getMessage()); } } else if (d instanceof PerPlayer) { if (d.getTeamNumber() == PluginLoader.TEAMNUMBER_COMMON || d.getTeamNumber() == teamNumbers[GameState.TEAM_LEFT]) { synchronized (leftRobots) { for (final RobotState r : leftRobots) { try { ((PerPlayer) d).draw(gl, r, camera); } catch (final Throwable e) { Log.error(e.getClass().getSimpleName() + " was thrown while drawing custom drawing " + d.getClass().getName() + ": " + e.getMessage()); } } } } if (d.getTeamNumber() == PluginLoader.TEAMNUMBER_COMMON || d.getTeamNumber() == teamNumbers[GameState.TEAM_RIGHT]) { camera.flip(gl); synchronized (rightRobots) { for (final RobotState r : rightRobots) { try { ((PerPlayer) d).draw(gl, r, camera); } catch (final Throwable e) { Log.error(e.getClass().getSimpleName() + " was thrown while drawing custom drawing " + d.getClass().getName() + ": " + e.getMessage()); } } } camera.flip(gl); } } } } } } /** * Method that gets called on a reshape event of the AWT canvas. Adjusts the * viewing frustum of the field view for the new shape. * * @param glad drawable * @param x new x offset * @param y new y offset * @param width new width * @param height new height */ @Override public void reshape(final GLAutoDrawable glad, final int x, final int y, final int width, final int height) { // Adjust projection matrix if (this.width != width || this.height != height) { this.width = width; this.height = height; camera.setupFrustum(glad.getGL().getGL2(), (double) width / (double) height); } } private void updateDrawingsMenu() { // Clear the current menu drawingsMenu.removeAll(); // Create submenus for teams final HashMap<Integer, JMenu> submenus = new HashMap<>(); for (final int teamNumber : teamNumbers) { if (teamNumber != PluginLoader.TEAMNUMBER_COMMON) { final String name = GameState.getInstance().getTeamName(teamNumber, false, false); if (!name.equals("Unknown")) { final JMenu submenu = new JMenu(name); submenus.put(teamNumber, submenu); drawingsMenu.add(submenu); } } } // Create menu items for drawings for (final Drawing d : drawings) { final JCheckBoxMenuItem m = new JCheckBoxMenuItem(d.getClass().getSimpleName(), d.isActive()); m.addItemListener(new ItemListener() { @Override public void itemStateChanged(final ItemEvent e) { d.setActive(e.getStateChange() == ItemEvent.SELECTED); } }); final JMenu submenu = submenus.get(d.getTeamNumber()); if (submenu != null) { submenu.add(m); } else { drawingsMenu.add(m); } } // Remove empty submenus for (final JMenu submenu : submenus.values()) { if (submenu.getMenuComponentCount() == 0) { drawingsMenu.remove(submenu); } } } /** * Returns a JMenu controlling which drawings are visible. * * @return menu */ public JMenu getDrawingsMenu() { return drawingsMenu; } @Override public void teamChanged(final TeamEvent e) { if (e.side != GameState.TEAM_OTHER) { if (teamNumbers[e.side] != (e.teamNumber == 0 ? PluginLoader.TEAMNUMBER_COMMON : e.teamNumber)) { teamNumbers[e.side] = e.teamNumber == 0 ? PluginLoader.TEAMNUMBER_COMMON : e.teamNumber; synchronized (drawings) { drawings.clear(); drawings.addAll(PluginLoader.getInstance().getCommonDrawings()); if (teamNumbers[0] != PluginLoader.TEAMNUMBER_COMMON) { drawings.addAll(PluginLoader.getInstance().getDrawings(teamNumbers[0])); } if (teamNumbers[1] != PluginLoader.TEAMNUMBER_COMMON) { drawings.addAll(PluginLoader.getInstance().getDrawings(teamNumbers[1])); } Collections.sort(drawings, drawingComparator); updateDrawingsMenu(); } } if (e.side == GameState.TEAM_LEFT) { synchronized (leftRobots) { leftRobots.clear(); leftRobots.addAll(e.players); } } else { synchronized (rightRobots) { rightRobots.clear(); rightRobots.addAll(e.players); } } } } }