DOBON.NET DOBON.NETプログラミング掲示板過去ログ

「ぐにゃぐにゃ」なエフェクトに時間がかかる

環境/言語:[Visual C++ (マネージ) / Windows XP / .NET 2.0]
分類:[.NET]

いつもお世話になっています。
現在、ノベルゲームエンジンもどきを作成していまして、
画像切り替えのエフェクトに「ぐにゃぐにゃ」を導入したいと思いました。
インターネットで検索しましたが、あまり見つからず、唯一見つけたのが下のサイトのものでした。
http://takabosoft.com/20000918163126.html

この情報も古いものでしたが、頑張って自己流に書き直したコードが下のものです。

int main(array<System::String ^> ^args)
{
// コントロールが作成される前に、Windows XP ビジュアル効果を有効にします
Application::EnableVisualStyles();
Application::SetCompatibleTextRenderingDefault(false);

// メイン ウィンドウを作成して、実行します
Form1^ frm = gcnew Form1();
frm->Show();

// ここからがぐにゃぐにゃ

// 初期化
int count = 0;
float width=5.f,width_max=50,width_add=0.15f;
Bitmap^ bmp = gcnew Bitmap("画像.bmp");
Bitmap^ newbmp = gcnew Bitmap(bmp->Width,bmp->Height);
Graphics^ g = Graphics::FromImage(newbmp);
while (frm->Created) {
g->FillRectangle(Brushes::Black,0,0,bmp->Width,bmp->Height);
count++;
//ゆれの大きさを時間によって変化させる
if (width_add > 0) {
if((width += width_add) > width_max) {
width_add =- width_add;
}
}
else {
if((width += width_add) < 0) {
width=0;
width_add =- width_add;
}
}
//ゆらす
for(int i = 0; i < bmp->Height; i++) {
Rectangle rect = Rectangle(0,i,bmp->Width,1);
g->DrawImage(bmp,Convert::ToInt32(Math::Sin((count+i)/10.f)*width),i,rect,GraphicsUnit::Pixel);
}
frm->BackgroundImage = newbmp;
frm->Refresh();
}
return 0;
}

これで一応起動すると、「画像.bmp」がぐにゃぐにゃするようになりましたが、
サンプルコードのものとは挙動が違いますし、描画に時間がかかりすぎます。
挙動が違う件に関しては式をいじれば直るのかもしれませんが、
描画に時間がかかりすぎるのは問題なので、どこを直せばいいでしょうか?
どなたかわかる方、回答をお願いします。
自己レスですが、サンプルコードに含まれていた画像で実行したところ、
スムーズに描画されました。
しかし、ゲームエンジンではせめて800*600の画像を表示したいので、
やはり描画速度を向上させることができると幸いです。
masa さんこんにちはおのでらです。

コードの複雑さとの兼ね合いにもなるかもしれませんが、DrawImage メソッドなどのような GDI+ 関連のメソッドは使用せずにバッファ(byte の配列)レベルで処理を行ったほうが一般的には高速になります。また、C++/CLI を使っているようなので、アンマネージコードで処理するのも手かもしれませんね。

ただ、どうしても市販ゲーム並みに高速化したいのであれば GDI ではなく DirectX や XNA など GPU で処理が可能なライブラリを使用するのが必須です。もちろん作るものや求めるパフォーマンスによって選択肢は変わってきますので masa さんのほうで考えてみてください。
おのでらさん、こんにちは。
回答ありがとうございます。そうですか...
とりあえず縦幅1ごとにずらしている処理を8ぐらいにすると、
640*480ぐらいの画像はスムーズに表示できましたが、
800*600はきつかったです(汗
そうなると、DirectXのほうがいいのでしょうかね?

Managed Direct X で音楽再生のコードを書いていますが、
画像の処理もManaged Direct Xでできるんでしょうか?
> とりあえず縦幅1ごとにずらしている処理を8ぐらいにすると、
> 640*480ぐらいの画像はスムーズに表示できましたが、
> 800*600はきつかったです(汗

コードあんまり読んでないで回答してしまったのですが、最近の PC であれば 800x600 ぐらいなら大して遅くならないような気がします(マシンにもよりますが…)。

まず、コードの

> frm->BackgroundImage = newbmp;
> frm->Refresh();

の部分は遅くなるような気がします。BackgroundImage プロパティにイメージをセットした時点でイメージは再描画されるはずなのですが、さらに Refresh をかけているので2重の呼び出しになっています。たぶんこうしている理由は、コントロール(フォーム)が再描画したくても while で無限ループしてしまっているので Refresh で強制的に再描画させるしかないからでしょう。UI に制御権を返すためにかならず while ループ内に System::Windows::Forms::Application::DoEvents() メソッドをいれるようにしましょう(フォームを×ボタンで閉じにくかったりしませんか?)。

あと、BackgroundImage プロパティにイメージをセットするのは低速なような気がしますので、フォームをダブルバッファリングするようにし(わからなかったら検索してみてください。今回の例は不要かも…)、フォームから取得したグラフィクスデバイスコンテキストに対して DrawImage で描画した方が早くなるような気がします。(このあたりが参考?http://d.hatena.ne.jp/pmoky/20040807)

(だらだらと書いていますが予想で書いている部分もあります。)


> そうなると、DirectXのほうがいいのでしょうかね?
>
> Managed Direct X で音楽再生のコードを書いていますが、
> 画像の処理もManaged Direct Xでできるんでしょうか?

Managed Direct X であれば Direct3D の HLSL を使うと有効かもしれません。HLSL は GPU 側のプログラムを記述するものでピクセル単位の処理を記述することができます(ピクセルシェーダ)。GPU 側のプログラムが書けるので GDI や CPU 演算よりも圧倒的に高速ですが、今までと違うことを覚えなくてはいけなかったり、そもそもフレームワークが異なるのでプログラムを大幅に変更しなくてはいけない可能性もあります。まあとりあえずさらっと調べるものいいかもしれません。

※前に WPF+HLSL で動画再生中にリアルタイムで画像変換なんかやったことがあります。GPU レベルで処理するのでコマ落ちとかなく処理できました。

後、Windows Vista 以降をターゲットにしてもいいのれあれば Direct2D という選択肢もあります。GDI, GDI+ の後継 API であり、こちらもハードウェアレベルで処理できるので高速な処理が期待できますが、使ったことないので画像エフェクトが可能かどうかはわかりません。

・http://ja.wikipedia.org/wiki/Direct2D
・http://msdn.microsoft.com/ja-jp/windows/dd647497.aspx
2011/07/28(Thu) 21:16:15 編集(投稿者)

回答ありがとうございます。

> コードあんまり読んでないで回答してしまったのですが、最近の PC であれば 800x600 ぐらいなら大して遅くならないような気がします(マシンにもよりますが…)。
マシンは FMV-S8390で、Intel(R) Celeron(R) CPU 900 @ 2.20GHz
メモリは 4GB積んでますが、OSが Windows XP Professional 32bitなので、2.93GB RAMとして認識されています。
やはり、640*480の画像と800*600の画像では動作速度が異なりました。

>
> まず、コードの
>
>>frm->BackgroundImage = newbmp;
>>frm->Refresh();
>
> の部分は遅くなるような気がします。BackgroundImage プロパティにイメージをセットした時点でイメージは再描画されるはずなのですが、さらに Refresh をかけているので2重の呼び出しになっています。たぶんこうしている理由は、コントロール(フォーム)が再描画したくても while で無限ループしてしまっているので Refresh で強制的に再描画させるしかないからでしょう。UI に制御権を返すためにかならず while ループ内に System::Windows::Forms::Application::DoEvents() メソッドをいれるようにしましょう(フォームを×ボタンで閉じにくかったりしませんか?)。
少しいじったソースコードにはApplication::DoEvents書いてありました(汗
ためしに、frm->Refresh()を外してみましたが、画面の再描画が行われなくなってしまいました...

> あと、BackgroundImage プロパティにイメージをセットするのは低速なような気がしますので、フォームをダブルバッファリングするようにし(わからなかったら検索してみてください。今回の例は不要かも…)、フォームから取得したグラフィクスデバイスコンテキストに対して DrawImage で描画した方が早くなるような気がします。(このあたりが参考?http://d.hatena.ne.jp/pmoky/20040807)
DoubleBufferedはtrueに設定されていました。
ためしにfrm->CreateGraphics()にしてみたところ、若干早くなりましたが、
画面が点滅するようになってしまいました...

>>そうなると、DirectXのほうがいいのでしょうかね?
>>
>>Managed Direct X で音楽再生のコードを書いていますが、
>>画像の処理もManaged Direct Xでできるんでしょうか?
>
> Managed Direct X であれば Direct3D の HLSL を使うと有効かもしれません。HLSL は GPU 側のプログラムを記述するものでピクセル単位の処理を記述することができます(ピクセルシェーダ)。GPU 側のプログラムが書けるので GDI や CPU 演算よりも圧倒的に高速ですが、今までと違うことを覚えなくてはいけなかったり、そもそもフレームワークが異なるのでプログラムを大幅に変更しなくてはいけない可能性もあります。まあとりあえずさらっと調べるものいいかもしれません。
ピクセルシェーダはグラフィックカードの対応が必要なんでしたっけ?

> 後、Windows Vista 以降をターゲットにしてもいいのれあれば Direct2D という選択肢もあります。GDI, GDI+ の後継 API であり、こちらもハードウェアレベルで処理できるので高速な処理が期待できますが、使ったことないので画像エフェクトが可能かどうかはわかりません。
>
> ・http://ja.wikipedia.org/wiki/Direct2D
> ・http://msdn.microsoft.com/ja-jp/windows/dd647497.aspx
>
開発環境がXPなので少し難しいですね...
2011/07/28(Thu) 21:40:39 編集(投稿者)

初期化部分を
float width=7.f,width_max=200,width_add=5.f;
に、
描画部分を
g->DrawImage(bmp,Convert::ToInt32(Math::Sin((count+i)/40.f)*width),i,rect,GraphicsUnit::Pixel);
に、書き換えて動作速度を測ってみました。


640*480(最初のコード)
9203

640*480(frm->CreateGraphicsのコード)
8562

800*600(最初のコード)
17343

800*600(frm->CreateGraphicsのコード)
16687

1280*720(最初のコード)
38703

1280*720(frm->CreateGraphicsのコード)
36437


と、やはりfrm->CreateGraphicsのほうが早いこと、
また、画像サイズで描画速度が大きく異なることがわかりました。
しかし、点滅問題が残ってますね...
■No28803に返信(masaさんの記事)
> Managed Direct X で音楽再生のコードを書いていますが、
> 画像の処理もManaged Direct Xでできるんでしょうか?

Managed DirectX 自体、すでにサポートが終了しているものです。
新規に利用することはお勧めできません。
http://blogs.msdn.com/b/windows_multimedia_jp/archive/2009/09/16/sdk.aspx

代替としては SlimDX か、XNA でしょうか。
こんにちは、おのでらです。

> やはり、640*480の画像と800*600の画像では動作速度が異なりました。

まあ、画像サイズが大きくなると遅くなるのは一般的な話ですね(^^;)。単純に計算してもわかるように比例して処理コストが上がるので…

 640*480 = 307,200
 800*600 = 480,000 (1.6倍のコスト)
1280*720 = 921,600 (640〜の3倍のコスト)


> DoubleBufferedはtrueに設定されていました。
> ためしにfrm->CreateGraphics()にしてみたところ、若干早くなりましたが、
> 画面が点滅するようになってしまいました

ゲームのような描画速度を主体にするものはできる限り BackgroundImage や Refresh は使わない方がいいですね。


> ピクセルシェーダはグラフィックカードの対応が必要なんでしたっけ? 

はい。グラフィックカードごとに対応しているピクセルシェーダのバージョンが決まっているので使う場合は気を付けないといけません。
DirectX 10 以降対応と書いてあれば ピクセルシェーダバージョン 4.0 以降が必ず使えます。 
まあ最近の PC であればチップセットレベルで最低でもバージョン 2.0 に対応しているので大丈夫だと思いますが・・・。



時間が取れたのでためしてみました。
おそらくですが、遅くなると思われる原因がわかりました。

> Rectangle rect = Rectangle(0,i,bmp->Width,1);
> g->DrawImage(bmp,Convert::ToInt32(Math::Sin((count+i)/10.f)*width),i,rect,GraphicsUnit::Pixel);

で描画先の指定を X, Y しか指定していないと思いますが、
この場合描画先の範囲が指定されていないので X, Y 以降、下の領域に対してすべて描画してしまいます。例を挙げると

【1ループ内の処理】
高さ範囲 0〜639 に対して描画
高さ範囲 1〜639 に対して描画
高さ範囲 2〜639 に対して描画
  :
高さ範囲 639〜639 に対して描画

となるので明らかに下の方で無駄な描画処理が発生してしまっています。DramImage メソッドを呼ぶ時は

  int offset = Convert::ToInt32(Math::Sin((count + h) / 10f) * width);
  Rectangle srcRect = Rectangle(0, h, bmp->Width, 1);
  Rectangle destRect = Rectangle(offset, h, bmp->Width, 1);
  g->DrawImage(bmp, destRect, srcRect, GraphicsUnit::Pixel);

のように描画先の範囲も指定しましょう。私の環境では 50 倍ぐらい早くなりました。



一応私の方で試したサンプルコードを載せておきます。
C# で書いてしまっていますが、同じ .NET Framework を使用しているので C++/CLI でもそのまま置き換えて読めると思います。
(※ using はスコープを抜けるときにリソースを自動的に破棄する構文です(Disposeメソッドを呼ぶ))

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace RasterScrollEffect
{
  class Program
  {
    static void Main(string[] args)
    {
      using (Form1 frm = new Form1()) // ウィンドウを作成して実行します (From1 を新規作成した状態です)
      using (Bitmap bmpLoad = new Bitmap("画像_1280x720.bmp")) // 画像読み込み
      using (Bitmap bmpRender = new Bitmap(bmpLoad.Width, bmpLoad.Height)) // 一時書き込み先ビットマップ
      {
        //===================================================
        // フォーム関連
        //===================================================

        // フォームのサイズを画像に合わせる
        frm.ClientSize = new Size(bmpLoad.Width, bmpLoad.Height);

        // フォーム表示
        frm.Show();

        //===================================================
        // 値の初期化
        //===================================================

        int count = 0;
        float width = 5f;
        float width_max = 50;
        float width_add = 0.15f;

        // 画像の矩形サイズ(事前作成)
        Rectangle baseRect = new Rectangle(0, 0, bmpLoad.Width, bmpLoad.Height);

        // 画像の幅と高さ
        int bmpW = bmpLoad.Width;
        int bmpH = bmpLoad.Height;

        // 描画計測用
        Stopwatch swAll = new Stopwatch();
        Stopwatch swRaster = new Stopwatch();

        //===================================================
        // ループ
        //===================================================
        while (frm.Created)
        {
          // フォームのグラフィクスデバイスコンテキスト作成
          using (Graphics formG = frm.CreateGraphics())
          using (Graphics renderG = Graphics.FromImage(bmpRender))
          {
            // 1ループの計測
            swAll.Restart();
            count++;

            // 背景黒
            renderG.FillRectangle(Brushes.Black, 0, 0, bmpW, bmpH);

            //===================================================
            // ゆらす計算
            //===================================================
   
            // ゆらす処理と一時バッファ内の描画計測
            swRaster.Reset();
            swRaster.Start();

            // ゆれの大きさを時間によって変化させる
            if (width_add > 0)
            {
              if ((width += width_add) > width_max)
              {
                width_add = -width_add;
              }
            }
            else
            {
              if ((width += width_add) < 0)
              {
                width = 0;
                width_add = -width_add;
              }
            }

            //===================================================
            // ゆらす描画
            //===================================================

            for (int h = 0; h < bmpH; h++)
            {
              int offset = Convert.ToInt32(Math.Sin((count + h) / 10f) * width);
              Rectangle srcRect = new Rectangle(0, h, bmpW, 1);
              Rectangle destRect = new Rectangle(offset, h, bmpW, 1);
              renderG.DrawImage(bmpLoad, destRect, srcRect, GraphicsUnit.Pixel);
              //renderG.DrawImage(bmpLoad, offset, h, srcRect, GraphicsUnit.Pixel);
            }

            swRaster.Stop();

            //===================================================
            // フォームに描画
            //===================================================
            formG.DrawImage(bmpRender, Point.Empty);

            swAll.Stop();

            // 「出力」に経過時間 表示
            Trace.WriteLine("Raster Ellapse =" + swAll.ElapsedMilliseconds.ToString());
            Trace.WriteLine("All    Ellapse =" + swRaster.ElapsedMilliseconds.ToString());

            // UI に処理を返す
            Application.DoEvents();
          }
        }
      }
    }
  }
}
>Azuleanさん
そうでしたか、知りませんでした...
AudioPlayBackは再生できる形式が多く、コードも簡単で利用していたんですが(汗

>おのでらさん
ソースコードまでありがとうございます!
前回ちょっといじったやつ(前回のタイムはこれで測定)を
さらにおのでらさんのコードを参考にいじってみました。

// ぐにゃぐにゃ.cpp : メイン プロジェクト ファイルです。

#include "stdafx.h"
#include "Form1.h"

using namespace ぐにゃぐにゃ;

[STAThreadAttribute]
int main(array<System::String ^> ^args)
{
// コントロールが作成される前に、Windows XP ビジュアル効果を有効にします
Application::EnableVisualStyles();
Application::SetCompatibleTextRenderingDefault(false);

// メイン ウィンドウを作成します
Form1^ frm = gcnew Form1();
// 初期化
int count = 0;
float width=7.f,width_max=200,width_add=5.f;
// 画像読み込み
Bitmap^ bmp = gcnew Bitmap(Application::StartupPath + "\\Test.jpg");
// 画像の幅
int bmpwidth = bmp->Width;
int bmpheight = bmp->Height;
// フォームの大きさを画像に合わせる
frm->ClientSize = System::Drawing::Size(bmpwidth,bmpheight);
// 一時描画用の画像
Bitmap^ tmpbmp = gcnew Bitmap(bmpwidth,bmpheight);
// Graphics作成
Graphics^ g = frm->CreateGraphics();
Graphics^ tmpg = Graphics::FromImage(tmpbmp);
// Form1の表示
frm->Show();
// ループ
while (frm->Created) {
// 背景を黒に
tmpg->FillRectangle(Brushes::Black,0,0,bmpwidth,bmpheight);
count+=10;
//ゆれの大きさを時間によって変化させる
if (width_add > 0) {
if((width += width_add) > width_max) {
width_add =- width_add;
}
}
else {
if((width += width_add) < 0) {
width=0;
count = 0;
width_add =- width_add;
}
}
//ゆらす
for(int i = 0; i < bmpheight; i+=8) {
int formula = Convert::ToInt32(Math::Sin((count+i)/40.f)*width);
Rectangle srcrect = Rectangle(0,i,bmpwidth,8);
Rectangle destrect = Rectangle(formula,i,bmpwidth,8);
tmpg->DrawImage(bmp,destrect,srcrect,GraphicsUnit::Pixel);
}
g->DrawImage(tmpbmp,Point::Empty);
Application::DoEvents();
}
return 0;
}

実行してみましたが、どの画像サイズでもCreateGraphicsを使った時と使わなかった時の中間のタイムでした...
(1280*720だと、37232でした)
私がいじったソースコードがおかしいのでしょうか?
masa さんのコードで C++/CLI で実行してみました。すると C++/CLI で作成したプログラムのほうが C# のプロジェクトで実行したときよりも2倍程度時間がかかりました。

ただ処理時間がかかるのは C++/CLI で「デバッグ実行」を行っている時で、デバッグ実行なし、または EXE ファイルを直接実行した場合はどちらも変わらない速度で処理されました。C++/CLI のデバッグ実行は内部でなんかいろいろやってそうですね…。

ちなみに私の環境で計測した場合は以下のようになりました。

CPU : Core 2 Quad 2.66GHz 4コア (ただし実行時は1コアのみ使用)
メモリ:8GB
GPU:Radeon 4800HD 1GB
OS:Windows 7 64bit

while ループ内1回の処理経過時間(高さ1px単位の処理の場合)
 画素 640x480:17ms
 画素 800x600:27ms
 画素1280x720:48ms

(そういえば masa さんの計測した時間の単位がわかりませんね…)

masa さんがどれぐらいの動きを期待しているのかわからないので、私のマシンでどう動いたか動画でキャプチャしてみました。フレーム落ちしているかもしれませんが、リアルタイムでとっているので動きは実際に実行したときと同じイメージです。(640x480 と 1280x720 のバージョン用意しました。800 を用意してませんが、大体2つの間よりちょっと動きが早いぐらいです)

・http://tinyurl.com/4xcq9jv
動画までありがとうございます!返信遅れて申し訳ないです...
実行結果見ました。だいぶ早いですね...環境でここまで差が出るものなのでしょうか?
計測した単位はミリ秒です。
一応プロジェクトを添付しましたのでよかったら見てください。
releaseにビルド後のファイルが入っています。
添付ファイル: 1312041686.zip (31 KB)
masa さんのプロジェクトダウンロードして実行してみました。やはり動画サンプルと同程度のパフォーマンスで実行できました。

> 実行結果見ました。だいぶ早いですね...環境でここまで差が出るものなのでしょうか?
> 計測した単位はミリ秒です。

GDI系の描画はCPUに依存するので処理パフォーマンスはCPUのスペックに依存します。Celeron は廉価版なので、比較的低スペックな分類に入りますが(周波数だけがスペックを決定するわけではないので)、1ループで数秒〜数十秒はかかりすぎですね・・・
複数のスペックのマシンで検証しないとなんで遅いのか特定するの難しそうですね。単純にスペックの問題とも言えそうなのですが…
別マシンで実行してみました。
FMV-K620 Intel(R) Celeron(R) CPU 2.50GHz
メモリ:1GB Windows XP 32bit
640*480 17391ms
周波数はこっちのほうが上ですが、速度は遅かったです。

また、前回測定したFMV-S8390(こっちがメインマシンです)はVistaとのデュアルブートなので、
Vista側でも測定してみましたが、XPと同じ速度(640*480 8753ms)でした...
マシンによって速度が異なる、ということは、
エフェクトにかける時間を指定しておいて、
それを基準に速度が速すぎたらウェイトをかける、
遅すぎたらフレームスキップみたいなことを考えたのですが、
エフェクトに17秒もかかるマシンに「5秒」と指定したら
エフェクトとしての効果が見れないかなぁと思いました...
どうでしょうか?

DOBON.NET | プログラミング道 | プログラミング掲示板