OpenSiv3DでPSDファイルを読み込む

SivPSD

この記事はSiv3D Advent Calendar 2023の21日目の記事として執筆いたしました。

OpenSiv3Dのゲーム開発でPSDファイルを利用したいと思い、PSD読み込みライブラリをpsd_sdkを用いて作成してみました。以下に導入方法と使い方を説明いたします。

github.com

要件

導入方法

📝 導入するOpenSiv3Dのプロジェクトを新規作成または開いてください。ここでは、HelloPSDというプロジェクトで進めます。

📗ターミナルで以下のコマンドを実行し、SivPSDとpsd_sdkをサブモジュールとして追加します。

git submodule add https://github.com/sashi0034/SivPSD
git submodule update --init --recursive
  • gitを用いない場合は、代わりにSivPSDをダウンロードしてプロジェクト直下に配置してください。

📝 SivPSDは現在v0.6.12のSiv3Dが設定されています。ご自身のプロジェクトのバージョンと違う場合は手動またはスクリプトを実行してSivPSDで使うSiv3Dのバージョンを変更してください。例えば、v0.6.10に変更する場合は以下のように実行すれば簡単に変更できます。

cd .\SivPSD\SivPSD\
python .\s3d_switch.py 0_6_10

📗 SivPSDをソリューションに追加します。ソリューションエクスプローラーのルートから右クリックをして、追加 / 既存のプロジェクトを選択します。プロジェクトルートからSivPSD/SivPSD/SivPSD.vcproj を探して追加します。

📝 同様の手順で SivPSD/psd_sdk/build/VS2022/Psd.vcxproj も追加します。

📗 プロジェクトからSivPSDを参照します。自身のプロジェクトを右クリックして、追加 / 参照 をクリックします。SivPSDにチェックマークを入れてOKを押してください。

🎉お疲れ様でした。これで導入は終わりです。

使い方

基本的なコード

📝 以下のようPSDを読み込んで使用できます。

PSDImporter psdImporter{U"path/to/file.psd"}; // 読み込み
if (const auto e = psdImporter.getCriticalError()) throw e; // エラー
PSDObject psdObject = psdImporter.getObject(); // 読み込んだデータの取得
while (System::Update())
{
    psdObject.draw(); // PSDを描画
}

📗 psdObject.layers[index] でレイヤー単体にもアクセスが可能です。以下にサンプルコードを掲載するのでご参考ください。イラスト miko15.psdSivPSD/Test/App/psd にございます。

gist.github.com

実行結果

非同期処理にして実行

📝 このPSDImporterですが、以下のようにコンストラクタで細かな設定が可能でございます。

  • storeTarget でレイヤーを Image として格納、DynamicTexture として格納するかの設定が可能です。

  • storeTarget で読み込み時に用いるスレッド数を決められます。適切なスレッド数を決めることで高速な読み込みが実現できます。

  • asyncStartでバックグラウンドとして非同期実行ができます。

PSDImporter psdImporter{
    {
        .filepath = U"psd/miko15.psd",
        .storeTarget = StoreTarget::MipmapTexture,
        .maxThreads = 4,
        .asyncStart= true
    }
};

📗 非同期処理にしたサンプルコードは以下をご確認ください。

gist.github.com

PSD形式の注意について

今回実装したライブラリではレイヤーのマスク処理やクリッピング機能をあえて対応しませんでした。

また、カラーモードやカラーチャンネルについてもRGB、8ビット/チャンネルと制限しました。

理由としまして、今回はゲーム用に利用する目的であり、マスク処理やクリッピングといったリアルタイムでレンダリングに負荷がかかることを避けたいということがあります。

また、CLIP STUDIO PAINTでイラストを作成するときは基本的にこのカラーモードやカラーチャンネルになっているように思います。

実際、Live2D Cubismのマニュアルを確認しますとこのような細かい制約が確認できます。

今回の開発においてもこの規則に則っていくことにしました。

🤖しかし、このようなレイヤーの統合作業は大変だろうということで、今回次のようなクリスタで使えるレイヤー自動統合スクリプトを作ってみました。ぜひご活用ください。

おしまい

PSDを利用して素敵なゲームを作ってみてくださいね。

蛇足でございますが、先頃のゲームジャムにて大変ありがたいことに賞を頂いています。楽しく遊んでいただけましたら幸いです。

siv3d.github.io

2023年から始めるNINTENDO64ゲーム開発

2023年から始めるNINTENDO64ゲーム開発
~64-bitセンセーション~

この記事はKMCアドベントカレンダー15日目の記事として執筆いたしました。

NINTENDO64について

みなさん御存知の通り、N64は20世紀で最も素晴らしいゲームハードウェアの一つです。発売されたソフトの数自体は少ないとはいえ、時のオカリナF-ZERO Xマリオ64といった世に影響をもたらした名作が満ち溢れています。

OOT

令和におけるN64について

発売から約28年の時を経たとはいえ色褪せることなくN64タイトルは現在でも多くの人に遊ばれていますが、技術系のN64コミュニティやイベントも数多く存在します。

以下にいくつかご紹介します。

N64ゲーム開発チュートリアル (2023年)

N64の主要なソフトウェア開発キットには、当時任天堂が実際に開発に使用した libultra と2023年現在においてOSSとして開発が続けられているlibdragon が存在します。

ただlibultraは機能豊富ですが、Windows Vista以前の32-bit環境でのみ動作し、さらには使用に関して法的にもグレーであるため、ここではlibdragonを使用する方法をご紹介します。

以下では、次のリンク先に従って説明します。

https://github.com/DragonMinded/libdragon/wiki/Installing-libdragon

いくつか方法が記載されていますが、Dockerとnpm経由で利用するのが簡単だと思います。

Dockerとnpmがインストールされていない場合は、以下の記事などを参考にしてください。

Dockerとnpmをインストールし、起動した状態で、以下のように実行します。

mkdir hello64
cd hello64

npm init
npm install -g libdragon
npx libdragon init

これでlibdragonがインストールできました。しかし、2023/12/08執筆現時点においてlibdragonのデフォルトブランチでは基本的に2D描画のみをサポートしており、高度な機能を利用できません。

そこで、予めunstableブランチのlibdragonを使用することのがおすすめです。unstableブランチでは画像などのアセットを簡単にROMに組み込んだり、いにしえのOpenGL 1.1 APIを利用した快適で高度なコーディングが可能です。

以下のコマンドでunstableに切り替えます。

git submodule add -b unstable https://github.com/DragonMinded/libdragon.git
git submodule update --init --recursive
cd libdragon
npx libdragon install

これで、libdragonをunstableブランチとして利用することができるようになりました。

では、実際にN64 ROMを作成していきましょう。

src/main.c ファイルを開けて、以下のコードに書き換えて保存してください。

#include <libdragon.h>

#define TEXT_POS(x, y) ((x)*8), ((y)*8)
#define LX_8 8
#define CY_14 14

static display_context_t s_display = NULL;
static char s_sb_64[64];

int main(void)
{
    display_init(RESOLUTION_320x240, DEPTH_32_BPP, 2, GAMMA_NONE, ANTIALIAS_RESAMPLE);
    controller_init();

    int count = 0;
    while (1)
    {
        while (!(s_display = display_lock()))
            ;

        graphics_fill_screen(s_display, 0x1F1F1FFF);
        graphics_set_color(0xFFFF00FF, 0);
        graphics_draw_text(s_display, TEXT_POS(LX_8, CY_14 - 2), "Hello N64!\n");

        snprintf(s_sb_64, sizeof(s_sb_64), "Count: %2d\n", count);
        graphics_draw_text(s_display, TEXT_POS(LX_8, CY_14 + 2), s_sb_64);
        display_show(s_display);

        count++;
    }
}

以下のコマンドでビルドを行います。

(プロジェクトのルートディレクトリにMakefileが存在するので、ルートディレクトリで行ってください)

npx libdragon make

おめでとうございます。これで hello.z64 というN64で動作可能なROMファイルが生成されました。

では実際に動作確認をしてみましょう。

私が開発中のN64エミュレーターをぜひご使用ください、と言いたいところですがまだまだ実用段階に到達していないので、aresやdgb-n64、simple64といった新世代のエミュレーターで動作させるといいと思います (paraLLEl-RDPを利用したこれらのエミュレーターは「Bandicam」というソフトウェアとなぜか競合するのでご注意ください)

実行画面

実行して以下のように「Hello 64!」とカウントアップと表示されれば成功です (エミュレータの種類によってはうまくいかない可能性があるので改めてご注意ください)

画像を表示したり、ポリゴン描画や音声再生などについては libdragon/examples が参考になると思います。

次に、これらの前にあらかじめ開発において知っておきたい前提知識を説明いたします。

開発に必要なN64のハードウェア基本知識

私自身、20世紀にはこの世に生を受けておらず、未熟者として学習の途上にございます。以下に述べる内容には、不備や誤りが含まれる可能性がございますが、何卒ご了承いただけますと幸いです。

グラフィックについて

ファミコンゲームボーイなどを少しかじったことがある方はPPUによるSpriteやBGの画面構成に馴染みがあるかもしれませんが、N64では完全に廃止されRDP (Reality Display Processor) によるポリゴン描画がメインになり、現代コンピュータのGPU描画に近い形態となりました。RDPへの描画コマンドの送信はVR4300i(MIPSアーキテクチャ64-bit CPU) からも可能ですが、基本的にはRSP (Reality Signal Processor) にマイクロコードを乗せてコマンド送信します。

従来の古典的ハードウェアと違い、描画領域VRAMは予め確保されておらず、主記憶RDRAMのうち描画領域をソフトウェア側でMMIOビデオインターフェースを通じて指定します。画面の幅や高さも独自で指定可能ですが、N64の多くの商業用ゲームは320x240のSD画質で表現されています (ただし、WiiのVCの一部タイトルでは640x480の2倍の解像度に修正されていたりします)

現代のビデオゲームと同様に、libultraやlibdragonではダブルバッファリングしています。

サウンドについて

8, 16-bit音、ピコピコ音が特徴的なAPUを搭載した古典的ハードウェアから脱却し、N64MIDI再生が可能となりました。N64はオーディオ専用のプロセッサを搭載していないので、基本的に上記でも登場したRSPを利用して音を流し込みます。実はRSPはたったの4KiBのマイクロコードしか搭載できないので、1フレームの間だけでかなりRSPのマイクロコードが書き換わります。

余談ですが、F-ZERO XスターフォックスはリアルタイムでCPU処理の負荷軽減のため、MIDIファイルを圧縮せずにそのまま再生しているので、ROMファイルの大半が音源ファイルであると言われています。

ゲームボーイなどのサンプリングレートは44.1kHz固定ですが、N64ではサンプリング周波数を変更できるので多くの商用ゲームではこの数を割っています。

VR4300iについて

NINTENDO64の名が表す通り、VR4300iは時代を先取りした64-bit CPU でございます。同世代PlayStation1、セガサターンは32-bit幅であるのはもちろんのこと、後の任天堂ハードウェアGCWiiWii Uに至るまで基本的に32-bitであったことを考えるとこの特異性が伺えます。N64は32個も64-bit汎用レジスタを持っています。

かつてのZ80プログラミングにおいては条件分岐やforループがかなりクリティカルポイントになっていたところですが、本ハードウェアは5段パイプライン処理で分岐命令に対して遅延スロットを設けており、パイプラインハザードが大きく軽減されています。

メモリアクセスは64-bit仮想アドレスから32-bit物理アドレスMMUを通して変換されます。従来のハードウェアと違い、手動でメモリバンクを切り替えなくてよくなったのでプログラマのメモリ管理の負担が少なくなったかもしれません。

N64と携帯ゲーム機DSはスペックが似ているイメージがあるかもしれませんが、DSとは違いN64浮動小数点ユニットFPUを備えています。これにより、float値やdouble値を扱うことができるようになるため、正確な数値計算が可能になります。

RSPについて

さて上記で登場したRSPですが、RSPは基本的にはVR4300iと同じMIPSアーキテクチャのCPUが実態となります。詳細は解説しませんが、RSPでは例外処理といった一部機能が制限されている上、アドレス幅は64-bitではなく32-bitであるために64-bit演算命令が使えなかったりします。しかし、RSPの最大の特徴はベクトル演算の命令が可能であることです。これにより、高速な行列変換が可能になり3Dゲームにおける座標変換が効率よく行えます。

RSPの詳細な命令を知りたい方は以下をご確認ください。

現状、RSPのマイクロコードを高級言語で抽象化したものはまだメジャーなものが存在しないので、もし自分でRSPコードを書きたいときはMIPSアセンブリで直接記述する必要がありそうです(libdragon/examples/rspqdemo が参考なると思います)

RSPは、VR4300iとは完全に別のメモリ(SP IMEM / SP DMEM)を使用します。IMEMはマイクロコード書き込み用の命令用のメモリ、DMEMはRSPが使用するデータ用のメモリでどちらも4KiBです。RSPはVR4300iが使用するRDRAMにアクセスできません。つまり、DMEMのみアクセスできます。

RDPについて

上記の通り、RDPには頂点情報やカラー情報、テクスチャ情報などの情報を含んだ描画コマンドを送信してメモリ (4MiBのRDRAM) の描画領域を書き換えます。N64は世界初のアンチエイリアシングが可能なゲームハードウェアですのでアンチエイリアス描画ももちろん可能です。ブレンド、ミップマップ、Zバッファリングといった現代グラフィックAPIで馴染み深い機能も搭載されいます。描画コマンドは以下の資料で確認できますので、少し見てみると雰囲気がわかると思います (が、かなり間違っている部分が多いのでご注意ください)

https://ultra64.ca/files/documentation/silicon-graphics/SGI_RDP_Command_Summary.pdf

上述の通り、コマンド送信はVR4300iからでもRSPからでも可能です。特筆すべきことに、三角形描画のコマンドだけではなく、長方形描画のためのコマンドも別で用意されています。確かlibdragonのRSPQ内部では四角形描画のときに、回転量が0かそうでないかで三角形に分割するかしないか選択してた気がします。

RSPとRDPはまとめてRCP (Reality Co-Processor) と呼ばれることもあります。

さて、ディスプレイ描画に関してですが、実はRDPを介さなくてもVR4300iから直接描画領域を書き換えるソフトウェア描画が可能です。実は、先程のサンプルコードでも使用したgraphics_draw_text 関数はソフトウェア描画になっています。ただし、自明なことですがソフトウェア描画は圧倒的に遅いので基本的にRDPから描画する必要があります。

VCの時のオカリナにおいて、実機と水の神殿のボスのグラフィックが微妙に違っていたりするのは透明処理でソフトウェア描画を使ったりしていて正しくエミュレートできていないからなのではないでしょうか (要検証)

RDPのテクスチャメモリはたったの4KiBしかありません。そのうち、2KiBはTLU(Texture Look up)テーブルに使用されるので、一度にわずかなサイズの画像テクスチャしか使用できません。これは、多くのN64プログラマの悩みの種となったようです。

図 (手書き)

以下は、VR4300iやRCPなどをまとめた簡単な図です。

おしまい

N64ゲーム開発に興味を持っていただけたら幸いです。

素敵なN64ゲームやライブラリを作ってくださいね。

DOTweenとUniTaskのキャンセル処理について

DOTweenとDOTweenの間にキャンセル処理を挟むとき....

await transform.DOMoveY(5.0f, 0.3f).SetEase(Ease.OutBack)
    .ToUniTask(cancellationToken: cancel);
cancel.ThrowIfCancellationRequested();
await transform.DOMoveY(-5.0f, 0.3f).SetEase(Ease.OutBack)
    .ToUniTask(cancellationToken: cancel);

これだとコードが冗長になってしまう感じがする

キャンセル用の拡張メソッドをつくる

    public static class UniTaskExtensions
    {
        public static async UniTask ThrowIfCancelled(this UniTask task, CancellationToken cancel)
        {
            await task;
            cancel.ThrowIfCancellationRequested();
        }

        public static async UniTask ThrowIfCancelled(this Tween tween, CancellationToken cancel)
        {
            await tween.ToUniTask(cancellationToken: cancel).ThrowIfCancelled(cancel);

        }
    }

こうすれば簡潔に書けてよさそう

await transform.DOMoveY(5.0f, 0.3f).SetEase(Ease.OutBack).ThrowIfCancelled(cancel);
await transform.DOMoveY(-5.0f, 0.3f).SetEase(Ease.OutBack).ThrowIfCancelled(cancel);

VisualC++で異なるディレクトリに同名ファイルが入っていてもビルドが通るようにする

以下の記事を参照した。

altebute.hatenablog.com

構成プロパティ / C++ / 出力ファイル

ASM リストの場所オブジェクト ファイル名を変える。

上記事では $(IntDir)%(RelativeDir) であったが、%(RelativeDir)絶対パスになってしまったのでこれだけならビルドが通った

追加: <ObjectFileName>$(IntDir)%(Directory)</ObjectFileName> でいけた

既存のChromeの拡張機能を改造する

既存のChrome拡張機能を改造する

  1. ローカルに保存されているソースディレクトリをコピー

    • Windowsの場合は %LOCALAPPDATA%\Google\Chrome\User Data\ まで行ってから {拡張機能ID} で探せば出てくる
  2. 識別しやすいように、コピーした方にあるmanifestの "name"を一応変える

  3. Chrome拡張機能画面 (chrome://extensions/) から拡張機能をパッケージ化を押して パッケージ化されていない拡張機能を読み込む で読み込む