© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
K. Sharan, P. SpäthLearn JavaFX 17https://doi.org/10.1007/978-1-4842-7848-2_16

16. Understanding 3D Shapes

Kishori Sharan1   and Peter Späth2
(1)
Montgomery, AL, USA
(2)
Leipzig, Sachsen, Germany
 
In this chapter, you will learn:
  • About 3D shapes and the classes representing 3D shapes in JavaFX

  • How to check whether your machine supports 3D

  • About the 3D coordinate system used in JavaFX

  • About the rendering order of nodes

  • How to draw predefined 3D shapes

  • About the different types of cameras and how to use them to render scenes

  • How to use light sources to view 3D objects in scenes

  • How to create and use subscenes

  • How to draw user-defined 3D shapes in JavaFX

The examples of this chapter lie in the com.jdojo.shape3d package. In order for them to work, you must add a corresponding line to the module-info.java file:
...
opens com.jdojo.shape3d to javafx.graphics, javafx.base;
...

What Are 3D Shapes?

Any shape, drawn in a three-dimensional space, having three dimensions (length, width, and depth) is known as a 3D shape. Cubes, spheres, and pyramids are examples.

JavaFX offers real 3D shapes as nodes. It provides two types of 3D shapes:
  • Predefined shapes

  • User-defined shapes

Box, sphere, and cylinder are three predefined 3D shapes that you can readily use in your JavaFX applications. You can also create any type of 3D shapes using a triangle mesh.

Figure 16-1 shows a class diagram of classes representing JavaFX 3D shapes. The 3D shape classes are in the javafx.scene.shape package. The Box, Sphere, and Cylinder classes represent the three predefined shapes. The MeshView class represents a user-defined 3D shape in a scene.
Figure 16-1

A class diagram for classes representing 3D shapes

The 3D visualization in JavaFX is accomplished using lights and cameras. Lights and cameras are also nodes, which are added to the scene. You add 3D nodes to a scene, light it with lights, and view it using a camera. The positions of lights and cameras in the space determine the lighted and viewable areas of the scene. Figure 16-2 shows a 3D box, which is created using an instance of the Box class.
Figure 16-2

An example of a 3D box shape

Checking Support for 3D

JavaFX 3D support is a conditional feature. If it is not supported on your platform, you get a warning message on the console when you run a program that attempts to use 3D features. Run the program in Listing 16-1 to check if your machine supports JavaFX 3D. The program will print a message stating whether the 3D support is available.
// Check3DSupport.java
package com.jdojo.shape3d;
import javafx.application.ConditionalFeature;
import javafx.application.Platform;
public class Check3DSupport {
    public static void main(String[] args) {
        boolean supported =
              Platform.isSupported(ConditionalFeature.SCENE3D);
        if (supported) {
            System.out.println("3D is supported on your machine.");
        } else {
            System.out.println("3D is not supported on your machine.");
        }
    }
}
Listing 16-1

Checking JavaFX 3D Support on Your Machine

The 3D Coordinate System

A point in the 3D space is represented by (x, y, z) coordinates. A 3D object has three dimensions: x, y, and z. Figure 16-3 shows the 3D coordinate system used in JavaFX.
Figure 16-3

The 3D coordinate system used in JavaFX

The positive direction of the x-axis points to the right from the origin; the positive direction of the y-axis points down; the positive direction of the z-axis points into the screen (away from the viewer). The negative directions on the axes, which are not shown, extend in the opposite directions at the origin.

Rendering Order of Nodes

Suppose you are looking at two overlapping objects at a distance. The object closer to you always overlaps the object farther from you, irrespective of the sequence in which they appeared in the view. When dealing with 3D objects in JavaFX, you would like them to appear the same way.

In JavaFX, by default, nodes are rendered in the order they are added to the scene graph. Consider the following snippet of code:
Rectangle r1 = new Rectangle(0, 0, 100, 100);
Rectangle r2 = new Rectangle(50, 50, 100, 100);
Group group = new Group(r1, r2);
Two rectangles are added to a group. The rectangle r1 is rendered first followed by rectangle r2. The overlapping area will show only the area of r2, not r1. If the group was created as new Group(r2, r1), the rectangle r2 will be rendered first followed with rectangle r1. The overlapping area will show the area of r1, not r2. Let us add the z coordinates for the two rectangles as follows:
Rectangle r1 = new Rectangle(0, 0, 100, 100);
r1.setTranslateZ(10);
Rectangle r2 = new Rectangle(50, 50, 100, 100);
r2.setTranslateZ(50);
Group group = new Group(r1, r2);

The foregoing snippet of code will produce the same effect as before. The rectangle r1 will be rendered first followed by the rectangle r2. The z values for the rectangles are ignored. In this case, you would like to render the rectangle r1 last as it is closer to the viewer (z=10 is closer than z=50).

The previous rendering behavior is not desirable in a 3D space. You expect the 3D objects to appear the same way as they would appear in a real world. You need to do two things two achieve this.
  • When creating a Scene object, specify that it needs to have a depth buffer.

  • Specify in the nodes that their z coordinate values should be used during rendering. That is, they need to be rendered according to their depth (the distance from the viewer).

When you create a Scene object, you need to specify the depthBuffer flag , which is set to false by default:
// Create a Scene object with depthBuffer set to true
double width = 300;
double height = 200;
boolean depthBuffer = true;
Scene scene = new Scene(root, width, height, depthBuffer);

The depthBuffer flag for a scene cannot be changed after the scene is created. You can check whether a scene has a depthBuffer using the isDepthBuffer() method of the Scene object.

The Node class contains a depthTest property, which is available for all nodes in JavaFX. Its value is one of the constants of the javafx.scene.DepthTest enum:
  • ENABLE

  • DISABLE

  • INHERIT

The ENABLE value for the depthTest indicates that the z coordinate values should be taken into account when the node is rendered. When the depth testing is enabled for a node, its z coordinate is compared with all other nodes with depth testing enabled, before rendering.

The DISABLE value indicates that the nodes are rendered in the order they are added to the scene graph.

The INHERIT value indicates that the depthTest property for a node is inherited from its parent. If a node has a null parent, it is the same as ENABLE.

The program in Listing 16-2 demonstrates the concepts of using the depth buffer for a scene and the depth test for nodes. It adds two rectangles to a group. The rectangles are filled with red and green colors. The z coordinates for the red and green rectangles are 400px and 300px, respectively. The green rectangle is added to the group first. However, it is rendered first as it is closer to the viewer. You have added a camera to the scene, which is needed to view objects having depth (the z coordinate). The CheckBox is used to enable and disable the depth test for the rectangles. When the depth test is disabled, the rectangles are rendered in the order they are added to the group: the green rectangle followed with the red rectangle. Figure 16-4 shows rectangles in both states.
// DepthTestCheck.java
package com.jdojo.shape3d;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.DepthTest;
import javafx.stage.Stage;
public class DepthTestCheck  extends Application {
        public static void main(String[] args) {
                Application.launch(args);
        }
        @Override
        public void start(Stage stage) {
                // Create two rectangles and add then to a Group
                Rectangle red = new Rectangle(100, 100);
                red.setFill(Color.RED);
                red.setTranslateX(100);
                red.setTranslateY(100);
                red.setTranslateZ(400);
                Rectangle green = new Rectangle(100, 100);
                green.setFill(Color.GREEN);
                green.setTranslateX(150);
                green.setTranslateY(150);
                green.setTranslateZ(300);
                Group center = new Group(green, red);
                CheckBox depthTestCbx =
                         new CheckBox("DepthTest for Rectangles");
                depthTestCbx.setSelected(true);
                depthTestCbx.selectedProperty().addListener(
                    (prop, oldValue, newValue) -> {
                        if (newValue) {
                            red.setDepthTest(DepthTest.ENABLE);
                            green.setDepthTest(DepthTest.ENABLE);
                        }
                        else {
                            red.setDepthTest(DepthTest.DISABLE);
                            green.setDepthTest(DepthTest.DISABLE);
                        }
                });
                // Create a BorderPane as the root node for the scene.
                // Need to set the background transparent, so the camera
                // can view the rectangles behind the surface of the
                // BorderPane
                BorderPane root = new BorderPane();
                root.setStyle("-fx-background-color: transparent;");
                root.setTop(depthTestCbx);
                root.setCenter(center);
                // Create a scene with depthBuffer enabled
                Scene scene = new Scene(root, 200, 200, true);
                // Need to set a camera to look into the 3D space of
                // the scene
                scene.setCamera(new PerspectiveCamera());
                stage.setScene(scene);
                stage.setTitle("Depth Test");
                stage.show();
        }
}
Listing 16-2

Enabling/Disabling the DepthTest Property for Nodes

Figure 16-4

Effects of the depthTest property on rendering nodes

Using Predefined 3D Shapes

JavaFX 8 provides the following three built-in 3D geometric shapes:
  • Box

  • Sphere

  • Cylinder

The shapes are represented by instances of the Box, Sphere, and Cylinder classes. The classes inherit from the Shape3D class, which contains three properties that are common to all types of 3D shapes:
  • Material

  • Draw mode

  • Cull face

I will discuss these properties in detail in subsequent sections. If you do not specify these properties for a shape, reasonable defaults are provided.

The properties specific to a shape type are defined in the specific class defining the shape. For example, properties for a box are defined in the Box class. All shapes are nodes. Therefore, you can apply transformations to them. You can position them at any point in the 3D space using the translateX, translateY, and translateZ transformations.

Tip

The center of a 3D shape is located at the origin of the local coordinate system of the shape.

A Box is defined by the following three properties:
  • width

  • height

  • depth

The Box class contains two constructors:
  • Box()

  • Box(double width, double height, double depth)

The no-args constructor creates a Box with width, height, and depth of 2.0 each. The other constructor lets you specify the dimensions of the Box. The center of the Box is located at the origin of its local coordinate system:
// Create a Box with width=10, height=20, and depth=50
Box box = new Box(10, 20, 50);
A Sphere is defined by only one property named radius. The Sphere class contains three constructors:
  • Sphere()

  • Sphere(double radius)

  • Sphere(double radius, int divisions)

The no-args constructor creates a sphere of radius 1.0.

The second constructor lets you specify the radius of the sphere.

The third constructor lets you specify the radius and divisions. A 3D sphere is made up of many divisions, which are constructed from connected triangles. The value of the number of divisions defines the resolution of the sphere. The higher the number of divisions, the smoother the sphere looks. By default, a value of 64 is used for the divisions. The value of divisions cannot be less than 1.
// Create a Sphere with radius =50
Sphere sphere = new Sphere(50);
A Cylinder is defined by two properties:
  • radius

  • height

The radius of the cylinder is measured on the XZ plane. The axis of the cylinder is measured along the y-axis. The height of the cylinder is measured along its axis. The Cylinder class contains three constructors:
  • Cylinder()

  • Cylinder(double radius, double height)

  • Cylinder(double radius, double height, int divisions)

The no-args constructor creates a Cylinder with a 1.0 radius and a 2.0 height.

The second constructor lets you specify the radius and height properties.

The third constructor lets you specify the number of divisions, which defines the resolution of the cylinder. The higher the number of divisions, the smoother the cylinder looks. Its default value is 64 (the documentation specifies 15 here, which is wrong) along the x-axis and z-axis each. Its value cannot be less than 3. If a value less than 3 is specified, a value of 3 is used. Note that the number of divisions does not apply along the y-axis. Suppose the number of divisions is 10. It means that the vertical surface of the cylinder is created using 10 triangles. The height of the triangle will extend the entire height of the cylinder. The base of the cylinder will be created using 10 triangles as well.
// Create a cylinder with radius=40 and height=120
Cylinder cylinder = new Cylinder(40, 120);
The program in Listing 16-3 shows how to create 3D shapes. Figure 16-5 shows the shapes.
// PreDefinedShapes.java
package com.jdojo.shape3d;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.shape.Box;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.Sphere;
import javafx.stage.Stage;
public class PreDefinedShapes extends Application {
        public static void main(String[] args) {
                Application.launch(args);
        }
        @Override
        public void start(Stage stage) {
                // Create a Box
                Box box = new Box(100, 100, 100);
                box.setTranslateX(150);
                box.setTranslateY(0);
                box.setTranslateZ(400);
                // Create a Sphere
                Sphere sphere = new Sphere(50);
                sphere.setTranslateX(300);
                sphere.setTranslateY(-5);
                sphere.setTranslateZ(400);
                // Create a cylinder
                Cylinder cylinder = new Cylinder(40, 120);
                cylinder.setTranslateX(500);
                cylinder.setTranslateY(-25);
                cylinder.setTranslateZ(600);
                // Create a light
                PointLight light = new PointLight();
                light.setTranslateX(350);
                light.setTranslateY(100);
                light.setTranslateZ(300);
                // Add shapes and a light to the group
                Group root = new Group(box, sphere, cylinder, light);
                // Create a Scene with depth buffer enabled
                Scene scene = new Scene(root, 300, 100, true);
                // Set a camera to view the 3D shapes
                PerspectiveCamera camera = new PerspectiveCamera(false);
                camera.setTranslateX(100);
                camera.setTranslateY(-50);
                camera.setTranslateZ(300);
                scene.setCamera(camera);
                stage.setScene(scene);
                stage.setTitle(
                         "Using 3D Shapes: Box, Sphere and Cylinder");
                stage.show();
        }
}
Listing 16-3

Creating 3D Primitive Shapes: Box, Sphere, and Cylinder

Figure 16-5

Primitive 3D shapes: a box, a sphere, and a cylinder

The program creates the three shapes and positions them in the space. It creates a light, which is an instance of the PointLight, and positions it in the space. Note that a light is also a Node. The light is used to light the 3D shapes. All shapes and the light are added to a group, which is added to the scene.

To view the shapes, you need to add a camera to the scene. The program adds a PerspectiveCamera to the scene. Note that you need to position the camera as its position and orientation in the space determine what you see. The origin of the local coordinate system of the camera is located at the center of the scene. Try resizing the window after you run the program. You will notice that the view of the shapes changes as you resize the window. It happens because the center of the scene is changing when you resize the window, which in turn repositions the camera, resulting in the change in the view.

Specifying the Shape Material

A material is used for rendering the surface of shapes. You can specify the material for the surface of 3D objects using the material property, which is defined in the Shape3D class. The material property is an instance of the abstract class Material. JavaFX provides the PhongMaterial class as the only concrete implementation of Material. Both classes are in the javafx.scene.paint package. An instance of the PhongMaterial class represents Phong shaded material. Phong shaded material is based on Phong shading and the Phong reflection model (also known as Phong illumination and Phong lighting), which were developed at the University of Utah by Bui Tuong Phong as part of his Ph.D. dissertation in 1973. A complete discussion of the Phong model is beyond the scope of this book. The model provides an empirical formula to compute the color of a pixel on the geometric surface in terms of the following properties defined in the PhongMaterial class:
  • diffuseColor

  • diffuseMap

  • specularColor

  • specularMap

  • selfIlluminationMap

  • specularPower

  • bumpMap

The PhongMaterial class contains three constructors:
  • PhongMaterial()

  • PhongMaterial(Color diffuseColor)

  • PhongMaterial(Color diffuseColor, Image diffuseMap, Image specularMap, Image bumpMap, Image selfIlluminationMap)

The no-args constructor creates a PhongMaterial with the diffuse color as Color.WHITE. The other two constructors are used to create a PhongMaterial with the specified properties.

When you do not provide a material for a 3D shape, a default material with a diffuse color of Color.LIGHTGRAY is used for rendering the shape. All shapes in our previous example in Listing 16-3 used the default material.

The following snippet of code creates a Box, creates a PhongMaterial with tan diffuse color, and sets the material to the box:
Box box = new Box(100, 100, 100);
PhongMaterial material = new PhongMaterial();
material.setDiffuseColor(Color.TAN);
box.setMaterial(material);
You can use an Image as the diffuse map to have texture for the material, as shown in the following code:
Box boxWithTexture = new Box(100, 100, 100);
PhongMaterial textureMaterial = new PhongMaterial();
Image randomness = new Image("resources/picture/randomness.jpg");
textureMaterial.setDiffuseMap(randomness);
boxWithTexture.setMaterial(textureMaterial);
The program in Listing 16-4 shows how to create and set material for shapes. It creates two boxes. It sets the diffuse color for one box and the diffuse map for the other. The image used for the diffuse map provides the texture for the surface of the second box. The two boxes look as shown in Figure 16-6.
// MaterialTest.java
package com.jdojo.shape3d;
import com.jdojo.util.ResourceUtil;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.stage.Stage;
public class MaterialTest extends Application {
        public static void main(String[] args) {
                Application.launch(args);
        }
        @Override
        public void start(Stage stage) {
                // Create a Box
                Box box = new Box(100, 100, 100);
                // Set the material for the box
                PhongMaterial material = new PhongMaterial();
                material.setDiffuseColor(Color.TAN);
                box.setMaterial(material);
                // Place the box in the space
                box.setTranslateX(250);
                box.setTranslateY(0);
                box.setTranslateZ(400);
                // Create a Box with texture
                Box boxWithTexture = new Box(100, 100, 100);
                PhongMaterial textureMaterial = new PhongMaterial();
                     Image randomness =
                         new Image(ResourceUtil.getResourceURLStr(
                             "picture/randomness.jpg"));
                textureMaterial.setDiffuseMap(randomness);
                boxWithTexture.setMaterial(textureMaterial);
                // Place the box in the space
                boxWithTexture.setTranslateX(450);
                boxWithTexture.setTranslateY(-5);
                boxWithTexture.setTranslateZ(400);
                PointLight light = new PointLight();
                light.setTranslateX(250);
                light.setTranslateY(100);
                light.setTranslateZ(300);
                Group root = new Group(box, boxWithTexture);
                // Create a Scene with depth buffer enabled
                Scene scene = new Scene(root, 300, 100, true);
                // Set a camera to view the 3D shapes
                PerspectiveCamera camera = new PerspectiveCamera(false);
                camera.setTranslateX(200);
                camera.setTranslateY(-50);
                camera.setTranslateZ(325);
                scene.setCamera(camera);
                stage.setScene(scene);
                stage.setTitle(
                         "Using Material Color and Texture for 3D Surface");
                stage.show();
        }
}
Listing 16-4

Using the Diffuse Color and Diffuse Map to Create PhongMaterial

Figure 16-6

Two boxes: one with a tan diffuse color and one with texture using a diffuse map

Specifying the Draw Mode of Shapes

A 3D shape surface consists of many connected polygons made up of triangles. For example, a Box is made up of 12 triangles—each side of the Box using two triangles. The drawMode property in the Shape3D class specifies how the surface of 3D shapes is rendered. Its value is one of the constants of the DrawMode enum:
  • DrawMode.FILL

  • DrawMode.LINE

The DrawMode.FILL is the default, and it fills the interior of the triangles. The DrawMode.LINE draws only the outline of the triangles. That is, it draws only lines connecting the vertices of the consecutive triangles.
// Create a Box with outline only
Box box = new Box(100, 100, 100);
box.setDrawMode(DrawMode.LINE);
The program in Listing 16-5 shows how to draw only the outline of 3D shapes. Figure 16-7 shows the shapes. The program is similar to the one shown in Listing 16-3. The program sets the drawMode property of all shapes to DrawMode.LINE. The program specifies the divisions of creating the Sphere and Cylinder. Change the value for divisions to a lesser value. You will notice that the number of triangles used to create the shapes decreases, making the shape less smooth.
// DrawModeTest.java
package com.jdojo.shape3d;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.shape.Box;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.Sphere;
import javafx.stage.Stage;
public class DrawModeTest extends Application {
        public static void main(String[] args) {
                Application.launch(args);
        }
        @Override
        public void start(Stage stage) {
                // Create a Box
                Box box = new Box(100, 100, 100);
                box.setDrawMode(DrawMode.LINE);
                box.setTranslateX(150);
                box.setTranslateY(0);
                box.setTranslateZ(400);
                // Create a Sphere: radius = 50, divisions=20
                Sphere sphere = new Sphere(50, 20);
                sphere.setDrawMode(DrawMode.LINE);
                sphere.setTranslateX(300);
                sphere.setTranslateY(-5);
                sphere.setTranslateZ(400);
                // Create a cylinder: radius=40, height=120, divisions=5
                Cylinder cylinder = new Cylinder(40, 120, 5);
                cylinder.setDrawMode(DrawMode.LINE);
                cylinder.setTranslateX(500);
                cylinder.setTranslateY(-25);
                cylinder.setTranslateZ(600);
                PointLight light = new PointLight();
                light.setTranslateX(350);
                light.setTranslateY(100);
                light.setTranslateZ(300);
                Group root = new Group(box, sphere, cylinder, light);
                // Create a Scene with depth buffer enabled
                Scene scene = new Scene(root, 300, 100, true);
                // Set a camera to view the 3D shapes
                PerspectiveCamera camera = new PerspectiveCamera(false);
                camera.setTranslateX(100);
                camera.setTranslateY(-50);
                camera.setTranslateZ(300);
                scene.setCamera(camera);
                stage.setScene(scene);
                stage.setTitle("Drawing Only Lines");
                stage.show();
        }
}
Listing 16-5

Drawing Only Lines for 3D Shapes

Figure 16-7

Drawing the outline of 3D shapes

Specifying the Face Culling for Shapes

A 3D object is never visible entirely. For example, you can never see an entire building at once. When you change the viewing angle, you see different parts of the building. If you face the front of the building, you see only the front part of the building. Standing in front, if you move to the right, you see the front and right sides of the building.

The surface of 3D objects is made of connected triangles. Each triangle has two faces: the exterior face and the interior face. You see the exterior face of the triangles when you look at the 3D objects. Not all triangles are visible all the time. Whether a triangle is visible depends on the position of the camera. There is a simple rule to determine the visibility of triangles making up the surface of a 3D object. Draw a line coming out from the plane of the triangle, and the line is perpendicular to the plane of a triangle. Draw another line from the point where the first line intersects the plane of the triangle to the viewer. If the angle between two lines is greater than 90 degrees, the face of the triangle is not visible to the view. Otherwise, the face of the triangle is visible to the viewer. Note that not both faces of a triangle are visible at the same time.

Face culling is a technique of rendering 3D geometry based on the principle that the nonvisible parts of an object should not be rendered. For example, if you are facing a building from the front, there is no need to render the sides, top, and bottom of the building, as you cannot see them.

Tip

Face culling is used in 3D rendering to enhance performance.

The Shape3D class contains a cullFace property that specifies the type of culling applied in rendering the shape. Its value is one of the constants of the CullFace enum:
  • BACK

  • FRONT

  • NONE

The CullFace.BACK specifies that all triangles that cannot be seen through the camera in its current position should be culled (i.e., not rendered). That is, all triangles whose exterior faces are not facing the camera should be culled. If you are facing the front of a building, this setting will render only the front part of the building. This is the default.

The CullFace.FRONT specifies that all triangles whose exterior faces are facing the camera should be culled. If you are facing the front of a building, this setting will render all parts of the building, except the front part.

The CullFace.NONE specifies that no face culling should be applied. That is, all triangles making up the shape should be rendered:
// Create a Box with no face culling
Box box = new Box(100, 100, 100);
Box.setCullFace(CullFace.NONE);
It is easy to see the effect of face culling when you draw the shape using the drawMode as DrawMode.LINE. I will draw only nonculled triangles. Figure 16-8 shows the same Box using three different face cullings. The first Box (from left) uses the back-face culling, the second front-face culling, and the third one uses no culling. Notice that the first picture of the Box shows the front, right, and top faces, whereas these faces are culled in the second Box. In the second picture, you see the back, left, and bottom faces. Note that when you use front-face culling, you see the interior faces of the triangles as the exterior faces are hidden from the view.
Figure 16-8

A box using different cullFace properties

Using Cameras

Cameras are used to render the scene. Two types of cameras are available:
  • Perspective camera

  • Parallel camera

The names of the cameras suggest the projection type they use to render the scene. Cameras in JavaFX are nodes. They can be added to the scene graph and positioned like other nodes.

The abstract base class Camera represents a camera. Two concrete subclasses of the Camera class exist: PerspectiveCamera and ParallelCamera. The three classes are in the javafx.scene package.

A PerspectiveCamera defines the viewing volume for a perspective projection, which is a truncated right pyramid as shown in Figure 16-9. The camera projects the objects contained within the near and far clipping planes onto the projection plane. Therefore, any objects outside the clipping planes are not visible.
Figure 16-9

The viewing volume of a perspective camera defined by the near clip and far clip planes

The content that the camera will project onto the projection plane is defined by two properties in the Camera class:
  • nearClip

  • farClip

The nearClip is the distance between the camera and the near clipping plane. Objects closer to the camera than the nearClip are not rendered. The default value is 0.1.

The farClip is the distance between the camera and the far clipping plane. Objects farther from the camera than the farClip are not rendered. The default value is 100.

The PerspectiveCamera class contains two constructors:
  • PerspectiveCamera()

  • PerspectiveCamera(boolean fixedEyeAtCameraZero)

The no-args constructor creates a PerspectiveCamera with the fixedEyeAtCameraZero flag set to false, which makes it behave more or less like a parallel camera where the objects in the scene at Z=0 stay the same size when the scene is resized. The second constructor lets you specify this flag. If you want to view 3D objects with real 3D effects, you need to set this flag to true. Setting this flag to true will adjust the size of the projected images of the 3D objects as the scene is resized. Making the scene smaller will make the objects look smaller as well.
// Create a perspective camera for viewing 3D objects
PerspectiveCamera camera = new PerspectiveCamera(true);
The PerspectiveCamera class declares two additional properties:
  • fieldOfView

  • verticalFieldOfView

The fieldOfView is measured in degrees, and it is the view angle of the camera. Its default value is 30 degrees.

The verticalFieldOfView property specifies whether the fieldOfView property applies to the vertical dimension of the projection plane. By default, its value is true. Figure 16-10 depicts the camera, its view angle, and field of view.
Figure 16-10

The view angle and field of view for a perspective camera

An instance of the ParallelCamera specifies the viewing volume for a parallel projection, which is a rectangular box. The ParallelCamera class does not declare any additional properties. It contains a no-args constructor:
ParallelCamera camera = new ParallelCamera();
You can set a camera for a scene using the setCamera() method of the Scene class:
Scene scene = create a scene....
PerspectiveCamera camera = new PerspectiveCamera(true);
scene.setCamera(camera);
Because a camera is a node, you can add it to the scene graph:
PerspectiveCamera camera = new PerspectiveCamera(true);
Group group = new Group(camera);

You can move and rotate the camera as you move and rotate nodes. To move it to a different position, use the translateX, translateY, and translateZ properties. To rotate, use the Rotate transformation.

The program in Listing 16-6 uses a PerspectiveCamera to view a Box. You have used two lights: one to light the front and the top faces and one to light the bottom face of the box. The camera is animated by rotating it indefinitely along the x-axis. As the camera rotates, it brings different parts of the box into the view. You can see the effect of the two lights when the bottom of the box comes into the view. The bottom is shown in green, whereas the top and front are in red.
// CameraTest.java
package com.jdojo.shape3d;
import javafx.animation.Animation;
import javafx.animation.RotateTransition;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Box;
import javafx.scene.shape.CullFace;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;
public class CameraTest extends Application {
        public static void main(String[] args) {
                Application.launch(args);
        }
        @Override
        public void start(Stage stage) {
                Box box = new Box(100, 100, 100);
                box.setCullFace(CullFace.NONE);
                box.setTranslateX(250);
                box.setTranslateY(100);
                box.setTranslateZ(400);
                PerspectiveCamera camera = new PerspectiveCamera(false);
                camera.setTranslateX(100);
                camera.setTranslateY(-50);
                camera.setTranslateZ(300);
                // Add a Rotation animation to the camera
                RotateTransition rt =
                         new RotateTransition(Duration.seconds(2), camera);
                rt.setCycleCount(Animation.INDEFINITE);
                rt.setFromAngle(0);
                rt.setToAngle(90);
                rt.setAutoReverse(true);
                rt.setAxis(Rotate.X_AXIS);
                rt.play();
                PointLight redLight = new PointLight();
                redLight.setColor(Color.RED);
                redLight.setTranslateX(250);
                redLight.setTranslateY(-100);
                redLight.setTranslateZ(250);
                PointLight greenLight = new PointLight();
                greenLight.setColor(Color.GREEN);
                greenLight.setTranslateX(250);
                greenLight.setTranslateY(300);
                greenLight.setTranslateZ(300);
                Group root = new Group(box, redLight, greenLight);
                root.setRotationAxis(Rotate.X_AXIS);
                root.setRotate(30);
                Scene scene = new Scene(root, 500, 300, true);
                scene.setCamera(camera);
                stage.setScene(scene);
                stage.setTitle("Using camaras");
                stage.show();
        }
}
Listing 16-6

Using a PerspectiveCamera As a Node

Using Light Sources

Similar to the real world, you need a light source to view the 3D objects in a scene. An instance of the abstract base class LightBase represents a light source. Its two concrete subclasses, AmbientLight and PointLight, represent an ambient light and a point light. Light source classes are in the javafx.scene package. The LightBase class inherits from the Node class. Therefore, a light source is a node, and it can be added to the scene graph as any other nodes.

A light source has three properties: light color, on/off switch, and a list of affected nodes. The LightBase class basically contains the following two properties:
  • color

  • lightOn

The color specifies the color of the light. The lightOn specifies whether the light is on. The getScope() method of the LightBase class returns an ObservableList<Node>, which is the hierarchical list of nodes affected by this light source. If the list is empty, the scope of the light source is universe, which means that it affects all nodes in the scene. The latter however does not affect nodes that are part of the exclusion scope; see the API documentation for details.

An instance of the AmbientLight class represents an ambient light source. An ambient light is a nondirectional light that seems to come from all directions. Its intensity is constant on the surface of the affected shapes.
// Create a red ambient light
AmbientLight redLight = new AmbientLight(Color.RED);
An instance of the PointLight class represents a point light source. A point light source is a fixed point in space and radiates lights equally in all directions. The intensity of a point light decreases as the distance of the lighted point increases from the light source.
// Create a Add the point light to a group
PointLight redLight = new PointLight(Color.RED);
redLight.setTranslateX(250);
redLight.setTranslateY(-100);
redLight.setTranslateZ(290);
Group group = new Group(node1, node2, redLight);

Creating Subscenes

A scene can use only one camera. Sometimes, you may want to view different parts of a scene using multiple cameras. JavaFX introduces the concept as subscenes. A subscene is a container for a scene graph. It can have its own width, height, fill color, depth buffer, antialiasing flag, and camera. An instance of the SubScene class represents a subscene. The SubScene inherits from the Node class. Therefore, a subscene can be used wherever a node can be used. A subscene can be used to separate 2D and 3D nodes in an application. You can use a camera for the subscene to view 3D objects that will not affect the 2D nodes in the other part of the main scene. The following snippet of code creates a SubScene and sets a camera to it:
SubScene ss = new SubScene(root, 200, 200, true, SceneAntialiasing.BALANCED);
PerspectiveCamera camera = new PerspectiveCamera(false);
ss.setCamera(camera);
Tip

If a SubScene contains Shape3D nodes having a light node, a head light with a PointLight with Color.WHITE light source is provided. The head light is positioned at the camera position.

The program in Listing 16-7 shows how to use subscenes. The getSubScene() method creates a SubScene with a Box, a PerspectiveCamera, and a PointLight. An animation is set up to rotate the camera along the specified axis. The start() method creates two subscenes and adds them to an HBox. One subscene swings the camera along the y-axis and another along the x-axis. The HBox is added to the main scene.
// SubSceneTest.java
package com.jdojo.shape3d;
import javafx.animation.Animation;
import javafx.animation.RotateTransition;
import javafx.application.Application;
import javafx.geometry.Point3D;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Box;
import javafx.scene.shape.CullFace;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;
public class SubSceneTest extends Application {
        public static void main(String[] args) {
                Application.launch(args);
        }
        @Override
        public void start(Stage stage) {
                SubScene ySwing = getSubScene(Rotate.Y_AXIS);
                SubScene xSwing = getSubScene(Rotate.X_AXIS);
                HBox root = new HBox(20, ySwing, xSwing);
                Scene scene = new Scene(root, 500, 300, true);
                stage.setScene(scene);
                stage.setTitle("Using Sub-Scenes");
                stage.show();
        }
        private SubScene getSubScene(Point3D rotationAxis) {
                Box box = new Box(100, 100, 100);
                box.setCullFace(CullFace.NONE);
                box.setTranslateX(250);
                box.setTranslateY(100);
                box.setTranslateZ(400);
                PerspectiveCamera camera = new PerspectiveCamera(false);
                camera.setTranslateX(100);
                camera.setTranslateY(-50);
                camera.setTranslateZ(300);
                // Add a Rotation animation to the camera
                RotateTransition rt =
                         new RotateTransition(Duration.seconds(2), camera);
                rt.setCycleCount(Animation.INDEFINITE);
                rt.setFromAngle(-10);
                rt.setToAngle(10);
                rt.setAutoReverse(true);
                rt.setAxis(rotationAxis);
                rt.play();
                PointLight redLight = new PointLight(Color.RED);
                redLight.setTranslateX(250);
                redLight.setTranslateY(-100);
                redLight.setTranslateZ(290);
                // If you remove the redLight from the following group,
                // a default head light will be provided by the SubScene .
                Group root = new Group(box, redLight);
                root.setRotationAxis(Rotate.X_AXIS);
                root.setRotate(30);
                SubScene ss =
                          new SubScene(root, 200, 200, true,
                                       SceneAntialiasing.BALANCED);
                ss.setCamera(camera);
                return ss;
        }
}
Listing 16-7

Using Subscenes

Creating User-Defined Shapes

JavaFX lets you define a 3D shape using a mesh of polygons. An instance of the abstract Mesh class represents the mesh data. The TriangleMesh class is a concrete subclass of the Mesh class. A TriangleMesh represents a 3D surface consisting of a mesh of triangles.

Tip

In 3D modeling, a mesh of different types of polygons can be used to construct a 3D object. JavaFX supports only a mesh of triangles.

An instance of the MeshView class represents a 3D surface. The data for constructing a MeshView is specified as an instance of the Mesh.

Supplying the mesh data by hand is not an easy task. The problem is complicated by the way you need to specify the data. I will make it easier by demonstrating the mesh usage from a very simple use case to a more complex one.

A TriangleMesh needs to supply data for three aspects of a 3D object:
  • Points

  • Texture coordinates

  • Faces

Note

If you have not worked with 3D objects using a mesh of triangles before, the explanation may seem a little complex. You need to be patient and learn a step at a time to understand the process of creating a 3D object using a mesh of triangles.

Points are the vertices of the triangles in the mesh. You need to specify the (x, y, z) coordinates of vertices in an array. Suppose v0, v1, v2, v3, v4, and so on are the points in 3D space that represent the vertices of the triangles in a mesh. Points in a TriangleMesh are specified as an array of floats.

The texture of a 3D surface is provided as an image that is a 2D object. Texture coordinates are points in a 2D plane, which are mapped to the vertices of triangles. You need to think of the triangles in a mesh unwrapped and placed onto a 2D plane. Overlay the image that supplies the surface texture for the 3D shape onto the same 2D plane. Map the vertices of the triangles to the 2D coordinates of the image to get a pair of (u, v) coordinates for each vertex in the mesh. The array of such (u, v) coordinates is the texture coordinate. Suppose t0, t1, t2, t3, t4, and so on are the texture coordinates.

Faces are the planes created by joining the three edges of the triangles. Each triangle has two faces: a front face and a back face. A face is specified in terms of indexes in the points and texture coordinates arrays. A face is specified as v0, t0, v1, t1, v2, t2, and so on, where v1 is the index of the vertex in the points array and t1 is the index of the vertex in the texture coordinates array.

Consider the box shown in Figure 16-11.
Figure 16-11

A box made of 12 triangles

A box consists of six sides. Each side is a rectangle. Each rectangle consists of two triangles. Each triangle has two faces: a front face and a back face. A box has eight vertices. You have named vertices as v0, v1, v2, and so on, and the faces as f0, f1, f2, and so on in the figure. You do not see the numberings for the vertices and faces that are not visible in the current orientation of the box. Each vertex is defined by a triple (x, y, z), which is the coordinate of the vertex in the 3D space. When you use the term vertex v1, you, technically, mean its coordinates (x1, y1, z1) for the vertex.

To create a mesh of triangles, you need to specify all vertices making up the 3D object. In the case of a box, you need to specify the eight vertices. In the TriangleMesh class, the vertices are known as points , and they are specified as an observable array of float. The following pseudo-code creates the array of vertices. The first array is for understanding purposes only. The actual array specifies the coordinates of the vertices:
// For understanding purpose only
float[] points = {
    v0,
    v1,
   v2,
   ...
   v7};
// The actual array contain (x, y, z) coordinates of all vertices
float[] points = {
    x0, y0, z0, // v0
   x1, y1, z1, // v1
   x2, y2, z2, // v2
   ...
   x7, y7, z7  // v7
};

In the points array, the indexes 0 to 2 contain coordinates of the first vertex, indexes 3 to 5 contain the coordinates of the second vertex, and so on. How do you number the vertices? That is, which vertex is #1 and which one is #2, and so on? There is no rule to specify the order to vertices. It is all up to you how you number them. JavaFX cares about only one thing: you must include all vertices making up the shape in the points array. You are done with generating the points array. You will use it later.

Now, you need to create an array containing coordinates of 2D points. Creating this array is a little tricky. Beginners have hard time understanding this. Consider the figure shown in Figure 16-12.
Figure 16-12

Surface of a box mapped onto a 2D plane

Figures 16-11 and 16-12 are two views of the surface of the same box. Figure 16-12 mapped the surface from the 3D space to a 2D plane. Think of the box as a 3D object made of 12 triangular pieces of paper. Figure 16-11 shows those 12 pieces of paper put together as a 3D box, whereas Figure 16-12 shows the same pieces of paper put side by side on the floor (a 2D plane).

Tip

It is up to you to decide how you want to map the surface of a 3D object into a 2D plane. For example, in Figure 16-12, you could have also mapped the bottom side of the box into the lower, left, or top of the unit square.

Think of an image that you want to use as the texture for your box. The image will not have the third dimension (z dimension). The image needs to be applied on the surface of the box. JavaFX needs to know how the vertices on the box are mapped to the points on the image. You provide this information in terms of mapping of box vertices to the points on the image.

Now, think of a unit square (a 1 x 1 square) that represents the texture image. Overlay the unit square on the unwrapped faces of the box. The unit square is shown in dotted outline in Figure 16-12. The upper-left corner of the square has the coordinates (0, 0); the lower-left corner has the coordinates (0, 1); the upper-right corner has the coordinates (1, 0); the lower-right corner has the coordinates (1, 1).

In Figure 16-12, when you opened the surface of the box to put it onto a 2D plane, some of the vertices had to be split into multiple vertices. The box has eight vertices. The mapped box into the 2D plane has 14 vertices. The figure shows some of the vertices having the same number as those vertices representing the same vertex in the 3D box. Each vertex mapped into 2D plane (in Figure 16-12) becomes an element in the texture coordinates array. Figure 16-13 shows those 14 texture points; they are numbered as t0, t1, t2, and so on. You can number the vertices of the box onto the 2D plane in any order you want. The x and y coordinates of a texture point will be between 0 and 1. The actual mapping of these coordinates to the actual image size is performed by JavaFX. For example, (0.25, 0.) may be used for the coordinates of the vertex t9 and (0.25, 0.25) for the vertex t10.
Figure 16-13

A box surface mapped onto a 2D plane with texture coordinates

You can create the texture coordinates array as shown in the following code. Like the points array, the following is the pseudo-code. The first array is for understanding the concept, and the second array is the actual one that is used in code:
// For understanding purpose-only
float[] texCoords = {
    t0,
   t1,
   t2,
   ...
   t14};
// The actual texture coordinates of vertices
float[] texCoords = {
    x0, y0, // t0
   x1, y1, // t1
   x2, y2, // t2
   ...
   x13, y13 // t13
};
The third piece of information that you need to specify is an array of faces. Note that each triangle has two faces. In our figures, you have shown only the front faces of the triangles. Specifying faces is the most confusing step in creating a TriangleMesh object. A face is specified using the points array and texture coordinates array. You use the indexes of the vertices in the point array and the indexes of the texture points in the texture coordinates array to specify a face. A face is specified using six integers in the following formats:
iv0, it0, iv1, it1, iv2, it2
Here
  • iv0 is the index of the vertex v0 in the points array, and it0 is the index of the point t0 in the texture coordinates array.

  • iv1 and it1 are the indexes of the vertex v1 and point t1 in the points and texture coordinates arrays.

  • iv2 and it2 are the indexes of the vertex v2 and point t2 in the points and texture coordinates arrays.

Figure 16-14 shows only two triangles, which make up the front side of the box.
Figure 16-14

Two triangles of the box with their vertices in points and texture coordinates arrays

Figure 16-14 is the superimposition of the figures shown in Figures 16-12 and 16-13. The figure shows the vertex number and their corresponding texture coordinate point number. To specify the f0 in the faces array, you can specify the vertices of the triangle in two ways, counterclockwise and clockwise:
ivo, it1, iv2, it7, iv3, it10 (Counterclockwise)
ivo, it1, iv3, it10, iv2, it7 (Clockwise)
The starting vertex does not matter in specifying a face. You can start with any vertex and go in a clockwise or a counterclockwise direction. When the vertices for a face are specified in the counterclockwise direction, it is considered the front face. Otherwise, it is considered the back face. The following series of numbers will specify the face f1 in our figure:
ivo, it1, iv1, it2, iv2, it7 (Counterclockwise: front-face)
ivo, it1, iv2, it7, iv1, it2 (Clockwise: back-face)
To determine whether you are specifying the front face or back face, apply the following rules as illustrated in Figure 16-15:
  • Draw a line perpendicular to the surface of the triangle going outward.

  • Imagine you are looking into the surface by aligning your view along the line.

  • Try traversing the vertices in counterclockwise. The sequence of vertices will give you a front face. If you traverse the vertices clockwise, the sequence will give you a back face.

Figure 16-15

Winding order of vertices of a triangle

The following pseudo-code illustrates how to create an int array for specifying faces. The int values are the array indexes from the points and texture coordinates arrays:
int[] faces = new int[] {
ivo, it1, iv2, it7, iv3, it10, // f0: front-face
ivo, it1, iv3, it10, iv2, it7, // f0: back-face
ivo, it1, iv1, it2, iv2, it7,  // f1: front-face
ivo, it1, iv2, it7, iv1, it2   // f1: back-face
...
};
Once you have the points, texture coordinates, and faces arrays, you can construct a TriangleMesh object as follows:
TriangleMesh mesh = new TriangleMesh();
mesh.getPoints().addAll(points);
mesh.getTexCoords().addAll(texCoords);
mesh.getFaces().addAll(faces);
A TriangleMesh provides the data for constructing a user-defined 3D object. A MeshView object creates the surface for the object with a specified TriangleMesh:
// Create a MeshView
MeshView meshView = new MeshView();
meshView.setMesh(mesh);

Once you have a MeshView object, you need to add it to a scene graph to view it. You can view it the same way you have been viewing the predefined 3D shapes Boxes, Spheres, and Cylinders.

In the next few sections, you will create 3D objects using a TriangleMesh. You will start with the simplest 3D object, which is a triangle.

Creating a 3D Triangle

You may argue that a triangle is a 2D shape, not a 3D shape. It is agreed that a triangle is a 2D shape. You will create a triangle in a 3D space using a TriangleMesh. The triangle will have two faces. This example is chosen because it is the simplest shape you can create with a mesh of triangles. In the case of a triangle, the mesh consists of only one triangle. Figure 16-16 shows a triangle in the 3D space and its vertices mapped into a 2D plane.
Figure 16-16

Vertices of a triangle in the 3D space and mapped onto a 2D plane

The triangle can be created using a mesh of one triangle. Let us create the points array for the TriangleMesh object:
float[] points = {50, 0, 0,  // v0 (iv0 = 0)
             45, 10, 0, // v1 (iv1 = 1)
             55, 10, 0  // v2 (iv2 = 2)
};
The second part of the figure, shown on the right, maps the vertices of the triangle to a unit square. You can create the texture coordinates array as follows:
float[] texCoords = {0.5f, 0.5f,  // t0 (it0 = 0)
     0.0f, 1.0f,  // t1 (it1 = 1)
    1.0f, 1.0f   // t2 (it2 = 2)
};
Using the points and texture coordinates arrays, you can specify the faces array as follows:
int[] faces = {
    0, 0, 2, 2, 1, 1,  // iv0, it0, iv2, it2, iv1, it1 (front face)
   0, 0, 1, 1, 2, 2   // iv0, it0, iv1, it1, iv2, it2 back face
};
Listing 16-8 contains the complete program to create a triangle using a TriangleMesh. It adds two different lights to light the two faces of the triangle. An animation rotates the camera, so you can view both sides of the triangle in different colors. The createMeshView() method has the coordinate values and logic to create the MeshView.
// TriangleWithAMesh.java
package com.jdojo.shape3d;
import javafx.animation.Animation;
import javafx.animation.RotateTransition;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TriangleWithAMesh extends Application {
        public static void main(String[] args) {
                Application.launch(args);
        }
        @Override
        public void start(Stage stage) {
                // Create a MeshView and position it in the space
                MeshView meshView = this.createMeshView();
                meshView.setTranslateX(250);
                meshView.setTranslateY(100);
                meshView.setTranslateZ(400);
                // Scale the Meshview to make it look bigger
                meshView.setScaleX(10.0);
                meshView.setScaleY(10.0);
                meshView.setScaleZ(10.0);
                PerspectiveCamera camera = new PerspectiveCamera(false);
                camera.setTranslateX(100);
                camera.setTranslateY(-50);
                camera.setTranslateZ(300);
                // Add a Rotation animation to the camera
                RotateTransition rt =
                         new RotateTransition(Duration.seconds(2), camera);
                rt.setCycleCount(Animation.INDEFINITE);
                rt.setFromAngle(-30);
                rt.setToAngle(30);
                rt.setAutoReverse(true);
                rt.setAxis(Rotate.Y_AXIS);
                rt.play();
                // Front light is red
                PointLight redLight = new PointLight();
                redLight.setColor(Color.RED);
                redLight.setTranslateX(250);
                redLight.setTranslateY(150);
                redLight.setTranslateZ(300);
                // Back light is green
                PointLight greenLight = new PointLight();
                greenLight.setColor(Color.GREEN);
                greenLight.setTranslateX(200);
                greenLight.setTranslateY(150);
                greenLight.setTranslateZ(450);
                Group root = new Group(meshView, redLight, greenLight);
                // Rotate the triangle with its lights to 90 degrees
                root.setRotationAxis(Rotate.Y_AXIS);
                root.setRotate(90);
                Scene scene = new Scene(root, 400, 300, true);
                scene.setCamera(camera);
                stage.setScene(scene);
                stage.setTitle(
                         "Creating a Triangle using a TriangleMesh");
                stage.show();
        }
        public MeshView createMeshView() {
                float[] points = {50, 0, 0,  // v0 (iv0 = 0)
                              45, 10, 0, // v1 (iv1 = 1)
                              55, 10, 0  // v2 (iv2 = 2)
                             };
                float[] texCoords = { 0.5f, 0.5f, // t0 (it0 = 0)
                                 0.0f, 1.0f, // t1 (it1 = 1)
                                 1.0f, 1.0f  // t2 (it2 = 2)
                               };
                int[] faces = {
                    0, 0, 2, 2, 1, 1, // iv0, it0, iv2, it2, iv1, it1
                                             // (front face)
                    0, 0, 1, 1, 2, 2  // iv0, it0, iv1, it1, iv2, it2
                                             // (back face)
                };
                // Create a TriangleMesh
                TriangleMesh mesh = new TriangleMesh();
                mesh.getPoints().addAll(points);
                mesh.getTexCoords().addAll(texCoords);
                mesh.getFaces().addAll(faces);
                // Create a MeshView
                MeshView meshView = new MeshView();
                meshView.setMesh(mesh);
                return meshView;
        }
}
Listing 16-8

Creating a Triangle Using a TriangleMesh

Creating a 3D Rectangle

In this section, you will create a rectangle using a mesh of two triangles. This will give us an opportunity to use what you have learned so far. Figure 16-17 shows a rectangle in the 3D space and its vertices mapped into a 2D plane.
Figure 16-17

Vertices of a rectangle in the 3D space and mapped into a 2D plane

The rectangle consists of two triangles. Both triangles have two faces. In the figure, I have shown only two faces f0 and f1. The following is the points array for the four vertices of the rectangle:
float[] points = {
    50, 0, 0,  // v0 (iv0 = 0)
   50, 10, 0, // v1 (iv1 = 1)
   60, 10, 0, // v2 (iv2 = 2)
   60, 0, 0   // v3 (iv3 = 3)
};
The texture coordinates array can be constructed as follows:
float[] texCoords = {
    0.0f, 0.0f,  // t0 (it0 = 0)
   0.0f, 1.0f,  // t1 (it1 = 1)
   1.0f, 1.0f,  // t2 (it2 = 2)
   1.0f, 0.0f   // t3 (it3 = 3)
};
You will specify the four faces as follows:
int[] faces =
      { 0, 0, 3, 3, 1, 1,  // iv0, it0, iv3, it3, iv1, it1 (f0 front face)
        0, 0, 1, 1, 3, 3,  // iv0, it0, iv1, it1, iv3, it3 (f0 back face)
        1, 1, 3, 3, 2, 2,  // iv1, it1, iv3, it3, iv2, it2 (f1 front face)
        1, 1, 2, 2, 3, 3   // iv1, it1, iv2, it2, iv3, it3 (f1 back face)
      };

If you plug the aforementioned three arrays into the createMeshView() method in Listing 16-8, you will get a rotating rectangle.

Creating a Tetrahedron

Now, you are prepared to create a little complex 3D object. You will create a tetrahedron. Figure 16-18 shows the top view of a tetrahedron.
Figure 16-18

A tetrahedron

A tetrahedron consists of four triangles. It has four vertices. Three triangles meet at a point. Figure 16-19 shows the two views of the tetrahedron. On the left, you have numbered the four vertices as v0, v1, v2, and v3 and four faces as f0, f1, f2, and f3. Note that the face f3 is the face of the triangle at the base, and it is not visible from the top view. The second view has unwrapped the four triangles giving rise to eight vertices on the 2D plane. The dotted rectangle is the unit square into which the eight vertices will be mapped.
Figure 16-19

Vertices of a tetrahedron in the 3D space and mapped into a 2D plane

You can create the points, faces, and texture coordinates arrays as follows:
float[] points = {10, 10, 10, // v0 (iv0 = 0)
             20, 20, 0,  // v1 (iv1 = 1)
             0, 20, 0,   // v2 (iv2 = 2)
             10, 20, 20  // v3 (iv3 = 3)
         };
float[] texCoords = {
        0.50f, 0.33f, // t0 (it0 = 0)
        0.25f, 0.75f, // t1 (it1 = 1)
        0.50f, 1.00f, // t2 (it2 = 2)
        0.66f, 0.66f, // t3 (it3 = 3)
        1.00f, 0.35f, // t4 (it4 = 4)
        0.90f, 0.00f, // t5 (it5 = 5)
        0.10f, 0.00f, // t6 (it6 = 6)
        0.00f, 0.35f  // t7 (it7 = 7)
};
int[] faces = {
        0, 0, 2, 1, 1, 3, // f0 front-face
        0, 0, 1, 3, 2, 1, // f0 back-face
        0, 0, 1, 4, 3, 5, // f1 front-face
        0, 0, 3, 5, 1, 4, // f1 back-face
        0, 0, 3, 6, 2, 7, // f2 front-face
        0, 0, 2, 7, 3, 6, // f2 back-face
        1, 3, 3, 2, 2, 1, // f3 front-face
        1, 3, 2, 1, 3, 2  // f3 back-face
};
Listing 16-9 contains a complete program to show how to construct a tetrahedron using a TriangleMesh. The tetrahedron is rotated along the y-axis, so you can view two of its vertical faces. Figure 16-20 shows the window with the tetrahedron.
// Tetrahedron.java
package com.jdojo.shape3d;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.PointLight;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
public class Tetrahedron extends Application {
        public static void main(String[] args) {
                Application.launch(args);
        }
        @Override
        public void start(Stage stage) {
                MeshView meshView = this.createMeshView();
                meshView.setTranslateX(250);
                meshView.setTranslateY(50);
                meshView.setTranslateZ(400);
                meshView.setScaleX(10.0);
                meshView.setScaleY(20.0);
                meshView.setScaleZ(10.0);
                PerspectiveCamera camera = new PerspectiveCamera(false);
                camera.setTranslateX(100);
                camera.setTranslateY(0);
                camera.setTranslateZ(100);
                PointLight redLight = new PointLight();
                redLight.setColor(Color.RED);
                redLight.setTranslateX(250);
                redLight.setTranslateY(-100);
                redLight.setTranslateZ(250);
                Group root = new Group(meshView, redLight);
                root.setRotationAxis(Rotate.Y_AXIS);
                root.setRotate(45);
                Scene scene = new Scene(root, 200, 150, true);
                scene.setCamera(camera);
                stage.setScene(scene);
                stage.setTitle("A Tetrahedron using a TriangleMesh");
                stage.show();
        }
        public MeshView createMeshView() {
                float[] points = {10, 10, 10, // v0 (iv0 = 0)
                             20, 20, 0,  // v1 (iv1 = 1)
                             0, 20, 0,   // v2 (iv2 = 2)
                             10, 20, 20  // v3 (iv3 = 3)
                           };
                float[] texCoords = {
                        0.50f, 0.33f, // t0 (it0 = 0)
                        0.25f, 0.75f, // t1 (it1 = 1)
                        0.50f, 1.00f, // t2 (it2 = 2)
                        0.66f, 0.66f, // t3 (it3 = 3)
                        1.00f, 0.35f, // t4 (it4 = 4)
                        0.90f, 0.00f, // t5 (it5 = 5)
                        0.10f, 0.00f, // t6 (it6 = 6)
                        0.00f, 0.35f  // t7 (it7 = 7)
                };
                int[] faces = {
                        0, 0, 2, 1, 1, 3, // f0 front-face
                        0, 0, 1, 3, 2, 1, // f0 back-face
                        0, 0, 1, 4, 3, 5, // f1 front-face
                        0, 0, 3, 5, 1, 4, // f1 back-face
                        0, 0, 3, 6, 2, 7, // f2 front-face
                        0, 0, 2, 7, 3, 6, // f2 back-face
                        1, 3, 3, 2, 2, 1, // f3 front-face
                        1, 3, 2, 1, 3, 2, // f3 back-face
                };
                TriangleMesh mesh = new TriangleMesh();
                mesh.getPoints().addAll(points);
                mesh.getTexCoords().addAll(texCoords);
                mesh.getFaces().addAll(faces);
                MeshView meshView = new MeshView();
                meshView.setMesh(mesh);
                return meshView;
        }
}
Listing 16-9

Creating a Tetrahedron Using a TriangleMesh

Figure 16-20

A tetrahedron using a TriangleMesh

Summary

Any shape, drawn in a three-dimensional space, having three dimensions (length, width, and depth) is known as a 3D shape such as cubes, spheres, pyramids, and so on. JavaFX provides 3D shapes as nodes. It offers two types of 3D shapes: predefined shapes and user-defined shapes.

Box, sphere, and cylinder are three predefined 3D shapes that you can readily use in your JavaFX applications. You can create any type of 3D shapes using a triangle mesh. The Box, Sphere, and Cylinder classes represent the three predefined shapes. The MeshView class represents a user-defined 3D shape in a scene. The 3D shape classes are in the javafx.scene.shape package.

JavaFX 3D support is a conditional feature. If it is not supported on your platform, you get a warning message on the console when you run a program that attempts to use 3D features. The method Platform.isSupported(ConditionalFeature.SCENE3D) returns true if 3D is supported on your platform.

When dealing with 3D objects in JavaFX, you would like the object closer to you to overlap the object farther from you. In JavaFX, by default, nodes are rendered in the order they are added to the scene graph. In order for 3D shapes to appear as they would appear in the real world, you need to specify two things. First, when you create a Scene object, specify that it needs to have a depth buffer, and, second, specify that the nodes’ z coordinate values should be used when they are rendered.

Cameras are used to render the scene. Cameras in JavaFX are nodes. They can be added to the scene graph and positioned like other nodes. Perspective camera and parallel camera are two types of cameras used in JavaFX, and they are represented by the PerspectiveCamera and ParallelCamera classes. A perspective camera defines the viewing volume for a perspective projection, which is a truncated right pyramid. The camera projects the objects contained within the near and far clipping planes onto the projection plane. Therefore, any objects outside the clipping planes are not visible. A parallel camera specifies the viewing volume for a parallel projection, which is a rectangular box.

Similar to the real world, you need a light source to view the 3D objects in a scene. An instance of the abstract base class LightBase represents a light source. Its two concrete subclasses, AmbientLight and PointLight, represent an ambient light and a point light.

A scene can use only one camera. Sometimes, you may want to view different parts of a scene using multiple cameras. JavaFX includes the concept of subscenes. A subscene is a container for a scene graph. It can have its own width, height, fill color, depth buffer, antialiasing flag, and camera. An instance of the SubScene class represents a subscene. The SubScene inherits from the Node class.

The next chapter will discuss how to apply different types of effects to nodes in a scene graph.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset