我正在使用 JavaFX 制作一個非常簡單的影片。我的目標只是讓一個矩形在視窗中平滑移動。
我正在嘗試使用 來實作這一點AnimationTimer,這似乎適合該任務。我嘗試了不同的渲染方式,例如 a Rectanglein anAnchorPane或簡單地繪制到 a 上Canvas,但最終它總是歸結為相同的東西,具有相同的結果。
我基本上存盤我的矩形的位置,并在每一幀對其應用移動速率。
事實上,當我在我的handle方法中使用恒定移動速率時AnimationTimer,影片非常流暢。但是,這種技術存在兩個問題:
- 幀速率似乎與平臺有關,沒有簡單的方法來控制它。所以影片會在不同的機器上呈現不同的效果。
- 幀率有時會發生變化,例如在調整視窗大小時,它有時會下降一半,有時甚至會加倍,這會相應地改變影片速度。
所以我嘗試通過使用AnimationTimer.handle(long now). 它解決了不一致的問題,但影片現在很緊張!每秒幾次,矩形似乎會向前“跳躍”幾個像素,然后停頓一兩幀以恢復其預期位置。隨著速度的提高,它變得越來越明顯。
這是相關的代碼(簡化):
AnimationTimer anim = new AnimationTimer() {
private long lastRun = 0;
@Override
public void handle(long now) {
//Ignore first frame as I'm not sure of the timing here
if (lastRun == 0) {
lastRun = now;
return;
}
//Now we've got a reference, so let's animate
double elapsed = (now - lastRun) / 1e9; //Convert to seconds
//Update position according to speed
position = position.add(speed.multiply(elapsed)); //Apply speed in pixels/second
lastRun = now; //Store current time for next loop
draw();
}
};
我試圖記錄時間差異、幀速率和位置。嘗試了一些不同的修復,使我的代碼總是更復雜但沒有任何結果。
根據您的評論編輯 2022-03-15(謝謝)
I've tried this on my usual computer (Win 10, Xeon processor, 2 Geforce 1050Ti GPUs), and also on a Microsoft Surface Go 3 tablet under Windows 11. I've tried it using Java 17.0.1 (Temurin) and JavaFX 17.0.1, as well as JDK 8u211 with the same results.
Using JVM argument -Djavafx.animation.pulse=10 has no effect whatsoever other than showing "Setting PULSE_DURATION to 10 hz" in stderr. -Djavafx.animation.framerate=10 doesn't do a thing.
End of edit
I can't figure out what I'm doing wrong here. Can you please help me out ?
Here's my entire code : (Edited on 2022-03-15 to include FPS-meter)
import java.math.BigDecimal;
import java.math.RoundingMode;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Paint;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;
import javafx.stage.Stage;
public class TestFxCanvas2 extends Application {
// Set your panel size here
private static final int FRAME_WIDTH = 800;
private static final int FRAME_HEIGHT = 800;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
BorderPane root = new BorderPane();
MyAnimation2 myAnimation = new MyAnimation2();
myAnimation.widthProperty().bind(root.widthProperty());
myAnimation.heightProperty().bind(root.heightProperty());
root.setCenter(myAnimation);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setWidth(FRAME_WIDTH);
stage.setHeight(FRAME_HEIGHT);
stage.show();
}
}
class MyAnimation2 extends Canvas {
private static final double SPEED = 500; // Speed value to be applied in either direction in px/s
private static final Point2D RECT_DIMS = new Point2D(50, 50); // rectangle size
// Canvas painting colors
private static final Paint BLACK = Paint.valueOf("black");
private static final Paint RED = Paint.valueOf("red");
private static final Paint GREEN = Paint.valueOf("ForestGreen");
// Defines rectangle start position
private Point2D recPos = new Point2D(0, 300);
// Stores previous position
private Point2D oldPos = new Point2D(0, 0);
// Current speed
private Point2D speed = new Point2D(SPEED, 0);
public MyAnimation2() {
AnimationTimer anim = new AnimationTimer() {
private long lastRun = 0;
long[] frameTimes = new long[10];
long frameCount = 0;
@Override
public void handle(long now) {
// Measure FPS
BigDecimal fps = null;
int frameIndex = (int) (frameCount % frameTimes.length);
frameTimes[frameIndex] = now;
if (frameCount > frameTimes.length) {
int prev = (int) ((frameCount 1) % frameTimes.length);
long delta = now - frameTimes[prev];
double fr = 1e9 / (delta / frameTimes.length);
fps = new BigDecimal(fr).setScale(2, RoundingMode.HALF_UP);
}
frameCount ;
// Skip first frame but record its timing
if (lastRun == 0) {
lastRun = now;
return;
}
// Animate
double elapsed = (now - lastRun) / 1e9;
// Reverse when hitting borders
if (hitsBorders())
speed = speed.multiply(-1.);
// Update position according to speed
oldPos = recPos;
recPos = recPos.add(speed.multiply(elapsed));
lastRun = now;
draw(oldPos, recPos, fps);
}
};
// Start
anim.start();
}
private void draw(Point2D oldPos, Point2D recPos, BigDecimal fps) {
GraphicsContext gfx = this.getGraphicsContext2D();
// Clear and draw border
gfx.setStroke(BLACK);
gfx.setLineWidth(1);
gfx.clearRect(0, 0, getWidth(), getHeight());
gfx.strokeRect(0, 0, getWidth(), getHeight());
// Draw moving shape
gfx.setFill(RED);
gfx.fillRect(recPos.getX(), recPos.getY(), RECT_DIMS.getX(), RECT_DIMS.getY());
// Draw FPS meter
String fpsText = fps == null ? "FPS" : fps.toString();
gfx.setTextAlign(TextAlignment.RIGHT);
gfx.setFill(GREEN);
gfx.setFont(Font.font(24));
gfx.setTextBaseline(VPos.TOP);
gfx.fillText(fpsText, getWidth() - 5, 5);
}
private boolean hitsBorders() {
Rectangle2D frame = new Rectangle2D(0, 0, getWidth(), getHeight());
Rectangle2D rect = new Rectangle2D(recPos.getX(), recPos.getY(), RECT_DIMS.getX(), RECT_DIMS.getY());
if (speed.getX() < 0 && rect.getMinX() < frame.getMinX())
return true;
else if (speed.getX() > 0 && rect.getMaxX() > frame.getMaxX())
return true;
else if (speed.getY() < 0 && rect.getMinY() < frame.getMinY())
return true;
else if (speed.getY() > 0 && rect.getMaxY() > frame.getMaxY())
return true;
return false;
}
}
Addition after testing same program in JavaScript
This JavaScript version runs smoothly on my devices
在此處查看比較兩個版本的視頻(頂部為 JavaScript,底部為 JavaFX):https ://www.ahpc??-services.com/dl/20220315_150431_edit1.mp4
const RED = 'red';
const GREEN = 'ForestGreen';
const BLACK = 'black';
window.addEventListener('DOMContentLoaded', e => {
const animArea = document.querySelector('#anim-area');
const ctx = animArea.getContext('2d');
const cWidth = animArea.clientWidth;
const cHeight = animArea.clientHeight;
adjustCanvasSize();
window.addEventListener('resize', adjustCanvasSize);
const rect = {
x: 0,
y: 50,
width: 50,
height: 50
}
const speed = {
x: 500,
y: 0
}
const frameTiming = {
frameCount: 0,
frameTimes: Array(10),
lastRun: 0,
}
requestAnimationFrame(animate);
function animate() {
const now = Date.now();
requestAnimationFrame(animate);
//Count FPS
let fps;
const frameIndex = frameTiming.frameCount % frameTiming.frameTimes.length;
frameTiming.frameTimes[frameIndex] = now;
if (frameTiming.frameCount > frameTiming.frameTimes.length) {
const prev = (frameTiming.frameCount 1) % frameTiming.frameTimes.length;
const delta = now - frameTiming.frameTimes[prev];
fps = Math.round(100 * 1000 * frameTiming.frameTimes.length / delta) / 100;
}
frameTiming.frameCount ;
//Ignore first frame
if (frameTiming.lastRun == 0) {
frameTiming.lastRun = now;
return;
}
//Animate
const elapsed = (now - frameTiming.lastRun) / 1e3;
// Reverse when hitting borders
if (hitsBorders()) {
speed.x *= -1;
speed.y *= -1;
}
// Update position according to speed
const oldRect = Object.assign({}, rect);
rect.x = speed.x * elapsed;
rect.y = speed.y * elapsed;
frameTiming.lastRun = now;
draw();
function draw() {
// Clear and draw border
ctx.clearRect(0, 0, animArea.width, animArea.height);
ctx.strokeStyle = BLACK;
ctx.strokeRect(0, 0, animArea.width, animArea.height);
// Draw moving shape
ctx.fillStyle = RED;
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
// Draw FPS meter
const fpsText = fps == undefined ? "FPS" : `${fps}`;
ctx.textAlign = 'right';
ctx.fillStyle = GREEN;
ctx.font = "24px sans-serif";
ctx.textBaseline = 'top';
ctx.fillText(fpsText, animArea.width - 5, 5);
}
function hitsBorders() {
if (speed.x < 0 && rect.x < 0)
return true;
else if (speed.x > 0 && rect.x rect.width > animArea.width)
return true;
else if (speed.y < 0 && rect.y < 0)
return true;
else if (speed.y > 0 && rect.y rect.height > animArea.height)
return true;
return false;
}
}
function adjustCanvasSize() {
if (window.innerWidth < cWidth 30)
animArea.style.width = (window.innerWidth - 30) "px";
else
animArea.style.width = "";
if (window.innerHeight < cHeight 30)
animArea.style.height = (window.innerHeight - 30) "px";
else
animArea.style.height = "";
animArea.width = animArea.clientWidth;
animArea.height = animArea.clientHeight;
}
});
html,
body {
margin: 0;
padding: 0;
}
#anim-area {
width: 800px;
height: 800px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Moving square</title>
</head>
<body>
<div class="content-wrapper">
<canvas id="anim-area"></canvas>
</div>
</body>
</html>
uj5u.com熱心網友回復:
我自己想通了。事實證明,JavaFX 沒有考慮顯示幕的實際重繪 率。它以大約 67Hz 的平均頻率呼叫AnimationTimer.handle(盡管變化很大),而典型的顯示幕重繪 頻率約為 60Hz。
這會導致某些幀延遲渲染(呼叫與螢屏顯示幀有很大的偏移),并且某些幀會以各種長度報告,而螢屏實際上會以恒定速率顯示它們,因此不一致我觀察到的運動。
我可以通過測量螢屏的重繪 率并根據要顯示的下一幀計算我的矩形的移動率來補償這一點(我不知道確切的時間,但恒定的偏移量就可以了)。
所以這里是代碼部分:
1.獲取螢屏重繪 率
stage.setOnShown(e -> {
Screen screen = Screen.getScreensForRectangle(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight())
.get(0);
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice d = ge.getScreenDevices()[Screen.getScreens().indexOf(screen)];
int r = d.getDisplayMode().getRefreshRate();
System.out.println("Screen refresh rate : " r);
//Calculate frame duration in nanoseconds
this.frameNs = 1_000_000_000L / refreshRate; //Store it as it better suits you
});
請注意,此方法給出了int重繪 率,而螢屏重繪 率通常不是整數(我的當前是 60.008 Hz)。但這似乎是一個足夠好的近似值,從結果來看
它還依賴于awt我寧愿擁有純 JavaFX 解決方案的地方,并且我假設兩個系統以相同的順序報告螢屏,這遠不能保證:所以在生產中謹慎使用它!
2. 更改影片回圈以考慮此重繪 率
AnimationTimer anim = new AnimationTimer() {
private long lastRun = 0;
@Override
public void handle(long now) {
// Skip first frame but record its timing
if (lastRun == 0) {
lastRun = now;
return;
}
// If we had 2 JFX frames for 1 screen frame, save a cycle
if (now <= lastRun)
return;
// Calculate remaining time until next screen frame (next multiple of frameNs)
long rest = now % frameNs;
long nextFrame = now;
if (rest != 0) //Fix timing to next screen frame
nextFrame = frameNs - rest;
// Animate
double elapsed = (nextFrame - lastRun) / 1e9;
// Reverse when hitting borders
if (hitsBorders())
speed = speed.multiply(-1.);
// Update position according to speed
oldPos = recPos;
recPos = recPos.add(speed.multiply(elapsed));
log.println(String.format("%d\t: %d", frameCount, (now - lastRun) / 1_000_000));
lastRun = nextFrame;
draw();
}
};
通過這些改動,影片運行流暢
完整代碼(改進了一點)
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Paint;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
public class TestFxAnimationCanvas extends Application {
// Set your panel size here
private static final int FRAME_WIDTH = 1024;
private static final int FRAME_HEIGHT = 480;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
BorderPane root = new BorderPane();
SmootherAnimation myAnimation = new SmootherAnimation();
myAnimation.widthProperty().bind(root.widthProperty());
myAnimation.heightProperty().bind(root.heightProperty());
root.setCenter(myAnimation);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setWidth(FRAME_WIDTH);
stage.setHeight(FRAME_HEIGHT);
// Get screen refresh rate and apply it to animation
stage.addEventHandler(WindowEvent.WINDOW_SHOWN, e -> {
Screen screen = Screen.getScreensForRectangle(
stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight()
).get(0);
if (screen == null)
return;
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
// /!\ Does ge.getScreenDevices really return same order as Screen.getScreens?
GraphicsDevice d = ge.getScreenDevices()[Screen.getScreens().indexOf(screen)];
int r = d.getDisplayMode().getRefreshRate(); // /!\ r is an int whereas screen refresh rate is often not an integer
myAnimation.setRefreshRate(r);
//TODO: re-assess when window is moved to other screen
});
stage.show();
}
}
class SmootherAnimation extends Canvas {
private static final double SPEED = 500; // Speed value to be applied in either direction in px/s
private static final Point2D RECT_DIMS = new Point2D(50, 50); // rectangle size
// Canvas painting colors
private static final Paint BLACK = Paint.valueOf("black");
private static final Paint RED = Paint.valueOf("red");
private static final Paint GREEN = Paint.valueOf("ForestGreen");
private static final Paint BLUE = Paint.valueOf("SteelBlue");
// Defines rectangle start position, stores current position
private Point2D recPos = new Point2D(0, 50);
// Defines initial speed, stores current speed
private Point2D speed = new Point2D(SPEED, 0);
//Frame rate measurement
private long frameCount = 0;
private BigDecimal fps = null;
long[] frameTimes = new long[120]; //length defines number of rendered frames to average over
//Frame duration in nanoseconds according to screen refresh rate
private long frameNs = 1_000_000_000L / 60; //Default to 60Hz
public SmootherAnimation() throws IOException {
AnimationTimer anim = new AnimationTimer() {
private long previousFrame = 0;
@Override
public void handle(long now) {
// Skip first frame but record its timing
if (previousFrame == 0) {
previousFrame = now;
frameTimes[0] = now;
frameCount ;
return;
}
// If we had 2 JFX frames for 1 screen frame, save a cycle by skipping render
if (now <= previousFrame)
return;
// Measure FPS
int frameIndex = (int) (frameCount % frameTimes.length);
frameTimes[frameIndex] = now;
if (frameCount > frameTimes.length) {
int prev = (int) ((frameCount 1) % frameTimes.length);
long delta = now - frameTimes[prev];
double fr = 1e9 / (delta / frameTimes.length);
fps = new BigDecimal(fr).setScale(2, RoundingMode.HALF_UP);
}
frameCount ;
// Calculate remaining time until next screen frame (next multiple of frameNs)
long rest = now % frameNs;
long nextFrame = now;
if (rest != 0) //Fix timing to next screen frame
nextFrame = frameNs - rest;
// Animate
updateWorld(previousFrame, nextFrame);
previousFrame = nextFrame; //Saving last execution
draw();
}
};
// Start
anim.start();
}
/**
* Save frame interval in nanoseconds given passed refresh rate
* @param refreshRate in Hz
*/
public void setRefreshRate(int refreshRate) {
this.frameNs = 1_000_000_000L / refreshRate;
}
/**
* Perform animation (calculate object positions)
* @param previousFrame previous animation frame execution time in ns
* @param nextFrame next animation frame execution time in ns
*/
private void updateWorld(long previousFrame, long nextFrame) {
double elapsed = (nextFrame - previousFrame) / 1e9; //Interval in seconds
// Reverse when hitting borders
if ( rectHitsBorders( recPos.getX(), recPos.getY(),
RECT_DIMS.getX(), RECT_DIMS.getY(),
speed.getX(), speed.getY()) ) {
speed = speed.multiply(-1.);
}
// Update position according to speed
recPos = recPos.add(speed.multiply(elapsed));
}
/**
* Draw world onto canvas. Also display calculated frame rate and frame count
*/
private void draw() {
GraphicsContext gfx = this.getGraphicsContext2D();
// Clear and draw border
gfx.setStroke(BLACK);
gfx.setLineWidth(1);
gfx.clearRect(0, 0, getWidth(), getHeight());
gfx.strokeRect(0, 0, getWidth(), getHeight());
// Draw moving shape
gfx.setFill(RED);
gfx.fillRect(recPos.getX(), recPos.getY(), RECT_DIMS.getX(), RECT_DIMS.getY());
// Draw FPS meter
String fpsText = fps == null ? "FPS" : fps.toString();
gfx.setTextAlign(TextAlignment.RIGHT);
gfx.setTextBaseline(VPos.TOP);
gfx.setFill(GREEN);
gfx.setFont(Font.font(24));
gfx.fillText(fpsText, getWidth() - 5, 5);
// Draw frame counter
gfx.setTextAlign(TextAlignment.LEFT);
gfx.setFill(BLUE);
gfx.fillText("" frameCount, 5, 5);
}
/**
* Tells whether moving rectangle is hitting canvas borders
* @param x considered rectangle horizontal coordinate (top-left from left)
* @param y considered rectangle vertical coordinate (top-left from top)
* @param width considered rectangle width
* @param height considered rectangle height
* @param speedX speed component in x direction
* @param speedY speed component in y direction
* @return true if a canvas border is crossed in the direction of movement
*/
private boolean rectHitsBorders(double x, double y, double width, double height, double speedX, double speedY) {
Rectangle2D frame = new Rectangle2D(0, 0, getWidth(), getHeight());
Rectangle2D rect = new Rectangle2D(x, y, width, height);
if (speedX < 0 && rect.getMinX() < frame.getMinX())
return true;
else if (speedX > 0 && rect.getMaxX() > frame.getMaxX())
return true;
else if (speedY < 0 && rect.getMinY() < frame.getMinY())
return true;
else if (speedY > 0 && rect.getMaxY() > frame.getMaxY())
return true;
return false;
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/449632.html
