Archive for the ‘流動指向ゲームエンジン『Fooo』’ Category

めざせ60FPS!

Foooはありがたいことに、とても快適に動作するという評価をいただいている。
今回はその快適さがどのように実現されているかについて紹介しようと思う。

高い要求と過酷な環境

Foooに最初に要求された動作水準は800×600サイズのフルカラー。
それに対し当時の標準的なPCのスペックは Celeron 800MHz 程度だった。
コンピュータが日々進歩を続けているとは言え、その環境は
800×600サイズのフルカラー表示をソフトウェア処理で行うにはまだまだ過酷だった。

どのくらい過酷かというと、800×600サイズの画像を1枚表示させるだけで30FPSなくらい。
言い換えると

画面に背景を表示するだけで30FPS

になってしまうのだ。
他に画像を表示すればあっという間に
20FPS…10FPS…5FPS…

ちなみに、FPSとは1秒の間に画面を再描画する回数(Frame Per Secnod)のことである。
モニタの標準的なFPSは60FPSであるが、
ゲームを快適にプレイできるようにするためには最低でも平均30FPS程度は出したいところだ。
それが、背景を表示するだけで30FPS…
もっと低スペックの環境も当然ありえるが400MHzだと15FPS、166MHzだと5FPS…

5FPSともなると、もはやまともにプレイできるような状態ではない。
400MHZでも最低30FPS、166MHzでも最低15FPSは出るようにしたい。
そのためには800×600で60FPS出るくらいじゃないとお話にならない。

無理じゃね!?

描画処理の最適化

泣き言を言っても始まらない。
FPSが出ないのは、一にも二にも描画に非常に時間がかかっているからである。
そこで最初に行った対策は

とにかく描画を速くする

ことである。
次のような最適化を施した。

・ケースバイケースごとに細かく処理分けして少しでも無駄な処理を省く
・コンパイラに頼らずアセンブラで最適化
・MMXを使う
・SSEを使う
・パイプラインストールを減らす
・メモリアライメントを意識する
・キャッシュヒット率を考える
・キャッシュにプリフェッチする

そんなこんなで~描画処理を1.5倍程度高速化することができた!
16msかかっていた800×600の画像の描画処理が11msになった。

──────しかしFPSにすると、30FPSだったのが40FPSになったくらい。
60FPSにはまるで届いていない。

無効領域処理

描画処理そのものの高速化はもう限界。
そこでもっと別の角度から高速化を行えないかと考えた。
それは…

無駄な描画をしない

ことである。

無駄な描画を省く最適化は一般的にカリング(Culling)と呼ばれる。
Cullingとは「間引き」という意味だ。
Windowsのウィンドウの描画においても、このカリング処理が行われている。

画面の再描画を行うには、画面を背景から何から全て描き直してしまうのが最もシンプルな方法である。
しかしそこには非常に多くの無駄が存在する。

まず画面が何も変化してないのに画面を書き直すのはまったくの無意味だ。
そのような画面の再描画は根本的に省くことができる。
さらに画面の中で前回描画した時から何も変化していない部分を再描画するのも大きな無駄だ。
何も変化していないのだから再描画する必要はない。
要するに

変化がない部分は再描画する必要がない

のである。
実際の最適化の様子を見てみよう。

YouTube Preview Image

赤い箱枠で示されているのが、変化があり再描画が行われている領域である。
箱枠がない部分は再描画の必要のない領域だ。
再描画領域が非常に小さく限定されていることがわかるだろう。

このような再描画の必要のある領域を『無効領域』(Invalid Rect)と呼ぶ。
Update Rect(更新領域), Dirty Rect(汚れた領域)などと呼ばれることもある。

この最適化の効果は絶大だった。
事実、この最適化によって”画面に動きがない部分では”あっさり60FPSを達成できた!

しかしこの最適化を活かすには、演出においてこの最適化を意識することが大切だ。
画面全体を動かすような演出をできるだけ避けるといった工夫である。

無効領域の分割管理

無効領域は効率のために矩形で管理するが、
無効領域は画面の中で飛び飛びに複数発生する可能性がある。
そのような無効領域を矩形で管理しようとすると無駄が発生してしまう。
次の例を見て欲しい。

YouTube Preview Image

雪の粒が画面を埋め尽くしているので、無効領域はほぼ画面サイズと等しくなっている。
しかしこの無効領域には明らかに大きな無駄がある。
再描画の必要のない部分まで無効領域に含めてしまっている。

このような無駄が発生するのは、無効領域を単一の矩形を管理しているせいだ。
この問題を解決するには

無効領域を複数の矩形で管理すればいい

実際の例を見て見よう。

YouTube Preview Image

無効領域が非常に細かく管理され、無駄が省かれている様子がわかるだろう。
けれども無効領域をあまりに細かく管理しすぎると
無効領域の管理の処理負荷が大きくなってしまうため加減が肝心だ。

この最適化により、画面の各所で複数のものが動作する場合でも
非常に高いFPSを出すことができるようになった。

遮蔽される面の除去

描画において大きな無駄がもう1つある。

見えない部分は描画する必要はない

のである。

複数の画像が重なっていて、後ろの画像が手前の画像によって覆い隠されている場合、
後ろの画像はどうせ隠れてしまうので描画する必要がない。
言われて見れば、至極当たり前の最適化である。
この最適化は3D方面ではオクルージョンカリング(Occlusion Culling)と呼ばれる。
Occlusionとは「遮蔽」という意味だ。
実際の動作の様子を見てみよう。

YouTube Preview Image

青い箱枠が遮蔽面、緑の箱枠が遮蔽しない面である。
この箱枠の形に描画処理が行われる。
遮蔽によって、背後の画像の領域が分割されている様子がわかるだろう。

この最適化の効果は思いのほか大きい。
実際の演出では画面の切り替えにフェードイン、フェードアウトが多様されるが
フェード幕で隠れた部品を、幕の背後に残したまま演出を続けるようなことが多々ある。
そのような場合にこの最適化がすごく活きる。

幕の背後の描画を自動的に行わないようになるからだ。
逆にこの最適化がないと、非常に大きな無駄が発生してしまう。
そして、ソレになかなか気がつかない。

Foooの演出において、真四角のコマが多用されているのもこの最適化が関連している。
コマを真四角にすることで、背景を遮蔽しやすくしているのだ。
そのようにすると、コマがたとえ画面に何十枚もある場合でも
背景を1枚描画するのとさほど変わらないコストで画面を描画できるのである。

遮蔽される無効領域の除去

次の最適化は併せ技だ。

見えない部分が変化しても再描画する必要はない

遮蔽されている部分で何かが動いても、見た目に変化は起きないので再描画する必要がない。
要するに「無効領域に対して遮蔽を行う」ということである。
これも実際の動作を見るのがわかりやすいだろう。

YouTube Preview Image

まとめ

全ての最適化を適用したもの。

YouTube Preview Image

このような最適化と、その最適化を意識した演出によって
Foooはソフトウェア処理でありながら高速な(高速に見える)動作を実現しているのである。

以下おまけ。

YouTube Preview Image
YouTube Preview Image


Windows95でも動く!

よく誤解されるのだけれど…

Foooはソフトウェアエンジンである!

グラフィックスのハードウェアアクセラレーション…
いわゆるビデオカードの機能はまったく利用しておらず
ほぼ全ての処理をCPUのみで行っている。

なのでビデオカードを最新のものに買い換えても恩恵はさほどない。
VRAMの容量なんかはパフォーマンスにはほとんど関係ない。
当然ビデオカードの3D機能なんかはまったく使っていない。
前回書いたような3D機能も全てソフトウェア処理で実現されている。

というわけで、いつも動作環境に「3Dアクセラレーション不用」とチラッと書いていたのだけれど
………お気づきになられただろうか(汗)

ハードウェア処理のリスク

ビデオカードが提供するハードウェアアクセラレーションは非常にパワフルな機能である。
またDirectXが提供する強力なフレームワークもプログラマの手間を大きく軽減してくれる。
3Dはすごく簡単に扱えるし、そして何より圧倒的に高速だ。

ハードウェア処理を利用することには極めて大きなメリットがあるが、同時に大きなリスクも孕んでいる。
ハードウェアの機能を利用するということは、
ハードウェアの機能に依存してしまうということと同義だからだ。

ソフトウェア処理のメリット

ハードウェアに依存しないことには、
ハードウェアの恩恵を受けられないハンデを背負ってでも代えがたい
非常に大きなメリットがある。
わかりやすく一言で言えば

FoooはWindows95でも動く!

たぶんWindowsNT4.0でも動く(笑)
え、何の意味があるのかって?
意味なんかない!

動くことが正義である!

というのは半分冗談で、
あらゆる環境で動作するようにと配慮を重ねた結果だ。
Windows95で動けばどんな環境でも動くだろうということで!

今でこそハードウェアアクセラレーションは気軽に使えるものになっているが
Foooが開発された10年前はそんなに気軽に使えるものではなかった。
各社のビデオカードの機能は様々で、動作も様々。

DirectXがそのあたりの差異を適宜ソフトウェア処理で埋めてくれるという謳い文句だったのだが
期待しているような水準にはまったく達しておらず
半透明で描画するように指定したらメッシュ状態で表示したりと散々だった。
確かに透けてるといえば透けてるけど!
そういった中でハードウェアアクセラレーションを利用することはあまりに無謀だったのである。

ソフトウェア処理の強み

その頃の仕様をいまだに引きずってる意味があるのかと言われるとアレだが…
ソフトウェア処理であるということは環境に依存しないこと以外にもメリットがある。
ハードウェアが提供していない処理も自在に行うことができるという点だ。

例えば、昨今では画像を合成する際に様々な合成方法が使われる。
オーバーレイや覆い焼き、焼きこみなどPhotoshopでおなじみだろう。

しかし意外に思うかもしれないが、現在のハードウェア処理では
そういった合成方法を扱うのは非常に難しい。
ソフトウェア処理ならば、比較的低いコストで導入することができる。
これはソフトウェア処理の大きな強みだ。


流動指向とは

流動指向ゲームエンジン『Fooo』と銘打ってはいるが、
「流動指向」とは完全なる造語である。
では「流動指向」とはいったいどういう意味なのか。

それはFoooが、複数のオブジェクトがまるで浮き流れるかのように
同時平行で動き回ることを前提としたシステムであるということを象徴している。
オブジェクトが同時に動くシステムなんて珍しくないと思うかもしれない。
しかしFoooが実現しているその機構は、おそらく他のシステムとは一線を画している。

時間の概念

Foooスクリプトには
言語レベルで時間の概念がある

他の多くのプログラミング言語でも時間を扱うことはできるが、
それらは時間を参照できるとか、時間を元にイベントを起こせるとかいう形である。
Foooがそれらと決定的に違うのは「プログラムが時間に同期して実行される」という部分だ。
言い換えればFoooスクリプトは「実時間同期型の言語」であると言ってもいい。

時間と同期しながらプログラムが実行されるシステムというと
シューティングゲームやアクションゲームのスクリプトシステムを思い出すかもしれない。
それもそのはず、Foooはもともとシューティングゲーム用に作っていたシステムの設計を
転用して作られているのだ。

シューティングゲームやアクションゲームなどのリアルタイム性を要求されるゲームでは、
時間に同期しながらスクリプトを実行するのが一般的だろう。
しかしFoooにはまたそれらの多くのシステムとはおそらく異なる特徴がある。

細かな時間単位

Foooスクリプトは
時間がミリ秒単位である

古典的なリアルタイムゲームシステムでは、
時間の単位が画面の更新フレーム単位であることが多い。
しかしそのようなシステムでは時間がフレームに拘束されてしまい
演出の速度を細かく変更することが難しいという問題がある。

Foooでは時間の単位をミリ秒とすることでフレームに拘束されない柔軟性の高い構造をとっている。
ゲームの進行速度をプレイヤーが自由に変更できるようにするという要件があったためだ。
これにより速度をシームレスに調節するという機構が実現されている。

正確な時間同期

またさらにFoooには
実時間と正確に同期をとる
という大きな特徴がある。

1000msかかる演出を1回やる処理と、
2msかかる演出を500回やる処理と、
1msかかる演出を1000回やる処理が
ぴったり1000msで同時に終わることが完全に保証されている。

1000msかかる演出を実行してから
500ms後に500msかかる演出を実行して
ぴったり1000msで同時に終わることが完全に保証されている。

何を当たり前なことを!
…と思うかもしれないが、当たり前でないことの方がたぶん多いだろう。

多くのプログラミング言語において正確に同期をとるというのは非常に悩ましい問題だ。
複数のプログラムを同時に実行するスレッドやなどは、
プログラマの間では鬼門とされているくらい扱いの難しいしろものだ。

しかしFoooでは、スレッド(正確にはコルーチンであるが)をいとも簡単に扱うことができる。
複数のスレッド、オブジェクトデーカーなどが同時並行で動く場合でも
時間の上で考えて同時になるはずの部分は、想像通り同時になるように動く。
このように時間で同期がとられることが保証されているので、
複数の部品が同時並行的に動作するスクリプトを極めて直感的に記述することができる。

そんなこんなで

───────などとFoooの特徴についてまとめてみたが…
他のシステムについて別段詳しいわけでもないので、
井の中の蛙が勝手に特徴的だと思いこんでるだけかもしれず…
「似たような有名なシステム既にあるよ!」とかあったら教えてください(苦笑)


3Dで雨を降らす

以前に書いた 「雨を降らす」 の記事では、
2Dで雨を降らせていたが、3Dが扱えるということで今回は3Dで雨を降らせてみよう。

2Dの時は雨粒の大きさで距離感を表現していたが、
3Dでは雨粒を3D空間に配置できるので実際に遠くの位置に配置できる。
雨粒を3D空間に配置できるということは、雨粒を別の角度から見ることもできるということだ!

3D空間の見え方を変えるには、視点と注視点の位置を指定する。
ただしパラメータで指定するのではなく、ちょっと変わった方法で指定する。

具体的には3Dステージデーカーの子として”Camera”という名前のデーカーを作ると
そのデーカーの位置が視点となる。
同様に”Target”という名前のデーカーを作るとそのデーカーの位置が注視点となる。
デーカーの位置を基準とするので、デーカーを動かせば視点を操作できるという仕掛けだ。


class 雨クラス
{
  method 雨クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "雨粒"+String($number++);
      ThreadCreate(call=@雨粒スレッド(name=$name));
      wait 10;
    }
  }

  method 雨粒スレッド(string $name)
  {
    float $x = Float(rand_range(-5000,5000));
    float $y = 10000.0;
    float $z = Float(rand_range(-5000,5000));
    int $time = 1000;

    Create3DImage(name=$name
      , x=$x, y=$y, z=$z, rz=-90.0, ox="Center", oy="Middle", sx=500%, sy=500%
      , image="雨粒.png", sampling="BieLinear", back=true);
    Enter(to=$name);

    Move3D(to=$name, time=$time, y=0.0);
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Main()
{
  Create3DStage(name="Stage", w=1280, h=720);
  Create3DModel(name="Stage/空", model="空.lwo", render="Color");
  Create3DObject(name="Stage/雨", class=@雨クラス());
  Create3DNode(name="Stage/Target", y=1000.0);
  Create3DNode(name="Stage/Camera", y=0.0, z=-10.0);
  Enter(to="Stage");
  Enter(to="Stage/*");

 // 視点を移動
  Move3D(to="Stage/Camera", time=10000, y=1000.0, z=-2000.0, step="AccSig");
}
YouTube Preview Image

3Dモデルを表示する

Foooの3D機能は、必要だから作ったというより

3D表示もできたらすごくね!?

くらいの軽いノリで作った。
もはやすごくもなんともないけれど…
昔はちょっとしたことだったのだ。

今でこそ3Dベースのゲームエンジンも珍しくはないが
10年ほど前はまだまだ2Dベース全盛の時代。
3Dといえばトランジッションでソレっぽいことがちょっとできる程度が普通。
自由度の高い3D演出ができれば、頭一つ抜きんでられるのは間違いなかった!

そして実際、3D機能を作った!
が…

3D使う機会がぜんぜんない!

悔しいので、サイコロとか魔方陣とか地球儀とか
こまかーいところで地味~に使っていた(苦笑)

というわけで今回はサイコロを表示してみよう。
サイコロのモデルには3Dソフトで作成したものを使う。
3Dモデルを表示する機能を持つのが、3Dモデルデーカーだ。
Create3DModel命令で作成する。


method Main()
{
  CreateColor(name="Back", w=1280, h=720, color=black);
  Create3DStage(name="Stage", w=1280, h=720);
  Create3DModel(name="Stage/サイコロ", model="サイコロ.lwo", z=400.0);
  Enter(to="Back");
  Enter(to="Stage");
  Enter(to="Stage/サイコロ");
  wait 1000;

  Rotate3D(to="Stage/サイコロ", ry=360.0, time=3000, step="AccSig");
  wait 3000;
  Rotate3D(to="Stage/サイコロ", ry=0.0, time=0);
  Rotate3D(to="Stage/サイコロ", rx=360.0, time=3000, step="AccSig");
  wait 3000;
  Rotate3D(to="Stage/サイコロ", rz=360.0, time=3000, step="AccSig");
  wait 3000;
}
YouTube Preview Image

うん、3Dは楽しい。


3Dで画像を表示する

Foooでは3Dを扱うこともできる。
とはいえFoooは基本的に2Dで処理を行っているので、3Dの扱いは少々特別だ。

3Dを扱うためには、3Dの空間を持つ『3Dステージ』を作る必要がある。
Foooではデーカーが配置される空間のことを『ステージ』と呼ぶ。
普段デーカーを配置しているステージは、2Dの空間である。
2Dの空間上では3Dを扱うことができない。
このためまず、3Dの空間を用意する必要があるのである。

なんともややこしそうな話だが、やることは簡単だ。
3Dステージデーカーを作るだけである。


method Main()
{
  Create3DStage(name="Stage", w=1280, h=720);
  Enter(to="Stage");
}

この3Dステージデーカーの子としてデーカーを作成すると、
そのデーカーは3Dステージデーカーが管理する3D空間内に配置される。

3D空間に配置できるデーカーは3D用の特別なものである。
3Dで画像を表示するには、3Dイメージデーカーを使用する。
3Dイメージデーカーを作成するにはCreate3DImage命令を使用する。


method Main()
{
  Create3DStage(name="Stage", w=1280, h=720);
  Create3DImage(name="Stage/画像", image="画像.png", z=800.0);
  Enter(to="Stage");
  Enter(to="Stage/画像");
}

3D用のデーカーを操作する命令も3D用に特殊なものを使う。
3Dのカードを作成して回転させてみよう。


method Main()
{
  Create3DStage(name="Stage", w=1280, h=720);
  Create3DNode(name="Stage/カード", z=800.0);
  Create3DImage(name="Stage/カード/表", image="表.png");
  Create3DImage(name="Stage/カード/裏", image="裏.png", ry=180.0);
  Enter(to="Stage");
  Enter(to="Stage/カード");
  Enter(to="Stage/カード/*");
  wait 1000;
  Rotate3D(to="Stage/カード", ry=360.0, time=3000);
}
YouTube Preview Image

3D用の命令を使ってはいるが、このように2Dの場合とほとんど同じ感覚で
3Dを制御するスクリプトを書くことができる。


ボタンを作る

さてさてここまでFoooの多様な機能を紹介してきたが
ここまで読んだ方々は皆、薄々…疑問に…思っているのではないかと思う。

「いったいどこがアドベンチャーゲーム用の
ゲームエンジンなんだ!」

と。

どんな演出でも実現できるようにと、自由度を追求し続けた結果がコレである。
もはやアドベンチャーゲーム用のエンジンというよりは
より汎用的なマルチメディアエンジンのようなかんじである。
しかしどんなに複雑なことができるとは言っても
Foooはあくまで『動く漫画』を実現するために開発されたゲームエンジンだ。

けれどもゲームを構成する要素は『動く漫画』だけではない。
ゲームがゲームである以上、ユーザーが操作できる部分…
ユーザーインターフェース(UI)がある。

開発が進むにつれ、このUI部分についても『動く漫画』部分と同様に
高度な演出を行いたいという要望が出始めた。

高い汎用性を持ち始めたFooo…
高度な演出が必要なUI…
………………

「UIもFoooスクリプトで作っちゃえばよくね?」

となるのは必然的な流れだった。

Decor_Button ボタンデーカー

そんなこんなでFoooにはUIを実現するデーカーがいろいろあるが、
そのもっとも代表的なものが『ボタンデーカー』だ。
ボタンデーカーは名前の通り、ボタン機能を実現するデーカーである。
ボタンデーカーを作るにはCreateButton命令を使用する。


  CreateButton(name="ボタン", class=@ボタンクラス());

一見するとCreateObject命令となんら違いがない。
ボタンデーカーはオブジェクトデーカーのようにクラスを指定して使う。
ボタンデーカーがオブジェクトデーカーと異なるのは、
ユーザーの操作に応じて予め決められたメソッドを自動的に呼び出す点だ。
次のようなメソッドが呼び出される。

OnButton_Focus ボタンがフォーカスされた時
OnButton_UnFocus ボタンからフォーカスがはずれた時
OnButton_Down ボタンが押し下げられた時
OnButton_Up ボタンが押し上げられた時
OnButton_Enable ボタンが有効になった時
OnButton_Disable ボタンが無効になった時

このような特定のタイミングで自動的に呼び出されるメソッドのことを『イベントハンドラ』と呼ぶ。
ボタンデーカーがやってくれることはこのイベントハンドラの呼び出しだけである。
ボタンに必要な画像の作成や、ボタンの画像の制御などは
コンストラクタやイベントハンドラに動作を書き込むことで実現する。

これは実際の使い方を見たほうがわかりやすいだろう。

Button_Normal Button_Focus Button_Down
ボタン_通常.png ボタン_フォーカス.png ボタン_ダウン.png

style ボタン書式
{
  face="MS ゴシック", size=40, color=white, interval=0
}

class ボタンクラス
{
  method ボタンクラス(string $text)
  {
    CreateImage(name="通常"
      , ox="Center", oy="Middle"
      , image="ボタン_通常.png", sampling="BieLinear");
    CreateImage(name="フォーカス"
      , ox="Center", oy="Middle"
      , image="ボタン_フォーカス.png", sampling="BieLinear"
      , alpha=0%);
    CreateImage(name="ダウン"
      , ox="Center", oy="Middle"
      , image="ボタン_ダウン.png", sampling="BieLinear"
      , alpha=0%);

    CreateText(name="テキスト"
      , ox="Center", oy="Middle"
      , style="ボタン書式", text=$text, sampling="BieLinear");
  }
  method OnEnter()
  {
    Enter(to="*");
  }

  method OnButton_Focus()
  {
    Opaque(to="フォーカス", alpha=100%, time=200);
  }
  method OnButton_UnFocus()
  {
    Opaque(to="フォーカス", alpha=0%, time=200);
  }
  method OnButton_Down()
  {
    Opaque(to="ダウン", alpha=100%, time=0);
    Zoom(to=".", sx=90%, sy=90%, time=50);
  }
  method OnButton_Up()
  {
    Opaque(to="ダウン", alpha=0%, time=200);
    Zoom(to=".", sx=100%, sy=100%, time=200);
  }
}

method Main()
{
  CreateButton(name="ボタン1"
    , x=320, y="Middle"
    , class=@ボタンクラス(text="ボタン1"));
  CreateButton(name="ボタン2"
    , x=640, y="Middle"
    , class=@ボタンクラス(text="ボタン2"));
  CreateButton(name="ボタン3"
    , x=960, y="Middle"
    , class=@ボタンクラス(text="ボタン3"));
  Enter(to="*");
}
YouTube Preview Image

OnButton~メソッドが自動的に呼び出されている様子がわかるだろう。
ボタンデーカーの実装はちょっと手間だが、その分ボタンの動作を細かくカスタマイズできる。


マスクを動かす

マスクデーカーはマスク画像を使って表示制限を行うが、
「画像」とは「画像ファイル」じゃなくてもいいんじゃないか?
と、勘のいい人なら気づくかもしれない。

そう、マスクデーカーにはマスクとして「画像ファイル」ではなく、
「画像として扱えるデーカー」を指定することもできる。
やり方はGetDecor関数でデーカーへの参照を取得しmaskパラメータに渡すだけだ。


CreateMask(name="マスク", mask=GetDecor("画像を持つデーカー"));

これでいったい何ができるのかというと、
マスクにバッファデーカーを指定することでマスク画像を動かすことが可能になる。
試してみよう。


class 煙クラス
{
  method 煙クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "煙粒"+String($number++);
      ThreadCreate(call=@煙粒スレッド(name=$name));
      wait 100;
    }
  }

  method 煙粒スレッド(string $name)
  {
    CreateImage(name=$name
      , x=0, y=0, sx=20%, sy=20%, ox="Center", oy="Middle"
      , angle=rand_range(0,359), image="煙粒.png");
    Enter(to=$name);

	int $time = 2000;
    int $angle = 90+rand_range(-10,10);
    float $rad = radian(Float($angle));
    float $move = 300.0;
    float $x = $move*cos($rad);
    float $y = -$move*sin($rad);

    Move(to=$name, time=$time, x=$x, y=$y, step="DecSin");
    Zoom(to=$name, time=$time, sx=200%, sy=200%);
    Opaque(to=$name, time=$time, alpha=0%, step="AccSin");
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Test(string $effect)
{
  Transform(to="マスク", effect=$effect, time=1000);
  wait 1000;
  Transform(to="マスク", effect=$effect, time=1000, begin=100%, end=0%);
  wait 1000;
}

method Main()
{
  CreateBuffer(name="バッファ", w=1280, h=720, x=10000);
  CreateImage(name="バッファ/星", image="マスク.png"
    , x="Center", y="Middle", ox="Center", oy="Middle");
  Enter(to="バッファ");
  Enter(to="バッファ/星");
  Rotate(to="バッファ/星", time=30000, angle=360);
  Zoom(to="バッファ/星", time=30000, sx=400%, sy=400%);

  CreateImage(name="画像2", image="画像2.png");
  CreateMask(name="マスク", mask=GetDecor("バッファ"), mask_mode="Blue");
  CreateImage(name="マスク/画像1", image="画像1.png");
  CreateObject(name="マスク/煙", x=1280/2, y=720/2+150, class=@煙クラス());
  Enter(to="*");
  Enter(to="マスク/*");
  wait 3000;

  call @Test(effect="LinearLeft");
  call @Test(effect="BlindLeft");
  call @Test(effect="CurtainLeft");
  call @Test(effect="ShaveLeft");
  call @Test(effect="SlashH");
  call @Test(effect="BoxCenter");
  call @Test(effect="HoleCenter");
  call @Test(effect="FanCenter");
  call @Test(effect="TensileLeft");
  call @Test(effect="Mosaic");
}
YouTube Preview Image

mask_modeというパラメータは画像の何の情報を不透明度として扱うかという指定である。
デフォルトでは自動で判断されるのだが、今回は都合が悪いので直接指定している。
“Blue”は青色要素を不透明度とする指定だ。

ちなみに似たようなことがUniversalTransit命令などでも可能である。


マスクを使う

バッファデーカーは子デーカーの表示をバッファの領域内に制限するので
ウィンドウデーカーのような用途でも使用できる。

実はウィンドウデーカーには「回転できない」という欠点がある。
ただしその分非常に高速に動作する。
バッファデーカーはウィンドウデーカーよりもかなり低速だが、
画像として扱えるのでもちろん回転も可能だ。

しかしウィンドウデーカーもバッファデーカーも表示を制限できるのは四角の形だけである。
丸形や星形に表示を制限するといったようなことはできない。

Decor_Mask マスクデーカー

バッファデーカーを拡張し、自由な形に表示制限できる窓を実現するのがマスクデーカーである。
マスクデーカーはマスク画像を使ってバッファに写しこまれた画像に透明部分を設定する。
バッファは画像として扱えるのだから、バッファの表示を制限したい部分を透明にしてやれば、
自由な形に表示を制限することができるという寸法だ。

マスクデーカーを作成するにはCreateMask命令を使用する。


CreateMask(name="マスク", mask="マスク.png");

組み込んでみよう。

マスク.png
Mask


class 煙クラス
{
  method 煙クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "煙粒"+String($number++);
      ThreadCreate(call=@煙粒スレッド(name=$name));
      wait 100;
    }
  }

  method 煙粒スレッド(string $name)
  {
    CreateImage(name=$name
      , x=0, y=0, sx=20%, sy=20%, ox="Center", oy="Middle"
      , angle=rand_range(0,359), image="煙粒.png");
    Enter(to=$name);

	int $time = 2000;
    int $angle = 90+rand_range(-10,10);
    float $rad = radian(Float($angle));
    float $move = 300.0;
    float $x = $move*cos($rad);
    float $y = -$move*sin($rad);

    Move(to=$name, time=$time, x=$x, y=$y, step="DecSin");
    Zoom(to=$name, time=$time, sx=200%, sy=200%);
    Opaque(to=$name, time=$time, alpha=0%, step="AccSin");
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Test(string $effect)
{
  Transform(to="マスク", effect=$effect, time=1000);
  wait 1000;
  Transform(to="マスク", effect=$effect, time=1000, begin=100%, end=0%);
  wait 1000;
}

method Main()
{
  CreateImage(name="画像2", image="画像2.png");
  CreateMask(name="マスク", mask="マスク.png");
  CreateImage(name="マスク/画像1", image="画像1.png");
  CreateObject(name="マスク/煙", x=1280/2, y=720/2+150, class=@煙クラス());
  Enter(to="*");
  Enter(to="マスク/*");
  wait 3000;

  call @Test(effect="LinearLeft");
  call @Test(effect="BlindLeft");
  call @Test(effect="CurtainLeft");
  call @Test(effect="ShaveLeft");
  call @Test(effect="SlashH");
  call @Test(effect="BoxCenter");
  call @Test(effect="HoleCenter");
  call @Test(effect="FanCenter");
  call @Test(effect="TensileLeft");
  call @Test(effect="Mosaic");
}
YouTube Preview Image

今回も変更点はごく僅かだ。
このようにFoooではデーカーの「組み合わせ方」を利用した機能がとても多く
Foooの大きな特徴の一つである。


バッファを使う

複数のデーカーを一枚の画像として扱えるようにするには
もう一つのアプローチがある。

スナップショットデーカーの動作サンプルを見ればわかるが、
複数のデーカーを一枚の画像として扱えるようになってはいるものの、
画像は停止してしまっていた。
スナップショットなのだから当然と言えば当然である。

毎フレームスナップショットをとるようにして、
デーカーを動かしながら一枚の画像として扱えるようにできなくもないが
ちょっと非効率なかんじがする。

Decor_Buffer バッファデーカー

そんな時はバッファデーカーを使う。
バッファデーカーは子デーカーの表示を自身が持つバッファに写しこみ、
一枚の画像として扱えるようにするデーカーである。
子デーカーが動くとバッファの内容は自動的に適切に更新される。

バッファデーカーを作るにはCreateBuffer命令を使用する。


CreateBuffer(name="バッファ", w=1280, h=720);

組み込んでみよう。


class 煙クラス
{
  method 煙クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "煙粒"+String($number++);
      ThreadCreate(call=@煙粒スレッド(name=$name));
      wait 100;
    }
  }

  method 煙粒スレッド(string $name)
  {
    CreateImage(name=$name
      , x=0, y=0, sx=20%, sy=20%, ox="Center", oy="Middle"
      , angle=rand_range(0,359), image="煙粒.png");
    Enter(to=$name);

	int $time = 2000;
    int $angle = 90+rand_range(-10,10);
    float $rad = radian(Float($angle));
    float $move = 300.0;
    float $x = $move*cos($rad);
    float $y = -$move*sin($rad);

    Move(to=$name, time=$time, x=$x, y=$y, step="DecSin");
    Zoom(to=$name, time=$time, sx=200%, sy=200%);
    Opaque(to=$name, time=$time, alpha=0%, step="AccSin");
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Test(string $effect)
{
  Transform(to="バッファ", effect=$effect, time=1000);
  wait 1000;
  Transform(to="バッファ", effect=$effect, time=1000, begin=100%, end=0%);
  wait 1000;
}

method Main()
{
  CreateImage(name="画像2", image="画像2.png");
  CreateBuffer(name="バッファ", w=1280, h=720);
  CreateImage(name="バッファ/画像1", image="画像1.png");
  CreateObject(name="バッファ/煙", x=1280/2, y=720/2+150, class=@煙クラス());
  Enter(to="*");
  Enter(to="バッファ/*");
  wait 3000;

  call @Test(effect="LinearLeft");
  call @Test(effect="BlindLeft");
  call @Test(effect="CurtainLeft");
  call @Test(effect="ShaveLeft");
  call @Test(effect="SlashH");
  call @Test(effect="BoxCenter");
  call @Test(effect="HoleCenter");
  call @Test(effect="FanCenter");
  call @Test(effect="TensileLeft");
  call @Test(effect="Mosaic");
}
YouTube Preview Image

単に対象をバッファデーカーの子デーカーにしているだけにしか見えないが
これだけでデーカーを動かしたまま一枚の画像として扱えるようになる。