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
main
Hamish Ball 8 years ago
commit 0888a743a7

@ -219,7 +219,6 @@ public class Event {
long millisecondsToAdd = racePreStartTime + racePreparatoryTime; long millisecondsToAdd = racePreStartTime + racePreparatoryTime;
long secondsToAdd = millisecondsToAdd / 1000; long secondsToAdd = millisecondsToAdd / 1000;
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"); 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 network.Messages.LatestMessages;
import shared.model.RunnableWithFramePeriod; import shared.model.RunnableWithFramePeriod;
import java.util.ArrayList;
import java.util.Observable; import java.util.Observable;
import java.util.Observer; import java.util.Observer;
@ -139,6 +140,8 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer {
*/ */
private void raceLoop() { private void raceLoop() {
ArrayList<MockBoat> collisionBoats = new ArrayList<>();
long previousFrameTime = System.currentTimeMillis(); long previousFrameTime = System.currentTimeMillis();
while (race.getRaceStatusEnum() != RaceStatusEnum.FINISHED && loopBool) { 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 it is still racing, update its position.
if (boat.getStatus() == BoatStatusEnum.RACING) { if (boat.getStatus() == BoatStatusEnum.RACING) {
race.updatePosition(boat, framePeriod, race.getRaceClock().getDurationMilli()); 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 { } else {
@ -183,9 +194,14 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer {
// Change wind direction // Change wind direction
race.changeWindDirection(); race.changeWindDirection();
//Pass collision boats in
server.parseBoatCollisions(collisionBoats);
//Parse the race snapshot. //Parse the race snapshot.
server.parseSnapshot(); server.parseSnapshot();
collisionBoats.clear();
//Update the last frame time. //Update the last frame time.
previousFrameTime = currentTime; previousFrameTime = currentTime;
} }

@ -3,6 +3,7 @@ package mock.model;
import network.AckSequencer; import network.AckSequencer;
import network.Messages.*; import network.Messages.*;
import network.Messages.Enums.BoatLocationDeviceEnum; import network.Messages.Enums.BoatLocationDeviceEnum;
import network.Messages.Enums.YachtEventEnum;
import network.Messages.Enums.XMLMessageType; import network.Messages.Enums.XMLMessageType;
import shared.model.Bearing; import shared.model.Bearing;
import shared.model.CompoundMark; import shared.model.CompoundMark;
@ -24,6 +25,7 @@ public class RaceServer {
private MockRace race; private MockRace race;
private LatestMessages latestMessages; private LatestMessages latestMessages;
private static RaceServer server; private static RaceServer server;
private List<YachtEvent> collisionEvents = new ArrayList<>();
/** /**
@ -75,9 +77,19 @@ public class RaceServer {
//Parse the race status. //Parse the race status.
snapshotMessages.add(parseRaceStatus()); snapshotMessages.add(parseRaceStatus());
//Parse collisions
if(collisionEvents.size()>0){
snapshotMessages.addAll(collisionEvents);
}
latestMessages.setSnapshot(snapshotMessages); latestMessages.setSnapshot(snapshotMessages);
updateXMLFiles(); updateXMLFiles();
//Reset collision list
collisionEvents.clear();
//System.out.println(collisionEvents.size());
} }
/** /**
@ -314,4 +326,28 @@ public class RaceServer {
return message; 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; package mock.model.commandFactory;
import java.util.ArrayDeque;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;
/** /**
* Wraps multiple commands into a composite to execute queued commands during a frame. * 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; private Queue<Command> commands;
public CompositeCommand() { public CompositeCommand() {
this.commands = new ArrayDeque<>(); this.commands = new ConcurrentLinkedDeque<>();
} }
public void addCommand(Command command) { public void addCommand(Command command) {
commands.add(command); commands.offer(command);
} }
@Override @Override
public void execute() { 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. * Rotate the features in a race such as the boundary, and the marks.
* @param race the race to alter * @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; package visualiser.Commands.VisualiserRaceCommands;
import javafx.scene.media.AudioClip;
import mock.model.commandFactory.Command; import mock.model.commandFactory.Command;
import network.Messages.BoatStatus; import network.Messages.BoatStatus;
import network.Messages.Enums.BoatStatusEnum; import network.Messages.Enums.BoatStatusEnum;
@ -176,6 +177,10 @@ public class RaceStatusCommand implements Command {
//Record order in which boat finished leg. //Record order in which boat finished leg.
visualiserRace.getLegCompletionOrder().get(boat.getCurrentLeg()).add(boat); visualiserRace.getLegCompletionOrder().get(boat.getCurrentLeg()).add(boat);
//play sound
AudioClip sound = new AudioClip(getClass().getResource("/visualiser/sounds/passmark.wav").toExternalForm());
sound.play();
//Update boat. //Update boat.
boat.setCurrentLeg(leg); boat.setCurrentLeg(leg);
boat.setTimeAtLastMark(visualiserRace.getRaceClock().getCurrentTime()); boat.setTimeAtLastMark(visualiserRace.getRaceClock().getCurrentTime());

@ -23,7 +23,9 @@ public class VisualiserRaceCommandFactory {
switch (message.getType()) { 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); 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 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()); 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.add(playerBoatToSet, (count % 3) , row);
playerContainer.setMargin(playerBoatToSet, new Insets(10, 10, 10, 10)); playerContainer.setMargin(playerBoatToSet, new Insets(10, 10, 10, 10));
SeaSurface sea = new SeaSurface(750, 200, 250, 0, 210); SeaSurface sea = new SeaSurface(750, 200);
subjects.add(sea.getSurface()); sea.setX(250);
sea.setZ(210);
subjects.add(sea);
MeshView mesh = new MeshView(importer.getImport()); MeshView mesh = new MeshView(importer.getImport());
Subject3D subject = new Subject3D(mesh); Subject3D subject = new Subject3D(mesh,0);
subjects.add(subject); subjects.add(subject);
playerBoatToSet.setDistance(50); playerBoatToSet.setDistance(50);

@ -7,6 +7,8 @@ import javafx.scene.control.Button;
import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.scene.media.AudioClip;
import network.Messages.HostGame; import network.Messages.HostGame;
import visualiser.app.MatchBrowserSingleton; import visualiser.app.MatchBrowserSingleton;
import visualiser.model.RaceConnection; import visualiser.model.RaceConnection;
@ -34,6 +36,8 @@ public class LobbyController extends Controller {
private ObservableList<RaceConnection> connections; private ObservableList<RaceConnection> connections;
private ObservableList<RaceConnection> customConnections; private ObservableList<RaceConnection> customConnections;
private AudioClip sound;
//the socket for match browser //the socket for match browser
private DatagramSocket udpSocket; private DatagramSocket udpSocket;
private MatchBrowserLobbyInterface matchBrowserLobbyInterface; private MatchBrowserLobbyInterface matchBrowserLobbyInterface;
@ -67,6 +71,8 @@ public class LobbyController extends Controller {
* Refreshes the connections in the lobby * Refreshes the connections in the lobby
*/ */
public void refreshBtnPressed(){ public void refreshBtnPressed(){
sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/buttonpress.wav").toExternalForm());
sound.play();
addServerGames(); addServerGames();
for(RaceConnection connection: connections) { for(RaceConnection connection: connections) {
connection.check(); connection.check();
@ -92,6 +98,8 @@ public class LobbyController extends Controller {
} }
public void menuBtnPressed() throws IOException { public void menuBtnPressed() throws IOException {
sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/buttonpress.wav").toExternalForm());
sound.play();
matchBrowserLobbyInterface.closeSocket(); matchBrowserLobbyInterface.closeSocket();
loadScene("title.fxml"); loadScene("title.fxml");
} }
@ -100,6 +108,8 @@ public class LobbyController extends Controller {
* adds a new connection * adds a new connection
*/ */
public void addConnectionPressed(){ public void addConnectionPressed(){
sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/buttonpress.wav").toExternalForm());
sound.play();
String hostName = addressFld.getText(); String hostName = addressFld.getText();
String portString = portFld.getText(); String portString = portFld.getText();
try { try {

@ -3,29 +3,37 @@ package visualiser.Controllers;
import com.interactivemesh.jfx.importer.stl.StlMeshImporter; import com.interactivemesh.jfx.importer.stl.StlMeshImporter;
import javafx.animation.AnimationTimer; import javafx.animation.AnimationTimer;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList; import javafx.collections.transformation.SortedList;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.AmbientLight;
import javafx.scene.PointLight;
import javafx.scene.chart.LineChart; import javafx.scene.chart.LineChart;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.effect.Light;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.shape.MeshView; import javafx.scene.paint.Color;
import javafx.scene.shape.Sphere; import javafx.scene.paint.Material;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.*;
import javafx.scene.transform.Translate; import javafx.scene.transform.Translate;
import javafx.util.Callback; import javafx.util.Callback;
import network.Messages.Enums.RaceStatusEnum; import network.Messages.Enums.RaceStatusEnum;
import shared.dataInput.RaceDataSource; import shared.dataInput.RaceDataSource;
import shared.model.Leg; import shared.exceptions.BoatNotFoundException;
import shared.model.Mark; import shared.model.*;
import visualiser.app.App; import visualiser.app.App;
import visualiser.enums.TutorialState; import visualiser.enums.TutorialState;
import visualiser.gameController.ControllerClient; import visualiser.gameController.ControllerClient;
import visualiser.gameController.Keys.ControlKey; import visualiser.gameController.Keys.ControlKey;
import visualiser.layout.*;
import visualiser.model.*;
import visualiser.gameController.Keys.KeyFactory; import visualiser.gameController.Keys.KeyFactory;
import visualiser.layout.Subject3D; import visualiser.layout.Subject3D;
import visualiser.layout.View3D; import visualiser.layout.View3D;
@ -61,6 +69,15 @@ public class RaceViewController extends Controller {
private View3D view3D; private View3D view3D;
private ObservableList<Subject3D> viewSubjects; 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 :) // note: it says it's not used but it is! do not remove :)
private @FXML ArrowController arrowController; private @FXML ArrowController arrowController;
private @FXML GridPane canvasBase; private @FXML GridPane canvasBase;
@ -121,9 +138,8 @@ public class RaceViewController extends Controller {
} }
} }
/** private AnimationTimer arrowToNextMark;
* Sets up the listener and actions that occur when a key is pressed.
*/
private void initKeypressHandler() { private void initKeypressHandler() {
racePane.addEventFilter(KeyEvent.KEY_PRESSED, event -> { racePane.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
String codeString = event.getCode().toString(); 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}. * Initialises the various UI components to listen to the {@link #visualiserRace}.
*/ */
private void initialiseRaceVisuals() { 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 // initialise displays
initialiseFps(); initialiseFps();
initialiseInfoTable(); initialiseInfoTable();
initialiseView3D(); initialiseView3D(this.visualiserRace);
initialiseRaceClock(); initialiseRaceClock();
raceTimer(); // start the timer raceTimer(); // start the timer
new Sparkline(this.raceState, this.sparklineChart); new Sparkline(this.raceState, this.sparklineChart);
@ -200,55 +229,122 @@ public class RaceViewController extends Controller {
arrowController.setWindProperty(this.raceState.windProperty()); arrowController.setWindProperty(this.raceState.windProperty());
} }
private void initialiseView3D() { private void initialiseView3D(VisualiserRaceEvent race) {
viewSubjects = FXCollections.observableArrayList(); 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 // Import boat mesh
URL asset = RaceViewController.class.getClassLoader().getResource("assets/V1.2 " + URL asset = RaceViewController.class.getClassLoader().getResource("assets/V1.2 Complete Boat.stl");
"Complete Boat.stl");
StlMeshImporter importer = new StlMeshImporter(); StlMeshImporter importer = new StlMeshImporter();
importer.read(asset); importer.read(asset);
// Configure camera angles and control // 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.setDistance(1050);
view3D.setYaw(0); view3D.setBirdsEye();
view3D.setPitch(60);
view3D.enableTracking(); view3D.enableTracking();
view3D.addAmbientLight(ambientLight);
view3D.addPointLight(pointLight);
canvasBase.add(view3D, 0, 0); canvasBase.add(view3D, 0, 0);
// Set up projection from GPS to view // Set up projection from GPS to view
RaceDataSource raceData = raceState.getRaceDataSource(); RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource();
final GPSConverter gpsConverter = new GPSConverter(raceData, 450, 450); final GPSConverter gpsConverter = new GPSConverter(raceData, 450, 450);
view3D.setItems(viewSubjects); SkyBox skyBox = new SkyBox(750, 200, 250, 0, 210);
viewSubjects.addAll(skyBox.getSkyBoxPlanes());
// 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 // Position and add each mark to view
for (Mark mark : raceState.getMarks()) { for(Mark mark: race.getVisualiserRaceState().getMarks()) {
Subject3D subject = new Subject3D(new Sphere(2)); MeshView mesh = new MeshView(importerMark.getImport());
subject.setX(gpsConverter.convertGPS(mark.getPosition()).getX()); Subject3D markModel = new Subject3D(mesh, mark.getSourceID());
subject.setZ(gpsConverter.convertGPS(mark.getPosition()).getY());
viewSubjects.add(subject); markModel.setX(gpsConverter.convertGPS(mark.getPosition()).getX());
markModel.setZ(gpsConverter.convertGPS(mark.getPosition()).getY());
viewSubjects.add(markModel);
} }
// Position and add each boat to view // Position and add each boat to view
for (VisualiserBoat boat : raceState.getBoats()) { for(VisualiserBoat boat: race.getVisualiserRaceState().getBoats()) {
MeshView mesh = new MeshView(importer.getImport()); MeshView mesh;
Subject3D subject = new Subject3D(mesh); if(boat.getSourceID() == race.getVisualiserRaceState().getPlayerBoatID()) {
viewSubjects.add(subject); 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 // Track this boat's movement with the new subject
AnimationTimer trackBoat = new AnimationTimer() { AnimationTimer trackBoat = new AnimationTimer() {
@Override public void handle(long now) { @Override
subject.setHeading(boat.getBearing().degrees()); public void handle(long now) {
subject.setX(gpsConverter.convertGPS(boat.getPosition()).getX()); boatModel.setHeading(boat.getBearing().degrees());
subject.setZ(gpsConverter.convertGPS(boat.getPosition()).getY()); boatModel.setX(gpsConverter.convertGPS(boat.getPosition()).getX());
boatModel.setZ(gpsConverter.convertGPS(boat.getPosition()).getY());
} }
}; };
trackBoat.start(); 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 // Fix initial bird's-eye position
view3D.updatePivot(new Translate(250, 0, 210)); 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 // Bind zooming to scrolling
view3D.setOnScroll(e -> { view3D.setOnScroll(e -> {
view3D.updateDistance(e.getDeltaY()); view3D.updateDistance(e.getDeltaY());
@ -257,7 +353,7 @@ public class RaceViewController extends Controller {
// Bind zooming to keypress (Z/X default) // Bind zooming to keypress (Z/X default)
racePane.addEventFilter(KeyEvent.KEY_PRESSED, e -> { racePane.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
ControlKey key = keyFactory.getKey(e.getCode().toString()); ControlKey key = keyFactory.getKey(e.getCode().toString());
if (key != null) { if(key != null) {
switch (key.toString()) { switch (key.toString()) {
case "Zoom In": case "Zoom In":
//Check if race is a tutorial //Check if race is a tutorial
@ -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 * Initialises the frame rate functionality. This allows for toggling the
* frame rate, and connect the fps label to the race's fps property. * 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.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane; 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 javafx.stage.Modality;
import mock.exceptions.EventConstructionException; import mock.exceptions.EventConstructionException;
import visualiser.app.App; import visualiser.app.App;
import java.io.File;
import java.io.IOException; 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. * 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; 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 javafx.scene.shape.TriangleMesh;
import java.util.ArrayList; import java.util.ArrayList;
@ -29,13 +37,13 @@ public class Plane3D extends TriangleMesh{
for (float l = 0; l <= length; l += subLength) { for (float l = 0; l <= length; l += subLength) {
for (float w = 0; w <= width; w += subWidth){ for (float w = 0; w <= width; w += subWidth){
//add points //add points
pointsList.add(w + startW); pointsList.add(w + startW);
pointsList.add(l + startL); pointsList.add(l + startL);
pointsList.add(0f); pointsList.add(0f);
//addTexture coords //addTexture coords
textureCoord.add(1 - w/width); textureCoord.add(1 - w/width);
textureCoord.add(1 - l/length); textureCoord.add(1 - l/length);
} }
} }
@ -92,6 +100,7 @@ public class Plane3D extends TriangleMesh{
float x = this.getPoints().get(i); float x = this.getPoints().get(i);
float y = this.getPoints().get(i + 1); float y = this.getPoints().get(i + 1);
float z = this.getPoints().get(i + 2); 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));
} }
/** /**
@ -121,4 +130,4 @@ public class Plane3D extends TriangleMesh{
} }
} }

@ -1,65 +1,64 @@
package visualiser.layout; package visualiser.layout;
import javafx.geometry.Point3D;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter; import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage; import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial; import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.MeshView; import javafx.scene.shape.MeshView;
import javafx.scene.shape.Shape3D;
import javafx.scene.shape.TriangleMesh;
import visualiser.utils.PerlinNoiseGenerator; import visualiser.utils.PerlinNoiseGenerator;
/** /**
* Creates a SeaSurface * Creates a SeaSurface
*/ */
public class SeaSurface { public class SeaSurface extends Subject3D {
private float[][] noiseArray; private static Image image;
private Subject3D surface;
/** /**
* Sea Surface Constructor * Sea Surface Constructor
*
* @param size size of the sea surface (has to be square for simplicity's sake) * @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 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){ public SeaSurface(int size, double freq) {
noiseArray = PerlinNoiseGenerator.createNoise(size, freq); super(createSurface(size, freq), 0);
createSurface(); image = new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/ThickCloudsWaterDown2048.png"));
surface.setZ(z);
surface.setY(y);
surface.setX(x);
} }
/** /**
* Creates the sea surface * Creates the sea surface
*/ */
private void createSurface(){ private static Shape3D createSurface(int size, double freq) {
Image diffuseMap = createImage(noiseArray.length, noiseArray); float[][] noiseArray = PerlinNoiseGenerator.createNoise(size, freq);
Image diffuseMap = createImage(noiseArray.length, noiseArray); //image
PhongMaterial material = new PhongMaterial(); PhongMaterial material = new PhongMaterial();
material.setDiffuseColor(Color.web("#FFFFFF"));
material.setSpecularColor(Color.web("#000000"));
material.setDiffuseMap(diffuseMap); material.setDiffuseMap(diffuseMap);
//material.setSpecularColor(Color.WHITE);
Plane3D seaPlane = new Plane3D(noiseArray.length, noiseArray.length, 10, 10); Plane3D seaPlane = new Plane3D(noiseArray.length, noiseArray.length, 10, 10);
MeshView seaSurface = new MeshView(seaPlane); MeshView seaSurface = new MeshView(seaPlane);
// Box seaSurface = new Box(noiseArray.length, 0.1, noiseArray.length);
seaSurface.setMaterial(material); seaSurface.setMaterial(material);
seaSurface.setMouseTransparent(true); seaSurface.setMouseTransparent(true);
seaSurface.toFront(); seaSurface.toFront();
//seaSurface.setRotationAxis(new Point3D(1, 0, 0));
//seaSurface.setRotate(90);
surface = new Subject3D(seaSurface); return seaSurface;
} }
/** /**
* Create texture for uv mapping * Create texture for uv mapping
* @param size size of the image to make *
* @param size size of the image to make
* @param noise array of noise * @param noise array of noise
* @return image that is created * @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 width = (int) size;
int height = (int) size; int height = (int) size;
@ -94,9 +93,10 @@ public class SeaSurface {
/** /**
* Nomalises the values so that the colours are correct * Nomalises the values so that the colours are correct
* @param value value to normalise *
* @param min current min * @param value value to normalise
* @param max current max * @param min current min
* @param max current max
* @param newMin new min * @param newMin new min
* @param newMax new max * @param newMax new max
* @return returns normalised value * @return returns normalised value
@ -109,9 +109,10 @@ public class SeaSurface {
/** /**
* clamps a value between a min and max * clamps a value between a min and max
*
* @param value value to clamp * @param value value to clamp
* @param min minimum value it can be * @param min minimum value it can be
* @param max maximum value it can be * @param max maximum value it can be
* @return result after clamp * @return result after clamp
*/ */
private static double clamp(double value, double min, double max) { private static double clamp(double value, double min, double max) {
@ -126,11 +127,11 @@ public class SeaSurface {
} }
/** /**
* Get surface * Prevent rescaling of sea surface
* @return the surface so it can be drawn *
* @param scale ignored
*/ */
public Subject3D getSurface(){ @Override
return surface; 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.shape.Shape3D;
import javafx.scene.transform.Rotate; import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate; import javafx.scene.transform.Translate;
/** /**
@ -12,6 +13,10 @@ public class Subject3D {
* Rendered mesh * Rendered mesh
*/ */
private Shape3D mesh; private Shape3D mesh;
/**
* Source ID of subject in game model
*/
private int sourceID;
/** /**
* Position translation updated by state listeners * Position translation updated by state listeners
@ -23,22 +28,29 @@ public class Subject3D {
*/ */
private Rotate heading; private Rotate heading;
private Scale scale;
/** /**
* Constructor for view subject wrapper * Constructor for view subject wrapper
* @param mesh to be rendered * @param mesh to be rendered
*/ */
public Subject3D(Shape3D mesh) { public Subject3D(Shape3D mesh, int sourceID) {
this.mesh = mesh; this.mesh = mesh;
this.sourceID = sourceID;
this.scale = new Scale();
this.position = new Translate(); this.position = new Translate();
this.heading = new Rotate(0, Rotate.Y_AXIS); this.heading = new Rotate(0, Rotate.Y_AXIS);
this.mesh.getTransforms().addAll(position, scale, heading, new Rotate(90, Rotate.X_AXIS), new Rotate(180, Rotate.Y_AXIS));
this.mesh.getTransforms().addAll(position, heading, new Rotate(90, Rotate.X_AXIS), new Rotate(180, Rotate.Y_AXIS));
} }
public Shape3D getMesh() { public Shape3D getMesh() {
return mesh; return mesh;
} }
public int getSourceID() {
return sourceID;
}
public Translate getPosition() { public Translate getPosition() {
return this.position; return this.position;
} }
@ -47,6 +59,12 @@ public class Subject3D {
return heading; return heading;
} }
public void setScale(double scale) {
this.scale.setX(scale);
this.scale.setY(scale);
this.scale.setZ(scale);
}
public void setX(double x) { public void setX(double x) {
position.setX(x); position.setX(x);
} }

@ -1,11 +1,12 @@
package visualiser.layout; package visualiser.layout;
import javafx.animation.AnimationTimer;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.scene.Group; import javafx.scene.*;
import javafx.scene.PerspectiveCamera;
import javafx.scene.SubScene;
import javafx.scene.input.PickResult; import javafx.scene.input.PickResult;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
@ -33,11 +34,15 @@ public class View3D extends Pane {
/** /**
* Map for selecting Subject3D from Shape3D * 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 * Subject tracked by camera
*/ */
private Subject3D target; private ObjectProperty<Subject3D> target;
/** /**
* Rendering container for shapes * Rendering container for shapes
*/ */
@ -67,44 +72,43 @@ public class View3D extends Pane {
*/ */
private Rotate pitch; 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); private AnimationTimer trackBoat;
/** /**
* Single listener for subject position (z) changes * Distance to switch from third person to bird's eye
*/ */
private ChangeListener<? super Number> pivotZ = (o, prev, curr) -> pivot.setZ((double)curr); private final double THIRD_PERSON_LIMIT = 100;
/** /**
* Distance to switch from third person to bird's eye * Distance to stop zoom
*/ */
private double THIRD_PERSON_LIMIT = 100; private final double FIRST_PERSON_LIMIT = 2;
/** /**
* Default constructor for View3D. Sets up Scene and PerspectiveCamera. * 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.world = new Group();
this.selectionMap = new HashMap<>(); this.shapeMap = new HashMap<>();
this.target = null; this.sourceMap = new HashMap<>();
this.target = new SimpleObjectProperty<>(null);
this.scene = new SubScene(world, 300, 300); this.scene = new SubScene(world, 300, 300);
scene.widthProperty().bind(this.widthProperty()); scene.widthProperty().bind(this.widthProperty());
scene.heightProperty().bind(this.heightProperty()); 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()); scene.setCamera(buildCamera());
this.getChildren().add(scene); this.getChildren().add(scene);
} }
public View3D(){
this(true);
}
/** /**
* Sets up camera view frustum and binds transformations * Sets up camera view frustum and binds transformations
* @return perspective camera * @return perspective camera
@ -140,17 +144,23 @@ public class View3D extends Pane {
if (c.wasRemoved() || c.wasAdded()) { if (c.wasRemoved() || c.wasAdded()) {
for (Subject3D shape : c.getRemoved()) { for (Subject3D shape : c.getRemoved()) {
world.getChildren().remove(shape.getMesh()); world.getChildren().remove(shape.getMesh());
selectionMap.remove(shape.getMesh()); shapeMap.remove(shape.getMesh());
sourceMap.remove(shape.getSourceID());
} }
for (Subject3D shape : c.getAddedSubList()) { for (Subject3D shape : c.getAddedSubList()) {
world.getChildren().add(shape.getMesh()); 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. * 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 -> { scene.setOnMousePressed(e -> {
PickResult result = e.getPickResult(); PickResult result = e.getPickResult();
if(result != null && result.getIntersectedNode() != null && result.getIntersectedNode() instanceof Shape3D) { 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 * Stop camera from following the last selected subject
*/ */
private void untrackSubject() { private void untrackSubject() {
if(target != null) { if(target.get() != null) {
target.getPosition().xProperty().removeListener(pivotX); trackBoat.stop();
target.getPosition().yProperty().removeListener(pivotY); target.setValue(null);
target.getPosition().zProperty().removeListener(pivotZ);
target.getHeading().angleProperty().removeListener(pivotHeading);
} }
} }
@ -180,19 +218,16 @@ public class View3D extends Pane {
* @param subject to track * @param subject to track
*/ */
private void trackSubject(Subject3D subject) { private void trackSubject(Subject3D subject) {
untrackSubject(); target.set(subject);
target = 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.trackBoat = new AnimationTimer() {
this.setPitch(20); @Override
public void handle(long now) {
updatePivot(target.get().getPosition());
setYaw(target.get().getHeading().getAngle());
}
};
trackBoat.start();
} }
public void setNearClip(double nearClip) { public void setNearClip(double nearClip) {
@ -203,6 +238,10 @@ public class View3D extends Pane {
this.farClip = farClip; this.farClip = farClip;
} }
public Translate getPivot() {
return pivot;
}
/** /**
* Sets the coordinates of the camera pivot once. * Sets the coordinates of the camera pivot once.
* @param pivot source of coordinates * @param pivot source of coordinates
@ -230,13 +269,12 @@ public class View3D extends Pane {
public void updateDistance(double delta) { public void updateDistance(double delta) {
double distance = -this.distance.getZ() + delta; double distance = -this.distance.getZ() + delta;
if(distance <= 0) { if(distance <= FIRST_PERSON_LIMIT) {
this.setDistance(0); this.setDistance(FIRST_PERSON_LIMIT);
} else if(distance > THIRD_PERSON_LIMIT) { } else if(distance > THIRD_PERSON_LIMIT) {
untrackSubject();
this.setYaw(0);
this.setPitch(60);
this.setDistance(distance); this.setDistance(distance);
untrackSubject();
setBirdsEye();
} else { } else {
this.setDistance(distance); this.setDistance(distance);
} }
@ -257,4 +295,12 @@ public class View3D extends Pane {
public void setPitch(double pitch) { public void setPitch(double pitch) {
this.pitch.setAngle(-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; package visualiser.model;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import network.Messages.Enums.BoatStatusEnum; import network.Messages.Enums.BoatStatusEnum;
@ -62,6 +64,7 @@ public class VisualiserBoat extends Boat {
private ObjectProperty<GPSCoordinate> positionProperty; private ObjectProperty<GPSCoordinate> positionProperty;
private ObjectProperty<Bearing> bearingProperty; private ObjectProperty<Bearing> bearingProperty;
private BooleanProperty hasCollided;
/** /**
@ -74,6 +77,7 @@ public class VisualiserBoat extends Boat {
super(boat.getSourceID(), boat.getName(), boat.getCountry()); super(boat.getSourceID(), boat.getName(), boat.getCountry());
this.color = color; this.color = color;
this.hasCollided = new SimpleBooleanProperty(false);
} }
@ -253,10 +257,6 @@ public class VisualiserBoat extends Boat {
this.positionProperty.set(position); this.positionProperty.set(position);
} }
public ObjectProperty<GPSCoordinate> positionProperty() {
return positionProperty;
}
@Override @Override
public Bearing getBearing() { public Bearing getBearing() {
return bearingProperty.get(); return bearingProperty.get();
@ -270,7 +270,15 @@ public class VisualiserBoat extends Boat {
this.bearingProperty.set(bearing); this.bearingProperty.set(bearing);
} }
public ObjectProperty<Bearing> bearingProperty() { public boolean hasCollided() {
return bearingProperty; 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); compositeRaceCommand.addCommand(command);
} catch (CommandConstructionException e) { } 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) { } catch (InterruptedException e) {
Logger.getGlobal().log(Level.SEVERE, "VisualiserRaceController was interrupted on thread: " + Thread.currentThread() + " while waiting for messages."); 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()); 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());
}
} }

@ -85,4 +85,4 @@ public class PerlinNoiseGenerator {
static { for (int i=0; i < 256 ; i++) p[256+i] = p[i] = permutation[i]; } static { for (int i=0; i < 256 ; i++) p[256+i] = p[i] = permutation[i]; }
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

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>
Loading…
Cancel
Save