Является ли Graphics.DrawImage слишком медленным для больших изображений?

В настоящее время я работаю над игрой, и хочу иметь главное меню с фоновым изображением.

Однако метод Graphics.DrawImage() очень медленный. Я сделал некоторые измерения. Предположим, что MenuBackground - это мое изображение ресурса с разрешением 800 x 1200 пикселей. Я нарисую его на другой 800 x 1200 bitmap (сначала я делаю все для растрового изображения буфера, затем масштабирую его и, наконец, рисую на экране, - как я имею дело с возможностью разрешения нескольких игроков, но это не должно влиять это в любом случае, см. следующий параграф).

Итак, я измерил следующий код:

Stopwatch SW = new Stopwatch();
SW.Start();

// First let render background image into original-sized bitmap:

OriginalRenderGraphics.DrawImage(Properties.Resources.MenuBackground,
   new Rectangle(0, 0, Globals.OriginalScreenWidth, Globals.OriginalScreenHeight));

SW.Stop();
System.Windows.Forms.MessageBox.Show(SW.ElapsedMilliseconds + " milliseconds");

Результат меня удивляет - Stopwatch меряет что-то между 40 - 50 milliseconds. И поскольку фоновое изображение - это не единственное, что нужно рисовать, для отображения всего меню требуется более 100 мс, что подразумевает наблюдаемое отставание.

Я попытался привлечь его к объекту Graphics, заданному событием Paint, но результат был 30 - 40 milliseconds - не сильно изменился.

Итак, означает ли это, что Graphics.DrawImage() непригодно для рисования больших изображений? Если да, то что мне делать, чтобы улучшить производительность моей игры?

Ответ 1

Да, это слишком медленно.

Я столкнулся с этой проблемой несколько лет назад при разработке Paint.NET(с самого начала, на самом деле, и это было довольно неприятно!). Производительность рендеринга была ужасной, так как она всегда была пропорциональна размеру растрового изображения, а не размеру области, которую ему было предложено перерисовать. То есть, частота кадров снизилась по мере увеличения размера растрового изображения, и частота кадров никогда не увеличивалась, так как размер области недействительной/перерисовывающей области снижался при реализации OnPaint() и вызывал Graphics.DrawImage(). Небольшое растровое изображение, скажем, 800x600, всегда работало нормально, но большие изображения (например, 2400x1800) были очень медленными. (В любом случае вы можете предположить, что в предыдущем абзаце ничего не происходило, например, масштабирование с помощью какого-то дорогого бикубического фильтра, что отрицательно сказалось бы на производительности.)

Можно заставить WinForms использовать GDI вместо GDI + и избежать даже создания объекта Graphics за кулисами, после чего вы можете наложить на него еще один инструментарий рендеринга (например, Direct2D). Однако это не просто. Я делаю это в Paint.NET, и вы можете увидеть, что требуется, используя что-то вроде Reflector в классе с именем GdiPaintControl в DLL SystemLayer, но за то, что вы делаете, я считаю его последним средством.

Однако размер растрового изображения, который вы используете (800x1200), должен по-прежнему работать достаточно хорошо в GDI +, не прибегая к передовому взаимодействию, если вы не нацеливаете что-то столь же низко, как Pentium II с частотой 300 МГц. Вот несколько советов, которые могут помочь:

  • Если вы используете непрозрачный растровый рисунок (нет альфа/прозрачности) в вызове Graphics.DrawImage(), и особенно если это 32-битная растровая карта с альфа-каналом (но вы знаете, что это непрозрачно, или вам все равно), затем установите Graphics.CompositingMode в CompositingMode.SourceCopy перед вызовом DrawImage() (после этого обязательно верните его к исходному значению, иначе обычные примитивы рисования будут выглядеть очень уродливыми). Это пропускает много дополнительного смешивания по каждому пикселю.
  • Убедитесь, что Graphics.InterpolationMode не настроено на что-то вроде InterpolationMode.HighQualityBicubic. Использование NearestNeighbor будет самым быстрым, хотя, если есть растягивание, оно может выглядеть не очень хорошо (если оно не растягивается ровно в 2x, 3x, 4x и т.д.) Bilinear обычно является хорошим компромиссом. Вы не должны использовать ничего, кроме NearestNeighbor, если размер растрового изображения соответствует области, в которую вы рисуете, в пикселях.
  • Всегда рисуйте объект Graphics, указанный вам в OnPaint().
  • Всегда делайте рисунок в OnPaint. Если вам нужно перерисовать область, вызовите Invalidate(). Если вам нужно, чтобы рисунок появился прямо сейчас, вызовите Update() после Invalidate(). Это разумный подход, поскольку сообщения WM_PAINT (что приводит к вызову OnPaint()) являются сообщениями с низким приоритетом. Любая другая обработка оконным менеджером будет выполнена в первую очередь, и, таким образом, вы можете столкнуться с большим количеством пропусков кадров и сцепления в противном случае.
  • Использование System.Windows.Forms.Timer в качестве таймера частоты кадров/тика не будет работать очень хорошо. Они реализуются с использованием Win32 SetTimer и приводят к сообщениям WM_TIMER, которые затем приводят к возникновению события Timer.Tick, а WM_TIMER - другое сообщение с низким приоритетом, которое отправляется только тогда, когда очередь сообщений пуста. Вам лучше использовать System.Threading.Timer, а затем использовать Control.Invoke() (чтобы убедиться, что вы находитесь в правильном потоке!) И вызываете Control.Update().
  • В общем, не используйте Control.CreateGraphics(). (следствие "всегда рисовать в OnPaint()" и "всегда использовать Graphics, предоставленный вам OnPaint() ')
  • Я не рекомендую использовать обработчик событий Paint. Вместо этого внесите OnPaint() в класс, который вы пишете, который должен быть получен из Control. Вывод из другого класса, например. PictureBox или UserControl, либо не добавит вам никакого значения, либо добавит дополнительные служебные данные. (BTW PictureBox часто неправильно понимается. Вероятно, вы почти никогда не захотите его использовать.)

Надеюсь, что это поможет.

Ответ 2

GDI +, вероятно, не лучший выбор для игр. DirectX/XNA или OpenGL должны быть предпочтительнее, поскольку они используют любое графическое ускорение и очень быстрые.

Ответ 3

GDI + не является демоном скорости любыми способами. Любые серьезные манипуляции с изображениями обычно должны входить в ту сторону вещей (вызовы pInvoke и/или манипуляции с помощью указателя, полученного вызовом LockBits.)

Вы изучали XNA/DirectX/OpenGL?. Это рамки, предназначенные для разработки игр, и будут на порядок более эффективными и гибкими, чем использование инфраструктуры пользовательского интерфейса, например WinForms или WPF. Все библиотеки предлагают привязки С#.

Вы можете pInvoke в собственный код, используя такие функции, как BitBlt, но есть накладные расходы, связанные с пересечением границы управляемого кода.

Ответ 4

Хотя это древний вопрос, а WinForms - это древний фреймворк, я хотел бы поделиться тем, что я случайно обнаружил: рисование растрового изображения в BufferedGraphics и его последующее отображение в графическом контексте, предоставляемом OnPaint, намного быстрее, чем рисование растрового изображения непосредственно в графический контекст OnPaint - по крайней мере, на моем компьютере с Windows 10.

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

Поэтому создайте BufferedGraphics в конструкторе элемента управления, который будет содержать растровое изображение (в моем случае я хотел нарисовать полноэкранное растровое изображение 1920x1080):

        using (Graphics graphics = CreateGraphics())
        {
            graphicsBuffer = BufferedGraphicsManager.Current.Allocate(graphics, new Rectangle(0,0,Screen.PrimaryScreen.Bounds.Width,Screen.PrimaryScreen.Bounds.Height));
        }

и использовать его в OnPaint (при отмене OnPaintBackground)

    protected override void OnPaintBackground(PaintEventArgs e) {/* just rely on the bitmap to fill the screen */}

    protected override void OnPaint(PaintEventArgs e)
    {   
        Graphics g = graphicsBuffer.Graphics;

        g.DrawImage(someBitmap,0,0,bitmap.Width, bitmap.Height);

        graphicsBuffer.Render(e.Graphics);
    }

вместо наивного определения

    protected override void OnPaintBackground(PaintEventArgs e) {/* just rely on the bitmap to fill the screen */}

    protected override void OnPaint(PaintEventArgs e)
    {   
        e.Graphics.DrawImage(someBitmap,0,0,bitmap.Width, bitmap.Height);
    }

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

enter image description here