diff --git a/.gitignore b/.gitignore index 48326a70..c56ab43c 100644 --- a/.gitignore +++ b/.gitignore @@ -183,3 +183,5 @@ local.properties # IntelliJDEA ignore *.iml dedicatedServer/.idea/ +.idea/copyright/ +settings/keyBindings.xml diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index 93f33ab5..1ff92196 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -18,7 +18,6 @@ import shared.exceptions.InvalidRegattaDataException; import shared.exceptions.XMLReaderException; import shared.model.Bearing; import shared.model.Constants; -import shared.xml.XMLUtilities; import javax.xml.bind.JAXBException; import javax.xml.parsers.ParserConfigurationException; @@ -71,8 +70,6 @@ public class Event { private Thread connectionThread; - private int mapIndex; - @@ -82,32 +79,11 @@ public class Event { * @param singlePlayer Whether or not to create a single player event. * @throws EventConstructionException Thrown if we cannot create an Event for any reason. */ - public Event(boolean singlePlayer, int mapIndex) throws EventConstructionException { + public Event(boolean singlePlayer) throws EventConstructionException { -// System.out.println(XMLUtilities.validateXML(this.getClass().getClassLoader().getResource("mock/mockXML/iMapLayout.xml").toString() -// , this.getClass().getClassLoader().getResource("mock/mockXML/schema/raceSchema.xsd"))); - this.mapIndex = mapIndex; - String raceXMLFile; + String raceXMLFile = "mock/mockXML/raceThreePlayers.xml"; String boatsXMLFile = "mock/mockXML/boatTest.xml"; String regattaXMLFile = "mock/mockXML/regattaTest.xml"; - switch (mapIndex){ - case 0:raceXMLFile = "mock/mockXML/raceSixPlayers.xml"; - break; - case 1:raceXMLFile = "mock/mockXML/oMapLayout.xml"; - break; - case 2: raceXMLFile = "mock/mockXML/iMapLayout.xml"; - break; - case 3: raceXMLFile = "mock/mockXML/mMapLayout.xml"; - break; - case 4: - raceXMLFile = "mock/mockXML/raceTutorial.xml"; - boatsXMLFile = "mock/mockXML/boatTutorial.xml"; - regattaXMLFile = "mock/mockXML/regattaTutorial.xml"; - break; - default: raceXMLFile = "mock/mockXML/raceSixPlayers.xml"; - - } - if (singlePlayer) { raceXMLFile = "mock/mockXML/raceSinglePlayer.xml"; @@ -119,9 +95,7 @@ public class Event { //this.raceXML = RaceXMLCreator.alterRaceToWind(raceXMLFile, 90); this.raceXML = XMLReader.readXMLFileToString(raceXMLFile, StandardCharsets.UTF_8); this.raceXML = Event.setRaceXMLAtCurrentTimeToNow(XMLReader.readXMLFileToString(raceXMLFile, StandardCharsets.UTF_8)); - if(mapIndex==4){ - this.raceXML = Event.setRaceXMLAtCurrentTimeToNow(XMLReader.readXMLFileToString(raceXMLFile, StandardCharsets.UTF_8), 1000, 5000); - } + this.boatXML = XMLReader.readXMLFileToString(boatsXMLFile, StandardCharsets.UTF_8); this.regattaXML = XMLReader.readXMLFileToString(regattaXMLFile, StandardCharsets.UTF_8); @@ -204,15 +178,9 @@ public class Event { * @return String containing edited xml */ public static String setRaceXMLAtCurrentTimeToNow(String raceXML) { - return setRaceXMLAtCurrentTimeToNow(raceXML, Constants.RacePreStartTime, Constants.RacePreparatoryTime); - } - public static String setRaceXMLAtCurrentTimeToNow(String raceXML, long racePreStartTime, long racePreparatoryTime){ //The start time is current time + 4 minutes. prestart is 3 minutes, and we add another minute. - - long millisecondsToAdd = racePreStartTime + racePreparatoryTime; - - + long millisecondsToAdd = Constants.RacePreStartTime + 1 * 60 * 1000; long secondsToAdd = millisecondsToAdd / 1000; //Scale the time using our time scalar. secondsToAdd = secondsToAdd / Constants.RaceTimeScale; @@ -222,6 +190,7 @@ public class Event { raceXML = raceXML.replace("RACE_CREATION_TIME", dateFormat.format(creationTime)); raceXML = raceXML.replace("RACE_START_TIME", dateFormat.format(creationTime.plusSeconds(secondsToAdd))); + return raceXML; } diff --git a/racevisionGame/src/main/java/mock/model/SourceIdAllocator.java b/racevisionGame/src/main/java/mock/model/SourceIdAllocator.java index f590d9b7..fb0cfd5a 100644 --- a/racevisionGame/src/main/java/mock/model/SourceIdAllocator.java +++ b/racevisionGame/src/main/java/mock/model/SourceIdAllocator.java @@ -42,12 +42,14 @@ public class SourceIdAllocator { } List allocatedIDs = mockRace.getRaceDataSource().getParticipants(); - + System.out.println(allocatedIDs); List allIDs = new ArrayList<>(mockRace.getBoatDataSource().getBoats().keySet()); + System.out.println(allIDs); //Get list of unallocated ids. List unallocatedIDs = new ArrayList<>(allIDs); unallocatedIDs.removeAll(allocatedIDs); + System.out.println(unallocatedIDs.isEmpty()); if (!unallocatedIDs.isEmpty()) { diff --git a/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java b/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java index 0e538925..9b835f82 100644 --- a/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java +++ b/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java @@ -74,7 +74,7 @@ public class RaceXMLCreator { * @throws SAXException error in schema file * @throws ParserConfigurationException error in parsing the schema file */ - public static String alterRaceToWind(String s, double degrees, boolean tutorial) throws XMLReaderException, InvalidRaceDataException, JAXBException, IOException, SAXException, ParserConfigurationException { + public static String alterRaceToWind(String s, double degrees) throws XMLReaderException, InvalidRaceDataException, JAXBException, IOException, SAXException, ParserConfigurationException { RaceXMLReader reader = new RaceXMLReader(s, XMLFileType.ResourcePath); XMLRace race = XMLUtilities.xmlToClass( @@ -82,11 +82,7 @@ public class RaceXMLCreator { RaceXMLCreator.class.getClassLoader().getResource("mock/mockXML/schema/raceSchema.xsd"), XMLRace.class); - if(tutorial){ - setRaceXMLAtCurrentTimeToNow(race, 1000l, 5000l); - } else { - setRaceXMLAtCurrentTimeToNow(race); - } + setRaceXMLAtCurrentTimeToNow(race); double raceOriginalBearing = getLineAngle(getLeewardGate(reader).getMark1Position(), getWindwardGate(reader).getMark1Position()); @@ -97,7 +93,6 @@ public class RaceXMLCreator { return XMLUtilities.classToXML(race); } - /** * Rotate the features in a race such as the boundary, and the marks. * @param race the race to alter @@ -185,11 +180,14 @@ public class RaceXMLCreator { } + /** + * Sets the xml description of the race to show the race was created now, and starts in 4 minutes + * @param raceXML The race.xml contents. + */ + public static void setRaceXMLAtCurrentTimeToNow(XMLRace raceXML) { - - public static void setRaceXMLAtCurrentTimeToNow(XMLRace raceXML, long racePrestartTime, long racePreparatoryTime){ //The start time is current time + 4 minutes. prestart is 3 minutes, and we add another minute. - long millisecondsToAdd = racePrestartTime + racePreparatoryTime; + long millisecondsToAdd = Constants.RacePreStartTime + 1 * 60 * 1000; long secondsToAdd = millisecondsToAdd / 1000; //Scale the time using our time scalar. secondsToAdd = secondsToAdd / Constants.RaceTimeScale; @@ -200,13 +198,4 @@ public class RaceXMLCreator { raceXML.getRaceStartTime().setTime(dateFormat.format(creationTime.plusSeconds(secondsToAdd))); } - /** - * Sets the xml description of the race to show the race was created now, and starts in 4 minutes - * @param raceXML The race.xml contents. - */ - public static void setRaceXMLAtCurrentTimeToNow(XMLRace raceXML) { - setRaceXMLAtCurrentTimeToNow(raceXML, Constants.RacePreStartTime, Constants.RacePreparatoryTime); - - } - } diff --git a/racevisionGame/src/main/java/shared/model/Boat.java b/racevisionGame/src/main/java/shared/model/Boat.java index 31ce7ad4..1e4c0f09 100644 --- a/racevisionGame/src/main/java/shared/model/Boat.java +++ b/racevisionGame/src/main/java/shared/model/Boat.java @@ -411,14 +411,14 @@ public class Boat extends Collider { @Override public boolean rayCast(Boat boat) { if(boat != this) { - return rayCast(boat, 100); + return rayCast(boat, 15); } else return false; } @Override public void onCollisionEnter(Boat collider, Collision e) { if(e.getBearing().degrees() > 270 || e.getBearing().degrees() < 90) { - collider.bounce(100); + collider.bounce(15); } } } diff --git a/racevisionGame/src/main/java/shared/model/Constants.java b/racevisionGame/src/main/java/shared/model/Constants.java index 7a5e2820..26c56d11 100644 --- a/racevisionGame/src/main/java/shared/model/Constants.java +++ b/racevisionGame/src/main/java/shared/model/Constants.java @@ -39,14 +39,14 @@ public class Constants { * The race pre-start time, in milliseconds. 3 minutes (30 seconds for development). */ // public static final long RacePreStartTime = 30 * 1000; - public static final long RacePreStartTime = 1000; + public static final long RacePreStartTime = 6 * 1000; /** * The race preparatory time, in milliseconds. 1 minute. */ // public static final long RacePreparatoryTime = 60 * 1000; - public static final long RacePreparatoryTime = 1 * 60 * 1000; + public static final long RacePreparatoryTime = 6 * 1000; diff --git a/racevisionGame/src/main/java/shared/model/Mark.java b/racevisionGame/src/main/java/shared/model/Mark.java index d236d076..b4025b8e 100644 --- a/racevisionGame/src/main/java/shared/model/Mark.java +++ b/racevisionGame/src/main/java/shared/model/Mark.java @@ -29,11 +29,6 @@ public class Mark extends Collider{ */ private GPSCoordinate position; - /** - * Repulsion radius of the mark - */ - private double repulsionRadius = 50; - /** * Constructs a mark with a given source ID, name, and position. * @param sourceID The source ID of the mark. @@ -97,11 +92,11 @@ public class Mark extends Collider{ @Override public boolean rayCast(Boat boat) { - return rayCast(boat, repulsionRadius); + return rayCast(boat, 15); } @Override public void onCollisionEnter(Boat collider, Collision e) { - collider.bounce(repulsionRadius); + collider.bounce(15); } } diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java index 2d710e75..9fd35e2c 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -8,11 +8,13 @@ import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; +import javafx.scene.AmbientLight; +import javafx.scene.PointLight; import javafx.scene.chart.LineChart; import javafx.scene.control.*; +import javafx.scene.effect.Light; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; -import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; @@ -32,8 +34,7 @@ import visualiser.app.App; import visualiser.enums.TutorialState; import visualiser.gameController.ControllerClient; import visualiser.gameController.Keys.ControlKey; -import visualiser.layout.Subject3D; -import visualiser.layout.View3D; +import visualiser.layout.*; import visualiser.model.*; import visualiser.utils.GPSConverter; @@ -208,46 +209,85 @@ public class RaceController extends Controller { private void initialiseView3D(VisualiserRaceEvent race) { viewSubjects = FXCollections.observableArrayList(); + AmbientLight ambientLight = new AmbientLight(new Color(1, 1, 1, 0.75)); + ambientLight.setTranslateX(250); + ambientLight.setTranslateZ(210); + ambientLight.setLightOn(true); + + PointLight pointLight = new PointLight(); + ambientLight.setTranslateX(250); + ambientLight.setTranslateZ(210); + ambientLight.setLightOn(true); + // Import boat mesh URL asset = HostController.class.getClassLoader().getResource("assets/V1.2 Complete Boat.stl"); StlMeshImporter importer = new StlMeshImporter(); importer.read(asset); // Configure camera angles and control - view3D = new View3D(); + URL markerAsset = HostController.class.getClassLoader().getResource("assets/Bouy V1.1.stl"); + StlMeshImporter importerMark = new StlMeshImporter(); + importerMark.read(markerAsset); + + URL alternateBoatAsset = HostController.class.getClassLoader().getResource("assets/V1.3 BurgerBoat.stl"); + StlMeshImporter importerBurgerBoat = new StlMeshImporter(); + importerBurgerBoat.read(alternateBoatAsset); + + view3D = new View3D(false); + view3D.setItems(viewSubjects); view3D.setDistance(1050); - view3D.setYaw(0); - view3D.setPitch(60); + view3D.setBirdsEye(); view3D.enableTracking(); - //newPane.getChildren().add(view3D); + view3D.addAmbientLight(ambientLight); + view3D.addPointLight(pointLight); canvasBase.add(view3D, 0, 0); // Set up projection from GPS to view RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource(); final GPSConverter gpsConverter = new GPSConverter(raceData, 450, 450); - view3D.setItems(viewSubjects); + // Set up sea surface + SeaSurface sea = new SeaSurface(750, 200); + sea.setX(250); + sea.setZ(210); + viewSubjects.add(sea); + + SkyBox skyBox = new SkyBox(750, 200, 250, 0, 210); + viewSubjects.addAll(skyBox.getSkyBoxPlanes()); + + Boundary3D boundary3D = new Boundary3D(visualiserRace.getVisualiserRaceState().getRaceDataSource().getBoundary(), gpsConverter); + for (Subject3D subject3D: boundary3D.getBoundaryNodes()){ + viewSubjects.add(subject3D); + } // Position and add each mark to view for(Mark mark: race.getVisualiserRaceState().getMarks()) { - Subject3D subject = new Subject3D(new Sphere(2), mark.getSourceID()); - subject.setX(gpsConverter.convertGPS(mark.getPosition()).getX()); - subject.setZ(gpsConverter.convertGPS(mark.getPosition()).getY()); + MeshView mesh = new MeshView(importerMark.getImport()); + Subject3D markModel = new Subject3D(mesh, mark.getSourceID()); + + markModel.setX(gpsConverter.convertGPS(mark.getPosition()).getX()); + markModel.setZ(gpsConverter.convertGPS(mark.getPosition()).getY()); - viewSubjects.add(subject); + viewSubjects.add(markModel); } // Position and add each boat to view for(VisualiserBoat boat: race.getVisualiserRaceState().getBoats()) { - MeshView mesh = new MeshView(importer.getImport()); - Subject3D subject = new Subject3D(mesh, boat.getSourceID()); - viewSubjects.add(subject); + MeshView mesh; + if(boat.getSourceID() == race.getVisualiserRaceState().getPlayerBoatID()) { + mesh = new MeshView(importer.getImport()); + } else { + mesh = new MeshView(importerBurgerBoat.getImport()); + } + Subject3D boatModel = new Subject3D(mesh, boat.getSourceID()); + + viewSubjects.add(boatModel); // Track this boat's movement with the new subject AnimationTimer trackBoat = new AnimationTimer() { @Override public void handle(long now) { - subject.setHeading(boat.getBearing().degrees()); - subject.setX(gpsConverter.convertGPS(boat.getPosition()).getX()); - subject.setZ(gpsConverter.convertGPS(boat.getPosition()).getY()); + boatModel.setHeading(boat.getBearing().degrees()); + boatModel.setX(gpsConverter.convertGPS(boat.getPosition()).getX()); + boatModel.setZ(gpsConverter.convertGPS(boat.getPosition()).getY()); } }; trackBoat.start(); diff --git a/racevisionGame/src/main/java/visualiser/layout/Boundary3D.java b/racevisionGame/src/main/java/visualiser/layout/Boundary3D.java new file mode 100644 index 00000000..beb59914 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/layout/Boundary3D.java @@ -0,0 +1,89 @@ +package visualiser.layout; + +import com.sun.corba.se.impl.orbutil.graph.Graph; +import javafx.scene.shape.Box; +import javafx.scene.shape.MeshView; +import javafx.scene.shape.Rectangle; +import javafx.scene.shape.Sphere; +import shared.model.GPSCoordinate; +import visualiser.model.GraphCoordinate; +import visualiser.utils.GPSConverter; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class that creates a 3d boundary based on gps coordinates + */ +public class Boundary3D { + + public static double thickness = 1; + private List boundaryNodes = new ArrayList<>(); + private List boundaryConnectors = new ArrayList<>(); + private GPSConverter gpsConverter; + + public Boundary3D(List points, GPSConverter gpsConverter){ + this.gpsConverter = gpsConverter; + setBoundaryNodes(points); + } + + /** + * Splits up the list so that it generates a edge of the boundary + * @param points boundary gpscoordinate + */ + private void setBoundaryNodes(List points){ + if (points.size() < 2){ + return; + } + System.out.println(points.size()); + for (int i = 0; i < points.size(); i++){ + if (i + 1 != points.size()){ + addBound(points.get(i), points.get(i + 1)); + } else { + addBound(points.get(i), points.get(0)); + } + } + } + + /** + * Add a two point boundary this will create a sphere at coord1 and a line to coord 2 + * this is to reduce double up (2 spheres in one point). + * @param coord1 point to make sphere and start the line. + * @param coord2 point to end the line. + */ + private void addBound(GPSCoordinate coord1, GPSCoordinate coord2){ + GraphCoordinate graphCoord1 = gpsConverter.convertGPS(coord1); + GraphCoordinate graphCoord2 = gpsConverter.convertGPS(coord2); + GraphCoordinate avgCoord = new GraphCoordinate((graphCoord1.getX() + graphCoord2.getX()) / 2, + (graphCoord1.getY() + graphCoord2.getY()) / 2); + + double a = (graphCoord1.getX() - graphCoord2.getX()); + double b = (graphCoord1.getY() - graphCoord2.getY()); + double c = Math.sqrt(a * a + b * b); + + + Subject3D bound1 = new Subject3D(new Sphere(thickness * 2)); + bound1.setX(graphCoord1.getX()); + bound1.setZ(graphCoord1.getY()); + boundaryNodes.add(bound1); + + Subject3D connector = new Subject3D(new Box(c, thickness, thickness)); + connector.setX(avgCoord.getX()); + connector.setZ(avgCoord.getY()); + double angle = 90 + Math.toDegrees(GPSConverter.getAngle(graphCoord2, graphCoord1)); + connector.setHeading(angle); + + boundaryConnectors.add(connector); + } + + /** + * get the 3d objects to draw + * @return 3d boundary to draw + */ + public List getBoundaryNodes(){ + //these two must be concatenated with nodes after connectors else the spheres will not overlap the lines + ArrayList result = new ArrayList<>(boundaryConnectors); + result.addAll(boundaryNodes); + return result; + } +} diff --git a/racevisionGame/src/main/java/visualiser/layout/Plane3D.java b/racevisionGame/src/main/java/visualiser/layout/Plane3D.java new file mode 100644 index 00000000..bfdfdc22 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/layout/Plane3D.java @@ -0,0 +1,130 @@ +package visualiser.layout; + +import com.sun.javafx.geom.PickRay; +import com.sun.javafx.scene.input.PickResultChooser; +import com.sun.javafx.sg.prism.NGNode; +import javafx.scene.Node; +import javafx.scene.shape.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 3D plane + */ +public class Plane3D extends TriangleMesh{ + + /** + * Length is up down, and width is left right. Drawn on the x-y plane with z kept at 0. + * @param width width of the plane + * @param length length of the plane + * @param subdivisionsWidth number of divisions along the width of the plane + * @param subdivisionsLength number of division along the length of the plane + */ + public Plane3D(float width, float length, int subdivisionsWidth, int subdivisionsLength){ + //add texture points and vertex points + float subWidth = width / (float) subdivisionsWidth; + float subLength = length / (float) subdivisionsLength; + + ArrayList pointsList = new ArrayList<>(); + ArrayList textureCoord = new ArrayList<>(); + float startW = -width/2; + float startL = -length/2; + + for (float l = 0; l <= length; l += subLength) { + for (float w = 0; w <= width; w += subWidth){ + //add points + pointsList.add(w + startW); + pointsList.add(l + startL); + pointsList.add(0f); + //addTexture coords + textureCoord.add(1 - w/width); + textureCoord.add(1 - l/length); + } + } + + this.getPoints().setAll(copyListToArray(pointsList)); + this.getTexCoords().setAll(copyListToArray(textureCoord)); + + //connect points to make faces + ArrayList faces = new ArrayList<>(); + + int listSize = pointsList.size()/3; + int divsInRow = subdivisionsWidth + 1; + for (int i = 0; i < listSize; i++){ + int row = i/divsInRow; + + if (row < 1){ + continue; + } + + boolean notFirstCol = (i) % divsInRow != 0; + boolean notLastCol = (i + 1) % divsInRow != 0; + if (notFirstCol){ + faces.add(i); + faces.add(i); +// printPointAtIndex(i); + faces.add(i - divsInRow); + faces.add(i - divsInRow); +// printPointAtIndex(i - divsInRow); + faces.add(i - 1); + faces.add(i - 1); +// printPointAtIndex(i-1); + } + if (notLastCol) { + faces.add(i - divsInRow + 1); + faces.add(i - divsInRow + 1); +// printPointAtIndex(i - divsInRow + 1); + faces.add(i - divsInRow); + faces.add(i - divsInRow); +// printPointAtIndex(i - divsInRow); + faces.add(i); + faces.add(i); +// printPointAtIndex(i); + } + + } + this.getFaces().setAll(copyListToIntArray(faces)); + } + + /** + * Testing function to see if the points are correct + * @param index index that the points correspond to (remember 3 is a point) + */ + private void printPointAtIndex(int index){ + int i = index * 3; + float x = this.getPoints().get(i); + float y = this.getPoints().get(i + 1); + float z = this.getPoints().get(i + 2); + System.out.println(String.format("Point at %d is x:%f, y:%f, z:%f", index, x, y, z)); + } + + /** + * copies the list to a float array because java List.toArray isn't working + * @param list list to copy + * @return array + */ + private static float[] copyListToArray(List list){ + float[] res = new float[list.size()]; + for (int i = 0; i < list.size(); i++){ + res[i] = list.get(i); + } + return res; + } + + /** + * copies the list to an integer array because java List.toArray isn't working + * @param list list to copy + * @return array + */ + private static int[] copyListToIntArray(List list){ + int[] res = new int[list.size()]; + for (int i = 0; i < list.size(); i++){ + res[i] = list.get(i); + } + return res; + } + + +} diff --git a/racevisionGame/src/main/java/visualiser/layout/SeaSurface.java b/racevisionGame/src/main/java/visualiser/layout/SeaSurface.java new file mode 100644 index 00000000..57056556 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/layout/SeaSurface.java @@ -0,0 +1,126 @@ +package visualiser.layout; + +import javafx.geometry.Point3D; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.image.PixelWriter; +import javafx.scene.image.WritableImage; +import javafx.scene.paint.Color; +import javafx.scene.paint.PhongMaterial; +import javafx.scene.shape.Box; +import javafx.scene.shape.MeshView; +import javafx.scene.shape.Shape3D; +import javafx.scene.shape.TriangleMesh; +import visualiser.utils.PerlinNoiseGenerator; + +/** + * Creates a SeaSurface + */ +public class SeaSurface extends Subject3D { + /** + * Sea Surface Constructor + * @param size size of the sea surface (has to be square for simplicity's sake) + * @param freq frequency the perlin noise is to be generated at + */ + public SeaSurface(int size, double freq){ + super(createSurface(size, freq)); + } + + /** + * Creates the sea surface + */ + private static Shape3D createSurface(int size, double freq){ + float[][] noiseArray = PerlinNoiseGenerator.createNoise(size, freq); + Image diffuseMap = createImage(noiseArray.length, noiseArray); + + PhongMaterial material = new PhongMaterial(); + material.setDiffuseMap(diffuseMap); + + Plane3D seaPlane = new Plane3D(noiseArray.length, noiseArray.length, 10, 10); + MeshView seaSurface = new MeshView(seaPlane); + seaSurface.setMaterial(material); + seaSurface.setMouseTransparent(true); + seaSurface.toFront(); + + return seaSurface; + } + + /** + * Create texture for uv mapping + * @param size size of the image to make + * @param noise array of noise + * @return image that is created + */ + private static Image createImage(double size, float[][] noise) { + + int width = (int) size; + int height = (int) size; + + WritableImage wr = new WritableImage(width, height); + PixelWriter pw = wr.getPixelWriter(); + //interpolate colours based on noise + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + + float value = noise[x][y]; + + double gray = normalizeValue(value, -.5, .5, 0., 1.); + + gray = clamp(gray, 0, 1); + + //values to interpolate on + Color brightBlue = new Color(0.06, 0.5, .78, 1); + Color lightBlue = new Color(0.15, 0.68, .88, 1); + Color lighterBlue = new Color(0.28, 0.73, .91, 1); + + Color colour = Color.WHITE.interpolate(brightBlue, gray).interpolate(lighterBlue, gray).interpolate(lightBlue, gray); + + pw.setColor(x, y, colour); + + } + } + + return wr; + + } + + /** + * Nomalises the values so that the colours are correct + * @param value value to normalise + * @param min current min + * @param max current max + * @param newMin new min + * @param newMax new max + * @return returns normalised value + */ + private static double normalizeValue(double value, double min, double max, double newMin, double newMax) { + + return (value - min) * (newMax - newMin) / (max - min) + newMin; + + } + + /** + * clamps a value between a min and max + * @param value value to clamp + * @param min minimum value it can be + * @param max maximum value it can be + * @return result after clamp + */ + private static double clamp(double value, double min, double max) { + + if (Double.compare(value, min) < 0) + return min; + + if (Double.compare(value, max) > 0) + return max; + + return value; + } + + /** + * Prevent rescaling of sea surface + * @param scale ignored + */ + @Override + public void setScale(double scale) {} +} diff --git a/racevisionGame/src/main/java/visualiser/layout/SkyBox.java b/racevisionGame/src/main/java/visualiser/layout/SkyBox.java new file mode 100644 index 00000000..72c1e89d --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/layout/SkyBox.java @@ -0,0 +1,162 @@ +package visualiser.layout; + +import javafx.geometry.Point3D; +import javafx.scene.image.Image; +import javafx.scene.image.PixelWriter; +import javafx.scene.image.WritableImage; +import javafx.scene.paint.Color; +import javafx.scene.paint.PhongMaterial; +import javafx.scene.shape.MeshView; +import javafx.scene.transform.Rotate; +import visualiser.utils.PerlinNoiseGenerator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Creates a skyBox + */ +public class SkyBox { + private int size; + private double x; + private double y; + private double z; + private double freq; + private List skyBoxPlanes = new ArrayList<>(); + + public SkyBox(int edgeLength, double freq, double x, double y, double z) { + this.size = edgeLength; + this.x = x; + this.y = y; + this.z = z; + this.freq = freq; + makeSkyBox(); + } + + private void makeSkyBox() { + addTop(); + addFront(); + addBack(); + addLeft(); + addRight(); + addSeaOverlay(); + } + + private void addTop() { + MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/skyTop.png"))); + + surface.setRotationAxis(new Point3D(0, 0, 1)); + surface.setRotate(180); + + surface.setTranslateX(x); + surface.setTranslateY(y - size + 1); + surface.setTranslateZ(z); + + Subject3D top = new Subject3D(surface); + skyBoxPlanes.add(top); + } + + private void addRight() { + MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/skyRight.png"))); + + surface.setTranslateX(size/2); + surface.setTranslateY(size/2); + surface.setRotationAxis(new Point3D(1, 0, 0)); + surface.setRotate(90); + surface.setTranslateX(-size/2); + surface.setTranslateY(-size/2); + + surface.setTranslateX(x); + surface.setTranslateY(y - size/2); + surface.setTranslateZ(z + size/2 - 1); + + + Subject3D right = new Subject3D(surface); + skyBoxPlanes.add(right); + } + + private void addLeft() { + MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/skyLeft.png"))); + + surface.setTranslateX(size/2); + surface.setTranslateY(size/2); + surface.setRotationAxis(new Point3D(1, 0, 0)); + surface.setRotate(-90); + surface.setTranslateX(-size/2); + surface.setTranslateY(-size/2); + + surface.setScaleX(-1); + surface.setScaleZ(-1); + + surface.setTranslateX(x); + surface.setTranslateY(y - size/2); + surface.setTranslateZ(z - size/2 + 1); + + + Subject3D left = new Subject3D(surface); + skyBoxPlanes.add(left); + } + + private void addBack() { + MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/skyBack.png"))); + surface.getTransforms().add(new Rotate(90, 0, 0)); + + surface.setRotationAxis(new Point3D(1, 0, 0)); + surface.setRotate(-90); + + surface.setScaleY(-1); + surface.setScaleZ(-1); + + surface.setTranslateX(x - size/2 + 1); + surface.setTranslateY(y - size/2); + surface.setTranslateZ(z); + + Subject3D back = new Subject3D(surface); + skyBoxPlanes.add(back); + } + + private void addFront() { + MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/skyFront.png"))); + + surface.setTranslateX(size/2); + surface.setTranslateY(size/2); + surface.setRotationAxis(new Point3D(0, 0, 1)); + surface.setRotate(-90); + surface.setTranslateX(-size/2); + surface.setTranslateY(-size/2); + + surface.setTranslateX(x + size/2 - 1); + surface.setTranslateY(y - size/2); + surface.setTranslateZ(z); + + Subject3D front = new Subject3D(surface); + skyBoxPlanes.add(front); + } + + private MeshView makeSurface(Image diffuseMap) { + + PhongMaterial material = new PhongMaterial(); + //material.setDiffuseColor(Color.BLUE); + material.setDiffuseMap(diffuseMap); + //material.setSpecularColor(Color.WHITE); + + Plane3D plane = new Plane3D(size, size, 10, 10); + MeshView surface = new MeshView(plane); + surface.setMaterial(material); + surface.setMouseTransparent(true); + return surface; + } + + private void addSeaOverlay() { + SeaSurface seaSurface = new SeaSurface(size * 3, freq); + seaSurface.setX(x); + seaSurface.setY(y - size/4 + 1); + seaSurface.setZ(z); + skyBoxPlanes.add(seaSurface); + } + + public List getSkyBoxPlanes() { + return skyBoxPlanes; + } +} + diff --git a/racevisionGame/src/main/java/visualiser/layout/Subject3D.java b/racevisionGame/src/main/java/visualiser/layout/Subject3D.java index 6bcf60b2..4b18fec4 100644 --- a/racevisionGame/src/main/java/visualiser/layout/Subject3D.java +++ b/racevisionGame/src/main/java/visualiser/layout/Subject3D.java @@ -2,6 +2,7 @@ package visualiser.layout; import javafx.scene.shape.Shape3D; import javafx.scene.transform.Rotate; +import javafx.scene.transform.Scale; import javafx.scene.transform.Translate; /** @@ -27,6 +28,8 @@ public class Subject3D { */ private Rotate heading; + private Scale scale; + /** * Constructor for view subject wrapper * @param mesh to be rendered @@ -34,10 +37,11 @@ public class Subject3D { public Subject3D(Shape3D mesh, int sourceID) { this.mesh = mesh; this.sourceID = sourceID; + this.scale = new Scale(); this.position = new Translate(); this.heading = new Rotate(0, Rotate.Y_AXIS); - this.mesh.getTransforms().addAll(position, heading, new Rotate(90, Rotate.X_AXIS), new Rotate(180, Rotate.Y_AXIS)); + this.mesh.getTransforms().addAll(position, scale, heading, new Rotate(90, Rotate.X_AXIS), new Rotate(180, Rotate.Y_AXIS)); } public Shape3D getMesh() { @@ -56,6 +60,12 @@ public class Subject3D { return heading; } + public void setScale(double scale) { + this.scale.setX(scale); + this.scale.setY(scale); + this.scale.setZ(scale); + } + public void setX(double x) { position.setX(x); } diff --git a/racevisionGame/src/main/java/visualiser/layout/View3D.java b/racevisionGame/src/main/java/visualiser/layout/View3D.java index cc36f492..9adcaf32 100644 --- a/racevisionGame/src/main/java/visualiser/layout/View3D.java +++ b/racevisionGame/src/main/java/visualiser/layout/View3D.java @@ -3,9 +3,7 @@ package visualiser.layout; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; -import javafx.scene.Group; -import javafx.scene.PerspectiveCamera; -import javafx.scene.SubScene; +import javafx.scene.*; import javafx.scene.input.PickResult; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; @@ -89,12 +87,17 @@ public class View3D extends Pane { /** * Distance to switch from third person to bird's eye */ - private double THIRD_PERSON_LIMIT = 100; + private final double THIRD_PERSON_LIMIT = 100; + /** + * Distance to stop zoom + */ + private final double FIRST_PERSON_LIMIT = 2; /** * Default constructor for View3D. Sets up Scene and PerspectiveCamera. + * @param fill whether or not to fill the background of the view. */ - public View3D() { + public View3D(boolean fill) { this.world = new Group(); this.shapeMap = new HashMap<>(); this.sourceMap = new HashMap<>(); @@ -103,13 +106,18 @@ public class View3D extends Pane { scene.widthProperty().bind(this.widthProperty()); scene.heightProperty().bind(this.heightProperty()); - scene.setFill(new Color(0.2, 0.6, 1, 1)); - + if (fill) { + scene.setFill(new Color(0.2, 0.6, 1, 1)); + } scene.setCamera(buildCamera()); this.getChildren().add(scene); } + public View3D(){ + this(true); + } + /** * Sets up camera view frustum and binds transformations * @return perspective camera @@ -169,11 +177,37 @@ public class View3D extends Pane { scene.setOnMousePressed(e -> { PickResult result = e.getPickResult(); if(result != null && result.getIntersectedNode() != null && result.getIntersectedNode() instanceof Shape3D) { + untrackSubject(); trackSubject(shapeMap.get(result.getIntersectedNode())); + setThirdPerson(); } }); } + /** + * Configures camera to third person view + */ + public void setThirdPerson() { + this.setDistance(THIRD_PERSON_LIMIT / 2); + this.setPitch(10); + + for(Subject3D item: items) { + item.setScale(0.1); + } + } + + /** + * Configures camera to bird's eye view + */ + public void setBirdsEye() { + this.setYaw(0); + this.setPitch(60); + + for(Subject3D item: items) { + item.setScale(1); + } + } + /** * Stop camera from following the last selected subject */ @@ -191,7 +225,6 @@ public class View3D extends Pane { * @param subject to track */ private void trackSubject(Subject3D subject) { - untrackSubject(); target = subject; updatePivot(target.getPosition()); @@ -201,9 +234,6 @@ public class View3D extends Pane { target.getPosition().yProperty().addListener(pivotY); target.getPosition().zProperty().addListener(pivotZ); target.getHeading().angleProperty().addListener(pivotHeading); - - this.setDistance(THIRD_PERSON_LIMIT); - this.setPitch(20); } public void setNearClip(double nearClip) { @@ -241,13 +271,12 @@ public class View3D extends Pane { public void updateDistance(double delta) { double distance = -this.distance.getZ() + delta; - if(distance <= 0) { - this.setDistance(0); + if(distance <= FIRST_PERSON_LIMIT) { + this.setDistance(FIRST_PERSON_LIMIT); } else if(distance > THIRD_PERSON_LIMIT) { - untrackSubject(); - this.setYaw(0); - this.setPitch(60); this.setDistance(distance); + untrackSubject(); + setBirdsEye(); } else { this.setDistance(distance); } @@ -268,4 +297,12 @@ public class View3D extends Pane { public void setPitch(double pitch) { this.pitch.setAngle(-pitch); } + + public void addAmbientLight(AmbientLight ambientLight) { + this.world.getChildren().add(ambientLight); + } + + public void addPointLight(PointLight pointLight) { + this.world.getChildren().add(pointLight); + } } diff --git a/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java b/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java index 22dd937f..542b043f 100644 --- a/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java +++ b/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java @@ -101,4 +101,14 @@ public class GPSConverter { return convertGPS(coordinate.getLatitude(), coordinate.getLongitude()); } + /** + * Gets the bearing between two coordinates + * @param coord1 coordinate 1 + * @param coord2 coordinate 2 + * @return return the bearing between the two. + */ + public static double getAngle(GraphCoordinate coord1, GraphCoordinate coord2){ + return Math.atan2(coord2.getX() - coord1.getX(), coord2.getY() - coord1.getY()); + } + } diff --git a/racevisionGame/src/main/java/visualiser/utils/PerlinNoiseGenerator.java b/racevisionGame/src/main/java/visualiser/utils/PerlinNoiseGenerator.java new file mode 100644 index 00000000..00167656 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/utils/PerlinNoiseGenerator.java @@ -0,0 +1,88 @@ +package visualiser.utils; + +/** + * Perlin Noise Generator + */ +public class PerlinNoiseGenerator { + + + /** + * Create an array of the given size with values of perlin noise + * @param size size of array that you wish to create + * @param freq frequency that the noise is to be generated at. + * @return noise generated + */ + public static float[][] createNoise( int size, double freq) { + float[][] noiseArray = new float[(int) size][(int) size]; + + for (int x = 0; x < size; x++) { + for (int y = 0; y < size; y++) { + + double frequency = freq / (double) size; + + double noise = ImprovedNoise.noise(x * frequency, y * frequency, 0); + + noiseArray[x][y] = (float) noise; + } + } + + return noiseArray; + + } + + /** + * Perlin noise generator + * + * // JAVA REFERENCE IMPLEMENTATION OF IMPROVED NOISE - COPYRIGHT 2002 KEN PERLIN. + * // http://mrl.nyu.edu/~perlin/paper445.pdf + * // http://mrl.nyu.edu/~perlin/noise/ + */ + public final static class ImprovedNoise { + static public double noise(double x, double y, double z) { + int X = (int)Math.floor(x) & 255, // FIND UNIT CUBE THAT + Y = (int)Math.floor(y) & 255, // CONTAINS POINT. + Z = (int)Math.floor(z) & 255; + x -= Math.floor(x); // FIND RELATIVE X,Y,Z + y -= Math.floor(y); // OF POINT IN CUBE. + z -= Math.floor(z); + double u = fade(x), // COMPUTE FADE CURVES + v = fade(y), // FOR EACH OF X,Y,Z. + w = fade(z); + int A = p[X ]+Y, AA = p[A]+Z, AB = p[A+1]+Z, // HASH COORDINATES OF + B = p[X+1]+Y, BA = p[B]+Z, BB = p[B+1]+Z; // THE 8 CUBE CORNERS, + + return lerp(w, lerp(v, lerp(u, grad(p[AA ], x , y , z ), // AND ADD + grad(p[BA ], x-1, y , z )), // BLENDED + lerp(u, grad(p[AB ], x , y-1, z ), // RESULTS + grad(p[BB ], x-1, y-1, z ))),// FROM 8 + lerp(v, lerp(u, grad(p[AA+1], x , y , z-1 ), // CORNERS + grad(p[BA+1], x-1, y , z-1 )), // OF CUBE + lerp(u, grad(p[AB+1], x , y-1, z-1 ), + grad(p[BB+1], x-1, y-1, z-1 )))); + } + static double fade(double t) { return t * t * t * (t * (t * 6 - 15) + 10); } + static double lerp(double t, double a, double b) { return a + t * (b - a); } + static double grad(int hash, double x, double y, double z) { + int h = hash & 15; // CONVERT LO 4 BITS OF HASH CODE + double u = h<8 ? x : y, // INTO 12 GRADIENT DIRECTIONS. + v = h<4 ? y : h==12||h==14 ? x : z; + return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v); + } + static final int p[] = new int[512], permutation[] = { 151,160,137,91,90,15, + 131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23, + 190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33, + 88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166, + 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, + 102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196, + 135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123, + 5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42, + 223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9, + 129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228, + 251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107, + 49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254, + 138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180 + }; + static { for (int i=0; i < 256 ; i++) p[256+i] = p[i] = permutation[i]; } + } + +} diff --git a/racevisionGame/src/main/resources/assets/Bouy V1.1.stl b/racevisionGame/src/main/resources/assets/Bouy V1.1.stl new file mode 100644 index 00000000..3ba11615 Binary files /dev/null and b/racevisionGame/src/main/resources/assets/Bouy V1.1.stl differ diff --git a/racevisionGame/src/main/resources/assets/V1.3 BurgerBoat.stl b/racevisionGame/src/main/resources/assets/V1.3 BurgerBoat.stl new file mode 100644 index 00000000..b8f0f54a Binary files /dev/null and b/racevisionGame/src/main/resources/assets/V1.3 BurgerBoat.stl differ diff --git a/racevisionGame/src/main/resources/images/skybox/skyBack.png b/racevisionGame/src/main/resources/images/skybox/skyBack.png new file mode 100644 index 00000000..390ff4e8 Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox/skyBack.png differ diff --git a/racevisionGame/src/main/resources/images/skybox/skyFront.png b/racevisionGame/src/main/resources/images/skybox/skyFront.png new file mode 100644 index 00000000..50769c3d Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox/skyFront.png differ diff --git a/racevisionGame/src/main/resources/images/skybox/skyLeft.png b/racevisionGame/src/main/resources/images/skybox/skyLeft.png new file mode 100644 index 00000000..9ee2c8b7 Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox/skyLeft.png differ diff --git a/racevisionGame/src/main/resources/images/skybox/skyRight.png b/racevisionGame/src/main/resources/images/skybox/skyRight.png new file mode 100644 index 00000000..df386ef8 Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox/skyRight.png differ diff --git a/racevisionGame/src/main/resources/images/skybox/skyTop.png b/racevisionGame/src/main/resources/images/skybox/skyTop.png new file mode 100644 index 00000000..90e33729 Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox/skyTop.png differ diff --git a/racevisionGame/src/main/resources/mock/mockXML/raceThreePlayers.xml b/racevisionGame/src/main/resources/mock/mockXML/raceThreePlayers.xml new file mode 100644 index 00000000..c4efcd17 --- /dev/null +++ b/racevisionGame/src/main/resources/mock/mockXML/raceThreePlayers.xml @@ -0,0 +1,51 @@ + + + 5326 + FLEET + RACE_CREATION_TIME + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +