概要
プログラマーであれば、自動でいい感じにしてくれるカッコいいプログラムを書きたいと思うし、創作をしている人であればプログラムでモノを作りたいと思います。
マップが1つしかないゲームより、無限に生成されるローグライクのダンジョンにあこがれると思いますし、キャラクターのモーションなんかもある程度自動化したい。マインクラフトのように無数の世界を自動生成できたらかっこいい。
そういった何かを生成したいという思いの原点と、ノイズの関係性を結びつける記事です。
ホワイトノイズ
例えば テクスチャを自動で生成したい とします。
まずシンプルなアイディアとして、**ランダム関数を使って色をつけたらどうか?**という事です。
・RGBのそれぞれの要素をランダム関数で決める。
・用意されたカラーテーブルの中からランダム関数の結果を使って着色する。
・各ピクセルの濃さをランダム関数の結果を使って決める。
などなど…
ランダム関数をちゃんと理解してないのにランダム関数を使う事に抵抗感がある場合、XORシフトのアルゴリズムを調べるといいと思います。
OpenAL道場 第10回「疑似乱数」( https://youtu.be/wxsPmz5DxBk )
int _textureSize = 256;
Texture2D noise = new Texture2D(_textureSize, _textureSize);
for (int y = 0; y < _textureSize; y++)
{
for (int x = 0; x < _textureSize; x++)
{
float value = Random.Range(0, 1.0f);
Color color = new Color(value, value, value, 1.0f);
noise.SetPixel(x, y, color);
}
}
では、実際に上記のようなプログラムを書いてみます。
前後についてるfor文はテクスチャの解像度(256×256)分、ピクセルの色をランダムで決めるという繰り返してるだけで、大事なのはここの2行です。
float value = Random.Range(0, 1.0f);
Color color = new Color(value, value, value, 1.0f);
valueをランダムで決めて、その値を使ってcolorを作っている。
シンプルで分かりやすいです。その結果は下記のようになります。
毎回実行する度に異なるテクスチャを作成できます。
とりあえず、自動でテクスチャを作ってみるという事には成功した。ウレシイ!
しかし、これはエフェクトの一部とか、地面のゴツゴツ感を足す為に薄~く合成するための素材など…ランダムをそのまま視覚化しただけあって、完全に無秩序なものを表現するのにしか使えそうにありません。
横に引き延ばしたり、ランダムの領域を変えたりすることで多少は傾向のようなものを操作できそうですが、あまりにも無秩序であるわけです。
このように考えると、潜在的に欲しいモノが分かってきます。
ランダムであるが、なめらかに連続している結果がほしいという事です。
バリューノイズ
「ランダムであるが、なめらかに連続している」を形にするにはどうすればよいか?
ランダムという事と、なめらかに連続しているを素直に書くしかありません。
まず X = 0~1 Y = 0~1 の領域があるとします。
X=0 の時の Y の値v0 と X=1 の時の Y の値v1 をそれぞれRandomで決めます。
その後、X=0~1までの空間については、v0とv1を補完して求めます。
そうすると、**毎回 v0 v1 の値はランダムで決まるので異なる結果が得られるのにも関わらず、なめらかな連続性のある値が得られます。**やった!
例えるなら、スタート地点とゴール地点はランダムで決めて、その道中をちゃんと歩く?みたいな事です。
きっと、実行する度に色々な旅になるけど、その間の想い出はちゃんと連続性のあるストーリーとして残る…みたいな、そんな感じです。
では、これをテクスチャに拡張してみます。
テクスチャの領域は X = 0~1 Y = 0~1 の領域なので、それぞれの四隅になる
v00 = (0,0) , v01 = (0,1) , v10 = (1,0) , v11 = (1,1) を4つ決めます。
それぞれの点について、高さをランダムで決めるとします。
(高さと解釈してもいいし、点のもつ色の濃さと解釈してもいいし、とにかく4つの点についてランダムな要素を1つ設けます)
あとは、このランダム4を入力にして、テクスチャの色を決めてみようと思います。
前述したホワイトノイズが全てのピクセルに対してランダムだったのに対して、今回はランダムな値を4つしか使いません。
一見複雑ですが、最初にやった**「ランダムな2点を決めて、その間を補完する」**という事を縦横方向にやってるだけです。
複雑だとおもったら、次元を落とすとイメージしやすくなります。
実際のプログラムは下記の通りになります。
int _textureSize = 256;
Texture2D noise = new Texture2D(_textureSize, _textureSize);
//四隅の頂点について、ランダムで値を決める
float v00 = Random.Range(0, 1.0f);
float v01 = Random.Range(0, 1.0f);
float v10 = Random.Range(0, 1.0f);
float v11 = Random.Range(0, 1.0f);
for (int y = 0; y < _textureSize; y++)
{
//pixel(x,y) の y に対応したRandomを補完した値を求める
float v0001 = Mathf.Lerp(v00, v01, (float)y / (float)_textureSize);
float v1011 = Mathf.Lerp(v10, v11, (float)y / (float)_textureSize);
for (int x = 0; x < _textureSize; x++)
{
//pixel(x,y) の x に対応した値を v0001 と v1011 の値を補完して求める
float value = Mathf.Lerp(v1011, v0001, (float)x / (float)_textureSize);
Color color = new Color(value, value, value, 1.0f);
noise.SetPixel(x, y, color);
}
}
1.四隅の頂点について、ランダムな要素を4つ宣言。
2.V0001 と v1011 を宣言している所で、pixelのYに対応している補完した値を求めます。
3.float valueを宣言している所で、pixelのXに対応している補完した値を…2の結果を使って求めます。
4.縦×横全てのピクセルについて繰り返すと…
これだよこれこれ(バリューノイズと呼ばれています)
ホワイトノイズだと扱いがピーキーだったのに対して、バリューノイズは勝手がよさそうです。
例えば、今回は得られた結果を色の濃さとして使いましたが、この値は地形の高さにつかってもいいわけです。
マインクラフトのアルゴリズムはより複雑ですが、簡易的であればこのアルゴリズムで連続性のある地形はランダムで作れそうです。また、雑な実装でいいのであれば、水面の高さとして使えば簡易的な水面のゆらぎにも使えそうです。
カメラの手ブレ…エフェクト…なんにでも使えそう…
実際には、さらに拡張した パーリンノイズ というやつが使われます。
また、パーリンノイズはUnityのMathfクラスにも存在します。( Mathf.PerlinNoise )
バリューノイズでは線形性がはっきりしてるので、パーリンノイズはより有機的にしたアルゴリズムです。
複雑そうですが、要するにやりたい事は同じで…パーリンノイズであっても**「代表的な値をランダムで決めて、その間を補完する」**という事です。
さらに…パーリンノイズを重ね合わせて fBmノイズ という物が作れます。
これはPhotoshopでおなじみ**「雲模様」**を作る事のできるノイズです。
あれは誰かが手動で雲っぽいテクスチャを用意したのではなく、プログラムによって描かれたものだったんですね。
まとめ
ここまでくると、ノイズの脅威というか…
マインクラフトの地形を作ろうとするとパーリンノイズが出てきて、雲を作ろうとするとfbmノイズがで見えてきます。
つまり、自然物はある程度、定式化されていて、必ずしもわざわざ人の手で1つ1つ作る必要がないという事も見えてきます。(もちろん、作りたいモノと演出によります)
**ノイズを表現する式は、自然を表現する式だった。**という事です。
実際に、**パーリンノイズの初出はいつだったのか・・・?**というと
映画トロン のために作られた技術であったりします。
当時、メモリの容量が少なかった為、動的にプログラムでテクスチャを作りたいという欲求の中からうまれた技術なわけです。
パーリンノイズを開発したケン・パーリンさんは1997年、映画芸術科学アカデミーからアカデミー科学技術賞を受賞しています。( パーリンノイズ )
今となってはCGの世界で炎や煙を表現する定番の手法ですが、意外と最近うまれてるのがわかります。
ノイズには「何かを表現したいという気持ち」がつまってる!
サンプルプログラム
Unityでバリューノイズを生成するサンプルプログラムです。
説明するのに余計な行が多すぎるので、最後にもってきました。
改造したり、再配布したり…自由につかってください。(いざ書くと細かい部分が面倒なので)
適当なオブジェクトに下記のスクリプトを貼り付けて実行すると、Gameタブでノイズが作れます。
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using System.IO;
using System;
using Random = UnityEngine.Random;
public class NoiseGenerator : MonoBehaviour
{
//ファイル名
[SerializeField]
private string _fileName = "Noise";
[SerializeField]
private int _textureSize = 128;
[SerializeField, Tooltip("Trueならタイムスタンプが付きます")]
private bool _useTimeStamp = false;
//保存先
private string _filePath = "Assets/Noise/";
//UIの表示管理
private bool _uiMessageActiv = false;
private void CreateTexture(string path, string name)
{
//Pathが無効の場合作成する
AccessTexturePath(path);
//タイムスタンプ
string time = DateTime.Now.ToString("yyyyMMddHHmmss");
string fullpath;
//判定
if (_useTimeStamp)
{
fullpath = path + name + time + ".png";
}
else
{
fullpath = path + name + ".png";
}
//テクスチャ作成
Texture2D noise = new Texture2D(_textureSize, _textureSize);
float v00 = Random.Range(0, 1.0f);
float v01 = Random.Range(0, 1.0f);
float v10 = Random.Range(0, 1.0f);
float v11 = Random.Range(0, 1.0f);
for (int y = 0; y < _textureSize; y++)
{
float v0001 = Mathf.Lerp(v00, v01, (float)y / (float)_textureSize);
float v1011 = Mathf.Lerp(v10, v11, (float)y / (float)_textureSize);
for (int x = 0; x < _textureSize; x++)
{
float value = Mathf.Lerp(v1011, v0001, (float)x / (float)_textureSize);
Color color = new Color(value, value, value, 1.0f);
noise.SetPixel(x, y, color);
}
}
//書き出し処理
noise.Apply();
var bytes = noise.EncodeToPNG();
File.WriteAllBytes(fullpath, bytes);
Debug.Log("Create : " + fullpath);
AssetDatabase.Refresh();
StartCoroutine(DisplayUiMessage());
}
//パスが無ければ作成
private void AccessTexturePath(string path)
{
if (Directory.Exists(path)) { return; }
if (Directory.Exists(path) == false)
{
Directory.CreateDirectory(path);
Debug.Log("CreatePath : " + path);
}
}
//メッセージの表示管理
private IEnumerator DisplayUiMessage()
{
if (_uiMessageActiv) { yield break; }
_uiMessageActiv = true;
yield return new WaitForSeconds(3.0f);
_uiMessageActiv = false;
}
//UI
void OnGUI()
{
GUI.color = Color.black;
GUI.Label(new Rect(Screen.width - 130, 50, 100, 20), "タイムスタンプ有効");
if (_uiMessageActiv)
{
GUI.Label(new Rect(Screen.width - 150, 70, 100, 20), "テクスチャ生成完了");
}
GUI.color = Color.white;
_useTimeStamp = GUI.Toggle(new Rect(Screen.width - 150, 50, 100, 20), _useTimeStamp, "");
if (GUI.Button(new Rect(Screen.width - 150, 10, 140, 30), "テクスチャ生成"))
{
MenuCreateTexture();
}
}
[ContextMenu("テクスチャ生成")]
public void MenuCreateTexture()
{
CreateTexture(_filePath, _fileName);
}
}
参考
The Book of Shaders
Shaderとして実装したい場合
パーリンノイズを理解する
ケン・パーリンさんの翻訳
OpenAL道場 第10回「疑似乱数」
ノイズの大本となるランダム関数について
XORシフトは一時期Googleとかも使ってたらしい?
調べると、色々な方法があるので沼にハマる。また乱数自体に周期性があると、ノイズにも周期性が反映されてします。