Merge branch 'storyD-3D' into 'master'
D and A D) As user I wish to play on a modern day 3D view Description: The current base of the game is run on a 2D dot and line canvas, this will be overhauled and replaced with a 3D view of the game. Acceptance Criteria: - Boats, and markers in the race are now 3D. - Sea has a surface. - Race runs and is reflected on the 3D view. - Skybox must be present. - Boundary must be shown (2D or 3D) - Annotations do not need to be shown. Tests: - When the FXML's are split make sure that the race still changes panes to reflect each stage of the race, prestart, racing, and finish. - The boats headings on the 3D models are the correct orientation (if they aren't facting forward on load please tell Fan-Wu as this should be an issue with the stl file). - The camera is "fixed" for now. - The base pane of the race is a stack pane so we can stack an annotations layer, or control layer, etc in the future. A) [V] As a player I would like for the style of the application to be informative Acceptance criteria: * The direction to the next mark is shown in 3rd person view * The next mark to be rounded is highlighted * Player is notified with a sound effect when the mark is rounded * Collisions are made clear and give visual and audio feedback * The score table overlays on the race and does not affect scaling See merge request !46
@@ -219,7 +219,6 @@ public class Event {
|
||||
|
||||
long millisecondsToAdd = racePreStartTime + racePreparatoryTime;
|
||||
|
||||
|
||||
long secondsToAdd = millisecondsToAdd / 1000;
|
||||
|
||||
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
|
||||
|
||||
@@ -12,6 +12,7 @@ import network.Messages.Enums.RaceStatusEnum;
|
||||
import network.Messages.LatestMessages;
|
||||
import shared.model.RunnableWithFramePeriod;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Observable;
|
||||
import java.util.Observer;
|
||||
|
||||
@@ -139,6 +140,8 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer {
|
||||
*/
|
||||
private void raceLoop() {
|
||||
|
||||
ArrayList<MockBoat> collisionBoats = new ArrayList<>();
|
||||
|
||||
long previousFrameTime = System.currentTimeMillis();
|
||||
|
||||
while (race.getRaceStatusEnum() != RaceStatusEnum.FINISHED && loopBool) {
|
||||
@@ -168,9 +171,17 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer {
|
||||
//If it is still racing, update its position.
|
||||
if (boat.getStatus() == BoatStatusEnum.RACING) {
|
||||
race.updatePosition(boat, framePeriod, race.getRaceClock().getDurationMilli());
|
||||
race.getColliderRegistry().rayCast(boat);
|
||||
|
||||
if(race.getColliderRegistry().rayCast(boat)){
|
||||
//System.out.println("Collision!");
|
||||
//Add boat to list
|
||||
collisionBoats.add(boat);
|
||||
}
|
||||
|
||||
//System.out.println(race.getColliderRegistry().rayCast(boat));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -183,9 +194,14 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer {
|
||||
// Change wind direction
|
||||
race.changeWindDirection();
|
||||
|
||||
//Pass collision boats in
|
||||
server.parseBoatCollisions(collisionBoats);
|
||||
|
||||
//Parse the race snapshot.
|
||||
server.parseSnapshot();
|
||||
|
||||
collisionBoats.clear();
|
||||
|
||||
//Update the last frame time.
|
||||
previousFrameTime = currentTime;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package mock.model;
|
||||
import network.AckSequencer;
|
||||
import network.Messages.*;
|
||||
import network.Messages.Enums.BoatLocationDeviceEnum;
|
||||
import network.Messages.Enums.YachtEventEnum;
|
||||
import network.Messages.Enums.XMLMessageType;
|
||||
import shared.model.Bearing;
|
||||
import shared.model.CompoundMark;
|
||||
@@ -24,6 +25,7 @@ public class RaceServer {
|
||||
private MockRace race;
|
||||
private LatestMessages latestMessages;
|
||||
private static RaceServer server;
|
||||
private List<YachtEvent> collisionEvents = new ArrayList<>();
|
||||
|
||||
|
||||
/**
|
||||
@@ -75,9 +77,19 @@ public class RaceServer {
|
||||
//Parse the race status.
|
||||
snapshotMessages.add(parseRaceStatus());
|
||||
|
||||
//Parse collisions
|
||||
if(collisionEvents.size()>0){
|
||||
snapshotMessages.addAll(collisionEvents);
|
||||
}
|
||||
|
||||
latestMessages.setSnapshot(snapshotMessages);
|
||||
|
||||
updateXMLFiles();
|
||||
|
||||
//Reset collision list
|
||||
collisionEvents.clear();
|
||||
//System.out.println(collisionEvents.size());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -314,4 +326,28 @@ public class RaceServer {
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the yacht event and return it
|
||||
* @param boat yacht with event
|
||||
* @param event event that happened
|
||||
* @return yacht event
|
||||
*/
|
||||
private YachtEvent parseYachtEvent(MockBoat boat, YachtEventEnum event){
|
||||
YachtEvent yachtEvent = new YachtEvent(
|
||||
System.currentTimeMillis(),
|
||||
this.boatLocationSequenceNumber,
|
||||
race.getRaceId(),
|
||||
boat.getSourceID(),
|
||||
1337,
|
||||
event);
|
||||
return yachtEvent;
|
||||
}
|
||||
|
||||
public void parseBoatCollisions(ArrayList<MockBoat> boats){
|
||||
for (MockBoat boat : boats){
|
||||
collisionEvents.add(parseYachtEvent(boat, YachtEventEnum.COLLISION));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package mock.model.commandFactory;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
|
||||
/**
|
||||
* Wraps multiple commands into a composite to execute queued commands during a frame.
|
||||
@@ -10,15 +10,15 @@ public class CompositeCommand implements Command {
|
||||
private Queue<Command> commands;
|
||||
|
||||
public CompositeCommand() {
|
||||
this.commands = new ArrayDeque<>();
|
||||
this.commands = new ConcurrentLinkedDeque<>();
|
||||
}
|
||||
|
||||
public void addCommand(Command command) {
|
||||
commands.add(command);
|
||||
commands.offer(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
while(!commands.isEmpty()) commands.remove().execute();
|
||||
while(commands.peek() != null) commands.poll().execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,6 @@ public class RaceXMLCreator {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Rotate the features in a race such as the boundary, and the marks.
|
||||
* @param race the race to alter
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package visualiser.Commands.VisualiserRaceCommands;
|
||||
|
||||
import javafx.scene.media.AudioClip;
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.YachtEvent;
|
||||
import shared.exceptions.BoatNotFoundException;
|
||||
import visualiser.model.VisualiserBoat;
|
||||
import visualiser.model.VisualiserRaceState;
|
||||
|
||||
/**
|
||||
* Created by zwu18 on 4/09/17.
|
||||
*/
|
||||
public class BoatCollisionCommand implements Command {
|
||||
|
||||
YachtEvent yachtEvent;
|
||||
|
||||
VisualiserRaceState visualiserRace;
|
||||
|
||||
public BoatCollisionCommand(YachtEvent yachtEvent, VisualiserRaceState visualiserRace){
|
||||
this.yachtEvent = yachtEvent;
|
||||
this.visualiserRace = visualiserRace;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
|
||||
if(visualiserRace.getPlayerBoatID()==yachtEvent.getSourceID()){
|
||||
//System.out.println("I crashed!");
|
||||
AudioClip sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/collision.wav").toExternalForm());
|
||||
sound.play();
|
||||
|
||||
} else {
|
||||
//System.out.println("Someone else crashed!");
|
||||
AudioClip sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/quietcollision.wav").toExternalForm());
|
||||
sound.play();
|
||||
}
|
||||
|
||||
try {
|
||||
VisualiserBoat boat = visualiserRace.getBoat(yachtEvent.getSourceID());
|
||||
boat.setHasCollided(true);
|
||||
} catch (BoatNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package visualiser.Commands.VisualiserRaceCommands;
|
||||
|
||||
import javafx.scene.media.AudioClip;
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.BoatStatus;
|
||||
import network.Messages.Enums.BoatStatusEnum;
|
||||
@@ -176,6 +177,10 @@ public class RaceStatusCommand implements Command {
|
||||
//Record order in which boat finished leg.
|
||||
visualiserRace.getLegCompletionOrder().get(boat.getCurrentLeg()).add(boat);
|
||||
|
||||
//play sound
|
||||
AudioClip sound = new AudioClip(getClass().getResource("/visualiser/sounds/passmark.wav").toExternalForm());
|
||||
sound.play();
|
||||
|
||||
//Update boat.
|
||||
boat.setCurrentLeg(leg);
|
||||
boat.setTimeAtLastMark(visualiserRace.getRaceClock().getCurrentTime());
|
||||
|
||||
@@ -23,7 +23,9 @@ public class VisualiserRaceCommandFactory {
|
||||
|
||||
switch (message.getType()) {
|
||||
|
||||
case BOATLOCATION: return new BoatLocationCommand((BoatLocation) message, visualiserRace);
|
||||
case BOATLOCATION:
|
||||
//System.out.println("Boat location received");
|
||||
return new BoatLocationCommand((BoatLocation) message, visualiserRace);
|
||||
|
||||
case RACESTATUS: return new RaceStatusCommand((RaceStatus) message, visualiserRace);
|
||||
|
||||
@@ -31,6 +33,11 @@ public class VisualiserRaceCommandFactory {
|
||||
|
||||
case ASSIGN_PLAYER_BOAT: return new AssignPlayerBoatCommand((AssignPlayerBoat) message, visualiserRace);
|
||||
|
||||
case YACHTEVENTCODE:
|
||||
return new BoatCollisionCommand((YachtEvent) message, visualiserRace);
|
||||
|
||||
|
||||
|
||||
default: throw new CommandConstructionException("Could not create VisualiserRaceCommand. Unrecognised or unsupported MessageType: " + message.getType());
|
||||
|
||||
}
|
||||
|
||||
@@ -138,11 +138,13 @@ public class InGameLobbyController extends Controller {
|
||||
playerContainer.add(playerBoatToSet, (count % 3) , row);
|
||||
playerContainer.setMargin(playerBoatToSet, new Insets(10, 10, 10, 10));
|
||||
|
||||
SeaSurface sea = new SeaSurface(750, 200, 250, 0, 210);
|
||||
subjects.add(sea.getSurface());
|
||||
SeaSurface sea = new SeaSurface(750, 200);
|
||||
sea.setX(250);
|
||||
sea.setZ(210);
|
||||
subjects.add(sea);
|
||||
|
||||
MeshView mesh = new MeshView(importer.getImport());
|
||||
Subject3D subject = new Subject3D(mesh);
|
||||
Subject3D subject = new Subject3D(mesh,0);
|
||||
subjects.add(subject);
|
||||
|
||||
playerBoatToSet.setDistance(50);
|
||||
|
||||
@@ -7,6 +7,8 @@ import javafx.scene.control.Button;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.media.AudioClip;
|
||||
import network.Messages.HostGame;
|
||||
import visualiser.app.MatchBrowserSingleton;
|
||||
import visualiser.model.RaceConnection;
|
||||
@@ -34,6 +36,8 @@ public class LobbyController extends Controller {
|
||||
private ObservableList<RaceConnection> connections;
|
||||
private ObservableList<RaceConnection> customConnections;
|
||||
|
||||
private AudioClip sound;
|
||||
|
||||
//the socket for match browser
|
||||
private DatagramSocket udpSocket;
|
||||
private MatchBrowserLobbyInterface matchBrowserLobbyInterface;
|
||||
@@ -67,6 +71,8 @@ public class LobbyController extends Controller {
|
||||
* Refreshes the connections in the lobby
|
||||
*/
|
||||
public void refreshBtnPressed(){
|
||||
sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/buttonpress.wav").toExternalForm());
|
||||
sound.play();
|
||||
addServerGames();
|
||||
for(RaceConnection connection: connections) {
|
||||
connection.check();
|
||||
@@ -92,6 +98,8 @@ public class LobbyController extends Controller {
|
||||
}
|
||||
|
||||
public void menuBtnPressed() throws IOException {
|
||||
sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/buttonpress.wav").toExternalForm());
|
||||
sound.play();
|
||||
matchBrowserLobbyInterface.closeSocket();
|
||||
loadScene("title.fxml");
|
||||
}
|
||||
@@ -100,6 +108,8 @@ public class LobbyController extends Controller {
|
||||
* adds a new connection
|
||||
*/
|
||||
public void addConnectionPressed(){
|
||||
sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/buttonpress.wav").toExternalForm());
|
||||
sound.play();
|
||||
String hostName = addressFld.getText();
|
||||
String portString = portFld.getText();
|
||||
try {
|
||||
|
||||
@@ -3,29 +3,37 @@ package visualiser.Controllers;
|
||||
import com.interactivemesh.jfx.importer.stl.StlMeshImporter;
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
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.GridPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.MeshView;
|
||||
import javafx.scene.shape.Sphere;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Material;
|
||||
import javafx.scene.paint.PhongMaterial;
|
||||
import javafx.scene.shape.*;
|
||||
import javafx.scene.transform.Translate;
|
||||
import javafx.util.Callback;
|
||||
import network.Messages.Enums.RaceStatusEnum;
|
||||
import shared.dataInput.RaceDataSource;
|
||||
import shared.model.Leg;
|
||||
import shared.model.Mark;
|
||||
import shared.exceptions.BoatNotFoundException;
|
||||
import shared.model.*;
|
||||
import visualiser.app.App;
|
||||
import visualiser.enums.TutorialState;
|
||||
import visualiser.gameController.ControllerClient;
|
||||
import visualiser.gameController.Keys.ControlKey;
|
||||
import visualiser.layout.*;
|
||||
import visualiser.model.*;
|
||||
import visualiser.gameController.Keys.KeyFactory;
|
||||
import visualiser.layout.Subject3D;
|
||||
import visualiser.layout.View3D;
|
||||
@@ -61,6 +69,15 @@ public class RaceViewController extends Controller {
|
||||
private View3D view3D;
|
||||
private ObservableList<Subject3D> viewSubjects;
|
||||
|
||||
/**
|
||||
* Arrow pointing to next mark in third person
|
||||
*/
|
||||
private Subject3D nextMarkArrow;
|
||||
/**
|
||||
* Animation loop for rotating mark arrow
|
||||
*/
|
||||
private AnimationTimer pointToMark;
|
||||
|
||||
// note: it says it's not used but it is! do not remove :)
|
||||
private @FXML ArrowController arrowController;
|
||||
private @FXML GridPane canvasBase;
|
||||
@@ -121,9 +138,8 @@ public class RaceViewController extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the listener and actions that occur when a key is pressed.
|
||||
*/
|
||||
private AnimationTimer arrowToNextMark;
|
||||
|
||||
private void initKeypressHandler() {
|
||||
racePane.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
String codeString = event.getCode().toString();
|
||||
@@ -189,10 +205,23 @@ public class RaceViewController extends Controller {
|
||||
* Initialises the various UI components to listen to the {@link #visualiserRace}.
|
||||
*/
|
||||
private void initialiseRaceVisuals() {
|
||||
|
||||
// Import arrow mesh
|
||||
URL asset = this.getClass().getClassLoader().getResource("assets/arrow V1.0.4.stl");
|
||||
StlMeshImporter importer = new StlMeshImporter();
|
||||
importer.read(asset);
|
||||
|
||||
MeshView arrow = new MeshView(importer.getImport());
|
||||
PhongMaterial arrowMat = new PhongMaterial(Color.RED);
|
||||
arrow.setMaterial(arrowMat);
|
||||
|
||||
this.nextMarkArrow = new Annotation3D(arrow);
|
||||
this.nextMarkArrow.setScale(0.1);
|
||||
|
||||
// initialise displays
|
||||
initialiseFps();
|
||||
initialiseInfoTable();
|
||||
initialiseView3D();
|
||||
initialiseView3D(this.visualiserRace);
|
||||
initialiseRaceClock();
|
||||
raceTimer(); // start the timer
|
||||
new Sparkline(this.raceState, this.sparklineChart);
|
||||
@@ -200,55 +229,122 @@ public class RaceViewController extends Controller {
|
||||
arrowController.setWindProperty(this.raceState.windProperty());
|
||||
}
|
||||
|
||||
private void initialiseView3D() {
|
||||
private void initialiseView3D(VisualiserRaceEvent race) {
|
||||
viewSubjects = FXCollections.observableArrayList();
|
||||
|
||||
AmbientLight ambientLight = new AmbientLight(Color.web("#CCCCFF"));
|
||||
ambientLight.setTranslateX(250);
|
||||
ambientLight.setTranslateZ(210);
|
||||
ambientLight.setLightOn(true);
|
||||
|
||||
PointLight pointLight = new PointLight(Color.web("#AAAAFF"));
|
||||
pointLight.setTranslateX(250);
|
||||
pointLight.setTranslateZ(210);
|
||||
pointLight.setLightOn(true);
|
||||
|
||||
// Import boat mesh
|
||||
URL asset = RaceViewController.class.getClassLoader().getResource("assets/V1.2 " +
|
||||
"Complete Boat.stl");
|
||||
URL asset = RaceViewController.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 = RaceViewController.class.getClassLoader().getResource("assets/Bouy V1.1.stl");
|
||||
StlMeshImporter importerMark = new StlMeshImporter();
|
||||
importerMark.read(markerAsset);
|
||||
|
||||
URL alternateBoatAsset = RaceViewController.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();
|
||||
view3D.addAmbientLight(ambientLight);
|
||||
view3D.addPointLight(pointLight);
|
||||
canvasBase.add(view3D, 0, 0);
|
||||
|
||||
// Set up projection from GPS to view
|
||||
RaceDataSource raceData = raceState.getRaceDataSource();
|
||||
RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource();
|
||||
final GPSConverter gpsConverter = new GPSConverter(raceData, 450, 450);
|
||||
|
||||
view3D.setItems(viewSubjects);
|
||||
// Position and add each mark to view
|
||||
for (Mark mark : raceState.getMarks()) {
|
||||
Subject3D subject = new Subject3D(new Sphere(2));
|
||||
subject.setX(gpsConverter.convertGPS(mark.getPosition()).getX());
|
||||
subject.setZ(gpsConverter.convertGPS(mark.getPosition()).getY());
|
||||
SkyBox skyBox = new SkyBox(750, 200, 250, 0, 210);
|
||||
viewSubjects.addAll(skyBox.getSkyBoxPlanes());
|
||||
|
||||
viewSubjects.add(subject);
|
||||
// Set up sea surface
|
||||
SeaSurface sea = new SeaSurface(750, 200);
|
||||
sea.setX(250);
|
||||
sea.setZ(210);
|
||||
viewSubjects.add(sea);
|
||||
|
||||
// Set up sea surface overlay
|
||||
SeaSurface seaOverlay = new SeaSurface(4000, 200);
|
||||
seaOverlay.setX(250);
|
||||
seaOverlay.setZ(210);
|
||||
viewSubjects.add(seaOverlay);
|
||||
|
||||
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()) {
|
||||
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(markModel);
|
||||
}
|
||||
// Position and add each boat to view
|
||||
for (VisualiserBoat boat : raceState.getBoats()) {
|
||||
MeshView mesh = new MeshView(importer.getImport());
|
||||
Subject3D subject = new Subject3D(mesh);
|
||||
viewSubjects.add(subject);
|
||||
for(VisualiserBoat boat: race.getVisualiserRaceState().getBoats()) {
|
||||
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());
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
boatModel.setHeading(boat.getBearing().degrees());
|
||||
boatModel.setX(gpsConverter.convertGPS(boat.getPosition()).getX());
|
||||
boatModel.setZ(gpsConverter.convertGPS(boat.getPosition()).getY());
|
||||
}
|
||||
};
|
||||
trackBoat.start();
|
||||
|
||||
Material markColor = new PhongMaterial(new Color(0.15,0.9,0.2,1));
|
||||
CompoundMark nextMark = boat.getCurrentLeg().getEndCompoundMark();
|
||||
view3D.getShape(nextMark.getMark1().getSourceID()).setMaterial(markColor);
|
||||
if(nextMark.getMark2() != null) {
|
||||
view3D.getShape(nextMark.getMark2().getSourceID()).setMaterial(markColor);
|
||||
}
|
||||
Subject3D shockwave = new Shockwave(10);
|
||||
|
||||
viewSubjects.add(shockwave);
|
||||
|
||||
boat.legProperty().addListener((o, prev, curr) -> swapColours(curr));
|
||||
boat.hasCollidedProperty().addListener((o, prev, curr) -> showCollision(boatModel, shockwave));
|
||||
}
|
||||
// Fix initial bird's-eye position
|
||||
view3D.updatePivot(new Translate(250, 0, 210));
|
||||
|
||||
view3D.targetProperty().addListener((o, prev, curr)-> {
|
||||
if(curr != null && visualiserRace.getVisualiserRaceState().isVisualiserBoat(curr.getSourceID())) {
|
||||
addThirdPersonAnnotations(curr);
|
||||
} else {
|
||||
removeThirdPersonAnnotations(prev);
|
||||
}
|
||||
});
|
||||
|
||||
// Bind zooming to scrolling
|
||||
view3D.setOnScroll(e -> {
|
||||
view3D.updateDistance(e.getDeltaY());
|
||||
@@ -294,6 +390,112 @@ public class RaceViewController extends Controller {
|
||||
});
|
||||
}
|
||||
|
||||
private void showCollision(Subject3D boat, Subject3D shockwave) {
|
||||
AnimationTimer shockFront = new AnimationTimer() {
|
||||
double opacity = 1;
|
||||
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
shockwave.setX(boat.getPosition().getX());
|
||||
shockwave.setY(boat.getPosition().getY());
|
||||
shockwave.setZ(boat.getPosition().getZ());
|
||||
|
||||
if(opacity <= 0) {
|
||||
shockwave.getMesh().setMaterial(new PhongMaterial(new Color(1,0,0,0)));
|
||||
this.stop();
|
||||
}
|
||||
else {
|
||||
shockwave.getMesh().setMaterial(new PhongMaterial(new Color(1,0,0,opacity)));
|
||||
opacity -= 0.1;
|
||||
}
|
||||
}
|
||||
};
|
||||
shockFront.start();
|
||||
}
|
||||
|
||||
private void addThirdPersonAnnotations(Subject3D subject3D) {
|
||||
viewSubjects.add(nextMarkArrow);
|
||||
final VisualiserBoat boat;
|
||||
try {
|
||||
boat = visualiserRace.getVisualiserRaceState().getBoat(subject3D.getSourceID());
|
||||
} catch (BoatNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
arrowToNextMark = new AnimationTimer() {
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
CompoundMark target = boat.getCurrentLeg().getEndCompoundMark();
|
||||
Bearing headingToMark = GPSCoordinate.calculateBearing(boat.getPosition(), target.getAverageGPSCoordinate());
|
||||
|
||||
nextMarkArrow.setX(view3D.getPivot().getX());
|
||||
nextMarkArrow.setY(view3D.getPivot().getY());
|
||||
nextMarkArrow.setZ(view3D.getPivot().getZ() + 15);
|
||||
nextMarkArrow.setHeading(headingToMark.degrees());
|
||||
}
|
||||
};
|
||||
arrowToNextMark.start();
|
||||
/*
|
||||
try {
|
||||
VisualiserBoat boat = visualiserRace.getVisualiserRaceState().getBoat(subject3D.getSourceID());
|
||||
this.pointToMark = new AnimationTimer() {
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
CompoundMark target = boat.getCurrentLeg().getEndCompoundMark();
|
||||
Bearing headingToMark = GPSCoordinate.calculateBearing(boat.getPosition(), target.getAverageGPSCoordinate());
|
||||
|
||||
nextMarkArrow.setX(view3D.getPivot().getX());
|
||||
nextMarkArrow.setY(view3D.getPivot().getY());
|
||||
nextMarkArrow.setZ(view3D.getPivot().getZ() + 10);
|
||||
nextMarkArrow.setHeading(headingToMark.degrees());
|
||||
}
|
||||
};
|
||||
pointToMark.start();
|
||||
} catch (BoatNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}*/
|
||||
}
|
||||
|
||||
private void removeThirdPersonAnnotations(Subject3D subject3D) {
|
||||
viewSubjects.remove(nextMarkArrow);
|
||||
arrowToNextMark.stop();
|
||||
/*
|
||||
try {
|
||||
VisualiserBoat boat = visualiserRace.getVisualiserRaceState().getBoat(subject3D.getSourceID());
|
||||
boat.positionProperty().removeListener(pointToMark);
|
||||
} catch (BoatNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}*/
|
||||
//pointToMark.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap the colour of the next mark to pass with the last mark passed
|
||||
* @param leg boat has started on
|
||||
*/
|
||||
private void swapColours(Leg leg) {
|
||||
CompoundMark start = leg.getStartCompoundMark();
|
||||
CompoundMark end = leg.getEndCompoundMark();
|
||||
|
||||
Shape3D start1 = view3D.getShape(start.getMark1().getSourceID());
|
||||
Shape3D end1 = view3D.getShape(end.getMark1().getSourceID());
|
||||
|
||||
Material nextMark = start1.getMaterial();
|
||||
Material lastMark = end1.getMaterial();
|
||||
|
||||
start1.setMaterial(lastMark);
|
||||
if(start.getMark2() != null) {
|
||||
Shape3D start2 = view3D.getShape(start.getMark2().getSourceID());
|
||||
start2.setMaterial(lastMark);
|
||||
}
|
||||
|
||||
end1.setMaterial(nextMark);
|
||||
if(end.getMark2() != null) {
|
||||
Shape3D end2 = view3D.getShape(end.getMark2().getSourceID());
|
||||
end2.setMaterial(nextMark);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises the frame rate functionality. This allows for toggling the
|
||||
* frame rate, and connect the fps label to the race's fps property.
|
||||
|
||||
@@ -6,11 +6,20 @@ import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.media.AudioClip;
|
||||
import javafx.scene.media.Media;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javafx.stage.Modality;
|
||||
import mock.exceptions.EventConstructionException;
|
||||
import visualiser.app.App;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Controller for the opening title window.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package visualiser.layout;
|
||||
|
||||
import javafx.scene.shape.Shape3D;
|
||||
|
||||
/**
|
||||
* Created by connortaylorbrown on 13/09/17.
|
||||
*/
|
||||
public class Annotation3D extends Subject3D {
|
||||
/**
|
||||
* Constructor for view subject wrapper
|
||||
*
|
||||
* @param mesh to be rendered
|
||||
*/
|
||||
public Annotation3D(Shape3D mesh) {
|
||||
super(mesh, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent rescaling of this subject
|
||||
* @param scale ignored
|
||||
*/
|
||||
@Override
|
||||
public void setScale(double scale) {}
|
||||
}
|
||||
@@ -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<Subject3D> boundaryNodes = new ArrayList<>();
|
||||
private List<Subject3D> boundaryConnectors = new ArrayList<>();
|
||||
private GPSConverter gpsConverter;
|
||||
|
||||
public Boundary3D(List<GPSCoordinate> 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<GPSCoordinate> 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 Annotation3D(new Sphere(thickness * 2));
|
||||
bound1.setX(graphCoord1.getX());
|
||||
bound1.setZ(graphCoord1.getY());
|
||||
boundaryNodes.add(bound1);
|
||||
|
||||
Subject3D connector = new Annotation3D(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<Subject3D> getBoundaryNodes(){
|
||||
//these two must be concatenated with nodes after connectors else the spheres will not overlap the lines
|
||||
ArrayList<Subject3D> result = new ArrayList<>(boundaryConnectors);
|
||||
result.addAll(boundaryNodes);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
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 javafx.scene.shape.TriangleMesh;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -92,6 +100,7 @@ public class Plane3D extends TriangleMesh{
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,65 +1,64 @@
|
||||
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 {
|
||||
private float[][] noiseArray;
|
||||
private Subject3D surface;
|
||||
public class SeaSurface extends Subject3D {
|
||||
private static Image image;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param x offset that the sea should be set at position-wise
|
||||
* @param y offset that the sea should be set at position-wise
|
||||
* @param z offset that the sea should be set at position-wise
|
||||
*/
|
||||
public SeaSurface(int size, double freq, double x, double y, double z){
|
||||
noiseArray = PerlinNoiseGenerator.createNoise(size, freq);
|
||||
createSurface();
|
||||
surface.setZ(z);
|
||||
surface.setY(y);
|
||||
surface.setX(x);
|
||||
public SeaSurface(int size, double freq) {
|
||||
super(createSurface(size, freq), 0);
|
||||
image = new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/ThickCloudsWaterDown2048.png"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the sea surface
|
||||
*/
|
||||
private void createSurface(){
|
||||
Image diffuseMap = createImage(noiseArray.length, noiseArray);
|
||||
private static Shape3D createSurface(int size, double freq) {
|
||||
float[][] noiseArray = PerlinNoiseGenerator.createNoise(size, freq);
|
||||
Image diffuseMap = createImage(noiseArray.length, noiseArray); //image
|
||||
|
||||
PhongMaterial material = new PhongMaterial();
|
||||
material.setDiffuseColor(Color.web("#FFFFFF"));
|
||||
material.setSpecularColor(Color.web("#000000"));
|
||||
material.setDiffuseMap(diffuseMap);
|
||||
//material.setSpecularColor(Color.WHITE);
|
||||
|
||||
Plane3D seaPlane = new Plane3D(noiseArray.length, noiseArray.length, 10, 10);
|
||||
MeshView seaSurface = new MeshView(seaPlane);
|
||||
// Box seaSurface = new Box(noiseArray.length, 0.1, noiseArray.length);
|
||||
seaSurface.setMaterial(material);
|
||||
seaSurface.setMouseTransparent(true);
|
||||
seaSurface.toFront();
|
||||
//seaSurface.setRotationAxis(new Point3D(1, 0, 0));
|
||||
//seaSurface.setRotate(90);
|
||||
|
||||
surface = new Subject3D(seaSurface);
|
||||
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 Image createImage(double size, float[][] noise) {
|
||||
private static Image createImage(double size, float[][] noise) {
|
||||
|
||||
int width = (int) size;
|
||||
int height = (int) size;
|
||||
@@ -94,6 +93,7 @@ public class SeaSurface {
|
||||
|
||||
/**
|
||||
* Nomalises the values so that the colours are correct
|
||||
*
|
||||
* @param value value to normalise
|
||||
* @param min current min
|
||||
* @param max current max
|
||||
@@ -109,6 +109,7 @@ public class SeaSurface {
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -126,11 +127,11 @@ public class SeaSurface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get surface
|
||||
* @return the surface so it can be drawn
|
||||
* Prevent rescaling of sea surface
|
||||
*
|
||||
* @param scale ignored
|
||||
*/
|
||||
public Subject3D getSurface(){
|
||||
return surface;
|
||||
@Override
|
||||
public void setScale(double scale) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package visualiser.layout;
|
||||
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.PhongMaterial;
|
||||
import javafx.scene.shape.Cylinder;
|
||||
import javafx.scene.transform.Rotate;
|
||||
|
||||
/**
|
||||
* Created by cbt24 on 14/09/17.
|
||||
*/
|
||||
public class Shockwave extends Subject3D {
|
||||
public Shockwave(double radius) {
|
||||
super(new Cylinder(radius,0),0);
|
||||
getMesh().getTransforms().add(new Rotate(-90, Rotate.X_AXIS));
|
||||
getMesh().setMaterial(new PhongMaterial(new Color(0,0,0,0)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package visualiser.layout;
|
||||
|
||||
import javafx.geometry.Point3D;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.PhongMaterial;
|
||||
import javafx.scene.shape.MeshView;
|
||||
import javafx.scene.transform.Rotate;
|
||||
|
||||
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 double yshift;
|
||||
private double clipOverlap;
|
||||
private List<Subject3D> 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;
|
||||
this.yshift = -size/64;
|
||||
clipOverlap = 0;
|
||||
makeSkyBox();
|
||||
}
|
||||
|
||||
private void makeSkyBox() {
|
||||
addTop();
|
||||
addFront();
|
||||
addBack();
|
||||
addLeft();
|
||||
addRight();
|
||||
//addSeaOverlay();
|
||||
}
|
||||
|
||||
private void addTop() {
|
||||
MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/ThickCloudsWaterUp2048.png")), size);
|
||||
|
||||
surface.setRotationAxis(new Point3D(0, 0, 1));
|
||||
surface.setRotate(180);
|
||||
|
||||
surface.setTranslateX(x);
|
||||
surface.setTranslateY(y - size + 1);
|
||||
surface.setTranslateZ(z);
|
||||
|
||||
Subject3D top = new SkyBoxPlane(surface,0);
|
||||
skyBoxPlanes.add(top);
|
||||
}
|
||||
|
||||
private void addRight() {
|
||||
MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/ThickCloudsWaterRight2048.png")), size + 1);
|
||||
|
||||
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 + yshift);
|
||||
surface.setTranslateZ(z + size/2 - clipOverlap);
|
||||
|
||||
|
||||
Subject3D right = new SkyBoxPlane(surface,0);
|
||||
skyBoxPlanes.add(right);
|
||||
}
|
||||
|
||||
private void addLeft() {
|
||||
MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/ThickCloudsWaterLeft2048.png")), size + 1);
|
||||
|
||||
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 + yshift);
|
||||
surface.setTranslateZ(z - size/2 + clipOverlap);
|
||||
|
||||
|
||||
Subject3D left = new SkyBoxPlane(surface,0);
|
||||
skyBoxPlanes.add(left);
|
||||
}
|
||||
|
||||
private void addBack() {
|
||||
MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/ThickCloudsWaterBack2048.png")), size);
|
||||
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 + clipOverlap);
|
||||
surface.setTranslateY(y + yshift);
|
||||
surface.setTranslateZ(z);
|
||||
|
||||
Subject3D back = new SkyBoxPlane(surface,0);
|
||||
skyBoxPlanes.add(back);
|
||||
}
|
||||
|
||||
private void addFront() {
|
||||
MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/ThickCloudsWaterFront2048.png")), size);
|
||||
|
||||
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 - clipOverlap);
|
||||
surface.setTranslateY(y + yshift);
|
||||
surface.setTranslateZ(z);
|
||||
|
||||
Subject3D front = new SkyBoxPlane(surface,0);
|
||||
skyBoxPlanes.add(front);
|
||||
}
|
||||
|
||||
private MeshView makeSurface(Image diffuseMap, int size) {
|
||||
|
||||
PhongMaterial material = new PhongMaterial();
|
||||
material.setDiffuseColor(Color.web("#FFFFFF"));
|
||||
material.setSpecularColor(Color.web("#000000"));
|
||||
material.setDiffuseMap(diffuseMap);
|
||||
|
||||
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<Subject3D> getSkyBoxPlanes() {
|
||||
return skyBoxPlanes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package visualiser.layout;
|
||||
|
||||
import javafx.scene.shape.Shape3D;
|
||||
|
||||
public class SkyBoxPlane extends Subject3D {
|
||||
|
||||
public SkyBoxPlane(Shape3D mesh, int sourceID) {
|
||||
super(mesh,sourceID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScale(double scale) {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -12,6 +13,10 @@ public class Subject3D {
|
||||
* Rendered mesh
|
||||
*/
|
||||
private Shape3D mesh;
|
||||
/**
|
||||
* Source ID of subject in game model
|
||||
*/
|
||||
private int sourceID;
|
||||
|
||||
/**
|
||||
* Position translation updated by state listeners
|
||||
@@ -23,22 +28,29 @@ public class Subject3D {
|
||||
*/
|
||||
private Rotate heading;
|
||||
|
||||
private Scale scale;
|
||||
|
||||
/**
|
||||
* Constructor for view subject wrapper
|
||||
* @param mesh to be rendered
|
||||
*/
|
||||
public Subject3D(Shape3D mesh) {
|
||||
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() {
|
||||
return mesh;
|
||||
}
|
||||
|
||||
public int getSourceID() {
|
||||
return sourceID;
|
||||
}
|
||||
|
||||
public Translate getPosition() {
|
||||
return this.position;
|
||||
}
|
||||
@@ -47,6 +59,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);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package visualiser.layout;
|
||||
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
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;
|
||||
@@ -33,11 +34,15 @@ public class View3D extends Pane {
|
||||
/**
|
||||
* Map for selecting Subject3D from Shape3D
|
||||
*/
|
||||
private Map<Shape3D, Subject3D> selectionMap;
|
||||
private Map<Shape3D, Subject3D> shapeMap;
|
||||
/**
|
||||
* Map for selecting Shape3D from source ID
|
||||
*/
|
||||
private Map<Integer, Shape3D> sourceMap;
|
||||
/**
|
||||
* Subject tracked by camera
|
||||
*/
|
||||
private Subject3D target;
|
||||
private ObjectProperty<Subject3D> target;
|
||||
/**
|
||||
* Rendering container for shapes
|
||||
*/
|
||||
@@ -67,44 +72,43 @@ public class View3D extends Pane {
|
||||
*/
|
||||
private Rotate pitch;
|
||||
/**
|
||||
* Single listener for subject heading changes
|
||||
* Animation loop for camera tracking
|
||||
*/
|
||||
private ChangeListener<? super Number> pivotHeading = (o, prev, curr) -> yaw.setAngle((double)curr);
|
||||
/**
|
||||
* Single listener for subject position (x) changes
|
||||
*/
|
||||
private ChangeListener<? super Number> pivotX = (o, prev, curr) -> pivot.setX((double)curr);
|
||||
/**
|
||||
* Single listener for subject position (y) changes
|
||||
*/
|
||||
private ChangeListener<? super Number> pivotY = (o, prev, curr) -> pivot.setY((double)curr);
|
||||
/**
|
||||
* Single listener for subject position (z) changes
|
||||
*/
|
||||
private ChangeListener<? super Number> pivotZ = (o, prev, curr) -> pivot.setZ((double)curr);
|
||||
private AnimationTimer trackBoat;
|
||||
/**
|
||||
* 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.selectionMap = new HashMap<>();
|
||||
this.target = null;
|
||||
this.shapeMap = new HashMap<>();
|
||||
this.sourceMap = new HashMap<>();
|
||||
this.target = new SimpleObjectProperty<>(null);
|
||||
this.scene = new SubScene(world, 300, 300);
|
||||
|
||||
scene.widthProperty().bind(this.widthProperty());
|
||||
scene.heightProperty().bind(this.heightProperty());
|
||||
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
|
||||
@@ -140,17 +144,23 @@ public class View3D extends Pane {
|
||||
if (c.wasRemoved() || c.wasAdded()) {
|
||||
for (Subject3D shape : c.getRemoved()) {
|
||||
world.getChildren().remove(shape.getMesh());
|
||||
selectionMap.remove(shape.getMesh());
|
||||
shapeMap.remove(shape.getMesh());
|
||||
sourceMap.remove(shape.getSourceID());
|
||||
}
|
||||
for (Subject3D shape : c.getAddedSubList()) {
|
||||
world.getChildren().add(shape.getMesh());
|
||||
selectionMap.put(shape.getMesh(), shape);
|
||||
shapeMap.put(shape.getMesh(), shape);
|
||||
sourceMap.put(shape.getSourceID(), shape.getMesh());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Shape3D getShape(int sourceID) {
|
||||
return sourceMap.get(sourceID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept mouse clicks on subjects in view. The applied listener cannot be removed.
|
||||
*/
|
||||
@@ -158,20 +168,48 @@ public class View3D extends Pane {
|
||||
scene.setOnMousePressed(e -> {
|
||||
PickResult result = e.getPickResult();
|
||||
if(result != null && result.getIntersectedNode() != null && result.getIntersectedNode() instanceof Shape3D) {
|
||||
trackSubject(selectionMap.get(result.getIntersectedNode()));
|
||||
untrackSubject();
|
||||
trackSubject(shapeMap.get(result.getIntersectedNode()));
|
||||
setThirdPerson();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ObjectProperty<Subject3D> targetProperty() {
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private void untrackSubject() {
|
||||
if(target != null) {
|
||||
target.getPosition().xProperty().removeListener(pivotX);
|
||||
target.getPosition().yProperty().removeListener(pivotY);
|
||||
target.getPosition().zProperty().removeListener(pivotZ);
|
||||
target.getHeading().angleProperty().removeListener(pivotHeading);
|
||||
if(target.get() != null) {
|
||||
trackBoat.stop();
|
||||
target.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,19 +218,16 @@ public class View3D extends Pane {
|
||||
* @param subject to track
|
||||
*/
|
||||
private void trackSubject(Subject3D subject) {
|
||||
untrackSubject();
|
||||
target = subject;
|
||||
target.set(subject);
|
||||
|
||||
updatePivot(target.getPosition());
|
||||
setYaw(target.getHeading().getAngle());
|
||||
|
||||
target.getPosition().xProperty().addListener(pivotX);
|
||||
target.getPosition().yProperty().addListener(pivotY);
|
||||
target.getPosition().zProperty().addListener(pivotZ);
|
||||
target.getHeading().angleProperty().addListener(pivotHeading);
|
||||
|
||||
this.setDistance(THIRD_PERSON_LIMIT);
|
||||
this.setPitch(20);
|
||||
this.trackBoat = new AnimationTimer() {
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
updatePivot(target.get().getPosition());
|
||||
setYaw(target.get().getHeading().getAngle());
|
||||
}
|
||||
};
|
||||
trackBoat.start();
|
||||
}
|
||||
|
||||
public void setNearClip(double nearClip) {
|
||||
@@ -203,6 +238,10 @@ public class View3D extends Pane {
|
||||
this.farClip = farClip;
|
||||
}
|
||||
|
||||
public Translate getPivot() {
|
||||
return pivot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the coordinates of the camera pivot once.
|
||||
* @param pivot source of coordinates
|
||||
@@ -230,13 +269,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);
|
||||
}
|
||||
@@ -257,4 +295,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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package visualiser.model;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.paint.Color;
|
||||
import network.Messages.Enums.BoatStatusEnum;
|
||||
@@ -62,6 +64,7 @@ public class VisualiserBoat extends Boat {
|
||||
|
||||
private ObjectProperty<GPSCoordinate> positionProperty;
|
||||
private ObjectProperty<Bearing> bearingProperty;
|
||||
private BooleanProperty hasCollided;
|
||||
|
||||
|
||||
/**
|
||||
@@ -74,6 +77,7 @@ public class VisualiserBoat extends Boat {
|
||||
super(boat.getSourceID(), boat.getName(), boat.getCountry());
|
||||
|
||||
this.color = color;
|
||||
this.hasCollided = new SimpleBooleanProperty(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -253,10 +257,6 @@ public class VisualiserBoat extends Boat {
|
||||
this.positionProperty.set(position);
|
||||
}
|
||||
|
||||
public ObjectProperty<GPSCoordinate> positionProperty() {
|
||||
return positionProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bearing getBearing() {
|
||||
return bearingProperty.get();
|
||||
@@ -270,7 +270,15 @@ public class VisualiserBoat extends Boat {
|
||||
this.bearingProperty.set(bearing);
|
||||
}
|
||||
|
||||
public ObjectProperty<Bearing> bearingProperty() {
|
||||
return bearingProperty;
|
||||
public boolean hasCollided() {
|
||||
return hasCollided.get();
|
||||
}
|
||||
|
||||
public BooleanProperty hasCollidedProperty() {
|
||||
return hasCollided;
|
||||
}
|
||||
|
||||
public void setHasCollided(boolean hasCollided) {
|
||||
this.hasCollided.set(hasCollided);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class VisualiserRaceController implements RunnableWithFramePeriod {
|
||||
compositeRaceCommand.addCommand(command);
|
||||
|
||||
} catch (CommandConstructionException e) {
|
||||
Logger.getGlobal().log(Level.WARNING, "VisualiserRaceController could not create a command for incoming message.");
|
||||
//Logger.getGlobal().log(Level.WARNING, "VisualiserRaceController could not create a command for incoming message.");
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Logger.getGlobal().log(Level.SEVERE, "VisualiserRaceController was interrupted on thread: " + Thread.currentThread() + " while waiting for messages.");
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 5.0 MiB |
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 4.8 MiB |
|
After Width: | Height: | Size: 4.4 MiB |
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 48 KiB |
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<Race>
|
||||
<RaceID>5326</RaceID>
|
||||
<RaceType>FLEET</RaceType>
|
||||
<CreationTimeDate>RACE_CREATION_TIME</CreationTimeDate>
|
||||
<RaceStartTime Postpone="false" Time="RACE_START_TIME"/>
|
||||
<Participants>
|
||||
</Participants>
|
||||
<CompoundMarkSequence>
|
||||
<Corner SeqID="1" CompoundMarkID="1" Rounding="SP" ZoneSize="3" />
|
||||
<Corner SeqID="2" CompoundMarkID="2" Rounding="Port" ZoneSize="3" />
|
||||
<Corner SeqID="3" CompoundMarkID="4" Rounding="Port" ZoneSize="3" />
|
||||
<Corner SeqID="4" CompoundMarkID="3" Rounding="Starboard" ZoneSize="3" />
|
||||
<Corner SeqID="5" CompoundMarkID="4" Rounding="Port" ZoneSize="3" />
|
||||
<Corner SeqID="6" CompoundMarkID="5" Rounding="SP" ZoneSize="3"/>
|
||||
</CompoundMarkSequence>
|
||||
<Course>
|
||||
<CompoundMark CompoundMarkID="1" Name="Start Line">
|
||||
<Mark SeqId="1" Name="PRO" TargetLat="32.296577" TargetLng="-64.854304" SourceID="101"/>
|
||||
<Mark SeqId="2" Name="PIN" TargetLat="32.293771" TargetLng="-64.855242" SourceID="102"/>
|
||||
</CompoundMark>
|
||||
<CompoundMark CompoundMarkID="2" Name="Marker 1">
|
||||
<Mark Name="Marker1" TargetLat="32.293039" TargetLng="-64.843983" SourceID="103"/>
|
||||
</CompoundMark>
|
||||
<CompoundMark CompoundMarkID="3" Name="Windward Gate">
|
||||
<Mark Name="WGL" SeqId="1" TargetLat="32.28468" TargetLng="-64.850045" SourceID="104"/>
|
||||
<Mark Name="WGR" SeqId="2" TargetLat="32.280164" TargetLng="-64.847591" SourceID="105"/>
|
||||
</CompoundMark>
|
||||
<CompoundMark CompoundMarkID="4" Name="Leeward Gate">
|
||||
<Mark Name="LGL" SeqId="1" TargetLat="32.309693" TargetLng="-64.835249" SourceID="106"/>
|
||||
<Mark Name="LGR" SeqId="2" TargetLat="32.308046" TargetLng="-64.831785" SourceID="107"/>
|
||||
</CompoundMark>
|
||||
<CompoundMark CompoundMarkID="5" Name="Finish Line">
|
||||
<Mark Name="FL" SeqId="1" TargetLat="32.317379" TargetLng="-64.839291" SourceID="108"/>
|
||||
<Mark Name="FR" SeqId="2" TargetLat="32.317257" TargetLng="-64.83626" SourceID="109"/>
|
||||
</CompoundMark>
|
||||
</Course>
|
||||
<CourseLimit>
|
||||
<Limit Lat="32.313922" Lon="-64.837168" SeqID="1"/>
|
||||
<Limit Lat="32.317379" Lon="-64.839291" SeqID="2"/>
|
||||
<Limit Lat="32.317911" Lon="-64.836996" SeqID="3"/>
|
||||
<Limit Lat="32.317257" Lon="-64.83626" SeqID="4"/>
|
||||
<Limit Lat="32.304273" Lon="-64.822834" SeqID="5"/>
|
||||
<Limit Lat="32.279097" Lon="-64.841545" SeqID="6"/>
|
||||
<Limit Lat="32.279604" Lon="-64.849871" SeqID="7"/>
|
||||
<Limit Lat="32.289545" Lon="-64.854162" SeqID="8"/>
|
||||
<Limit Lat="32.290198" Lon="-64.858711" SeqID="9"/>
|
||||
<Limit Lat="32.297164" Lon="-64.856394" SeqID="10"/>
|
||||
<Limit Lat="32.296148" Lon="-64.849184" SeqID="11"/>
|
||||
</CourseLimit>
|
||||
</Race>
|
||||