javafxjavafx-8javafx-3dfxyz3d

What is the most practical way to create coordinate grid with JavaFX 3D?


I would like to create a 3D demo application with JavaFX to visualize movements of points in 3D space and first I need to set up a coordinate grid for visual reference. Unfortunately, I was not able to find a sample code for a grid like in this picture:

this picture

Does anyone know what is the most practical way to create something like it?


Solution

  • There are a few solutions out there already.

    FXyz3D library has a CubeWorld class, that gives you precisely a reference grid.

    CubeWorld

    It is quite easy to use. Just import the 'org.fxyz3d:fxyz3d:0.3.0' dependency from JCenter and use it:

    CubeWorld cubeWorld = new CubeWorld(5000, 500, true);
    Sphere sphere = new Sphere(100);
    sphere.setMaterial(new PhongMaterial(Color.FIREBRICK));
    sphere.getTransforms().add(new Translate(100, 200, 300));
    
    Scene scene = new Scene(new Group(cubeWorld, sphere), 800, 800, true, SceneAntialiasing.BALANCED);
    

    As you can see, the solution is based on using 2D rectangles for each face, and the grid lines are created with 3D cylinders. It has very nice features (like self lightning or frontal faces according to camera don't show grid), but it is quite intensive in nodes (sample above has 168 nodes).

    There are other solutions that use a lower number of nodes. For instance, for this sample, that also happens to be related to Leap Motion, I used a TriangleMesh.

    LeapV2

    This is an easy solution, and with just two meshes. However, you see the triangles, instead of squares.

    So let's try to get rid of the triangles. For that I'll use a PolygonMesh, as in this other question, based on the 3DViewer project that is available at the OpenJFX repository, contains already a PolygonalMesh implementation, that allows any number of points per face, so any polygon can be a face.

    This will give you a plane grid based in square faces:

    private PolygonMesh createQuadrilateralMesh(float width, float height, int subDivX, int subDivY) {
        final float minX = - width / 2f;
        final float minY = - height / 2f;
        final float maxX = width / 2f;
        final float maxY = height / 2f;
    
        final int pointSize = 3;
        final int texCoordSize = 2;
        // 4 point indices and 4 texCoord indices per face
        final int faceSize = 8;
        int numDivX = subDivX + 1;
        int numVerts = (subDivY + 1) * numDivX;
        float points[] = new float[numVerts * pointSize];
        float texCoords[] = new float[numVerts * texCoordSize];
        int faceCount = subDivX * subDivY;
        int faces[][] = new int[faceCount][faceSize];
    
        // Create points and texCoords
        for (int y = 0; y <= subDivY; y++) {
            float dy = (float) y / subDivY;
            double fy = (1 - dy) * minY + dy * maxY;
    
            for (int x = 0; x <= subDivX; x++) {
                float dx = (float) x / subDivX;
                double fx = (1 - dx) * minX + dx * maxX;
    
                int index = y * numDivX * pointSize + (x * pointSize);
                points[index] = (float) fx;
                points[index + 1] = (float) fy;
                points[index + 2] = 0.0f;
    
                index = y * numDivX * texCoordSize + (x * texCoordSize);
                texCoords[index] = dx;
                texCoords[index + 1] = dy;
            }
        }
    
        // Create faces
        int index = 0;
        for (int y = 0; y < subDivY; y++) {
            for (int x = 0; x < subDivX; x++) {
                int p00 = y * numDivX + x;
                int p01 = p00 + 1;
                int p10 = p00 + numDivX;
                int p11 = p10 + 1;
                int tc00 = y * numDivX + x;
                int tc01 = tc00 + 1;
                int tc10 = tc00 + numDivX;
                int tc11 = tc10 + 1;
    
                faces[index][0] = p00;
                faces[index][1] = tc00;
                faces[index][2] = p10;
                faces[index][3] = tc10;
                faces[index][4] = p11;
                faces[index][5] = tc11;
                faces[index][6] = p01;
                faces[index++][7] = tc01;
            }
        }
    
        int[] smooth = new int[faceCount];
    
        PolygonMesh mesh = new PolygonMesh(points, texCoords, faces);
        mesh.getFaceSmoothingGroups().addAll(smooth);
        return mesh;
    }
    

    So you can use 2 or 3 of them to create a coordinate system like this:

    public Group createGrid(float size, float delta) {
        if (delta < 1) {
            delta = 1;
        }
        final PolygonMesh plane = createQuadrilateralMesh(size, size, (int) (size / delta), (int) (size / delta));
    
        final PolygonMesh plane2 = createQuadrilateralMesh(size, size, (int) (size / delta / 5), (int) (size / delta / 5));
    
        PolygonMeshView meshViewXY = new PolygonMeshView(plane);
        meshViewXY.setDrawMode(DrawMode.LINE);
        meshViewXY.setCullFace(CullFace.NONE);
    
        PolygonMeshView meshViewXZ = new PolygonMeshView(plane);
        meshViewXZ.setDrawMode(DrawMode.LINE);
        meshViewXZ.setCullFace(CullFace.NONE);
        meshViewXZ.getTransforms().add(new Rotate(90, Rotate.X_AXIS));
    
        PolygonMeshView meshViewYZ = new PolygonMeshView(plane);
        meshViewYZ.setDrawMode(DrawMode.LINE);
        meshViewYZ.setCullFace(CullFace.NONE);
        meshViewYZ.getTransforms().add(new Rotate(90, Rotate.Y_AXIS));
    
        PolygonMeshView meshViewXY2 = new PolygonMeshView(plane2);
        meshViewXY2.setDrawMode(DrawMode.LINE);
        meshViewXY2.setCullFace(CullFace.NONE);
        meshViewXY2.getTransforms().add(new Translate(size / 1000f, size / 1000f, 0));
    
        PolygonMeshView meshViewXZ2 = new PolygonMeshView(plane2);
        meshViewXZ2.setDrawMode(DrawMode.LINE);
        meshViewXZ2.setCullFace(CullFace.NONE);
        meshViewXZ2.getTransforms().add(new Translate(size / 1000f, size / 1000f, 0));
        meshViewXZ2.getTransforms().add(new Rotate(90, Rotate.X_AXIS));
    
        PolygonMeshView meshViewYZ2 = new PolygonMeshView(plane2);
        meshViewYZ2.setDrawMode(DrawMode.LINE);
        meshViewYZ2.setCullFace(CullFace.NONE);
        meshViewYZ2.getTransforms().add(new Translate(size / 1000f, size / 1000f, 0));
        meshViewYZ2.getTransforms().add(new Rotate(90, Rotate.Y_AXIS));
    
        return new Group(meshViewXY, meshViewXY2, meshViewXZ, meshViewXZ2 /*, meshViewYZ, meshViewYZ2 */);
    }
    

    Note that I've duplicated the plane to mock a wider stroke every 5 lines.

    Finally adding axes:

    public Group getAxes(double scale) {
        Cylinder axisX = new Cylinder(1, 200);
        axisX.getTransforms().addAll(new Rotate(90, Rotate.Z_AXIS), new Translate(0, -100, 0));
        axisX.setMaterial(new PhongMaterial(Color.RED));
    
        Cylinder axisY = new Cylinder(1, 200);
        axisY.getTransforms().add(new Translate(0, 100, 0));
        axisY.setMaterial(new PhongMaterial(Color.GREEN));
    
        Cylinder axisZ = new Cylinder(1, 200);
        axisZ.setMaterial(new PhongMaterial(Color.BLUE));
        axisZ.getTransforms().addAll(new Rotate(90, Rotate.X_AXIS), new Translate(0, 100, 0));
    
        Group group = new Group(axisX, axisY, axisZ);
        group.getTransforms().add(new Scale(scale, scale, scale));
        return group;
    }
    

    Now you have:

    final Group axes = getAxes(0.5);
    final Group grid = createGrid(200, 10);
    
    final Sphere sphere = new Sphere(5);
    sphere.getTransforms().add(new Translate(20, 15, 40));
    
    Scene scene = new Scene(new Group(axes, grid, sphere), 800, 800, true, SceneAntialiasing.BALANCED);
    

    Quadrilateral Grid

    The total amount of nodes of this sample is 14.

    Of course, it can be improved to add labels and many other features.