JavaFX MeshView 3D Torus

JavaFX comes with three pre-defined 3D shapes: cylinder, sphere, and box. It is possible to create other shapes such as the pyramid and torus using TriangleMesh and MeshView. The following source code may be used to create a torus in the Processing editor using the function createTorus(), found during an internet search. Another technique demonstrated in a NetBeans project may be found here: http://www.lagers.org.uk/javafx/toroidclass.html .

A .png image (see below) was created with Processing using grid source code and has an aspect ratio of approximately 2:1 (width:height). It should be copy/pasted to your sketch folder and entitled “procTile.png”. The aspect ratio of the image is important to get complete coverage and avoid visible seams.

A Scene DepthBuffer was required to avoid “see through”. This meant using a javafx window and not the default Processing FX2D window.

meshView.setTranslateZ(-80) was necessary to pull the meshView back out of the screen, otherwise a portion of the torus disappeared into the screen during rotation.

meshView.setScaleX(-1) was used to reverse the text direction, otherwise it was rendered backwards.

Ambient light was used to avoid a black halo effect. A camera was not used in this demo.

This code was developed on macOS and has not been tested on other platforms.

Source code:

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.TilePane;
import javafx.scene.DepthTest;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.CullFace;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.image.Image;
import javafx.scene.AmbientLight;
import javafx.scene.transform.Rotate;
import javafx.scene.control.Button;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ToggleButton;
import javafx.scene.shape.DrawMode;
import javafx.animation.RotateTransition;
import javafx.animation.Interpolator;
import javafx.util.Duration;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import javafx.geometry.Insets;

Pane pane;
TriangleMesh mesh;
RotateTransition rt;
ToggleButton btnPause;

int _wndW = 450;
int _wndH = 400;

public TriangleMesh createTorus(float R, float r, int ringSteps, int tubeSteps) {
  mesh = new TriangleMesh();

  // 1. Vertices: Calculate coordinates using parametric torus equations
  for (int ring = 0; ring <= ringSteps; ring++) {
    double theta = (double) ring * 2.0 * Math.PI / ringSteps;
    float cosTheta = (float) Math.cos(theta);
    float sinTheta = (float) Math.sin(theta);

    for (int tube = 0; tube <= tubeSteps; tube++) {
      double phi = (double) tube * 2.0 * Math.PI / tubeSteps;
      float cosPhi = (float) Math.cos(phi);
      float sinPhi = (float) Math.sin(phi);

      // Torus Equations: (R + r*cos(phi))*cos(theta), (R + r*cos(phi))*sin(theta), r*sin(phi)
      float x = (R + r * cosPhi) * cosTheta;
      float y = (R + r * cosPhi) * sinTheta;
      float z = r * sinPhi;
      mesh.getPoints().addAll(x, y, z);

      // 2. Texture Coordinates (0.0 to 1.0)
      mesh.getTexCoords().addAll((float)ring/ringSteps, (float)tube/tubeSteps);
    }
  }

  // 3. Faces: Connect vertices into triangles
  for (int ring = 0; ring < ringSteps; ring++) {
    for (int tube = 0; tube < tubeSteps; tube++) {
      int p0 = ring * (tubeSteps + 1) + tube;
      int p1 = (ring + 1) * (tubeSteps + 1) + tube;
      int p2 = ring * (tubeSteps + 1) + (tube + 1);
      int p3 = (ring + 1) * (tubeSteps + 1) + (tube + 1);

      // Each face is 2 triangles (p0, p1, p2) and (p2, p1, p3)
      // Format: [pointIndex, texCoordIndex, ...]
      mesh.getFaces().addAll(p0, p0, p1, p1, p2, p2);
      mesh.getFaces().addAll(p2, p2, p1, p1, p3, p3);

      // 4. Smoothing: Use a single group ID for a smooth surface
      mesh.getFaceSmoothingGroups().addAll(1, 1);
    }
  }
  return mesh;
}

void doRotation(int axis) {
  rt.stop();
  if (axis == 0) {
    rt.setAxis(Rotate.X_AXIS);
    btnPause.setText("Pause");
    btnPause.setSelected(false);
  } else if (axis == 1) {
    rt.setAxis(Rotate.Y_AXIS);
    btnPause.setText("Pause");
    btnPause.setSelected(false);
  } else {
    rt.setAxis(Rotate.Z_AXIS);
    btnPause.setText("Pause");
    btnPause.setSelected(false);
  }
  rt.play();
}

void setup() {
  size(1, 1, FX2D);
  Stage stage = new Stage();
  stage.setTitle("JavaFX MeshView Torus");
  pane = new Pane();
  pane.setBackground(new Background(new BackgroundFill(Color.BLACK, new CornerRadii(0), new Insets(0))));

  mesh = createTorus(100, 40, 50, 25);
  MeshView meshView = new MeshView(mesh);

  PhongMaterial material = new PhongMaterial();
  try {
    Image image = new Image(new FileInputStream(sketchPath() + "/procTile.png"));
    material.setDiffuseMap(image);
  }
  catch ( FileNotFoundException e) {
    println("File not found: ", e);
  }

  Rotate rotx = new Rotate(30, Rotate.X_AXIS);
  Rotate roty = new Rotate(50, Rotate.Y_AXIS);
  Rotate rotz = new Rotate(30, Rotate.Z_AXIS);

  meshView.setCullFace(CullFace.BACK);
  meshView.setLayoutX(_wndW/2);
  meshView.setLayoutY(220);
  meshView.setTranslateZ(-80); // Pulls view back out of screen
  meshView.setScaleX(-1);  // Text is backwards without this.
  meshView.setDrawMode(DrawMode.FILL);
  meshView.setMaterial(material);
  meshView.getTransforms().addAll(rotx, roty, rotz);

  rt = new RotateTransition(Duration.millis(8000), meshView);
  rt.setByAngle(360); // Rotate 360 degrees
  rt.setCycleCount(RotateTransition.INDEFINITE); // Repeat indefinitely
  rt.setInterpolator(Interpolator.LINEAR);

  TilePane radioPane = new TilePane(10, 10);
  ToggleGroup group = new ToggleGroup();
  RadioButton btnX = new RadioButton("X");
  btnX.setDepthTest(DepthTest.DISABLE); // Allows buttons to work with Scene DepthBuffer enabled
  btnX.setTextFill(Color.WHITE);
  btnX.setOnAction(e -> {
    doRotation(0);
  }
  );
  btnX.setToggleGroup(group);
  RadioButton btnY = new RadioButton("Y");
  btnY.setDepthTest(DepthTest.DISABLE);
  btnY.setTextFill(Color.WHITE);
  btnY.setOnAction(e -> {
    doRotation(1);
  }
  );
  btnY.setToggleGroup(group);
  RadioButton btnZ = new RadioButton("Z");
  btnZ.setDepthTest(DepthTest.DISABLE);
  btnZ.setTextFill(Color.WHITE);
  btnZ.setOnAction(e -> {
    doRotation(2);
  }
  );
  btnZ.setToggleGroup(group);
  radioPane.setLayoutX(30);
  radioPane.setLayoutY(30);
  radioPane.getChildren().addAll(btnX, btnY, btnZ);

  CheckBox wireFrame = new CheckBox("Wireframe");
  wireFrame.setDepthTest(DepthTest.DISABLE);
  wireFrame.setTextFill(Color.WHITE);
  wireFrame.setSelected(false);
  wireFrame.setLayoutX(160);
  wireFrame.setLayoutY(30);
  wireFrame.setOnAction(e -> {
    if (wireFrame.isSelected()) {
      meshView.setDrawMode(DrawMode.LINE);
    } else {
      meshView.setDrawMode(DrawMode.FILL);
    }
  }
  );
  btnPause = new ToggleButton("Pause");
  btnPause.setDepthTest(DepthTest.DISABLE);
  btnPause.setLayoutX(260);
  btnPause.setLayoutY(25);
  btnPause.selectedProperty().addListener(((observable, onState, offState) -> {
    if (offState == true) {
      rt.stop();
      btnPause.setText("Play");
    } else {
      rt.stop();
      btnPause.setText("Pause");
      rt.play();
    }
  }
  ));
  AmbientLight light = new AmbientLight(Color.WHITE);
  light.getScope().add(meshView);
  pane.getChildren().addAll(meshView, radioPane, wireFrame, btnPause, light);
  Scene scene = new Scene(pane, _wndW, _wndH, true); // Enable DepthBuffer to avoid "see through"
  stage.setScene(scene);
  stage.show();
}

Image “procTile.png” to be copy/pasted to your sketch folder:

Output:

1 Like