如何使用JavaFX创建一个3d /表面图? [英] How to create a 3d / surface chart with JavaFX?

查看:350
本文介绍了如何使用JavaFX创建一个3d /表面图?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

问题



我试图用JavaFX创建一个3d图表,但似乎比预期更难。 p>

我目前的做法是创建一个TriangleMesh,但这是相当间接的。所有我想做的是向图表提供列表< Point3D> ,然后图表应呈现为表面。



但是,即使是一个有5个数据点的简单金字塔也是相当复杂的:

  float h = 200; // Height 
float s = 200; // Side

TriangleMesh pyramidMesh = new TriangleMesh();

pyramidMesh.getTexCoords()。addAll(0,0);
pyramidMesh.getPoints()。addAll(
0,0,0,0 - 0 - top
0,h,-s / 2, b -s / 2,h,0,//点2 - 左
s / 2,h,0,//点3 - 返回
0,h,s / 2 //点4 - 右
);

pyramidMesh.getFaces()。addAll(
0,0,2,0,1,0,//左前面
0,0,1,0,3 ,0,//右前脸
0,0,3,0,4,0,//右后脸
0,0,4,0,2,0,//左后脸
4,0,1,0,2,0,//底面背面
4,0,3,0,1,0 //底面正面
);

问题




  • 是否有人知道如何使用JavaFX创建一个3d图表?

  • 是否使用TriangleMesh是正确的方式?

  • 如何将 List< Point3D> 转换为TriangleMesh?



代码

  import javafx.animation.Timeline; 
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.util.Duration;

public class Chart3dSampleApp extends应用程序{

final Group root = new Group();
final group axisGroup = new Group();
final Xform world = new Xform();
final PerspectiveCamera camera = new PerspectiveCamera(true);
final Xform cameraXform = new Xform();
final Xform cameraXform2 = new Xform();
final Xform cameraXform3 = new Xform();
final double cameraDistance = 1450;
final Xform moleculeGroup = new Xform();
私人时间轴时间表;
boolean timelinePlaying = false;
double ONE_FRAME = 1.0 / 24.0;
double DELTA_MULTIPLIER = 200.0;
double CONTROL_MULTIPLIER = 10.1;
double SHIFT_MULTIPLIER = 0.1;
double ALT_MULTIPLIER = 0.5;
double mousePosX;
double mousePosY;
double mouseOldX;
double mouseOldY;
double mouseDeltaX;
double mouseDeltaY;

private void buildScene(){
root.getChildren()。add(world);
}

private void buildCamera(){
root.getChildren()。add(cameraXform);
cameraXform.getChildren()。add(cameraXform2);
cameraXform2.getChildren()。add(cameraXform3);
cameraXform3.getChildren()。add(camera);
cameraXform3.setRotateZ(0);

camera.setNearClip(0.1);
camera.setFarClip(10000.0);
camera.setTranslateZ(-cameraDistance);
cameraXform.ry.setAngle(0);
CameraXform.rx.setAngle(0);
}

private void buildAxes(){
final PhongMaterial redMaterial = new PhongMaterial();
redMaterial.setDiffuseColor(Color.DARKRED);
redMaterial.setSpecularColor(Color.RED);

final Phong材料greenMaterial = new PhongMaterial();
greenMaterial.setDiffuseColor(Color.DARKGREEN);
greenMaterial.setSpecularColor(Color.GREEN);

final PhongMaterial blueMaterial = new PhongMaterial();
blueMaterial.setDiffuseColor(Color.DARKBLUE);
blueMaterial.setSpecularColor(Color.BLUE);

final Box xAxis = new Box(300,1,300);
final box yAxis = new Box(1,300,300);
final Box zAxis = new Box(300,300,1);

yAxis.setTranslateY(-150);
yAxis.setTranslateX(150);
zAxis.setTranslateY(-150);
zAxis.setTranslateZ(150);

xAxis.setMaterial(redMaterial);
yAxis.setMaterial(greenMaterial);
zAxis.setMaterial(blueMaterial);

axisGroup.getChildren()。addAll(xAxis,yAxis,zAxis);
world.getChildren()。addAll(axisGroup);
}

private void buildChart(){

final PhongMaterial whiteMaterial = new PhongMaterial();
whiteMaterial.setDiffuseColor(Color.WHITE);
whiteMaterial.setSpecularColor(Color.LIGHTBLUE);

float h = 200; // Height
float s = 200; // Side

TriangleMesh pyramidMesh = new TriangleMesh();

pyramidMesh.getTexCoords()。addAll(0,0);
pyramidMesh.getPoints()。addAll(
0,0,0,0 - 0 - top
0,h,-s / 2, b -s / 2,h,0,//点2 - 左
s / 2,h,0,//点3 - 返回
0,h,s / 2 //点4 - 右
);

pyramidMesh.getFaces()。addAll(
0,0,2,0,1,0,//左前面
0,0,1,0,3 ,0,//右前脸
0,0,3,0,4,0,//右后脸
0,0,4,0,2,0,//左后脸
4,0,1,0,2,0,//底部后脸
4,0,3,0,1,0 //底部前脸
);


MeshView pyramid = new MeshView(pyramidMesh);
pyramid.setDrawMode(DrawMode.FILL);
pyramid.setMaterial(whiteMaterial);
pyramid.setTranslateY(-h);

world.getChildren()。addAll(pyramid);
}


private void handleMouse(Scene scene,final Node root){
scene.setOnMousePressed(new EventHandler< MouseEvent>(){
@覆盖公共无效句柄(MouseEvent me){
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
}
});
scene.setOnMouseDragged(new EventHandler< MouseEvent>(){
@Override
public void handle(MouseEvent me){
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX =(mousePosX - mouseOldX);
mouseDeltaY =(mousePosY - mouseOldY);

double modifier = 1.0;
double modifierFactor = 0.1;

if(me.isControlDown()){
modifier = 0.1;
}
if(me.isShiftDown()){
modifier = 10.0;
}
if(me.isPrimaryButtonDown()){
cameraXform.ry.setAngle .ry.getAngle() - mouseDeltaX * modifierFactor * modifier * 2.0); // +
cameraXform.rx.setAngle(cameraXform.rx.getAngle()+ mouseDeltaY * modifierFactor * modifier * 2.0); // -
} else if(me.isSecondaryButtonDown()){
double z = camera.getTranslateZ();
double newZ = z + mouseDeltaX * modifierFactor * modifier;
camera.setTranslateZ(newZ);
} else if(me.isMiddleButtonDown()){
cameraXform2.t.setX(cameraXform2.t.getX()+ mouseDeltaX * modifierFactor * modifier * 0.3); // -
cameraXform2.t.setY(cameraXform2.t.getY()+ mouseDeltaY * modifierFactor * modifier * 0.3); // -
}
}
});
}

private void handleKeyboard(Scene scene,final Node root){
final boolean moveCamera = true;
scene.setOnKeyPressed(new EventHandler< KeyEvent>(){
@Override
public void handle(KeyEvent event){
Duration currentTime;
switch(event.getCode ()){
case Z:
if(event.isShiftDown()){
cameraXform.ry.setAngle(0.0);
cameraXform.rx.setAngle
camera.setTranslateZ(-300.0);
}
cameraXform2.t.setX(0.0);
cameraXform2.t.setY(0.0);
break;
case X:
if(event.isControlDown()){
if(axisGroup.isVisible()){
axisGroup.setVisible(false);
} else {
axisGroup.setVisible(true);
}
}
break;
case S:
if(event.isControlDown()){
if(moleculeGroup.isVisible()){
moleculeGroup.setVisible(false);
} else {
moleculeGroup.setVisible(true);
}
}
break;
case SPACE:
if(timelinePlaying){
timeline.pause();
timelinePlaying = false;
} else {
timeline.play();
timelinePlaying = true;
}
break;
case UP:
if(event.isControlDown()&& event.isShiftDown()){
cameraXform2.t.setY(cameraXform2.t.getY() - 10.0 * CONTROL_MULTIPLIER );
} else if(event.isAltDown()&&& event.isShiftDown()){
cameraXform.rx.setAngle(cameraXform.rx.getAngle() - 10.0 * ALT_MULTIPLIER);
} else if(event.isControlDown()){
cameraXform2.t.setY(cameraXform2.t.getY() - 1.0 * CONTROL_MULTIPLIER);
} else if(event.isAltDown()){
cameraXform.rx.setAngle(cameraXform.rx.getAngle() - 2.0 * ALT_MULTIPLIER);
} else if(event.isShiftDown()){
double z = camera.getTranslateZ();
double newZ = z + 5.0 * SHIFT_MULTIPLIER;
camera.setTranslateZ(newZ);
}
break;
case DOWN:
if(event.isControlDown()&& event.isShiftDown()){
cameraXform2.t.setY(cameraXform2.t.getY()+ 10.0 * CONTROL_MULTIPLIER );
} else if(event.isAltDown()&& event.isShiftDown()){
cameraXform.rx.setAngle(cameraXform.rx.getAngle()+ 10.0 * ALT_MULTIPLIER);
} else if(event.isControlDown()){
cameraXform2.t.setY(cameraXform2.t.getY()+ 1.0 * CONTROL_MULTIPLIER);
} else if(event.isAltDown()){
cameraXform.rx.setAngle(cameraXform.rx.getAngle()+ 2.0 * ALT_MULTIPLIER);
} else if(event.isShiftDown()){
double z = camera.getTranslateZ();
double newZ = z - 5.0 * SHIFT_MULTIPLIER;
camera.setTranslateZ(newZ);
}
break;
case RIGHT:
if(event.isControlDown()&& event.isShiftDown()){
cameraXform2.t.setX(cameraXform2.t.getX()+ 10.0 * CONTROL_MULTIPLIER );
} else if(event.isAltDown()&& event.isShiftDown()){
cameraXform.ry.setAngle(cameraXform.ry.getAngle() - 10.0 * ALT_MULTIPLIER);
} else if(event.isControlDown()){
cameraXform2.t.setX(cameraXform2.t.getX()+ 1.0 * CONTROL_MULTIPLIER);
} else if(event.isAltDown()){
cameraXform.ry.setAngle(cameraXform.ry.getAngle() - 2.0 * ALT_MULTIPLIER);
}
break;
case LEFT:
if(event.isControlDown()&& event.isShiftDown()){
cameraXform2.t.setX(cameraXform2.t.getX() - 10.0 * CONTROL_MULTIPLIER );
} else if(event.isAltDown()&&& event.isShiftDown()){
cameraXform.ry.setAngle(cameraXform.ry.getAngle()+ 10.0 * ALT_MULTIPLIER); // -
} else if(event.isControlDown()){
cameraXform2.t.setX(cameraXform2.t.getX() - 1.0 * CONTROL_MULTIPLIER);
} else if(event.isAltDown()){
cameraXform.ry.setAngle(cameraXform.ry.getAngle()+ 2.0 * ALT_MULTIPLIER); // -
}
break;
}
}
});
}

@Override
public void start(Stage primaryStage){
buildScene();
buildCamera();
buildAxes()
buildChart();

场景scene = new Scene(root,1600,900,true);
scene.setFill(Color.GREY);
handleKeyboard(scene,world);
handleMouse(scene,world);

primaryStage.setScene(scene);
primaryStage.show();

scene.setCamera(camera);

}

/ **
*正确部署的JavaFX应用程序中忽略main()方法。
* main()仅用作应用程序不能通过部署工件启动的备份,例如在具有有限FX
*支持的IDE中。 NetBeans忽略main()。
*
* @param args命令行参数
* /
public static void main(String [] args){
System.setProperty(prism.dirtyopts ,false);
launch(args);
}

public static class Xform extends Group {

public enum RotateOrder {
XYZ,XZY,YXZ,YZX,ZXY,ZYX
}

public Translate t = new Translate();
public Translate p = new Translate();
public翻译ip = new Translate();
public Rotate rx = new Rotate();
{rx.setAxis(Rotate.X_AXIS); }
public Rotate ry = new Rotate();
{ry.setAxis(Rotate.Y_AXIS); }
public Rotate rz = new Rotate();
{rz.setAxis(Rotate.Z_AXIS); }
public Sc​​ale s = new Scale();

public Xform(){
super();
getTransforms()。addAll(t,rz,ry,rx,s);
}

public Xform(RotateOrder rotateOrder){
super();
//根据rotateOrder选择旋转顺序
switch(rotateOrder){
case XYZ:
getTransforms()。addAll(t,p,rz,ry,rx , 啜);
break;
case XZY:
getTransforms()。addAll(t,p,ry,rz,rx,s,ip);
break;
case YXZ:
getTransforms()。addAll(t,p,rz,rx,ry,s,ip);
break;
case YZX:
getTransforms()。addAll(t,p,rx,rz,ry,s,ip); // For Camera
break;
case ZXY:
getTransforms()。addAll(t,p,ry,rx,rz,s,ip);
break;
case ZYX:
getTransforms()。addAll(t,p,rx,ry,rz,s,ip);
break;
}
}

public void setTranslate(double x,double y,double z){
t.setX(x);
t.setY(y);
t.setZ(z);
}

public void setTranslate(double x,double y){
t.setX(x);
t.setY(y);
}

//不能覆盖这些方法,因为它们是final:
// public void setTranslateX(double x){t.setX(x); }
// public void setTranslateY(double y){t.setY(y); }
// public void setTranslateZ(double z){t.setZ(z); }
//使用这些方法:
public void setTx(double x){t.setX(x); }
public void setTy(double y){t.setY(y); }
public void setTz(double z){t.setZ(z); }

public void setRotate(double x,double y,double z){
rx.setAngle(x);
ry.setAngle(y);
rz.setAngle(z);
}

public void setRotateX(double x){rx.setAngle(x); }
public void setRotateY(double y){ry.setAngle(y); }
public void setRotateZ(double z){rz.setAngle(z); }
public void setRx(double x){rx.setAngle(x); }
public void setRy(double y){ry.setAngle(y); }
public void setRz(double z){rz.setAngle(z); }

public void setScale(double scaleFactor){
s.setX(scaleFactor);
s.setY(scaleFactor);
s.setZ(scaleFactor);
}

public void setScale(double x,double y,double z){
s.setX(x);
s.setY(y);
s.setZ(z);
}

//不能覆盖这些方法,因为它们是final:
// public void setScaleX(double x){s.setX(x); }
// public void setScaleY(double y){s.setY(y); }
// public void setScaleZ(double z){s.setZ(z); }
//改用这些方法:
public void setSx(double x){s.setX(x); }
public void setSy(double y){s.setY(y); }
public void setSz(double z){s.setZ(z); }

public void setPivot(double x,double y,double z){
p.setX(x);
p.setY(y);
p.setZ(z);
ip.setX(-x);
ip.setY(-y);
ip.setZ(-z);
}

public void reset(){
t.setX(0.0);
t.setY(0.0);
t.setZ(0.0);
rx.setAngle(0.0);
ry.setAngle(0.0); $ b¥b rz.setAngle(0.0);
s.setX(1.0);
s.setY(1.0);
s.setZ(1.0);
p.setX(0.0);
p.setY(0.0); $ b $ p p.setZ(0.0);
ip.setX(0.0);
ip.setY(0.0);
ip.setZ(0.0);
}

public void resetTSP(){
t.setX(0.0);
t.setY(0.0);
t.setZ(0.0);
s.setX(1.0);
s.setY(1.0);
s.setZ(1.0);
p.setX(0.0);
p.setY(0.0);
p.setZ(0.0);
ip.setX(0.0);
ip.setY(0.0);
ip.setZ(0.0);
}
}

}

应该是e。 G。像这样:



/ p>

或:





最后应该可以显示e。 G。 perlin噪声的结果,但是不是perlin噪声值是颜色

解决方案

div>

感谢 NwDx的回答我设法创造有用的东西。这不是一个完整的图表应用程序,我希望有更多的知道如何可以提供一个更好的答案,但我会发布结果,但是。



你可以使用鼠标拖动旋转和鼠标滚轮进行缩放。该示例显示了一个perlin噪声图,其中使用了在网格上使用的漫射映射。



核心实际上并不是很多代码。它只是把一个2d数组变成网格:

  // perlin noise 
float [] [] noiseArray = createNoise(size);

// mesh
TriangleMesh mesh = new TriangleMesh();

//为x / z创建点
float amplification = 100; // amplifying noise

for(int x = 0; x for(int z = 0; z mesh.getPoints()。addAll(x,noiseArray [x] [z] * amplification,z);
}
}

// texture
int length = size;
float total = length;

for(float x = 0; x for(float y = 0; y
float x0 = x / total;
float y0 = y / total;
float x1 =(x + 1)/ total;
float y1 =(y + 1)/ total;

mesh.getTexCoords()。addAll(//
x0,y0,// 0,top-left
x0,y1,// 1,bottom-left
x1,y1,// 2,top-right
x1,y1 // 3,bottom-right
);


}
}

// faces
for(int x = 0; x< length - 1; x ++){
for(int z = 0; z
int tl = x * length + z; // top-left
int bl = x * length + z + 1; // bottom-left
int tr =(x + 1)* length + z; // top-right
int br =(x + 1)* length + z + 1; //右下

int offset =(x *(长度-1)+ z)* 8/2; // div 2因为我们在列表中有u AND v

// working
mesh.getFaces()。addAll(bl,offset + 1,tl,offset + 0,tr, offset + 2);
mesh.getFaces()。addAll(tr,offset + 2,br,offset + 3,bl,offset + 1);

}
}

如果有人有更好的算法,请分享。



完整的工作示例:

 

code> import java.util.ArrayList;
import java.util.List;

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Point3D;
import javafx.scene.DepthTest;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.canvas.Canvas;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Paint;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.paint.Stop;
import javafx.scene.shape.CullFace;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.Line;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;

public class Chart3dDemo extends应用程序{

//图形大小
int size = 400;

//鼠标交互的变量
private double mousePosX,mousePosY;
private double mouseOldX,mouseOldY;
private final Rotate rotateX = new Rotate(20,Rotate.X_AXIS);
private final Rotate rotateY = new Rotate(-45,Rotate.Y_AXIS);

@Override
public void start(Stage primaryStage){

//创建轴墙
组立方体= createCube(size);

//初始多维数据集旋转
cube.getTransforms()。addAll(rotateX,rotateY);

//将对象添加到场景
StackPane root = new StackPane();
root.getChildren()。add(cube);

// perlin noise
float [] [] noiseArray = createNoise(size);

// mesh
TriangleMesh mesh = new TriangleMesh();

//为x / z创建点
float amplification = 100; // amplifying noise

for(int x = 0; x for(int z = 0; z mesh.getPoints()。addAll(x,noiseArray [x] [z] * amplification,z);
}
}

// texture
int length = size;
float total = length;

for(float x = 0; x for(float y = 0; y
float x0 = x / total;
float y0 = y / total;
float x1 =(x + 1)/ total;
float y1 =(y + 1)/ total;

mesh.getTexCoords()。addAll(//
x0,y0,// 0,top-left
x0,y1,// 1,bottom-left
x1,y1,// 2,top-right
x1,y1 // 3,bottom-right
);


}
}

// faces
for(int x = 0; x< length - 1; x ++){
for(int z = 0; z
int tl = x * length + z; // top-left
int bl = x * length + z + 1; // bottom-left
int tr =(x + 1)* length + z; // top-right
int br =(x + 1)* length + z + 1; //右下

int offset =(x *(长度-1)+ z)* 8/2; // div 2因为我们在列表中有u AND v

// working
mesh.getFaces()。addAll(bl,offset + 1,tl,offset + 0,tr, offset + 2);
mesh.getFaces()。addAll(tr,offset + 2,br,offset + 3,bl,offset + 1);

}
}


// material
image diffuseMap = createImage(size,noiseArray);

PhongMaterial material = new PhongMaterial();
material.setDiffuseMap(diffuseMap);
material.setSpecularColor(Color.WHITE);

//网格视图
MeshView meshView = new MeshView(mesh);
meshView.setTranslateX(-0.5 * size);
meshView.setTranslateZ(-0.5 * size);
meshView.setMaterial(material);
meshView.setCullFace(CullFace.NONE);
meshView.setDrawMode(DrawMode.FILL);
meshView.setDepthTest(DepthTest.ENABLE);

cube.getChildren()。addAll(meshView);

//测试/调试东西:在图表上显示漫射图
ImageView iv = new ImageView(diffuseMap);
iv.setTranslateX(-0.5 * size);
iv.setTranslateY(-0.10 * size);
iv.setRotate(90);
iv.setRotationAxis(new Point3D(1,0,0));
cube.getChildren()。add(iv);

// scene
场景scene = new Scene(root,1600,900,true,SceneAntialiasing.BALANCED);
scene.setCamera(new PerspectiveCamera());

scene.setOnMousePressed(me - > {
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
});
scene.setOnMouseDragged(me - > {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
rotateX.setAngle(rotateX.getAngle - (mousePosY - mouseOldY));
rotateY.setAngle(rotateY.getAngle()+(mousePosX - mouseOldX));
mouseOldX = mousePosX;
mouseOldY = mousePosY;

});

makeZoomable(root);

primaryStage.setResizable(false);
primaryStage.setScene(scene);
primaryStage.show();


}

/ **
*为uv映射创建纹理
* @param size
* @param noise
* @return
* /
public Image createImage(double size,float [] [] noise){

int width =(int)size;
int height =(int)size;

WritableImage wr = new WritableImage(width,height);
PixelWriter pw = wr.getPixelWriter();
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {

float value = noise[x][y];

double gray = normalizeValue(value, -.5, .5, 0., 1.);

gray = clamp(gray, 0, 1);

Color color = Color.RED.interpolate(Color.YELLOW, gray);

pw.setColor(x, y, color);

}
}

return wr;

}

/**
* Axis wall
*/
public static class Axis extends Pane {

Rectangle wall;

public Axis(double size) {

// wall
// first the wall, then the lines => overlapping of lines over walls
// works
wall = new Rectangle(size, size);
getChildren().add(wall);

// grid
double zTranslate = 0;
double lineWidth = 1.0;
Color gridColor = Color.WHITE;

for (int y = 0; y <= size; y += size / 10) {

Line line = new Line(0, 0, size, 0);
line.setStroke(gridColor);
line.setFill(gridColor);
line.setTranslateY(y);
line.setTranslateZ(zTranslate);
line.setStrokeWidth(lineWidth);

getChildren().addAll(line);

}

for (int x = 0; x <= size; x += size / 10) {

Line line = new Line(0, 0, 0, size);
line.setStroke(gridColor);
line.setFill(gridColor);
line.setTranslateX(x);
line.setTranslateZ(zTranslate);
line.setStrokeWidth(lineWidth);

getChildren().addAll(line);

}

// labels
// TODO: for some reason the text makes the wall have an offset
// for( int y=0; y <= size; y+=size/10) {
//
// Text text = new Text( \"\"+y);
// text.setTranslateX(size + 10);
//
// text.setTranslateY(y);
// text.setTranslateZ(zTranslate);
//
// getChildren().addAll(text);
//
// }

}

public void setFill(Paint paint) {
wall.setFill(paint);
}

}

public void makeZoomable(StackPane control) {

final double MAX_SCALE = 20.0;
final double MIN_SCALE = 0.1;

control.addEventFilter(ScrollEvent.ANY, new EventHandler<ScrollEvent>() {

@Override
public void handle(ScrollEvent event) {

double delta = 1.2;

double scale = control.getScaleX();

if (event.getDeltaY() < 0) {
scale /= delta;
} else {
scale *= delta;
}

scale = clamp(scale, MIN_SCALE, MAX_SCALE);

control.setScaleX(scale);
control.setScaleY(scale);

event.consume();

}

});

}

/**
* Create axis walls
* @param size
* @return
*/
private Group createCube(int size) {

Group cube = new Group();

// size of the cube
Color color = Color.DARKCYAN;

List<Axis> cubeFaces = new ArrayList<>();
Axis r;

// back face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.5 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(-0.5 * size);
r.setTranslateZ(0.5 * size);

cubeFaces.add(r);

// bottom face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.4 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(0);
r.setRotationAxis(Rotate.X_AXIS);
r.setRotate(90);

cubeFaces.add(r);

// right face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.3 * 1), 1.0));
r.setTranslateX(-1 * size);
r.setTranslateY(-0.5 * size);
r.setRotationAxis(Rotate.Y_AXIS);
r.setRotate(90);

// cubeFaces.add( r);

// left face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.2 * 1), 1.0));
r.setTranslateX(0);
r.setTranslateY(-0.5 * size);
r.setRotationAxis(Rotate.Y_AXIS);
r.setRotate(90);

cubeFaces.add(r);

// top face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.1 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(-1 * size);
r.setRotationAxis(Rotate.X_AXIS);
r.setRotate(90);

// cubeFaces.add( r);

// front face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.1 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(-0.5 * size);
r.setTranslateZ(-0.5 * size);

// cubeFaces.add( r);

cube.getChildren().addAll(cubeFaces);

return cube;
}

/**
* Create an array of the given size with values of perlin noise
* @param size
* @return
*/
private float[][] createNoise( int size) {
float[][] noiseArray = new float[(int) size][(int) size];

for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {

double frequency = 10.0 / (double) size;

double noise = ImprovedNoise.noise(x * frequency, y * frequency, 0);

noiseArray[x][y] = (float) noise;
}
}

return noiseArray;

}

public static double normalizeValue(double value, double min, double max, double newMin, double newMax) {

return (value - min) * (newMax - newMin) / (max - min) + newMin;

}

public static double clamp(double value, double min, double max) {

if (Double.compare(value, min) < 0)
return min;

if (Double.compare(value, max) > 0)
return max;

return value;
}


/**
* Perlin noise generator
*
* // JAVA REFERENCE IMPLEMENTATION OF IMPROVED NOISE - COPYRIGHT 2002 KEN PERLIN.
* // http://mrl.nyu.edu/~perlin/paper445.pdf
* // http://mrl.nyu.edu/~perlin/noise/
*/
public final static class ImprovedNoise {
static public double noise(double x, double y, double z) {
int X = (int)Math.floor(x) & 255, // FIND UNIT CUBE THAT
Y = (int)Math.floor(y) & 255, // CONTAINS POINT.
Z = (int)Math.floor(z) & 255;
x -= Math.floor(x); // FIND RELATIVE X,Y,Z
y -= Math.floor(y); // OF POINT IN CUBE.
z -= Math.floor(z);
double u = fade(x), // COMPUTE FADE CURVES
v = fade(y), // FOR EACH OF X,Y,Z.
w = fade(z);
int A = p[X ]+Y, AA = p[A]+Z, AB = p[A+1]+Z, // HASH COORDINATES OF
B = p[X+1]+Y, BA = p[B]+Z, BB = p[B+1]+Z; // THE 8 CUBE CORNERS,

return lerp(w, lerp(v, lerp(u, grad(p[AA ], x , y , z ), // AND ADD
grad(p[BA ], x-1, y , z )), // BLENDED
lerp(u, grad(p[AB ], x , y-1, z ), // RESULTS
grad(p[BB ], x-1, y-1, z ))),// FROM 8
lerp(v, lerp(u, grad(p[AA+1], x , y , z-1 ), // CORNERS
grad(p[BA+1], x-1, y , z-1 )), // OF CUBE
lerp(u, grad(p[AB+1], x , y-1, z-1 ),
grad(p[BB+1], x-1, y-1, z-1 ))));
}
static double fade(double t) { return t * t * t * (t * (t * 6 - 15) + 10); }
static double lerp(double t, double a, double b) { return a + t * (b - a); }
static double grad(int hash, double x, double y, double z) {
int h = hash & 15; // CONVERT LO 4 BITS OF HASH CODE
double u = h<8 ? x : y, // INTO 12 GRADIENT DIRECTIONS.
v = h<4 ? y : h==12||h==14 ? x : z;
return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v);
}
static final int p[] = new int[512], permutation[] = { 151,160,137,91,90,15,
131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180
};
static { for (int i=0; i < 256 ; i++) p[256+i] = p[i] = permutation[i]; }
}

public static void main(String[] args) {
launch(args);
}


}

Screenshot:




Problem

I tried to create a 3d chart with JavaFX but it seems to be more difficult than what one would expect.

My current way of doing it would be to create a TriangleMesh, but that's rather circumstantial. All I'd like to do is to provide a List<Point3D> to the chart and then the chart should be rendered as a surface.

However, even a simple pyramid with 5 data points turns out to be rather complicated:

    float h = 200;                    // Height
    float s = 200;                    // Side

    TriangleMesh pyramidMesh = new TriangleMesh();

    pyramidMesh.getTexCoords().addAll(0,0);
    pyramidMesh.getPoints().addAll(
            0,    0,    0,            // Point 0 - Top
            0,    h,    -s/2,         // Point 1 - Front
            -s/2, h,    0,            // Point 2 - Left
            s/2,  h,    0,            // Point 3 - Back
            0,    h,    s/2           // Point 4 - Right
        );

    pyramidMesh.getFaces().addAll(
      0,0,  2,0,  1,0,          // Front left face
      0,0,  1,0,  3,0,          // Front right face
      0,0,  3,0,  4,0,          // Back right face
      0,0,  4,0,  2,0,          // Back left face
      4,0,  1,0,  2,0,          // Bottom rear face
      4,0,  3,0,  1,0           // Bottom front face
  ); 

Questions

  • Does anyone know how to create a 3d chart with JavaFX?
  • Is a TriangleMesh the proper way to do it?
  • How do you convert a List<Point3D> into a TriangleMesh?

Code

import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
import javafx.util.Duration;

public class Chart3dSampleApp extends Application {

    final Group root = new Group();
    final Group axisGroup = new Group();
    final Xform world = new Xform();
    final PerspectiveCamera camera = new PerspectiveCamera(true);
    final Xform cameraXform = new Xform();
    final Xform cameraXform2 = new Xform();
    final Xform cameraXform3 = new Xform();
    final double cameraDistance = 1450;
    final Xform moleculeGroup = new Xform();
    private Timeline timeline;
    boolean timelinePlaying = false;
    double ONE_FRAME = 1.0 / 24.0;
    double DELTA_MULTIPLIER = 200.0;
    double CONTROL_MULTIPLIER = 10.1;
    double SHIFT_MULTIPLIER = 0.1;
    double ALT_MULTIPLIER = 0.5;
    double mousePosX;
    double mousePosY;
    double mouseOldX;
    double mouseOldY;
    double mouseDeltaX;
    double mouseDeltaY;

    private void buildScene() {
        root.getChildren().add(world);
    }

    private void buildCamera() {
        root.getChildren().add(cameraXform);
        cameraXform.getChildren().add(cameraXform2);
        cameraXform2.getChildren().add(cameraXform3);
        cameraXform3.getChildren().add(camera);
        cameraXform3.setRotateZ(0);

        camera.setNearClip(0.1);
        camera.setFarClip(10000.0);
        camera.setTranslateZ(-cameraDistance);
        cameraXform.ry.setAngle(0);
        cameraXform.rx.setAngle(0);
    }

    private void buildAxes() {
        final PhongMaterial redMaterial = new PhongMaterial();
        redMaterial.setDiffuseColor(Color.DARKRED);
        redMaterial.setSpecularColor(Color.RED);

        final PhongMaterial greenMaterial = new PhongMaterial();
        greenMaterial.setDiffuseColor(Color.DARKGREEN);
        greenMaterial.setSpecularColor(Color.GREEN);

        final PhongMaterial blueMaterial = new PhongMaterial();
        blueMaterial.setDiffuseColor(Color.DARKBLUE);
        blueMaterial.setSpecularColor(Color.BLUE);

        final Box xAxis = new Box(300, 1, 300);
        final Box yAxis = new Box(1, 300, 300);
        final Box zAxis = new Box(300, 300, 1);

        yAxis.setTranslateY(-150);
        yAxis.setTranslateX(150);
        zAxis.setTranslateY(-150);
        zAxis.setTranslateZ(150);

        xAxis.setMaterial(redMaterial);
        yAxis.setMaterial(greenMaterial);
        zAxis.setMaterial(blueMaterial);

        axisGroup.getChildren().addAll(xAxis, yAxis, zAxis);
        world.getChildren().addAll(axisGroup);
    }

    private void buildChart() {

      final PhongMaterial whiteMaterial = new PhongMaterial();
      whiteMaterial.setDiffuseColor(Color.WHITE);
      whiteMaterial.setSpecularColor(Color.LIGHTBLUE);

        float h = 200;                    // Height
        float s = 200;                    // Side

        TriangleMesh pyramidMesh = new TriangleMesh();

        pyramidMesh.getTexCoords().addAll(0,0);
        pyramidMesh.getPoints().addAll(
                0,    0,    0,            // Point 0 - Top
                0,    h,    -s/2,         // Point 1 - Front
                -s/2, h,    0,            // Point 2 - Left
                s/2,  h,    0,            // Point 3 - Back
                0,    h,    s/2           // Point 4 - Right
            );

        pyramidMesh.getFaces().addAll(
          0,0,  2,0,  1,0,          // Front left face
          0,0,  1,0,  3,0,          // Front right face
          0,0,  3,0,  4,0,          // Back right face
          0,0,  4,0,  2,0,          // Back left face
          4,0,  1,0,  2,0,          // Bottom rear face
          4,0,  3,0,  1,0           // Bottom front face
      ); 


        MeshView pyramid = new MeshView(pyramidMesh);
        pyramid.setDrawMode(DrawMode.FILL);
        pyramid.setMaterial(whiteMaterial);
        pyramid.setTranslateY(-h);

        world.getChildren().addAll(pyramid);
    }


    private void handleMouse(Scene scene, final Node root) {
        scene.setOnMousePressed(new EventHandler<MouseEvent>() {
            @Override public void handle(MouseEvent me) {
                mousePosX = me.getSceneX();
                mousePosY = me.getSceneY();
                mouseOldX = me.getSceneX();
                mouseOldY = me.getSceneY();
            }
        });
        scene.setOnMouseDragged(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent me) {
                mouseOldX = mousePosX;
                mouseOldY = mousePosY;
                mousePosX = me.getSceneX();
                mousePosY = me.getSceneY();
                mouseDeltaX = (mousePosX - mouseOldX);
                mouseDeltaY = (mousePosY - mouseOldY);

                double modifier = 1.0;
                double modifierFactor = 0.1;

                if (me.isControlDown()) {
                    modifier = 0.1;
                }
                if (me.isShiftDown()) {
                    modifier = 10.0;
                }
                if (me.isPrimaryButtonDown()) {
                    cameraXform.ry.setAngle(cameraXform.ry.getAngle() - mouseDeltaX * modifierFactor * modifier * 2.0);  // +
                    cameraXform.rx.setAngle(cameraXform.rx.getAngle() + mouseDeltaY * modifierFactor * modifier * 2.0);  // -
                } else if (me.isSecondaryButtonDown()) {
                    double z = camera.getTranslateZ();
                    double newZ = z + mouseDeltaX * modifierFactor * modifier;
                    camera.setTranslateZ(newZ);
                } else if (me.isMiddleButtonDown()) {
                    cameraXform2.t.setX(cameraXform2.t.getX() + mouseDeltaX * modifierFactor * modifier * 0.3);  // -
                    cameraXform2.t.setY(cameraXform2.t.getY() + mouseDeltaY * modifierFactor * modifier * 0.3);  // -
                }
            }
        });
    }

    private void handleKeyboard(Scene scene, final Node root) {
        final boolean moveCamera = true;
        scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event) {
                Duration currentTime;
                switch (event.getCode()) {
                    case Z:
                        if (event.isShiftDown()) {
                            cameraXform.ry.setAngle(0.0);
                            cameraXform.rx.setAngle(0.0);
                            camera.setTranslateZ(-300.0);
                        }
                        cameraXform2.t.setX(0.0);
                        cameraXform2.t.setY(0.0);
                        break;
                    case X:
                        if (event.isControlDown()) {
                            if (axisGroup.isVisible()) {
                                axisGroup.setVisible(false);
                            } else {
                                axisGroup.setVisible(true);
                            }
                        }
                        break;
                    case S:
                        if (event.isControlDown()) {
                            if (moleculeGroup.isVisible()) {
                                moleculeGroup.setVisible(false);
                            } else {
                                moleculeGroup.setVisible(true);
                            }
                        }
                        break;
                    case SPACE:
                        if (timelinePlaying) {
                            timeline.pause();
                            timelinePlaying = false;
                        } else {
                            timeline.play();
                            timelinePlaying = true;
                        }
                        break;
                    case UP:
                        if (event.isControlDown() && event.isShiftDown()) {
                            cameraXform2.t.setY(cameraXform2.t.getY() - 10.0 * CONTROL_MULTIPLIER);
                        } else if (event.isAltDown() && event.isShiftDown()) {
                            cameraXform.rx.setAngle(cameraXform.rx.getAngle() - 10.0 * ALT_MULTIPLIER);
                        } else if (event.isControlDown()) {
                            cameraXform2.t.setY(cameraXform2.t.getY() - 1.0 * CONTROL_MULTIPLIER);
                        } else if (event.isAltDown()) {
                            cameraXform.rx.setAngle(cameraXform.rx.getAngle() - 2.0 * ALT_MULTIPLIER);
                        } else if (event.isShiftDown()) {
                            double z = camera.getTranslateZ();
                            double newZ = z + 5.0 * SHIFT_MULTIPLIER;
                            camera.setTranslateZ(newZ);
                        }
                        break;
                    case DOWN:
                        if (event.isControlDown() && event.isShiftDown()) {
                            cameraXform2.t.setY(cameraXform2.t.getY() + 10.0 * CONTROL_MULTIPLIER);
                        } else if (event.isAltDown() && event.isShiftDown()) {
                            cameraXform.rx.setAngle(cameraXform.rx.getAngle() + 10.0 * ALT_MULTIPLIER);
                        } else if (event.isControlDown()) {
                            cameraXform2.t.setY(cameraXform2.t.getY() + 1.0 * CONTROL_MULTIPLIER);
                        } else if (event.isAltDown()) {
                            cameraXform.rx.setAngle(cameraXform.rx.getAngle() + 2.0 * ALT_MULTIPLIER);
                        } else if (event.isShiftDown()) {
                            double z = camera.getTranslateZ();
                            double newZ = z - 5.0 * SHIFT_MULTIPLIER;
                            camera.setTranslateZ(newZ);
                        }
                        break;
                    case RIGHT:
                        if (event.isControlDown() && event.isShiftDown()) {
                            cameraXform2.t.setX(cameraXform2.t.getX() + 10.0 * CONTROL_MULTIPLIER);
                        } else if (event.isAltDown() && event.isShiftDown()) {
                            cameraXform.ry.setAngle(cameraXform.ry.getAngle() - 10.0 * ALT_MULTIPLIER);
                        } else if (event.isControlDown()) {
                            cameraXform2.t.setX(cameraXform2.t.getX() + 1.0 * CONTROL_MULTIPLIER);
                        } else if (event.isAltDown()) {
                            cameraXform.ry.setAngle(cameraXform.ry.getAngle() - 2.0 * ALT_MULTIPLIER);
                        }
                        break;
                    case LEFT:
                        if (event.isControlDown() && event.isShiftDown()) {
                            cameraXform2.t.setX(cameraXform2.t.getX() - 10.0 * CONTROL_MULTIPLIER);
                        } else if (event.isAltDown() && event.isShiftDown()) {
                            cameraXform.ry.setAngle(cameraXform.ry.getAngle() + 10.0 * ALT_MULTIPLIER);  // -
                        } else if (event.isControlDown()) {
                            cameraXform2.t.setX(cameraXform2.t.getX() - 1.0 * CONTROL_MULTIPLIER);
                        } else if (event.isAltDown()) {
                            cameraXform.ry.setAngle(cameraXform.ry.getAngle() + 2.0 * ALT_MULTIPLIER);  // -
                        }
                        break;
                }
            }
        });
    }

        @Override
    public void start(Stage primaryStage) {
        buildScene();
        buildCamera();
        buildAxes();
        buildChart();

        Scene scene = new Scene(root, 1600, 900, true);
        scene.setFill(Color.GREY);
        handleKeyboard(scene, world);
        handleMouse(scene, world);

        primaryStage.setScene(scene);
        primaryStage.show();

        scene.setCamera(camera);

    }

    /**
     * The main() method is ignored in correctly deployed JavaFX application.
     * main() serves only as fallback in case the application can not be
     * launched through deployment artifacts, e.g., in IDEs with limited FX
     * support. NetBeans ignores main().
     *
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        System.setProperty("prism.dirtyopts", "false");
        launch(args);
    }

    public static class Xform extends Group {

      public enum RotateOrder {
          XYZ, XZY, YXZ, YZX, ZXY, ZYX
      }

      public Translate t  = new Translate(); 
      public Translate p  = new Translate(); 
      public Translate ip = new Translate(); 
      public Rotate rx = new Rotate();
      { rx.setAxis(Rotate.X_AXIS); }
      public Rotate ry = new Rotate();
      { ry.setAxis(Rotate.Y_AXIS); }
      public Rotate rz = new Rotate();
      { rz.setAxis(Rotate.Z_AXIS); }
      public Scale s = new Scale();

      public Xform() { 
          super(); 
          getTransforms().addAll(t, rz, ry, rx, s); 
      }

      public Xform(RotateOrder rotateOrder) { 
          super(); 
          // choose the order of rotations based on the rotateOrder
          switch (rotateOrder) {
          case XYZ:
              getTransforms().addAll(t, p, rz, ry, rx, s, ip); 
              break;
          case XZY:
              getTransforms().addAll(t, p, ry, rz, rx, s, ip); 
              break;
          case YXZ:
              getTransforms().addAll(t, p, rz, rx, ry, s, ip); 
              break;
          case YZX:
              getTransforms().addAll(t, p, rx, rz, ry, s, ip);  // For Camera
              break;
          case ZXY:
              getTransforms().addAll(t, p, ry, rx, rz, s, ip); 
              break;
          case ZYX:
              getTransforms().addAll(t, p, rx, ry, rz, s, ip); 
              break;
          }
      }

      public void setTranslate(double x, double y, double z) {
          t.setX(x);
          t.setY(y);
          t.setZ(z);
      }

      public void setTranslate(double x, double y) {
          t.setX(x);
          t.setY(y);
      }

      // Cannot override these methods as they are final:
      // public void setTranslateX(double x) { t.setX(x); }
      // public void setTranslateY(double y) { t.setY(y); }
      // public void setTranslateZ(double z) { t.setZ(z); }
      // Use these methods instead:
      public void setTx(double x) { t.setX(x); }
      public void setTy(double y) { t.setY(y); }
      public void setTz(double z) { t.setZ(z); }

      public void setRotate(double x, double y, double z) {
          rx.setAngle(x);
          ry.setAngle(y);
          rz.setAngle(z);
      }

      public void setRotateX(double x) { rx.setAngle(x); }
      public void setRotateY(double y) { ry.setAngle(y); }
      public void setRotateZ(double z) { rz.setAngle(z); }
      public void setRx(double x) { rx.setAngle(x); }
      public void setRy(double y) { ry.setAngle(y); }
      public void setRz(double z) { rz.setAngle(z); }

      public void setScale(double scaleFactor) {
          s.setX(scaleFactor);
          s.setY(scaleFactor);
          s.setZ(scaleFactor);
      }

      public void setScale(double x, double y, double z) {
          s.setX(x);
          s.setY(y);
          s.setZ(z);
      }

      // Cannot override these methods as they are final:
      // public void setScaleX(double x) { s.setX(x); }
      // public void setScaleY(double y) { s.setY(y); }
      // public void setScaleZ(double z) { s.setZ(z); }
      // Use these methods instead:
      public void setSx(double x) { s.setX(x); }
      public void setSy(double y) { s.setY(y); }
      public void setSz(double z) { s.setZ(z); }

      public void setPivot(double x, double y, double z) {
          p.setX(x);
          p.setY(y);
          p.setZ(z);
          ip.setX(-x);
          ip.setY(-y);
          ip.setZ(-z);
      }

      public void reset() {
          t.setX(0.0);
          t.setY(0.0);
          t.setZ(0.0);
          rx.setAngle(0.0);
          ry.setAngle(0.0);
          rz.setAngle(0.0);
          s.setX(1.0);
          s.setY(1.0);
          s.setZ(1.0);
          p.setX(0.0);
          p.setY(0.0);
          p.setZ(0.0);
          ip.setX(0.0);
          ip.setY(0.0);
          ip.setZ(0.0);
      }

      public void resetTSP() {
          t.setX(0.0);
          t.setY(0.0);
          t.setZ(0.0);
          s.setX(1.0);
          s.setY(1.0);
          s.setZ(1.0);
          p.setX(0.0);
          p.setY(0.0);
          p.setZ(0.0);
          ip.setX(0.0);
          ip.setY(0.0);
          ip.setZ(0.0);
      }
  }

}

The chart should be e. g. something like this:

or this:

In the end it should be possible to display e. g. the result of perlin noise, but instead of the perlin noise value being a color value, it's a height value.

Thank you very much for the help!

解决方案

Thanks to NwDx's answer I managed to create something useful. It's not a complete chart application and I hope someone with more know how can provide a better answer, but I'll post the result nontheless.

You can use mouse dragging for rotation and mouse wheel for zooming. The example shows a perlin noise graph with a diffuse map that is used on the mesh.

The core isn't actually much code. It's just about turning a 2d-array into a mesh:

    // perlin noise
    float[][] noiseArray = createNoise( size);

    // mesh
    TriangleMesh mesh = new TriangleMesh();

    // create points for x/z
    float amplification = 100; // amplification of noise

    for (int x = 0; x < size; x++) {
        for (int z = 0; z < size; z++) {
            mesh.getPoints().addAll(x, noiseArray[x][z] * amplification, z);
        }
    }

    // texture
    int length = size;
    float total = length;

    for (float x = 0; x < length - 1; x++) {
        for (float y = 0; y < length - 1; y++) {

            float x0 = x / total;
            float y0 = y / total;
            float x1 = (x + 1) / total;
            float y1 = (y + 1) / total;

            mesh.getTexCoords().addAll( //
                    x0, y0, // 0, top-left
                    x0, y1, // 1, bottom-left
                    x1, y1, // 2, top-right
                    x1, y1 // 3, bottom-right
            );


        }
    }

    // faces
    for (int x = 0; x < length - 1; x++) {
        for (int z = 0; z < length - 1; z++) {

            int tl = x * length + z; // top-left
            int bl = x * length + z + 1; // bottom-left
            int tr = (x + 1) * length + z; // top-right
            int br = (x + 1) * length + z + 1; // bottom-right

            int offset = (x * (length - 1) + z ) * 8 / 2; // div 2 because we have u AND v in the list

            // working
            mesh.getFaces().addAll(bl, offset + 1, tl, offset + 0, tr, offset + 2);
            mesh.getFaces().addAll(tr, offset + 2, br, offset + 3, bl, offset + 1);

        }
    }

If someone has a better algorithm, please do share it. I don't mind if you reuse the code.

The full working example:

import java.util.ArrayList;
import java.util.List;

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Point3D;
import javafx.scene.DepthTest;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.canvas.Canvas;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Paint;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.paint.Stop;
import javafx.scene.shape.CullFace;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.Line;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;

public class Chart3dDemo extends Application {

    // size of graph
    int size = 400;

    // variables for mouse interaction
    private double mousePosX, mousePosY;
    private double mouseOldX, mouseOldY;
    private final Rotate rotateX = new Rotate(20, Rotate.X_AXIS);
    private final Rotate rotateY = new Rotate(-45, Rotate.Y_AXIS);

    @Override
    public void start(Stage primaryStage) {

        // create axis walls
        Group cube = createCube(size);

        // initial cube rotation
        cube.getTransforms().addAll(rotateX, rotateY);

        // add objects to scene
        StackPane root = new StackPane();
        root.getChildren().add(cube);

        // perlin noise
        float[][] noiseArray = createNoise( size);

        // mesh
        TriangleMesh mesh = new TriangleMesh();

        // create points for x/z
        float amplification = 100; // amplification of noise

        for (int x = 0; x < size; x++) {
            for (int z = 0; z < size; z++) {
                mesh.getPoints().addAll(x, noiseArray[x][z] * amplification, z);
            }
        }

        // texture
        int length = size;
        float total = length;

        for (float x = 0; x < length - 1; x++) {
            for (float y = 0; y < length - 1; y++) {

                float x0 = x / total;
                float y0 = y / total;
                float x1 = (x + 1) / total;
                float y1 = (y + 1) / total;

                mesh.getTexCoords().addAll( //
                        x0, y0, // 0, top-left
                        x0, y1, // 1, bottom-left
                        x1, y1, // 2, top-right
                        x1, y1 // 3, bottom-right
                );


            }
        }

        // faces
        for (int x = 0; x < length - 1; x++) {
            for (int z = 0; z < length - 1; z++) {

                int tl = x * length + z; // top-left
                int bl = x * length + z + 1; // bottom-left
                int tr = (x + 1) * length + z; // top-right
                int br = (x + 1) * length + z + 1; // bottom-right

                int offset = (x * (length - 1) + z ) * 8 / 2; // div 2 because we have u AND v in the list

                // working
                mesh.getFaces().addAll(bl, offset + 1, tl, offset + 0, tr, offset + 2);
                mesh.getFaces().addAll(tr, offset + 2, br, offset + 3, bl, offset + 1);

            }
        }


        // material
        Image diffuseMap = createImage(size, noiseArray);

        PhongMaterial material = new PhongMaterial();
        material.setDiffuseMap(diffuseMap);
        material.setSpecularColor(Color.WHITE);

        // mesh view
        MeshView meshView = new MeshView(mesh);
        meshView.setTranslateX(-0.5 * size);
        meshView.setTranslateZ(-0.5 * size);
        meshView.setMaterial(material);
        meshView.setCullFace(CullFace.NONE);
        meshView.setDrawMode(DrawMode.FILL);
        meshView.setDepthTest(DepthTest.ENABLE);

        cube.getChildren().addAll(meshView);

        // testing / debugging stuff: show diffuse map on chart
        ImageView iv = new ImageView(diffuseMap);
        iv.setTranslateX(-0.5 * size);
        iv.setTranslateY(-0.10 * size);
        iv.setRotate(90);
        iv.setRotationAxis(new Point3D(1, 0, 0));
        cube.getChildren().add(iv);

        // scene
        Scene scene = new Scene(root, 1600, 900, true, SceneAntialiasing.BALANCED);
        scene.setCamera(new PerspectiveCamera());

        scene.setOnMousePressed(me -> {
            mouseOldX = me.getSceneX();
            mouseOldY = me.getSceneY();
        });
        scene.setOnMouseDragged(me -> {
            mousePosX = me.getSceneX();
            mousePosY = me.getSceneY();
            rotateX.setAngle(rotateX.getAngle() - (mousePosY - mouseOldY));
            rotateY.setAngle(rotateY.getAngle() + (mousePosX - mouseOldX));
            mouseOldX = mousePosX;
            mouseOldY = mousePosY;

        });

        makeZoomable(root);

        primaryStage.setResizable(false);
        primaryStage.setScene(scene);
        primaryStage.show();


    }

    /**
     * Create texture for uv mapping
     * @param size
     * @param noise
     * @return
     */
    public Image createImage(double size, float[][] noise) {

        int width = (int) size;
        int height = (int) size;

        WritableImage wr = new WritableImage(width, height);
        PixelWriter pw = wr.getPixelWriter();
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {

                float value = noise[x][y];

                double gray = normalizeValue(value, -.5, .5, 0., 1.);

                gray = clamp(gray, 0, 1);

                Color color = Color.RED.interpolate(Color.YELLOW, gray);

                pw.setColor(x, y, color);

            }
        }

        return wr;

    }

    /**
     * Axis wall
     */
    public static class Axis extends Pane {

        Rectangle wall;

        public Axis(double size) {

            // wall
            // first the wall, then the lines => overlapping of lines over walls
            // works
            wall = new Rectangle(size, size);
            getChildren().add(wall);

            // grid
            double zTranslate = 0;
            double lineWidth = 1.0;
            Color gridColor = Color.WHITE;

            for (int y = 0; y <= size; y += size / 10) {

                Line line = new Line(0, 0, size, 0);
                line.setStroke(gridColor);
                line.setFill(gridColor);
                line.setTranslateY(y);
                line.setTranslateZ(zTranslate);
                line.setStrokeWidth(lineWidth);

                getChildren().addAll(line);

            }

            for (int x = 0; x <= size; x += size / 10) {

                Line line = new Line(0, 0, 0, size);
                line.setStroke(gridColor);
                line.setFill(gridColor);
                line.setTranslateX(x);
                line.setTranslateZ(zTranslate);
                line.setStrokeWidth(lineWidth);

                getChildren().addAll(line);

            }

            // labels
            // TODO: for some reason the text makes the wall have an offset
            // for( int y=0; y <= size; y+=size/10) {
            //
            // Text text = new Text( ""+y);
            // text.setTranslateX(size + 10);
            //
            // text.setTranslateY(y);
            // text.setTranslateZ(zTranslate);
            //
            // getChildren().addAll(text);
            //
            // }

        }

        public void setFill(Paint paint) {
            wall.setFill(paint);
        }

    }

    public void makeZoomable(StackPane control) {

        final double MAX_SCALE = 20.0;
        final double MIN_SCALE = 0.1;

        control.addEventFilter(ScrollEvent.ANY, new EventHandler<ScrollEvent>() {

            @Override
            public void handle(ScrollEvent event) {

                double delta = 1.2;

                double scale = control.getScaleX();

                if (event.getDeltaY() < 0) {
                    scale /= delta;
                } else {
                    scale *= delta;
                }

                scale = clamp(scale, MIN_SCALE, MAX_SCALE);

                control.setScaleX(scale);
                control.setScaleY(scale);

                event.consume();

            }

        });

    }

    /**
     * Create axis walls
     * @param size
     * @return
     */
    private Group createCube(int size) {

        Group cube = new Group();

        // size of the cube
        Color color = Color.DARKCYAN;

        List<Axis> cubeFaces = new ArrayList<>();
        Axis r;

        // back face
        r = new Axis(size);
        r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.5 * 1), 1.0));
        r.setTranslateX(-0.5 * size);
        r.setTranslateY(-0.5 * size);
        r.setTranslateZ(0.5 * size);

        cubeFaces.add(r);

        // bottom face
        r = new Axis(size);
        r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.4 * 1), 1.0));
        r.setTranslateX(-0.5 * size);
        r.setTranslateY(0);
        r.setRotationAxis(Rotate.X_AXIS);
        r.setRotate(90);

        cubeFaces.add(r);

        // right face
        r = new Axis(size);
        r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.3 * 1), 1.0));
        r.setTranslateX(-1 * size);
        r.setTranslateY(-0.5 * size);
        r.setRotationAxis(Rotate.Y_AXIS);
        r.setRotate(90);

        // cubeFaces.add( r);

        // left face
        r = new Axis(size);
        r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.2 * 1), 1.0));
        r.setTranslateX(0);
        r.setTranslateY(-0.5 * size);
        r.setRotationAxis(Rotate.Y_AXIS);
        r.setRotate(90);

        cubeFaces.add(r);

        // top face
        r = new Axis(size);
        r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.1 * 1), 1.0));
        r.setTranslateX(-0.5 * size);
        r.setTranslateY(-1 * size);
        r.setRotationAxis(Rotate.X_AXIS);
        r.setRotate(90);

        // cubeFaces.add( r);

        // front face
        r = new Axis(size);
        r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.1 * 1), 1.0));
        r.setTranslateX(-0.5 * size);
        r.setTranslateY(-0.5 * size);
        r.setTranslateZ(-0.5 * size);

        // cubeFaces.add( r);

        cube.getChildren().addAll(cubeFaces);

        return cube;
    }

    /**
     * Create an array of the given size with values of perlin noise
     * @param size
     * @return
     */
    private float[][] createNoise( int size) {
        float[][] noiseArray = new float[(int) size][(int) size];

        for (int x = 0; x < size; x++) {
            for (int y = 0; y < size; y++) {

                double frequency = 10.0 / (double) size;

                double noise = ImprovedNoise.noise(x * frequency, y * frequency, 0);

                noiseArray[x][y] = (float) noise;
            }
        }

        return noiseArray;

    }

    public static double normalizeValue(double value, double min, double max, double newMin, double newMax) {

        return (value - min) * (newMax - newMin) / (max - min) + newMin;

    }

    public static double clamp(double value, double min, double max) {

        if (Double.compare(value, min) < 0)
            return min;

        if (Double.compare(value, max) > 0)
            return max;

        return value;
    }   


    /**
     * Perlin noise generator
     * 
     * // JAVA REFERENCE IMPLEMENTATION OF IMPROVED NOISE - COPYRIGHT 2002 KEN PERLIN.
     * // http://mrl.nyu.edu/~perlin/paper445.pdf
     * // http://mrl.nyu.edu/~perlin/noise/
     */
    public final static class ImprovedNoise {
    static public double noise(double x, double y, double z) {
       int X = (int)Math.floor(x) & 255,                  // FIND UNIT CUBE THAT
           Y = (int)Math.floor(y) & 255,                  // CONTAINS POINT.
           Z = (int)Math.floor(z) & 255;
       x -= Math.floor(x);                                // FIND RELATIVE X,Y,Z
       y -= Math.floor(y);                                // OF POINT IN CUBE.
       z -= Math.floor(z);
       double u = fade(x),                                // COMPUTE FADE CURVES
              v = fade(y),                                // FOR EACH OF X,Y,Z.
              w = fade(z);
       int A = p[X  ]+Y, AA = p[A]+Z, AB = p[A+1]+Z,      // HASH COORDINATES OF
           B = p[X+1]+Y, BA = p[B]+Z, BB = p[B+1]+Z;      // THE 8 CUBE CORNERS,

       return lerp(w, lerp(v, lerp(u, grad(p[AA  ], x  , y  , z   ),  // AND ADD
                                      grad(p[BA  ], x-1, y  , z   )), // BLENDED
                              lerp(u, grad(p[AB  ], x  , y-1, z   ),  // RESULTS
                                      grad(p[BB  ], x-1, y-1, z   ))),// FROM  8
                      lerp(v, lerp(u, grad(p[AA+1], x  , y  , z-1 ),  // CORNERS
                                      grad(p[BA+1], x-1, y  , z-1 )), // OF CUBE
                              lerp(u, grad(p[AB+1], x  , y-1, z-1 ),
                                      grad(p[BB+1], x-1, y-1, z-1 ))));
    }
    static double fade(double t) { return t * t * t * (t * (t * 6 - 15) + 10); }
    static double lerp(double t, double a, double b) { return a + t * (b - a); }
    static double grad(int hash, double x, double y, double z) {
       int h = hash & 15;                      // CONVERT LO 4 BITS OF HASH CODE
       double u = h<8 ? x : y,                 // INTO 12 GRADIENT DIRECTIONS.
              v = h<4 ? y : h==12||h==14 ? x : z;
       return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v);
    }
    static final int p[] = new int[512], permutation[] = { 151,160,137,91,90,15,
    131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
    190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
    88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
    77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
    102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
    135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
    5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
    223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
    129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
    251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
    49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
    138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180
    };
    static { for (int i=0; i < 256 ; i++) p[256+i] = p[i] = permutation[i]; }
    }

    public static void main(String[] args) {
        launch(args);
    }


}

Screenshot:

这篇关于如何使用JavaFX创建一个3d /表面图?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆