/**
* ShapeBaseSample.java
*
* Copyright (c) 2013-2016, F(X)yz
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of F(X)yz, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL F(X)yz BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.fxyz3d.samples.shapes;
import com.sun.javafx.geom.Vec3d;
import java.text.NumberFormat;
import java.util.concurrent.CountDownLatch;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.Point3D;
import javafx.geometry.Side;
import javafx.scene.AmbientLight;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressBar;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.PickResult;
import static javafx.scene.layout.Region.USE_PREF_SIZE;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.CullFace;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.Shape3D;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.util.Duration;
import org.controlsfx.control.HiddenSidesPane;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import org.fxyz3d.client.ModelInfoTracker;
import org.fxyz3d.controls.ControlPanel;
import org.fxyz3d.controls.factory.ControlFactory;
import org.fxyz3d.samples.FXyzSample;
import org.fxyz3d.scene.Skybox;
import org.fxyz3d.shapes.primitives.Text3DMesh;
import org.fxyz3d.shapes.primitives.TexturedMesh;
import org.fxyz3d.utils.CameraTransformer;
/**
* + mainPane resizable StackPane ++ subScene SubScene, with camera +++ root
* Group ++++ group Group created in the subclass with the 3D Shape
*
* @author jpereda
* @param <T>
*/
public abstract class ShapeBaseSample<T extends Node> extends FXyzSample {
protected abstract void createMesh();
protected abstract void addMeshAndListeners();
private final double sceneWidth = 600;
private final double sceneHeight = 600;
protected long lastEffect;
protected PointLight sceneLight1;
protected PointLight sceneLight2;
private Group light1Group;
private Group light2Group;
private Group lightingGroup;
protected PerspectiveCamera camera;
private CameraTransformer cameraTransform;
protected Rotate rotateY = new Rotate(0, 0, 0, 0, Rotate.Y_AXIS);
protected SubScene subScene;
protected Group root;
protected Group group;
protected StackPane mainPane;
protected HiddenSidesPane parentPane;
protected T model;
protected ModelInfoTracker modelInfo;
protected PhongMaterial material = new PhongMaterial();
protected Button exportButton;
private Service<Void> service;
private ProgressBar progressBar;
private long time;
private final BooleanProperty isActive = new SimpleBooleanProperty(this, "activeSample", false);
protected final BooleanProperty useSkybox = new SimpleBooleanProperty(this, "SkyBox Enabled", false);
private final BooleanProperty isPicking = new SimpleBooleanProperty(this, "isPicking");
protected final Property<DrawMode> drawMode = new SimpleObjectProperty<>(model, "drawMode", DrawMode.FILL);
protected final Property<CullFace> culling = new SimpleObjectProperty<>(model, "culling", CullFace.BACK);
private final CountDownLatch latch = new CountDownLatch(1);
private final NumberFormat numberFormat = NumberFormat.getInstance();
private Vec3d vecIni, vecPos;
private double distance;
private Sphere sphere;
//Bindings..
Subscription modelWidthBinder, modelHeightBinder, modelDepthBinder;
Subscription sceneActiveBinder;
public ShapeBaseSample() {
numberFormat.setMaximumFractionDigits(1);
initSample();
}
private void initSample() {
buildCamera();
buildRootNode();
buildSkybox();
buildModelContainer();
buildSubScene();
buildSubScenePane();
buildParentPane();
createListeners();
}
private void releaseBinders() {
if(modelWidthBinder != null){
modelWidthBinder.unsubscribe();
modelHeightBinder.unsubscribe();
modelDepthBinder.unsubscribe();
}
}
private void attachBinders() {
// should be conditional on scene not being null
modelWidthBinder = EasyBind.subscribe(model.boundsInParentProperty(),
(s) -> modelInfo.getBoundsWidth().setText(numberFormat.format(s.getWidth())));
modelHeightBinder = EasyBind.subscribe(model.boundsInParentProperty(),
(s) -> modelInfo.getBoundsHeight().setText(numberFormat.format(s.getHeight())));
modelDepthBinder = EasyBind.subscribe(model.boundsInParentProperty(),
(s) -> modelInfo.getBoundsDepth().setText(numberFormat.format(s.getDepth())));
}
private void createListeners() {
drawMode.addListener((obs, b, b1) -> {
if (model != null) {
if (model instanceof Shape3D) {
((Shape3D) model).setDrawMode(drawMode.getValue());
} else if (model instanceof Group) {
((Group) model).getChildren().filtered(Shape3D.class::isInstance)
.forEach(shape -> ((Shape3D) shape).setDrawMode(drawMode.getValue()));
}
}
});
culling.addListener((obs, b, b1) -> {
if (model != null) {
if (model instanceof Shape3D) {
((Shape3D) model).setCullFace(culling.getValue());
} else if (model instanceof Group) {
((Group) model).getChildren().filtered(Shape3D.class::isInstance)
.forEach(shape -> ((Shape3D) shape).setCullFace(culling.getValue()));
}
}
});
}
private void buildSkybox() {
skyBox = new Skybox(
top,
bottom,
left,
right,
front,
back,
100000,
camera
);
skyBox.visibleProperty().bind(useSkybox);
root.getChildren().add(0, skyBox);
}
private void buildCamera() {
cameraTransform = new CameraTransformer();
camera = new PerspectiveCamera(true);
camera.setNearClip(0.1);
camera.setFarClip(100000.0);
camera.setTranslateZ(-50);
camera.setVerticalFieldOfView(false);
camera.setFieldOfView(42);
//setup camera transform for rotational support
cameraTransform.setTranslate(0, 0, 0);
cameraTransform.getChildren().add(camera);
cameraTransform.ry.setAngle(-45.0);
cameraTransform.rx.setAngle(-10.0);
//add a Point Light for better viewing of the grid coordinate system
final PointLight light = new PointLight(Color.GAINSBORO);
final AmbientLight amb = new AmbientLight(Color.WHITE);
amb.getScope().add(cameraTransform);
cameraTransform.getChildren().addAll(light);
light.translateXProperty().bind(camera.translateXProperty());
light.translateYProperty().bind(camera.translateYProperty());
light.translateZProperty().bind(camera.translateZProperty());
}
private void buildSubScenePane() {
mainPane = new StackPane();
mainPane.setPrefSize(sceneWidth, sceneHeight);
mainPane.setMaxSize(StackPane.USE_COMPUTED_SIZE, StackPane.USE_COMPUTED_SIZE);
mainPane.setMinSize(sceneWidth, sceneHeight);
mainPane.getChildren().add(subScene);
mainPane.setPickOnBounds(false);
subScene.widthProperty().bind(mainPane.widthProperty());
subScene.heightProperty().bind(mainPane.heightProperty());
}
private void buildRootNode() {
//==========================================================
// Need a scene control panel to allow alterations to properties
sceneLight1 = new PointLight();
sceneLight1.setTranslateX(500);
sceneLight2 = new PointLight();
sceneLight2.setTranslateX(-500);
light1Group = new Group(sceneLight1);
light2Group = new Group(sceneLight2);
lightingGroup = new Group(light1Group, light2Group);
//==========================================================
root = new Group(lightingGroup);
sceneLight1.getScope().add(root);
sceneLight2.getScope().add(root);
}
private void buildModelContainer() {
group = new Group();
group.getChildren().add(cameraTransform);
root.getChildren().add(group);
}
private void buildSubScene() {
subScene = new SubScene(root, sceneWidth, sceneHeight, true, SceneAntialiasing.BALANCED);
subScene.setFill(Color.TRANSPARENT);//Color.web("#0d0d0d"));
subScene.setCamera(camera);
subScene.setFocusTraversable(false);
//First person shooter keyboard movement
subScene.setOnKeyPressed(event -> {
double change = 10.0;
//Add shift modifier to simulate "Running Speed"
if (event.isShiftDown()) {
change = 50.0;
}
//What key did the user press?
KeyCode keycode = event.getCode();
//Step 2c: Add Zoom controls
if (keycode == KeyCode.W) {
camera.setTranslateZ(camera.getTranslateZ() + change);
}
if (keycode == KeyCode.S) {
camera.setTranslateZ(camera.getTranslateZ() - change);
}
//Step 2d: Add Strafe controls
if (keycode == KeyCode.A) {
camera.setTranslateX(camera.getTranslateX() - change);
}
if (keycode == KeyCode.D) {
camera.setTranslateX(camera.getTranslateX() + change);
}
});
subScene.setOnMousePressed((MouseEvent me) -> {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
PickResult pr = me.getPickResult();
if (pr != null && pr.getIntersectedNode() != null
&& pr.getIntersectedNode() instanceof Sphere
&& pr.getIntersectedNode().getId().equals("knot")) {
distance = pr.getIntersectedDistance();
sphere = (Sphere) pr.getIntersectedNode();
isPicking.set(true);
vecIni = unProjectDirection(mousePosX, mousePosY, subScene.getWidth(), subScene.getHeight());
}
});
subScene.setOnMouseDragged((MouseEvent me) -> {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
if (isPicking.get()) {
double modifier = (me.isControlDown() ? 0.01 : me.isAltDown() ? 1.0 : 0.1) * (30d / camera.getFieldOfView());
modifier *= (30d / camera.getFieldOfView());
vecPos = unProjectDirection(mousePosX, mousePosY, subScene.getWidth(), subScene.getHeight());
Point3D p = new Point3D(distance * (vecPos.x - vecIni.x),
distance * (vecPos.y - vecIni.y), distance * (vecPos.z - vecIni.z));
sphere.getTransforms().add(new Translate(modifier * p.getX(), modifier * p.getY(), modifier * p.getZ()));
vecIni = vecPos;
} else {
double modifier = 10.0;
double modifierFactor = 0.1;
if (me.isControlDown()) {
modifier = 0.1;
}
if (me.isShiftDown()) {
modifier = 50.0;
}
if (me.isPrimaryButtonDown()) {
cameraTransform.ry.setAngle(((cameraTransform.ry.getAngle() + mouseDeltaX * modifierFactor * modifier * 2.0) % 360 + 540) % 360 - 180); // +
cameraTransform.rx.setAngle(((cameraTransform.rx.getAngle() - mouseDeltaY * modifierFactor * modifier * 2.0) % 360 + 540) % 360 - 180); // -
} else if (me.isSecondaryButtonDown()) {
double z = camera.getTranslateZ();
double newZ = z + mouseDeltaX * modifierFactor * modifier;
camera.setTranslateZ(newZ);
} else if (me.isMiddleButtonDown()) {
cameraTransform.t.setX(cameraTransform.t.getX() + mouseDeltaX * modifierFactor * modifier * 0.3); // -
cameraTransform.t.setY(cameraTransform.t.getY() + mouseDeltaY * modifierFactor * modifier * 0.3); // -
}
}
});
subScene.setOnMouseReleased((MouseEvent me) -> {
if (isPicking.get()) {
isPicking.set(false);
}
});
}
private void buildParentPane() {
parentPane = new HiddenSidesPane();
modelInfo = new ModelInfoTracker(parentPane);
parentPane.setContent(mainPane);
parentPane.setBottom(modelInfo);
parentPane.setTriggerDistance(20);
parentPane.setAnimationDelay(Duration.ONE);
parentPane.setPinnedSide(Side.BOTTOM);
}
private void loadSample() {
service = new Service<Void>() {
@Override
protected Task<Void> createTask() {
return new Task<Void>() {
@Override
protected Void call() throws Exception {
createMesh();
return null;
}
};
}
@Override
protected void failed() {
super.failed();
getException().printStackTrace(System.err);
}
@Override
protected void succeeded() {
addMeshAndListeners();
attachBinders();
mainPane.getChildren().remove(progressBar);
if (model != null && model instanceof MeshView) {
material = (PhongMaterial) ((MeshView) model).getMaterial();
} else {
if (model != null && model instanceof Group) {
if (!((Group) model).getChildren().filtered(isShape -> isShape instanceof MeshView).isEmpty()) {
material = (PhongMaterial) ((MeshView) ((Group) model).getChildren().filtered(t -> t instanceof MeshView).get(0)).getMaterial();
}
}
}
if (model != null) {
group.getChildren().add(model);
} else {
throw new UnsupportedOperationException("Model returned Null ... ");
}
camera.setTranslateZ(-3d * Math.max(model.getBoundsInParent().getWidth(), model.getBoundsInParent().getHeight()));
if (controlPanel != null && ((ControlPanel) controlPanel).getPanes().filtered(t -> t.getText().contains("lighting")).isEmpty()) {
((ControlPanel) controlPanel).getPanes().add(0, ControlFactory.buildSceneAndLightCategory(
useSkybox,
sceneLight1.lightOnProperty(), sceneLight2.lightOnProperty(),
sceneLight1.colorProperty(), sceneLight2.colorProperty(),
sceneLight1.translateXProperty(), sceneLight2.translateXProperty(),
light1Group.rotateProperty(), light2Group.rotateProperty(),
light1Group.rotationAxisProperty(), light1Group.rotationAxisProperty()
));
exportButton = new Button("Export Mesh");
exportButton.setFocusTraversable(false);
exportButton.visibleProperty().addListener(l -> {
if (exportButton.isVisible()) {
if (exportButton.getParent() != null) {
exportButton.setMinWidth(((VBox) exportButton.getParent()).getPrefWidth());
exportButton.autosize();
}
}
});
//HBox expContainer = new HBox(exportButton);
//expContainer.setPrefSize(USE_COMPUTED_SIZE, USE_PREF_SIZE);
//HBox.setHgrow(exportButton, Priority.ALWAYS);
((VBox) controlPanel).getChildren().add(exportButton);
((ControlPanel) controlPanel).getPanes().get(0).setExpanded(true);
// setup model information
modelInfo.getTimeToBuild().setText(String.valueOf(System.currentTimeMillis() - time) + "ms");
if (model instanceof Text3DMesh) {
modelInfo.getNodeCount().setText(String.valueOf(((Group) model).getChildren().size()));
modelInfo.getPoints().textProperty().bind(((Text3DMesh) model).vertCountBinding());
modelInfo.getFaces().textProperty().bind(((Text3DMesh) model).faceCountBinding());
} else if (model instanceof Group) {
modelInfo.getNodeCount().setText(String.valueOf(((Group) model).getChildren().filtered(t -> t instanceof Shape3D).size()));
modelInfo.getPoints().setText("");
modelInfo.getFaces().setText("");
} else if (model instanceof Shape3D) {
modelInfo.getNodeCount().setText("1");
modelInfo.getPoints().textProperty().bind(((TexturedMesh) model).vertCountBinding());
modelInfo.getFaces().textProperty().bind(((TexturedMesh) model).faceCountBinding());
}
modelInfo.getSampleTitle().setText(getSampleName());
}
}
};
service.setExecutor(serviceExecutor);
time = System.currentTimeMillis();
service.start();
}
@Override
public Node getSample() {
loadSample();
progressBar = new ProgressBar();
progressBar.setPrefSize(mainPane.getPrefWidth() * 0.5, USE_PREF_SIZE);
progressBar.setProgress(-1);
mainPane.getChildren().add(progressBar);
parentPane.parentProperty().addListener(l->{
if(parentPane.getScene() != null){
if(model != null){
attachBinders();
}
}else{
releaseBinders();
}
});
return parentPane;
}
protected BooleanProperty pickingProperty() {
return isPicking;
}
@Override
public Node getControlPanel() {
return buildControlPanel() != null ? buildControlPanel() : null;
}
@Override
public double getControlPanelDividerPosition() {
//SplitPane.setResizableWithParent(controlPanel, Boolean.TRUE);
return -1;
}
public <T> T lookup(Node parent, String id, Class<T> clazz) {
for (Node node : parent.lookupAll(id)) {
if (node.getClass().isAssignableFrom(clazz)) {
return (T) node;
}
}
throw new IllegalArgumentException("Parent " + parent + " doesn't contain node with id " + id);
}
protected Scene getScene() {
return subScene.getScene();
}
/*
From fx83dfeatures.Camera3D
http://hg.openjdk.java.net/openjfx/8u-dev/rt/file/f4e58490d406/apps/toys/FX8-3DFeatures/src/fx83dfeatures/Camera3D.java
*/
/*
* returns 3D direction from the Camera position to the mouse
* in the Scene space
*/
public Vec3d unProjectDirection(double sceneX, double sceneY, double sWidth, double sHeight) {
double tanHFov = Math.tan(Math.toRadians(camera.getFieldOfView()) * 0.5f);
Vec3d vMouse = new Vec3d(2 * sceneX / sWidth - 1, 2 * sceneY / sWidth - sHeight / sWidth, 1);
vMouse.x *= tanHFov;
vMouse.y *= tanHFov;
Vec3d result = localToSceneDirection(vMouse, new Vec3d());
result.normalize();
return result;
}
public Vec3d localToScene(Vec3d pt, Vec3d result) {
Point3D res = camera.localToParentTransformProperty().get().transform(pt.x, pt.y, pt.z);
if (camera.getParent() != null) {
res = camera.getParent().localToSceneTransformProperty().get().transform(res);
}
result.set(res.getX(), res.getY(), res.getZ());
return result;
}
public Vec3d localToSceneDirection(Vec3d dir, Vec3d result) {
localToScene(dir, result);
result.sub(localToScene(new Vec3d(0, 0, 0), new Vec3d()));
return result;
}
}