/* Copyright (c) 2016 Jesper Öqvist <jesper@llbit.se> * * This file is part of Chunky. * * Chunky is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Chunky is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with Chunky. If not, see <http://www.gnu.org/licenses/>. */ package se.llbit.chunky.ui.render; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.value.ChangeListener; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; import javafx.scene.control.Button; import javafx.scene.control.ChoiceBox; import javafx.scene.control.ComboBox; import javafx.scene.control.MenuButton; import javafx.scene.control.MenuItem; import javafx.scene.control.Tab; import javafx.scene.control.TextField; import javafx.scene.control.TitledPane; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.util.converter.NumberStringConverter; import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.map.WorldMapLoader; import se.llbit.chunky.renderer.projection.ProjectionMode; import se.llbit.chunky.renderer.scene.Camera; import se.llbit.chunky.renderer.scene.CameraPreset; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.ui.DoubleAdjuster; import se.llbit.chunky.ui.RenderControlsFxController; import se.llbit.json.JsonMember; import se.llbit.json.JsonObject; import se.llbit.math.QuickMath; import se.llbit.math.Vector3; import java.io.IOException; import java.net.URL; import java.util.ResourceBundle; public class CameraTab extends Tab implements RenderControlsTab, Initializable { private Scene scene; @FXML private MenuButton loadPreset; @FXML private ComboBox<String> cameras; @FXML private Button duplicate; @FXML private Button removeCamera; @FXML private TitledPane positionOrientation; @FXML private TextField posX; @FXML private TextField posY; @FXML private TextField posZ; @FXML private TextField yawField; @FXML private TextField pitchField; @FXML private TextField rollField; @FXML private Button cameraToPlayer; @FXML private Button centerCamera; @FXML private ChoiceBox<ProjectionMode> projectionMode; @FXML private DoubleAdjuster fov; @FXML private DoubleAdjuster dof; @FXML private DoubleAdjuster subjectDistance; @FXML private Button autofocus; private DoubleProperty xpos = new SimpleDoubleProperty(); private DoubleProperty ypos = new SimpleDoubleProperty(); private DoubleProperty zpos = new SimpleDoubleProperty(); private DoubleProperty yaw = new SimpleDoubleProperty(); private DoubleProperty pitch = new SimpleDoubleProperty(); private DoubleProperty roll = new SimpleDoubleProperty(); private WorldMapLoader mapLoader; public CameraTab() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("CameraTab.fxml")); loader.setRoot(this); loader.setController(this); loader.load(); } @Override public void update(Scene scene) { updateCameraList(); updateCameraPosition(); updateCameraDirection(); updateProjectionMode(); updateFov(); updateDof(); updateSubjectDistance(); } @Override public Tab getTab() { return this; } private void updateProjectionMode() { projectionMode.getSelectionModel().select(scene.camera().getProjectionMode()); } private void updateSubjectDistance() { subjectDistance.set(scene.camera().getSubjectDistance()); } private void updateDof() { dof.set(scene.camera().getDof()); } private void updateFov() { fov.set(scene.camera().getFov()); } @Override public void initialize(URL location, ResourceBundle resources) { loadPreset.setTooltip(new Tooltip("Load a camera preset. Overwrites current camera settings.")); for (CameraPreset preset : CameraPreset.values()) { MenuItem menuItem = new MenuItem(preset.toString()); menuItem.setGraphic(new ImageView(preset.getIcon())); menuItem.setOnAction(e -> { Camera camera = scene.camera(); preset.apply(camera); projectionMode.getSelectionModel().select(camera.getProjectionMode()); updateFov(); updateCameraDirection(); }); loadPreset.getItems().add(menuItem); } ChangeListener<? super String> cameraSelectionListener = (observable, oldValue, newValue) -> { if (newValue != null && oldValue != null) { if (cameras.getItems().contains(newValue)) { // Save current camera and load existing camera preset. if (!cameras.getItems().contains(oldValue)) { cameras.getItems().add(oldValue); } scene.saveCameraPreset(oldValue); scene.loadCameraPreset(newValue); updateProjectionMode(); updateFov(); updateDof(); updateSubjectDistance(); updateCameraPosition(); updateCameraDirection(); } else { // Create new camera preset. cameras.getItems().add(newValue); scene.saveCameraPreset(oldValue); scene.camera().name = newValue; } } }; cameras.getSelectionModel().selectedItemProperty().addListener(cameraSelectionListener); duplicate.setTooltip(new Tooltip("Create a copy of the current camera.")); duplicate.setOnAction(e -> generateNextCameraName()); removeCamera.setTooltip(new Tooltip("Delete the current camera.")); removeCamera.setOnAction(e -> { String selected = cameras.getSelectionModel().getSelectedItem(); if (selected != null && cameras.getItems().size() > 1) { cameras.getSelectionModel().selectedItemProperty().removeListener(cameraSelectionListener); cameras.getItems().remove(selected); scene.deleteCameraPreset(selected); String next = cameras.getValue(); if (next == null) { next = cameras.getItems().get(0); cameras.setValue(next); } scene.loadCameraPreset(next); updateProjectionMode(); updateFov(); updateDof(); updateSubjectDistance(); updateCameraPosition(); updateCameraDirection(); cameras.getSelectionModel().selectedItemProperty().addListener(cameraSelectionListener); } }); positionOrientation.expandedProperty().addListener((observable, oldValue, newValue) -> { if (newValue) { updateCameraPosition(); updateCameraDirection(); } }); posX.textProperty().bindBidirectional(xpos, new NumberStringConverter()); posY.textProperty().bindBidirectional(ypos, new NumberStringConverter()); posZ.textProperty().bindBidirectional(zpos, new NumberStringConverter()); EventHandler<KeyEvent> positionHandler = e -> { if (e.getCode() == KeyCode.ENTER) { scene.camera() .setPosition(new Vector3(xpos.getValue(), ypos.get(), zpos.get())); } }; posX.addEventFilter(KeyEvent.KEY_PRESSED, positionHandler); posY.addEventFilter(KeyEvent.KEY_PRESSED, positionHandler); posZ.addEventFilter(KeyEvent.KEY_PRESSED, positionHandler); yawField.textProperty().bindBidirectional(yaw, new NumberStringConverter()); pitchField.textProperty().bindBidirectional(pitch, new NumberStringConverter()); rollField.textProperty().bindBidirectional(roll, new NumberStringConverter()); EventHandler<KeyEvent> directionHandler = e -> { if (e.getCode() == KeyCode.ENTER) { scene.camera() .setView(QuickMath.degToRad(yaw.get()), QuickMath.degToRad(pitch.get()), QuickMath.degToRad(roll.get())); } }; yawField.setTooltip(new Tooltip("Camera yaw.")); yawField.addEventFilter(KeyEvent.KEY_PRESSED, directionHandler); pitchField.setTooltip(new Tooltip("Camera pitch.")); pitchField.addEventFilter(KeyEvent.KEY_PRESSED, directionHandler); rollField.setTooltip(new Tooltip("Camera roll.")); rollField.addEventFilter(KeyEvent.KEY_PRESSED, directionHandler); cameraToPlayer.setTooltip(new Tooltip("Move camera to the player position.")); cameraToPlayer.setOnAction(e -> { scene.moveCameraToPlayer(); updateCameraPosition(); }); centerCamera.setTooltip(new Tooltip("Center camera above loaded chunks.")); centerCamera.setOnAction(e -> { scene.moveCameraToCenter(); updateCameraPosition(); }); projectionMode.getItems().addAll(ProjectionMode.values()); projectionMode.getSelectionModel().select(ProjectionMode.PINHOLE); projectionMode.getSelectionModel().selectedItemProperty() .addListener((observable, oldValue, newValue) -> { scene.camera().setProjectionMode(newValue); updateFov(); }); fov.setName("Field of view (zoom)"); fov.setRange(0.1, 180); fov.clampMin(); fov.onValueChange(value -> scene.camera().setFoV(value)); dof.setName("Depth of field"); dof.setRange(Camera.MIN_DOF, Camera.MAX_DOF); dof.clampMin(); dof.makeLogarithmic(); dof.setMaxInfinity(true); dof.onValueChange(value -> scene.camera().setDof(value)); subjectDistance.setName("Subject distance"); subjectDistance.setRange(Camera.MIN_SUBJECT_DISTANCE, Camera.MAX_SUBJECT_DISTANCE); subjectDistance.clampMax(); subjectDistance.makeLogarithmic(); subjectDistance.setTooltip("Distance to focal plane."); subjectDistance.onValueChange(value -> scene.camera().setSubjectDistance(value)); autofocus.setTooltip(new Tooltip( "Focuses on the object in the center of the view indicated by the crosshairs.")); autofocus.setOnAction(e -> { scene.autoFocus(); updateDof(); updateSubjectDistance(); }); } private void generateNextCameraName() { int index = cameras.getItems().size() + 1; while (true) { boolean unique = true; String newName = String.format("camera %d", index); for (String name : cameras.getItems()) { if (name.equals(newName)) { unique = false; break; } } if (unique) { cameras.setValue(newName); break; } else { index += 1; } } } private void updateCameraList() { cameras.getItems().clear(); JsonObject presets = scene.getCameraPresets(); for (JsonMember member : presets) { String name = member.getName().trim(); if (!name.isEmpty()) { cameras.getItems().add(name); } } Camera camera = scene.camera(); if (!cameras.getItems().contains(camera.name)) { cameras.getItems().add(camera.name); } if (cameras.getValue() == null || cameras.getValue().isEmpty()) { cameras.setValue(camera.name); } } private void updateCameraPosition() { Camera camera = scene.camera(); Vector3 pos = camera.getPosition(); if (positionOrientation.isExpanded()) { xpos.set(pos.x); ypos.set(pos.y); zpos.set(pos.z); } if (PersistentSettings.getFollowCamera()) { mapLoader.panTo(pos); } mapLoader.drawCameraVisualization(); } private void updateCameraDirection() { if (positionOrientation.isExpanded()) { Camera camera = scene.camera(); yaw.set(QuickMath.radToDeg(camera.getYaw())); pitch.set(QuickMath.radToDeg(camera.getPitch())); roll.set(QuickMath.radToDeg(camera.getRoll())); } mapLoader.drawCameraVisualization(); } @Override public void onChunksLoaded() { update(scene); } @Override public void setController(RenderControlsFxController controller) { this.mapLoader = controller.getChunkyController().getMapLoader(); scene = controller.getRenderController().getSceneManager().getScene(); scene.camera().setDirectionListener(this::updateCameraDirection); scene.camera().setPositionListener(this::updateCameraPosition); scene.camera().setProjectionListener(() -> { updateFov(); mapLoader.drawCameraVisualization(); }); } }