使用动画计时器绘制图像时ScalaFX画布性能不佳

问题描述 投票:2回答:1

我打算用ScalaFX和canvas一起制作一个节奏游戏,当我尝试运行代码时,我发现它消耗了大量的GPU,有时帧速率下降到30 fps,即使我只在画布上绘制一个图像没有绘制任何动画音符,舞者,过程仪表等。

canvas

performance

以下是我的代码

import scalafx.animation.AnimationTimer
import scalafx.application.JFXApp
import scalafx.scene.Scene
import scalafx.scene.canvas.{Canvas, GraphicsContext}
import scalafx.scene.image.Image
import scalafx.scene.layout.Pane
import scalafx.scene.paint.Color.Green

object MainApp extends JFXApp{
  var MainScene: Scene = new Scene {
    fill = Green
  }
  var MainStage: JFXApp.PrimaryStage = new JFXApp.PrimaryStage {
    scene = MainScene
    height = 720
    width = 1280
  }

  var gameCanvas:Canvas = new Canvas(){
    layoutY=0
    layoutX=0
    height=720
    width=1280
  }
  var gameImage:Image = new Image("notebar.png")

  var gc:GraphicsContext = gameCanvas.graphicsContext2D
  MainScene.root = new Pane(){
    children=List(gameCanvas)
  }

  var a:Long = 0
  val animateTimer = AnimationTimer(t => {
    val nt:Long = t/1000000
    val frameRate:Long = 1000/ (if((nt-a)==0) 1 else nt-a)

    //check the frame rate 
    println(frameRate)
    a = nt
    gc.clearRect(0,0,1280,720)
    gc.drawImage(gameImage,0,0,951,160)

  })

  animateTimer.start()
}

如何在不使用画布的情况下改善性能或有更好的方法来做同样的事情?

scala javafx benchmarking scalafx
1个回答
3
投票

有几个因素可能会降低帧速率:

  • 您正在每帧输出帧速率到控制台。这是一个非常慢的操作,也可以预期降低帧速率。 (这可能是最大的表现。)
  • 您正在计算帧渲染期间的帧速率。哲学上,海森堡不确定性原理的一个很好的例子,因为通过测量帧速率,你正在干扰它并减慢它... ;-)
  • 每次要重绘图像时,您都会清除整个画布,而不是只是将其中的一部分占用到图像上。 (最初证明这不是我的代码版本中的一个重要因素,但是当我禁用JavaFX的速度限制时 - 请参阅下面的更新 - 结果发现它有很大的不同。)

关于帧速率,在下面的版本中,我记录第一帧的时间(以纳秒为单位),并计算绘制的帧数。当应用程序退出时,它会报告平均帧速率。这是一个更简单的计算,不会过多干扰动画处理程序内部的操作,并且是衡量整体性能的一个很好的指标。 (由于垃圾收集,其他进程,JIT编译改进等原因,每帧的时序会有相当大的变化。我们将尝试通过查看平均速率来跳过所有这些。)

我还更改了代码以仅清除图像占用的区域。

我还简化了你的代码,使其在使用ScalaFX时更加传统(例如,使用主类的stage成员,以及更多地使用类型推断):

import scalafx.animation.AnimationTimer
import scalafx.application.JFXApp
import scalafx.scene.Scene
import scalafx.scene.canvas.Canvas
import scalafx.scene.image.Image
import scalafx.scene.layout.Pane
import scalafx.scene.paint.Color.Green

object MainApp
extends JFXApp {

  // Nanoseconds per second.
  val NanoPerSec = 1.0e9

  // Height & width of canvas. Change in a single place.
  val canvasHeight = 720
  val canvasWidth = 1280

  // Fixed canvas size.
  val gameCanvas = new Canvas(canvasWidth, canvasHeight)

  // The image.
  val gameImage = new Image("notebar.png")
  val gc = gameCanvas.graphicsContext2D

  stage = new JFXApp.PrimaryStage {
    height = canvasHeight
    width = canvasWidth
    scene = new Scene {
      fill = Green
      root = new Pane {
        children=List(gameCanvas)
      }
    }
  }

  // Class representing an initial frame time, last frame time and number of frames
  // drawn. The first frame is not counted.
  //
  // (Ideally, this would be declared in its own source file. I'm putting it here for
  // convenience.)
  final case class FrameRate(initTime: Long, lastTime: Long = 0L, frames: Long = 0L) {
    // Convert to time in seconds
    def totalTime: Double = if(frames == 0L) 1.0 else (lastTime - initTime) / NanoPerSec

    def mean: Double = frames / totalTime
    def update(time: Long): FrameRate = copy(lastTime = time, frames = frames + 1)
  }

  // Current frame rate.
  private var frames: Option[FrameRate] = None

  val animateTimer = AnimationTimer {t =>

    // Update frame rate.
    frames = Some(frames.fold(FrameRate(t))(_.update(t)))

    // Send information to console. Comment out to determine impact on frame rate.
    //println(s"Frame rate: ${frames.fold("Undefined")(_.mean.toString)}")

    // Clear region of canvas.
    //
    // First clears entire canvas, second only image. Comment one out.
    //gc.clearRect(0, 0, canvasWidth, canvasHeight)
    gc.clearRect(0, 0, gameImage.width.value, gameImage.height.value)

    // Redraw the image. This version doesn't need to know the size of the image.
    gc.drawImage(gameImage, 0, 0)
  }

  animateTimer.start()

  // When the application terminates, output the mean frame rate.
  override def stopApp(): Unit = {
    println(s"Mean frame rate: ${frames.fold("Undefined")(_.mean.toString)}")
  }
}

(顺便说一下:尽可能避免在Scala中使用var语句。使用JavaFX / ScalaFX时共享可变状态是不可避免的,但Propertys提供了更好的处理它的机制。尝试养成使用val元素声明的习惯,除非他们真的,确实需要是vars。如果你确实需要使用vars,它们应该几乎总是被声明为private以防止不受控制的外部访问和修改。)

对Java程序进行基准测试是一种艺术形式,但显然,每个版本运行的时间越长,平均帧速率就越高。在我的机器上(带有我自己的图像)我在运行应用程序5分钟后获得了以下相当不科学的结果:

  • 清除整个画布并写入控制台:39.69 fps
  • 清除整个画布,没有输出到控制台:59.85 fps
  • 仅清除图像,没有输出到控制台:59.86 fps

仅清除图像,而不是整个画布看起来效果不大,让我感到惊讶。但是,输出到控制台对帧速率有很大影响。

除了使用画布之外,另一种可能性是简单地将图像定位在场景组中,然后通过改变其坐标来移动它。下面的代码如下(使用属性间接移动图像):

import scalafx.animation.AnimationTimer
import scalafx.application.JFXApp
import scalafx.beans.property.DoubleProperty
import scalafx.scene.{Group, Scene}
import scalafx.scene.image.ImageView
import scalafx.scene.layout.Pane
import scalafx.scene.paint.Color.Green
import scalafx.scene.shape.Rectangle

object MainApp
extends JFXApp {

  // Height & width of app. Change in a single place.
  val canvasHeight = 720
  val canvasWidth = 1280

  // Nanoseconds per second.
  val NanoPerSec = 1.0e9

  // Center of the circle about which the image will move.
  val cX = 200.0
  val cY = 200.0

  // Radius about which we'll move the image.
  val radius = 100.0

  // Properties for positioning the image (might be initial jump).
  val imX = DoubleProperty(cX + radius)
  val imY = DoubleProperty(cY)

  // Image view. It's co-ordinates are bound to the above properties. As the properties
  // change, so does the image's position.
  val imageView = new ImageView("notebar.png") {
    x <== imX // Bind to property
    y <== imY // Bind to property
  }

  stage = new JFXApp.PrimaryStage {
    height = canvasHeight
    width = canvasWidth
    scene = new Scene {thisScene => // thisScene is a self reference
      fill = Green
      root = new Group {
        children=Seq(
          new Rectangle { // Background
            width <== thisScene.width // Bind to scene/stage width
            height <== thisScene.height // Bind to scene/stage height
            fill = Green
          },
          imageView
        )
      }
    }
  }

  // Class representing an initial frame time, last frame time and number of frames
  // drawn. The first frame is not counted.
  //
  // (Ideally, this would be declared in its own source file. I'm putting it here for
  // convenience.)
  final case class FrameRate(initTime: Long, lastTime: Long = 0L, frames: Long = 0L) {
    // Convert to time in seconds
    def totalTime: Double = if(frames == 0L) 1.0 else (lastTime - initTime) / NanoPerSec

    def mean: Double = frames / totalTime
    def update(time: Long) = copy(lastTime = time, frames = frames + 1)
  }

  // Current frame rate.
  var frames: Option[FrameRate] = None

  val animateTimer = AnimationTimer {t =>

    // Update frame rate.
    frames = Some(frames.fold(FrameRate(t))(_.update(t)))

    // Change the position of the image. We'll make the image move around a circle
    // clockwise, doing 1 revolution every 10 seconds. The center of the circle will be
    // (cX, cY). The angle is therefore the modulus of the time in seconds divided by 10
    // as a proportion of 2 pi radians.
    val angle = (frames.get.totalTime % 10.0) * 2.0 * Math.PI / 10.0

    // Update X and Y co-ordinates related to the center and angle.
    imX.value = cX + radius * Math.cos(angle)
    imY.value = cY + radius * Math.sin(angle)
  }

  animateTimer.start()

  // When the application terminates, output the mean frame rate.
  override def stopApp(): Unit = {
    println(s"Mean frame rate: ${frames.fold("Undefined")(_.mean.toString)}")
  }
}

这使我在运行5分钟后产生59.86 fps的平均帧速率 - 几乎与使用画布完全相同。

在这个例子中,运动有点不稳定,这很可能是由垃圾收集周期引起的。也许尝试尝试不同的GC?

顺便说一下,我在这个版本中移动图像以强迫某些事情发生。如果属性没有改变,那么我怀疑图像不会更新那个帧。实际上,如果我每次都将属性设置为相同的值,则帧速率变为:62.05 fps。

使用画布意味着您必须确定绘制的内容以及如何重绘它。但是使用JavaFX场景图(如上例所示)意味着JavaFX负责确定是否需要重绘帧。它在这种特殊情况下并没有太大的区别,但如果连续帧之间的内容差异很小,它可能会加快速度。要记住一些事情。

这够快吗?顺便说一下,在这个特定的例子中,与内容相关的开销很大。如果在动画中添加其他元素对帧速率的影响非常小,我也不会感到惊讶。最好尝试一下,看看。给你...

(关于动画的另一种可能性,请参阅ScalaFX源附带的ColorfulCircles演示。)

更新:我在评论中提到过这一点,但在主要答案中可能值得强调:JavaFX的默认速度限制为60 fps,这也影响了上面的基准测试 - 这也解释了为什么没有更好地利用CPU和GPU。

要使您的应用程序以尽可能高的帧速率运行(如果您希望在笔记本电脑上最大限度地提高电池电量,或者为了提高整体应用程序性能,可能不是一个好主意),请在运行应用程序时启用以下属性:

-Djavafx.animation.fullspeed=true

请注意,此属性未记录且不受支持,这意味着它可能会在JavaFX的未来版本中消失。

我用这个属性集重新运行了基准测试,并观察了这些结果:

使用画布:

  • 清除整个画布并写入控制台:64.72 fps
  • 清除整个画布,没有输出到控制台:144.74 fps
  • 仅清除图像,没有输出到控制台:159.48 fps

动画场景图:

  • 没有输出到控制台:217.68 fps

这些结果显着改变了我原来的结论:

  • 使用场景图渲染图像(甚至是动画图像)比在画布上绘制图像时获得的最佳结果更有效(帧速率提高36%)。考虑到优化场景图以提高性能,这并不意外。
  • 如果使用画布,仅清除图像占用的区域大约10%(对于此示例)比清除整个画布更好的帧速率。

有关this answer财产的更多详情,请参阅javafx.animation.fullspeed

© www.soinside.com 2019 - 2024. All rights reserved.