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:

