2022年7月6日水曜日

いろいろなノイズのアルゴリズム


Minecraftとかマップを自動生成するような時に、パーリンノイズというものが使えるというのを読んだことがあり、いつか用意しようと思っていた。

ノイズというもを調べてみるといろいろあるみたい。

ホワイトノイズ


ホワイトノイズ

これは単純にランダムの値をそのままドットに置き換えただけ。

バリューノイズ

ここから先のノイズには乱数の取得に仕組みが必要になる。
あるx,y座標についての値を乱数で決めたとき、再び同じx,y座標で同じ値を取得できるようになっていないといけない。
通常乱数は取得を繰り返すと次々に違う値が得られる。
ノイズを生成するときは、毎回Seedを指定するようなイメージで、指定した引数に対する値は毎回同じものが返る必要がある。

・テーブルを準備する方法
単純な仕組みは引数に渡す範囲の配列を用意して、固定値を設定しておいたり初期化時に乱数値を代入して順にする方法。
メリットは参照時に計算無しで値が得られる。
デメリットはサイズ分のメモリが必要なのと、その値の準備が必要。ソース上に固定値で準備する場合はサイズ変更が容易ではない。

・疑似乱数で計算する方法
Xorshiftのような疑似乱数で計算する方法。
メリットは準備不要。テーブル用のメモリも不要。
デメリットは参照のたびに計算が必要なので、テーブル参照よりは遅くなる。適切なSeedを指定しないと、乱数の結果に偏りがでる。


同じ地点で同じ値が得られるようになったら、各座標の4点で値を取得してそれぞれの点を補間するとこんな感じになる。

バリューノイズ1マス分

これをつなげていくとこんな感じになる。

バリューノイズ

ネットで調べていると、バリューノイズをこの後出てくるパーリンノイズと勘違いしているものがいくつかあった。

パーリンノイズ

パーリンノイズはバリューノイズの座標の4点の乱数値ではなく、その乱数値を利用して勾配ベクトルを取得して、各頂点から入力のx,y座標の距離ベクトルとの内積で求めるらしいが、この勾配ベクトルの選択が乱数の内容によって偏ってしまいいい感じにならない場合が多かった。
パーリンノイズ失敗例

単純にmod4で分けると計算で疑似乱数で生成した値だと下2ビットが偏っててうまく分かれないと予想して、もう少し大きい数字の範囲で4分割する感じにしてみた。
パーリンノイズ


シンプレックスノイズ

シンプレックスノイズはパーリンノイズを改良して計算量を減らしたものらしい。
パーリンノイズは四角で考えるが、シンプレックスノイズは3角形で考えて、1点分少なく計算できる。
ただ、高次元の場合に効果を発揮するらしく、2次元の場合はむしろ重い気がする。

シンプレックスノイズ

セルラーノイズ

セルラーノイズは細胞みたいなノイズ。格子内を9分割して一番近い距離を値にする。
セルラーノイズ

セルラーノイズ反転版

反転したのはキモい。

ボロノイズ

ボロノイ図というものがあるらしく、セルラーノイズを利用して作れる。距離の最小になった点の座標自体を値にする。
ボロノイズ
岩のテクスチャとかに使えそう。

非整数ブラウン運動(フラクタルノイズ)

大小の周波数を重ね合わせてフラクタル化することで、ノイズの粒度を細かくすることができる。この手法は「フラクタルブラウン運動」(fBM)または単に「フラクタルノイズ」と呼ばれる。
バリューノイズ&fBM

パーリンノイズ&fBM

シンプレックスノイズ&fBM

ノイズ画像加工


バリューノイズに色付け
バリューノイズ&fBMで得られた値を見て、半分よりも上なら緑、下なら青にするようにするとこんな画像になる。
auto fSeaCB = []( tF4 n ) -> tU4 {
	tU1 c = tU1( n * 255.0f ) ;
	if( c > 127 ) {
		return 0xff << 8*3 | RGB( 0, c, 0 ) ;
	}
	return 0xff << 8*3 | RGB( 0, 0, c*2 ) ;
} ;


パーリンノイズの木目
パーリンノイズで得られた値に適当な数値を掛けて、小数部だけを使うとこんな画像になる。

auto fWoodCB = []( tF4 n ) -> tU4 {
	n *= 20.0f ;
	n = n - tU4( n ) ;
	tU1 c = tU1( n * 255.0f ) ;
	return 0xff << 8*3 | RGB( c, c, c ) ;
} ;

ドメインワーピング

これまではCPU側でノイズ関数を使って画像を作って、それをテクスチャとして表示していた。
これを、GPU側のピクセルシェーダでノイズ関数を動かして出力するようにすれば、リアルタイムに動かすことができるようになる。

パーリンノイズとfBMのシェーダ
float2 rand( float2 st )
{
	float2 s = float2( dot( st, float2( 127.1, 311.7 )) + _Seed, dot( st, float2( 269.5, 183.3 )) + _Seed ) ;
	return -1 + 2 * frac( sin( s ) * 43758.5453123 ) ;
}
float Noise( float2 st )
{
	float2 p = floor( st ) ;
	float2 f = frac( st ) ;
 
	float w00 = dot( rand( p                 ), f                 ) ;
	float w10 = dot( rand( p + float2( 1, 0 )), f - float2( 1, 0 )) ;
	float w01 = dot( rand( p + float2( 0, 1 )), f - float2( 0, 1 )) ;
	float w11 = dot( rand( p + float2( 1, 1 )), f - float2( 1, 1 )) ;
				
	float2 u = f * f * f * ( f * ( f * 6 - 15 ) + 10 ) ;
 
	return lerp( lerp( w00, w10, u.x ), lerp( w01, w11, u.x ), u.y ) * 0.5 + 0.5 ;
}
float fBM( float2 st )
{
	float v = 0.0 ;
	float a = 0.5 ;
	for( int i = 0 ; i < _Octave ; i++ ) {
		v += a * Noise( st ) ;
		st *= 2.0 ;
		a *= 0.5 ;
	}
	return v ;
}
_Seedと_OctaveがConstantBufferで指定できる定義。
_Seedは乱数Seed。
_Octaveは周波数を重ね合わせる回数。4回か5回


ドメインワーピングのシェーダ
float4	DomainWarping( float2 st )
{
	float time = _Time / 60.0 * _Speed ;
	st *= _Scale2 ;

	float2 q ;
	q.x = fBM( st ) ;
	q.y = fBM( st + float2( 1.0, 1.0 )) ;
	float2 r ;
	r.x = fBM( st + _Scale1 * q + float2( 1.7, 9.2 ) + 0.15 * time ) ;
	r.y = fBM( st + _Scale1 * q + float2( 8.3, 2.8 ) + 0.126 * time ) ;
	float f = fBM( st + _Scale1 * r ) ;
	float3 color ;
	color = lerp( _Color1.rgb, _Color2.rgb, saturate( f * f * 4.0 )) ;
	color = lerp( color, _Color3.rgb, saturate( length( q ))) ;
	color = lerp( color, _Color4.rgb, saturate( length( r.x ))) ;
	return float4(( f*f*f+0.6*f*f+0.5*f ) * color, 1.0 ) ;
} 
_Time、_Speed、_Scale1、_Scale2、_Color1~4がConstantBufferで指定できる定義。
_Timeは60fpsで1フレーム毎に1ずつ増加する値。
_Speedは変化のスピード調整用。1.0fを指定。
_Scale1、_Scale2はそれぞれスケール調整用。1.0fを指定。
_Color1~4は色の指定。
デモの指定色。
_Color1 = { 0.101961f, 0.619608f, 0.666667f, 1.0f } ;
_Color2 = { 0.666667f, 0.666667f, 0.498039f, 1.0f } ;
_Color3 = { 0.0f, 0.0f, 0.164706f, 1.0f } ;
_Color4 = { 0.666667f, 1.0f, 1.0f, 1.0f } ;


シェーダでパーリンノイズ、fBMを計算して、ドメインワーピングしたのがこれ。
パーリンノイズ版ドメインワーピング


シンプレックスノイズと歪み追加版fBMのシェーダ
float3 mod289( float3 x ) { x += _Seed ; return x - floor( x * ( 1.0 / 289.0 )) * 289.0 ; }
float2 mod289( float2 x ) { x += _Seed ; return x - floor( x * ( 1.0 / 289.0 )) * 289.0 ; }
float3 permute( float3 x ) { return mod289((( x * 34.0 ) + 1.0 ) * x ) ; }
float Noise( float2 v ) {
	const float4 C = float4( 0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439 ) ;

	float2 i  = floor( v + dot( v, C.yy )) ;
	float2 x0 = v - i + dot( i, C.xx ) ;

	float2 i1 = float2( 0.0, 0.0 ) ;
	i1 = ( x0.x > x0.y ) ? float2( 1.0, 0.0 ) : float2( 0.0, 1.0 ) ;
	float2 x1 = x0.xy + C.xx - i1 ;
	float2 x2 = x0.xy + C.zz ;

	i = mod289( i ) ;
	float3 p = permute( permute( i.y + float3( 0.0, i1.y, 1.0 )) + i.x + float3( 0.0, i1.x, 1.0 )) ;
	float3 m = max( 0.5 - float3( dot( x0, x0 ), dot( x1, x1 ), dot( x2, x2 )), 0.0 ) ;

	m = m*m ;
	m = m*m ;

	float3 x = 2.0 * frac( p * C.www ) - 1.0 ;
	float3 h = abs( x ) - 0.5 ;
	float3 ox = floor( x + 0.5 ) ;
	float3 a0 = x - ox ;

	m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h ) ;

	float3 g ;
	g.x  = a0.x  * x0.x  + h.x  * x0.y ;
	g.yz = a0.yz * float2( x1.x, x2.x ) + h.yz * float2( x1.y, x2.y ) ;
	return (( 130.0 * dot( m, g )) + 1.0 ) * 0.5 ;
}

float fBM( float2 st )
{
	float v = 0.0 ;
	float a = 0.5 ;
	float2 shift = float2( 50.0, 50.0 ) ;
	float2x2 rot = { cos(0.5), sin(0.5), -sin(0.5), cos(0.5)} ;
	for( int i = 0 ; i < _Octave ; i++ ) {
		v += a * Noise( st ) ;
		st = mul( st * 2.0, rot ) + shift ;
		a *= 0.5 ;
	}
	return v ;
}
_Seedと_OctaveがConstantBufferで指定できる定義。

シンプレックスノイズ、歪みを追加したfBMをつかってドメインワーピングしたのがこれ。
シンプレックスノイズ版ドメインワーピング

テクスチャ参照版ドメインワーピング

シェーダでノイズ画像を作ってドメインワーピングしてると、タスクマネージャのGPU使用率が30%~40%ぐらいになっていた。
グラフィックボードは高くて買えないので、Intelの内蔵GPUが頑張ってる。
Intelのせいなのか、AMDやNVIDIAでも同じようなものなのかはわからないがGPUがすごく頑張ってる。
テクスチャの画像を元に、ドメインワーピングしたらドメインワーピング内で5回fBMを呼び出している部分がテクスチャ画像参照5回に置き換えられる。

CPU側で用意したシンプレックスノイズ&fBM画像をテクスチャに指定して、ピクセルシェーダで、そのテクスチャを参照するように置き換えたバージョンがこれ。


テクスチャ参照版ドメインワーピングのシェーダ
float4	DomainWarping( float2 st )
{
	float time = _Time / 60.0 * 0.05f * _Speed ;
	st *= _Scale2 * 0.03f ;

	float2 q ;
	q.x = Texture0.Sample( Sampler0, st ).r ;
	q.y = Texture0.Sample( Sampler0, st + float2( 1.0, 1.0 )).r ;

	float s = _Scale1 * 0.02 ;
	float2 r ;
	r.x = Texture0.Sample( Sampler0, st + s * q + float2( 1.7, 9.2 ) + 0.15 * time ).r ;
	r.y = Texture0.Sample( Sampler0, st + s * q + float2( 8.3, 2.8 ) + 0.126 * time ).r ;

	float f = Texture0.Sample( Sampler0, st + s * r ).r ;

	float3 color ;
	color = lerp( _Color1.rgb, _Color2.rgb, saturate( f * f * 4.0 )) ;
	color = lerp( color, _Color3.rgb, saturate( length( q ))) ;
	color = lerp( color, _Color4.rgb, saturate( length( r.x ))) ;

	return float4(( f*f*f+0.6*f*f+0.5*f ) * color, 1.0 ) ;
}
_Time、_Speed、_Scale1、_Scale2、_Color1~4がConstantBufferで指定できる定義。
fBMを呼び出しているところをテクスチャ参照に置き換えただけ。

テクスチャ参照版ドメインワーピング

GPU使用率は8%ぐらいに下がっていた。

ライブラリの準備ができたけど、これらをどう使っていくのかがまだ良くわからない。
マップの自動生成とかは予想がつくが、水とか火とかの表現につかったりできるんだろうなぁ。

参考記事

このサイト超すごい。
プログラムのソースを触れてリアルタイムで結果が反映される。

0 件のコメント:

コメントを投稿