问题
Problem
I tried to create a particle system using a Canvas. Interestingly the method drawImage is very fast. I got to 90.000 particles at 60 fps, even using drawImage. However, that was only with the same image. When I used different images for the particles the performance dropped down considerably. Then I changed the order of the images, so that e. g. first all particles of image1 are drawn, then all of image2, etc and the performance was good again.
Question
Does anyone know why that is? Is there some internal caching mechanism in drawImage that one has to consider?
Code
Here's example code:
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class CanvasExample extends Application {
GraphicsContext gc;
double width = 800;
double height = 600;
Image[] images;
@Override
public void start(Stage primaryStage) {
Canvas canvas = new Canvas( width, height);
gc = canvas.getGraphicsContext2D();
BorderPane root = new BorderPane();
root.setCenter( canvas);
Scene scene = new Scene( root, width, height);
scene.setFill(Color.BEIGE);
primaryStage.setScene(scene);
primaryStage.show();
createImages(255);
AnimationTimer loop = new AnimationTimer() {
double prev = 0;
double frameCount = 0;
double fps = 0;
@Override
public void handle(long now) {
// very basic frame counter
if( now - prev > 1_000_000_000) {
System.out.println( "FPS: " + frameCount);
fps = frameCount;
prev = now;
frameCount = 0;
} else {
frameCount++;
}
// clear canvas
gc.setFill( Color.BLACK);
gc.fillRect(0, 0, width, height);
// paint images
int numIterations = 90000;
for( int i=0; i < numIterations; i++) {
int index = i % 2; // <==== change here: i % 1 is fast, i % 2 is slow
gc.drawImage(images[ index], 100, 100);
}
gc.setFill(Color.WHITE);
gc.fillText("fps: " + fps, 0, 10);
}
};
loop.start();
}
public void createImages( int count) {
Rectangle rect = new Rectangle(10,10);
rect.setFill(Color.RED);
images = new Image[count];
for( int i=0; i < count; i++) {
images[i] = createImage(rect);
}
}
/**
* Snapshot an image out of a node, consider transparency.
*
* @param node
* @return
*/
public Image createImage(Node node) {
WritableImage wi;
SnapshotParameters parameters = new SnapshotParameters();
parameters.setFill(Color.TRANSPARENT);
int imageWidth = (int) node.getBoundsInLocal().getWidth();
int imageHeight = (int) node.getBoundsInLocal().getHeight();
wi = new WritableImage(imageWidth, imageHeight);
node.snapshot(parameters, wi);
return wi;
}
public static void main(String[] args) {
launch(args);
}
}
It's very basic. An image is created from a rectangle, then put into an array and in the AnimationTimer loop the image is drawn numIterations-times on the Canvas.
When you use index = i % 1
, i. e. the same image over and over, then the fps are at 60 fps on my system. If you use index = i % 2
, i. e. alternating images, then the fps are at 14 fps on my system. That's a considerable difference.
Thank you very much for the help!
回答1:
Background on how canvas works
Canvas keeps a buffer. Every time you issue a command to the canvas, such as drawing an image, the command is appended to the buffer. On the next render pulse, the buffer is flushed by processing each of the commands and rendering them to a texture. The texture is sent to the graphics card which will render it on the screen.
Tracing canvas operation in source code
Note: The linked code referenced here is outdated and not official as it is a backport, but it is the easiest code to cross-reference online and the implementation is similar (or the same) as the official code in the JDK. So just trace through it:
You invoke drawImage and the GraphicsContext writes an image to the buffer.
For the next pulse of the JavaFX graphics system or a snapshot request, the buffer is emptied, issuing render commands. The render command for the image uses a cached texture from a resource factory to render the image.
Why your app slows down (I don't actually know)
I thought perhaps, when you alternate images, you aren't keeping a "live" image to be rendered in the scene as part of the scene graph, so the texture cache evicts the old image, resulting in the system thrashing and recreating the texture for the image (just a guess). However, I stepped through the process in a debugger (remove your on screen fps label and set a breakpoint in BaseShaderGraphics.drawTexture after your app has been running for a few seconds). You will see the same cached textures being reused. The texture cache seems well behaved and doing it's job caching the textures for each image, so I really don't know what the root cause of your observed slowdown would be.
来源:https://stackoverflow.com/questions/31670203/how-does-the-optimization-of-canvas-drawimage-work