NotZack
NotZack

Reputation: 518

Draw only nodes that should be on screen?

I am making a simulation within JavaFX using the scene graph, (not canvas) and am having issues drawing only what I need on the screen.

This simulation has 10 million+ nodes within it, but the user only needs to see a small fraction of those nodes at the same time on screen (max 160,000 nodes). All the nodes that I am concerned about are 400x400 ImageViews.

Each node is a member of a Group (node chunk) that holds roughly 40,000 nodes, so 4 or less of these 'node chunks' need to be displayed at a time. For these 'node chunks' to be displayed they are added onto a static Pane and that pane is in the root node, which is a Group.

So my graph from the first parent to the last child looks like so:

Root node Group\ Display Pane \ (many) Node ChunksGroup\<= 40,000 ImageViews

Since the display pane is constantly moving around (panning and rescaling) based off of user input, and there are so many nodes, the application doesn't run at the speed I would like it. It makes sense that JavaFX has trouble keeping track of 10 million+ nodes at the same time, so my solution has been to remove all 'node chunks' from the display pane; saving them in a hash map until I need them to be drawn.

Each 'node chunk' has had its LayoutX and LayoutYs set to be uniformly distributed across the display pane in a grid like so:

Node chunk display

In this example I would need to grab and display 'node chunk' 7, 8, 12, and 13 since that is what the user is seeing.

Here is a screenshot with 'node chunk' 0 manually added. The greenish yellow color is where 'node chunks' 1, 5, and 6 would be placed.

chunk 1

My issue is: Since the 'node chunks' are not added into the display pane until they are needed, I cannot reference their layout bounds with respect to the constantly changing section of the display pane that users are seeing, so I do not know which 'node chunks' need to be displayed.

Is there an easy way to solve this? Or am I down the wrong path? (or both) Thanks.

Upvotes: 0

Views: 359

Answers (2)

NotZack
NotZack

Reputation: 518

I ended up creating a rectangle for each 'node chunk', set the opacity to zero, and then checked for collision with the section of the display pane that the user is seeing. When a collision with a rectangle occurred, the corresponding 'node chunk' is then added to the display pane.

Upvotes: 0

Sai Dandem
Sai Dandem

Reputation: 9869

Note: This is not the answer to your exact question.

I dont know what is the reason to create ImageView for each and every image. Rather I would draw one image for each nodeChunk (160000 images) and create one ImageView for this nodeChunk.

Functionally I couldnt see any difference between these two approaches ( you can let me know why you want to go with each imageview). But by peformance this approach is very fast and smooth.

Please find the below demo of what I am saying. enter image description here

import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

    import java.awt.*;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import java.security.SecureRandom;
    import java.util.HashMap;
    import java.util.Map;

    /**
     * Combining 4,000,000 images
     */
    public class ImageLoadingDemo extends Application {

        SecureRandom rnd = new SecureRandom();
        // I created 5 images of dimension 1 x 1  each with new color
        String[] images = {"1.png", "2.png", "3.png", "4.png", "5.png"};
        Map<Integer, BufferedImage> buffImages = new HashMap<>();
        Map<Integer, Image> fxImages = new HashMap<>();

        @Override
        public void start(Stage primaryStage) throws IOException {
            ScrollPane root = new ScrollPane();
            root.setPannable(true);
            Scene scene = new Scene(root, 800, 800);
            primaryStage.setScene(scene);
            primaryStage.show();

            root.setContent(approach1());  // Fast & smooth rendering

            // Your approach of creating ImageViews
            // root.setContent(approach2());  // Very very very very very slow rendering
        }

        private Node approach1(){
            // Creating 5 x 5 nodeChunk grid
            GridPane grid = new GridPane();
            for (int i = 0; i < 5; i++) {
                for (int j = 0; j < 5; j++) {
                    ImageView nodeChunk = new ImageView(SwingFXUtils.toFXImage(createChunk(), null));
                    grid.add(nodeChunk, i, j);
                }
            }
            return grid;
        }

        private BufferedImage createChunk() {
            // Combining 160000 1px images into a single image of 400 x 400
            BufferedImage chunck = new BufferedImage(400, 400, BufferedImage.TYPE_INT_ARGB);
            Graphics2D paint;
            paint = chunck.createGraphics();
            paint.setPaint(Color.WHITE);
            paint.fillRect(0, 0, chunck.getWidth(), chunck.getHeight());
            paint.setBackground(Color.WHITE);
            for (int i = 0; i < 400; i++) {
                for (int j = 0; j < 400; j++) {
                    int index = rnd.nextInt(5); // Picking a random image
                    BufferedImage buffImage = buffImages.get(index);
                    if (buffImage == null) {
                        Image image = new Image(ImageLoadingDemo.class.getResourceAsStream(images[index]));
                        buffImages.put(index, SwingFXUtils.fromFXImage(image, null));
                    }
                    paint.drawImage(buffImage, i, j, null);
                }
            }
            return chunck;
        }

        private Node approach2(){
            GridPane grid = new GridPane();
            for (int i = 0; i < 5; i++) {
                for (int j = 0; j < 5; j++) {
                    grid.add(createGroup(), i, j);
                }
            }
            return grid;
        }
        private Group createGroup() {
            GridPane grid = new GridPane();
            for (int i = 0; i < 400; i++) {
                for (int j = 0; j < 400; j++) {
                    int index = rnd.nextInt(5);
                    Image fxImage = fxImages.get(index);
                    if (fxImage == null) {
                        fxImage = new Image(ImageLoadingDemo.class.getResourceAsStream(images[index]));
                        fxImages.put(index, fxImage);
                    }
                    grid.add(new ImageView(fxImage), i, j);
                }
            }
            return new Group(grid);
        }



        public static void main(String[] args) {
            Application.launch(args);
        }
    }

Upvotes: 1

Related Questions