Низкая производительность холста ScalaFX при рисовании изображения с таймером анимации - PullRequest
0 голосов
/ 11 октября 2018

Я планирую сделать ритмическую игру, используя ScalaFX с canvas. Когда я пытался запустить код, я обнаружил, что он потребляет много графических процессоров, а иногда падает частота кадров при 30 кадрах в секунду, даже я рисую только одно изображениена холсте без рисования анимированных заметок, танцовщиц, измерительных приборов и т. д.

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()
}

как я могу улучшить производительность или есть ли лучшие способы сделать то же самое без использования canvas?

1 Ответ

0 голосов
/ 11 октября 2018

Существует несколько факторов, которые могут замедлить частоту кадров:

  • Вы выводите частоту кадров на консоль каждый кадр.Это очень медленная операция, которая также может замедлить частоту кадров.(Вероятно, это самый большой удар по производительности.)
  • Вы вычисляете частоту кадров во время рендеринга кадров.С философской точки зрения, хороший пример принципа неопределенности Гейзенберга , поскольку, измеряя частоту кадров, вы влияете на нее и замедляете ее ...; -)
  • Вы очищаете всюcanvas каждый раз, когда вы хотите перерисовать ваше изображение, а не только ту его часть, которая занимает изображение.(Изначально это оказалось не слишком важным фактором в моей версии вашего кода, но когда я отключил ограничение скорости JavaFX *1011* - см. Обновление ниже - оказалось, чтоРазница.)

Что касается частоты кадров, в приведенной ниже версии я записываю время (в наносекундах) первого кадра и подсчитываю количество нарисованных кадров.Когда приложение выходит, оно сообщает о средней частоте кадров.Это более простое вычисление, которое не слишком сильно мешает операциям внутри обработчика анимации и является хорошим показателем общей производительности.(Временные рамки каждого кадра будут значительно различаться из-за сбора мусора, других процессов, улучшений компиляции 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)}")
  }
}

(Кстати: избегайте использования операторов var в Scala всякий раз, когда это возможно. Общее изменяемое состояние неизбежно при использовании JavaFX / ScalaFX , но Property s предоставляют гораздо лучшие механизмы для борьбы с ним.чтобы привыкнуть к использованию val объявлений элементов, если они действительно, действительно не должны быть var s. И если вам действительно нужно использовать var s, они почти всегда должны быть объявлены private для предотвращениянеконтролируемый внешний доступ и модификация.)

Сравнительный анализ Java программ - это искусство, но очевидно, что чем дольше вы будете запускать каждую версию, тем лучше будет средняя частота кадров.На моей машине (с моим собственным изображением) я достиг следующих, довольно ненаучных результатов, после запуска приложения в течение 5 минут:

  • Очистка всего холста и запись в консоль: 39.69 fps
  • Очистка всего холста без вывода на консоль: 59,85 кадров в секунду
  • Очистка только изображения без вывода на консоль: 59,86 кадров в секунду

Очистка только изображения, а не всего холстакажется, мало влияет, и немного удивил меня.Тем не менее, вывод на консоль оказал огромное влияние на частоту кадров.

Помимо использования холста, другая возможность состоит в том, чтобы просто расположить изображение в группе сцен, а затем переместить его, изменив его совместное изображение.ординаты.Код для этого приведен ниже (используя свойства для косвенного перемещения изображения):

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 кадров в секунду - почти точно так же, какиспользование холста.

В этом примере движение немного прерывистое, что вполне может быть вызвано циклами сборки мусора .Может быть, попробуйте поэкспериментировать с различными GC ?

Кстати, я перемещаю изображение в этой версии, чтобы что-то произошло.Если свойства не меняются, то я подозревал, что изображение не будет обновлено в этом кадре.Действительно, если я просто каждый раз устанавливаю свойства на одно и то же значение, частота кадров становится равной: 62,05 кадров в секунду.

Использование холста означает, что вы должны определить, что нарисовано, и как его перерисовать.Но использование графа сцены JavaFX (как в последнем примере) означает, что JavaFX заботится о том, нужно ли перерисовывать кадр.Это не имеет большого значения в данном конкретном случае, но может ускорить процесс, если между последовательными кадрами мало различий в содержании.Что-то иметь в виду.

Это достаточно быстро?Кстати, в этом конкретном примере много накладных расходов на контент.Я совсем не удивлюсь, если добавление других элементов в анимацию окажет очень мало влияния на частоту кадров.Вероятно, было бы лучше попробовать и посмотреть.Обращаемся к вам ...

(Еще одна возможность, касающаяся анимации, приведена в демонстрационной версии ColorfulCircles , которая поставляется с источниками ScalaFX .)

ОБНОВЛЕНИЕ : я упоминал об этом в комментарии, но, возможно, стоит выделить его в основном ответе: JavaFX имеет значение по умолчанию ограничение скорости из60 кадров в секунду, что также влияет на сравнительный анализ, описанный выше, и объясняет, почему ЦП и ГП не используются лучше.

Чтобы приложение могло работать с максимально возможной частотой кадров (возможно, это не очень хорошая идея, если выВы хотите максимально увеличить заряд батареи на ноутбуке или повысить общую производительность приложения), включите следующее свойство при запуске приложения:

-Djavafx.animation.fullspeed=true

Обратите внимание, что это свойство недокументировано ине поддерживается, это означает, что он может исчезнуть в будущей версии JavaFX .

Я повторно запустил тесты с этим набором свойств и наблюдал эти результатыs:

Использование холста:

  • Очистка всего холста и запись в консоль: 64,72 кадров в секунду
  • Очистка всего холста, без вывода на консоль: 144,74 кадров в секунду
  • Очистка только изображения без вывода на консоль: 159,48 кадров в секунду

Анимация графика сцены:

  • Нет вывода на консоль: 217,68 кадров в секунду

Эти результаты существенно меняют мои первоначальные выводы:

  • Рендеринг изображений - и даже их анимация - использование графа сцены намного эффективнее (на 36% лучше с точки зрения частоты кадров), чем лучшийРезультат получен при нанесении изображения на холст.Это не является неожиданным, учитывая, что график сцены оптимизирован для повышения производительности.
  • При использовании холста очистка только области, занятой изображением, имеет примерно на 10% (в этом примере) лучшую частоту кадров, чем очисткавесь холст.

См. этот ответ для получения дополнительной информации о свойстве javafx.animation.fullspeed.

...