Heliodor 2021/06/18 19:30

少しだけ詳しく水たまりプログラム説明 その5

「暫く人と会わんし」とニンニク一気食いしておかしくなっていた味覚がようやく元に戻ってきました。
なんかまだ胃の調子がおかしい気がしますがまたニンニクたくさん買ってきます、ヘリオドールです(挨拶)。



少し間が空いてしまいましたが少しだけ詳しく水たまりプログラム説明 その4の続きです。

とうとうピクセルシェーダーに手を出します。
……そう、私は今まで水面計算においては色々あってGPUの力をほとんど借りてこなかったんです。

水面計算をするには、水面上に格子を割り当てて、格子の各交点に当たる部分(質点)についての上下運動を計算していました。


前回のこんなの。

・格子状に並んだ大量の質点の計算
・一つの質点に影響するのはそれに隣接する質点だけ
・全く同じ計算式を全ての質点に対して実行する

それってまさにピクセルシェーダーでやる事ではないですか。
もう腹を括ってやるしかない!!


ピクセルシェーダーで処理するということは、チマチマとポリゴンメッシュを作る必要は無くなります。
例えば今までは 64x64 個の質点が並んだ水面格子を処理したい場合はそのまま 64x64 個の頂点からなるポリゴンメッシュを作り、各頂点を計算する必要がありました。

しかしシェーダーで処理すれば、テクスチャの1ピクセルがそのまま1質点に対応することになるので、64x64ピクセルのテクスチャを処理すれば良い、ということになります。
こうして計算して出来上がった水面画像を、たったの4頂点から成る四角形ポリゴン(1枚!)に貼り付れば良いだけです。


話が劇的に単純になりましたね!
この方法なら 1024x1024 とか 2048x2048とかの膨大な量の質点の計算も無限のGPUパゥワァァァァーで簡単にできます。多分。



問題はどうやって質点情報をテクスチャに落とし込むかという事なんですが、まずは質点の情報を保持するためだけの計算用テクスチャを用意します。

計算用といってもテクスチャであることに変わりはありませんから、一つの点で保持できるのはRGBAの4つの数値です。
そのままいつものように D3DFMT_R8G8B8A8 とかでテクスチャを作ってしまうと、RGBA を各 0~255 の整数でしか扱えない(1質点につき0~255の整数が4個だけ使える)ことになります。
つまり、次のような構造体で1質点を表すということ。


struct Pixel {
	BYTE r;
	BYTE g;
	BYTE b;
	BYTE a;
};

う、うーん……これではさすがに無理があるので、浮動小数テクスチャ D3DFMT_A32B32G32R32 を使って1点につき4個の実数が使えるようにしましょう。


struct Pixel {
	float r;
	float g;
	float b
	float a;
};

本来ならば r, g, b, a には色の値を入れますが、そんな事は完全に無視して、自分が入れておきたい値を勝手に入れることにします。今回の用途でいえば、r, g, b には位置(高度)、加速、傾きを割り当てることにします。a は使いません。

つまり

struct Pixel {
	float pos; // 高さ
	float accel; // 加速度
	float tangent; // 傾き(x方向と y方向の傾きの平均値を入れるものとする)
	float not_used;
};

と定義するのと同じことです。


さて、このテクスチャに適当な初期値を画像として設定して処理させると水面ができるわけですが、ここで問題になるのは、テクスチャはただの四角系だということです。水面はもっと複雑な形をして欲しい。

というわけで、もう一枚同じサイズのテクスチャを用意して「形状定義用」として使うことにします。マスクと呼んでもよいかもしれません。

このマスクテクスチャは、質点がどのぐらいの範囲で動けるか、という範囲情報を 0.0~1.0 の値で持ちます。0.0 だと、そこの質点は全く運動を許されていない、1.0だと最大の上下運動ができるという具合です。

これが何の意味を持つのかということですけど、この 0.0 を地面、1.0 を水面とみなすと、水面の形を定義するのにちょうどよいんです。
これで水面の形に依存した波をシミュレートすることができます。
(水面の形に依存した波…というのは具体的には、水面の端で波が反射したり、
狭い通路を波が通過した場合はその出口を中心として再び波が広がるといった、まさに波の性質で習ったような動きです)

結果、こんなシェーダーになりました(抜粋)。





uniform float2 TextureSize; // テクスチャサイズ
uniform texture CalcTexture; // 前回の計算結果テクスチャ
uniform texture MaskTexture; // 波が活動できる範囲。ピクセルごとの質点の上下移動可能範囲を明度で表したもの。0(=黒) だとそこの質点は一切上下運動しない
uniform int Pull = 0; // 引っ張る?(波の発生)
uniform float2 PullPos = {0, 0}; // どこを引っ張る?(uv)
uniform float TimeDelta = 0.2;
uniform float WaveSpeedK = 0.2; // 上下差から速度への変換係数
uniform float WaterRestoreK = 0.01; // 変位 0 に戻ろうとする力(発散防止のため)
uniform float MaxSpeed = 2.0; // 上下運動の速度制限
uniform float MaxPos = 20.0; // 上下位置制限

sampler calc_sampler = sampler_state { // 計算結果テクスチャ
	Texture = <CalcTexture>;
	MinFilter = Linear;
	MagFilter = Linear;
	MipFilter = Linear;
	AddressU  = Clamp;
	AddressV  = Clamp;
};
sampler mask_sampler = sampler_state { // 形状テクスチャ
	Texture = <MaskTexture>;
	MinFilter = Linear;
	MagFilter = Linear;
	MipFilter = Linear;
	AddressU  = Clamp;
	AddressV  = Clamp;
};

// 質点
struct Dot {
	float pos; // 質点の高さ
	float vel; // 質点の速度
	float tan; // 質点における傾き
};
Dot dot_decode(float4 v) { // RGBを質点情報に変換
	Dot dot;
	dot.pos = v.r;
	dot.vel = v.g;
	dot.tan = v.b;
	return dot;
}
float4 dot_encode(Dot dot) { // 質点情報をRGBAに変換
	return float4(
		dot.pos, 
		dot.vel,
		dot.tan,
		1.0
	);
}
float4 ps(float2 uv: TEXCOORD): COLOR {
	Dot dot0 = dot_decode(tex2D(calc_sampler, uv));

	// 質点の速度(高さ方向)を更新
	{
		float spanU = 1.0 / TextureSize.x; // 左右隣の点との距離(UV単位)
		float spanV = 1.0 / TextureSize.y; // 上下隣の点との距離(UV単位)
		Dot dotL = dot_decode(tex2D(calc_sampler, float2(uv.x-spanU, uv.y))); // 左の点
		Dot dotR = dot_decode(tex2D(calc_sampler, float2(uv.x+spanU, uv.y))); // 右の点
		Dot dotU = dot_decode(tex2D(calc_sampler, float2(uv.x, uv.y-spanV))); // 上の点
		Dot dotD = dot_decode(tex2D(calc_sampler, float2(uv.x, uv.y+spanV))); // 下の点

		// 隣接点との高低差に応じて速度を決定
		dot0.vel += (dotL.pos - dot0.pos) * WaveSpeedK;
		dot0.vel += (dotR.pos - dot0.pos) * WaveSpeedK;
		dot0.vel += (dotU.pos - dot0.pos) * WaveSpeedK;
		dot0.vel += (dotD.pos - dot0.pos) * WaveSpeedK;
		dot0.vel = clamp(dot0.vel, -MaxSpeed, MaxSpeed); // 速度リミットを適用

		// 質点の傾き平均
		float tanX = dotR.pos - dotL.pos;
		float tanY = dotD.pos - dotU.pos;
		dot0.tan = (tanX + tanY) * 0.5;
	}

	// 質点の高度を更新
	{
		dot0.pos += dot0.vel * TimeDelta; // とりあえず現在の速度をそのまま適用する
		dot0.pos += (0.0 - dot0.pos) * WaterRestoreK; // 発散防止のため、常に 0 に戻るよう補正しておく

		float maxpos = MaxPos * tex2D(mask_sampler, uv).r;
		dot0.pos = clamp(dot0.pos, -maxpos, maxpos); // 高度制限を適用
	}

	// Pull に 1 が指定された場合は PullPos で指定された場所をぐいっと盛り上げる(波を発生させる)
	if (Pull) {
		float2 delta = PullPos - uv;
		if (length(delta) < 0.01) {
			dot0.pos = MaxPos;
		}
	}

	
	float4 color = dot_encode(dot0);
	color.a = tex2D(mask_sampler, uv).r; // 形状マスクの値をアルファに入れておく(後で水面を描画するときに役に立つ)
	return color;
}

プログラム側で準備するのは3枚のテクスチャーで、サイズはすべて同じにしておきます。

・液体範囲設定用のマスクテクスチャー (A)
グレースケールの不透明テクスチャで、水面として使いたい範囲を白く塗ったもの。
水面以外は真っ黒。面倒な場合は全面白のテクスチャーでもOK

・計算用のレンダーテクスチャー(浮動小数フォーマット)二枚 (B, C)
・計算用のシェーダー(上で説明したヤツ)


シェーダーをセットして MaskTextuxre に A を、CalcTexture に B を、レンダーターゲットとして C を設定します。
この時、波を発生させたい場所を PullPos にセットして Pull=1 にすると、その場所がボコっと盛り上がります。
これで描画すると、テクスチャ C には B から 1 tick だけ時間を進めたものが入っているので、これを適当に画面に描画します。

最後に std::swap(B, C) でBCを入れ替えることで、テクスチャの入力と出力を入れ替えます。
あとはこれを繰り返し実行していけば、いい感じで波が広がります。


https://twitter.com/helio_dor/status/1360194732270395394
これは描画方法だけ変えたものを3個横に並べています。

左は、計算結果の値をそのままRGBに対応させて描画。
なので、0.0 と 1.0 の中間である 0.5 が水面の高さという扱いになっています。
水面が高くなれば 1.0 に近づき、波が全くない場所では 0.5 に、水面が低くなったら 0.0 という具合です。
そのため、初期状態では R=0.5 G=0.5 B=0.5 の灰色になっています。


中央は、計算結果の傾き情報を利用して背景をゆがませたもの。
右は、計算結果の高さと傾きを利用して陰影をつけたものと、範囲設定用のマスクテクスチャーを合成したものです。
動画だとちょっと見づらいのですが、ちゃんと水際で反射したり、狭い通路に波が入り込んだりしているのがわかるでしょうか?


これの良いところは、マスクテクスチャにいくらでも好きな形の水面を書いておくことができるということです。
それはつまり、なにもあらかじめマスク画像(水面の形状画像)を用意しておかなくても、マスクテクスチャにリアルタイムで値を書き込んでいけば、その場でどんどん形を変えられるということですよ。


さあ、ここまでできればあとはやりたい放題ですよ!
たとえばこんな感じ。


詳細は次回書きますが、キャラクターの場所にどんどん生成されている灰色部分がマスクテクスチャに書き込まれた絵です。
(これは単に、レンダーターゲットとして指定したマスクテクスチャに対して、あらかじめ用意しておいた飛沫画像を追加描画しているだけです)

そして灰色領域のなかで、虹色の波が広がっていますね。
これをちゃんとした色で描画すれば、いい感じに波紋が広がる白濁液に見えるはず……!!




そしてまた つづく










おまけ

なんか新しいWindowsのISO(Windows11?)が流出したとかナントカ。

少し前にWindows10のサポートを2024年で打ち切るという発表があったので、やっぱりそうなのかとも思いましたが、今のところは真偽不明。一応ね。

個人的に一番気になったのは問題のISO、インストールにはTPM 2.0が必須……つまりIvy Bridge(第3世代)以前のPCは対応していないんだとか。


やってくれた喃……Microsoft!





Sandy Bridge(第2世代)は切り捨てか……。
やっぱりそろそろ新しいPC組むかなぁ。


この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

記事のタグから探す

月別アーカイブ

限定特典から探す

記事を検索