I am creating a 3D plot in JavaFX using the same technique discussed in this answer, with a MeshView and a PhongMaterial to provide the colours. However, only the top side of the material is coloured, and if the user rotates the camera to view from below, it's impossible to determine the shape of the plot because it is all black.
My questions:
- Is there any way to set the material of the reverse side of the mesh?
- If not, is there a good approach to "faking" it? I would imagine creating a new mesh upside-down in exactly the same position would cause rendering issues; is the best approach to do that but apply a very small offset so that the two meshes are not exactly on top of each other?
Edit: I have included some example code below, which is cut down from my real code but contains enough to illustrate the problem. By default it displays the top of the mesh, which is coloured red in this example. If you change the line that reads new Rotate(-30, Rotate.X_AXIS)
so that the angle becomes +30
rather than -30
, it will rotate the camera to show the underside of the mesh, which you will see appears black.
package test;
import javafx.application.Application;
import javafx.scene.DepthTest;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.image.Image;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
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.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class TestApp extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
TestPlot tp = new TestPlot();
tp.setPrefSize(600, 400);
Scene scene = new Scene(tp);
stage.setScene(scene);
stage.show();
}
class TestPlot extends Region {
private final PerspectiveCamera camera = new PerspectiveCamera(true);
private double[][] data = new double[500][500];
private final StackPane root = new StackPane();
private final SubScene subscene;
public TestPlot() {
subscene = new SubScene(root, 1, 1, true, SceneAntialiasing.BALANCED);
subscene.setCamera(camera);
getChildren().add(subscene);
widthProperty().addListener((obs, oldVal, newVal) -> refreshPlot());
heightProperty().addListener((obs, oldVal, newVal) -> refreshPlot());
refreshPlot();
}
private void refreshPlot() {
// Set the subscene bounds to match the plot bounds, in case the plot was
// resized
subscene.setHeight(this.getHeight());
subscene.setWidth(this.getWidth());
// Clear any existing stuff
root.getChildren().clear();
root.setStyle("-fx-background-color: rgba(0, 0, 0, 0);");
root.getChildren().add(camera);
camera.getTransforms().clear();
int xDataPoints = data.length;
int zDataPoints = data[0].length;
// Create data mesh
TriangleMesh mesh = new TriangleMesh();
for (int x = 0; x < xDataPoints; x++) {
for (int z = 0; z < zDataPoints; z++) {
// Invert the data as JavaFX meshes are positive-down, whereas we expect
// the plot to be positive-up
mesh.getPoints().addAll(x, (float) (-data[x][z]), z);
}
}
// Create faces from data mesh
for (int x = 0; x < xDataPoints - 1; x++) {
for (int z = 0; z < zDataPoints - 1; z++) {
int tl = x * zDataPoints + z; // top-left
int bl = x * zDataPoints + z + 1; // bottom-left
int tr = (x + 1) * zDataPoints + z; // top-right
int br = (x + 1) * zDataPoints + z + 1; // bottom-right
int offset = (x * (zDataPoints - 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);
}
}
// Create data mesh texture map
for (float x = 0; x < xDataPoints - 1; x++) {
for (float z = 0; z < zDataPoints - 1; z++) {
float x0 = x / xDataPoints;
float z0 = z / zDataPoints;
float x1 = (x + 1) / xDataPoints;
float z1 = (z + 1) / zDataPoints;
mesh.getTexCoords().addAll( //
x0, z0, // 0, top-left
x0, z1, // 1, bottom-left
x1, z1, // 2, top-right
x1, z1 // 3, bottom-right
);
}
}
// Create texture material
Image diffuseMap = createTexture(data);
PhongMaterial material = new PhongMaterial();
material.setDiffuseMap(diffuseMap);
// Create & add mesh view
MeshView meshView = new MeshView(mesh);
meshView.setTranslateZ(-zDataPoints);
meshView.setMaterial(material);
meshView.setCullFace(CullFace.NONE);
meshView.setDrawMode(DrawMode.FILL);
meshView.setDepthTest(DepthTest.ENABLE);
root.getChildren().addAll(meshView);
double biggestAxisSize = xDataPoints;
double z = -(0.5 * biggestAxisSize) / Math.tan(0.5 * Math.toRadians(camera.getFieldOfView()));
camera.getTransforms().addAll(
new Translate(0, 0, -zDataPoints / 3.0),
new Rotate(-30, Rotate.X_AXIS),
new Translate(0, 0.5, z)
);
camera.setFarClip(biggestAxisSize * 200.0);
}
private Image createTexture(double[][] data) {
int width = data.length;
int height = data[0].length;
WritableImage wr = new WritableImage(width, height);
PixelWriter pw = wr.getPixelWriter();
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
pw.setColor(x, y, Color.RED);
}
}
return wr;
}
}
}