ターン制オンラインゲーム(カタン)の作り方:RPC

今回はターン制オンラインゲームの作り方について、カタンボードゲーム)を例に紹介していきます。

自作オンラインカタン

カタンボードゲーム)の特徴

カタンのようなボードゲームの場合、アクションゲームのような高速同期システムは不要なので、ある程度はシステムをシンプルに作れます。だたし、ボードゲームでは1試合が長く多人数でプレイするため、プレイヤーのネットワーク切断を考慮に入れた設計をしなければゲームが成立しにくくなります。切断後に再接続を行っても、不整合が発生しないように注意する必要があります。

ターン制オンラインゲームに必要な2つの仕組み

実は、ターン制オンラインゲームで必要なシステムを最もシンプルに考えると、シングルプレイヤーゲームに追加するべきものは以下の2点のみです。

1.ルームシステム

2.RPCシステム

ルームシステム

まずは、ルームシステムです。マッチングシステムと言ってもいいです。ボードゲームなので、ルーム作成が基準になるでしょう。二人プレイならランダムマッチでもいいでしょうね。役割としては、連絡先の交換と通信の中継です。ルームシステムには、基本的にどこぞのサーバーを利用することになります。Unityで有名なPUNでは、最大人数に制限はありますが無料でサーバーを利用できますし、後述のRPCシステムも一緒に付いてきます。(ただPUNはもうレガシーでFusionが後継のようです。)システム作れるならもちろんマイサーバーでもいいですし、どこかのクラウドサーバーを使ってもいいですね。

UI以外は同じものが見える

RPCシステム

RPCとは、Remote Procedure Callの略で、遠隔コード実行です。簡単に言うと、相手クライアントに所定のコードを(引数指定して)実行させるということです。セキュリティの文脈でもたまに出てきますね。もし所定コードのではなくて、任意コードを実行できる状態になってたら脆弱性なので注意しましょう。

基本的に、ターン制オンラインゲームではほぼ全ての関数コールをRPCを通して行うことになるでしょう。カタンを例に紹介します。

  1. ルーム作成(一人目・マスター)
  2. 二人目以降入場(全プレイヤーが自己情報をRPCで通達)
  3. ゲーム開始・地形生成(マスターがRPCで通達)
  4. ダイスロール(RPC)
  5. アクション(RPC)

こんな具合に、全プレイヤーで共有しておくべき情報は全てRPCで行います。ここで、RPCは遠隔コード実行ですが、自分に対しても発動させることでコードを省略できます。

ダイスロールでいうと、プレイヤーがダイスロールボタンを押すと、RPCのDiceRoll関数にダイス結果の引数が与えられて実行されます。自分を含む全プレイヤーが同じダイス値のDiceRoll関数を実行するので、その通りに各自アニメーションなどで演出しましょう。

地形生成では、RPCでMapGenerate関数に乱数シードを与えて実行します。全プレイヤーで共通の乱数シードを使えば、乱数を使う場面でも全プレイヤーに同一の結果がでます。全プレイヤーが共通の回数で乱数を使うようにすればかなりの通信を節約できます。

ゲーム進行など、一人が代表して行うべき処理もあります、RPC関数内にてif (MasterClient) {} で分岐して処理しましょう。

自分のカタンでは、以下図のようにRPCとゲームフローを作成しています。

カタンのコントロールフロー

上記のコントロールフローに載っている事柄はRPCで実行しています(他にもある)。ひとつひとつの操作で、割と細かくRPCを実行しています。他プレイヤーへの通達のためと、自分の切断・復帰時の行動ログとしての役割もあります。

回線切断・復帰対策

回線切断で落ちた後、同一ルームに帰ってきたときに必要な処理は同期です。ここで、全プレイヤー分の行動ログの保存は、全てのRPCコールを保存しておくことで実現できます。復帰してきたプレイヤーは、RPCコールを古いものから全て順番に実行すると、切断前とほぼ同じ状態に復元できます。

ここで、復元の注意点について紹介します。

UI操作であってもRPCで実行する

相手プレイヤーにYes/Noダイアログを出すRPCがあることはわかると思います。そして、その返答もRPCで行われることもわかると思いますが、ここで、ダイアログの消去も返答のRPC上で行いましょう。もしダイアログ消去をRPC上で行わなかった場合、復帰してきたときに返答済みのダイアログが残ってしまいます。カタンではコントロールフロー上の「パネル消し」が相当します。

Yes/Noダイアログ

アニメーションは全て投げっぱなしで

もしアニメーション終了を起点に関数を実行しようとしているのならそれはやめておきましょう。復帰時にRPCコールとアニメーション時間の整合性が取れなくなるためです。アニメーションは時間指定で実行しておき、次のアニメーションが来た場合は即座に終了状態までアニメーション遷移してから次のアニメーションにとりかかるようにしましょう。

カタンでは、ダイス値を決めてダイスが動き始めるRPCの後、アニメーションと同じ時間が経った後ダイス値に応じたアクションを行うRPCが送られます。それでもアニメーション中にダイスを回したプレイヤーが落ちると破綻するので、もうひと工夫必要です。

全プレイヤーで情報共有する

もしマスタークライアントのみが知っている情報などがある場合、マスタークライアントが落ちると情報が消えてしまいます。また、マスタークライアントが落ちるとマスターが移譲されるシステムが多いでしょう。マスター以外必要ないようなゲーム進行情報だとしてもできるだけ共有しておきましょう。

RPCログ

StableDiffusionの歴史

StableDiffusionの初版は、2022年8月22日に公開されました。

まずは基礎として、Latent space、VAE、ノイズ除去、U-Net、CLIPなどキーワードの解説です。

Latent space

StableDiffusionでは、画像をピクセル基準ではなく、主に潜在ベクトルとして扱います。大体768次元くらいの大きさです。768個の値が、1枚の画像全てを表現することになります。

VAE

VAEは、Value Auto Encoderの略です。値を変換するということで、Latent spaceの潜在空間ベクトルとピクセル空間画像を変換する役割があります。Encoderがピクセル→ベクトル、Decoderがベクトル→ピクセルです。txt2imgの場合は最終段階でVAEの出番となっています。

ノイズ除去

StableDiffusionは、ノイズ除去拡散モデルが元になっています。ノイズ除去拡散モデルというのは画像にノイズを徐々に加えていき完全なノイズになるまでの過程を学習し、ノイズから画像への復元を実現するものです。

これを潜在空間ベクトルで行い、なおかつノイズ除去時にcross attentionとしてノイズ除去の方向性を指定する仕組みを追加したものがStableDiffusionです。

U-Net

ノイズ除去に使うモデル構造が、U-Netです。これは、扱うデータの抽象度を段階的に落としながら処理を行い、次に段階的に抽象度を上げながら処理することで、抽象度の高い情報から詳細な情報まで考慮に入れることができます。StableDiffusionでも層によって大まかに構図の層から指・塗り方の層まで抽象度の違う情報を処理しているそうです。

CLIP

CLIPは、画像とテキストを変換するためのモデルで、画像のテキスト説明、テキストの潜在ベクトル化を行えます。StableDiffusionでは最初のプロンプト入力段階でテキストの潜在ベクトル化を利用しています。

 

StableDiffusion v1.4 2022年8月22日

最初に公開されたモデルがv1.4です。実写風の画像生成が主で、イラスト風画像は非常に困難でした。かなり頑張ってCG風画像や、名画風、もしくは超デフォルメ風画像が限界でした。

手足の破綻率はかなりのもので、指が正確に生成されることはほぼありません。顔もかなりの確率で破綻するので、顔修復専用の機械学習モデルが追加で使われていました。

StableDiffusion v1.4

WaifuDiffusion v1.2 2022年9月8日

有志による追加学習により、イラスト風画像の生成を目指したモデルです。今ではもう公開されていませんが、様々な品質のイラストと実写画像が融合したような画像が生成されるようになりました。

WaifuDiffusion v1.2

NovelAI 2022年10月3日

NovelAI社が公開したStableDiffusionをベースに多少カスタマイズ、追加学習を行い、オンラインで上で利用できる有料画像生成機能です。非常に優れた品質のアニメイラスト画像を生成できました。Danbooruというイラスト転載・タグ付けサイトのデータが利用され、プロンプトがタグベースで記述できるようになっています。

当時の学習

ところで、このころは特定のキャラクターなどを追加学習するためにはモデル全体に対して学習するのが一般的でした。つまり、1キャラクターごとに4~8GBのモデルが必要になります。

そこで、簡易的な方法が出てきます。

Textual Inversion

Textual Inversion (TI)は、画像の生成方法をモデル側で学習するのではなく、Text側、つまりプロンプトとして表現される、意味のベクトルを学習するものです。例えば白髪ツインテエルフを「フリーレン」という単語に覚えさせることができます。しかし、モデルが表現できる範囲の画像しか生成されませんので、細かい特徴は一切学習できません。

 

WaifuDiffusion v1.3 2022年10月7日

さらなる追加学習により、v1.2よりイラスト品質の向上と実写感の減少が達成されました。破綻率も多少改善されました。

WaifuDiffusion v1.3

NovelAIモデル流出 2022年10月6日

NovelAIが追加学習し、有料画像生成のために利用されていたモデルが流出しました。多少カスタムされているとはいえStableDiffusionのモデルなので、少しの手間でローカル利用のモデルとして利用できたそうです。

 

StableDiffusionのモデルは、ミックス・マージすることが可能です。単純に値を2つのモデルの中間にしてもよいし、EMAと呼ばれる学習度合いのような値を使って2つのモデルのいいとこ取りをすることも可能です。多くの追加学習モデルをマージしていくと、多くのモデルの特徴を持ったモデルを作ることも可能です。

 

Anything v3.0 2022年11月13日

出自不明のモデルですが、NovelAIモデルと同等か上回るような性能を持ったイラストモデルです。NovelAIと同じくDanbooruタグが有効な他、生成される画像にもNovelAIと似た傾向があるため流出モデルがマージ成分として含まれていると考えられます。手足の破綻はかなり少なく、顔修復は不要になりました。

Anything v3.0 (これはVAE不良)

StableDiffusion v2.0 2022年11月24日

StableDiffusionの次世代モデルで、より大きな画像解像度で学習されたモデルで、CLIPモデルも変更されているなど、v1.4とは異なるモデル構造のため旧モデルとは非互換の新しいものでした。しかし、手足の破綻問題はあまり改善されていなかったり、要求スペックが高い他、非互換のために旧モデルの学習結果を利用できなかったためあまり普及しませんでした。

 

WaifuDiffusion v1.4 2023年01月01日

StableDiffusion v2.0 をベースモデルとして学習されたイラストモデルで、NovelAI流出モデルを使用していないクリーンなモデルと言えます。しかし、そこまで品質は高くありません。

WaifuDiffusion v1.4

少し停滞と洗練

ここから、しばらくStableDiffusionモデルの進化はゆっくりになります。他社のMidjourneyやDallといったStableDiffusionより優れた品質のものが出てきますが、いずれも非ローカルの有料サービスです。

StableDiffusionでは、マージをU-Netの層別化によるイラストモデルと実写モデルのいいとこ取りが流行ったり、追加学習の効率化や、生成の高速・省メモリ化が行われていました。

追加学習法

Hyper Network

Textual Inversionの学習ではモデルが表現できないものは学習できず、モデル学習では学習結果が数GBにもなりました。100MB程度でモデル追加学習を保存できるのがHyperNetworkです。キャラクター再現や、画風再現も可能だそうです。ただし、学習に必要なGPUスペックは多いのでそこまで流行っていません。

Dream Booth

こちらもHyper Networkと似たような追加学習方法の1つです。HyperNetworkよりも学習再現率が高いですが、モデルは数GBにもなるのでこれも流行っていません。

LoRA

Low-Rank Adaptation of LLMの略です。Hyper NetworkやDream Boothよりも低VRAMで高速に学習でき、学習結果も数十MB程度で収まります。かなり手軽なので多くのキャラクターLoRAやポーズ、顔表情のLoRAが作られています。

ControlNet 2023年02月16日

プロンプトもしくはimg2imgの利用でしか生成結果をコントロールできなかったStableDiffusionですが、ControlNetにより詳細なimg2imgコントロールを行えるようになりました。棒人間によってポーズを決めたり、線画に色付けしてもらったり、写真をイラスト化してもらったり、落書きから完成まで持っていってもらったりできます。

ただしモデルが表現できないものは描けません。

 

たくさんのモデル

これまでのモデルは、huggingfaceという機械学習版のgithub風サイトで公開されたものがほとんどでしたが、CivitAIというStableDiffusion特化の共有サイトの利用が増えていきます。

モデルも、イラストからCG,実写まで非常に多種多様な専用モデルが作られました。

4つのモデル

StableDiffusion XL (SDXL) 2023年07月27日

StabilityAIより公開された、StableDiffusion v2.0の次世代モデルです。学習解像度が1024x1024基準となっており、v1.4の512x512と比べると4倍の画素数になっています。

かなり広範囲の学習をされているようで、少しの追加学習で効果がでる柔軟さをもったモデルになっているそうです。手足の破綻が少ない上に、v1.4系列では困難だった指の生成もかなり破綻率が少なく生成できます。v2.0と同様に前モデルとの互換性はありませんが、互換性がないのでNovelAI流出問題もありません。SDXLの利用者は増えているようです。

SDXLベースモデル

高速化

最近は、生成の高速化が流行っている印象です。NvidiaGPUのTensorCoreを利用したTensorRTや、高速生成に最適化されたLCM SamplingLCMモデルなどがあります。

高速化を利用して、今後は動画生成も増えていくことでしょう。

 

Unityでボリュームレンダリング ~CTデータビジュアライゼーション~

Unityでボリュームレンダリングをするのに必要な実装について簡易的に解説してみます。方式としてはダイレクトボリュームレンダリング・レイキャスティングです。

 

今回は以下2編です。

データ入力編

カスタムシェーダー編

コンピュートシェーダー編

 

データ入力編

ボリュームデータは、3次元画像の形で表せます。Unityでは、Texture3Dとして3次元画像がサポートされていますので、そちらを使います。

CTデータは、3次元×CT値です。CTの値域は-1000 ~ 1000といった値をとりますので、通常の8bitカラーでは不足します。16bit浮動小数点のTextureFormat.RHalfあたりが適当でしょう。こちらはスカラー3次元画像用フォーマットです。

 

入力するボリュームデータは16bit整数(short)のraw形式であるとします。dicomを使いたかったらライブラリを探してきましょう。

rawはこんな感じで始まるデータ



入力の擬似コード

バイナリリーダーを用意し、ファイルを読み込みます。

BinaryReader reader = new BinaryReader(new FileStream(filePath, FileMode.Open));

3DTextureを作る前に、ボリュームデータを配列(1次元)に入れます。

float[] data = new float[dimX * dimY * dimZ];

ここで、ボリュームデータのエンディアン(バイトオーダー)に注意!データとOSの組み合わせによっては変換が必要です。

ushort dataval = reader.ReadUInt16();

トルエンディアンからビッグエンディアンへの変換はこんな感じ

dataval = (ushort)(((dataval >> 8) | (dataval << 8)) & 0xFFFF);

データ値は、0~1の範囲の値に変換するとテクスチャアクセスの都合上扱いやすいです。シェーダーでは、テクスチャに0~1でアクセスします。

dataval = (dataval - min) / range;

 

forでもなんでもいいので回して配列に入れられたら実際にTexture3Dを作ります。

Texture3D texture3d = new Texture3D(dimX, dimY, dimZ, TextureFormat.RHalf, false);
texture3d.filterMode = FilterMode.Bilinear;
texture3d.wrapModeW = TextureWrapMode.Clamp;

作ったTexture3Dにデータを流し込みます。

texture3d.SetPixelData<float>(data, 0);
texture3d.Apply();

 

最後にキューブのシェーダーにデータを渡します。(シェーダー作った後で)
meshRenderer.sharedMaterial.SetTexture("_DataTex", 3dtexture);

 

これでボリュームデータの入力は完成です。

 

※発展

色設定はTexture2D(RGBA)で表現します。テクスチャの左端が0~1に変換したデータの0、右端がデータの1に対応する色に塗ります。

 

カスタムシェーダー編

Unityで3Dレンダリングするためには、基本的にMeshRendererが必要です。

また、ボリュームは3Dオブジェクトで、データの形も立方体(直方体は一旦無視)なので、プリミティブのキューブ立方体を用意しましょう。

Unityでボリュームレンダリングする際は、キューブのメッシュレンダリングを、ボリュームレンダリングに書き換えます。

 

カスタムシェーダー

では、カスタムシェーダーを作成します。かなり難しいのでUnityドキュメントをよく読む必要があります。

 

Unlit Shaderを改変していきます。

まず、Properties。マテリアルの項目設定にあたります。この場合3Dテクスチャを1つと、0~1の値を2つ設定できるマテリアルになります。

Properties
{
    //マテリアル
    _DataTex("Data Texture", 3D) = "" {}
    _MinVal("Min val", Range(0.0, 1.0)) = 0.0
    _MaxVal("Max val", Range(0.0, 1.0)) = 1.0
}

次はシェーダー設定。透過シェーダーであることを設定しています。途中のブロックコメントはコードのフォーマッタで壊されないためにあります。

//レンダリングステート
Tags { "Queue" = "Transparent" "RenderType" = "Transparent"}
//カリングモード
Cull /* */ Front
//デプスバッファテストモード
ZTest /* */ LEqual
//デプスバッファ書き込みモード
ZWrite /* */ On
//ブレンドモード アルファ値乗算 フレームバッファの(1 - Source Alpha)を乗算 # 昔ながらの透明
Blend /* */ SrcAlpha /* */OneMinusSrcAlpha
そしてCGPROGRAMの中です。
Unity組み込み関数を入れておきます。
    //頂点シェーダとしてvertをコンパイル
    #pragma vertex vert
    //フラグメントシェーダとしてfragをコンパイル
    #pragma fragment frag
 
#include "UnityCG.cginc"
入出力の内容を設定しています。
struct vert_in
{
    UNITY_VERTEX_INPUT_INSTANCE_ID //GPUインスタンスID
        //命名 : セマンティクス
    float4 vertex : POSITION; // 頂点位置
    float4 normal : NORMAL; //法線ベクトル
    float2 uv : TEXCOORD0; // テクスチャ座標
};

struct frag_in
{
    UNITY_VERTEX_OUTPUT_STEREO //GPUインスタンスID
        //SV... システム値セマンティクス
    float4 vertex : SV_POSITION;
    float2 uv : TEXCOORD0; //0
    float3 vertexLocal : TEXCOORD1; //1
    float3 normal : NORMAL;
};

struct frag_out
{
    float4 colour : SV_TARGET; //レンダー ターゲットに格納される出力値
    float depth : SV_DEPTH; //深度バッファー データ。 ピクセル シェーダーで記述できます。
};
マテリアルにアクセスするための宣言。同じ名前なら勝手にリンクされます。
sampler3D _DataTex;

float _MinVal;
float _MaxVal;
ボリュームレンダリング用のレイ構造体です。
//レイ単体
struct RayInfo
{
    float3 startPos;
    float3 endPos;
    float3 direction;
        //交差距離 近・遠
    float2 aabbInters;
};
//レイマーチング(レイを含む)
struct RaymarchInfo
{
    RayInfo ray;
    int numSteps;
    float numStepsRecip;
    float stepSize;
};
頂点シェーダー。座標系変換が行われています。この先出てくる見慣れない関数は大体Unity組み込みかHLSLの組み込みです。
        // 頂点シェーダエントリ
frag_in vert(vert_in v)
{
  // フラグメントシェーダ入力
    frag_in o;
    UNITY_SETUP_INSTANCE_ID(v);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = v.uv;
    o.vertexLocal = v.vertex;
    o.normal = UnityObjectToWorldNormal(v.normal);
    return o;
}
レイ生成関数1。レイの方向がわかります。
float3 getViewRayDir(float3 vertexLocal)
{
    if (unity_OrthoParams.w == 0)
    {
            // Perspective
            // カメラへのオブジェクト空間方向 (正規化なし)
        return normalize(ObjSpaceViewDir(float4(vertexLocal, 0.0f)));
    }
    else
    {
            // Orthographic
        float3 camfwd = mul( (float3x3) unity_CameraToWorld, float3(0, 0, -1) );
        float4 camfwdobjspace = mul(unity_WorldToObject, camfwd);
        return normalize(camfwdobjspace);
    }
}
レイ生成関数2。ボックスとの交差地点がわかります。
// 軸平行境界ボックスより交差地点を見つける
// 近・遠の距離を返す
float2 intersectAABB(float3 rayOrigin, float3 rayDir, float3 boxMin, float3 boxMax)
{
    float3 tMin = (boxMin - rayOrigin) / rayDir;
    float3 tMax = (boxMax - rayOrigin) / rayDir;
    float3 t1 = min(tMin, tMax);
    float3 t2 = max(tMin, tMax);
    float tNear = max(max(t1.x, t1.y), t1.z);
    float tFar = min(min(t2.x, t2.y), t2.z);
    return float2(tNear, tFar);
}
レイ生成関数3 レイを生成します。プリミティブのキューブを使ったのは、キューブサイズを1に固定するためでした。これで、テクスチャ座標系の0~1でのレイができました。
RayInfo getRayFront2Back(float3 vertexLocal)
{
    RayInfo ray;
    ray.direction = getViewRayDir(vertexLocal);
    ray.endPos = vertexLocal + float3(0.5f, 0.5f, 0.5f);
        // Find intersections with axis aligned boundinng box (the volume)
        // (1, 1, 1)サイズボリューム
    ray.aabbInters = intersectAABB(ray.endPos, ray.direction, float3(0.0, 0.0, 0.0), float3(1.0f, 1.0f, 1.0));

        // Check if camera is inside AABB
        // ボリューム内カメラ調整
    const float3 farPos = ray.endPos + ray.direction * ray.aabbInters.y - float3(0.5f, 0.5f, 0.5f);
        // オブジェクト空間からカメラのクリップ空間へ点を変換
    float4 clipPos = UnityObjectToClipPos(float4(farPos, 1.0f));
    ray.aabbInters += min(clipPos.w, 0.0);
   
    ray.startPos = ray.endPos + ray.direction * ray.aabbInters.y;
    ray.direction = -ray.direction;
    return ray;
}
レイマーチングの構造体です。
        // レイマーチングを作成
RaymarchInfo initRaymarch(RayInfo ray, int maxNumSteps)
{
    RaymarchInfo raymarchInfo;
    raymarchInfo.stepSize = 1.0 / maxNumSteps;
    raymarchInfo.numSteps = (int) clamp(length(ray.startPos - ray.endPos) / raymarchInfo.stepSize, 1, maxNumSteps);
    raymarchInfo.numStepsRecip = 1.0 / raymarchInfo.numSteps;
    return raymarchInfo;
}
ボリュームデータ値へのアクセスです。0~1でアクセスします。
        // ボリュームスカラー値取得
float getDensity(float3 pos)
{
    return tex3Dlod(_DataTex, float4(pos.x, pos.y, pos.z, 0.0f));
}
深度取得の関数です。深度は他オブジェクトとの前後を決定するために使われます。
    // Converts local position to depth value
    // デプス取得
float localToDepth(float3 localPos)
{
    float4 clipPos = UnityObjectToClipPos(float4(localPos, 1.0f));

#if defined(SHADER_API_GLCORE) || defined(SHADER_API_OPENGL) || defined(SHADER_API_GLES) || defined(SHADER_API_GLES3)
        return (clipPos.z / clipPos.w) * 0.5 + 0.5;
#else
    return clipPos.z / clipPos.w;
#endif
}
ではコアの部分です。レイを作成し、ボリューム内をループで行進、カラーを積算し、最後に書き出しています。
frag_out frag(frag_in i)
{

    RayInfo ray = getRayFront2Back(i.vertexLocal);
   
    RaymarchInfo raymarchInfo = initRaymarch(ray, 128);

    float4 col = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float tDepth = raymarchInfo.numStepsRecip * (raymarchInfo.numSteps - 1);
        // レイマーチング
        [loop]
    for (float iStep = 0; iStep < raymarchInfo.numSteps; iStep += 1)
    {
        const float t = iStep * raymarchInfo.numStepsRecip;
        const float3 currPos = lerp(ray.startPos, ray.endPos, t);
 
       const float density = getDensity(currPos);

            // Apply visibility window
            // 可視スカラー範囲
        if (density < _MinVal || density > _MaxVal) continue;

       float4 src = float4(1.0f, 1.0f, 1.0f, density);
// 前からブレンド
    src.rgb *= src.a; // サンプルカラー
        col = (1.0f - col.a) * src + col; // 積算値とブレンド
    }
   
    //透明時は0の深度
    float fragDepth = step(0.01f, col.a) * localToDepth(ray.startPos);
       
    //フラグメント書き出し
    frag_out output;
    output.colour = col;
    output.depth = fragDepth;
    return output;
}
以上でシェーダーの完成です。お好みでsrcカラーの値をdensityに応じて変えましょう。
 
作ったシェーダーは、まずマテリアルを作ってシェーダーを割り当て、そのマテリアルをキューブのMeshRendererに割り当てると使用可能です。
 
今回は陰影やライティングは全く考慮していない簡単なバージョンを紹介しました。
陰影やライティングを考慮する場合は、法線を生成し、ライト位置を取ってきてシェーディングしましょう。また、深度も正確なものにする必要がありますね。
 

今回つくったもの

シェーダー(マテリアル)
 

参考

(これの簡易化版だよ!)

github.com

 

コンピュートシェーダー編

ボリュームはデータ量が大きいので、全体処理するときはコンピュートシェーダーを使うと高速化できます。法線計算だったり0~1化だったり。ただしメインメモリとGPUメモリの複雑な操作が必要になります。

 

AIイラスト 呪術師 vs 調整職人 vs 絵師 【Stable-Diffusion-WebUI】

この記事はStableDiffusion-webuiを触った個人的感触を書いたものです。

はじめに

なにもStableDiffusionに望みの絵を描いてもらう方法は1つではない。

一発で至高の1枚を描けるプロンプトを探す呪術師、ガチャとパラメータ修正を繰り返して理想に近づけていく調整職人、自ら描く絵の補佐を頼む絵師。

これらの3種類の描き方を紹介していく。

 

呪術師

StableDiffusion-webuiでは、正の意味をもつプロンプトと、負の意味をもつネガティブプロンプトをそれぞれ75トークンほど指示することができる。(トークンは単語とほぼ同じ)

そして、プロンプトは意味ベクトルとなり、描くものを決めていく。ここで大事なのはベクトルであるということだ。つまり、意味上の加算・減算ができる。例えば"花" - ”赤" = "赤以外の花"だ。

次に大事なのは、トークン(単語)の意味は辞書的で単一なものではないということだ。辞書でなければ何なのかというと、その単語が(学習時に)よく用いられる状況的な意味を指している。簡単に言うと、AIは偏見がすごく、単語に多くの意味を持っている。例えば"日本"→"和装・神社仏閣""テロリスト"→"中東人""水着"→”プール”, といったようにステレオタイプと拡大解釈に溢れている。

そして、プロンプトが呪文と呼ばれる由縁になっているのは、トークン(単語)に人間が知らない意味を見出していることがあるからだ。呪術師の間では、"high detailed"や、"4k 8k""masterpiece" などといった極めて主観的なプロンプトが使われている。AIは、人間が高精細と判断したり、傑作と呼ぶ作品に画像的な共通点を見出している。

これらのプロンプトに関する特徴を組み合わせ、長大なプロンプトを書き上げていくのが呪術師のやり方だ。

簡単な例はこんな感じ

プロンプト:「傑作、水着少女

ネガティブ:「低クオリティ、プール」

 

プロンプト以外にも重要な項目が2つあるので記しておきたい。

縦横比と、CFG Scaleだ。

縦横比は、文字通り生成画像のアスペクト比である。正方形のままだと、頭部が見切れてしまうことがよくある。この場合、縦長にすると改善することが多い。重要なのは、縦横比が異なれば生成する画像の趣向も変わるということだ。縦長だと立ち絵で、横長だと複数人出たり、足を伸ばした構図になったりする。

CFG Scaleclassifier-free guidance scale)は画像生成でどれだけプロンプトに従うかという説明がなされるが、基本的には5~15の範囲内で動かす必要はない。感覚的にはプロンプトが短いならばCFGも小さめのほうが柔軟で、長いプロンプトならCFGを高めにしないと言うことを聞いてくれない。逆に、CFGが低すぎるとカオスで印象派の絵画っぽくなり、CFGが高すぎると極彩色でAI特有の色収差風なものが現れる。

 

調整職人

StableDiffusion-webuiではテキストから画像を生成するだけでも様々なパラメータを調整できるし、それ以外にも、様々な機能が備わっている。プロンプトはほどほどに、パラメータ調整とランダム性を活用することで、より多彩で意外性のある生成をすることができる。

img2imgでは、画像とプロンプトから画像を生成することができる。この機能の主な使い方は、バリエーション生成か構図指定だ。

バリエーション生成は、txt2imgかimg2imgで生成した画像を入力画像とすることで、微妙な変化をもたらす使い方だ。この際重要なのは、入力画像を生成するのに使った設定を引き継ぐことと、Denoising strengthの値だ。プロンプトや設定が大きく異なると、色彩と形が似ていても全く別のものが生成されたりする。プロンプトに数トークン追加する程度なら大丈夫。そして、Denoising strengthは入力画像にどれだけ近づけないかを指定できる。0.5でほとんど同じ、1で全く別物なので、0.65~0.8ほどでガチャを繰り返すことになる。生成された画像を、さらに入力画像とするといったことを繰り返していく。

構図指定は、バリエーション生成の逆で、プロンプトを一致させない使い方だ。構図の入力は落書きレベルでよい。重要なのは、なにを描いているのか上手くプロンプトでAIに説明することだ。Denoising strengthは高めにする。背景が単色になりがちなのが難しい。

inpaint/outpaintingは、それぞれ細部調整と画角調整に使われる。ただし、こちらを使った結果は不自然なものになりやすく、結果をimg2imgで再描画するとよい。

inpaintは、指定箇所のみを再描画する。設定は元画像と同じでよい。主な用途としては余計なものを消すか、別のものに変えてもらうことだ。手指の修正をこれでやるのは難しい。

outpaintingは、主に見切れてしまった頭部の補完に使う。基本的にわかりやすい境界が拡張部にできてしまうので、inpaintで消してimg2imgで再描画するといった流れになる。

以上のように、調整職人は数多くのガチャを回し続けることになる。重要なのは、プロンプトで伝わりきらない意味を、入力画像とシード値で伝えるということだ。(プロンプトが少々異なっても、同じシード値であればAIの魂が似ていることに気づくだろう)

 

絵師

これまで紹介してきた呪術師や調整職人は、基本的に自ら線を描かない。呪術師や調整職人が行う方法と、自らが描く絵を組み合わせるのが絵師のやり方だ。

絵師は、主にimg2imgを使う。ラフ画を読み込ませ、塗りを担当してもらうケースが多い。AIの一番の得意分野が、実写レベルまで可能な塗りだからだ。塗ってもらった絵に絵師が更に線を加え、さらに再描画してもらうといったように、AIとの対話作業を行うことになる。

逆に、AIにラフだけ描いてもらったり、背景だけ用意してもらうといったことも可能だ。

StableDiffusionWebUIをローカル環境で動かす

はじめに

StableDiffusionをローカル環境で動かすには4GB以上のグラフィックメモリを積んだGPUが必要です。初心者の方には環境構築の簡単なNMKD StableDiffusion GUIの方をオススメします。(機能が少なくロードが長いですが)

NMKD Stable Diffusion GUI - AI Image Generator by N00MKRAD

導入

StableDiffusionWebUIはGitHubで公開されてます。

前提にインストールが必要なのは以下の通り

  • PythonwindowsストアでインストールするだけでOK)
  • Git(アプリ内からもgitコマンド呼ばれてるので必要)
  • CUDA(いらんかも?)

StableDiffusionのモデルデータ(学習済み)も用意する必要があります。有名なのは以下の3つ。好みでどうぞ

あとはReadmeに従えばOKです

親切丁寧な導入記事

gigazine.net

StableDiffusionWebUIの主な機能

  • テキスト→画像 (txt2img)
  • 画像+テキスト→画像 (img2img)
  • 画像+テキスト+領域選択→画像 (Inpaint)
  • 画像+テキスト→拡張画像 (Outpainting)
  • 画像→超解像画像 (Extras)
  • 追加学習機能

公式説明はこちら

Home · AUTOMATIC1111/stable-diffusion-webui Wiki · GitHub

 

続きは次の記事…

VOICEROID? VOCALOID? VOICEVOX? 音声合成ソフトあれこれとキャラクター

大まかな分類

機能によって大まかに3つに分けられる。

続きを読む

JavaからCUDAを触る GPUプログラミング

GPUGPGPU(汎用計算)目的で操作する方法は、CUDAOpenCLが有名です。

ただし、CUDAはNvidiaGPUでしか使えませんし、OpenCLGPU専用ではないのでとっつきにくいです。というわけでNvidiaGPUがあるなら最初はCUDAをオススメします。どちらも、C言語をベースにした言語でプログラミングします。

CUDA

CUDA言語は、C言語の拡張で、CUDA ToolKitに含まれるnvccコンパイラを用いてコンパイルします。

__device__ __host__ float2 operator+(float2 a, float2 b)
{
    return make_float2(a.x + b.x, a.y + b.y);
}

__device__ や __host__ を記述することで、関数がGPUで使えるのか、CPUで使えるのか定義できます。

 

GPU処理の起点

extern "C" __global__ void mapping(int *map, short *ctvol, int *vol)
{
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    vol[tid] = map[ctvol[tid] + 32768];
}

CPU側のコードからCUDAランタイムAPIを呼び出して、__global__が記された関数をGPUで実行させることができます。上記の例だと、tidは何番目の処理かに相当する。

 

JavaからGPUを動かす

Javaコードを直接GPUで動かせるライブラリはAparapiくらいしかありません。Aparapiはビルドが少々難しいうえ、GPUの限られた機能しか使えません。

そこで、JavaコードからCUDAランタイムAPIを呼び出すことを考えます。JavaコードでCPU処理部分を記述し、GPU処理部分はCUDA言語で記述します。

JavaコードからCUDAランタイムを呼び出せるライブラリ

JCuda

おそらく最もライブラリとして完全です。今回はスルー

lwjgl.Cuda

LWJGLはゲームライブラリですが、GLからCUDA、ほかにも沢山組み合わせることができ、Minecraftでも利用されています。Maven一発で入れられるのもいい点です。

 

LWJGLからCUDAを呼び出すサンプルはこちらが参考になります。

github.com

内容はこんな感じです。

  1. GPU初期化
  2. CUDAコードコンパイル
  3. CPUでデータ用意
  4. GPUにデータコピー
  5. GPUで並列処理
  6. CPUに結果をコピー
  7. CPUでチェック
  8. CPU・GPUで確保したメモリを解放

注意点

  • 最初のCUDAファイルは文字列で渡してコンパイルしてもらう
  • Java側でデータはスタック領域に配置しなければGPUメモリにコピーできない
  • スタック領域では明示的なメモリ開放が必要
  • 一次元配列以外のデータを渡すのはけっこう面倒

CUDAでのベクトル関数はこちらが便利

github.com