/************************************************************ /* Authors: Dr. Parson /* Creation Date: 9/17/2020 /* Due Date: demo /* Course: CSC220 Object-Oriented Multimedia Programming /* Avatar3D Name: Dr. Parson /* Assignment: 2 preparation. /* *********************************************************/ /* KEYBOARD COMMANDS: /* 'b' toggles display of bounding boxes for debugging, initially on /* 'f' toggles freezing of display in draw() off / on. /* 'v' toggles isImmobile to inhibit/enable calls to Avatar.move(); /* 'm' toggles issmear mode for no-erase painting. /* '~' applies shuffle() to each Avatar object, repositioning the mobile ones. /* '!' applies forceshuffle() to each Avatar object, repositioning all of them. /* 'p' sets perspective projection; 'o' sets orthographic * 'u' when held down moves camera up in Z direction slowly * 'U' when held down moves camera up in Z direction quickly * 'd' when held down moves camera down in Z direction slowly * 'D' when held down moves camera down in Z direction quickly * 'n' when held down moves camera up in Y direction slowly * 'N' when held down moves camera up in Y direction quickly * 's' when held down moves camera down in Y direction slowly * 'S' when held down moves camera down in Y direction quickly * 'e' when held down moves camera right in X direction slowly * 'E' when held down moves camera right in X direction quickly * 'w' when held down moves camera left in X direction slowly * 'W' when held down moves camera left in X direction quickly * 'x' when held down rotates image positive degrees around x * 'X' when held down rotates image negative degrees around x * 'y' when held down rotates image positive degrees around y * 'Y' when held down rotates image negative degrees around y * 'z' when held down rotates image positive degrees around z * 'Z' when held down rotates image negative degrees around z * 'R' resets to original camera point of view * SPACE BAR held down moves camera x,y to mouseX*2-width, mouseY*2-height */ // GLOBALS ADDED FOR 3D FURNITURE: int minimumZ, maximumZ ; // initialized in setup. // Assignment 1 GLOBAL VARIABLES are for the collection of Avatar objects. // All Avatar state variables go inside of Avatar-derived subclasses. Avatar avatars [] ; // An array holding multiple Avatar objects. int backgroundColor = 0 ; // black boolean showBoundingBox = true ; // toggle with 'b' key boolean isFrozen = false ; // toggle with 'f' key to freeze display boolean isImmobile = false ; // toggle with 'v' key to freeze display // Added 9/15/2018 for camera manipulation: float xeye, yeye, zeye ; // locations of the camera's eye in 3D space // Next 3 variables rotate the world from the camera's point of view. float worldxrotate = 0.0, worldyrotate = 0.0, worldzrotate = 0.0 ; // Some basic symbolic constants. final float degree = radians(1.0), around = radians(360.0); boolean isortho = false ; // 'o' sets to true, 'p' to false for perspective boolean issmear = false ; void setup() { // setup() runs once when the sketch starts, initializes sketch state. size(1200, 900, P3D); // fullScreen(P3D); background(backgroundColor); maximumZ = height / 2 ; // front of the scene minimumZ = - height / 2 ; // back of the scene avatars = new Avatar [ 4 ] ; // reduced from 50 to reduce CPU load avatars[0] = new Avatar3D(width/4, height/4, 0, 2, 3, 1, 2); for (int i = 0 ; i < avatars.length ; i++) { // STUDENT BE CAREFUL TO START i AT *YOUR* NEXT OPEN SLOT! int zspeed = round(random(-5,5)); if (zspeed == 0) { zspeed = 1 ; } avatars[i] = new Avatar3D(int(random(0,width)), int(random(0, height)), int(random(minimumZ, maximumZ))/2, round(random(1,5)), round(random(-5,-1)), zspeed, 1); } rectMode(CENTER); // I make them CENTER by default. rectMode is otherwise CORNER. ellipseMode(CENTER); imageMode(CENTER); shapeMode(CENTER); textAlign(CENTER, CENTER); // Added 9/15/2018, put the camera above the middle of the scene: xeye = width / 2 ; yeye = height / 2 ; zeye = (height*2) /* / tan(PI*30.0 / 180.0) */ ; } void draw() { // draw() is run once every frameRate, every 60th of a sec by default. if (isFrozen) { return ; } if (isortho) { ortho(); } else { perspective(); } if (! issmear) { background(backgroundColor); // This erases the previous frame. } // END EXPERIMENTAL */ rectMode(CENTER); ellipseMode(CENTER); imageMode(CENTER); shapeMode(CENTER); textAlign(CENTER, CENTER); moveCameraRotateWorldKeys(); // CAMERA ADDITION 9/15/2018, holding key repeats its action // Display & move all avatars in a for loop. for (int i = 0 ; i < avatars.length ; i++) { // Reinitialze these modes in case an Avatar changed them. rectMode(CENTER); ellipseMode(CENTER); imageMode(CENTER); shapeMode(CENTER); textAlign(CENTER, CENTER); stroke(0); noFill(); strokeWeight(1); push(); // Isolate the avatar internals from the outside world. if (! isImmobile) { avatars[i].move(); // Move before display so the bounding boxes are correct. } avatars[i].display(); pop(); } if (showBoundingBox && ! issmear) { // Do this in a separate loop so we can do the initial part once. rectMode(CORNER); noFill(); stroke(255-backgroundColor); strokeWeight(1); for (Avatar avt : avatars) { // For testing bounding box int [] bb = avt.getBoundingBox(); line(bb[0], bb[1], bb[2], bb[3], bb[1], bb[2]); // across back top line(bb[0], bb[4], bb[2], bb[3], bb[4], bb[2]); // across back bottom line(bb[0], bb[1], bb[5], bb[3], bb[1], bb[5]); // across front top line(bb[0], bb[4], bb[5], bb[3], bb[4], bb[5]); // across front bottom line(bb[0], bb[1], bb[2], bb[0], bb[4], bb[2]); // down back left line(bb[3], bb[1], bb[2], bb[3], bb[4], bb[2]); // down back right line(bb[0], bb[1], bb[5], bb[0], bb[4], bb[5]); // down front left line(bb[3], bb[1], bb[5], bb[3], bb[4], bb[5]); // down front right line(bb[0], bb[1], bb[2], bb[0], bb[1], bb[5]); // into top left line(bb[3], bb[1], bb[2], bb[3], bb[1], bb[5]); // into top right line(bb[0], bb[4], bb[2], bb[0], bb[4], bb[5]); // into bottom left line(bb[3], bb[4], bb[2], bb[3], bb[4], bb[5]); // into bottom right } } rectMode(CENTER); // back to defaults ellipseMode(CENTER); imageMode(CENTER); shapeMode(CENTER); textAlign(CENTER, CENTER); } // KEYBOARD COMMANDS documented at top of this sketch. // System calls keyPressed when user presses a *key*. // Examples of control characters like arrows in a later example. void keyPressed() { if (key == 'b') { // toggle bounding boxes on/off showBoundingBox = ! showBoundingBox ; } else if (key == 'f') { isFrozen = ! isFrozen ; } else if (key == 'v') { isImmobile = ! isImmobile ; } else if (key == '~') { for (int i = 0 ; i < avatars.length ; i++) { avatars[i].shuffle(); } } else if (key == '!') { for (Avatar a : avatars) { a.forceshuffle(); } } else if (key == 'R') { // Reset POV to starting point. xeye = width / 2 ; yeye = height / 2 ; zeye = (height*2) /* / tan(PI*30.0 / 180.0) */ ; worldxrotate = worldyrotate = worldzrotate = 0.0 ; } else if (key == 'o') { isortho = true ; } else if (key == 'p') { isortho = false ; } else if (key == 'm') { issmear = ! issmear ; } } /** 3D overlap checks whether two objects' bounding boxes overlap **/ boolean overlap(Avatar avatar1, Avatar avatar2) { int [] bb1 = avatar1.getBoundingBox(); int [] bb2 = avatar2.getBoundingBox(); // If bb1 is completely above, below, // left or right of bb2, we have an easy reject. if (bb1[0] > bb2[3] // bb1_left is right of bb2_right || bb1[1] > bb2[4] // bb1_top is below bb2_bottom || bb1[2] > bb2[5] // bb1_back is front of bb2_front || bb2[0] > bb1[3] // bb2_left is right of bb1_right || bb2[1] > bb1[4] // bb2_top is below bb1_bottom || bb2[2] > bb1[5] // bb2_back is front of bb1_front ) { return false ; } // In this case one contains the other or they overlap. return true ; } /** Return 1 for non-negative num, -1 for negative. **/ int signum(float num) { return ((num >= 0) ? 1 : -1); } // Added 9/15/2018 to move camera and rotate world when these keys are held down. void moveCameraRotateWorldKeys() { if (keyPressed) { if (key == 'u') { zeye += 10 ; // println("DEBUG u " + zeye + ", minZ: " + minimumZ + ", maxZ: " + maximumZ); } else if (key == 'U') { zeye += 100 ; // println("DEBUG U " + zeye + ", minZ: " + minimumZ + ", maxZ: " + maximumZ); } else if (key == 'd') { zeye -= 10 ; // println("DEBUG d " + zeye + ", minZ: " + minimumZ + ", maxZ: " + maximumZ); } else if (key == 'D') { zeye -= 100 ; // println("DEBUG D " + zeye + ", minZ: " + minimumZ + ", maxZ: " + maximumZ); } else if (key == 'n') { yeye -= 1 ; } else if (key == 'N') { yeye -= 10 ; } else if (key == 's') { yeye += 1 ; } else if (key == 'S') { yeye += 10 ; } else if (key == 'w') { xeye -= 1 ; } else if (key == 'W') { xeye -= 10 ; } else if (key == 'e') { xeye += 1 ; } else if (key == 'E') { xeye += 10 ; } else if (key == 'x') { worldxrotate += degree ; if (worldxrotate >= around) { worldxrotate = 0 ; } } else if (key == 'X') { worldxrotate -= degree ; if (worldxrotate < -around) { worldxrotate = 0 ; } } else if (key == 'y') { worldyrotate += degree ; if (worldyrotate >= around) { worldyrotate = 0 ; } } else if (key == 'Y') { worldyrotate -= degree ; if (worldyrotate < -around) { worldyrotate = 0 ; } } else if (key == 'z') { worldzrotate += degree ; if (worldzrotate >= around) { worldzrotate = 0 ; } } else if (key == 'Z') { worldzrotate -= degree ; if (worldzrotate < -around) { worldzrotate = 0 ; } } else if (mousePressed && key == ' ') { xeye = mouseX ; yeye = mouseY ; } } // Make sure 6th parameter -- focus in the Z direction -- is far, far away // towards the horizon. Otherwise, ortho() does not work. camera(xeye, yeye, zeye, xeye, yeye, zeye-signum(zeye-minimumZ)*maximumZ*2 , 0,1,0); if (worldxrotate != 0 || worldyrotate != 0 || worldzrotate != 0) { translate(width/2, height/2, 0); // rotate from the middle of the world if (worldxrotate != 0) { rotateX(worldxrotate); } if (worldyrotate != 0) { rotateY(worldyrotate); } if (worldzrotate != 0) { rotateZ(worldzrotate); } translate(-width/2, -height/2, 0); // Apply the inverse of the above translate. // Do not use push()-pop() instead of the inverse translate, // because pop() would discard the rotations. } } /** * An *interface* is a specification of methods (functions) that * subclasses must provide. It provides a means to specify requirements * that plug-in derived classes must provide. * This interface Avatar specifies functions for both mobile & immobile * objects that interact in this sketch. Added 3D getBoundingBox() for assn2. **/ interface Avatar { /** * Avatar-derived class must have one or more variable * data fields, at a minimum for the myx,myy,myz location, * where 0,0,0 is the Avatar's reference point after translate(myx, myy, myz). **/ /** Derived classes provide a constructor that takes some parameters. **/ /** * Write a display function that starts like this: push(); translate(myx, myy, myz); and ends like this: pop(); with all display code inside the function. Write this in your derived class, not here in Avatar. In addition to translate, the display() code in your class must use one or more of scale (with 1 or 2 arguments), rotate, shearX, or shearY. You can also manipulate variables for color & speed. See my example classes for ideas. **/ void display(); /** Write move() to update variable fields inside the object. * Write this in your derived class, not here in Avatar. **/ void move(); /** * getBoundingBox returns an array of 6 integers where elements * [0],[1],[2] have the minimum X,Y,Z coordinates respectively, and * [3],[4],[5] have the maximum X,Y,Z coordinates respectively. * This function always returns a cuboid bounding box that contains the * entire avatar. Coordinates are those in effect when display() or * move() are called from the draw() function, **/ int [] getBoundingBox(); /** Return the X coordinate of this avatar, center. **/ int getX(); /** Return the Y coordinate of this avatar's center. **/ int getY(); /** Return the Z coordinate of this avatar's center. **/ int getZ(); /** Randomize parts of a *mobile* object's space, including x,y,z location. **/ void shuffle() ; /** Randomize parts of *every* object's space, including x,y,z location. **/ void forceshuffle(); } /** * An abstract class provides helper functions and data fields required by * all subclasses. Abstract class CollisionDetector provides location and * scaling and rotation data fields that subclasses use. It also provides * helper functions, notably detectCollisions() for collision detection, that * are used by all subclasses. The keyword *protected* means that only subclasses * can use protected data & methods. The keyword *private* means that only the * defining class can use them, and *public* means that any class can use them. **/ abstract class CollisionDetector implements Avatar { protected int myx, myy, myz ; // x,y,z location of this object protected float myscale ; // scale of this object, 1.0 for no scaling protected float speedX ; // speed of motion, negative for left. protected float speedY ; // speed of motion, negative for up. protected float speedZ ; // speed of motion, negative for away from front. float myrotZ = 0.0 ; // subclasses may rotate & scale in other dimensions float rotZspeed = 0.0, sclspeed = 0.0 ; // subclasses may change myscale, myrotZ in move(). // Testing shows that mobile shapes may push other mobile shapes // off of the screen, depending on order of collision detection. // Some Avatar classes may want their displays to wander around outside. // Data field xlimit and ylimit test for that. // See java.lang.Integer in https://docs.oracle.com/javase/8/docs/api/index.html protected int xlimitleft = Integer.MIN_VALUE ; // no limit by default protected int ylimittop = Integer.MIN_VALUE ; // no limit by default protected int xlimitright = Integer.MAX_VALUE ; // no limit by default protected int ylimitbottom = Integer.MAX_VALUE ; // no limit by default protected int zlimitmin = minimumZ ; // default for drawing protected int zlimitmax = maximumZ ; // default for drawing // The constructor initializes the data fields. CollisionDetector(int avx, int avy, int avz, float spdx, float spdy, float spdz, float avscale, float scalespeed, float rotation, float rotatespeed) { myx = avx ; myy = avy ; myz = avz ; speedX = spdx ; speedY = spdy ; speedZ = spdz ; myscale = avscale ; sclspeed = scalespeed ; myrotZ = rotation ; rotZspeed = rotatespeed ; } void shuffle() { // default is to do nothing; override this in derived class. } void forceshuffle() { // default is to change location; add to this in derived class. myx = round(random(10, width-10)); // Put it somewhere on the display. myy = round(random(10, height-10)); myz = round(random(minimumZ/4, maximumZ/4)); // don't go too far out } int getX() { return myx ; } int getY() { return myy ; } int getZ() { return myz ; } // Check this object against every other Avatar object for a collision. // Also make sure it doesn't wander outside the x and y limit values // set by the constructor. Putting detectCollisions() in this abstract class // eliminates the need to put it into multiple derived class move() functions, // which can simply call this function. protected void detectCollisions() { int [] mine = getBoundingBox(); for (Avatar a : avatars) { if (a == this) { continue ; // this avatar always overlaps with itself } int [] theirs = a.getBoundingBox(); if (overlap(this,a)) { if (mine[0] >= theirs[0] && mine[0] <= theirs[3]) { // my left side is within them, move to the right speedX = abs(speedX); myx += 2*speedX ; // jump away a little extra } else if (mine[3] >= theirs[0] && mine[3] <= theirs[3]) { // my right side is within them, move to the left speedX = - abs(speedX); myx += 2*speedX ; } // Above may have eliminated the overlap, check before proceeding. mine = getBoundingBox(); if (overlap(this,a)) { // Do equivalent check for vertical overlap. if (mine[1] >= theirs[1] && mine[1] <= theirs[4]) { speedY = abs(speedY); // my top, send it down myy += 2*speedY ; } else if (mine[4] >= theirs[1] && mine[4] <= theirs[4]) { speedY = - abs(speedY); // my bottom, send it up myy += 2*speedY ; } } // Z test added for assignment 2 3D. mine = getBoundingBox(); if (overlap(this,a)) { // Do equivalent check for vertical overlap. if (mine[2] >= theirs[2] && mine[2] <= theirs[5]) { speedZ = abs(speedZ); myz += 2*speedZ ; } else if (mine[5] >= theirs[2] && mine[5] <= theirs[5]) { speedZ = - abs(speedZ); myz += 2*speedZ ; } } } } // Testing shows that mobile shapes may push other mobile shapes // off of the screen or thru Avatars, depending on order of collision detection. // Some Avatar classes may want their displays to wander around outside the display. // Data fields xlimit and ylimit test for that. if (xlimitleft != Integer.MIN_VALUE && myx <= xlimitleft && speedX < 0) { speedX = - speedX ; myx = xlimitleft + 1 ; // if (myscale >= 1) println("DEBUG WENT OFF LEFT " + speedX); // Too many print statements, restrict to the bigger Avatars. // I usually comment out print statements until I am sure the bug is gone. } if (xlimitright != Integer.MAX_VALUE && myx >= xlimitright && speedX > 0) { speedX = - speedX ; myx = xlimitright - 1 ; // if (myscale >= 1) println("DEBUG WENT OFF RIGHT " + speedX); } if (ylimittop != Integer.MIN_VALUE && myy <= ylimittop && speedY < 0) { speedY = - speedY ; myy = ylimittop + 1 ; // if (myscale >= 1) println("DEBUG WENT OFF TOP " + speedY); } if (ylimitbottom != Integer.MAX_VALUE && myy >= ylimitbottom && speedY > 0) { speedY = - speedY ; myy = ylimitbottom - 1 ; // if (myscale >= 1) println("DEBUG WENT OFF BOTTOM " + speedY); } if (myz <= zlimitmin && speedZ < 0) { speedZ = - speedZ ; myz = zlimitmin + 1 ; // if (myscale >= 1) println("DEBUG WENT OFF BACK " + speedY); } if (myz >= zlimitmax && speedZ > 0) { speedZ = - speedZ ; myz = zlimitmax - 1 ; // if (myscale >= 1) println("DEBUG WENT OFF FRONT " + speedY); } } } /** * Avatar3D is my Avatar-derived class that displays & moves a mobile Avatar3D. * You must write your own Avatar-derived class. You can delete class Avatar3D * or use it as a starting point for your re-named class. Document what your * class adds or changes at the top of the class declaration like this. **/ class Avatar3D extends CollisionDetector { /* The data fields store the state of the Avatar. */ int boxsize = 100 ; // set a constant to be used multiple places float rotateAmount = 0, rotateSpeed = 0.25 ; // rotateing around Y Avatar3D(int avx, int avy, int avz, float spdx, float spdy, float spdz, float avscale) { super(avx,avy,avz,spdx,spdy,spdz,avscale,0,0,0); // Call the base class constructor to initialize its data fields, // then initialize this class' data fields. xlimitright = width ; ylimitbottom = height ; // limit off-screen motion to xlimitleft = 0 ; // one width or height off the display ylimittop = 0 ; // in either direction } void shuffle() { forceshuffle(); // always do it. } // The display() function simply draws the Avatar object. // The move() function pdates the Avatar object's state. void display() { // Draw the avatar. push(); // STUDENT *MUST* use push() & translate first in display(). translate(myx, myy, myz); scale(myscale); if (issmear) { stroke(backgroundColor); // noStroke(); // don't use outline when painting strokeWeight(4); // black out the box color in the painting } else { stroke(255-backgroundColor); strokeWeight(1); } push(); // Do a nested push before transforms for box. // Leave the box transforms in effect for the spheres that decorate it. fill(255,0,255); rotateY(radians(rotateAmount)); // spin around the Y axis // The box functions does not take coordinates, so you must translate // for it box(boxsize, boxsize, boxsize); // width,height,depth are = to make a perfect cube // Put elongated spheres at each corner vertex of the box. // The fill() calls go before their nested pushes because I want to apply // the same color to two spheroids in a row. If they were inside the // innnermost push, its pop would undo them. pushMatrix/popMatrix would // not pop fill or stroke, only translate/rotate/scale/shearX/shearY. fill(255, 255, 0); // yellow noStroke(); push(); // isolate the sphere's transforms translate(-boxsize/2, -boxsize/2, -boxsize/2); // left, top, back scale(1, 1, 2); // stretch it in the Z direction sphere(boxsize/8); // This is the RADIUS, not diameter pop(); // pop off this box's transforms. Then, do next box. push(); translate(boxsize/2, -boxsize/2, -boxsize/2); // right, top, back scale(1, 1, 2); // stretch it in the Z direction sphere(boxsize/8); pop(); fill(255, 0, 0); // red push(); translate(-boxsize/2, boxsize/2, -boxsize/2); // left, bottom, back scale(2, 1, 1); // stretch it in the X direction sphere(boxsize/8); pop(); push(); translate(boxsize/2, boxsize/2, -boxsize/2); // right, bottom, back scale(2, 1, 1); // stretch it in the X direction sphere(boxsize/8); pop(); fill(0, 255, 255); // cyan push(); translate(-boxsize/2, -boxsize/2, boxsize/2); // left, top, front scale(1, 2, 1); // stretch it in the Y direction sphere(boxsize/8); pop(); push(); translate(boxsize/2, -boxsize/2, boxsize/2); // right, top, front scale(1, 2, 1); // stretch it in the Y direction sphere(boxsize/8); pop(); fill(0, 0, 255); // blue push(); translate(-boxsize/2, boxsize/2, boxsize/2); // left, bottom, front // No scale, plain sphere. sphere(boxsize/8); pop(); push(); translate(boxsize/2, boxsize/2, boxsize/2); // right, bottom, front // No scale, plain sphere. sphere(boxsize/8); pop(); pop() ; // pop the push() that ran just before the box transforms. pop(); // STUDENT *MUST* use pop() last in display(). } // The move() function updates the Avatar object's state. void move() { // get ready for movement in next frame. myx = round(myx + speedX) ; myy = round(myy + speedY) ; myz = round(myz + speedZ); rotateAmount = rotateAmount + rotateSpeed ; detectCollisions(); } int [] getBoundingBox() { int [] result = new int[6]; // without rotation & sphere decorations, boxsize/2 would be the extents. // I am using twice the minimal size for collision avoidance. result[0] = myx - round(myscale*boxsize); result[1] = myy - round(myscale*boxsize); result[2] = myz - round(myscale*boxsize); result[3] = myx + round(myscale*boxsize); result[4] = myy + round(myscale*boxsize); result[5] = myz + round(myscale*boxsize); return result ; } }