2Dや3D描画に関する情報をネットで調べていたりすると、
ときおり副次的に見かけたりする「パーリンノイズ」という単語。
パーリンノイズって何ぞ?
その昔になんとなく気になって調べてみた感じでは、
「自然環境っぽい表現が可能なランダムな分布」という、
ざっくりとした理解でしたが、実装を試したことは一度もありませんでした。
Wikipediaのこちらのページでは、
雲のような自然な感じのモヤモヤなテクスチャをその実際例の一つとして挙げていますね。
例えば↓こんな感じ。いかにも自然にありそうな地形の起伏とかにも使えるようです。
さて、
なんとなく自分でも確認したくなったので、試してみることにした次第です。
Wikipediaのページのリンクをたどると、Javaのソースコードや解説のページが紹介されているので、
参照してみたのですがイマイチぴんと来ない感じ?
良さげな解説ページを見つけた
そこで、
さらにネットで調べてみると、こちらのページを見つけました。
詳しく解説されていて良いですね。何より日本語なのが有り難い(^_^)
あと、こちらも参考になります。
パーリンノイズは1次元、2次元、3次元・・・といくらでも次元を拡張できるようですが、
テクスチャのような平面、つまり2次元の場合が適用範囲が広いと思うので、今回は2次元の場合についてやって行きたいと思います。
2次元でのパーリンノイズというのはざっくり言えば、
0 ≦ X < 1、 0 ≦ Y < 1 という単位座標内の任意の点の値を得るということになります。
なお、解説画像はそのまま引用させていただきました(^_^;)
JavaScriptでやってみる
解説ページにはソースコードが載っているので、JavaScriptに移植してみることにしました。
3次元の場合を扱っているので、自力で2次元に落とし込もうとも思ったのですが、
何か面倒そうな感じもしたので2次元版のやつが無いかなと探してみることに(^_^;)
そしたらこちらを発見!
しかも JavaScript だったので、そのまま行けそうです(^_^;)
さらにソースコードの内容についての解説までありました。
さっそく、
GitHub のここから clone してデモを実行してみたら、それっぽい画像がアッサリと出来ました(^_^)
ソースコードを読むと、単位座標を grid という単位で扱っているようです。
以下のような感じに単位座標を拡張するようになっています。
grid = 1 ➔ 単位座標 1×1 = 1個
grid = 2 ➔ 単位座標 2×2 = 4個
grid = 3 ➔ 単位座標 3×3 = 9個
︙
実際に試してみると違いが良く分かります。
grid = 1 の場合。
見ての通り、
だんだんと模様が複雑になっています。
単位座標の四隅に「勾配ベクトル」っていうのが設定されるのですが、
その数が多ければ必然的に複雑な感じになるわけですね。
コードを作り変えてみた
ところで、
オリジナルのコードでは入力座標に応じて、grid を自動で拡張してたり、パーリンノイズの結果を再利用しやすいように全て保存しておくような作りになっているのですが、そのために少々煩雑なコードになっている感じがしたので個人的に作り変えてみました。
grid の値は予め決めておけば勾配ベクトルも予め設定しておけるし、
計算結果を再利用したければ必要に応じて保存すれば良いので、
コードをもう少しスッキリさせてみました。
疑似乱数を使うと、入力に対応する出力が一意に決まることになるので、勾配ベクトルを予め計算して保存しなくても済むようになるようなのですが、オリジナル通りに一様乱数で求めました。
疑似乱数だとノイズの結果が画一的になりそうだし。
ということで、オリジナルを変更したコードを以下に載せます。
使い方はオリジナルと同様ですが、seed() に grid 値を渡すようになります。
/*!
* PerlinNoise.js ver 1.0.0 (2022-01-11)
* (c) katwat [katwat.s1005.xrea.com]
* based on https://github.com/joeiddon/perlin
* The MIT License
*/
(function(definition) {
if (typeof exports === "object") { // CommonJS
module.exports = definition();
} else if (typeof define === "function" && define.amd) { // AMD (RequireJS)
define(definition);
} else { // <script>
PerlinNoise = definition();
}
}(function() {
var _grid, _gradients;
function randomVector() {
var theta = 2 * Math.PI * Math.random();
return {
x: Math.cos(theta),
y: Math.sin(theta)
};
}
function gradient(x,y,gx,gy) {
var g = _gradients[gx + gy*(_grid+1)];
return (x - gx) * g.x + (y - gy) * g.y;
}
function smoother(x) {
var x3 = x*x*x,
x4 = x3 * x,
x5 = x4 * x;
return 6*x5 - 15*x4 + 10*x3;
}
function interp(x,a,b) {
return a + smoother(x) * (b - a);
}
return {
seed: function(grid) { // grid: require integer
var x,y,d;
_grid = grid || 1; // default is 1
d = _grid + 1;
_gradients = new Array(d*d);
for (y = 0; y < d; y++) {
for (x = 0; x < d; x++) {
_gradients[x + y*d] = randomVector();
}
}
},
get: function(x,y) { // range: 0 <= x < grid, 0 <= y < grid
var xi = Math.floor(x),
yi = Math.floor(y),
tl = gradient(x,y,xi ,yi ),
tr = gradient(x,y,xi+1,yi ),
bl = gradient(x,y,xi ,yi+1),
br = gradient(x,y,xi+1,yi+1);
return interp(y-yi,interp(x-xi,tl,tr),interp(x-xi,bl,br)); // range: -1.0 to 1.0
},
get grid() {
return _grid;
},
get gradients() {
return _gradients;
}
};
}));
上記のコードを使って、例えば↓こんな風に html に書いて実行すると、
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Perlin Noise - Test</title>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="PerlinNoise.js"></script>
<script>
window.onload = function() {
var x,y,v,
grid = 4,
size = 256, // pixel
resolution = 128,
sscale = size/resolution,
gscale = grid/resolution,
c = document.getElementById('canvas');
c.width = c.height = size;
c = c.getContext('2d');
PerlinNoise.seed(grid);
for (y=0;y<resolution;y++) {
for (x=0;x<resolution;x++) {
v = PerlinNoise.get(x*gscale,y*gscale);
c.fillStyle = 'hsl(200,50%,' + (((v+1)/2*50+50)|0) + '%)';
c.fillRect(x * sscale, y * sscale, sscale, sscale);
}
}
};
</script>
</body>
</html>
ところで、
get() の結果は -1.0 ~ 1.0 の範囲ということになっているのですが、
実際にやってみると、だいたい -0.5 ~ 0.5 くらいになるような感じでした。
勾配ベクトルの大きさは1ですが、距離ベクトルの大きさは 0 ~ √2 の範囲となります。
したがって両者の内積値は理論上は -√2 ~ √2 の範囲に収まるはずですが、
単位座標の四隅ごとの値を加重平均すると果たして -1.0 ~ 1.0 の範囲になるのでしょうか?
個人的にはイマイチよく分かりません。
いずれにしろ、勾配ベクトルを一様乱数で求めるやり方では、だいたい -0.5 ~ 0.5 くらいになってしまうようです。
タイプの違う乱数を使うと、また別な結果を得られるのかもしれませんね。
なお、
オリジナルを改変したソースコードのファイルは以下からダウンロードできます。
オクターブ拡張について
ところで、
上記でやってきたやつは「基本」のパーリンノイズだったりします。
Persistence、Amplitude という要素を導入し、
周波数が2倍、4倍・・・音楽でいうところのオクターブに相当するわけですが、
「複数オクターブ重ねたノイズ」としてやると、より自然な感じになるっぽいようです。
先の解説ページで紹介されているこちらを見ると、なかなか興味深い感じですね。
いずれやってみようかな。