import com.jogamp.opengl.util.texture.Texture; import java.awt.Color; import java.awt.Desktop; import java.awt.KeyEventDispatcher; import java.awt.KeyboardFocusManager; import java.awt.event.KeyEvent; import java.awt.Window; import java.io.IOException; import javax.media.opengl.GL; import static javax.media.opengl.GL2.*; import javax.swing.UIManager; import robotrace.Base; import robotrace.MainFrame; import robotrace.Vector; import static java.lang.Math.*; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; import java.util.Locale; import java.util.Random; /** * Handles all of the RobotRace graphics functionality, * which should be extended per the assignment. * * OpenGL functionality: * - Basic commands are called via the gl object; * - Utility commands are called via the glu and * glut objects; * * GlobalState: * The gs object contains the GlobalState as described * in the assignment: * - The camera viewpoint angles, phi and theta, are * changed interactively by holding the left mouse * button and dragging; * - The camera view width, vWidth, is changed * interactively by holding the right mouse button * and dragging upwards or downwards; * - The center point can be moved up and down by * pressing the 'q' and 'z' keys, forwards and * backwards with the 'w' and 's' keys, and * left and right with the 'a' and 'd' keys; * - Other settings are changed via the menus * at the top of the screen. * * Textures: * Place your "track.jpg", "brick.jpg", "head.jpg", * and "torso.jpg" files in the same folder as this * file. These will then be loaded as the texture * objects track, bricks, head, and torso respectively. * Be aware, these objects are already defined and * cannot be used for other purposes. The texture * objects can be used as follows: * * gl.glColor3f(1f, 1f, 1f); * track.bind(gl); * gl.glBegin(GL_QUADS); * gl.glTexCoord2d(0, 0); * gl.glVertex3d(0, 0, 0); * gl.glTexCoord2d(1, 0); * gl.glVertex3d(1, 0, 0); * gl.glTexCoord2d(1, 1); * gl.glVertex3d(1, 1, 0); * gl.glTexCoord2d(0, 1); * gl.glVertex3d(0, 1, 0); * gl.glEnd(); * * Note that it is hard or impossible to texture * objects drawn with GLUT. Either define the * primitives of the object yourself (as seen * above) or add additional textured primitives * to the GLUT object. */ public class RobotRace extends Base { /** * Array of the four robots. */ private final Robot[] robots; /** * Instance of the camera. */ private final Camera camera; /** * Instance of the race track. */ final RaceTrack raceTrack; /** * Instance of the terrain. */ private final Terrain terrain; /** * Whether lighting effects should be enabled or not. For testing purposes, * defaults to true. It can be changed by pressing "L". */ private boolean lightingEnabled = true; /** * The main window for this program. */ private MainFrame mainWindow; /** * Frames per second monitor: total frames in the past period. */ private float frames; /** * Frames per second monitor: the timestamp in milliseconds for the begin of * the previous period (or 0 if there is no previous period.) */ private long last_frame_time; /** * Last (Global State) time when the speed got calculated. */ private double last_speed_update; /** * Last animation time when the position of robots were updated. */ private double last_t; /** * Only recalculate the robot speed if at least this number of seconds has * been elapsed. */ private final static double SPEED_RECALC_INTERVAL = .500; /** * Desired average robot speed in meter per second. (Ne3 / 3600 is N km/h) */ private final static double TARGET_ROBOT_SPEED = 19e3 / 3600; /** * The targeted distance between the robot and the average walking position. */ private final double DIST_DIFF = 5; /** * Used for randomizing robot speed. */ private final Random random = new Random(); /** * Whether textures are enabled for surfaces. */ boolean enableTextures; /** * Real animation time at which the game is paused (or -1 if not paused). */ private float pausedSince = -1; /** * Total number of animation time that was spent in the paused state. */ private float pausedTimeTotal; /** * Trigger a pause on the next frame update. */ private boolean requestPauseToggle = false; /** * Constructs this robot race by initializing robots, camera, track, and * terrain. */ public RobotRace() { // Initialize global OpenGL provider with GLU and GLUT reference. BetterBase.setGLU(glu); BetterBase.setGLUT(glut); // find the MainFrame for (Window w : Window.getWindows()) { if (w instanceof MainFrame) { mainWindow = (MainFrame) w; } } if (mainWindow == null) { System.err.println("No window found, FPS will be unavailable!"); } // Create a new array of four robots robots = new Robot[4]; // Materials for robots 0, 1, ..., N Material[] materials = new Material[] { Material.GOLD, Material.SILVER, Material.WOOD, Material.ORANGE }; // Initialize robots assert materials.length == robots.length; for (int i = 0; i < materials.length; i++) { // different material color; each robot walks its own lane. robots[i] = new Robot(this, materials[i], i); } // Initialize the race track raceTrack = new RaceTrack(this); // Initialize the camera camera = new Camera(gs, raceTrack, robots); // Initialize the terrain terrain = new Terrain(this, raceTrack); } /** * Called upon the start of the application. Primarily used to configure * OpenGL. */ @Override public void initialize() { // Initialize global OpenGL context. BetterBase.setGL(gl); // enable textures if supported toggleTextures(); // Enable blending. gl.glEnable(GL_BLEND); gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Enable anti-aliasing. gl.glEnable(GL_LINE_SMOOTH); gl.glEnable(GL_POLYGON_SMOOTH); gl.glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); gl.glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST); // Enable depth testing. gl.glEnable(GL_DEPTH_TEST); gl.glDepthFunc(GL_LESS); // Enable textures. gl.glEnable(GL_TEXTURE_2D); gl.glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); gl.glBindTexture(GL_TEXTURE_2D, 0); // initialize lighting effects (lighting bit is not enabled here though) initLighting(); // init terrain terrain.createTerrain(); } /** * Set lighting colors and effects, but do not enable them yet. */ private void initLighting() { // greyish color float[] ambientRGBA = {0.2f, 0.2f, 0.2f, 1.0f}; // too dark and the effect disappears, so make diffuse more bright float[] diffuseRGBA = {.7f, .7f, .7f, 1.0f}; // default is 100% specular, but that causes the surfaces pointing to // the normal to be fully white. gl.glLightfv(GL_LIGHT0, GL_SPECULAR, new float[] {0, 0, 0, 0.5f}, 0); // set the light-source colors gl.glLightfv(GL_LIGHT0, GL_AMBIENT, ambientRGBA, 0); gl.glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseRGBA, 0); // turn the light on (note: display func must set or unset LIGHTING) gl.glEnable(GL_LIGHT0); //gl.glEnable(GL_LIGHTING); // necessary because normals are improperly scaled gl.glEnable(GL_NORMALIZE); } /** * Updates the title with frames per second. */ private void updateFPS() { // just to be sure, avoid accessing the window if unavailable if (mainWindow == null) { return; } long now = System.currentTimeMillis(); float timediff = (now - last_frame_time) / 1000.0f; frames++; // periodically update the frame time if (timediff >= 1.0f) { // prevent inaccurate FPS on start if (last_frame_time != 0) { float fps = frames / timediff; // display FPS with 2 decimals at most mainWindow.setTitle(String.format(Locale.getDefault(), "RobotRace (FPS %.2f)", fps)); } last_frame_time = now; frames = 0; } } /** * Configures the viewing transform. */ @Override public void setView() { // due to limitations of the Base class (unable to override display // function), the FPS update is done in the first accessible function // (here, in setView) updateFPS(); // reset our state when a reset it detected. Must be done before hacking // with time. detectReset(); // similarly, reset the time very early here when paused applyPausedTime(); // Select part of window. gl.glViewport(0, 0, gs.w, gs.h); // Set projection matrix. gl.glMatrixMode(GL_PROJECTION); gl.glLoadIdentity(); // Set the perspective. // angle = 2 arctan(vWidth / 2vDist) float angle; angle = 2f * (float) atan((0.5f * gs.vWidth) / gs.vDist); // radians to degree (degree = rad / pi * 180) angle = 180 * angle / (float) PI; // lower than 1 would yield no picture, great values cause an "infinite" line segment angle = max(1, min(179, angle)); glu.gluPerspective(angle, (float) gs.w / (float) gs.h, 0.1f, 10.0 * gs.vDist); // Set camera. gl.glMatrixMode(GL_MODELVIEW); gl.glLoadIdentity(); // Update the view according to the camera mode camera.update(gs.camMode); glu.gluLookAt(camera.eye.x(), camera.eye.y(), camera.eye.z(), camera.center.x(), camera.center.y(), camera.center.z(), camera.up.x(), camera.up.y(), camera.up.z()); // Enable lighting effects if (lightingEnabled) { float[] lightPos = { // light position (slightly away from top-left corner of the // camera (eye) point) (float) camera.eye.x(), (float) camera.eye.y() + 1f, (float) camera.eye.z() - 1f, // Light-source type, 0 sets a directional light which starts in // (x,y,z) and points to the origin. 1 means positional where // the light is located in (x,y,z) and shines in all directions. 0 }; // set the light-source position gl.glLightfv(GL_LIGHT0, GL_POSITION, lightPos, 0); gl.glEnable(GL_LIGHTING); } else { gl.glDisable(GL_LIGHTING); } } /** * Draws the entire scene. */ @Override public void drawScene() { // Background color. gl.glClearColor(.7f, .6f, .6f, 0f); // Clear background. gl.glClear(GL_COLOR_BUFFER_BIT); // Clear depth buffer. gl.glClear(GL_DEPTH_BUFFER_BIT); // Set color to black. BetterBase.setColor(Color.BLACK); gl.glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // Draw the axis frame if (gs.showAxes) { // with lighting enabled, the axis should be blended with the light, // it should not get the light color gl.glEnable(GL_COLOR_MATERIAL); drawAxisFrame(); // disable for robot materials gl.glDisable(GL_COLOR_MATERIAL); } // Draw the robots drawRobots(); // use color of race track, not lighting gl.glEnable(GL_COLOR_MATERIAL); // Draw race track raceTrack.draw(gs.trackNr); // Draw terrain terrain.draw(); // restore gl.glDisable(GL_COLOR_MATERIAL); } /** * Detect when the reset button is pressed and act on it. */ private void detectReset() { float current_t = gs.tAnim; if (current_t < last_speed_update) { System.err.println("Reset detected..."); // on reset, position the robots on the begin last_speed_update = 0; last_t = 0; for (Robot robot : robots) { robot.setSpeed(0); robot.resetPosition(); } // pause in beginning if already paused if (pausedSince != -1) { pausedSince = current_t; } pausedTimeTotal = 0; } } /** * Periodically calculate the robot speed based and update robot position. */ private void calculateRobotSpeedAndLocation() { double current_t = gs.tAnim; // periodically calculate a new speed double last_speed_update_t_diff = current_t - last_speed_update; if (last_speed_update_t_diff >= SPEED_RECALC_INTERVAL) { // (expected) position double avg_pos = current_t * TARGET_ROBOT_SPEED; for (Robot robot : robots) { double robot_pos = Robot.racepost2meter(robot.getTimePos()); double old_speed = robot.getSpeed(); // initially the speed of robots is unknown, assume target speed if (old_speed <= 0) { old_speed = TARGET_ROBOT_SPEED; } /* Speed is the sum of: * 10% previous speed (on average 20% target speed) * 50% minimum speed (100% target speed = 50%) * 40% random speed (0% - 60% target speed) * * The random speed ranges from 0%-200% of the target speed. Its * range can change to 0%-150% or 0%-250% depending on the * difference with the expected average position. */ double speedup_factor = 2.0; // if a robot is lagging behind too much (> 0), then give it a // higher chance to walk faster. Similarly, if it is too fast // (< 0), make it slow down a bit. double pos_diff = avg_pos - robot_pos; // do not change speed too much for huge speed differences double n = Math.max(-DIST_DIFF, Math.min(pos_diff, DIST_DIFF)); speedup_factor += n / DIST_DIFF * .5; double rnd = random.nextFloat(); double speed = .1 * old_speed; speed += .5 * TARGET_ROBOT_SPEED; speed += .4 * rnd * speedup_factor * TARGET_ROBOT_SPEED; robot.setSpeed(speed); } // DEBUG: average speed to robot 1 for showing expected position //robots[0].setSpeed(TARGET_ROBOT_SPEED); last_speed_update = current_t; } double t_diff = current_t - last_t; // do not walk at every fart, that is expensive and inaccurate. if (t_diff >= .010) { for (Robot robot : robots) { robot.walkSome(t_diff); } last_t = current_t; } } /** * Draw all four robots in the robots array on the scene */ private void drawRobots() { // before repositioning the robots, change speed if needed calculateRobotSpeedAndLocation(); // Draw each robot on the X-axis for (int i = 0; i < robots.length; i++) { gl.glPushMatrix(); Robot robot = robots[i]; // put robot centered on the lane, slightly rotated to look forward Vector robotPos = raceTrack.getPointForLane(robot.getTimePos(), i); gl.glTranslated(robotPos.x(), robotPos.y(), robotPos.z()); /* While the robot looks in the tangent direction, the position of * the robot on the track is determined by the normal. For cases * where the origin is used to create the track (circles), the * robot position could be used as well (for direction). In the * general case though, the normal is needed. */ Vector norm = raceTrack.getNormal(robot.getTimePos()); double angle = atan2(norm.y(), norm.x()) * 180 / PI; gl.glRotated(angle, 0, 0, 1); // Draw the current robot robot.draw(gs.showStick); // restore positions gl.glPopMatrix(); } } /** * Draw a colored arrow from left to right. * * @param r Red color scale (0 to 1). * @param g Green color scale (0 to 1). * @param b Blue color scale (0 to 1). */ private void drawColoredArrow(Color color) { gl.glPushMatrix(); // change color BetterBase.setColor(color); // draw a thin line from the origin to the right. gl.glTranslatef(0.5f, 0, 0); gl.glScalef(1f, 0.01f, 0.01f); glut.glutSolidCube(1f); // restore scale for clarity. gl.glScalef(1f, 1 / 0.01f, 1 / 0.01f); // draw a cone on the end of the line that has a head that has a radius // which is three times larger than the line segment. gl.glTranslatef(0.5f, 0, 0); // turn head to the right (rotate 90 degree from the Y-axis) gl.glRotatef(90, 0, 1, 0); glut.glutSolidCone(.03f, .1f, 10, 10); // restore previous matrix gl.glPopMatrix(); } /** * Draws the x-axis (red), y-axis (green), z-axis (blue), and origin * (yellow). */ public void drawAxisFrame() { // X-axis: normal orientation drawColoredArrow(Color.RED); // Y-axis: rotate 90 degree clockwise in the Z-axis gl.glRotatef(90, 0, 0, 1); drawColoredArrow(Color.GREEN); gl.glRotatef(-90, 0, 0, 1); // Z-axis: rotate 90 degree in the XY ais gl.glRotatef(-90, 0, 1, 0); drawColoredArrow(Color.BLUE); gl.glRotatef(90, 0, 1, 0); // yellow sphere of 0.03m (with ten divisions) gl.glColor3f(1, 1, 0); glut.glutSolidSphere(0.05f, 10, 10); // reset color gl.glColor3f(0, 0, 0); } public Texture getTorsoTexture() { assert torso != null; return torso; } public Texture getHeadTexture() { assert head != null; return head; } public Texture getBrickTexture() { assert brick != null; return brick; } public Texture getTrackTexture() { assert track != null; return track; } /** * Main program execution body, delegates to an instance of the RobotRace * implementation. */ public static void main(String args[]) { System.out.println("JOGL version: " + com.jogamp.opengl.JoglVersion.getInstance().getImplementationBuild()); final RobotRace robotRace = new RobotRace(); // Being able to exit by pressing Escape would be nice. KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() { @Override public boolean dispatchKeyEvent(KeyEvent e) { if (e.getID() != KeyEvent.KEY_PRESSED) { return false; } switch (e.getKeyCode()) { case KeyEvent.VK_ESCAPE: /* Exit from program */ System.err.println("Exiting..."); System.exit(0); return true; // applies anti-Gravity case KeyEvent.VK_G: try { Desktop.getDesktop().browse(new URI("\u0068\u0074" + "\u0074\u0070\u003a\u002f\u002f\u0078\u006b" + "\u0063\u0064\u002e\u0063\u006f\u006d\u002f" + "\u0033\u0035\u0033\u002f")); } catch (IOException ex) { } catch (URISyntaxException ex) { } return true; case KeyEvent.VK_L: /* toggle Lighting */ robotRace.lightingEnabled = !robotRace.lightingEnabled; System.err.println("Lighting set to " + robotRace.lightingEnabled); return true; case KeyEvent.VK_I: /* print Info */ System.err.println("GlobalState: " + robotRace.gs); return true; case KeyEvent.VK_T: /* toggle Textures */ boolean state = robotRace.toggleTextures(); System.err.println("Textures are " + (state ? "enabled" : "disabled")); return true; case KeyEvent.VK_SPACE: /* pause time */ System.err.println("Triggering pause..."); robotRace.requestPauseToggle = true; return true; case KeyEvent.VK_O: /* camera mode: Overview */ case KeyEvent.VK_H: /* camera mode: Helicopter */ case KeyEvent.VK_M: /* camera mode: Motorcycle */ if (robotRace.mainWindow != null) { System.err.println("Changing camera to: " + e.getKeyChar()); // map from camera mode (array index) to key codes Integer cameraModes[] = new Integer[] { KeyEvent.VK_O, /* 0: default mode (Overview) */ KeyEvent.VK_H, /* 1: helicopter */ KeyEvent.VK_M, /* 2: Motor cycle */ }; int i = Arrays.asList(cameraModes).indexOf(e.getKeyCode()); assert i != -1 : "Camera mode not found for key"; robotRace.gs.camMode = i; robotRace.mainWindow.updateElements(); } return true; case KeyEvent.VK_F: /* toggle robot to Focus on */ robotRace.camera.followTopSpeed = !robotRace.camera.followTopSpeed; String what; if (robotRace.camera.followTopSpeed) { what = "with the highest speed"; } else { what = "that made the most meters"; } System.err.println("Now following the robot " + what); return true; case KeyEvent.VK_0: case KeyEvent.VK_1: case KeyEvent.VK_2: case KeyEvent.VK_3: case KeyEvent.VK_4: if (robotRace.mainWindow != null) { // map from track number (array index) to key codes Integer trackNumbers[] = new Integer[] { KeyEvent.VK_0, /* 0: test track */ KeyEvent.VK_1, /* 1: O track */ KeyEvent.VK_2, /* 2: L track */ KeyEvent.VK_3, /* 3: C track */ KeyEvent.VK_4, /* 4: custom track */ }; int i = Arrays.asList(trackNumbers).indexOf(e.getKeyCode()); assert i != -1 : "Track number not found for key"; System.err.println("Changing to track number " + i); robotRace.gs.trackNr = i; robotRace.mainWindow.updateElements(); } return true; case KeyEvent.VK_B: boolean enabled = !robotRace.raceTrack.debugBezierTracks; robotRace.raceTrack.debugBezierTracks = enabled; System.err.println("Show Bézier debug track: " + enabled); return true; default: return false; } } }); } /** * Disable textures if enabled, enable textures iff disabled AND supported. * @return The new enabled state. */ boolean toggleTextures() { if (track == null || brick == null || head == null || torso == null) { System.err.println("Some textures are missing"); enableTextures = false; } else { enableTextures = !enableTextures; } return enableTextures; } /** * If pause toggle is requested, check state and update. Otherwise, adjust * global animation time. */ private void applyPausedTime() { if (requestPauseToggle) { if (pausedSince == -1) { // just paused, store start time pausedSince = gs.tAnim; System.err.println("Paused since gs.tAnim=" + gs.tAnim); } else { // continuing, increase paused time and clear pause flag float pausedTime = gs.tAnim - pausedSince; pausedTimeTotal += pausedTime; pausedSince = -1; System.err.println("Continued at gs.tAnim=" + gs.tAnim + ". Paused for " + pausedTime + " secs (new total: " + pausedTimeTotal + " secs)"); } requestPauseToggle = false; } // if paused, set clock back. if (pausedSince != -1) { gs.tAnim = pausedSince; } // always apply correction for time drift gs.tAnim -= pausedTimeTotal; } }