2

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:

  1. Is there any way to set the material of the reverse side of the mesh?
  2. 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;
    }
  }
}
Ian Renton
  • 699
  • 2
  • 8
  • 21

1 Answers1

5

Modify the cullFace property of your MeshView:

meshView.setCullFace(CullFace.NONE);

Also you need to add ambient light to the scene. The normals of the surface are automatiacally determined and the scalar product used with won't be positive, if the normal is facing away from the light source...

root.getChildren().add(new AmbientLight(Color.WHITE));
fabian
  • 80,457
  • 12
  • 86
  • 114